@@ -4,6 +4,7 @@ use std::collections::{BTreeMap, HashMap};
44use std:: path:: { Path , PathBuf } ;
55
66use anyhow:: { bail, Context , Result } ;
7+ use async_trait:: async_trait;
78use docker_credential:: DockerCredential ;
89use futures_util:: future;
910use futures_util:: stream:: { self , StreamExt , TryStreamExt } ;
@@ -18,7 +19,7 @@ use spin_common::ui::quoted_path;
1819use spin_common:: url:: parse_file_url;
1920use spin_loader:: cache:: Cache ;
2021use spin_loader:: FilesMountStrategy ;
21- use spin_locked_app:: locked:: { ContentPath , ContentRef , LockedApp } ;
22+ use spin_locked_app:: locked:: { ContentPath , ContentRef , LockedApp , LockedComponentSource } ;
2223use tokio:: fs;
2324use walkdir:: WalkDir ;
2425
@@ -119,6 +120,7 @@ impl Client {
119120 reference : impl AsRef < str > ,
120121 annotations : Option < BTreeMap < String , String > > ,
121122 infer_annotations : InferPredefinedAnnotations ,
123+ skip_compose : bool ,
122124 ) -> Result < Option < String > > {
123125 let reference: Reference = reference
124126 . as_ref ( )
@@ -137,8 +139,15 @@ impl Client {
137139 )
138140 . await ?;
139141
140- self . push_locked_core ( locked, auth, reference, annotations, infer_annotations)
141- . await
142+ self . push_locked_core (
143+ locked,
144+ auth,
145+ reference,
146+ annotations,
147+ infer_annotations,
148+ skip_compose,
149+ )
150+ . await
142151 }
143152
144153 /// Push a Spin application to an OCI registry and return the digest (or None
@@ -149,15 +158,23 @@ impl Client {
149158 reference : impl AsRef < str > ,
150159 annotations : Option < BTreeMap < String , String > > ,
151160 infer_annotations : InferPredefinedAnnotations ,
161+ skip_compose : bool ,
152162 ) -> Result < Option < String > > {
153163 let reference: Reference = reference
154164 . as_ref ( )
155165 . parse ( )
156166 . with_context ( || format ! ( "cannot parse reference {}" , reference. as_ref( ) ) ) ?;
157167 let auth = Self :: auth ( & reference) . await ?;
158168
159- self . push_locked_core ( locked, auth, reference, annotations, infer_annotations)
160- . await
169+ self . push_locked_core (
170+ locked,
171+ auth,
172+ reference,
173+ annotations,
174+ infer_annotations,
175+ skip_compose,
176+ )
177+ . await
161178 }
162179
163180 /// Push a Spin application to an OCI registry and return the digest (or None
@@ -169,10 +186,11 @@ impl Client {
169186 reference : Reference ,
170187 annotations : Option < BTreeMap < String , String > > ,
171188 infer_annotations : InferPredefinedAnnotations ,
189+ skip_compose : bool ,
172190 ) -> Result < Option < String > > {
173191 let mut locked_app = locked. clone ( ) ;
174192 let mut layers = self
175- . assemble_layers ( & mut locked_app, AssemblyMode :: Simple )
193+ . assemble_layers ( & mut locked_app, AssemblyMode :: Simple , skip_compose )
176194 . await
177195 . context ( "could not assemble layers for locked application" ) ?;
178196
@@ -183,7 +201,7 @@ impl Client {
183201 {
184202 locked_app = locked. clone ( ) ;
185203 layers = self
186- . assemble_layers ( & mut locked_app, AssemblyMode :: Archive )
204+ . assemble_layers ( & mut locked_app, AssemblyMode :: Archive , skip_compose )
187205 . await
188206 . context ( "could not assemble archive layers for locked application" ) ?;
189207 }
@@ -246,43 +264,57 @@ impl Client {
246264 & mut self ,
247265 locked : & mut LockedApp ,
248266 assembly_mode : AssemblyMode ,
267+ skip_compose : bool ,
249268 ) -> Result < Vec < ImageLayer > > {
250269 let mut layers = Vec :: new ( ) ;
251270 let mut components = Vec :: new ( ) ;
252271 for mut c in locked. clone ( ) . components {
253- // Add the wasm module for the component as layers.
254- let source = c
255- . clone ( )
256- . source
257- . content
258- . source
259- . context ( "component loaded from disk should contain a file source" ) ?;
260-
261- let source = parse_file_url ( source. as_str ( ) ) ?;
262- let layer = Self :: wasm_layer ( & source) . await ?;
263-
264- // Update the module source with the content ref of the layer.
265- c. source . content = self . content_ref_for_layer ( & layer) ;
266-
267- layers. push ( layer) ;
268-
269- let mut deps = BTreeMap :: default ( ) ;
270- for ( dep_name, mut dep) in c. dependencies {
271- let source = dep
272+ if !skip_compose {
273+ let composed = spin_compose:: compose ( & ComponentSourceLoader , & c)
274+ . await
275+ . with_context ( || {
276+ format ! ( "failed to resolve dependencies for component {:?}" , c. id)
277+ } ) ?;
278+
279+ let layer = ImageLayer :: new ( composed, WASM_LAYER_MEDIA_TYPE . to_string ( ) , None ) ;
280+ c. source . content = self . content_ref_for_layer ( & layer) ;
281+ c. dependencies . clear ( ) ;
282+ layers. push ( layer) ;
283+ } else {
284+ // Add the wasm module for the component as layers.
285+ let source = c
286+ . clone ( )
272287 . source
273288 . content
274289 . source
275- . context ( "dependency loaded from disk should contain a file source" ) ?;
276- let source = parse_file_url ( source. as_str ( ) ) ?;
290+ . context ( "component loaded from disk should contain a file source" ) ?;
277291
292+ let source = parse_file_url ( source. as_str ( ) ) ?;
278293 let layer = Self :: wasm_layer ( & source) . await ?;
279294
280- dep . source . content = self . content_ref_for_layer ( & layer) ;
281- deps . insert ( dep_name , dep ) ;
295+ // Update the module source with the content ref of the layer.
296+ c . source . content = self . content_ref_for_layer ( & layer ) ;
282297
283298 layers. push ( layer) ;
299+
300+ let mut deps = BTreeMap :: default ( ) ;
301+ for ( dep_name, mut dep) in c. dependencies {
302+ let source = dep
303+ . source
304+ . content
305+ . source
306+ . context ( "dependency loaded from disk should contain a file source" ) ?;
307+ let source = parse_file_url ( source. as_str ( ) ) ?;
308+
309+ let layer = Self :: wasm_layer ( & source) . await ?;
310+
311+ dep. source . content = self . content_ref_for_layer ( & layer) ;
312+ deps. insert ( dep_name, dep) ;
313+
314+ layers. push ( layer) ;
315+ }
316+ c. dependencies = deps;
284317 }
285- c. dependencies = deps;
286318
287319 let mut files = Vec :: new ( ) ;
288320 for f in c. files {
@@ -669,6 +701,32 @@ impl Client {
669701 }
670702}
671703
704+ struct ComponentSourceLoader ;
705+
706+ #[ async_trait]
707+ impl spin_compose:: ComponentSourceLoader for ComponentSourceLoader {
708+ async fn load_component_source (
709+ & self ,
710+ source : & LockedComponentSource ,
711+ ) -> anyhow:: Result < Vec < u8 > > {
712+ let source = source
713+ . content
714+ . source
715+ . as_ref ( )
716+ . context ( "component loaded from disk should contain a file source" ) ?;
717+
718+ let source = parse_file_url ( source. as_str ( ) ) ?;
719+
720+ let bytes = fs:: read ( & source)
721+ . await
722+ . with_context ( || format ! ( "cannot read wasm module {}" , quoted_path( source) ) ) ?;
723+
724+ let component = spin_componentize:: componentize_if_necessary ( & bytes) ?;
725+
726+ Ok ( component. into ( ) )
727+ }
728+ }
729+
672730/// Unpack contents of the provided archive layer, represented by bytes and its
673731/// corresponding digest, into the provided cache.
674732/// A temporary staging directory is created via tempfile::tempdir() to store
@@ -946,6 +1004,7 @@ mod test {
9461004 locked_components : Vec < LockedComponent > ,
9471005 expected_layer_count : usize ,
9481006 expected_error : Option < & ' static str > ,
1007+ skip_compose : bool ,
9491008 }
9501009
9511010 let tests: Vec < TestCase > = [
@@ -968,6 +1027,7 @@ mod test {
9681027 } } ] ) ,
9691028 expected_layer_count : 2 ,
9701029 expected_error : None ,
1030+ skip_compose : false ,
9711031 } ,
9721032 TestCase {
9731033 name : "One component layer and two file layers" ,
@@ -992,6 +1052,7 @@ mod test {
9921052 } ] ) ,
9931053 expected_layer_count : 3 ,
9941054 expected_error : None ,
1055+ skip_compose : false ,
9951056 } ,
9961057 TestCase {
9971058 name : "One component layer and one file with inlined content" ,
@@ -1012,6 +1073,7 @@ mod test {
10121073 } ] ) ,
10131074 expected_layer_count : 1 ,
10141075 expected_error : None ,
1076+ skip_compose : false ,
10151077 } ,
10161078 TestCase {
10171079 name : "One component layer and one dependency component layer" ,
@@ -1036,6 +1098,7 @@ mod test {
10361098 } ] ) ,
10371099 expected_layer_count : 2 ,
10381100 expected_error : None ,
1101+ skip_compose : false ,
10391102 } ,
10401103 TestCase {
10411104 name : "Component has no source" ,
@@ -1050,6 +1113,7 @@ mod test {
10501113 } ] ) ,
10511114 expected_layer_count : 0 ,
10521115 expected_error : Some ( "Invalid URL: \" \" " ) ,
1116+ skip_compose : false ,
10531117 } ,
10541118 TestCase {
10551119 name : "Duplicate component sources" ,
@@ -1070,6 +1134,7 @@ mod test {
10701134 } } ] ) ,
10711135 expected_layer_count : 1 ,
10721136 expected_error : None ,
1137+ skip_compose : false ,
10731138 } ,
10741139 TestCase {
10751140 name : "Duplicate file paths" ,
@@ -1107,6 +1172,7 @@ mod test {
11071172 } ] ) ,
11081173 expected_layer_count : 4 ,
11091174 expected_error : None ,
1175+ skip_compose : false ,
11101176 } ,
11111177 ]
11121178 . to_vec ( ) ;
@@ -1137,7 +1203,7 @@ mod test {
11371203 assert_eq ! (
11381204 e,
11391205 client
1140- . assemble_layers( & mut locked, AssemblyMode :: Simple )
1206+ . assemble_layers( & mut locked, AssemblyMode :: Simple , tc . skip_compose )
11411207 . await
11421208 . unwrap_err( )
11431209 . to_string( ) ,
@@ -1149,7 +1215,7 @@ mod test {
11491215 assert_eq ! (
11501216 tc. expected_layer_count,
11511217 client
1152- . assemble_layers( & mut locked, AssemblyMode :: Simple )
1218+ . assemble_layers( & mut locked, AssemblyMode :: Simple , tc . skip_compose )
11531219 . await
11541220 . unwrap( )
11551221 . len( ) ,
0 commit comments