@@ -7,6 +7,7 @@ mod manifest;
77use anyhow:: { anyhow, bail, Context , Result } ;
88use manifest:: ComponentBuildInfo ;
99use spin_common:: { paths:: parent_dir, ui:: quoted_path} ;
10+ use spin_manifest:: schema:: v2;
1011use std:: {
1112 collections:: HashSet ,
1213 path:: { Path , PathBuf } ,
@@ -124,6 +125,15 @@ fn build_components(
124125 return Ok ( ( ) ) ;
125126 }
126127
128+ // If dependencies are being built as part of `spin build`, we would like
129+ // them to be rebuilt earlier (e.g. so that consumers using the binary as a source
130+ // of type information see the latest interface).
131+ let ( components_to_build, has_cycle) = sort ( components_to_build) ;
132+
133+ if has_cycle {
134+ terminal:: warn!( "There is a dependency cycle among components. Spin cannot guarantee to build dependencies before consumers." ) ;
135+ }
136+
127137 components_to_build
128138 . into_iter ( )
129139 . map ( |c| build_component ( c, app_dir) )
@@ -215,6 +225,97 @@ fn construct_workdir(app_dir: &Path, workdir: Option<impl AsRef<Path>>) -> Resul
215225 Ok ( cwd)
216226}
217227
228+ #[ derive( Clone ) ]
229+ struct SortableBuildInfo {
230+ source : Option < String > ,
231+ local_dependency_paths : Vec < String > ,
232+ build_info : ComponentBuildInfo ,
233+ }
234+
235+ impl From < & ComponentBuildInfo > for SortableBuildInfo {
236+ fn from ( value : & ComponentBuildInfo ) -> Self {
237+ fn local_dep_path ( dep : & v2:: ComponentDependency ) -> Option < String > {
238+ match dep {
239+ v2:: ComponentDependency :: Local { path, .. } => Some ( path. display ( ) . to_string ( ) ) ,
240+ _ => None ,
241+ }
242+ }
243+
244+ let source = match value. source . as_ref ( ) {
245+ Some ( spin_manifest:: schema:: v2:: ComponentSource :: Local ( path) ) => Some ( path. clone ( ) ) ,
246+ _ => None ,
247+ } ;
248+ let local_dependency_paths = value
249+ . dependencies
250+ . inner
251+ . values ( )
252+ . filter_map ( local_dep_path)
253+ . collect ( ) ;
254+
255+ Self {
256+ source,
257+ local_dependency_paths,
258+ build_info : value. clone ( ) ,
259+ }
260+ }
261+ }
262+
263+ impl std:: hash:: Hash for SortableBuildInfo {
264+ fn hash < H : std:: hash:: Hasher > ( & self , state : & mut H ) {
265+ self . build_info . id . hash ( state) ;
266+ self . source . hash ( state) ;
267+ self . local_dependency_paths . hash ( state) ;
268+ }
269+ }
270+
271+ impl PartialEq for SortableBuildInfo {
272+ fn eq ( & self , other : & Self ) -> bool {
273+ self . build_info . id == other. build_info . id
274+ && self . source == other. source
275+ && self . local_dependency_paths == other. local_dependency_paths
276+ }
277+ }
278+
279+ impl Eq for SortableBuildInfo { }
280+
281+ /// Topo sort by local path dependency. Second result is if there was a cycle.
282+ fn sort ( components : Vec < ComponentBuildInfo > ) -> ( Vec < ComponentBuildInfo > , bool ) {
283+ let sortables = components
284+ . iter ( )
285+ . map ( SortableBuildInfo :: from)
286+ . collect :: < Vec < _ > > ( ) ;
287+ let mut sorter = topological_sort:: TopologicalSort :: < SortableBuildInfo > :: new ( ) ;
288+
289+ for s in & sortables {
290+ sorter. insert ( s. clone ( ) ) ;
291+ }
292+
293+ for s1 in & sortables {
294+ for dep in & s1. local_dependency_paths {
295+ for s2 in & sortables {
296+ if s2. source . as_ref ( ) . is_some_and ( |src| src == dep) {
297+ // s1 depends on s2
298+ sorter. add_link ( topological_sort:: DependencyLink {
299+ prec : s2. clone ( ) ,
300+ succ : s1. clone ( ) ,
301+ } ) ;
302+ }
303+ }
304+ }
305+ }
306+
307+ let result = sorter. map ( |s| s. build_info ) . collect :: < Vec < _ > > ( ) ;
308+
309+ // We shouldn't refuse to build if a cycle occurs, so return the original order to allow
310+ // stuff to proceed. (We could be smarter about this, but really it's a pathological situation
311+ // and we don't need to bust a gut over it.)
312+ if result. len ( ) == components. len ( ) {
313+ ( result, false )
314+ } else {
315+ ( components, true )
316+ }
317+ }
318+
218319/// Specifies target environment checking behaviour
219320pub enum TargetChecking {
220321 /// The build should check that all components are compatible with all target environments.
@@ -301,4 +402,162 @@ mod tests {
301402 assert ! ( err. contains( "requires imports named" ) ) ;
302403 assert ! ( err. contains( "wasi:cli/stdout" ) ) ;
303404 }
405+
406+ fn dummy_buildinfo ( id : & str ) -> ComponentBuildInfo {
407+ dummy_build_info_deps ( id, & [ ] )
408+ }
409+
410+ fn dummy_build_info_dep ( id : & str , dep_on : & str ) -> ComponentBuildInfo {
411+ dummy_build_info_deps ( id, & [ dep_on] )
412+ }
413+
414+ fn dummy_build_info_deps ( id : & str , dep_on : & [ & str ] ) -> ComponentBuildInfo {
415+ ComponentBuildInfo {
416+ id : id. into ( ) ,
417+ source : Some ( v2:: ComponentSource :: Local ( format ! ( "{id}.wasm" ) ) ) ,
418+ build : None ,
419+ dependencies : depends_on ( dep_on) ,
420+ }
421+ }
422+
423+ fn depends_on ( paths : & [ & str ] ) -> v2:: ComponentDependencies {
424+ let mut deps = vec ! [ ] ;
425+ for ( index, path) in paths. iter ( ) . enumerate ( ) {
426+ let dep_name =
427+ spin_serde:: DependencyName :: Plain ( format ! ( "dummy{index}" ) . try_into ( ) . unwrap ( ) ) ;
428+ let dep = v2:: ComponentDependency :: Local {
429+ path : path. into ( ) ,
430+ export : None ,
431+ } ;
432+ deps. push ( ( dep_name, dep) ) ;
433+ }
434+ v2:: ComponentDependencies {
435+ inner : deps. into_iter ( ) . collect ( ) ,
436+ }
437+ }
438+
439+ /// Asserts that id `before` comes before id `after` in collection `cs`
440+ fn assert_before ( cs : & [ ComponentBuildInfo ] , before : & str , after : & str ) {
441+ assert ! (
442+ cs. iter( ) . position( |c| c. id == before) . unwrap( )
443+ < cs. iter( ) . position( |c| c. id == after) . unwrap( )
444+ ) ;
445+ }
446+
447+ #[ test]
448+ fn if_no_dependencies_then_all_build ( ) {
449+ let ( cs, had_cycle) = sort ( vec ! [ dummy_buildinfo( "1" ) , dummy_buildinfo( "2" ) ] ) ;
450+ assert_eq ! ( 2 , cs. len( ) ) ;
451+ assert ! ( cs. iter( ) . any( |c| c. id == "1" ) ) ;
452+ assert ! ( cs. iter( ) . any( |c| c. id == "2" ) ) ;
453+ assert ! ( !had_cycle) ;
454+ }
455+
456+ #[ test]
457+ fn dependencies_build_before_consumers ( ) {
458+ let ( cs, had_cycle) = sort ( vec ! [
459+ dummy_buildinfo( "1" ) ,
460+ dummy_build_info_dep( "2" , "3.wasm" ) ,
461+ dummy_buildinfo( "3" ) ,
462+ dummy_build_info_dep( "4" , "1.wasm" ) ,
463+ ] ) ;
464+ assert_eq ! ( 4 , cs. len( ) ) ;
465+ assert_before ( & cs, "1" , "4" ) ;
466+ assert_before ( & cs, "3" , "2" ) ;
467+ assert ! ( !had_cycle) ;
468+ }
469+
470+ #[ test]
471+ fn multiple_dependencies_build_before_consumers ( ) {
472+ let ( cs, had_cycle) = sort ( vec ! [
473+ dummy_buildinfo( "1" ) ,
474+ dummy_build_info_dep( "2" , "3.wasm" ) ,
475+ dummy_buildinfo( "3" ) ,
476+ dummy_build_info_dep( "4" , "1.wasm" ) ,
477+ dummy_build_info_dep( "5" , "3.wasm" ) ,
478+ dummy_build_info_deps( "6" , & [ "3.wasm" , "2.wasm" ] ) ,
479+ dummy_buildinfo( "7" ) ,
480+ ] ) ;
481+ assert_eq ! ( 7 , cs. len( ) ) ;
482+ assert_before ( & cs, "1" , "4" ) ;
483+ assert_before ( & cs, "3" , "2" ) ;
484+ assert_before ( & cs, "3" , "5" ) ;
485+ assert_before ( & cs, "3" , "6" ) ;
486+ assert_before ( & cs, "2" , "6" ) ;
487+ assert ! ( !had_cycle) ;
488+ }
489+
490+ #[ test]
491+ fn circular_dependencies_dont_prevent_build ( ) {
492+ let ( cs, had_cycle) = sort ( vec ! [
493+ dummy_buildinfo( "1" ) ,
494+ dummy_build_info_dep( "2" , "3.wasm" ) ,
495+ dummy_build_info_dep( "3" , "2.wasm" ) ,
496+ dummy_build_info_dep( "4" , "1.wasm" ) ,
497+ ] ) ;
498+ assert_eq ! ( 4 , cs. len( ) ) ;
499+ assert ! ( cs. iter( ) . any( |c| c. id == "1" ) ) ;
500+ assert ! ( cs. iter( ) . any( |c| c. id == "2" ) ) ;
501+ assert ! ( cs. iter( ) . any( |c| c. id == "3" ) ) ;
502+ assert ! ( cs. iter( ) . any( |c| c. id == "4" ) ) ;
503+ assert ! ( had_cycle) ;
504+ }
505+
506+ #[ test]
507+ fn non_path_dependencies_do_not_prevent_sorting ( ) {
508+ let mut depends_on_remote = dummy_buildinfo ( "2" ) ;
509+ depends_on_remote. dependencies . inner . insert (
510+ spin_serde:: DependencyName :: Plain ( "remote" . to_owned ( ) . try_into ( ) . unwrap ( ) ) ,
511+ v2:: ComponentDependency :: Version ( "1.2.3" . to_owned ( ) ) ,
512+ ) ;
513+
514+ let mut depends_on_local_and_remote = dummy_build_info_dep ( "4" , "1.wasm" ) ;
515+ depends_on_local_and_remote. dependencies . inner . insert (
516+ spin_serde:: DependencyName :: Plain ( "remote" . to_owned ( ) . try_into ( ) . unwrap ( ) ) ,
517+ v2:: ComponentDependency :: Version ( "1.2.3" . to_owned ( ) ) ,
518+ ) ;
519+
520+ let ( cs, _) = sort ( vec ! [
521+ dummy_buildinfo( "1" ) ,
522+ depends_on_remote,
523+ dummy_buildinfo( "3" ) ,
524+ depends_on_local_and_remote,
525+ ] ) ;
526+
527+ assert_eq ! ( 4 , cs. len( ) ) ;
528+ assert_before ( & cs, "1" , "4" ) ;
529+ }
530+
531+ #[ test]
532+ fn non_path_sources_do_not_prevent_sorting ( ) {
533+ let mut remote_source = dummy_build_info_dep ( "2" , "3.wasm" ) ;
534+ remote_source. source = Some ( v2:: ComponentSource :: Remote {
535+ url : "far://away" . into ( ) ,
536+ digest : "loadsa-hex" . into ( ) ,
537+ } ) ;
538+
539+ let ( cs, _) = sort ( vec ! [
540+ dummy_buildinfo( "1" ) ,
541+ remote_source,
542+ dummy_buildinfo( "3" ) ,
543+ dummy_build_info_dep( "4" , "1.wasm" ) ,
544+ ] ) ;
545+
546+ assert_eq ! ( 4 , cs. len( ) ) ;
547+ assert_before ( & cs, "1" , "4" ) ;
548+ }
549+
550+ #[ test]
551+ fn dependencies_on_non_manifest_components_do_not_prevent_sorting ( ) {
552+ let ( cs, had_cycle) = sort ( vec ! [
553+ dummy_buildinfo( "1" ) ,
554+ dummy_build_info_deps( "2" , & [ "3.wasm" , "crikey.wasm" ] ) ,
555+ dummy_buildinfo( "3" ) ,
556+ dummy_build_info_dep( "4" , "1.wasm" ) ,
557+ ] ) ;
558+ assert_eq ! ( 4 , cs. len( ) ) ;
559+ assert_before ( & cs, "1" , "4" ) ;
560+ assert_before ( & cs, "3" , "2" ) ;
561+ assert ! ( !had_cycle) ;
562+ }
304563}
0 commit comments