aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-03 22:30:16 +0900
committernsfisis <nsfisis@gmail.com>2026-05-03 22:30:16 +0900
commit94d217dc9a6a23b6bcd695b776a34ac0db0ce539 (patch)
treeb390530d0b4dd1374c2a5b1d2659adc4a9ade3db
parent2a680f571eae31737525ef96105e929ae420b061 (diff)
downloadphp-mozart-94d217dc9a6a23b6bcd695b776a34ac0db0ce539.tar.gz
php-mozart-94d217dc9a6a23b6bcd695b776a34ac0db0ce539.tar.zst
php-mozart-94d217dc9a6a23b6bcd695b776a34ac0db0ce539.zip
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`.
-rw-r--r--crates/mozart/src/commands/install.rs70
-rw-r--r--crates/mozart/tests/installer.rs2
2 files changed, 71 insertions, 1 deletions
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<String> {
+ 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);