Skip to content

Commit 76f0b66

Browse files
committed
feat(core,wkg): resolve multiple versions of the same package
Key dependencies by package plus version requirement rather than by package alone, so a wkg.toml can declare a separate override per version and a world naming two versions of the same package resolves both instead of dropping one
1 parent 94501bc commit 76f0b66

10 files changed

Lines changed: 559 additions & 40 deletions

File tree

crates/wasm-pkg-core/src/manifest.rs

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
//! Type definitions and functions for working with `wkg.toml` files.
22
33
use std::{
4-
collections::HashMap,
4+
collections::{HashMap, HashSet},
55
path::{Path, PathBuf},
66
};
77

88
use anyhow::{Context, Result};
99
use semver::VersionReq;
1010
use serde::{Deserialize, Serialize};
11+
use wasm_pkg_common::package::{PackageRef, PackageSpec};
1112
mod paths;
1213
pub mod workspace;
1314

@@ -79,10 +80,56 @@ impl Manifest {
7980
// `Manifest` validations, mirrors cargo's `Workspace::validate`
8081
fn validate(&self) -> Result<()> {
8182
self.validate_workspace_exclusivity()?;
83+
self.validate_override_keys()?;
8284
// Add new validation rules with `self.validate_*()?;`
8385
Ok(())
8486
}
8587

88+
/// Checks that override keys parse and that no package is covered by both a bare and a
89+
/// versioned key.
90+
///
91+
/// Run as part of [`validate`](Self::validate) on load, and again when resolving, since a
92+
/// `Manifest` can also be built directly through the library API without going through TOML.
93+
pub(crate) fn validate_override_keys(&self) -> Result<()> {
94+
let Some(overrides) = self.overrides.as_ref() else {
95+
return Ok(());
96+
};
97+
// `overrides` is a map, so walk it in a stable order
98+
let mut sorted_keys: Vec<&String> = overrides.keys().collect();
99+
sorted_keys.sort_unstable();
100+
101+
let mut bare: HashSet<PackageRef> = HashSet::new();
102+
let mut versioned: HashMap<PackageRef, Vec<&str>> = HashMap::new();
103+
for key in sorted_keys {
104+
let spec: PackageSpec = key
105+
.parse()
106+
.with_context(|| format!("invalid override key `{key}`"))?;
107+
match spec.version {
108+
Some(_) => versioned.entry(spec.package).or_default().push(key),
109+
None => {
110+
bare.insert(spec.package);
111+
}
112+
}
113+
}
114+
115+
let mut conflicts: Vec<String> = versioned
116+
.iter()
117+
.filter(|(package, _)| bare.contains(*package))
118+
.map(|(package, keys)| {
119+
format!(
120+
"override `{package}` applies to every version of the package, so it \
121+
conflicts with the versioned override(s) `{}`",
122+
keys.join("`, `")
123+
)
124+
})
125+
.collect();
126+
if conflicts.is_empty() {
127+
return Ok(());
128+
}
129+
conflicts.sort_unstable();
130+
anyhow::bail!("{} - remove one or the other", conflicts.join("; "));
131+
}
132+
86133
// no overrides or top-level metadata when workspace is present
87134
fn validate_workspace_exclusivity(&self) -> Result<()> {
88135
if self.workspace.is_none() {
@@ -241,4 +288,68 @@ mod tests {
241288
"manifest loaded from file does not match original manifest"
242289
);
243290
}
291+
292+
#[test]
293+
fn override_keys_may_carry_a_version() {
294+
let manifest = Manifest::from_toml(
295+
r#"
296+
[overrides]
297+
"foo:bar@0.1.0" = { path = "bar-0.1.0" }
298+
"foo:bar@0.2.0" = { path = "bar-0.2.0" }
299+
"foo:baz" = { path = "baz" }
300+
"#,
301+
)
302+
.expect("versioned override keys should be accepted");
303+
assert_eq!(manifest.overrides.unwrap().len(), 3);
304+
}
305+
306+
#[test]
307+
fn override_keys_conflict_when_bare_and_versioned() {
308+
let err = Manifest::from_toml(
309+
r#"
310+
[overrides]
311+
"foo:bar" = { path = "bar" }
312+
"foo:bar@0.1.0" = { path = "bar-0.1.0" }
313+
"#,
314+
)
315+
.expect_err("a bare key alongside a versioned one is ambiguous");
316+
let err = format!("{err:#}");
317+
assert!(err.contains("foo:bar@0.1.0"), "unexpected error: {err}");
318+
}
319+
320+
#[test]
321+
fn override_key_conflicts_are_all_reported_in_a_stable_order() {
322+
// Two conflicting packages: both must appear, and always in the same order, rather than
323+
// whichever the underlying map happened to yield first.
324+
let err = Manifest::from_toml(
325+
r#"
326+
[overrides]
327+
"zzz:two" = { path = "z" }
328+
"zzz:two@0.2.0" = { path = "z2" }
329+
"aaa:one" = { path = "a" }
330+
"aaa:one@0.1.0" = { path = "a1" }
331+
"#,
332+
)
333+
.expect_err("both packages conflict");
334+
let err = format!("{err:#}");
335+
let aaa = err.find("aaa:one").expect("aaa:one should be reported");
336+
let zzz = err.find("zzz:two").expect("zzz:two should be reported");
337+
assert!(aaa < zzz, "conflicts should be sorted: {err}");
338+
}
339+
340+
#[test]
341+
fn override_keys_must_parse() {
342+
let err = Manifest::from_toml(
343+
r#"
344+
[overrides]
345+
"not a package ref" = { path = "bar" }
346+
"#,
347+
)
348+
.expect_err("an unparseable override key should be rejected");
349+
let err = format!("{err:#}");
350+
assert!(
351+
err.contains("invalid override key"),
352+
"unexpected error: {err}"
353+
);
354+
}
244355
}

0 commit comments

Comments
 (0)