aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/mozart/src')
-rw-r--r--crates/mozart/src/commands/archive.rs367
1 files changed, 116 insertions, 251 deletions
diff --git a/crates/mozart/src/commands/archive.rs b/crates/mozart/src/commands/archive.rs
index 4655edc..82af052 100644
--- a/crates/mozart/src/commands/archive.rs
+++ b/crates/mozart/src/commands/archive.rs
@@ -1,9 +1,10 @@
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;
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
#[derive(Args)]
pub struct ArchiveArgs {
@@ -30,72 +31,11 @@ pub struct ArchiveArgs {
pub ignore_filters: bool,
}
-/// Read `archive.name` and `archive.exclude` from a composer.json file.
-fn read_archive_config(
- composer_json_path: &std::path::Path,
-) -> anyhow::Result<(Option<String>, Vec<String>)> {
- 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))
-}
-
-struct PackageMeta {
- source_dir: PathBuf,
- package_name: String,
- archive_name: Option<String>,
- archive_excludes: Vec<String>,
- version: Option<String>,
- dist_reference: Option<String>,
- dist_type: Option<String>,
- source_reference: Option<String>,
- /// Holds an optional temp directory that must outlive `source_dir`.
- _temp_dir: Option<PathBuf>,
-}
-
-impl Drop for PackageMeta {
- fn drop(&mut self) {
- // Clean up temporary directory used for remote packages
- if let Some(ref dir) = self._temp_dir {
- let _ = std::fs::remove_dir_all(dir);
- }
- }
-}
-
pub async fn execute(
args: &ArchiveArgs,
cli: &super::Cli,
- console: &mozart_core::console::Console,
+ io: &mozart_core::console::Console,
) -> anyhow::Result<()> {
- use mozart_archiver::{
- ArchiveFormat, collect_archivable_files, create_archive, generate_archive_filename,
- parse_composer_excludes, parse_gitattributes, parse_gitignore_pattern,
- self_exclusion_patterns,
- };
-
- let cache_config = mozart_registry::cache::build_cache_config(cli.no_cache);
- let repo_cache = mozart_registry::cache::Cache::repo(&cache_config);
- let files_cache = mozart_registry::cache::Cache::files(&cache_config);
-
- // 1. Determine working directory
let working_dir = cli.working_dir()?;
let composer = Composer::try_load(&working_dir)?;
@@ -105,254 +45,179 @@ pub async fn execute(
Cow::Owned(create_config()?)
};
- let format_str = args.format.as_deref().unwrap_or(&config.archive_format);
- let format = ArchiveFormat::parse(format_str).ok_or_else(|| {
- anyhow::anyhow!(
- "Unsupported archive format \"{}\". Supported formats: tar, tar.gz, tar.bz2, zip",
- format_str
- )
- })?;
+ let format = args.format.as_deref().unwrap_or(&config.archive_format);
+ let dir = args.dir.as_deref().unwrap_or(&config.archive_dir);
- let output_dir_str = args.dir.as_deref().unwrap_or(&config.archive_dir);
- let output_dir = if std::path::Path::new(output_dir_str).is_absolute() {
- PathBuf::from(output_dir_str)
- } else {
- working_dir.join(output_dir_str)
- };
- std::fs::create_dir_all(&output_dir)?;
+ archive(
+ io,
+ args.package.as_deref(),
+ args.version.as_deref(),
+ format,
+ dir,
+ args.file.as_deref(),
+ args.ignore_filters,
+ &working_dir,
+ cli.no_cache,
+ )
+ .await
+}
- // 5. Determine source directory and package metadata
- let meta: PackageMeta = if let Some(ref pkg_name) = args.package {
- // Remote package mode
- console.info("Searching for the specified package.");
- resolve_remote_package(
- pkg_name,
- args.version.as_deref(),
- &repo_cache,
- &files_cache,
- console,
- )
- .await?
+#[allow(clippy::too_many_arguments)]
+async fn archive(
+ io: &mozart_core::console::Console,
+ package_name: Option<&str>,
+ version: Option<&str>,
+ format: &str,
+ dest: &str,
+ file_name: Option<&str>,
+ ignore_filters: bool,
+ working_dir: &Path,
+ no_cache: bool,
+) -> anyhow::Result<()> {
+ let cache_config = mozart_registry::cache::build_cache_config(no_cache);
+ let repo_cache = mozart_registry::cache::Cache::repo(&cache_config);
+ let files_cache = mozart_registry::cache::Cache::files(&cache_config);
+
+ let archive_manager = ArchiveManager::new();
+
+ let package = if let Some(package_name) = package_name {
+ select_package(io, package_name, version, &repo_cache).await?
} else {
- let composer_json_path = working_dir.join("composer.json");
- // Root package mode
- if !composer_json_path.exists() {
- anyhow::bail!("No composer.json found in {}", working_dir.display());
- }
- let root = mozart_core::package::read_from_file(&composer_json_path)?;
- let (archive_name, archive_excludes) = read_archive_config(&composer_json_path)?;
- let version = root
- .extra_fields
- .get("version")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
- PackageMeta {
- source_dir: working_dir.clone(),
- package_name: root.name.clone(),
- archive_name,
- archive_excludes,
- version,
- dist_reference: None,
- dist_type: None,
- source_reference: None,
- _temp_dir: None,
- }
+ load_root_package(working_dir)?
};
- // 6. Generate output filename
- let filename_base = if let Some(ref f) = args.file {
- f.clone()
+ let dest_dir = if Path::new(dest).is_absolute() {
+ PathBuf::from(dest)
} else {
- generate_archive_filename(
- &meta.package_name,
- meta.archive_name.as_deref(),
- meta.version.as_deref(),
- meta.dist_reference.as_deref(),
- meta.dist_type.as_deref(),
- meta.source_reference.as_deref(),
- )
+ working_dir.join(dest)
};
- // 7. Build exclude patterns
- // Self-exclusion: prevent the archive from including itself
- let has_extra_parts = args.file.is_none()
- && (meta.version.is_some()
- || meta.dist_reference.is_some()
- || meta.source_reference.is_some());
- let self_exclusion_strs = self_exclusion_patterns(&filename_base, has_extra_parts);
-
- let mut all_patterns = Vec::new();
+ io.info(&format!("Creating the archive into \"{}\".", dest));
+ let package_path = archive_manager
+ .archive(
+ &package,
+ format,
+ &dest_dir,
+ file_name,
+ ignore_filters,
+ &files_cache,
+ )
+ .await?;
- // Self-exclusion always applies
- for rule in &self_exclusion_strs {
- if let Some(p) = parse_gitignore_pattern(rule) {
- all_patterns.push(p);
- }
- }
+ let absolute = package_path.display().to_string();
+ let short_path = package_path
+ .strip_prefix(working_dir)
+ .ok()
+ .map(|rel| rel.display().to_string())
+ .filter(|rel| rel.len() < absolute.len())
+ .unwrap_or(absolute);
+ console_writeln!(io, &format!("Created: {}", short_path));
- if !args.ignore_filters {
- // Parse .gitattributes export-ignore rules
- let git_patterns = parse_gitattributes(&meta.source_dir);
- all_patterns.extend(git_patterns);
+ Ok(())
+}
- // Parse composer.json archive.exclude rules
- let composer_patterns = parse_composer_excludes(&meta.archive_excludes);
- all_patterns.extend(composer_patterns);
+fn load_root_package(working_dir: &Path) -> anyhow::Result<ArchivePackage> {
+ let composer_json_path = working_dir.join("composer.json");
+ if !composer_json_path.exists() {
+ anyhow::bail!("No composer.json found in {}", working_dir.display());
}
-
- // 8. Collect files
- let files = collect_archivable_files(&meta.source_dir, &all_patterns)?;
-
- // 9. Create archive
- let target_path = output_dir.join(format!("{}.{}", filename_base, format.extension()));
- console.info(&format!(
- "Creating the archive into \"{}\".",
- output_dir.display()
- ));
- create_archive(&meta.source_dir, &files, &target_path, &format)?;
-
- // Print relative path if possible
- let display_path = if let Ok(rel) = target_path.strip_prefix(&working_dir) {
- rel.display().to_string()
- } else {
- target_path.display().to_string()
- };
- console_writeln!(console, &format!("Created: {}", display_path),);
-
- Ok(())
+ let root = mozart_core::package::read_from_file(&composer_json_path)?;
+ let version = root
+ .extra_fields
+ .get("version")
+ .and_then(|v| v.as_str())
+ .map(|s| s.to_string());
+ Ok(ArchivePackage::Root {
+ name: root.name.clone(),
+ version,
+ source_dir: working_dir.to_path_buf(),
+ })
}
-async fn resolve_remote_package(
+async fn select_package(
+ io: &mozart_core::console::Console,
package_name: &str,
- version_constraint: Option<&str>,
+ version: Option<&str>,
repo_cache: &mozart_registry::cache::Cache,
- files_cache: &mozart_registry::cache::Cache,
- console: &mozart_core::console::Console,
-) -> anyhow::Result<PackageMeta> {
+) -> anyhow::Result<ArchivePackage> {
use mozart_core::package::Stability;
use mozart_registry::version::find_best_candidate;
- // Parse @stability suffix from version constraint (e.g. "^1.0@beta" → "^1.0", Stability::Beta)
- let (constraint_stripped, stability) = if let Some(raw) = version_constraint {
+ io.info("Searching for the specified package.");
+
+ // Strip @stability suffix from the version constraint (e.g. "^1.0@beta" → "^1.0", Stability::Beta)
+ let (version, min_stability) = if let Some(raw) = version {
if let Some(at_pos) = raw.find('@') {
- let ver_part = raw[..at_pos].trim();
+ let ver_part = raw[..at_pos].trim().to_string();
let stab_part = raw[at_pos + 1..].trim();
- let stab = Stability::parse(stab_part);
- (Some(ver_part.to_string()), stab)
+ (Some(ver_part), Stability::parse(stab_part))
} else {
(Some(raw.to_string()), Stability::Stable)
}
} else {
(None, Stability::Stable)
};
- let version_constraint = constraint_stripped.as_deref();
+ let version = version.as_deref();
- // Fetch versions from Packagist
- let versions =
+ let packages =
mozart_registry::packagist::fetch_package_versions(package_name, repo_cache).await?;
- if versions.is_empty() {
+ if packages.is_empty() {
anyhow::bail!("No versions found for package \"{}\"", package_name);
}
- // Apply version constraint filtering if given
- let candidate = if let Some(constraint) = version_constraint {
- let matches: Vec<_> = versions
+ let package = if let Some(version) = version {
+ let matches: Vec<_> = packages
.iter()
- .filter(|v| v.version == constraint || v.version_normalized.starts_with(constraint))
+ .filter(|v| v.version == version || v.version_normalized.starts_with(version))
.collect();
if matches.is_empty() {
anyhow::bail!(
"Could not find version \"{}\" for package \"{}\"",
- constraint,
+ version,
package_name
);
}
- let best = matches[0];
+ let package = matches[0];
if matches.len() > 1 {
- console.info(&format!(
+ io.info(&format!(
"Found multiple matches, selected {} {}.",
- package_name, best.version
+ package_name, package.version
));
} else {
- console.info(&format!(
+ io.info(&format!(
"Found an exact match {} {}.",
- package_name, best.version
+ package_name, package.version
));
}
- best
+ package
} else {
- let best = find_best_candidate(&versions, stability)
- .or_else(|| find_best_candidate(&versions, Stability::Dev))
+ let package = find_best_candidate(&packages, min_stability)
+ .or_else(|| find_best_candidate(&packages, Stability::Dev))
.ok_or_else(|| {
anyhow::anyhow!("No suitable version found for package \"{}\"", package_name)
})?;
- console.info(&format!(
+ io.info(&format!(
"Found an exact match {} {}.",
- package_name, best.version
+ package_name, package.version
));
- best
+ package
};
- let dist = candidate.dist.as_ref().ok_or_else(|| {
+ let dist = package.dist.as_ref().ok_or_else(|| {
anyhow::anyhow!(
"Package \"{}\" version \"{}\" has no dist available",
package_name,
- candidate.version
+ package.version
)
})?;
- // Create a temp directory using std (not tempfile crate, which is dev-only)
- 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 = mozart_registry::downloader::download_dist(
- &dist.url,
- dist.shasum.as_deref(),
- None,
- files_cache,
- )
- .await?;
-
- match dist.dist_type.as_str() {
- "zip" => mozart_registry::downloader::extract_zip(&bytes, &temp_dir)?,
- "tar" | "tar.gz" | "tgz" => mozart_registry::downloader::extract_tar_gz(&bytes, &temp_dir)?,
- other => {
- let _ = std::fs::remove_dir_all(&temp_dir);
- anyhow::bail!("Unsupported dist type: {}", other);
- }
- }
-
- // Try to read composer.json from the extracted source for archive.name / archive.exclude
- 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![])
- };
-
- let version: Option<String> = Some(candidate.version.clone());
- let dist_reference: Option<String> = dist.reference.clone();
- let dist_type: Option<String> = Some(dist.dist_type.clone());
- let source_reference: Option<String> =
- candidate.source.as_ref().and_then(|s| s.reference.clone());
-
- Ok(PackageMeta {
- source_dir: temp_dir.clone(),
- package_name: package_name.to_string(),
- archive_name,
- archive_excludes,
- version,
- dist_reference,
- dist_type,
- source_reference,
- _temp_dir: Some(temp_dir),
+ Ok(ArchivePackage::Remote {
+ name: package_name.to_string(),
+ version: package.version.clone(),
+ dist_url: dist.url.clone(),
+ dist_type: dist.dist_type.clone(),
+ dist_shasum: dist.shasum.clone(),
+ dist_reference: dist.reference.clone(),
+ source_reference: package.source.as_ref().and_then(|s| s.reference.clone()),
})
}