aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-02 22:21:25 +0900
committernsfisis <nsfisis@gmail.com>2026-05-02 22:21:25 +0900
commit8da98493daf5013585e07ec98ca6960a42924edf (patch)
treebe57603fec29a4bf1e5f546b1ba2e14778595cb3 /crates/mozart
parent804b5b9a2a7759af24e41408c82dfc60c6092cf3 (diff)
downloadphp-mozart-8da98493daf5013585e07ec98ca6960a42924edf.tar.gz
php-mozart-8da98493daf5013585e07ec98ca6960a42924edf.tar.zst
php-mozart-8da98493daf5013585e07ec98ca6960a42924edf.zip
feat(resolver): add branch-alias support across the resolution pipeline
Plumb Composer's `extra.branch-alias` mechanism end-to-end so a dev branch (e.g. `dev-foobar`) can be installed alongside its numeric alias (e.g. `3.2.x-dev`) and resolve constraints written against the alias target. Concretely: - `mozart-semver`: stop treating pure-numeric `-dev` as a wildcard branch — `3.2.9999999.9999999-dev` (the form `normalizeBranch` emits) now parses as a classical version with `is_dev_branch=false`, so constraints like `3.2.*` match it. - `mozart-registry/composer_repo`: load `type: composer` repositories from `file://` URLs (legacy embedded `packages.json`). - `mozart-registry/resolver`: emit pool entries in pairs for dev branches with `extra.branch-alias`, link them via `is_alias_of`, and apply `@dev`/`@beta` etc. stability suffix flags from root requires. - `mozart-sat-resolver`: alias rules (`PackageAlias` / `PackageInverseAlias`) so alias and target install together; alias packages skipped from same-name conflict indexing. - `mozart-sat-resolver/policy`: `DefaultPolicy` now honors `prefer_stable` via Composer's stability-tier comparison. - `mozart-registry/lockfile`: split resolved set into real packages vs. alias entries; populate the `aliases[]` block. - `mozart-registry/installer_executor`: new `MarkAliasInstalled` operation; `format_full_pretty_version` mirroring `BasePackage::getFullPrettyVersion` (appends source ref[0..7] for dev/git packages). - Test harness rewrites fixture-relative `file://` URLs to absolute paths. Newly green fixtures: `install_branch_alias_composer_repo`, `alias_solver_problems`, `alias_solver_problems2`, `conflict_with_all_dependencies_option_dont_recommend_to_use_it`, `unbounded_conflict_does_not_match_default_branch_with_branch_alias`, `unbounded_conflict_does_not_match_default_branch_with_numeric_branch`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart')
-rw-r--r--crates/mozart/src/commands/install.rs16
-rw-r--r--crates/mozart/src/commands/update.rs1
-rw-r--r--crates/mozart/tests/installer.rs67
3 files changed, 67 insertions, 17 deletions
diff --git a/crates/mozart/src/commands/install.rs b/crates/mozart/src/commands/install.rs
index 356d622..9555ba7 100644
--- a/crates/mozart/src/commands/install.rs
+++ b/crates/mozart/src/commands/install.rs
@@ -670,6 +670,22 @@ pub async fn install_from_lock(
}
};
executor.install_package(op, &exec_ctx).await?;
+
+ // After the target install/update, emit MarkAliasInstalled for any
+ // aliases whose `package`+`version` (the target's pretty version)
+ // match. Mirrors Composer's `Transaction::calculateOperations` DFS
+ // which pushes alias targets first and emits MarkAliasInstalled
+ // when the alias itself is processed.
+ for alias in &lock.aliases {
+ if alias.package.eq_ignore_ascii_case(&pkg.name) && alias.version == pkg.version {
+ executor
+ .install_package(
+ PackageOperation::MarkAliasInstalled { alias, target: pkg },
+ &exec_ctx,
+ )
+ .await?;
+ }
+ }
}
// Step 8: Write updated vendor/composer/installed.json (unless download_only)
diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs
index 9ac2664..847ccf7 100644
--- a/crates/mozart/src/commands/update.rs
+++ b/crates/mozart/src/commands/update.rs
@@ -1370,6 +1370,7 @@ mod tests {
version: version.to_string(),
version_normalized: format!("{}.0", version),
is_dev: false,
+ alias_of_normalized: None,
}
}
diff --git a/crates/mozart/tests/installer.rs b/crates/mozart/tests/installer.rs
index 173418a..d674485 100644
--- a/crates/mozart/tests/installer.rs
+++ b/crates/mozart/tests/installer.rs
@@ -25,6 +25,47 @@ fn fixtures_dir() -> PathBuf {
.join("../../composer/tests/Composer/Test/Fixtures/installer")
}
+/// Rewrite `file://foobar` URLs in COMPOSER content to absolute fixture
+/// paths. Mirrors `composer/tests/Composer/Test/InstallerTest.php:540-542`:
+/// when a fixture's repository entry uses a relative `file://` URL, anchor
+/// it to the fixtures directory so the on-disk `packages.json` is reachable.
+fn rewrite_fixture_file_urls(input: &str) -> String {
+ let fixtures = fixtures_dir();
+ let canonical = fixtures
+ .canonicalize()
+ .unwrap_or(fixtures)
+ .display()
+ .to_string()
+ .replace('\\', "/");
+ // Match `"file://X"` where X does not start with `/` — those are the
+ // fixture-relative form. Absolute URLs (`file:///abs/...`) are passed
+ // through.
+ let mut out = String::with_capacity(input.len());
+ let mut rest = input;
+ while let Some(idx) = rest.find("file://") {
+ out.push_str(&rest[..idx]);
+ let after = &rest[idx + "file://".len()..];
+ let first_byte = after.as_bytes().first().copied();
+ if first_byte == Some(b'/') {
+ out.push_str("file://");
+ rest = after;
+ continue;
+ }
+ // Read the rest of the URL until a `"` or whitespace.
+ let end = after
+ .find(|c: char| c == '"' || c.is_whitespace())
+ .unwrap_or(after.len());
+ let target = &after[..end];
+ out.push_str("file://");
+ out.push_str(&canonical);
+ out.push('/');
+ out.push_str(target);
+ rest = &after[end..];
+ }
+ out.push_str(rest);
+ out
+}
+
struct InProcessRunResult {
_working_dir: TempDir,
trace: Vec<String>,
@@ -37,7 +78,8 @@ async fn run_fixture_in_process(test: &ParsedTest) -> anyhow::Result<InProcessRu
let working_dir = TempDir::new()?;
let root = working_dir.path();
- std::fs::write(root.join("composer.json"), &test.composer)?;
+ let composer_json = rewrite_fixture_file_urls(&test.composer);
+ std::fs::write(root.join("composer.json"), &composer_json)?;
if let Some(lock) = &test.lock {
std::fs::write(root.join("composer.lock"), lock)?;
}
@@ -178,8 +220,8 @@ installer_fixture!(alias_in_complex_constraints, ignore);
installer_fixture!(alias_in_lock, ignore);
installer_fixture!(alias_in_lock2, ignore);
installer_fixture!(alias_on_unloadable_package, ignore);
-installer_fixture!(alias_solver_problems, ignore);
-installer_fixture!(alias_solver_problems2, ignore);
+installer_fixture!(alias_solver_problems);
+installer_fixture!(alias_solver_problems2);
installer_fixture!(alias_with_reference, ignore);
installer_fixture!(aliased_priority, ignore);
installer_fixture!(aliased_priority_conflicting, ignore);
@@ -203,10 +245,7 @@ installer_fixture!(
installer_fixture!(conflict_with_alias_in_lock_does_prevents_install, ignore);
installer_fixture!(conflict_with_alias_prevents_update, ignore);
installer_fixture!(conflict_with_alias_prevents_update_if_not_required, ignore);
-installer_fixture!(
- conflict_with_all_dependencies_option_dont_recommend_to_use_it,
- ignore
-);
+installer_fixture!(conflict_with_all_dependencies_option_dont_recommend_to_use_it);
installer_fixture!(deduplicate_solver_problems);
installer_fixture!(disjunctive_multi_constraints);
installer_fixture!(full_update_minimal_changes, ignore);
@@ -220,7 +259,7 @@ installer_fixture!(github_issues_9012, ignore);
installer_fixture!(github_issues_9290, ignore);
installer_fixture!(hint_main_rename, ignore);
installer_fixture!(install_aliased_alias, ignore);
-installer_fixture!(install_branch_alias_composer_repo, ignore);
+installer_fixture!(install_branch_alias_composer_repo);
installer_fixture!(install_dev);
installer_fixture!(install_dev_using_dist, ignore);
installer_fixture!(install_forces_reinstall_if_abandon_changes, ignore);
@@ -320,14 +359,8 @@ installer_fixture!(suggest_prod);
installer_fixture!(suggest_prod_nolock);
installer_fixture!(suggest_replaced);
installer_fixture!(suggest_uninstalled);
-installer_fixture!(
- unbounded_conflict_does_not_match_default_branch_with_branch_alias,
- ignore
-);
-installer_fixture!(
- unbounded_conflict_does_not_match_default_branch_with_numeric_branch,
- ignore
-);
+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, ignore);
installer_fixture!(
update_abandoned_package_required_but_blocked_via_audit_config,
@@ -369,7 +402,7 @@ installer_fixture!(update_ignore_platform_package_requirement_list);
installer_fixture!(update_ignore_platform_package_requirement_list_upper_bounds);
installer_fixture!(update_ignore_platform_package_requirement_wildcard);
installer_fixture!(update_ignore_platform_package_requirements);
-installer_fixture!(update_installed_alias, ignore);
+installer_fixture!(update_installed_alias);
installer_fixture!(update_installed_alias_dry_run);
installer_fixture!(update_installed_reference, ignore);
installer_fixture!(update_installed_reference_dry_run);