From 49d97c91fdfb091e5583484a7f5dab89246dfe00 Mon Sep 17 00:00:00 2001 From: Sebastian Battle Date: Fri, 9 Oct 2026 01:45:14 -0400 Subject: [PATCH 1/4] fix(lock): apply aqua version prefixes before selecting overrides --- ...test_lockfile_aqua_version_prefix_override | 42 +++++++++ src/backend/aqua.rs | 88 ++++++++++++++++--- 2 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 e2e/lockfile/test_lockfile_aqua_version_prefix_override diff --git a/e2e/lockfile/test_lockfile_aqua_version_prefix_override b/e2e/lockfile/test_lockfile_aqua_version_prefix_override new file mode 100644 index 00000000000..b003cdb9912 --- /dev/null +++ b/e2e/lockfile/test_lockfile_aqua_version_prefix_override @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +export MISE_LOCKFILE=1 +export MISE_AQUA_BAKED_REGISTRY=0 + +# An HTTP registry entry has no release-tag lookup. Exercise the same fallback +# used when a pinned GitHub release is absent from the release listing. +ASSET="codex-package-x86_64-pc-windows-msvc.tar.gz" +mkdir -p server/rust-v0.154.0 +printf 'archive\n' >"server/rust-v0.154.0/$ASSET" +start_http_server --dir server + +REGISTRY_DIR="$PWD/aqua-registry-local" +mkdir -p "$REGISTRY_DIR" +cat <"$REGISTRY_DIR/registry.yaml" +packages: + - type: http + name: example/codex-package + version_prefix: rust- + version_constraint: "false" + version_overrides: + - version_constraint: semver("<= 0.133.0-alpha.1") + url: http://127.0.0.1:$HTTP_PORT/legacy.zst + format: zst + - version_constraint: "true" + url: http://127.0.0.1:$HTTP_PORT/{{.Version}}/codex-package-{{.Arch}}-{{.OS}}.{{.Format}} + format: tar.gz + replacements: + amd64: x86_64 + windows: pc-windows-msvc +EOF +export MISE_AQUA_REGISTRY_URL="file://$REGISTRY_DIR" + +cat <<'EOF' >mise.toml +[tools] +"aqua:example/codex-package" = "0.154.0" +EOF + +mise lock --platform windows-x64 + +assert_contains "cat mise.lock" '"platforms.windows-x64"' +assert_contains "cat mise.lock" "url = \"http://127.0.0.1:$HTTP_PORT/rust-v0.154.0/$ASSET\"" diff --git a/src/backend/aqua.rs b/src/backend/aqua.rs index aca12c45cf3..36c051eb707 100644 --- a/src/backend/aqua.rs +++ b/src/backend/aqua.rs @@ -770,17 +770,14 @@ impl Backend for AquaBackend { self.id, tv.version ); } - let mut v = tag.unwrap_or_else(|| tv.version.clone()); - let mut v_prefixed = (tag_is_none && !tv.version.starts_with('v')).then(|| format!("v{v}")); - let versions = match &v_prefixed { - Some(v_prefixed) => vec![v.as_str(), v_prefixed.as_str()], - None => vec![v.as_str()], - }; - // Get package and apply version/overrides directly for the target platform. // Using package_with_version() here would apply overrides for the current host // platform first, which can leak host-specific overrides into cross-platform lock. let pkg = AQUA_REGISTRY.package(&self.id).await?; + let versions = resolved_package_version_candidates(&tv.version, tag.as_deref(), &pkg); + let versions = versions.iter().map(|v| v.as_ref()).collect_vec(); + let mut v = tag.clone().unwrap_or_else(|| tv.version.clone()); + let mut v_prefixed = (tag_is_none && !tv.version.starts_with('v')).then(|| format!("v{v}")); let raw_opts = tv.request.options(); let opts = AquaOptions::new(&raw_opts); let target_libc = Self::target_variant_libc(target, self.tool_libc); @@ -1073,7 +1070,7 @@ impl AquaBackend { let mut v_prefixed = (tag.is_none() && !tv.version.starts_with('v')).then(|| format!("v{v}")); let pkg = AQUA_REGISTRY.package(&self.id).await?; - let versions = install_package_version_candidates(&tv.version, tag.as_deref(), &pkg); + let versions = resolved_package_version_candidates(&tv.version, tag.as_deref(), &pkg); let versions = versions.iter().map(|v| v.as_ref()).collect_vec(); let pkg = Self::package_with_options_for_pkg(tv, pkg, &versions)?; if let Some(prefix) = &pkg.version_prefix @@ -3910,7 +3907,7 @@ fn package_version_candidates<'a>(version: &'a str, pkg: &AquaPackage) -> Vec( +fn resolved_package_version_candidates<'a>( version: &'a str, tag: Option<&'a str>, pkg: &AquaPackage, @@ -3918,9 +3915,9 @@ fn install_package_version_candidates<'a>( if let Some(tag) = tag { vec![Cow::Borrowed(tag)] } else { - // Locked HTTP package URLs do not contain a GitHub release tag. Include the - // registry's prefixes so prefix-scoped overrides still provide metadata such - // as the archive format. + // Tag lookup can miss a pinned release, and locked HTTP URLs have no release + // tag. Include registry prefixes before selecting version overrides, otherwise + // packages such as openai/codex keep their empty top-level asset and format. package_version_candidates(version, pkg) } } @@ -4630,7 +4627,7 @@ packages: let request = ToolRequest::new(backend, "3.16.1.0", crate::toolset::ToolSource::Unknown).unwrap(); let tv = ToolVersion::new(request, "3.16.1.0".to_string()); - let versions = install_package_version_candidates("3.16.1.0", None, &pkg); + let versions = resolved_package_version_candidates("3.16.1.0", None, &pkg); let versions = versions.iter().map(|v| v.as_ref()).collect_vec(); let pkg = AquaBackend::package_with_options_for_pkg(&tv, pkg, &versions).unwrap(); @@ -4638,6 +4635,71 @@ packages: assert_eq!(pkg.format, "tar.xz"); } + #[test] + fn test_resolved_package_without_tag_selects_codex_windows_archive() { + let pkg: AquaPackage = serde_yaml::from_str( + r#" +type: github_release +repo_owner: openai +repo_name: codex +version_prefix: rust- +version_constraint: "false" +version_overrides: + - version_constraint: semver("<= 0.133.0-alpha.1") + asset: codex-{{.Arch}}-{{.OS}}.exe.{{.Format}} + format: zst + - version_constraint: "true" + asset: codex-package-{{.Arch}}-{{.OS}}.{{.Format}} + format: tar.gz + files: + - name: codex + src: bin/codex + replacements: + amd64: x86_64 + windows: pc-windows-msvc +"#, + ) + .unwrap(); + + // A pinned release may be absent from GitHub's release listing. Its registry + // prefix must still be considered before applying the version overrides. + let versions = resolved_package_version_candidates("0.154.0", None, &pkg); + let versions = versions.iter().map(|v| v.as_ref()).collect_vec(); + let pkg = pkg.with_version(&versions, "windows", "amd64"); + + assert_eq!( + pkg.asset("rust-v0.154.0", "windows", "amd64").unwrap(), + "codex-package-x86_64-pc-windows-msvc.tar.gz" + ); + assert_eq!(pkg.format, "tar.gz"); + assert_eq!(pkg.files[0].src.as_deref(), Some("bin/codex")); + } + + #[test] + fn test_resolved_package_known_tag_preserves_override_prefix() { + let pkg: AquaPackage = serde_yaml::from_str( + r#" +version_constraint: "false" +version_overrides: + - version_constraint: "true" + version_prefix: old-v + asset: old-layout.zip + - version_constraint: "true" + version_prefix: new-v + asset: new-layout.zip +"#, + ) + .unwrap(); + let versions = resolved_package_version_candidates("1.0.0", Some("new-v1.0.0"), &pkg); + let versions = versions.iter().map(|v| v.as_ref()).collect_vec(); + let pkg = pkg.with_version(&versions, "windows", "amd64"); + + assert_eq!( + pkg.asset("new-v1.0.0", "windows", "amd64").unwrap(), + "new-layout.zip" + ); + } + #[test] fn test_candidate_bin_paths_use_v_prefixed_package_override() { let registry = ParsedRegistry::parse_yaml( From b3be66333999d8aadbe3452079e27777fcc0b5f4 Mon Sep 17 00:00:00 2001 From: Sebastian Battle Date: Fri, 9 Oct 2026 02:02:31 -0400 Subject: [PATCH 2/4] fix(lock): reject ambiguous aqua version prefixes --- ...test_lockfile_aqua_version_prefix_override | 28 +++++ src/backend/aqua.rs | 113 ++++++++++++++++-- 2 files changed, 132 insertions(+), 9 deletions(-) diff --git a/e2e/lockfile/test_lockfile_aqua_version_prefix_override b/e2e/lockfile/test_lockfile_aqua_version_prefix_override index b003cdb9912..a9e1257cb6b 100644 --- a/e2e/lockfile/test_lockfile_aqua_version_prefix_override +++ b/e2e/lockfile/test_lockfile_aqua_version_prefix_override @@ -8,6 +8,8 @@ export MISE_AQUA_BAKED_REGISTRY=0 ASSET="codex-package-x86_64-pc-windows-msvc.tar.gz" mkdir -p server/rust-v0.154.0 printf 'archive\n' >"server/rust-v0.154.0/$ASSET" +printf 'old archive\n' >server/old.zip +printf 'new archive\n' >server/new.zip start_http_server --dir server REGISTRY_DIR="$PWD/aqua-registry-local" @@ -28,6 +30,16 @@ packages: replacements: amd64: x86_64 windows: pc-windows-msvc + - type: http + name: example/ambiguous + version_constraint: "false" + version_overrides: + - version_constraint: "true" + version_prefix: old-v + url: http://127.0.0.1:$HTTP_PORT/old.zip + - version_constraint: "true" + version_prefix: new-v + url: http://127.0.0.1:$HTTP_PORT/new.zip EOF export MISE_AQUA_REGISTRY_URL="file://$REGISTRY_DIR" @@ -40,3 +52,19 @@ mise lock --platform windows-x64 assert_contains "cat mise.lock" '"platforms.windows-x64"' assert_contains "cat mise.lock" "url = \"http://127.0.0.1:$HTTP_PORT/rust-v0.154.0/$ASSET\"" + +# Both prefix families have reachable archives. A missing tag must not let +# registry order choose which one gets recorded in the lockfile. +mkdir ambiguous +cd ambiguous +cat <<'EOF' >mise.toml +[tools] +"aqua:example/ambiguous" = "1.0.0" +EOF + +output=$(mise lock aqua:example/ambiguous --platform windows-x64 2>&1) || true +assert_contains "echo '$output'" "ambiguous aqua version prefixes for 1.0.0: old-v, new-v" +if [[ -f mise.lock ]]; then + assert_not_contains "cat mise.lock" "/old.zip" + assert_not_contains "cat mise.lock" "/new.zip" +fi diff --git a/src/backend/aqua.rs b/src/backend/aqua.rs index 36c051eb707..9fc997ec0d2 100644 --- a/src/backend/aqua.rs +++ b/src/backend/aqua.rs @@ -774,14 +774,19 @@ impl Backend for AquaBackend { // Using package_with_version() here would apply overrides for the current host // platform first, which can leak host-specific overrides into cross-platform lock. let pkg = AQUA_REGISTRY.package(&self.id).await?; - let versions = resolved_package_version_candidates(&tv.version, tag.as_deref(), &pkg); - let versions = versions.iter().map(|v| v.as_ref()).collect_vec(); let mut v = tag.clone().unwrap_or_else(|| tv.version.clone()); let mut v_prefixed = (tag_is_none && !tv.version.starts_with('v')).then(|| format!("v{v}")); let raw_opts = tv.request.options(); let opts = AquaOptions::new(&raw_opts); let target_libc = Self::target_variant_libc(target, self.tool_libc); - let pkg = pkg.with_version_libc(&versions, target_os, target_arch, target_libc.as_deref()); + let pkg = lock_package_for_version( + pkg, + &tv.version, + tag.as_deref(), + target_os, + target_arch, + target_libc.as_deref(), + )?; let pkg = Self::apply_aqua_libc_replacement( pkg, target_os, @@ -3907,6 +3912,7 @@ fn package_version_candidates<'a>(version: &'a str, pkg: &AquaPackage) -> Vec( version: &'a str, tag: Option<&'a str>, @@ -3922,6 +3928,42 @@ fn resolved_package_version_candidates<'a>( } } +/// Select lock metadata only when the version candidates agree on a prefix family. +fn lock_package_for_version( + pkg: AquaPackage, + version: &str, + tag: Option<&str>, + os: &str, + arch: &str, + libc: Option<&str>, +) -> Result { + let candidates = resolved_package_version_candidates(version, tag, &pkg); + let versions = candidates.iter().map(|v| v.as_ref()).collect_vec(); + if tag.is_none() { + let prefixes = versions + .iter() + .filter(|v| pkg.version_constraint_ok(&[v])) + .map(|v| { + pkg.clone() + .with_version_libc(&[v], os, arch, libc) + .version_prefix + .unwrap_or_default() + }) + .unique() + .collect_vec(); + if prefixes.len() > 1 { + bail!( + "ambiguous aqua version prefixes for {version}: {}; no exact release tag was found", + prefixes + .iter() + .map(|prefix| if prefix.is_empty() { "(none)" } else { prefix }) + .join(", ") + ); + } + } + Ok(pkg.with_version_libc(&versions, os, arch, libc)) +} + fn github_release_tag_from_url(url: &str) -> Option { let url = Url::parse(url).ok()?; if url.host_str()? != "github.com" { @@ -4663,9 +4705,7 @@ version_overrides: // A pinned release may be absent from GitHub's release listing. Its registry // prefix must still be considered before applying the version overrides. - let versions = resolved_package_version_candidates("0.154.0", None, &pkg); - let versions = versions.iter().map(|v| v.as_ref()).collect_vec(); - let pkg = pkg.with_version(&versions, "windows", "amd64"); + let pkg = lock_package_for_version(pkg, "0.154.0", None, "windows", "amd64", None).unwrap(); assert_eq!( pkg.asset("rust-v0.154.0", "windows", "amd64").unwrap(), @@ -4690,9 +4730,14 @@ version_overrides: "#, ) .unwrap(); - let versions = resolved_package_version_candidates("1.0.0", Some("new-v1.0.0"), &pkg); - let versions = versions.iter().map(|v| v.as_ref()).collect_vec(); - let pkg = pkg.with_version(&versions, "windows", "amd64"); + let err = lock_package_for_version(pkg.clone(), "1.0.0", None, "windows", "amd64", None) + .unwrap_err(); + assert!(err.to_string().contains("ambiguous aqua version prefixes")); + assert!(err.to_string().contains("old-v, new-v")); + + let pkg = + lock_package_for_version(pkg, "1.0.0", Some("new-v1.0.0"), "windows", "amd64", None) + .unwrap(); assert_eq!( pkg.asset("new-v1.0.0", "windows", "amd64").unwrap(), @@ -4700,6 +4745,56 @@ version_overrides: ); } + #[test] + fn test_lock_package_prefixes_with_disjoint_constraints() { + let pkg: AquaPackage = serde_yaml::from_str( + r#" +version_constraint: "false" +version_overrides: + - version_constraint: semver("< 1.0.0") + version_prefix: old-v + asset: old-layout.zip + - version_constraint: semver(">= 1.0.0") + version_prefix: new-v + asset: new-layout.zip +"#, + ) + .unwrap(); + + for (version, expected) in [("0.9.0", "old-layout.zip"), ("1.0.0", "new-layout.zip")] { + let resolved = + lock_package_for_version(pkg.clone(), version, None, "windows", "amd64", None) + .unwrap(); + assert_eq!( + resolved.asset(version, "windows", "amd64").unwrap(), + expected + ); + } + } + + #[test] + fn test_lock_package_ignores_inactive_override_prefixes() { + let pkg: AquaPackage = serde_yaml::from_str( + r#" +asset: default.zip +version_overrides: + - version_constraint: "true" + version_prefix: old-v + asset: old-layout.zip + - version_constraint: "true" + version_prefix: new-v + asset: new-layout.zip +"#, + ) + .unwrap(); + + let pkg = lock_package_for_version(pkg, "1.0.0", None, "windows", "amd64", None).unwrap(); + assert_eq!( + pkg.asset("1.0.0", "windows", "amd64").unwrap(), + "default.zip" + ); + } + #[test] fn test_candidate_bin_paths_use_v_prefixed_package_override() { let registry = ParsedRegistry::parse_yaml( From 134c2c9b3f618eeb841ffdb441fd310043595356 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:31:16 +0000 Subject: [PATCH 3/4] refactor(lock): skip prefix ambiguity pass without override prefixes Avoid cloning the package per candidate when no version override sets a version_prefix, split the ambiguity unit test from the known-tag test, tighten the error text, and simplify the e2e ambiguity assertion. Co-Authored-By: Claude Sonnet 5.5 Entire-Checkpoint: 01M4G6X1ZQAGAFT759VTJYFF4T --- ...test_lockfile_aqua_version_prefix_override | 3 +- src/backend/aqua.rs | 36 +++++++++++++++---- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/e2e/lockfile/test_lockfile_aqua_version_prefix_override b/e2e/lockfile/test_lockfile_aqua_version_prefix_override index a9e1257cb6b..a51eef31bd4 100644 --- a/e2e/lockfile/test_lockfile_aqua_version_prefix_override +++ b/e2e/lockfile/test_lockfile_aqua_version_prefix_override @@ -62,8 +62,7 @@ cat <<'EOF' >mise.toml "aqua:example/ambiguous" = "1.0.0" EOF -output=$(mise lock aqua:example/ambiguous --platform windows-x64 2>&1) || true -assert_contains "echo '$output'" "ambiguous aqua version prefixes for 1.0.0: old-v, new-v" +assert_contains "mise lock aqua:example/ambiguous --platform windows-x64 2>&1" "ambiguous aqua version prefixes for 1.0.0: old-v, new-v" if [[ -f mise.lock ]]; then assert_not_contains "cat mise.lock" "/old.zip" assert_not_contains "cat mise.lock" "/new.zip" diff --git a/src/backend/aqua.rs b/src/backend/aqua.rs index 9fc997ec0d2..7d9cbf6775a 100644 --- a/src/backend/aqua.rs +++ b/src/backend/aqua.rs @@ -3939,7 +3939,13 @@ fn lock_package_for_version( ) -> Result { let candidates = resolved_package_version_candidates(version, tag, &pkg); let versions = candidates.iter().map(|v| v.as_ref()).collect_vec(); - if tag.is_none() { + // Without a prefix on any override every candidate resolves to the root prefix, + // so there is nothing to disambiguate and no reason to clone the package. + let has_override_prefix = pkg + .version_overrides + .iter() + .any(|vo| vo.version_prefix.is_some()); + if tag.is_none() && has_override_prefix { let prefixes = versions .iter() .filter(|v| pkg.version_constraint_ok(&[v])) @@ -3953,7 +3959,7 @@ fn lock_package_for_version( .collect_vec(); if prefixes.len() > 1 { bail!( - "ambiguous aqua version prefixes for {version}: {}; no exact release tag was found", + "ambiguous aqua version prefixes for {version}: {}; no exact release tag matched", prefixes .iter() .map(|prefix| if prefix.is_empty() { "(none)" } else { prefix }) @@ -4730,11 +4736,6 @@ version_overrides: "#, ) .unwrap(); - let err = lock_package_for_version(pkg.clone(), "1.0.0", None, "windows", "amd64", None) - .unwrap_err(); - assert!(err.to_string().contains("ambiguous aqua version prefixes")); - assert!(err.to_string().contains("old-v, new-v")); - let pkg = lock_package_for_version(pkg, "1.0.0", Some("new-v1.0.0"), "windows", "amd64", None) .unwrap(); @@ -4745,6 +4746,27 @@ version_overrides: ); } + #[test] + fn test_lock_package_without_tag_rejects_ambiguous_override_prefixes() { + let pkg: AquaPackage = serde_yaml::from_str( + r#" +version_constraint: "false" +version_overrides: + - version_constraint: "true" + version_prefix: old-v + asset: old-layout.zip + - version_constraint: "true" + version_prefix: new-v + asset: new-layout.zip +"#, + ) + .unwrap(); + let err = + lock_package_for_version(pkg, "1.0.0", None, "windows", "amd64", None).unwrap_err(); + assert!(err.to_string().contains("ambiguous aqua version prefixes")); + assert!(err.to_string().contains("old-v, new-v")); + } + #[test] fn test_lock_package_prefixes_with_disjoint_constraints() { let pkg: AquaPackage = serde_yaml::from_str( From 6ccff574f75d9503ce5e692a2d0715bdf94e8594 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:41:31 +0000 Subject: [PATCH 4/4] test(lock): note that a skipped platform leaves mise lock exiting 0 Co-Authored-By: Claude Sonnet 5.5 Entire-Checkpoint: 01M4G7FSS9W3EFPFAH6WAM7ESE --- e2e/lockfile/test_lockfile_aqua_version_prefix_override | 1 + 1 file changed, 1 insertion(+) diff --git a/e2e/lockfile/test_lockfile_aqua_version_prefix_override b/e2e/lockfile/test_lockfile_aqua_version_prefix_override index a51eef31bd4..17a116ce69b 100644 --- a/e2e/lockfile/test_lockfile_aqua_version_prefix_override +++ b/e2e/lockfile/test_lockfile_aqua_version_prefix_override @@ -62,6 +62,7 @@ cat <<'EOF' >mise.toml "aqua:example/ambiguous" = "1.0.0" EOF +# A platform that cannot resolve is reported and skipped; mise lock itself exits 0. assert_contains "mise lock aqua:example/ambiguous --platform windows-x64 2>&1" "ambiguous aqua version prefixes for 1.0.0: old-v, new-v" if [[ -f mise.lock ]]; then assert_not_contains "cat mise.lock" "/old.zip"