@@ -16,7 +16,10 @@ use crate::{
1616 canister:: { BuildSteps , SyncSteps } ,
1717 recipe:: { Recipe , RecipeType } ,
1818 } ,
19- package:: { PackageCache , cache_recipe, read_cached_recipe} ,
19+ package:: {
20+ PackageCache , cache_registry_recipe, cache_uri_recipe, read_cached_registry_recipe,
21+ read_cached_uri_recipe,
22+ } ,
2023 prelude:: * ,
2124} ;
2225
@@ -78,12 +81,6 @@ pub enum HandlebarsError {
7881
7982 #[ snafu( display( "failed to acquire lock on package cache" ) ) ]
8083 LockCache { source : crate :: fs:: lock:: LockError } ,
81-
82- #[ snafu( display( "failed to resolve git tag '{tag}' for recipe" ) ) ]
83- ResolveGitTag { source : reqwest:: Error , tag : String } ,
84-
85- #[ snafu( display( "failed to parse git tag response for '{tag}'" ) ) ]
86- ParseGitTag { tag : String } ,
8784}
8885
8986impl Handlebars {
@@ -92,7 +89,7 @@ impl Handlebars {
9289 recipe : & Recipe ,
9390 ) -> Result < ( BuildSteps , SyncSteps ) , HandlebarsError > {
9491 // Determine the template source
95- let tmpl = match & recipe. recipe_type {
92+ let tmpl_source = match & recipe. recipe_type {
9693 RecipeType :: File ( path) => TemplateSource :: LocalPath ( Path :: new ( & path) . into ( ) ) ,
9794 RecipeType :: Url ( url) => TemplateSource :: RemoteUrl ( url. to_owned ( ) ) ,
9895 RecipeType :: Registry {
@@ -103,18 +100,30 @@ impl Handlebars {
103100 } ;
104101
105102 // Retrieve the template, using cache for remote/registry sources
106- let tmpl = match tmpl {
103+ let ( tmpl, should_cache ) = match & tmpl_source {
107104 TemplateSource :: LocalPath ( path) => {
108- let bytes = read ( & path) . context ( ReadFileSnafu ) ?;
109- if let Some ( expected) = & recipe. sha256 {
110- verify_checksum ( & bytes, expected) ?;
111- }
112- parse_bytes_to_string ( bytes) ?
105+ let bytes = read ( path) . context ( ReadFileSnafu ) ?;
106+ ( parse_bytes_to_string ( bytes) ?, false )
113107 }
114108
115109 TemplateSource :: RemoteUrl ( u) => {
116- self . fetch_remote_template ( & u, recipe. sha256 . as_deref ( ) )
117- . await ?
110+ // Check cache
111+ let maybe_cached = self
112+ . pkg_cache
113+ . with_read ( async |r| {
114+ read_cached_uri_recipe ( r, u, recipe. sha256 . as_deref ( ) )
115+ . context ( ReadCacheSnafu )
116+ } )
117+ . await
118+ . context ( LockCacheSnafu ) ?;
119+ if let Some ( cached) = maybe_cached? {
120+ debug ! ( "Using cached recipe template for {u}" ) ;
121+ ( parse_bytes_to_string ( cached) ?, false )
122+ } else {
123+ // Download the template
124+ let tmpl = self . fetch_remote_bytes ( u) . await ?;
125+ ( parse_bytes_to_string ( tmpl) ?, true )
126+ }
118127 }
119128
120129 // TMP(or.ricon): Temporarily hardcode a dfinity registry
@@ -130,43 +139,31 @@ impl Handlebars {
130139 let maybe_cached = self
131140 . pkg_cache
132141 . with_read ( async |r| {
133- read_cached_recipe ( r, & package, & version) . context ( ReadCacheSnafu )
142+ read_cached_registry_recipe ( r, & package, version) . context ( ReadCacheSnafu )
134143 } )
135144 . await
136145 . context ( LockCacheSnafu ) ?;
137146 if let Some ( cached) = maybe_cached? {
138147 debug ! ( "Using cached recipe template for {package}@{version}" ) ;
139- parse_bytes_to_string ( cached) ?
148+ ( parse_bytes_to_string ( cached) ?, false )
140149 } else {
141150 // Download the template
142151 let url = format ! (
143152 "https://github.com/dfinity/icp-cli-recipes/releases/download/{release_tag}/recipe.hbs"
144153 ) ;
145154 let bytes = self . fetch_remote_bytes ( & url) . await ?;
146155
147- if let Some ( expected) = & recipe. sha256 {
148- verify_checksum ( & bytes, expected) ?;
149- }
150-
151- // Resolve the git tag to a commit SHA for caching
152- let git_sha = self
153- . resolve_git_tag_sha ( "dfinity" , "icp-cli-recipes" , & release_tag)
154- . await ?;
155-
156- // Cache the template keyed by the git SHA
157- self . pkg_cache
158- . with_write ( async |w| {
159- cache_recipe ( w, & package, & version, & git_sha, & bytes)
160- . context ( CacheRecipeSnafu )
161- } )
162- . await
163- . context ( LockCacheSnafu ) ??;
164-
165- parse_bytes_to_string ( bytes) ?
156+ ( parse_bytes_to_string ( bytes) ?, true )
166157 }
167158 }
168159 } ;
169160
161+ let hash = if let Some ( sha256) = & recipe. sha256 {
162+ verify_checksum ( tmpl. as_bytes ( ) , sha256) ?
163+ } else {
164+ Sha256 :: digest ( tmpl. as_bytes ( ) ) . into ( )
165+ } ;
166+
170167 // Load the template via handlebars
171168 let mut reg = handlebars:: Handlebars :: new ( ) ;
172169
@@ -204,8 +201,8 @@ impl Handlebars {
204201 }
205202
206203 let insts = serde_yaml:: from_str :: < BuildSyncHelper > ( & out) ;
207- match insts {
208- Ok ( helper) => Ok ( ( helper. build , helper. sync ) ) ,
204+ let ( build , sync ) = match insts {
205+ Ok ( helper) => ( helper. build , helper. sync ) ,
209206 Err ( e) => panic ! (
210207 "{}" ,
211208 formatdoc! { r#"
@@ -217,7 +214,41 @@ impl Handlebars {
217214 ------
218215 "# , recipe. recipe_type}
219216 ) ,
217+ } ;
218+
219+ // The template is verified good - now cache it if it was remote
220+ if should_cache {
221+ match tmpl_source {
222+ TemplateSource :: LocalPath ( _) => unreachable ! ( "local files are never cached" ) ,
223+ TemplateSource :: RemoteUrl ( u) => {
224+ self . pkg_cache
225+ . with_write ( async |w| {
226+ cache_uri_recipe ( w, & u, & hex:: encode ( hash) , tmpl. as_bytes ( ) )
227+ . context ( CacheRecipeSnafu ) ?;
228+ Ok ( ( ) )
229+ } )
230+ . await
231+ . context ( LockCacheSnafu ) ??;
232+ }
233+ TemplateSource :: Registry ( registry, recipe_name, version) => {
234+ let package = format ! ( "@{registry}/{recipe_name}" ) ;
235+ self . pkg_cache
236+ . with_write ( async |w| {
237+ cache_registry_recipe (
238+ w,
239+ & package,
240+ & version,
241+ & hex:: encode ( hash) ,
242+ tmpl. as_bytes ( ) ,
243+ )
244+ . context ( CacheRecipeSnafu )
245+ } )
246+ . await
247+ . context ( LockCacheSnafu ) ??;
248+ }
249+ }
220250 }
251+ Ok ( ( build, sync) )
221252 }
222253
223254 /// Fetch raw bytes from a remote URL.
@@ -241,80 +272,6 @@ impl Handlebars {
241272
242273 Ok ( resp. bytes ( ) . await . context ( HttpRequestSnafu ) ?. to_vec ( ) )
243274 }
244-
245- /// Fetch a remote template, verifying checksum if provided.
246- async fn fetch_remote_template (
247- & self ,
248- url : & str ,
249- sha256 : Option < & str > ,
250- ) -> Result < String , HandlebarsError > {
251- let bytes = self . fetch_remote_bytes ( url) . await ?;
252- if let Some ( expected) = sha256 {
253- verify_checksum ( & bytes, expected) ?;
254- }
255- parse_bytes_to_string ( bytes)
256- }
257-
258- /// Resolve a GitHub release tag to its underlying git commit SHA.
259- async fn resolve_git_tag_sha (
260- & self ,
261- owner : & str ,
262- repo : & str ,
263- tag : & str ,
264- ) -> Result < String , HandlebarsError > {
265- let ref_response = self
266- . github_api_get (
267- & format ! ( "https://api.github.com/repos/{owner}/{repo}/git/ref/tags/{tag}" ) ,
268- tag,
269- )
270- . await ?;
271-
272- let obj_type = ref_response[ "object" ] [ "type" ]
273- . as_str ( )
274- . ok_or_else ( || ParseGitTagSnafu { tag } . build ( ) ) ?;
275- let obj_sha = ref_response[ "object" ] [ "sha" ]
276- . as_str ( )
277- . ok_or_else ( || ParseGitTagSnafu { tag } . build ( ) ) ?;
278-
279- match obj_type {
280- // Lightweight tag — points directly at the commit
281- "commit" => Ok ( obj_sha. to_owned ( ) ) ,
282- // Annotated tag — dereference through the tag object to get the commit
283- "tag" => {
284- let tag_response = self
285- . github_api_get (
286- & format ! ( "https://api.github.com/repos/{owner}/{repo}/git/tags/{obj_sha}" ) ,
287- tag,
288- )
289- . await ?;
290- tag_response[ "object" ] [ "sha" ]
291- . as_str ( )
292- . map ( str:: to_owned)
293- . ok_or_else ( || ParseGitTagSnafu { tag } . build ( ) )
294- }
295- _ => ParseGitTagSnafu { tag } . fail ( ) ,
296- }
297- }
298-
299- /// Make an authenticated GET request to the GitHub API.
300- async fn github_api_get (
301- & self ,
302- url : & str ,
303- tag : & str ,
304- ) -> Result < serde_json:: Value , HandlebarsError > {
305- let mut req = self . http_client . get ( url) . header ( "User-Agent" , "icp-cli" ) ;
306- if let Ok ( token) = std:: env:: var ( "ICP_CLI_GITHUB_TOKEN" ) {
307- req = req. bearer_auth ( token) ;
308- }
309- req. send ( )
310- . await
311- . context ( ResolveGitTagSnafu { tag } ) ?
312- . error_for_status ( )
313- . context ( ResolveGitTagSnafu { tag } ) ?
314- . json ( )
315- . await
316- . context ( ResolveGitTagSnafu { tag } )
317- }
318275}
319276
320277#[ async_trait]
@@ -353,22 +310,21 @@ impl HelperDef for ReplaceHelper {
353310}
354311
355312/// Helper function to verify sha256 checksum of recipe template bytes
356- fn verify_checksum ( bytes : & [ u8 ] , expected : & str ) -> Result < ( ) , HandlebarsError > {
357- let actual = hex :: encode ( {
313+ fn verify_checksum ( bytes : & [ u8 ] , expected : & str ) -> Result < [ u8 ; 32 ] , HandlebarsError > {
314+ let actual_hash = {
358315 let mut h = Sha256 :: new ( ) ;
359316 h. update ( bytes) ;
360317 h. finalize ( )
361- } ) ;
362-
318+ } ;
319+ let actual = hex :: encode ( actual_hash ) ;
363320 if actual != expected {
364321 return ChecksumMismatchSnafu {
365322 expected : expected. to_string ( ) ,
366323 actual,
367324 }
368325 . fail ( ) ;
369326 }
370-
371- Ok ( ( ) )
327+ Ok ( actual_hash. into ( ) )
372328}
373329
374330/// Helper function to parse bytes into a UTF-8 string
0 commit comments