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