Skip to content

Commit 1ff8a8f

Browse files
committed
Sort builds if dependency relation between manifest components
Signed-off-by: itowlson <[email protected]>
1 parent 1525f7b commit 1ff8a8f

File tree

4 files changed

+268
-0
lines changed

4 files changed

+268
-0
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/build/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ serde = { workspace = true }
1010
spin-common = { path = "../common" }
1111
spin-environments = { path = "../environments" }
1212
spin-manifest = { path = "../manifest" }
13+
spin-serde = { path = "../serde" }
1314
subprocess = "0.2"
1415
terminal = { path = "../terminal" }
1516
tokio = { workspace = true, features = ["full"] }
1617
toml = { workspace = true }
18+
topological-sort = "0.2"

crates/build/src/lib.rs

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod manifest;
77
use anyhow::{anyhow, bail, Context, Result};
88
use manifest::ComponentBuildInfo;
99
use spin_common::{paths::parent_dir, ui::quoted_path};
10+
use spin_manifest::schema::v2;
1011
use 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
219320
pub 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
}

crates/build/src/manifest.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ fn build_configs_from_manifest(
108108
.map(|(id, c)| ComponentBuildInfo {
109109
id: id.to_string(),
110110
build: c.build.clone(),
111+
source: Some(c.source.clone()),
112+
dependencies: c.dependencies.clone(),
111113
})
112114
.collect()
113115
}
@@ -160,7 +162,10 @@ async fn has_deployment_targets(manifest_file: impl AsRef<Path>) -> Result<bool>
160162
pub struct ComponentBuildInfo {
161163
#[serde(default)]
162164
pub id: String,
165+
pub source: Option<v2::ComponentSource>,
163166
pub build: Option<v2::ComponentBuildConfig>,
167+
#[serde(default)]
168+
pub dependencies: v2::ComponentDependencies,
164169
}
165170

166171
#[derive(Deserialize)]

0 commit comments

Comments
 (0)