@@ -7,6 +7,7 @@ mod manifest;
7
7
use anyhow:: { anyhow, bail, Context , Result } ;
8
8
use manifest:: ComponentBuildInfo ;
9
9
use spin_common:: { paths:: parent_dir, ui:: quoted_path} ;
10
+ use spin_manifest:: schema:: v2;
10
11
use std:: {
11
12
collections:: HashSet ,
12
13
path:: { Path , PathBuf } ,
@@ -124,6 +125,15 @@ fn build_components(
124
125
return Ok ( ( ) ) ;
125
126
}
126
127
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
+
127
137
components_to_build
128
138
. into_iter ( )
129
139
. map ( |c| build_component ( c, app_dir) )
@@ -215,6 +225,97 @@ fn construct_workdir(app_dir: &Path, workdir: Option<impl AsRef<Path>>) -> Resul
215
225
Ok ( cwd)
216
226
}
217
227
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
+
218
319
/// Specifies target environment checking behaviour
219
320
pub enum TargetChecking {
220
321
/// The build should check that all components are compatible with all target environments.
@@ -301,4 +402,162 @@ mod tests {
301
402
assert ! ( err. contains( "requires imports named" ) ) ;
302
403
assert ! ( err. contains( "wasi:cli/stdout" ) ) ;
303
404
}
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
+ }
304
563
}
0 commit comments