From f9671f2dcde92d5c037595d0d3f01396a8190970 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 9 May 2026 18:44:31 +0900 Subject: refactor(composer): move Composer and Factory from mozart-core to mozart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Composer needs DownloadManager (from mozart-registry), but mozart-core sits below mozart-registry in the dependency graph — adding the field would create a dependency cycle. Moving Composer and create_composer to the mozart CLI crate breaks the cycle and lets the root state container hold a DownloadManager. Co-Authored-By: Claude Sonnet 4.6 --- crates/mozart/src/commands/archive.rs | 2 +- crates/mozart/src/commands/audit.rs | 2 +- crates/mozart/src/commands/base_config.rs | 3 +- crates/mozart/src/commands/browse.rs | 2 +- crates/mozart/src/commands/bump.rs | 3 +- crates/mozart/src/commands/clear_cache.rs | 2 +- crates/mozart/src/commands/config.rs | 8 +- crates/mozart/src/commands/config_helpers.rs | 3 +- crates/mozart/src/commands/diagnose.rs | 2 +- crates/mozart/src/commands/dump_autoload.rs | 3 +- crates/mozart/src/commands/exec.rs | 2 +- crates/mozart/src/commands/fund.rs | 2 +- crates/mozart/src/commands/global.rs | 3 +- crates/mozart/src/commands/licenses.rs | 2 +- crates/mozart/src/commands/reinstall.rs | 3 +- crates/mozart/src/commands/run_script.rs | 2 +- crates/mozart/src/commands/status.rs | 29 ++- crates/mozart/src/commands/update.rs | 2 +- crates/mozart/src/commands/validate.rs | 2 +- crates/mozart/src/composer.rs | 152 +++++++++++ crates/mozart/src/factory.rs | 362 +++++++++++++++++++++++++++ crates/mozart/src/lib.rs | 3 + 22 files changed, 559 insertions(+), 35 deletions(-) create mode 100644 crates/mozart/src/composer.rs create mode 100644 crates/mozart/src/factory.rs (limited to 'crates/mozart/src') diff --git a/crates/mozart/src/commands/archive.rs b/crates/mozart/src/commands/archive.rs index a3311df..7e1697f 100644 --- a/crates/mozart/src/commands/archive.rs +++ b/crates/mozart/src/commands/archive.rs @@ -1,6 +1,6 @@ +use crate::composer::Composer; use clap::Args; use mozart_archiver::{ArchiveManager, ArchivePackage}; -use mozart_core::composer::Composer; use mozart_core::console_writeln; use mozart_core::factory::create_config; use std::borrow::Cow; diff --git a/crates/mozart/src/commands/audit.rs b/crates/mozart/src/commands/audit.rs index df0e5a9..30f0716 100644 --- a/crates/mozart/src/commands/audit.rs +++ b/crates/mozart/src/commands/audit.rs @@ -1,9 +1,9 @@ use std::path::Path; +use crate::composer::Composer; use clap::Args; use indexmap::IndexMap; use mozart_core::advisory::{AbandonedHandling, AuditConfig, AuditFormat}; -use mozart_core::composer::Composer; use mozart_registry::advisory::{AuditOptions, Auditor, PackageInfo}; use mozart_registry::cache::{Cache, build_cache_config}; use mozart_registry::repository::RepositorySet; diff --git a/crates/mozart/src/commands/base_config.rs b/crates/mozart/src/commands/base_config.rs index bfed161..c10e7e7 100644 --- a/crates/mozart/src/commands/base_config.rs +++ b/crates/mozart/src/commands/base_config.rs @@ -1,9 +1,8 @@ use std::path::PathBuf; +use mozart_core::composer::composer_home; use mozart_core::config_source::JsonConfigSource; -use super::config_helpers::composer_home; - /// Mirrors Composer's `BaseConfigCommand`: resolves the target config file path /// and enforces the `--file` ↔ `--global` mutual exclusivity. pub(crate) struct BaseConfigContext { diff --git a/crates/mozart/src/commands/browse.rs b/crates/mozart/src/commands/browse.rs index c4c957b..a8ccab3 100644 --- a/crates/mozart/src/commands/browse.rs +++ b/crates/mozart/src/commands/browse.rs @@ -1,5 +1,5 @@ +use crate::composer::Composer; use clap::Args; -use mozart_core::composer::Composer; use mozart_core::console::Console; use mozart_core::console_writeln; use mozart_core::console_writeln_error; diff --git a/crates/mozart/src/commands/bump.rs b/crates/mozart/src/commands/bump.rs index 1ed4c54..4722fe2 100644 --- a/crates/mozart/src/commands/bump.rs +++ b/crates/mozart/src/commands/bump.rs @@ -1,6 +1,7 @@ +use crate::composer::Composer; use clap::Args; use indexmap::IndexMap; -use mozart_core::composer::{Composer, LocalRepository}; +use mozart_core::composer::LocalRepository; use mozart_core::console::Console; use mozart_core::{console_writeln, console_writeln_error}; use std::collections::BTreeMap; diff --git a/crates/mozart/src/commands/clear_cache.rs b/crates/mozart/src/commands/clear_cache.rs index 8fdf665..9ee27ed 100644 --- a/crates/mozart/src/commands/clear_cache.rs +++ b/crates/mozart/src/commands/clear_cache.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, path::Path}; +use crate::composer::Composer; use clap::Args; -use mozart_core::composer::Composer; use mozart_core::console_writeln_error; use mozart_core::factory::create_config; use mozart_registry::cache::Cache; diff --git a/crates/mozart/src/commands/config.rs b/crates/mozart/src/commands/config.rs index 7227556..5012163 100644 --- a/crates/mozart/src/commands/config.rs +++ b/crates/mozart/src/commands/config.rs @@ -1,15 +1,15 @@ +use super::config_helpers::{ + add_repository, read_json_file, remove_repository, render_value, write_json_file, +}; use anyhow::anyhow; use clap::Args; +use mozart_core::composer::composer_home; use mozart_core::config::resolve_references; use mozart_core::console_writeln; use mozart_core::factory::create_config; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; -use super::config_helpers::{ - add_repository, composer_home, read_json_file, remove_repository, render_value, write_json_file, -}; - #[derive(Args)] pub struct ConfigArgs { /// Setting key diff --git a/crates/mozart/src/commands/config_helpers.rs b/crates/mozart/src/commands/config_helpers.rs index fb626f6..32aacdb 100644 --- a/crates/mozart/src/commands/config_helpers.rs +++ b/crates/mozart/src/commands/config_helpers.rs @@ -1,8 +1,7 @@ use anyhow::anyhow; +use mozart_core::composer::composer_home; use std::path::{Path, PathBuf}; -pub(crate) use mozart_core::composer::composer_home; - /// Read TLS-related options (`config.cafile`, `config.capath`) from the merged /// global + local config. Local values override global. Relative paths are /// resolved against the directory of the config file that defined them. diff --git a/crates/mozart/src/commands/diagnose.rs b/crates/mozart/src/commands/diagnose.rs index af18fdc..17a8b78 100644 --- a/crates/mozart/src/commands/diagnose.rs +++ b/crates/mozart/src/commands/diagnose.rs @@ -1,7 +1,7 @@ +use crate::composer::Composer; use clap::Args; use colored::Colorize; use mozart_core::MOZART_VERSION; -use mozart_core::composer::Composer; use mozart_core::config::Config; use mozart_core::config_validator::{ValidatorOptions, validate_manifest}; use mozart_core::console::Console; diff --git a/crates/mozart/src/commands/dump_autoload.rs b/crates/mozart/src/commands/dump_autoload.rs index 0f3366c..7557d37 100644 --- a/crates/mozart/src/commands/dump_autoload.rs +++ b/crates/mozart/src/commands/dump_autoload.rs @@ -1,6 +1,7 @@ +use crate::composer::Composer; use clap::Args; use mozart_autoload::AutoloadGeneratorExt; -use mozart_core::composer::{AutoloadDumpOptions, Composer}; +use mozart_core::composer::AutoloadDumpOptions; use mozart_core::console_writeln; #[derive(Args, Default)] diff --git a/crates/mozart/src/commands/exec.rs b/crates/mozart/src/commands/exec.rs index 27d1a8a..2b3c836 100644 --- a/crates/mozart/src/commands/exec.rs +++ b/crates/mozart/src/commands/exec.rs @@ -1,5 +1,5 @@ +use crate::composer::Composer; use clap::Args; -use mozart_core::composer::Composer; use mozart_core::console_writeln; use std::path::{Path, PathBuf}; diff --git a/crates/mozart/src/commands/fund.rs b/crates/mozart/src/commands/fund.rs index 1f334b3..90a8418 100644 --- a/crates/mozart/src/commands/fund.rs +++ b/crates/mozart/src/commands/fund.rs @@ -1,5 +1,5 @@ +use crate::composer::Composer; use clap::Args; -use mozart_core::composer::Composer; use mozart_core::console::{Console, hyperlink}; use mozart_core::console_format; use mozart_core::console_writeln; diff --git a/crates/mozart/src/commands/global.rs b/crates/mozart/src/commands/global.rs index 1a8a5f8..05e8aae 100644 --- a/crates/mozart/src/commands/global.rs +++ b/crates/mozart/src/commands/global.rs @@ -1,4 +1,5 @@ use clap::Args; +use mozart_core::composer::composer_home; #[derive(Args)] pub struct GlobalArgs { @@ -27,7 +28,7 @@ pub async fn execute( } }; - let home = super::config_helpers::composer_home(); + let home = composer_home(); fs::create_dir_all(&home)?; diff --git a/crates/mozart/src/commands/licenses.rs b/crates/mozart/src/commands/licenses.rs index 394cc39..671ce2a 100644 --- a/crates/mozart/src/commands/licenses.rs +++ b/crates/mozart/src/commands/licenses.rs @@ -1,6 +1,6 @@ +use crate::composer::Composer; use clap::Args; use indexmap::IndexMap; -use mozart_core::composer::Composer; use mozart_core::console::Console; use mozart_core::console::hyperlink; use mozart_core::console_writeln; diff --git a/crates/mozart/src/commands/reinstall.rs b/crates/mozart/src/commands/reinstall.rs index ececa96..dc99a91 100644 --- a/crates/mozart/src/commands/reinstall.rs +++ b/crates/mozart/src/commands/reinstall.rs @@ -1,6 +1,7 @@ +use crate::composer::Composer; use clap::Args; use mozart_autoload::AutoloadGeneratorExt; -use mozart_core::composer::{AutoloadDumpOptions, Composer, LocalPackage}; +use mozart_core::composer::{AutoloadDumpOptions, LocalPackage}; use mozart_core::console_format; use mozart_core::validation::package_name_to_regexp; diff --git a/crates/mozart/src/commands/run_script.rs b/crates/mozart/src/commands/run_script.rs index ade389e..e4b701a 100644 --- a/crates/mozart/src/commands/run_script.rs +++ b/crates/mozart/src/commands/run_script.rs @@ -1,5 +1,5 @@ +use crate::composer::Composer; use clap::Args; -use mozart_core::composer::Composer; use mozart_core::script_events; use mozart_core::{console_writeln, console_writeln_error}; use std::collections::BTreeMap; diff --git a/crates/mozart/src/commands/status.rs b/crates/mozart/src/commands/status.rs index b6802a3..60185cb 100644 --- a/crates/mozart/src/commands/status.rs +++ b/crates/mozart/src/commands/status.rs @@ -1,11 +1,11 @@ +use crate::composer::Composer; use clap::Args; use indexmap::IndexMap; -use mozart_core::composer::{Composer, InstallationSource, LocalPackage}; +use mozart_core::composer::{InstallationSource, LocalPackage}; use mozart_core::console::Console; use mozart_core::console_writeln; use mozart_core::console_writeln_error; use mozart_core::exit_code; -use mozart_registry::download_manager::DownloadManager; use mozart_vcs::version_guesser::VersionGuesser; #[derive(Args)] @@ -26,19 +26,21 @@ pub async fn execute( cli: &super::Cli, console: &Console, ) -> anyhow::Result<()> { - let working_dir = cli.working_dir()?; - let composer = Composer::require(&working_dir)?; + let composer = Composer::require(cli.working_dir()?)?; + let installed_repo = composer.repository_manager().local_repository(); + + let dm = composer.download_manager(); let im = composer.installation_manager(); - let dm = DownloadManager::new(im.vendor_dir().join(".cache").join("git")); - let guesser = VersionGuesser::new(); - let mut errors: IndexMap = IndexMap::new(); - let mut unpushed_changes: IndexMap = IndexMap::new(); - let mut vcs_version_changes: IndexMap = IndexMap::new(); + let mut errors = IndexMap::new(); + let mut unpushed_changes = IndexMap::new(); + let mut vcs_version_changes = IndexMap::new(); + + let guesser = VersionGuesser::new(); for package in installed_repo.get_canonical_packages() { - let Some(downloader) = dm.for_package(package) else { + let Some(downloader) = dm.get_downloader_for_package(package) else { continue; }; let Some(target_dir) = im.get_install_path(package) else { @@ -111,6 +113,7 @@ pub async fn execute( console, "You have changes in the following dependencies:" ); + for (path, changes) in &errors { if verbose { console_writeln!(console, "{path}:"); @@ -126,6 +129,7 @@ pub async fn execute( console, "You have unpushed changes on the current branch in the following dependencies:" ); + for (path, changes) in &unpushed_changes { if verbose { console_writeln!(console, "{path}:"); @@ -141,6 +145,7 @@ pub async fn execute( console, "You have version variations in the following dependencies:" ); + for (path, change) in &vcs_version_changes { if verbose { let mut prev = if change.previous.version.is_empty() { @@ -173,8 +178,8 @@ pub async fn execute( } let code = (if !errors.is_empty() { 1 } else { 0 }) - | (if !unpushed_changes.is_empty() { 2 } else { 0 }) - | (if !vcs_version_changes.is_empty() { + + (if !unpushed_changes.is_empty() { 2 } else { 0 }) + + (if !vcs_version_changes.is_empty() { 4 } else { 0 diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs index a7c1412..11672fd 100644 --- a/crates/mozart/src/commands/update.rs +++ b/crates/mozart/src/commands/update.rs @@ -1,6 +1,6 @@ +use crate::composer::Composer; use clap::Args; use indexmap::{IndexMap, IndexSet}; -use mozart_core::composer::Composer; use mozart_core::console_format; use mozart_core::package; use mozart_core::platform::is_platform_package; diff --git a/crates/mozart/src/commands/validate.rs b/crates/mozart/src/commands/validate.rs index 3fd1f56..873b371 100644 --- a/crates/mozart/src/commands/validate.rs +++ b/crates/mozart/src/commands/validate.rs @@ -1,5 +1,5 @@ +use crate::composer::Composer; use clap::Args; -use mozart_core::composer::Composer; use mozart_core::config_validator::{ValidationResult, ValidatorOptions, validate_manifest}; use mozart_core::console_format; use mozart_core::console_writeln; diff --git a/crates/mozart/src/composer.rs b/crates/mozart/src/composer.rs new file mode 100644 index 0000000..108a5d3 --- /dev/null +++ b/crates/mozart/src/composer.rs @@ -0,0 +1,152 @@ +//! Composer-equivalent root state: composer.json + effective config + +//! the manager objects commands look up off the root [`Composer`]. +//! +//! Mirrors the role of `Composer\Composer` / `Composer\PartialComposer` +//! (PHP) — a state container with getters for the merged [`Config`], the +//! root [`RawPackageData`], the [`RepositoryManager`], and the +//! [`InstallationManager`]. Wiring lives in [`crate::factory`], the same +//! split as upstream's `Composer\Factory::createComposer`. +//! +//! See `Composer\Command\BaseCommand::requireComposer()` / +//! `Composer\Command\BaseCommand::tryComposer()` for the upstream contract +//! that [`Composer::require`] and [`Composer::try_load`] are modelled on. + +use std::path::{Path, PathBuf}; + +use crate::factory::create_composer; +use mozart_core::composer::{AutoloadGenerator, InstallationManager, Locker, RepositoryManager}; +use mozart_core::config::Config; +use mozart_core::package::RawPackageData; +use mozart_registry::download_manager::DownloadManager; + +/// Project-level Composer state. Mirrors `Composer\PartialComposer` / +/// `Composer\Composer` in PHP, exposing the subset of getters command +/// handlers need today: config, root package, repository manager, +/// installation manager, autoload generator, and locker. More +/// managers (download, …) can be layered on as commands need them. +pub struct Composer { + project_dir: PathBuf, + config: Config, + package: RawPackageData, + repository_manager: RepositoryManager, + installation_manager: InstallationManager, + download_manager: DownloadManager, + autoload_generator: AutoloadGenerator, + locker: Locker, +} + +impl Composer { + /// All-args constructor used by [`crate::factory::create_composer`]. + /// Mirrors the PHP pattern of `new Composer()` followed by + /// `setConfig` / `setPackage` / `setRepositoryManager` / + /// `setInstallationManager` / `setAutoloadGenerator` / `setLocker`, + /// collapsed into a single immutable build. + #[allow(clippy::too_many_arguments)] + pub fn new( + project_dir: PathBuf, + config: Config, + package: RawPackageData, + repository_manager: RepositoryManager, + installation_manager: InstallationManager, + download_manager: DownloadManager, + autoload_generator: AutoloadGenerator, + locker: Locker, + ) -> Self { + Self { + project_dir, + config, + package, + repository_manager, + installation_manager, + download_manager, + autoload_generator, + locker, + } + } + + /// Load Composer state for `project_dir`, requiring a composer.json. + /// Mirrors `BaseCommand::requireComposer()`, which delegates to + /// `Factory::createComposer` after asserting the file exists. + pub fn require(project_dir: impl Into) -> anyhow::Result { + let project_dir = project_dir.into(); + let composer_json = project_dir.join("composer.json"); + if !composer_json.exists() { + anyhow::bail!( + "Composer could not find a composer.json file in {}", + project_dir.display() + ); + } + create_composer(project_dir, &composer_json) + } + + /// Load Composer state for `project_dir`, returning `None` if no + /// composer.json exists. Other I/O or parse errors still propagate. + /// Mirrors `BaseCommand::tryComposer()`. + pub fn try_load(project_dir: impl Into) -> anyhow::Result> { + let project_dir = project_dir.into(); + let composer_json = project_dir.join("composer.json"); + if !composer_json.exists() { + return Ok(None); + } + create_composer(project_dir, &composer_json).map(Some) + } + + /// Load Composer state keyed on a specific `composer.json` file, deriving + /// the project directory from `file.parent()`. Mirrors + /// `ValidateCommand::createComposerInstance($file)` — Composer keys + /// instances on a file rather than a directory for non-default paths. + pub fn try_load_from_file(file: &Path) -> anyhow::Result> { + let project_dir = file + .parent() + .map(Path::to_path_buf) + .unwrap_or_else(|| PathBuf::from(".")); + Self::try_load(project_dir) + } + + pub fn project_dir(&self) -> &Path { + &self.project_dir + } + + pub fn config(&self) -> &Config { + &self.config + } + + /// Root package loaded from the project's `composer.json`. Mirrors + /// `Composer::getPackage()`; ideally this would return a fully + /// resolved `RootPackageInterface` equivalent, but Mozart does not + /// yet have a `RootPackageLoader` port — for now callers see the + /// raw, pre-normalised JSON shape. + pub fn package(&self) -> &RawPackageData { + &self.package + } + + /// Mirror of `Composer::getRepositoryManager()`. + pub fn repository_manager(&self) -> &RepositoryManager { + &self.repository_manager + } + + /// Mirror of `Composer::getInstallationManager()`. + pub fn installation_manager(&self) -> &InstallationManager { + &self.installation_manager + } + + pub fn download_manager(&self) -> &DownloadManager { + &self.download_manager + } + + /// Mirror of `Composer::getAutoloadGenerator()`. + /// + /// Returned by shared reference because Mozart's + /// [`AutoloadGenerator`] is stateless — per-call toggles live on + /// [`AutoloadDumpOptions`] passed into `dump()`, not on the + /// generator itself. Diverges from PHP's + /// `$composer->getAutoloadGenerator()->setDryRun(...)` chain. + pub fn autoload_generator(&self) -> &AutoloadGenerator { + &self.autoload_generator + } + + /// Mirror of `Composer::getLocker()`. + pub fn locker(&self) -> &Locker { + &self.locker + } +} diff --git a/crates/mozart/src/factory.rs b/crates/mozart/src/factory.rs new file mode 100644 index 0000000..67fc9a3 --- /dev/null +++ b/crates/mozart/src/factory.rs @@ -0,0 +1,362 @@ +//! Factory helpers for constructing Composer state. +//! +//! Ports the static factory methods from `Composer\Factory`. Today we +//! cover [`create_config`] (effective global [`Config`]) and +//! [`create_composer`] (the project-level [`Composer`] root, built from +//! `composer.json` plus the on-disk `vendor/composer/installed.json`). +//! +//! Auth loading, htaccess creation, and the plugin/event-dispatcher +//! wiring are intentionally omitted as they are out of scope for the +//! current port. + +use crate::composer::Composer; +use mozart_core::composer::{ + AutoloadGenerator, InstallationManager, InstallationSource, LocalPackage, LocalRepository, + Locker, PackageReference, RepositoryManager, +}; +use mozart_core::config::resolve_references; +use mozart_core::factory::create_config; +use mozart_core::package::read_from_file; +use mozart_registry::download_manager::DownloadManager; + +/// Rust port of `Factory::createComposer()`. +/// +/// Builds the project-level [`Composer`]: +/// 1. Read `composer.json` from `composer_json` and load it into both +/// the merged [`Config`] (overlaying [`create_config`]) and the +/// untyped [`crate::package::RawPackageData`]. +/// 2. Resolve all `{$home}` / `{$vendor-dir}` placeholders via +/// [`resolve_references`]. +/// 3. Resolve `vendor-dir` against `project_dir` if it is relative, so +/// the installation manager hands back absolute paths +/// (`Factory::createComposer` does the same via +/// `Filesystem::isAbsolutePath`). +/// 4. Wire up the [`InstallationManager`] and a [`RepositoryManager`] +/// whose local repository is populated from +/// `vendor/composer/installed.json` — the same role +/// `Factory::addLocalRepository` plays in PHP. +/// 5. Construct a fresh [`AutoloadGenerator`] with PHP defaults +/// (`new AutoloadGenerator($eventDispatcher, $io)` in PHP, minus the +/// not-yet-ported event dispatcher and IO dependencies). +/// 6. Construct a [`Locker`] pointed at `composer.lock` next to the +/// composer.json — same as `Factory::createComposer`'s +/// `new Locker($io, new JsonFile($lockFile, …), $im, $contents)`, +/// minus the IO/installation-manager/contents dependencies that +/// only matter once we port `setLockData`. +/// +/// The plugin manager, download manager, and event dispatcher that +/// `Factory::createComposer` also wires up are not yet ported. +pub fn create_composer( + project_dir: std::path::PathBuf, + composer_json: &std::path::Path, +) -> anyhow::Result { + let content = std::fs::read_to_string(composer_json)?; + let value: serde_json::Value = serde_json::from_str(&content)?; + let mut config = create_config()?; + if let Some(cfg_obj) = value.get("config").and_then(|v| v.as_object()) { + let overrides: std::collections::BTreeMap = cfg_obj + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + config.merge(&overrides)?; + } + resolve_references(&mut config); + + let package = read_from_file(composer_json)?; + + // Mirrors `Factory::createComposer`'s `vendorDir` handling. The + // value out of `Config::get('vendor-dir')` already had `{$...}` + // placeholders substituted, but it may still be relative — resolve + // it against the project root so install paths are absolute. + let vendor_dir = if std::path::Path::new(&config.vendor_dir).is_absolute() { + std::path::PathBuf::from(&config.vendor_dir) + } else { + project_dir.join(&config.vendor_dir) + }; + + let (local_packages, dev_mode) = read_local_packages(&vendor_dir)?; + let repository_manager = + RepositoryManager::new(LocalRepository::with_dev_mode(local_packages, dev_mode)); + let installation_manager = InstallationManager::new(vendor_dir.clone()); + let download_manager = DownloadManager::new(vendor_dir.join(".cache").join("git")); + let autoload_generator = AutoloadGenerator::new(); + + // Mirrors `Factory::createComposer`'s lock-file path: the lockfile + // sits next to composer.json, with `.json` swapped for `.lock`. + let lock_file_path = composer_json + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or_else(|| project_dir.clone()) + .join( + composer_json + .file_name() + .and_then(|n| n.to_str()) + .map(|n| n.strip_suffix(".json").unwrap_or(n)) + .map(|stem| format!("{stem}.lock")) + .unwrap_or_else(|| "composer.lock".to_string()), + ); + let locker = Locker::new(lock_file_path); + + Ok(Composer::new( + project_dir, + config, + package, + repository_manager, + installation_manager, + download_manager, + autoload_generator, + locker, + )) +} + +/// Read `vendor/composer/installed.json` into the minimal shape the +/// installation manager needs. Mirrors the relevant slice of +/// `Composer\Repository\FilesystemRepository::initialize`: accept both +/// the v2 object form (`{packages: [...]}`) and the legacy v1 array +/// form. Returns an empty list when the file is missing — the same +/// semantics as `FilesystemRepository::isFresh`. +/// +/// We deliberately avoid pulling the full `InstalledPackages` reader from +/// `mozart-registry` here to keep `mozart-core` at the bottom of the +/// dependency graph; the parsing that's actually load-bearing for the +/// install-path computation is just the package name + optional +/// `target-dir`. +fn read_local_packages( + vendor_dir: &std::path::Path, +) -> anyhow::Result<(Vec, Option)> { + let path = vendor_dir.join("composer/installed.json"); + if !path.exists() { + return Ok((Vec::new(), None)); + } + let content = std::fs::read_to_string(&path)?; + let value: serde_json::Value = serde_json::from_str(&content)?; + + let (entries, dev_mode): (&[serde_json::Value], Option) = match &value { + serde_json::Value::Object(obj) => { + let entries = match obj.get("packages") { + Some(serde_json::Value::Array(arr)) => arr.as_slice(), + _ => return Ok((Vec::new(), obj.get("dev").and_then(|v| v.as_bool()))), + }; + (entries, obj.get("dev").and_then(|v| v.as_bool())) + } + serde_json::Value::Array(arr) => (arr.as_slice(), None), + _ => return Ok((Vec::new(), None)), + }; + + let mut out = Vec::with_capacity(entries.len()); + for entry in entries { + let pretty_name = entry + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let pretty_version = entry + .get("version") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let target_dir = entry + .get("target-dir") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + let package_type = entry + .get("type") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + let installation_source = entry + .get("installation-source") + .and_then(|v| v.as_str()) + .and_then(InstallationSource::parse); + let source = read_package_reference(entry.get("source")); + let dist = read_package_reference(entry.get("dist")); + let extra = entry + .get("extra") + .cloned() + .unwrap_or(serde_json::Value::Null); + out.push(LocalPackage::new( + pretty_name, + pretty_version, + target_dir, + package_type, + installation_source, + source, + dist, + extra, + )); + } + Ok((out, dev_mode)) +} + +fn read_package_reference(value: Option<&serde_json::Value>) -> Option { + let v = value?; + let kind = v.get("type").and_then(|x| x.as_str())?.to_string(); + let url = v + .get("url") + .and_then(|x| x.as_str()) + .unwrap_or("") + .to_string(); + let reference = v + .get("reference") + .and_then(|x| x.as_str()) + .map(|s| s.to_string()); + let shasum = v + .get("shasum") + .and_then(|x| x.as_str()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()); + Some(PackageReference { + kind, + url, + reference, + shasum, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::path::Path; + use tempfile::tempdir; + + fn write(path: &Path, content: &str) { + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path, content).unwrap(); + } + + #[test] + fn install_path_is_vendor_dir_plus_pretty_name() { + let dir = tempdir().unwrap(); + write(&dir.path().join("composer.json"), r#"{"name": "acme/app"}"#); + write( + &dir.path().join("vendor/composer/installed.json"), + r#"{"packages": [{"name": "Vendor/Pkg", "version": "1.0.0"}]}"#, + ); + + let composer = Composer::require(dir.path()).unwrap(); + let pkg = composer + .repository_manager() + .local_repository() + .get_canonical_packages() + .next() + .unwrap(); + + let install_path = composer + .installation_manager() + .get_install_path(pkg) + .unwrap(); + + // Mirrors `LibraryInstaller::getInstallPath`: + // `vendorDir + '/' + prettyName`. `pretty-name` is preserved + // case (Composer/Repository/FilesystemRepository keeps the original). + assert_eq!(install_path, dir.path().join("vendor").join("Vendor/Pkg")); + } + + #[test] + fn install_path_appends_target_dir() { + let dir = tempdir().unwrap(); + write(&dir.path().join("composer.json"), r#"{"name": "acme/app"}"#); + write( + &dir.path().join("vendor/composer/installed.json"), + r#"{"packages": [{"name": "vendor/pkg", "target-dir": "src/lib"}]}"#, + ); + + let composer = Composer::require(dir.path()).unwrap(); + let pkg = composer + .repository_manager() + .local_repository() + .get_canonical_packages() + .next() + .unwrap(); + + let install_path = composer + .installation_manager() + .get_install_path(pkg) + .unwrap(); + + assert_eq!(install_path, dir.path().join("vendor/vendor/pkg/src/lib")); + } + + #[test] + fn local_repository_is_empty_when_installed_json_missing() { + let dir = tempdir().unwrap(); + write(&dir.path().join("composer.json"), r#"{"name": "acme/app"}"#); + + let composer = Composer::require(dir.path()).unwrap(); + let count = composer + .repository_manager() + .local_repository() + .get_canonical_packages() + .count(); + assert_eq!(count, 0); + } + + #[test] + fn local_repository_accepts_v1_array_form() { + // Older Composer 1.x / fixture format: bare array of packages. + // FilesystemRepository::initialize accepts this; our minimal + // reader must too. + let dir = tempdir().unwrap(); + write(&dir.path().join("composer.json"), r#"{"name": "acme/app"}"#); + write( + &dir.path().join("vendor/composer/installed.json"), + r#"[{"name": "a/a"}, {"name": "b/b"}]"#, + ); + + let composer = Composer::require(dir.path()).unwrap(); + let names: Vec<&str> = composer + .repository_manager() + .local_repository() + .get_canonical_packages() + .map(|p| p.pretty_name()) + .collect(); + assert_eq!(names, vec!["a/a", "b/b"]); + } + + #[test] + fn package_returns_root_composer_json() { + let dir = tempdir().unwrap(); + write( + &dir.path().join("composer.json"), + r#"{"name": "acme/app", "require": {"vendor/pkg": "^1.0"}}"#, + ); + + let composer = Composer::require(dir.path()).unwrap(); + assert_eq!(composer.package().name, "acme/app"); + assert_eq!( + composer + .package() + .require + .get("vendor/pkg") + .map(String::as_str), + Some("^1.0"), + ); + } + + #[test] + fn install_path_uses_configured_vendor_dir() { + let dir = tempdir().unwrap(); + write( + &dir.path().join("composer.json"), + r#"{"name": "acme/app", "config": {"vendor-dir": "deps"}}"#, + ); + write( + &dir.path().join("deps/composer/installed.json"), + r#"{"packages": [{"name": "vendor/pkg"}]}"#, + ); + + let composer = Composer::require(dir.path()).unwrap(); + let pkg = composer + .repository_manager() + .local_repository() + .get_canonical_packages() + .next() + .unwrap(); + + let install_path = composer + .installation_manager() + .get_install_path(pkg) + .unwrap(); + assert_eq!(install_path, dir.path().join("deps/vendor/pkg")); + } +} diff --git a/crates/mozart/src/lib.rs b/crates/mozart/src/lib.rs index 82b6da3..d28081a 100644 --- a/crates/mozart/src/lib.rs +++ b/crates/mozart/src/lib.rs @@ -1 +1,4 @@ pub mod commands; + +mod composer; +mod factory; -- cgit v1.3.1