aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-03 19:28:56 +0900
committernsfisis <nsfisis@gmail.com>2026-05-03 19:28:56 +0900
commitd554b62e1b578a88b796f34e6eb82b5c452cd785 (patch)
treec9bf835955c1760f12a360727c6819dc8f98a378
parent240b0dd14a607a9dfdb84bb339c87bb0effd6963 (diff)
downloadphp-mozart-d554b62e1b578a88b796f34e6eb82b5c452cd785.tar.gz
php-mozart-d554b62e1b578a88b796f34e6eb82b5c452cd785.tar.zst
php-mozart-d554b62e1b578a88b796f34e6eb82b5c452cd785.zip
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.
-rw-r--r--crates/mozart-registry/src/lockfile.rs3
-rw-r--r--crates/mozart-registry/src/packagist.rs7
-rw-r--r--crates/mozart-registry/src/resolver.rs33
-rw-r--r--crates/mozart-registry/src/vcs_bridge.rs1
-rw-r--r--crates/mozart-registry/src/version.rs1
-rw-r--r--crates/mozart/src/commands/create_project.rs1
-rw-r--r--crates/mozart/src/commands/remove.rs4
-rw-r--r--crates/mozart/src/commands/require.rs3
-rw-r--r--crates/mozart/src/commands/update.rs13
-rw-r--r--crates/mozart/tests/installer.rs5
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: "<replacement-package>"`. 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<serde_json::Value>,
}
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: "<replacement>"` 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<LockedPackageInfo>,
+ /// When true, drop abandoned packages (`abandoned: true|<replacement>`)
+ /// 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<Vec<ResolvedPackage>, 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<Vec<ResolvedPackage>, R
let mut composer_repo_names: IndexSet<String> = 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<Vec<ResolvedPackage>, 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<Vec<ResolvedPackage>, 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);