Skip to content

Commit 07c7281

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

File tree

4 files changed

+265
-0
lines changed

4 files changed

+265
-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: 256 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,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
219317
pub 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
}

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)