From d554b62e1b578a88b796f34e6eb82b5c452cd785 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 3 May 2026 19:28:56 +0900 Subject: feat(resolver): honour audit.block-abandoned config Read `config.audit.block-abandoned` from composer.json (defaults to false) and propagate it to the resolver. When set, the pool builder skips packages whose `abandoned` field is truthy (`true` or a non-empty replacement string), matching `SecurityAdvisoryPoolFilter`'s behavior in `Composer\DependencyResolver`. With no candidates left, a root require that only matches abandoned versions fails resolution with exit 2. --- crates/mozart-registry/src/lockfile.rs | 3 +++ crates/mozart-registry/src/packagist.rs | 7 ++++++ crates/mozart-registry/src/resolver.rs | 33 ++++++++++++++++++++++++++++ crates/mozart-registry/src/vcs_bridge.rs | 1 + crates/mozart-registry/src/version.rs | 1 + crates/mozart/src/commands/create_project.rs | 1 + crates/mozart/src/commands/remove.rs | 4 ++++ crates/mozart/src/commands/require.rs | 3 +++ crates/mozart/src/commands/update.rs | 13 +++++++++++ crates/mozart/tests/installer.rs | 5 +---- 10 files changed, 67 insertions(+), 4 deletions(-) diff --git a/crates/mozart-registry/src/lockfile.rs b/crates/mozart-registry/src/lockfile.rs index 3fc8fad..5e07e9d 100644 --- a/crates/mozart-registry/src/lockfile.rs +++ b/crates/mozart-registry/src/lockfile.rs @@ -1092,6 +1092,7 @@ mod tests { extra: Some(serde_json::json!({"branch-alias": {"dev-main": "1.0.x-dev"}})), notification_url: Some("https://packagist.org/downloads/".to_string()), default_branch: false, + abandoned: None, } } @@ -1167,6 +1168,7 @@ mod tests { extra: None, notification_url: None, default_branch: false, + abandoned: None, }; let locked = packagist_version_to_locked_package("vendor/pkg", &pv); @@ -1506,6 +1508,7 @@ mod tests { root_conflict: IndexMap::new(), locked_package_names: IndexSet::new(), locked_packages: Vec::new(), + block_abandoned: false, }; let resolved = resolve(&resolve_request) diff --git a/crates/mozart-registry/src/packagist.rs b/crates/mozart-registry/src/packagist.rs index 1d9356d..6b24589 100644 --- a/crates/mozart-registry/src/packagist.rs +++ b/crates/mozart-registry/src/packagist.rs @@ -135,6 +135,13 @@ pub struct PackagistVersion { /// `crate::resolver::packagist_to_pool_inputs`. #[serde(rename = "default-branch", default)] pub default_branch: bool, + + /// Abandonment marker. Composer accepts `abandoned: true` (no replacement + /// suggested) or `abandoned: ""`. Anything else + /// (absent, `false`, empty string) means the package is active. Mirrors + /// `Composer\Package\CompletePackage::isAbandoned`. + #[serde(default, deserialize_with = "deserialize_unset_as_none")] + pub abandoned: Option, } impl PackagistVersion { diff --git a/crates/mozart-registry/src/resolver.rs b/crates/mozart-registry/src/resolver.rs index e6f3c86..0716246 100644 --- a/crates/mozart-registry/src/resolver.rs +++ b/crates/mozart-registry/src/resolver.rs @@ -483,6 +483,19 @@ fn should_skip_platform_dep( // Packagist → PoolPackageInput conversion // ───────────────────────────────────────────────────────────────────────────── +/// Mirrors `Composer\Package\CompletePackage::isAbandoned`: any +/// `abandoned: true` or `abandoned: ""` value is truthy. +/// `abandoned: false` and an empty string both register as not-abandoned. +fn is_abandoned(pv: &packagist::PackagistVersion) -> bool { + match &pv.abandoned { + None => false, + Some(serde_json::Value::Null) => false, + Some(serde_json::Value::Bool(b)) => *b, + Some(serde_json::Value::String(s)) => !s.is_empty(), + Some(_) => true, + } +} + /// Convert a Packagist version entry to PoolPackageInput(s). /// May return multiple entries if branch aliases are present. fn packagist_to_pool_inputs( @@ -705,6 +718,13 @@ pub struct ResolveRequest { /// version (whether directly or via another package's `replace`). Empty /// for installs and full updates. pub locked_packages: Vec, + /// When true, drop abandoned packages (`abandoned: true|`) + /// from the pool before solving. Mirrors Composer's + /// `audit.block-abandoned` config feeding into + /// `SecurityAdvisoryPoolFilter`: the resolver simply never sees these + /// versions, so a root requirement that only matches abandoned candidates + /// fails with the standard "could not be resolved" error. + pub block_abandoned: bool, } /// Full data for a lock-pinned package, used in partial updates. Carried on @@ -1026,6 +1046,9 @@ pub async fn resolve(request: &ResolveRequest) -> Result, R return false; }; for ipkg in packages { + if request.block_abandoned && is_abandoned(&ipkg.version) { + continue; + } let inputs = packagist_to_pool_inputs( &ipkg.name, &ipkg.version, @@ -1052,6 +1075,9 @@ pub async fn resolve(request: &ResolveRequest) -> Result, R let mut composer_repo_names: IndexSet = IndexSet::new(); for cpkg in &composer_repo_packages { composer_repo_names.insert(cpkg.name.clone()); + if request.block_abandoned && is_abandoned(&cpkg.version) { + continue; + } let inputs = packagist_to_pool_inputs( &cpkg.name, &cpkg.version, @@ -1098,6 +1124,9 @@ pub async fn resolve(request: &ResolveRequest) -> Result, R .await .map_err(|e| ResolveError::DependencyFetchError(e.to_string()))?; for r in &seed_results { + if request.block_abandoned && is_abandoned(&r.version) { + continue; + } let inputs = packagist_to_pool_inputs( &r.name, &r.version, @@ -1143,6 +1172,9 @@ pub async fn resolve(request: &ResolveRequest) -> Result, R } }; for r in &results { + if request.block_abandoned && is_abandoned(&r.version) { + continue; + } let inputs = packagist_to_pool_inputs( &r.name, &r.version, @@ -1705,6 +1737,7 @@ mod tests { root_conflict: IndexMap::new(), locked_package_names: IndexSet::new(), locked_packages: Vec::new(), + block_abandoned: false, }; let result = resolve(&request).await; diff --git a/crates/mozart-registry/src/vcs_bridge.rs b/crates/mozart-registry/src/vcs_bridge.rs index e9a2f37..aae3d87 100644 --- a/crates/mozart-registry/src/vcs_bridge.rs +++ b/crates/mozart-registry/src/vcs_bridge.rs @@ -188,6 +188,7 @@ pub fn vcs_to_packagist_version(vpkg: &VcsPackageVersion) -> PackagistVersion { extra: vpkg.composer_json.get("extra").cloned(), notification_url: None, default_branch: vpkg.is_default_branch, + abandoned: vpkg.composer_json.get("abandoned").cloned(), } } diff --git a/crates/mozart-registry/src/version.rs b/crates/mozart-registry/src/version.rs index ba120fa..9a7c6e6 100644 --- a/crates/mozart-registry/src/version.rs +++ b/crates/mozart-registry/src/version.rs @@ -199,6 +199,7 @@ mod tests { extra: None, notification_url: None, default_branch: false, + abandoned: None, } } diff --git a/crates/mozart/src/commands/create_project.rs b/crates/mozart/src/commands/create_project.rs index ae7a550..13a2bb2 100644 --- a/crates/mozart/src/commands/create_project.rs +++ b/crates/mozart/src/commands/create_project.rs @@ -442,6 +442,7 @@ pub async fn execute( .collect(), locked_package_names: indexmap::IndexSet::new(), locked_packages: Vec::new(), + block_abandoned: false, }; console.info("Resolving dependencies..."); diff --git a/crates/mozart/src/commands/remove.rs b/crates/mozart/src/commands/remove.rs index 9c5f7fa..dc20a21 100644 --- a/crates/mozart/src/commands/remove.rs +++ b/crates/mozart/src/commands/remove.rs @@ -276,6 +276,7 @@ pub async fn execute( .collect(), locked_package_names: indexmap::IndexSet::new(), locked_packages: Vec::new(), + block_abandoned: false, }; // Print header messages @@ -554,6 +555,7 @@ async fn remove_unused( .collect(), locked_package_names: indexmap::IndexSet::new(), locked_packages: Vec::new(), + block_abandoned: false, }; console.info("Resolving dependencies to detect unused packages..."); @@ -908,6 +910,7 @@ mod tests { root_conflict: IndexMap::new(), locked_package_names: IndexSet::new(), locked_packages: Vec::new(), + block_abandoned: false, }; let resolved = resolve(&request) .await @@ -965,6 +968,7 @@ mod tests { root_conflict: IndexMap::new(), locked_package_names: IndexSet::new(), locked_packages: Vec::new(), + block_abandoned: false, }; let resolved2 = resolve(&request2) .await diff --git a/crates/mozart/src/commands/require.rs b/crates/mozart/src/commands/require.rs index caf88c1..24812fc 100644 --- a/crates/mozart/src/commands/require.rs +++ b/crates/mozart/src/commands/require.rs @@ -664,6 +664,7 @@ pub async fn execute( .collect(), locked_package_names: indexmap::IndexSet::new(), locked_packages: Vec::new(), + block_abandoned: false, }; // Print header messages @@ -1066,6 +1067,7 @@ mod tests { root_conflict: IndexMap::new(), locked_package_names: IndexSet::new(), locked_packages: Vec::new(), + block_abandoned: false, }; let resolved = resolver::resolve(&request) @@ -1141,6 +1143,7 @@ mod tests { root_conflict: IndexMap::new(), locked_package_names: IndexSet::new(), locked_packages: Vec::new(), + block_abandoned: false, }; let resolved = resolver::resolve(&request) diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs index 6003dd0..0d7d60e 100644 --- a/crates/mozart/src/commands/update.rs +++ b/crates/mozart/src/commands/update.rs @@ -1009,6 +1009,17 @@ pub async fn run( platform.apply_overrides(overrides); } + // Mirrors `Composer\Advisory\AuditConfig::fromConfig`: read + // `config.audit.block-abandoned` straight off composer.json. Defaults to + // false; when true the resolver drops abandoned packages from the pool. + let block_abandoned = composer_json + .extra_fields + .get("config") + .and_then(|c| c.get("audit")) + .and_then(|a| a.get("block-abandoned")) + .and_then(|v| v.as_bool()) + .unwrap_or(false); + let request = ResolveRequest { root_name: composer_json.name.clone(), root_version: composer_json.version.clone(), @@ -1042,6 +1053,7 @@ pub async fn run( .collect(), locked_package_names, locked_packages, + block_abandoned, }; // Step 6: Print header and run resolver @@ -2168,6 +2180,7 @@ mod tests { root_conflict: IndexMap::new(), locked_package_names: IndexSet::new(), locked_packages: Vec::new(), + block_abandoned: false, }; let resolved = resolve(&request).await.expect("Resolution should succeed"); diff --git a/crates/mozart/tests/installer.rs b/crates/mozart/tests/installer.rs index fd25dee..6213a6c 100644 --- a/crates/mozart/tests/installer.rs +++ b/crates/mozart/tests/installer.rs @@ -350,10 +350,7 @@ installer_fixture!(suggest_uninstalled); installer_fixture!(unbounded_conflict_does_not_match_default_branch_with_branch_alias); installer_fixture!(unbounded_conflict_does_not_match_default_branch_with_numeric_branch); installer_fixture!(unbounded_conflict_matches_default_branch); -installer_fixture!( - update_abandoned_package_required_but_blocked_via_audit_config, - ignore -); +installer_fixture!(update_abandoned_package_required_but_blocked_via_audit_config); installer_fixture!(update_alias); installer_fixture!(update_alias_lock, ignore); installer_fixture!(update_alias_lock2); -- cgit v1.3.1