From 94d217dc9a6a23b6bcd695b776a34ac0db0ce539 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 3 May 2026 22:30:16 +0900 Subject: fix(install): reject lock when a locked dep's require excludes the root Mozart's install verification didn't surface the slice of Composer's SAT-verify failure where a locked package's `require` targets the current root by name but the root's `version` no longer satisfies the declared constraint (e.g. lock has `b/requirer` requiring `root/pkg ^1`, root composer.json now ships `2.x-dev`). The install ran package operations against a lock that Composer would have rejected with exit-code 2. Add a targeted check that walks each locked package's requires, looks for ones aimed at the root's name, and fails with the same "found root/pkg[X.x-dev] but it does not match the constraint" pointer Composer prints from `Problem::getPrettyString`. --- crates/mozart/src/commands/install.rs | 70 +++++++++++++++++++++++++++++++++++ crates/mozart/tests/installer.rs | 2 +- 2 files changed, 71 insertions(+), 1 deletion(-) (limited to 'crates') diff --git a/crates/mozart/src/commands/install.rs b/crates/mozart/src/commands/install.rs index e34b0b8..ba9bd8a 100644 --- a/crates/mozart/src/commands/install.rs +++ b/crates/mozart/src/commands/install.rs @@ -731,6 +731,60 @@ fn collect_install_same_name_problems(lock: &lockfile::LockFile, dev_mode: bool) problems } +/// Detect locked-package requires whose target is the current root but +/// whose version constraint no longer matches the root's declared version. +/// Mirrors the slice of Composer's `Installer::doInstall` SAT verify that +/// surfaces messages like +/// `"b/requirer 1.0.0 requires root/pkg ^1 -> found root/pkg[2.x-dev] but +/// it does not match the constraint"`: when the user bumps the root's +/// `version` (or its branch alias) past the range a locked dependent +/// expects, the lock can't be installed as-is and the resolver-equivalent +/// must bail with exit-code 2 before any package operations run. +fn collect_install_root_require_problems( + lock: &lockfile::LockFile, + root: &mozart_core::package::RawPackageData, + dev_mode: bool, +) -> Vec { + use mozart_semver::{Version, VersionConstraint}; + + let Some(root_version) = root.version.as_deref() else { + return Vec::new(); + }; + if root.name.is_empty() || root_version.is_empty() { + return Vec::new(); + } + let root_name_lower = root.name.to_lowercase(); + let Ok(parsed_root_version) = Version::parse(root_version) else { + return Vec::new(); + }; + + let mut all_pkgs: Vec<&lockfile::LockedPackage> = lock.packages.iter().collect(); + if dev_mode { + all_pkgs.extend(lock.packages_dev.iter().flatten()); + } + + let mut problems = Vec::new(); + for &p in &all_pkgs { + for (target, constraint_str) in &p.require { + if target.to_lowercase() != root_name_lower { + continue; + } + let Ok(constraint) = VersionConstraint::parse(constraint_str) else { + continue; + }; + if constraint.matches(&parsed_root_version) { + continue; + } + problems.push(format!( + "- {pkg_name} is locked to version {pkg_version} and an update of this package was not requested.\n - {pkg_name} {pkg_version} requires {target} {constraint_str} -> found {target}[{root_version}] but it does not match the constraint.", + pkg_name = p.name, + pkg_version = p.version, + )); + } + } + problems +} + /// Detect declared `conflict` clashes between two packages already in the /// lock. Mirrors what Composer's `Installer::doInstall` SAT verify catches /// when one locked package conflicts with another locked package's version @@ -1463,6 +1517,22 @@ pub async fn run( mozart_core::exit_code::DEPENDENCY_RESOLUTION_FAILED, )); } + + let root_require_problems = + collect_install_root_require_problems(&lock, &root_pkg, dev_mode); + if !root_require_problems.is_empty() { + console.info( + "Your lock file does not contain a compatible set of packages. Please run composer update.", + ); + console.info(""); + for (i, msg) in root_require_problems.iter().enumerate() { + console.info(&format!(" Problem {}", i + 1)); + console.info(&format!(" {msg}")); + } + return Err(mozart_core::exit_code::bail_silent( + mozart_core::exit_code::DEPENDENCY_RESOLUTION_FAILED, + )); + } } // Step 6: Determine if prefer-source is enabled diff --git a/crates/mozart/tests/installer.rs b/crates/mozart/tests/installer.rs index 0759177..189a7c9 100644 --- a/crates/mozart/tests/installer.rs +++ b/crates/mozart/tests/installer.rs @@ -336,7 +336,7 @@ installer_fixture!(repositories_priorities2); installer_fixture!(repositories_priorities3); installer_fixture!(repositories_priorities4); installer_fixture!(repositories_priorities5); -installer_fixture!(root_alias_change_with_circular_dep, ignore); +installer_fixture!(root_alias_change_with_circular_dep); installer_fixture!(root_alias_gets_loaded_for_locked_pkgs); installer_fixture!(root_requirements_do_not_affect_locked_versions); installer_fixture!(solver_problem_with_hash_in_branch); -- cgit v1.3.1