From 8871b923fa3df1935c263db155cb8bc3d59705cd Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 10 May 2026 23:58:26 +0900 Subject: refactor(downloader): turn DownloadManager into downloader registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reshape DownloadManager from a hard-coded VCS match into a registry of DownloaderInterface instances keyed by source type, mirroring Composer's DownloadManager — with prefer-source/dist preferences, an IO handle, and a files cache. ArchiveManager now resolves dist sources through a shared DownloadManager instead of calling download_dist directly, and Composer::require / try_load take an IO so it flows through the factory wiring. --- .../mozart-core/src/downloader/download_manager.rs | 137 ++++++---- .../src/downloader/downloader_interface.rs | 6 +- .../mozart-core/src/downloader/git_downloader.rs | 19 +- crates/mozart-core/src/downloader/hg_downloader.rs | 17 +- .../mozart-core/src/downloader/svn_downloader.rs | 18 +- crates/mozart-core/src/lib.rs | 1 + crates/mozart-core/src/package/archiver.rs | 4 +- .../src/package/archiver/archive_manager.rs | 300 +++++++++++++++++++++ crates/mozart-core/src/package/archiver/manager.rs | 299 -------------------- crates/mozart-core/src/repository/downloader.rs | 2 +- .../repository/installer_executor/filesystem.rs | 10 +- crates/mozart-core/src/util.rs | 3 + crates/mozart-core/src/util/filesystem.rs | 8 + crates/mozart-core/tests/git_driver_test.rs | 9 +- 14 files changed, 450 insertions(+), 383 deletions(-) create mode 100644 crates/mozart-core/src/package/archiver/archive_manager.rs delete mode 100644 crates/mozart-core/src/package/archiver/manager.rs create mode 100644 crates/mozart-core/src/util.rs create mode 100644 crates/mozart-core/src/util/filesystem.rs (limited to 'crates/mozart-core') diff --git a/crates/mozart-core/src/downloader/download_manager.rs b/crates/mozart-core/src/downloader/download_manager.rs index c83bc64..68ae20e 100644 --- a/crates/mozart-core/src/downloader/download_manager.rs +++ b/crates/mozart-core/src/downloader/download_manager.rs @@ -1,40 +1,49 @@ -//! `DownloadManager` — pick the right [`VcsDownloader`] for a given -//! [`LocalPackage`]. Mirrors `Composer\Downloader\DownloadManager`. - use crate::composer::{InstallationSource, LocalPackage}; -use crate::downloader::{GitDownloader, HgDownloader, SvnDownloader, VcsDownloader}; -use crate::vcs::process::ProcessExecutor; -use crate::vcs::util::git::GitUtil; -use crate::vcs::util::hg::HgUtil; -use crate::vcs::util::svn::SvnUtil; -use std::path::PathBuf; - -/// Selects a `VcsDownloader` for a package based on its installation source -/// and source type. Mirrors `DownloadManager::getDownloaderForPackage`: -/// -/// - `metapackage` → `None`. -/// - `installation-source: dist` → `None` (Composer would return a -/// `FileDownloader`-family object that does not implement -/// `ChangeReportInterface` / `DvcsDownloaderInterface`, so the status -/// command's `instanceof` checks all become no-ops; returning `None` -/// directly is the equivalent in our trait-object world). -/// - `installation-source: source` → the matching VCS downloader by -/// `source.type` (`git` / `hg` / `svn`). +use crate::console::IoInterface; +use crate::downloader::{DownloaderInterface, VcsDownloader}; +use crate::repository::cache::Cache; +use crate::repository::downloader::{DownloadProgress, download_dist}; +use crate::util::Filesystem; + +/// ref: \Composer\Downloader\DownloadManager pub struct DownloadManager { - git_cache_dir: PathBuf, + #[allow(unused)] + io: std::sync::Arc>>, + prefer_dist: bool, + prefer_source: bool, + #[allow(unused)] + package_preferences: Vec<(String, InstallationSource)>, + #[allow(unused)] + filesystem: Filesystem, + downloaders: indexmap::IndexMap>, + files_cache: Cache, // TODO: remove } impl DownloadManager { - /// `git_cache_dir`: where `GitUtil` should keep mirror clones (e.g. - /// `/.cache/git`). - pub fn new(git_cache_dir: PathBuf) -> Self { - Self { git_cache_dir } + pub fn new( + io: std::sync::Arc>>, + prefer_source: bool, + filesystem: Filesystem, + files_cache: Cache, + ) -> Self { + Self { + io, + prefer_dist: false, + prefer_source, + package_preferences: Vec::new(), + filesystem, + downloaders: indexmap::IndexMap::new(), + files_cache, + } } - pub fn get_downloader_for_package( - &self, - package: &LocalPackage, - ) -> Option> { + pub fn set_downloader(&mut self, r#type: String, downloader: Box) { + assert!(r#type.chars().all(|c| c.is_ascii_lowercase())); + + self.downloaders.insert(r#type, downloader); + } + + pub fn get_downloader_for_package(&self, package: &LocalPackage) -> Option<&dyn VcsDownloader> { if package.package_type() == Some("metapackage") { return None; } @@ -42,32 +51,58 @@ impl DownloadManager { InstallationSource::Dist => None, InstallationSource::Source => { let kind = package.source()?.kind.as_str(); - match kind { - "git" => { - let git_util = - GitUtil::new(ProcessExecutor::new(), self.git_cache_dir.clone()); - Some(Box::new(GitDownloader::new(git_util))) - } - "hg" => { - let hg_util = HgUtil::new(ProcessExecutor::new()); - Some(Box::new(HgDownloader::new(hg_util))) - } - "svn" => { - let svn_util = SvnUtil::new(ProcessExecutor::new()); - Some(Box::new(SvnDownloader::new(svn_util))) - } - _ => None, - } + self.downloaders + .get(kind) + .and_then(|d| d.as_vcs_downloader()) } } } + + /// Makes downloader prefer source installation over the dist. + pub fn set_prefer_source(&mut self, prefer_source: bool) { + self.prefer_source = prefer_source; + } + + /// Makes downloader prefer dist installation over the source. + pub fn set_prefer_dist(&mut self, prefer_dist: bool) { + self.prefer_dist = prefer_dist; + } + + pub async fn download_legacy( + &self, + url: &str, + expected_shasum: Option<&str>, + progress: Option<&mut DownloadProgress>, + ) -> anyhow::Result> { + download_dist(url, expected_shasum, progress, &self.files_cache).await + } } #[cfg(test)] mod tests { use super::*; use crate::composer::PackageReference; + use crate::console::Console; + use crate::downloader::GitDownloader; + use crate::vcs::process::ProcessExecutor; use serde_json::Value; + use std::path::PathBuf; + use std::sync::{Arc, Mutex}; + + fn make_dm() -> DownloadManager { + let io: Arc>> = Arc::new(Mutex::new(Box::new(Console::new( + 0, true, false, true, true, + )) + as Box)); + let cache_dir = PathBuf::from("/tmp/mz-test-cache"); + let cache = Cache::new(cache_dir.clone(), false); + let mut dm = DownloadManager::new(io, false, Filesystem::new(), cache); + dm.set_downloader( + "git".to_owned(), + Box::new(GitDownloader::new(ProcessExecutor::new(), cache_dir)), + ); + dm + } fn pkg( installation_source: Option, @@ -93,7 +128,7 @@ mod tests { #[test] fn metapackage_returns_none() { - let dm = DownloadManager::new(PathBuf::from("/tmp/mz-test-cache")); + let dm = make_dm(); let mut p = pkg(Some(InstallationSource::Source), Some("git")); // override type p = LocalPackage::new( @@ -111,28 +146,28 @@ mod tests { #[test] fn dist_install_returns_none() { - let dm = DownloadManager::new(PathBuf::from("/tmp/mz-test-cache")); + let dm = make_dm(); let p = pkg(Some(InstallationSource::Dist), Some("git")); assert!(dm.get_downloader_for_package(&p).is_none()); } #[test] fn source_install_with_git_returns_some() { - let dm = DownloadManager::new(PathBuf::from("/tmp/mz-test-cache")); + let dm = make_dm(); let p = pkg(Some(InstallationSource::Source), Some("git")); assert!(dm.get_downloader_for_package(&p).is_some()); } #[test] fn unknown_source_kind_returns_none() { - let dm = DownloadManager::new(PathBuf::from("/tmp/mz-test-cache")); + let dm = make_dm(); let p = pkg(Some(InstallationSource::Source), Some("perforce")); assert!(dm.get_downloader_for_package(&p).is_none()); } #[test] fn missing_installation_source_returns_none() { - let dm = DownloadManager::new(PathBuf::from("/tmp/mz-test-cache")); + let dm = make_dm(); let p = pkg(None, Some("git")); assert!(dm.get_downloader_for_package(&p).is_none()); } diff --git a/crates/mozart-core/src/downloader/downloader_interface.rs b/crates/mozart-core/src/downloader/downloader_interface.rs index 9c1b585..6184a0d 100644 --- a/crates/mozart-core/src/downloader/downloader_interface.rs +++ b/crates/mozart-core/src/downloader/downloader_interface.rs @@ -1 +1,5 @@ -pub trait DownloaderInterface {} +use crate::downloader::VcsDownloader; + +pub trait DownloaderInterface: Send + Sync { + fn as_vcs_downloader(&self) -> Option<&dyn VcsDownloader>; +} diff --git a/crates/mozart-core/src/downloader/git_downloader.rs b/crates/mozart-core/src/downloader/git_downloader.rs index d4d8c44..6e1351c 100644 --- a/crates/mozart-core/src/downloader/git_downloader.rs +++ b/crates/mozart-core/src/downloader/git_downloader.rs @@ -1,12 +1,11 @@ +use crate::downloader::{DownloaderInterface, VcsDownloader}; +use crate::vcs::process::ProcessExecutor; +use crate::vcs::util::git::GitUtil; use anyhow::Result; use regex::Regex; use std::path::Path; use std::sync::LazyLock; -use crate::downloader::VcsDownloader; -use crate::vcs::process::ProcessExecutor; -use crate::vcs::util::git::GitUtil; - /// Match ` HEAD` lines in `git show-ref --head -d` output. static HEAD_REF_RE: LazyLock = LazyLock::new(|| Regex::new(r"(?im)^([a-f0-9]+) HEAD$").unwrap()); @@ -19,8 +18,10 @@ pub struct GitDownloader { } impl GitDownloader { - pub fn new(git_util: GitUtil) -> Self { - Self { git_util } + pub fn new(process: ProcessExecutor, cache_dir: std::path::PathBuf) -> Self { + Self { + git_util: GitUtil::new(process, cache_dir), + } } } @@ -235,6 +236,12 @@ impl VcsDownloader for GitDownloader { } } +impl DownloaderInterface for GitDownloader { + fn as_vcs_downloader(&self) -> Option<&dyn VcsDownloader> { + Some(self) + } +} + fn collect_show_ref(process: &ProcessExecutor, target: &Path) -> Result> { let output = process.execute(&["git", "show-ref", "--head", "-d"], Some(target))?; if output.status != 0 { diff --git a/crates/mozart-core/src/downloader/hg_downloader.rs b/crates/mozart-core/src/downloader/hg_downloader.rs index 9fb918e..dfe3546 100644 --- a/crates/mozart-core/src/downloader/hg_downloader.rs +++ b/crates/mozart-core/src/downloader/hg_downloader.rs @@ -1,16 +1,19 @@ +use crate::downloader::{DownloaderInterface, VcsDownloader}; +use crate::vcs::process::ProcessExecutor; +use crate::vcs::util::hg::HgUtil; use anyhow::Result; use std::path::Path; -use crate::{downloader::VcsDownloader, vcs::util::hg::HgUtil}; - /// Mercurial downloader using clone/pull/update. pub struct HgDownloader { hg_util: HgUtil, } impl HgDownloader { - pub fn new(hg_util: HgUtil) -> Self { - Self { hg_util } + pub fn new(process: ProcessExecutor) -> Self { + Self { + hg_util: HgUtil::new(process), + } } } @@ -82,3 +85,9 @@ impl VcsDownloader for HgDownloader { false } } + +impl DownloaderInterface for HgDownloader { + fn as_vcs_downloader(&self) -> Option<&dyn VcsDownloader> { + Some(self) + } +} diff --git a/crates/mozart-core/src/downloader/svn_downloader.rs b/crates/mozart-core/src/downloader/svn_downloader.rs index c78f9b7..690a090 100644 --- a/crates/mozart-core/src/downloader/svn_downloader.rs +++ b/crates/mozart-core/src/downloader/svn_downloader.rs @@ -1,11 +1,11 @@ +use crate::downloader::{DownloaderInterface, VcsDownloader}; +use crate::vcs::process::ProcessExecutor; +use crate::vcs::util::svn::SvnUtil; use anyhow::Result; use regex::Regex; use std::path::Path; use std::sync::LazyLock; -use crate::downloader::VcsDownloader; -use crate::vcs::util::svn::SvnUtil; - /// Match any non-`X` status line (mirror of Composer's /// `{^ *[^X ] +}m`). Ignores externals (`X` prefix). static SVN_STATUS_RE: LazyLock = LazyLock::new(|| Regex::new(r"(?m)^ *[^X ] +").unwrap()); @@ -16,8 +16,10 @@ pub struct SvnDownloader { } impl SvnDownloader { - pub fn new(svn_util: SvnUtil) -> Self { - Self { svn_util } + pub fn new(process: ProcessExecutor) -> Self { + Self { + svn_util: SvnUtil::new(process), + } } } @@ -83,3 +85,9 @@ impl VcsDownloader for SvnDownloader { false } } + +impl DownloaderInterface for SvnDownloader { + fn as_vcs_downloader(&self) -> Option<&dyn VcsDownloader> { + Some(self) + } +} diff --git a/crates/mozart-core/src/lib.rs b/crates/mozart-core/src/lib.rs index adf24a3..efb5528 100644 --- a/crates/mozart-core/src/lib.rs +++ b/crates/mozart-core/src/lib.rs @@ -21,6 +21,7 @@ pub mod repository; pub mod repository_utils; pub mod script_events; pub mod suggest; +pub mod util; pub mod validation; pub mod vcs; pub mod version_bumper; diff --git a/crates/mozart-core/src/package/archiver.rs b/crates/mozart-core/src/package/archiver.rs index 142edbd..a01a173 100644 --- a/crates/mozart-core/src/package/archiver.rs +++ b/crates/mozart-core/src/package/archiver.rs @@ -5,8 +5,8 @@ use std::fs; use std::io::Write as _; use std::path::{Path, PathBuf}; -pub mod manager; -pub use manager::{ArchiveManager, ArchivePackage}; +mod archive_manager; +pub use archive_manager::*; /// A compiled exclude pattern derived from a gitignore-style rule. pub struct ExcludePattern { diff --git a/crates/mozart-core/src/package/archiver/archive_manager.rs b/crates/mozart-core/src/package/archiver/archive_manager.rs new file mode 100644 index 0000000..b4f8e27 --- /dev/null +++ b/crates/mozart-core/src/package/archiver/archive_manager.rs @@ -0,0 +1,300 @@ +use crate::downloader::DownloadManager; + +use super::{ + ArchiveFormat, collect_archivable_files, create_archive, generate_archive_filename, + parse_composer_excludes, parse_gitattributes, parse_gitignore_pattern, self_exclusion_patterns, +}; +use std::path::{Path, PathBuf}; + +/// A package to be archived. +/// +/// Mirrors the role of Composer's `CompletePackageInterface` as input to +/// `ArchiveManager::archive()`. The `Root` variant points at an already-checked-out +/// source tree; the `Remote` variant carries dist metadata that the manager will +/// download and extract to a temporary directory. +pub enum ArchivePackage { + Root { + name: String, + version: Option, + source_dir: PathBuf, + }, + Remote { + name: String, + version: String, + dist_url: String, + dist_type: String, + dist_shasum: Option, + dist_reference: Option, + source_reference: Option, + }, +} + +impl ArchivePackage { + fn name(&self) -> &str { + match self { + Self::Root { name, .. } | Self::Remote { name, .. } => name, + } + } + + fn version(&self) -> Option<&str> { + match self { + Self::Root { version, .. } => version.as_deref(), + Self::Remote { version, .. } => Some(version), + } + } + + fn dist_reference(&self) -> Option<&str> { + match self { + Self::Root { .. } => None, + Self::Remote { dist_reference, .. } => dist_reference.as_deref(), + } + } + + fn dist_type(&self) -> Option<&str> { + match self { + Self::Root { .. } => None, + Self::Remote { dist_type, .. } => Some(dist_type), + } + } + + fn source_reference(&self) -> Option<&str> { + match self { + Self::Root { .. } => None, + Self::Remote { + source_reference, .. + } => source_reference.as_deref(), + } + } +} + +/// Holds an extracted source directory plus, for remote packages, a tempdir +/// that must outlive `source_dir`. Drop removes the tempdir. +struct AcquiredSource { + source_dir: PathBuf, + archive_name: Option, + archive_excludes: Vec, + _temp_dir: Option, +} + +impl Drop for AcquiredSource { + fn drop(&mut self) { + if let Some(ref dir) = self._temp_dir { + let _ = std::fs::remove_dir_all(dir); + } + } +} + +/// Read `archive.name` and `archive.exclude` from a composer.json file. +fn read_archive_config(composer_json_path: &Path) -> anyhow::Result<(Option, Vec)> { + let content = std::fs::read_to_string(composer_json_path)?; + let value: serde_json::Value = serde_json::from_str(&content)?; + + let name = value + .get("archive") + .and_then(|a| a.get("name")) + .and_then(|n| n.as_str()) + .map(|s| s.to_string()); + + let excludes = value + .get("archive") + .and_then(|a| a.get("exclude")) + .and_then(|e| e.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str()) + .map(|s| s.to_string()) + .collect() + }) + .unwrap_or_default(); + + Ok((name, excludes)) +} + +trait ArchiverInterface: Send + Sync {} + +/// ref: \Composer\Package\Archiver\ArchiveManager +pub struct ArchiveManager { + download_manager: std::sync::Arc>, + _archivers: Vec>, + _overwrite_files: bool, +} + +impl ArchiveManager { + pub fn new(download_manager: std::sync::Arc>) -> Self { + Self { + download_manager, + _archivers: Vec::new(), + _overwrite_files: true, + } + } + + /// Build the parts that make up a package archive's filename. + fn package_filename_parts(package: &ArchivePackage, archive_name: Option<&str>) -> String { + generate_archive_filename( + package.name(), + archive_name, + package.version(), + package.dist_reference(), + package.dist_type(), + package.source_reference(), + ) + } + + /// Generate the archive filename (without extension) for a package, using + /// any `archive.name` override from the package's source composer.json. + pub fn package_filename(package: &ArchivePackage) -> String { + let archive_name = match package { + ArchivePackage::Root { source_dir, .. } => { + read_archive_config(&source_dir.join("composer.json")) + .ok() + .and_then(|(n, _)| n) + } + ArchivePackage::Remote { .. } => None, + }; + Self::package_filename_parts(package, archive_name.as_deref()) + } + + /// Join filename parts with `-`, mirroring Composer's + /// `getPackageFilenameFromParts`. + pub fn package_filename_from_parts(parts: &[&str]) -> String { + parts.join("-") + } + + /// Create an archive of the given package. + /// + /// For a `Remote` package, the dist is downloaded into a tempdir and + /// extracted before archiving; the tempdir is removed afterward. For + /// `Root`, the package's `source_dir` is archived in place. + /// + /// Returns the absolute path to the created archive. + pub async fn archive( + &self, + package: &ArchivePackage, + format: &str, + target_dir: &Path, + file_name: Option<&str>, + ignore_filters: bool, + ) -> anyhow::Result { + let archive_format = ArchiveFormat::parse(format).ok_or_else(|| { + anyhow::anyhow!( + "Unsupported archive format \"{}\". Supported formats: tar, tar.gz, tar.bz2, zip", + format + ) + })?; + + let source = acquire_source(package, &self.download_manager).await?; + + let filename_base = if let Some(file_name) = file_name { + file_name.to_string() + } else { + Self::package_filename_parts(package, source.archive_name.as_deref()) + }; + + // Self-exclusion: prevent the archive from including itself + let has_extra_parts = file_name.is_none() + && (package.version().is_some() + || package.dist_reference().is_some() + || package.source_reference().is_some()); + let self_exclusion_strs = self_exclusion_patterns(&filename_base, has_extra_parts); + + let mut all_patterns = Vec::new(); + for rule in &self_exclusion_strs { + if let Some(p) = parse_gitignore_pattern(rule) { + all_patterns.push(p); + } + } + + if !ignore_filters { + let git_patterns = parse_gitattributes(&source.source_dir); + all_patterns.extend(git_patterns); + + let composer_patterns = parse_composer_excludes(&source.archive_excludes); + all_patterns.extend(composer_patterns); + } + + let files = collect_archivable_files(&source.source_dir, &all_patterns)?; + + std::fs::create_dir_all(target_dir)?; + let target_dir = target_dir + .canonicalize() + .unwrap_or_else(|_| target_dir.to_path_buf()); + let target = target_dir.join(format!("{}.{}", filename_base, archive_format.extension())); + create_archive(&source.source_dir, &files, &target, &archive_format)?; + + Ok(target) + } +} + +/// Acquire the source tree of a package — either by reusing the root +/// directory or by downloading and extracting the dist into a tempdir. +/// Also reads `archive.name` / `archive.exclude` from the package's +/// composer.json. +async fn acquire_source( + package: &ArchivePackage, + download_manager: &std::sync::Arc>, +) -> anyhow::Result { + match package { + ArchivePackage::Root { source_dir, .. } => { + let composer_json_path = source_dir.join("composer.json"); + let (archive_name, archive_excludes) = if composer_json_path.exists() { + read_archive_config(&composer_json_path).unwrap_or((None, vec![])) + } else { + (None, vec![]) + }; + Ok(AcquiredSource { + source_dir: source_dir.clone(), + archive_name, + archive_excludes, + _temp_dir: None, + }) + } + ArchivePackage::Remote { + dist_url, + dist_type, + dist_shasum, + .. + } => { + let temp_base = std::env::temp_dir(); + let unique = format!( + "mozart-archive-{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_nanos()) + .unwrap_or(0) + ); + let temp_dir = temp_base.join(&unique); + std::fs::create_dir_all(&temp_dir)?; + + let bytes = download_manager + .lock() + .await + .download_legacy(dist_url, dist_shasum.as_deref(), None) + .await?; + + match dist_type.as_str() { + "zip" => crate::repository::downloader::extract_zip(&bytes, &temp_dir)?, + "tar" | "tar.gz" | "tgz" => { + crate::repository::downloader::extract_tar_gz(&bytes, &temp_dir)? + } + other => { + let _ = std::fs::remove_dir_all(&temp_dir); + anyhow::bail!("Unsupported dist type: {}", other); + } + } + + let extracted_composer = temp_dir.join("composer.json"); + let (archive_name, archive_excludes) = if extracted_composer.exists() { + read_archive_config(&extracted_composer).unwrap_or((None, vec![])) + } else { + (None, vec![]) + }; + + Ok(AcquiredSource { + source_dir: temp_dir.clone(), + archive_name, + archive_excludes, + _temp_dir: Some(temp_dir), + }) + } + } +} diff --git a/crates/mozart-core/src/package/archiver/manager.rs b/crates/mozart-core/src/package/archiver/manager.rs deleted file mode 100644 index bd5083e..0000000 --- a/crates/mozart-core/src/package/archiver/manager.rs +++ /dev/null @@ -1,299 +0,0 @@ -use super::{ - ArchiveFormat, collect_archivable_files, create_archive, generate_archive_filename, - parse_composer_excludes, parse_gitattributes, parse_gitignore_pattern, self_exclusion_patterns, -}; -use std::path::{Path, PathBuf}; - -/// A package to be archived. -/// -/// Mirrors the role of Composer's `CompletePackageInterface` as input to -/// `ArchiveManager::archive()`. The `Root` variant points at an already-checked-out -/// source tree; the `Remote` variant carries dist metadata that the manager will -/// download and extract to a temporary directory. -pub enum ArchivePackage { - Root { - name: String, - version: Option, - source_dir: PathBuf, - }, - Remote { - name: String, - version: String, - dist_url: String, - dist_type: String, - dist_shasum: Option, - dist_reference: Option, - source_reference: Option, - }, -} - -impl ArchivePackage { - fn name(&self) -> &str { - match self { - Self::Root { name, .. } | Self::Remote { name, .. } => name, - } - } - - fn version(&self) -> Option<&str> { - match self { - Self::Root { version, .. } => version.as_deref(), - Self::Remote { version, .. } => Some(version), - } - } - - fn dist_reference(&self) -> Option<&str> { - match self { - Self::Root { .. } => None, - Self::Remote { dist_reference, .. } => dist_reference.as_deref(), - } - } - - fn dist_type(&self) -> Option<&str> { - match self { - Self::Root { .. } => None, - Self::Remote { dist_type, .. } => Some(dist_type), - } - } - - fn source_reference(&self) -> Option<&str> { - match self { - Self::Root { .. } => None, - Self::Remote { - source_reference, .. - } => source_reference.as_deref(), - } - } -} - -/// Holds an extracted source directory plus, for remote packages, a tempdir -/// that must outlive `source_dir`. Drop removes the tempdir. -struct AcquiredSource { - source_dir: PathBuf, - archive_name: Option, - archive_excludes: Vec, - _temp_dir: Option, -} - -impl Drop for AcquiredSource { - fn drop(&mut self) { - if let Some(ref dir) = self._temp_dir { - let _ = std::fs::remove_dir_all(dir); - } - } -} - -/// Read `archive.name` and `archive.exclude` from a composer.json file. -fn read_archive_config(composer_json_path: &Path) -> anyhow::Result<(Option, Vec)> { - let content = std::fs::read_to_string(composer_json_path)?; - let value: serde_json::Value = serde_json::from_str(&content)?; - - let name = value - .get("archive") - .and_then(|a| a.get("name")) - .and_then(|n| n.as_str()) - .map(|s| s.to_string()); - - let excludes = value - .get("archive") - .and_then(|a| a.get("exclude")) - .and_then(|e| e.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str()) - .map(|s| s.to_string()) - .collect() - }) - .unwrap_or_default(); - - Ok((name, excludes)) -} - -/// Manages the creation of package archives. -/// -/// Mirrors Composer's `Composer\Package\Archiver\ArchiveManager`. -pub struct ArchiveManager; - -impl Default for ArchiveManager { - fn default() -> Self { - Self::new() - } -} - -impl ArchiveManager { - pub fn new() -> Self { - ArchiveManager - } - - /// Build the parts that make up a package archive's filename. - fn package_filename_parts(package: &ArchivePackage, archive_name: Option<&str>) -> String { - generate_archive_filename( - package.name(), - archive_name, - package.version(), - package.dist_reference(), - package.dist_type(), - package.source_reference(), - ) - } - - /// Generate the archive filename (without extension) for a package, using - /// any `archive.name` override from the package's source composer.json. - pub fn package_filename(package: &ArchivePackage) -> String { - let archive_name = match package { - ArchivePackage::Root { source_dir, .. } => { - read_archive_config(&source_dir.join("composer.json")) - .ok() - .and_then(|(n, _)| n) - } - ArchivePackage::Remote { .. } => None, - }; - Self::package_filename_parts(package, archive_name.as_deref()) - } - - /// Join filename parts with `-`, mirroring Composer's - /// `getPackageFilenameFromParts`. - pub fn package_filename_from_parts(parts: &[&str]) -> String { - parts.join("-") - } - - /// Create an archive of the given package. - /// - /// For a `Remote` package, the dist is downloaded into a tempdir and - /// extracted before archiving; the tempdir is removed afterward. For - /// `Root`, the package's `source_dir` is archived in place. - /// - /// Returns the absolute path to the created archive. - pub async fn archive( - &self, - package: &ArchivePackage, - format: &str, - target_dir: &Path, - file_name: Option<&str>, - ignore_filters: bool, - files_cache: &crate::repository::cache::Cache, - ) -> anyhow::Result { - let archive_format = ArchiveFormat::parse(format).ok_or_else(|| { - anyhow::anyhow!( - "Unsupported archive format \"{}\". Supported formats: tar, tar.gz, tar.bz2, zip", - format - ) - })?; - - let source = acquire_source(package, files_cache).await?; - - let filename_base = if let Some(file_name) = file_name { - file_name.to_string() - } else { - Self::package_filename_parts(package, source.archive_name.as_deref()) - }; - - // Self-exclusion: prevent the archive from including itself - let has_extra_parts = file_name.is_none() - && (package.version().is_some() - || package.dist_reference().is_some() - || package.source_reference().is_some()); - let self_exclusion_strs = self_exclusion_patterns(&filename_base, has_extra_parts); - - let mut all_patterns = Vec::new(); - for rule in &self_exclusion_strs { - if let Some(p) = parse_gitignore_pattern(rule) { - all_patterns.push(p); - } - } - - if !ignore_filters { - let git_patterns = parse_gitattributes(&source.source_dir); - all_patterns.extend(git_patterns); - - let composer_patterns = parse_composer_excludes(&source.archive_excludes); - all_patterns.extend(composer_patterns); - } - - let files = collect_archivable_files(&source.source_dir, &all_patterns)?; - - std::fs::create_dir_all(target_dir)?; - let target_dir = target_dir - .canonicalize() - .unwrap_or_else(|_| target_dir.to_path_buf()); - let target = target_dir.join(format!("{}.{}", filename_base, archive_format.extension())); - create_archive(&source.source_dir, &files, &target, &archive_format)?; - - Ok(target) - } -} - -/// Acquire the source tree of a package — either by reusing the root -/// directory or by downloading and extracting the dist into a tempdir. -/// Also reads `archive.name` / `archive.exclude` from the package's -/// composer.json. -async fn acquire_source( - package: &ArchivePackage, - files_cache: &crate::repository::cache::Cache, -) -> anyhow::Result { - match package { - ArchivePackage::Root { source_dir, .. } => { - let composer_json_path = source_dir.join("composer.json"); - let (archive_name, archive_excludes) = if composer_json_path.exists() { - read_archive_config(&composer_json_path).unwrap_or((None, vec![])) - } else { - (None, vec![]) - }; - Ok(AcquiredSource { - source_dir: source_dir.clone(), - archive_name, - archive_excludes, - _temp_dir: None, - }) - } - ArchivePackage::Remote { - dist_url, - dist_type, - dist_shasum, - .. - } => { - let temp_base = std::env::temp_dir(); - let unique = format!( - "mozart-archive-{}", - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_nanos()) - .unwrap_or(0) - ); - let temp_dir = temp_base.join(&unique); - std::fs::create_dir_all(&temp_dir)?; - - let bytes = crate::repository::downloader::download_dist( - dist_url, - dist_shasum.as_deref(), - None, - files_cache, - ) - .await?; - - match dist_type.as_str() { - "zip" => crate::repository::downloader::extract_zip(&bytes, &temp_dir)?, - "tar" | "tar.gz" | "tgz" => { - crate::repository::downloader::extract_tar_gz(&bytes, &temp_dir)? - } - other => { - let _ = std::fs::remove_dir_all(&temp_dir); - anyhow::bail!("Unsupported dist type: {}", other); - } - } - - let extracted_composer = temp_dir.join("composer.json"); - let (archive_name, archive_excludes) = if extracted_composer.exists() { - read_archive_config(&extracted_composer).unwrap_or((None, vec![])) - } else { - (None, vec![]) - }; - - Ok(AcquiredSource { - source_dir: temp_dir.clone(), - archive_name, - archive_excludes, - _temp_dir: Some(temp_dir), - }) - } - } -} diff --git a/crates/mozart-core/src/repository/downloader.rs b/crates/mozart-core/src/repository/downloader.rs index 56b3652..f2e33a7 100644 --- a/crates/mozart-core/src/repository/downloader.rs +++ b/crates/mozart-core/src/repository/downloader.rs @@ -80,7 +80,7 @@ impl DownloadProgress { /// Downloaded bytes are cached by URL in `files_cache`; cache hits skip the network request /// entirely. #[tracing::instrument(skip(expected_shasum, progress, files_cache))] -pub async fn download_dist( +pub(crate) async fn download_dist( url: &str, expected_shasum: Option<&str>, progress: Option<&mut DownloadProgress>, diff --git a/crates/mozart-core/src/repository/installer_executor/filesystem.rs b/crates/mozart-core/src/repository/installer_executor/filesystem.rs index 2b34e02..05143a0 100644 --- a/crates/mozart-core/src/repository/installer_executor/filesystem.rs +++ b/crates/mozart-core/src/repository/installer_executor/filesystem.rs @@ -146,22 +146,18 @@ fn install_from_source( match source_type { "git" => { let process = crate::vcs::process::ProcessExecutor::new(); - let git_util = - crate::vcs::util::git::GitUtil::new(process, vendor_dir.join(".cache").join("git")); - let downloader = GitDownloader::new(git_util); + let downloader = GitDownloader::new(process, vendor_dir.join(".cache").join("git")); downloader.download(url, reference, &target)?; downloader.install(url, reference, &target)?; } "svn" => { let process = crate::vcs::process::ProcessExecutor::new(); - let svn_util = crate::vcs::util::svn::SvnUtil::new(process); - let downloader = SvnDownloader::new(svn_util); + let downloader = SvnDownloader::new(process); downloader.install(url, reference, &target)?; } "hg" => { let process = crate::vcs::process::ProcessExecutor::new(); - let hg_util = crate::vcs::util::hg::HgUtil::new(process); - let downloader = HgDownloader::new(hg_util); + let downloader = HgDownloader::new(process); downloader.install(url, reference, &target)?; } _ => { diff --git a/crates/mozart-core/src/util.rs b/crates/mozart-core/src/util.rs new file mode 100644 index 0000000..ced0e0e --- /dev/null +++ b/crates/mozart-core/src/util.rs @@ -0,0 +1,3 @@ +mod filesystem; + +pub use filesystem::*; diff --git a/crates/mozart-core/src/util/filesystem.rs b/crates/mozart-core/src/util/filesystem.rs new file mode 100644 index 0000000..efcb0fb --- /dev/null +++ b/crates/mozart-core/src/util/filesystem.rs @@ -0,0 +1,8 @@ +#[derive(Default)] +pub struct Filesystem; + +impl Filesystem { + pub fn new() -> Self { + Self {} + } +} diff --git a/crates/mozart-core/tests/git_driver_test.rs b/crates/mozart-core/tests/git_driver_test.rs index 2b2db57..8d5ba1a 100644 --- a/crates/mozart-core/tests/git_driver_test.rs +++ b/crates/mozart-core/tests/git_driver_test.rs @@ -2,7 +2,6 @@ use mozart_core::downloader::{GitDownloader, VcsDownloader}; use mozart_core::repository::vcs::{DriverConfig, DriverType, create_driver, detect_driver}; use mozart_core::vcs::process::ProcessExecutor; use mozart_core::vcs::repository::VcsRepository; -use mozart_core::vcs::util::git::GitUtil; use std::path::Path; use std::process::Command; use tempfile::TempDir; @@ -148,9 +147,7 @@ fn test_git_downloader() { let install_dir = TempDir::new().unwrap(); create_test_repo(repo_dir.path()); - let process = ProcessExecutor::new(); - let git_util = GitUtil::new(process, cache_dir.path().join("git")); - let downloader = GitDownloader::new(git_util); + let downloader = GitDownloader::new(ProcessExecutor::new(), cache_dir.path().join("git")); let url = repo_dir.path().to_str().unwrap(); let target = install_dir.path().join("test-package"); @@ -203,9 +200,7 @@ fn test_git_downloader_unpushed_changes() { let install_dir = TempDir::new().unwrap(); create_test_repo(repo_dir.path()); - let process = ProcessExecutor::new(); - let git_util = GitUtil::new(process, cache_dir.path().join("git")); - let downloader = GitDownloader::new(git_util); + let downloader = GitDownloader::new(ProcessExecutor::new(), cache_dir.path().join("git")); let url = repo_dir.path().to_str().unwrap(); let target = install_dir.path().join("test-package"); -- cgit v1.3.1