aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart/src/commands/install.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-21 12:50:15 +0900
committernsfisis <nsfisis@gmail.com>2026-02-21 12:50:15 +0900
commit96f94253d66eb9302855d7a6ae4534e12d818d58 (patch)
tree24340724112f439ca49c48c1cfcb138b01b623f4 /crates/mozart/src/commands/install.rs
parent70881be20ebedad2834566065444f76a67e7cc8c (diff)
downloadphp-mozart-96f94253d66eb9302855d7a6ae4534e12d818d58.tar.gz
php-mozart-96f94253d66eb9302855d7a6ae4534e12d818d58.tar.zst
php-mozart-96f94253d66eb9302855d7a6ae4534e12d818d58.zip
feat(update): implement update command with resolver-based dependency updates
Add full update command supporting --lock (content-hash refresh only), --dry-run, --no-install, --no-dev, --prefer-stable, --prefer-lowest, and partial updates (named packages). Extract install_from_lock() from install.rs for shared use. Add Stability::parse() to package.rs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart/src/commands/install.rs')
-rw-r--r--crates/mozart/src/commands/install.rs217
1 files changed, 124 insertions, 93 deletions
diff --git a/crates/mozart/src/commands/install.rs b/crates/mozart/src/commands/install.rs
index 97f42e7..c652569 100644
--- a/crates/mozart/src/commands/install.rs
+++ b/crates/mozart/src/commands/install.rs
@@ -107,7 +107,7 @@ pub struct InstallOp<'a> {
}
/// Resolve the working directory from the CLI option, falling back to cwd.
-fn resolve_working_dir(cli: &super::Cli) -> PathBuf {
+pub fn resolve_working_dir(cli: &super::Cli) -> PathBuf {
match &cli.working_dir {
Some(dir) => PathBuf::from(dir),
None => std::env::current_dir().expect("Failed to determine current directory"),
@@ -181,7 +181,7 @@ pub fn locked_to_installed_entry(
}
/// Clean up empty vendor namespace directories after removals.
-fn cleanup_empty_vendor_dirs(vendor_dir: &Path) -> anyhow::Result<()> {
+pub fn cleanup_empty_vendor_dirs(vendor_dir: &Path) -> anyhow::Result<()> {
if let Ok(entries) = std::fs::read_dir(vendor_dir) {
for entry in entries.flatten() {
let path = entry.path();
@@ -201,78 +201,28 @@ fn cleanup_empty_vendor_dirs(vendor_dir: &Path) -> anyhow::Result<()> {
Ok(())
}
-pub fn execute(args: &InstallArgs, cli: &super::Cli) -> anyhow::Result<()> {
- // Step 1: Resolve the working directory
- let working_dir = resolve_working_dir(cli);
-
- // Step 2: Validate arguments
- if !args.packages.is_empty() {
- let pkgs = args.packages.join(" ");
- eprintln!(
- "{}",
- console::error(&format!(
- "Invalid argument {pkgs}. Use \"mozart require {pkgs}\" instead to add packages to your composer.json."
- ))
- );
- std::process::exit(1);
- }
-
- if args.no_install {
- eprintln!(
- "{}",
- console::error(
- "Invalid option \"--no-install\". Use \"mozart update --no-install\" instead if you are trying to update the composer.lock file."
- )
- );
- std::process::exit(1);
- }
-
- if args.dev {
- eprintln!(
- "{}",
- console::warning(
- "The --dev option is deprecated. Dev packages are installed by default."
- )
- );
- }
-
- if args.no_suggest {
- eprintln!(
- "{}",
- console::warning("The --no-suggest option is deprecated and has no effect.")
- );
- }
-
- // Step 3: Read composer.lock
- let lock_path = working_dir.join("composer.lock");
- if !lock_path.exists() {
- eprintln!(
- "{}",
- console::warning(
- "No composer.lock file present. Run \"mozart update\" to generate one."
- )
- );
- std::process::exit(1);
- }
- let lock = lockfile::LockFile::read_from_file(&lock_path)?;
-
- // Step 4: Freshness check
- let composer_json_path = working_dir.join("composer.json");
- if composer_json_path.exists() {
- let content = std::fs::read_to_string(&composer_json_path)?;
- if !lock.is_fresh(&content) {
- eprintln!(
- "{}",
- console::warning(
- "Warning: The lock file is not up to date with the latest changes in composer.json. You may be getting outdated dependencies. It is recommended that you run `mozart update`."
- )
- );
- }
- }
-
- // Step 5: Determine which packages to install
- let dev_mode = !args.no_dev;
-
+/// Install packages from a lock file into vendor/.
+///
+/// Used by both the `install` and `update` commands.
+///
+/// This function:
+/// 1. Determines which packages to install (prod + optionally dev)
+/// 2. Reads currently installed packages
+/// 3. Computes install/update/skip/removal operations
+/// 4. Prints a summary
+/// 5. Executes downloads and removals (unless dry_run)
+/// 6. Writes vendor/composer/installed.json
+/// 7. Cleans up empty vendor directories
+/// 8. Generates the autoloader (unless no_autoloader)
+pub fn install_from_lock(
+ lock: &lockfile::LockFile,
+ working_dir: &Path,
+ vendor_dir: &Path,
+ dev_mode: bool,
+ dry_run: bool,
+ no_autoloader: bool,
+) -> anyhow::Result<()> {
+ // Step 1: Determine which packages to install
let mut packages_to_install: Vec<&lockfile::LockedPackage> = lock.packages.iter().collect();
if dev_mode && let Some(ref dev_pkgs) = lock.packages_dev {
@@ -287,16 +237,13 @@ pub fn execute(args: &InstallArgs, cli: &super::Cli) -> anyhow::Result<()> {
}
eprintln!("Verifying lock file contents can be installed on current platform.");
- // Step 6: Determine the vendor directory
- let vendor_dir = working_dir.join("vendor");
-
- // Step 7: Read currently installed packages
- let installed = installed::InstalledPackages::read(&vendor_dir)?;
+ // Step 2: Read currently installed packages
+ let installed = installed::InstalledPackages::read(vendor_dir)?;
- // Step 8: Compute install operations
+ // Step 3: Compute install operations
let (ops, removals) = compute_operations(&packages_to_install, &installed);
- // Step 9: Print operation summary
+ // Step 4: Print operation summary
let installs: Vec<_> = ops
.iter()
.filter(|(_, a)| matches!(a, Action::Install))
@@ -323,8 +270,8 @@ pub fn execute(args: &InstallArgs, cli: &super::Cli) -> anyhow::Result<()> {
);
}
- // Step 10: Execute operations (unless --dry-run)
- if args.dry_run {
+ // Step 5: Execute operations (unless dry_run)
+ if dry_run {
for (pkg, action) in &ops {
match action {
Action::Skip => {}
@@ -362,7 +309,7 @@ pub fn execute(args: &InstallArgs, cli: &super::Cli) -> anyhow::Result<()> {
&dist.url,
&dist.dist_type,
dist.shasum.as_deref(),
- &vendor_dir,
+ vendor_dir,
&pkg.name,
)?;
}
@@ -376,12 +323,12 @@ pub fn execute(args: &InstallArgs, cli: &super::Cli) -> anyhow::Result<()> {
}
}
- // Step 13: Clean up empty vendor namespace directories
+ // Step 6: Clean up empty vendor namespace directories
if !removals.is_empty() {
- cleanup_empty_vendor_dirs(&vendor_dir)?;
+ cleanup_empty_vendor_dirs(vendor_dir)?;
}
- // Step 11: Write updated vendor/composer/installed.json
+ // Step 7: Write updated vendor/composer/installed.json
let mut new_installed = installed::InstalledPackages::new();
new_installed.dev = dev_mode;
@@ -391,20 +338,20 @@ pub fn execute(args: &InstallArgs, cli: &super::Cli) -> anyhow::Result<()> {
}
for pkg in &packages_to_install {
- new_installed.upsert(locked_to_installed_entry(pkg, &vendor_dir));
+ new_installed.upsert(locked_to_installed_entry(pkg, vendor_dir));
}
- new_installed.write(&vendor_dir)?;
+ new_installed.write(vendor_dir)?;
- // Step 14: Generate autoloader (unless --no-autoloader)
- if !args.no_autoloader {
+ // Step 8: Generate autoloader (unless no_autoloader)
+ if !no_autoloader {
eprintln!("Generating autoload files");
let suffix = lock.content_hash.clone();
crate::autoload::generate(&crate::autoload::AutoloadConfig {
- project_dir: working_dir.clone(),
- vendor_dir: vendor_dir.clone(),
+ project_dir: working_dir.to_path_buf(),
+ vendor_dir: vendor_dir.to_path_buf(),
dev_mode,
suffix,
})?;
@@ -416,6 +363,90 @@ pub fn execute(args: &InstallArgs, cli: &super::Cli) -> anyhow::Result<()> {
Ok(())
}
+pub fn execute(args: &InstallArgs, cli: &super::Cli) -> anyhow::Result<()> {
+ // Step 1: Resolve the working directory
+ let working_dir = resolve_working_dir(cli);
+
+ // Step 2: Validate arguments
+ if !args.packages.is_empty() {
+ let pkgs = args.packages.join(" ");
+ eprintln!(
+ "{}",
+ console::error(&format!(
+ "Invalid argument {pkgs}. Use \"mozart require {pkgs}\" instead to add packages to your composer.json."
+ ))
+ );
+ std::process::exit(1);
+ }
+
+ if args.no_install {
+ eprintln!(
+ "{}",
+ console::error(
+ "Invalid option \"--no-install\". Use \"mozart update --no-install\" instead if you are trying to update the composer.lock file."
+ )
+ );
+ std::process::exit(1);
+ }
+
+ if args.dev {
+ eprintln!(
+ "{}",
+ console::warning(
+ "The --dev option is deprecated. Dev packages are installed by default."
+ )
+ );
+ }
+
+ if args.no_suggest {
+ eprintln!(
+ "{}",
+ console::warning("The --no-suggest option is deprecated and has no effect.")
+ );
+ }
+
+ // Step 3: Read composer.lock
+ let lock_path = working_dir.join("composer.lock");
+ if !lock_path.exists() {
+ eprintln!(
+ "{}",
+ console::warning(
+ "No composer.lock file present. Run \"mozart update\" to generate one."
+ )
+ );
+ std::process::exit(1);
+ }
+ let lock = lockfile::LockFile::read_from_file(&lock_path)?;
+
+ // Step 4: Freshness check
+ let composer_json_path = working_dir.join("composer.json");
+ if composer_json_path.exists() {
+ let content = std::fs::read_to_string(&composer_json_path)?;
+ if !lock.is_fresh(&content) {
+ eprintln!(
+ "{}",
+ console::warning(
+ "Warning: The lock file is not up to date with the latest changes in composer.json. You may be getting outdated dependencies. It is recommended that you run `mozart update`."
+ )
+ );
+ }
+ }
+
+ // Step 5: Determine dev mode and vendor directory
+ let dev_mode = !args.no_dev;
+ let vendor_dir = working_dir.join("vendor");
+
+ // Step 6: Delegate to shared install_from_lock()
+ install_from_lock(
+ &lock,
+ &working_dir,
+ &vendor_dir,
+ dev_mode,
+ args.dry_run,
+ args.no_autoloader,
+ )
+}
+
#[cfg(test)]
mod tests {
use super::*;