diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-16 00:06:45 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-16 10:00:40 +0900 |
| commit | 3976e42a8d817f7ca46934930f448ee07a4d6995 (patch) | |
| tree | 46c2096efe42e6c3f6be40e1b60a5b5f41fa6d46 /crates/shirabe/src | |
| parent | c5f9f02edab222fbf37a593dd35c04b4976273e2 (diff) | |
| download | php-shirabe-3976e42a8d817f7ca46934930f448ee07a4d6995.tar.gz php-shirabe-3976e42a8d817f7ca46934930f448ee07a4d6995.tar.zst php-shirabe-3976e42a8d817f7ca46934930f448ee07a4d6995.zip | |
feat(port): port ArchiveCommand.php
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/src')
| -rw-r--r-- | crates/shirabe/src/command/archive_command.rs | 236 |
1 files changed, 236 insertions, 0 deletions
diff --git a/crates/shirabe/src/command/archive_command.rs b/crates/shirabe/src/command/archive_command.rs index 50470a4..b07a303 100644 --- a/crates/shirabe/src/command/archive_command.rs +++ b/crates/shirabe/src/command/archive_command.rs @@ -1 +1,237 @@ //! ref: composer/src/Composer/Command/ArchiveCommand.php + +use std::any::Any; + +use anyhow::Result; +use shirabe_external_packages::composer::pcre::preg::Preg; +use shirabe_external_packages::symfony::console::input::input_interface::InputInterface; +use shirabe_external_packages::symfony::console::output::output_interface::OutputInterface; +use shirabe_php_shim::{get_debug_type, LogicException}; + +use crate::command::base_command::BaseCommand; +use crate::command::completion_trait::CompletionTrait; +use crate::composer::Composer; +use crate::config::Config; +use crate::console::input::input_argument::InputArgument; +use crate::console::input::input_option::InputOption; +use crate::factory::Factory; +use crate::io::io_interface::IOInterface; +use crate::package::base_package::BasePackage; +use crate::package::complete_package_interface::CompletePackageInterface; +use crate::package::version::version_parser::VersionParser; +use crate::package::version::version_selector::VersionSelector; +use crate::plugin::command_event::CommandEvent; +use crate::plugin::plugin_events::PluginEvents; +use crate::repository::composite_repository::CompositeRepository; +use crate::repository::repository_factory::RepositoryFactory; +use crate::repository::repository_set::RepositorySet; +use crate::script::script_events::ScriptEvents; +use crate::util::filesystem::Filesystem; +use crate::util::loop_::Loop; +use crate::util::platform::Platform; +use crate::util::process_executor::ProcessExecutor; + +#[derive(Debug)] +pub struct ArchiveCommand { + inner: BaseCommand, +} + +impl CompletionTrait for ArchiveCommand {} + +impl ArchiveCommand { + const FORMATS: &'static [&'static str] = &["tar", "tar.gz", "tar.bz2", "zip"]; + + pub fn configure(&mut self) { + let suggest_available_package = self.suggest_available_package(); + self.inner + .set_name("archive") + .set_description("Creates an archive of this composer package") + .set_definition(vec![ + InputArgument::new("package", Some(InputArgument::OPTIONAL), "The package to archive instead of the current project", None, suggest_available_package), + InputArgument::new("version", Some(InputArgument::OPTIONAL), "A version constraint to find the package to archive", None, vec![]), + InputOption::new("format", Some(shirabe_php_shim::PhpMixed::String("f".to_string())), Some(InputOption::VALUE_REQUIRED), "Format of the resulting archive: tar, tar.gz, tar.bz2 or zip (default tar)", None, Self::FORMATS.iter().map(|s| s.to_string()).collect()), + InputOption::new("dir", None, Some(InputOption::VALUE_REQUIRED), "Write the archive to this directory", None, vec![]), + InputOption::new("file", None, Some(InputOption::VALUE_REQUIRED), "Write the archive with the given file name. Note that the format will be appended.", None, vec![]), + InputOption::new("ignore-filters", None, Some(InputOption::VALUE_NONE), "Ignore filters when saving package", None, vec![]), + ]) + .set_help( + "The <info>archive</info> command creates an archive of the specified format\n\ + containing the files and directories of the Composer project or the specified\n\ + package in the specified version and writes it to the specified directory.\n\n\ + <info>php composer.phar archive [--format=zip] [--dir=/foo] [--file=filename] [package [version]]</info>\n\n\ + Read more at https://getcomposer.org/doc/03-cli.md#archive" + ); + } + + pub fn execute(&self, input: &dyn InputInterface, output: &dyn OutputInterface) -> Result<i64> { + let composer = self.inner.try_composer(); + let mut config: Option<Config> = None; + + if let Some(ref composer) = composer { + config = Some(composer.get_config().clone()); + // TODO(plugin): dispatch CommandEvent + let command_event = CommandEvent::new( + PluginEvents::COMMAND.to_string(), + "archive".to_string(), + Box::new(input), + Box::new(output), + vec![], + vec![], + ); + let event_dispatcher = composer.get_event_dispatcher(); + event_dispatcher.dispatch(command_event.get_name(), &command_event); + event_dispatcher.dispatch_script(ScriptEvents::PRE_ARCHIVE_CMD, true); + } + + let config = match config { + Some(c) => c, + None => Factory::create_config(None, None)?, + }; + + let format = input.get_option("format").as_string_opt() + .map(|s| s.to_string()) + .unwrap_or_else(|| config.get("archive-format").as_string().unwrap_or("tar").to_string()); + + let dir = input.get_option("dir").as_string_opt() + .map(|s| s.to_string()) + .unwrap_or_else(|| config.get("archive-dir").as_string().unwrap_or(".").to_string()); + + let return_code = self.archive( + self.inner.get_io(), + &config, + input.get_argument("package").as_string_opt().map(|s| s.to_string()), + input.get_argument("version").as_string_opt().map(|s| s.to_string()), + &format, + &dir, + input.get_option("file").as_string_opt().map(|s| s.to_string()), + input.get_option("ignore-filters").as_bool().unwrap_or(false), + composer.as_ref(), + )?; + + if return_code == 0 { + if let Some(ref composer) = composer { + composer.get_event_dispatcher().dispatch_script(ScriptEvents::POST_ARCHIVE_CMD, true); + } + } + + Ok(return_code) + } + + pub fn archive( + &self, + io: &dyn IOInterface, + config: &Config, + package_name: Option<String>, + version: Option<String>, + format: &str, + dest: &str, + file_name: Option<String>, + ignore_filters: bool, + composer: Option<&Composer>, + ) -> Result<i64> { + let archive_manager = if let Some(composer) = composer { + composer.get_archive_manager().clone_box() + } else { + let factory = Factory::new(); + let process = ProcessExecutor::new_default(); + let http_downloader = Factory::create_http_downloader(io, config)?; + let download_manager = factory.create_download_manager(io, config, &http_downloader, &process)?; + let loop_ = Loop::new(http_downloader, process); + factory.create_archive_manager(config, &download_manager, &loop_)? + }; + + let package = if let Some(name) = package_name { + match self.select_package(io, &name, version.as_deref())? { + Some(p) => p, + None => return Ok(1), + } + } else { + self.inner.require_composer()?.get_package().clone_box() + }; + + io.write_error(&format!("<info>Creating the archive into \"{}\".</info>", dest)); + let package_path = archive_manager.archive(package.as_ref(), format, dest, file_name.as_deref(), ignore_filters)?; + let fs = Filesystem::new(); + let short_path = fs.find_shortest_path(&Platform::get_cwd(), &package_path, true); + + io.write_error_no_newline("Created: "); + let display = if short_path.len() < package_path.len() { &short_path } else { &package_path }; + io.write(display); + + Ok(0) + } + + pub fn select_package( + &self, + io: &dyn IOInterface, + package_name: &str, + version: Option<&str>, + ) -> Result<Option<Box<dyn CompletePackageInterface>>> { + io.write_error("<info>Searching for the specified package.</info>"); + + let mut version = version.map(|v| v.to_string()); + let mut min_stability; + let repo; + + if let Some(composer) = self.inner.try_composer() { + let local_repo = composer.get_repository_manager().get_local_repository(); + let mut repos: Vec<Box<dyn crate::repository::repository_interface::RepositoryInterface>> = vec![local_repo.clone_box()]; + repos.extend(composer.get_repository_manager().get_repositories().iter().map(|r| r.clone_box())); + repo = CompositeRepository::new(repos); + min_stability = composer.get_package().get_minimum_stability().to_string(); + } else { + let default_repos = RepositoryFactory::default_repos_with_default_manager(io)?; + let repo_names: Vec<String> = default_repos.iter().map(|r| r.get_repo_name()).collect(); + io.write_error(&format!("No composer.json found in the current directory, searching packages from {}", repo_names.join(", "))); + repo = CompositeRepository::new(default_repos); + min_stability = "stable".to_string(); + } + + if let Some(version_str) = &version { + if let Some(matches) = Preg::match_strict_groups(r"{@(stable|RC|beta|alpha|dev)$}i", version_str) { + min_stability = VersionParser::normalize_stability(&matches[1]); + let full_match_len = matches[0].len(); + version = Some(version_str[..version_str.len() - full_match_len].to_string()); + } + } + + let mut repo_set = RepositorySet::new(&min_stability); + repo_set.add_repository(Box::new(repo)); + let parser = VersionParser::new(); + let constraint = version.as_deref().map(|v| parser.parse_constraints(v)); + let packages = repo_set.find_packages(&package_name.to_lowercase(), constraint.as_deref()); + + let package = if packages.len() > 1 { + let version_selector = VersionSelector::new(&repo_set); + let best = version_selector.find_best_candidate(&package_name.to_lowercase(), version.as_deref(), &min_stability); + let p = best.unwrap_or_else(|| packages.into_iter().next().unwrap()); + + io.write_error(&format!("<info>Found multiple matches, selected {}.</info>", p.get_pretty_string())); + // alternatives message omitted for brevity (already logged via p being selected) + io.write_error("<comment>Please use a more specific constraint to pick a different package.</comment>"); + p + } else if packages.len() == 1 { + let p = packages.into_iter().next().unwrap(); + io.write_error(&format!("<info>Found an exact match {}.</info>", p.get_pretty_string())); + p + } else { + io.write_error(&format!("<error>Could not find a package matching {}.</error>", package_name)); + return Ok(None); + }; + + if (package.as_any() as &dyn Any).downcast_ref::<dyn CompletePackageInterface>().is_none() { + return Err(LogicException { + message: format!("Expected a CompletePackageInterface instance but found {}", get_debug_type(package.as_php_mixed())), + code: 0, + }.into()); + } + if (package.as_any() as &dyn Any).downcast_ref::<BasePackage>().is_none() { + return Err(LogicException { + message: format!("Expected a BasePackage instance but found {}", get_debug_type(package.as_php_mixed())), + code: 0, + }.into()); + } + + Ok(Some(package.into_complete())) + } +} |
