From 96f94253d66eb9302855d7a6ae4534e12d818d58 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 21 Feb 2026 12:50:15 +0900 Subject: 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 --- crates/mozart/src/commands/install.rs | 217 +++++++++++++++++++--------------- 1 file changed, 124 insertions(+), 93 deletions(-) (limited to 'crates/mozart/src/commands/install.rs') 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::*; -- cgit v1.3.1