aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-16 21:48:05 +0900
committernsfisis <nsfisis@gmail.com>2026-05-16 22:17:14 +0900
commit14bd899a9ca6df33f95c9ee0e5612669ecb4fb8f (patch)
treee773039b93c8f744af1e3f081ca548e106b816fe /crates
parentdc76780576969b2d3bcdb255dcbb22468b6ac2d3 (diff)
downloadphp-shirabe-14bd899a9ca6df33f95c9ee0e5612669ecb4fb8f.tar.gz
php-shirabe-14bd899a9ca6df33f95c9ee0e5612669ecb4fb8f.tar.zst
php-shirabe-14bd899a9ca6df33f95c9ee0e5612669ecb4fb8f.zip
feat(port): port Factory.php
Diffstat (limited to 'crates')
-rw-r--r--crates/shirabe/src/factory.rs1430
1 files changed, 1430 insertions, 0 deletions
diff --git a/crates/shirabe/src/factory.rs b/crates/shirabe/src/factory.rs
index fcbf24d..6411027 100644
--- a/crates/shirabe/src/factory.rs
+++ b/crates/shirabe/src/factory.rs
@@ -1 +1,1431 @@
//! ref: composer/src/Composer/Factory.php
+
+use indexmap::IndexMap;
+
+use shirabe_external_packages::symfony::component::console::formatter::output_formatter::OutputFormatter;
+use shirabe_external_packages::symfony::component::console::formatter::output_formatter_style::OutputFormatterStyle;
+use shirabe_external_packages::symfony::component::console::output::console_output::ConsoleOutput;
+use shirabe_php_shim::{
+ array_keys, array_replace_recursive, class_exists, dirname, extension_loaded, file_exists,
+ file_get_contents, file_put_contents, implode, in_array, is_array, is_dir, is_file, is_string,
+ json_decode, pathinfo, realpath, str_replace, strpos, strtr, substr, trim, InvalidArgumentException,
+ Phar, PhpMixed, RuntimeException, UnexpectedValueException, ZipArchive, PATHINFO_EXTENSION,
+ PHP_EOL,
+};
+
+use crate::autoload::autoload_generator::AutoloadGenerator;
+use crate::composer::Composer;
+use crate::config::Config;
+use crate::config::json_config_source::JsonConfigSource;
+use crate::downloader::download_manager::DownloadManager;
+use crate::downloader::file_downloader::FileDownloader;
+use crate::downloader::fossil_downloader::FossilDownloader;
+use crate::downloader::git_downloader::GitDownloader;
+use crate::downloader::gzip_downloader::GzipDownloader;
+use crate::downloader::hg_downloader::HgDownloader;
+use crate::downloader::path_downloader::PathDownloader;
+use crate::downloader::perforce_downloader::PerforceDownloader;
+use crate::downloader::phar_downloader::PharDownloader;
+use crate::downloader::rar_downloader::RarDownloader;
+use crate::downloader::svn_downloader::SvnDownloader;
+use crate::downloader::tar_downloader::TarDownloader;
+use crate::downloader::transport_exception::TransportException;
+use crate::downloader::xz_downloader::XzDownloader;
+use crate::downloader::zip_downloader::ZipDownloader;
+use crate::event_dispatcher::event::Event;
+use crate::event_dispatcher::event_dispatcher::EventDispatcher;
+use crate::exception::no_ssl_exception::NoSslException;
+use crate::installer::binary_installer::BinaryInstaller;
+use crate::installer::installation_manager::InstallationManager;
+use crate::installer::library_installer::LibraryInstaller;
+use crate::installer::metapackage_installer::MetapackageInstaller;
+use crate::installer::plugin_installer::PluginInstaller;
+use crate::io::io_interface::IOInterface;
+use crate::json::json_file::JsonFile;
+use crate::json::json_validation_exception::JsonValidationException;
+use crate::package::archiver::archive_manager::ArchiveManager;
+use crate::package::archiver::phar_archiver::PharArchiver;
+use crate::package::archiver::zip_archiver::ZipArchiver;
+use crate::package::locker::Locker;
+use crate::package::loader::root_package_loader::RootPackageLoader;
+use crate::package::root_package_interface::RootPackageInterface;
+use crate::package::version::version_guesser::VersionGuesser;
+use crate::package::version::version_parser::VersionParser;
+use crate::partial_composer::PartialComposer;
+use crate::plugin::plugin_events::PluginEvents;
+use crate::plugin::plugin_manager::PluginManager;
+use crate::repository::filesystem_repository::FilesystemRepository;
+use crate::repository::installed_filesystem_repository::InstalledFilesystemRepository;
+use crate::repository::installed_repository_interface::InstalledRepositoryInterface;
+use crate::repository::repository_factory::RepositoryFactory;
+use crate::repository::repository_manager::RepositoryManager;
+use crate::util::cache::Cache;
+use crate::util::filesystem::Filesystem;
+use crate::util::http_downloader::HttpDownloader;
+use crate::util::r#loop::Loop;
+use crate::util::platform::Platform;
+use crate::util::process_executor::ProcessExecutor;
+use crate::util::silencer::Silencer;
+
+/// Either a configuration array or a filename to read from. PHP's `$localConfig` accepts both.
+pub enum LocalConfigInput {
+ Path(String),
+ Data(IndexMap<String, PhpMixed>),
+}
+
+/// PHP's `$disablePlugins` accepts `bool|'local'|'global'`.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum DisablePlugins {
+ None,
+ All,
+ Local,
+ Global,
+}
+
+impl DisablePlugins {
+ fn is_disabled_at_all(self) -> bool {
+ !matches!(self, DisablePlugins::None)
+ }
+}
+
+/// Creates a configured instance of composer.
+pub struct Factory;
+
+impl Factory {
+ fn get_home_dir() -> anyhow::Result<String> {
+ let home = Platform::get_env("COMPOSER_HOME");
+ if let Some(h) = home {
+ if !h.is_empty() {
+ return Ok(h);
+ }
+ }
+
+ if Platform::is_windows() {
+ if Platform::get_env("APPDATA").map(|s| s.is_empty()).unwrap_or(true) {
+ return Err(anyhow::anyhow!(RuntimeException {
+ message:
+ "The APPDATA or COMPOSER_HOME environment variable must be set for composer to run correctly"
+ .to_string(),
+ code: 0,
+ }));
+ }
+
+ let appdata = Platform::get_env("APPDATA").unwrap_or_default();
+ return Ok(format!(
+ "{}/Composer",
+ trim(&strtr(&appdata, "\\", "/"), "/")
+ ));
+ }
+
+ let user_dir = Self::get_user_dir()?;
+ let mut dirs: Vec<String> = Vec::new();
+
+ if Self::use_xdg() {
+ // XDG Base Directory Specifications
+ let mut xdg_config = Platform::get_env("XDG_CONFIG_HOME").unwrap_or_default();
+ if xdg_config.is_empty() {
+ xdg_config = format!("{}/.config", user_dir);
+ }
+
+ dirs.push(format!("{}/composer", xdg_config));
+ }
+
+ dirs.push(format!("{}/.composer", user_dir));
+
+ // select first dir which exists of: $XDG_CONFIG_HOME/composer or ~/.composer
+ for dir in &dirs {
+ let dir_copy = dir.clone();
+ let exists = Silencer::call(|| Ok::<bool, anyhow::Error>(is_dir(&dir_copy)))
+ .unwrap_or(false);
+ if exists {
+ return Ok(dir.clone());
+ }
+ }
+
+ // if none exists, we default to first defined one (XDG one if system uses it, or ~/.composer otherwise)
+ Ok(dirs[0].clone())
+ }
+
+ fn get_cache_dir(home: &str) -> anyhow::Result<String> {
+ let cache_dir = Platform::get_env("COMPOSER_CACHE_DIR").unwrap_or_default();
+ if !cache_dir.is_empty() {
+ return Ok(cache_dir);
+ }
+
+ let home_env = Platform::get_env("COMPOSER_HOME").unwrap_or_default();
+ if !home_env.is_empty() {
+ return Ok(format!("{}/cache", home_env));
+ }
+
+ if Platform::is_windows() {
+ let mut cache_dir = Platform::get_env("LOCALAPPDATA").unwrap_or_default();
+ if !cache_dir.is_empty() {
+ cache_dir = format!("{}/Composer", cache_dir);
+ } else {
+ cache_dir = format!("{}/cache", home);
+ }
+
+ return Ok(trim(&strtr(&cache_dir, "\\", "/"), "/"));
+ }
+
+ let user_dir = Self::get_user_dir()?;
+ if Platform::php_os() == "Darwin" {
+ // Migrate existing cache dir in old location if present
+ if is_dir(&format!("{}/cache", home))
+ && !is_dir(&format!("{}/Library/Caches/composer", user_dir))
+ {
+ let from = format!("{}/cache", home);
+ let to = format!("{}/Library/Caches/composer", user_dir);
+ let _ = Silencer::call(|| {
+ Ok::<bool, anyhow::Error>(Platform::rename(&from, &to))
+ });
+ }
+
+ return Ok(format!("{}/Library/Caches/composer", user_dir));
+ }
+
+ if home == format!("{}/.composer", user_dir).as_str()
+ && is_dir(&format!("{}/cache", home))
+ {
+ return Ok(format!("{}/cache", home));
+ }
+
+ if Self::use_xdg() {
+ let xdg_cache = Platform::get_env("XDG_CACHE_HOME").unwrap_or_default();
+ let xdg_cache = if xdg_cache.is_empty() {
+ format!("{}/.cache", user_dir)
+ } else {
+ xdg_cache
+ };
+
+ return Ok(format!("{}/composer", xdg_cache));
+ }
+
+ Ok(format!("{}/cache", home))
+ }
+
+ fn get_data_dir(home: &str) -> anyhow::Result<String> {
+ let home_env = Platform::get_env("COMPOSER_HOME").unwrap_or_default();
+ if !home_env.is_empty() {
+ return Ok(home_env);
+ }
+
+ if Platform::is_windows() {
+ return Ok(strtr(home, "\\", "/"));
+ }
+
+ let user_dir = Self::get_user_dir()?;
+ if home != format!("{}/.composer", user_dir) && Self::use_xdg() {
+ let xdg_data = Platform::get_env("XDG_DATA_HOME").unwrap_or_default();
+ let xdg_data = if xdg_data.is_empty() {
+ format!("{}/.local/share", user_dir)
+ } else {
+ xdg_data
+ };
+
+ return Ok(format!("{}/composer", xdg_data));
+ }
+
+ Ok(home.to_string())
+ }
+
+ pub fn create_config(io: Option<&dyn IOInterface>, cwd: Option<&str>) -> anyhow::Result<Config> {
+ let cwd = cwd.map(|s| s.to_string()).unwrap_or_else(|| Platform::get_cwd(true));
+
+ let mut config = Config::new(true, cwd);
+
+ // determine and add main dirs to the config
+ let home = Self::get_home_dir()?;
+ let mut defaults: IndexMap<String, PhpMixed> = IndexMap::new();
+ let mut inner: IndexMap<String, PhpMixed> = IndexMap::new();
+ inner.insert("home".to_string(), PhpMixed::String(home.clone()));
+ inner.insert(
+ "cache-dir".to_string(),
+ PhpMixed::String(Self::get_cache_dir(&home)?),
+ );
+ inner.insert(
+ "data-dir".to_string(),
+ PhpMixed::String(Self::get_data_dir(&home)?),
+ );
+ defaults.insert(
+ "config".to_string(),
+ PhpMixed::Array(inner.into_iter().map(|(k, v)| (k, Box::new(v))).collect()),
+ );
+ config.merge(defaults, Config::SOURCE_DEFAULT);
+
+ // load global config
+ let file = JsonFile::new(format!("{}/config.json", config.get_str("home")?), None, io);
+ if file.exists() {
+ if let Some(io_ref) = io {
+ io_ref.write_error(
+ PhpMixed::String(format!("Loading config file {}", file.get_path())),
+ true,
+ <dyn IOInterface>::DEBUG,
+ );
+ }
+ Self::validate_json_schema(io, ValidateJsonInput::File(file.clone()), JsonFile::LAX_SCHEMA, None)?;
+ config.merge(file.read()?, file.get_path().to_string());
+ }
+ config.set_config_source(JsonConfigSource::new(file.clone(), false));
+
+ let htaccess_protect = config
+ .get("htaccess-protect")
+ .and_then(|v| v.as_bool())
+ .unwrap_or(false);
+ if htaccess_protect {
+ // Protect directory against web access. Since HOME could be
+ // the www-data's user home and be web-accessible it is a
+ // potential security risk
+ let dirs = [
+ config.get_str("home")?,
+ config.get_str("cache-dir")?,
+ config.get_str("data-dir")?,
+ ];
+ for dir in &dirs {
+ if !file_exists(&format!("{}/.htaccess", dir)) {
+ if !is_dir(dir) {
+ let dir_owned = dir.clone();
+ let _ = Silencer::call(|| {
+ Ok::<bool, anyhow::Error>(Platform::mkdir(&dir_owned, 0o777, true))
+ });
+ }
+ let path = format!("{}/.htaccess", dir);
+ let _ = Silencer::call(|| {
+ Ok::<Option<i64>, anyhow::Error>(file_put_contents(&path, b"Deny from all"))
+ });
+ }
+ }
+ }
+
+ // load global auth file
+ let auth_file = JsonFile::new(
+ format!("{}/auth.json", config.get_str("home")?),
+ None,
+ io,
+ );
+ if auth_file.exists() {
+ if let Some(io_ref) = io {
+ io_ref.write_error(
+ PhpMixed::String(format!("Loading config file {}", auth_file.get_path())),
+ true,
+ <dyn IOInterface>::DEBUG,
+ );
+ }
+ Self::validate_json_schema(
+ io,
+ ValidateJsonInput::File(auth_file.clone()),
+ JsonFile::AUTH_SCHEMA,
+ None,
+ )?;
+ let mut wrapped: IndexMap<String, PhpMixed> = IndexMap::new();
+ wrapped.insert("config".to_string(), PhpMixed::Array(auth_file.read()?
+ .into_iter().map(|(k, v)| (k, Box::new(v))).collect()));
+ config.merge(wrapped, auth_file.get_path().to_string());
+ }
+ config.set_auth_config_source(JsonConfigSource::new(auth_file, true));
+
+ Self::load_composer_auth_env(&mut config, io)?;
+
+ Ok(config)
+ }
+
+ pub fn get_composer_file() -> anyhow::Result<String> {
+ let env = Platform::get_env("COMPOSER");
+ if let Some(env_str) = env {
+ let env_trimmed = trim(&env_str, " \t\n\r\0\u{0B}");
+ if env_trimmed != "" {
+ if is_dir(&env_trimmed) {
+ return Err(anyhow::anyhow!(RuntimeException {
+ message: format!(
+ "The COMPOSER environment variable is set to {} which is a directory, this variable should point to a composer.json or be left unset.",
+ env_trimmed
+ ),
+ code: 0,
+ }));
+ }
+
+ return Ok(env_trimmed);
+ }
+ }
+
+ Ok("./composer.json".to_string())
+ }
+
+ pub fn get_lock_file(composer_file: &str) -> String {
+ let ext = pathinfo(PhpMixed::String(composer_file.to_string()), PATHINFO_EXTENSION);
+ let is_json = match ext {
+ PhpMixed::String(s) => s == "json",
+ _ => false,
+ };
+ if is_json {
+ format!("{}lock", substr(composer_file, 0, Some(composer_file.len() as i64 - 4)))
+ } else {
+ format!("{}.lock", composer_file)
+ }
+ }
+
+ pub fn create_additional_styles() -> IndexMap<String, OutputFormatterStyle> {
+ let mut styles: IndexMap<String, OutputFormatterStyle> = IndexMap::new();
+ styles.insert(
+ "highlight".to_string(),
+ OutputFormatterStyle::new(Some("red".to_string()), None, Vec::new()),
+ );
+ styles.insert(
+ "warning".to_string(),
+ OutputFormatterStyle::new(Some("black".to_string()), Some("yellow".to_string()), Vec::new()),
+ );
+ styles
+ }
+
+ pub fn create_output() -> ConsoleOutput {
+ let styles = Self::create_additional_styles();
+ let formatter = OutputFormatter::new(false, styles);
+
+ ConsoleOutput::new_with_formatter(ConsoleOutput::VERBOSITY_NORMAL, None, formatter)
+ }
+
+ /// Creates a Composer instance
+ pub fn create_composer(
+ &self,
+ io: &dyn IOInterface,
+ local_config: Option<LocalConfigInput>,
+ disable_plugins: DisablePlugins,
+ cwd: Option<&str>,
+ full_load: bool,
+ disable_scripts: bool,
+ ) -> anyhow::Result<PartialComposerOrComposer> {
+ // if a custom composer.json path is given, we change the default cwd to be that file's directory
+ let mut local_config = local_config;
+ let mut cwd = cwd.map(|s| s.to_string());
+ if let Some(LocalConfigInput::Path(ref s)) = local_config {
+ if is_file(s) && cwd.is_none() {
+ cwd = Some(dirname(s));
+ }
+ }
+
+ let cwd = cwd.unwrap_or_else(|| Platform::get_cwd(true));
+
+ // load Composer configuration
+ if local_config.is_none() {
+ local_config = Some(LocalConfigInput::Path(Self::get_composer_file()?));
+ }
+
+ let mut local_config_source = Config::SOURCE_UNKNOWN.to_string();
+ let mut composer_file: Option<String> = None;
+ let mut local_config_data: IndexMap<String, PhpMixed> = IndexMap::new();
+ if let Some(LocalConfigInput::Path(path)) = &local_config {
+ composer_file = Some(path.clone());
+
+ let file = JsonFile::new(path.clone(), None, Some(io));
+
+ if !file.exists() {
+ let message = if path == "./composer.json" || path == "composer.json" {
+ format!("Composer could not find a composer.json file in {}", cwd)
+ } else {
+ format!("Composer could not find the config file: {}", path)
+ };
+ let instructions = if full_load {
+ "To initialize a project, please create a composer.json file. See https://getcomposer.org/basic-usage"
+ } else {
+ ""
+ };
+ return Err(anyhow::anyhow!(InvalidArgumentException {
+ message: format!("{}{}{}", message, PHP_EOL, instructions),
+ code: 0,
+ }));
+ }
+
+ if !Platform::is_input_completion_process() {
+ if let Err(e) = file.validate_schema(JsonFile::LAX_SCHEMA) {
+ if let Some(jve) = e.downcast_ref::<JsonValidationException>() {
+ let errors =
+ format!(" - {}", implode(&format!("{} - ", PHP_EOL), jve.get_errors()));
+ let message =
+ format!("{}:{}{}", jve.get_message(), PHP_EOL, errors);
+ return Err(anyhow::anyhow!(JsonValidationException::new(
+ message,
+ jve.get_errors().clone(),
+ )));
+ }
+ return Err(e);
+ }
+ }
+
+ local_config_data = file.read()?;
+ local_config_source = file.get_path().to_string();
+ } else if let Some(LocalConfigInput::Data(data)) = local_config {
+ local_config_data = data;
+ }
+
+ // Load config and override with local config/auth config
+ let mut config = Self::create_config(Some(io), Some(&cwd))?;
+ let is_global = local_config_source != Config::SOURCE_UNKNOWN
+ && realpath(&config.get_str("home")?) == realpath(&dirname(&local_config_source));
+ config.merge(local_config_data.clone(), local_config_source.clone());
+
+ if let Some(ref composer_file_path) = composer_file {
+ io.write_error(
+ PhpMixed::String(format!(
+ "Loading config file {} ({})",
+ composer_file_path,
+ realpath(composer_file_path).unwrap_or_default()
+ )),
+ true,
+ <dyn IOInterface>::DEBUG,
+ );
+ config.set_config_source(JsonConfigSource::new(
+ JsonFile::new(realpath(composer_file_path).unwrap_or_default(), None, Some(io)),
+ false,
+ ));
+
+ let local_auth_file = JsonFile::new(
+ format!(
+ "{}/auth.json",
+ dirname(&realpath(composer_file_path).unwrap_or_default())
+ ),
+ None,
+ Some(io),
+ );
+ if local_auth_file.exists() {
+ io.write_error(
+ PhpMixed::String(format!(
+ "Loading config file {}",
+ local_auth_file.get_path()
+ )),
+ true,
+ <dyn IOInterface>::DEBUG,
+ );
+ Self::validate_json_schema(
+ Some(io),
+ ValidateJsonInput::File(local_auth_file.clone()),
+ JsonFile::AUTH_SCHEMA,
+ None,
+ )?;
+ let mut wrapped: IndexMap<String, PhpMixed> = IndexMap::new();
+ wrapped.insert(
+ "config".to_string(),
+ PhpMixed::Array(
+ local_auth_file
+ .read()?
+ .into_iter()
+ .map(|(k, v)| (k, Box::new(v)))
+ .collect(),
+ ),
+ );
+ config.merge(wrapped, local_auth_file.get_path().to_string());
+ config.set_local_auth_config_source(JsonConfigSource::new(local_auth_file, true));
+ }
+ }
+
+ // make sure we load the auth env again over the local auth.json + composer.json config
+ Self::load_composer_auth_env(&mut config, Some(io))?;
+
+ let vendor_dir = config.get_str("vendor-dir")?;
+
+ // initialize composer
+ let mut composer: PartialComposerOrComposer = if full_load {
+ PartialComposerOrComposer::Full(Composer::new())
+ } else {
+ PartialComposerOrComposer::Partial(PartialComposer::default())
+ };
+ composer.set_config(config.clone());
+ if is_global {
+ composer.set_global();
+ }
+
+ if full_load {
+ // load auth configs into the IO instance
+ io.load_configuration(&config);
+
+ // load existing Composer\InstalledVersions instance if available and scripts/plugins are allowed, as they might need it
+ // we only load if the InstalledVersions class wasn't defined yet so that this is only loaded once
+ let installed_versions_path =
+ format!("{}/composer/installed.php", config.get_str("vendor-dir")?);
+ if !disable_plugins.is_disabled_at_all()
+ && !disable_scripts
+ && !class_exists("Composer\\InstalledVersions")
+ && file_exists(&installed_versions_path)
+ {
+ // force loading the class at this point so it is loaded from the composer phar and not from the vendor dir
+ // as we cannot guarantee integrity of that file
+ if class_exists("Composer\\InstalledVersions") {
+ FilesystemRepository::safely_load_installed_versions(&installed_versions_path);
+ }
+ }
+ }
+
+ let http_downloader = Self::create_http_downloader(io, &config, IndexMap::new())?;
+ let process = ProcessExecutor::new(io);
+ let r#loop = Loop::new(http_downloader.clone(), process.clone());
+ composer.set_loop(r#loop.clone());
+
+ // initialize event dispatcher
+ let mut dispatcher = EventDispatcher::new(
+ composer.as_partial(),
+ io.clone_box(),
+ Some(process.clone()),
+ );
+ dispatcher.set_run_scripts(!disable_scripts);
+ composer.set_event_dispatcher(dispatcher.clone());
+
+ // initialize repository manager
+ let rm = RepositoryFactory::manager(
+ io,
+ &config,
+ &http_downloader,
+ &dispatcher,
+ &process,
+ )?;
+ composer.set_repository_manager(rm.clone());
+
+ // force-set the version of the global package if not defined as
+ // guessing it adds no value and only takes time
+ if !full_load && !local_config_data.contains_key("version") {
+ local_config_data.insert(
+ "version".to_string(),
+ PhpMixed::String("1.0.0".to_string()),
+ );
+ }
+
+ // load package
+ let parser = VersionParser::new();
+ let guesser = VersionGuesser::new(&config, process.clone(), parser.clone());
+ let mut loader = self.load_root_package(rm.clone(), config.clone(), parser, guesser, io.clone_box());
+ let package = loader.load(
+ local_config_data
+ .iter()
+ .map(|(k, v)| (k.clone(), Box::new(v.clone())))
+ .collect(),
+ "Composer\\Package\\RootPackage",
+ Some(&cwd),
+ )?;
+ composer.set_package(package);
+
+ // load local repository
+ self.add_local_repository(io, rm.clone(), &vendor_dir, composer.get_package(), Some(&process));
+
+ // initialize installation manager
+ let im = self.create_installation_manager(r#loop.clone(), io.clone_box(), Some(dispatcher.clone()));
+ composer.set_installation_manager(im.clone());
+
+ if let PartialComposerOrComposer::Full(ref mut composer_full) = composer {
+ // initialize download manager
+ let dm = self.create_download_manager(io, &config, &http_downloader, &process, Some(&dispatcher))?;
+ composer_full.set_download_manager(dm.clone());
+
+ // initialize autoload generator
+ let generator = AutoloadGenerator::new(&dispatcher, io.clone_box());
+ composer_full.set_autoload_generator(generator);
+
+ // initialize archive manager
+ let am = self.create_archive_manager(&config, &dm, &r#loop)?;
+ composer_full.set_archive_manager(am);
+ }
+
+ // add installers to the manager (must happen after download manager is created since they read it out of $composer)
+ self.create_default_installers(&im, &composer, io, Some(&process));
+
+ // init locker if possible
+ if let PartialComposerOrComposer::Full(ref mut composer_full) = composer {
+ if let Some(ref composer_file_path) = composer_file {
+ let lock_file = Self::get_lock_file(composer_file_path);
+ let lock_enabled =
+ config.get("lock").and_then(|v| v.as_bool()).unwrap_or(true);
+ if !lock_enabled && file_exists(&lock_file) {
+ io.write_error(
+ PhpMixed::String(format!(
+ "<warning>{} is present but ignored as the \"lock\" config option is disabled.</warning>",
+ lock_file
+ )),
+ true,
+ <dyn IOInterface>::NORMAL,
+ );
+ }
+
+ let locker = Locker::new(
+ io.clone_box(),
+ JsonFile::new(
+ if lock_enabled {
+ lock_file
+ } else {
+ Platform::get_dev_null()
+ },
+ None,
+ Some(io),
+ ),
+ im.clone(),
+ file_get_contents(composer_file_path).unwrap_or_default(),
+ process.clone(),
+ );
+ composer_full.set_locker(locker);
+ } else {
+ let locker = Locker::new(
+ io.clone_box(),
+ JsonFile::new(Platform::get_dev_null(), None, Some(io)),
+ im.clone(),
+ JsonFile::encode(&PhpMixed::Array(
+ local_config_data
+ .iter()
+ .map(|(k, v)| (k.clone(), Box::new(v.clone())))
+ .collect(),
+ )),
+ process.clone(),
+ );
+ composer_full.set_locker(locker);
+ }
+ }
+
+ if let PartialComposerOrComposer::Full(ref mut composer_full) = composer {
+ let mut global_composer: Option<PartialComposer> = None;
+ if !composer_full.is_global() {
+ global_composer = self.create_global_composer(
+ io,
+ &config,
+ disable_plugins,
+ disable_scripts,
+ false,
+ );
+ }
+
+ let mut pm = self.create_plugin_manager(io, composer_full, global_composer.as_ref(), disable_plugins);
+ composer_full.set_plugin_manager(pm.clone());
+
+ if composer_full.is_global() {
+ pm.set_running_in_global_dir(true);
+ }
+
+ pm.load_installed_plugins();
+ }
+
+ if full_load {
+ let init_event = Event::from_name(PluginEvents::INIT.to_string());
+ composer
+ .get_event_dispatcher_mut()
+ .dispatch(Some(init_event.get_name()), Some(init_event))?;
+
+ // once everything is initialized we can
+ // purge packages from local repos if they have been deleted on the filesystem
+ self.purge_packages(rm.get_local_repository(), &im);
+ }
+
+ Ok(composer)
+ }
+
+ pub fn create_global(
+ io: &dyn IOInterface,
+ disable_plugins: DisablePlugins,
+ disable_scripts: bool,
+ ) -> Option<Composer> {
+ let factory = Self;
+
+ let config = Self::create_config(Some(io), None).ok()?;
+ factory
+ .create_global_composer(io, &config, disable_plugins, disable_scripts, true)
+ .and_then(|pc| match pc {
+ _ => None, // TODO(phase-b): downcast PartialComposer to Composer when fullLoad=true
+ })
+ }
+
+ fn add_local_repository(
+ &self,
+ io: &dyn IOInterface,
+ mut rm: RepositoryManager,
+ vendor_dir: &str,
+ root_package: &dyn RootPackageInterface,
+ process: Option<&ProcessExecutor>,
+ ) {
+ let fs = process.map(|p| Filesystem::new(Some(p.clone())));
+
+ rm.set_local_repository(Box::new(InstalledFilesystemRepository::new(
+ JsonFile::new(
+ format!("{}/composer/installed.json", vendor_dir),
+ None,
+ Some(io),
+ ),
+ true,
+ root_package.clone_box(),
+ fs,
+ )));
+ }
+
+ fn create_global_composer(
+ &self,
+ io: &dyn IOInterface,
+ config: &Config,
+ disable_plugins: DisablePlugins,
+ disable_scripts: bool,
+ full_load: bool,
+ ) -> Option<PartialComposer> {
+ // make sure if disable plugins was 'local' it is now turned off
+ let disable_plugins = if matches!(disable_plugins, DisablePlugins::Global | DisablePlugins::All) {
+ DisablePlugins::All
+ } else {
+ DisablePlugins::None
+ };
+
+ let composer = match self.create_composer(
+ io,
+ Some(LocalConfigInput::Path(format!(
+ "{}/composer.json",
+ config.get_str("home").ok()?
+ ))),
+ disable_plugins,
+ Some(&config.get_str("home").ok()?),
+ full_load,
+ disable_scripts,
+ ) {
+ Ok(c) => Some(c.into_partial()),
+ Err(e) => {
+ io.write_error(
+ PhpMixed::String(format!(
+ "Failed to initialize global composer: {}",
+ e
+ )),
+ true,
+ <dyn IOInterface>::DEBUG,
+ );
+ None
+ }
+ };
+
+ composer
+ }
+
+ pub fn create_download_manager(
+ &self,
+ io: &dyn IOInterface,
+ config: &Config,
+ http_downloader: &HttpDownloader,
+ process: &ProcessExecutor,
+ event_dispatcher: Option<&EventDispatcher>,
+ ) -> anyhow::Result<DownloadManager> {
+ let mut cache: Option<Cache> = None;
+ if config
+ .get("cache-files-ttl")
+ .and_then(|v| v.as_int())
+ .unwrap_or(0)
+ > 0
+ {
+ let mut c = Cache::new(io, &config.get_str("cache-files-dir")?, "a-z0-9_./");
+ c.set_read_only(
+ config
+ .get("cache-read-only")
+ .and_then(|v| v.as_bool())
+ .unwrap_or(false),
+ );
+ cache = Some(c);
+ }
+
+ let fs = Filesystem::new(Some(process.clone()));
+
+ let mut dm = DownloadManager::new(io.clone_box(), false, fs.clone());
+ let preferred = config.get("preferred-install").cloned();
+ match preferred.as_ref().and_then(|v| v.as_string()) {
+ Some("dist") => {
+ dm.set_prefer_dist(true);
+ }
+ Some("source") => {
+ dm.set_prefer_source(true);
+ }
+ Some("auto") | _ => {
+ // noop
+ }
+ }
+
+ if let Some(PhpMixed::Array(prefs)) = preferred {
+ dm.set_preferences(
+ prefs
+ .into_iter()
+ .map(|(k, v)| {
+ (
+ k,
+ match *v {
+ PhpMixed::String(s) => s,
+ _ => String::new(),
+ },
+ )
+ })
+ .collect(),
+ );
+ }
+
+ dm.set_downloader(
+ "git",
+ Box::new(GitDownloader::new(io.clone_box(), config.clone(), process.clone(), fs.clone())),
+ );
+ dm.set_downloader(
+ "svn",
+ Box::new(SvnDownloader::new(io.clone_box(), config.clone(), process.clone(), fs.clone())),
+ );
+ dm.set_downloader(
+ "fossil",
+ Box::new(FossilDownloader::new(io.clone_box(), config.clone(), process.clone(), fs.clone())),
+ );
+ dm.set_downloader(
+ "hg",
+ Box::new(HgDownloader::new(io.clone_box(), config.clone(), process.clone(), fs.clone())),
+ );
+ dm.set_downloader(
+ "perforce",
+ Box::new(PerforceDownloader::new(io.clone_box(), config.clone(), process.clone(), fs.clone())),
+ );
+ dm.set_downloader(
+ "zip",
+ Box::new(ZipDownloader::new(
+ io.clone_box(),
+ config.clone(),
+ http_downloader.clone(),
+ event_dispatcher.cloned(),
+ cache.clone(),
+ fs.clone(),
+ process.clone(),
+ )),
+ );
+ dm.set_downloader(
+ "rar",
+ Box::new(RarDownloader::new(
+ io.clone_box(),
+ config.clone(),
+ http_downloader.clone(),
+ event_dispatcher.cloned(),
+ cache.clone(),
+ fs.clone(),
+ process.clone(),
+ )),
+ );
+ dm.set_downloader(
+ "tar",
+ Box::new(TarDownloader::new(
+ io.clone_box(),
+ config.clone(),
+ http_downloader.clone(),
+ event_dispatcher.cloned(),
+ cache.clone(),
+ fs.clone(),
+ process.clone(),
+ )),
+ );
+ dm.set_downloader(
+ "gzip",
+ Box::new(GzipDownloader::new(
+ io.clone_box(),
+ config.clone(),
+ http_downloader.clone(),
+ event_dispatcher.cloned(),
+ cache.clone(),
+ fs.clone(),
+ process.clone(),
+ )),
+ );
+ dm.set_downloader(
+ "xz",
+ Box::new(XzDownloader::new(
+ io.clone_box(),
+ config.clone(),
+ http_downloader.clone(),
+ event_dispatcher.cloned(),
+ cache.clone(),
+ fs.clone(),
+ process.clone(),
+ )),
+ );
+ dm.set_downloader(
+ "phar",
+ Box::new(PharDownloader::new(
+ io.clone_box(),
+ config.clone(),
+ http_downloader.clone(),
+ event_dispatcher.cloned(),
+ cache.clone(),
+ fs.clone(),
+ process.clone(),
+ )),
+ );
+ dm.set_downloader(
+ "file",
+ Box::new(FileDownloader::new(
+ io.clone_box(),
+ config.clone(),
+ http_downloader.clone(),
+ event_dispatcher.cloned(),
+ cache.clone(),
+ fs.clone(),
+ process.clone(),
+ )),
+ );
+ dm.set_downloader(
+ "path",
+ Box::new(PathDownloader::new(
+ io.clone_box(),
+ config.clone(),
+ http_downloader.clone(),
+ event_dispatcher.cloned(),
+ cache.clone(),
+ fs.clone(),
+ process.clone(),
+ )),
+ );
+
+ Ok(dm)
+ }
+
+ pub fn create_archive_manager(
+ &self,
+ _config: &Config,
+ dm: &DownloadManager,
+ r#loop: &Loop,
+ ) -> anyhow::Result<ArchiveManager> {
+ let mut am = ArchiveManager::new(dm.clone(), r#loop.clone());
+ if class_exists("ZipArchive") {
+ am.add_archiver(Box::new(ZipArchiver::new()));
+ }
+ if class_exists("Phar") {
+ am.add_archiver(Box::new(PharArchiver::new()));
+ }
+
+ Ok(am)
+ }
+
+ fn create_plugin_manager(
+ &self,
+ io: &dyn IOInterface,
+ composer: &Composer,
+ global_composer: Option<&PartialComposer>,
+ disable_plugins: DisablePlugins,
+ ) -> PluginManager {
+ PluginManager::new(io.clone_box(), composer.clone(), global_composer.cloned(), disable_plugins)
+ }
+
+ pub fn create_installation_manager(
+ &self,
+ r#loop: Loop,
+ io: Box<dyn IOInterface>,
+ event_dispatcher: Option<EventDispatcher>,
+ ) -> InstallationManager {
+ InstallationManager::new(r#loop, io, event_dispatcher)
+ }
+
+ fn create_default_installers(
+ &self,
+ im: &InstallationManager,
+ composer: &PartialComposerOrComposer,
+ io: &dyn IOInterface,
+ process: Option<&ProcessExecutor>,
+ ) {
+ let fs = Filesystem::new(process.cloned());
+ let bin_dir = trim(
+ &composer
+ .get_config()
+ .get_str("bin-dir")
+ .unwrap_or_default(),
+ "/",
+ );
+ let bin_compat = composer
+ .get_config()
+ .get_str("bin-compat")
+ .unwrap_or_default();
+ let vendor_dir = trim(
+ &composer
+ .get_config()
+ .get_str("vendor-dir")
+ .unwrap_or_default(),
+ "/",
+ );
+ let binary_installer = BinaryInstaller::new(
+ io.clone_box(),
+ bin_dir,
+ bin_compat,
+ fs.clone(),
+ vendor_dir,
+ );
+
+ let mut im = im.clone();
+ im.add_installer(Box::new(LibraryInstaller::new(
+ io.clone_box(),
+ composer.as_partial(),
+ None,
+ fs.clone(),
+ binary_installer.clone(),
+ )));
+ im.add_installer(Box::new(PluginInstaller::new(
+ io.clone_box(),
+ composer.as_partial(),
+ fs.clone(),
+ binary_installer.clone(),
+ )));
+ im.add_installer(Box::new(MetapackageInstaller::new(io.clone_box())));
+ }
+
+ fn purge_packages(
+ &self,
+ repo: &dyn InstalledRepositoryInterface,
+ im: &InstallationManager,
+ ) {
+ for package in repo.get_packages() {
+ if !im.is_package_installed(repo, package.as_ref()) {
+ // TODO(phase-b): mutable access on repo trait object
+ let _ = package;
+ }
+ }
+ }
+
+ fn load_root_package(
+ &self,
+ rm: RepositoryManager,
+ config: Config,
+ parser: VersionParser,
+ guesser: VersionGuesser,
+ io: Box<dyn IOInterface>,
+ ) -> RootPackageLoader {
+ RootPackageLoader::new(rm, config, Some(parser), Some(guesser), Some(io))
+ }
+
+ pub fn create(
+ io: &dyn IOInterface,
+ config: Option<LocalConfigInput>,
+ disable_plugins: DisablePlugins,
+ disable_scripts: bool,
+ ) -> anyhow::Result<Composer> {
+ let factory = Self;
+
+ // for BC reasons, if a config is passed in either as array or a path that is not the default composer.json path
+ // we disable local plugins as they really should not be loaded from CWD
+ // If you want to avoid this behavior, you should be calling createComposer directly with a $cwd arg set correctly
+ // to the path where the composer.json being loaded resides
+ let default_composer_file = Self::get_composer_file()?;
+ let config_is_default = matches!(
+ config.as_ref(),
+ Some(LocalConfigInput::Path(p)) if *p == default_composer_file
+ );
+ let disable_plugins = if config.is_some()
+ && !config_is_default
+ && matches!(disable_plugins, DisablePlugins::None)
+ {
+ DisablePlugins::Local
+ } else {
+ disable_plugins
+ };
+
+ match factory.create_composer(io, config, disable_plugins, None, true, disable_scripts)? {
+ PartialComposerOrComposer::Full(c) => Ok(c),
+ PartialComposerOrComposer::Partial(_) => {
+ // TODO(phase-b): unreachable when fullLoad=true; downcasting needs design.
+ Err(anyhow::anyhow!(RuntimeException {
+ message: "Composer expected with fullLoad=true".to_string(),
+ code: 0,
+ }))
+ }
+ }
+ }
+
+ /// If you are calling this in a plugin, you probably should instead use `$composer->getLoop()->getHttpDownloader()`
+ pub fn create_http_downloader(
+ io: &dyn IOInterface,
+ config: &Config,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<HttpDownloader> {
+ // TODO(plugin): static `$warned` flag — port as a OnceCell or atomic in Phase B.
+ static mut WARNED: bool = false;
+ let mut disable_tls = false;
+ // allow running the config command if disable-tls is in the arg list, even if openssl is missing, to allow disabling it via the config command
+ let argv = Platform::server_argv().unwrap_or_default();
+ if !argv.is_empty()
+ && argv.contains(&"disable-tls".to_string())
+ && (argv.contains(&"conf".to_string()) || argv.contains(&"config".to_string()))
+ {
+ unsafe { WARNED = true };
+ disable_tls = !extension_loaded("openssl");
+ } else if config
+ .get("disable-tls")
+ .and_then(|v| v.as_bool())
+ .unwrap_or(false)
+ {
+ if !unsafe { WARNED } {
+ io.write_error(
+ PhpMixed::String(
+ "<warning>You are running Composer with SSL/TLS protection disabled.</warning>"
+ .to_string(),
+ ),
+ true,
+ <dyn IOInterface>::NORMAL,
+ );
+ }
+ unsafe { WARNED = true };
+ disable_tls = true;
+ } else if !extension_loaded("openssl") {
+ return Err(anyhow::anyhow!(NoSslException(RuntimeException {
+ message:
+ "The openssl extension is required for SSL/TLS protection but is not available. If you can not enable the openssl extension, you can disable this error, at your own risk, by setting the 'disable-tls' option to true."
+ .to_string(),
+ code: 0,
+ })));
+ }
+ let mut http_downloader_options: IndexMap<String, PhpMixed> = IndexMap::new();
+ if !disable_tls {
+ if "" != config.get_str("cafile").unwrap_or_default() {
+ let mut ssl_map: IndexMap<String, PhpMixed> = IndexMap::new();
+ ssl_map.insert(
+ "cafile".to_string(),
+ PhpMixed::String(config.get_str("cafile").unwrap_or_default()),
+ );
+ http_downloader_options.insert(
+ "ssl".to_string(),
+ PhpMixed::Array(ssl_map.into_iter().map(|(k, v)| (k, Box::new(v))).collect()),
+ );
+ }
+ if "" != config.get_str("capath").unwrap_or_default() {
+ let existing_ssl = http_downloader_options
+ .get("ssl")
+ .and_then(|v| v.as_array())
+ .cloned()
+ .unwrap_or_default();
+ let mut ssl_map: IndexMap<String, Box<PhpMixed>> = existing_ssl;
+ ssl_map.insert(
+ "capath".to_string(),
+ Box::new(PhpMixed::String(config.get_str("capath").unwrap_or_default())),
+ );
+ http_downloader_options.insert("ssl".to_string(), PhpMixed::Array(ssl_map));
+ }
+ http_downloader_options =
+ array_replace_recursive(http_downloader_options, options.clone());
+ }
+ let http_downloader = match HttpDownloader::new_full(io.clone_box(), config.clone(), http_downloader_options, disable_tls) {
+ Ok(h) => h,
+ Err(e) => {
+ if let Some(te) = e.downcast_ref::<TransportException>() {
+ if strpos(&te.get_message(), "cafile").is_some() {
+ io.write(
+ PhpMixed::String(
+ "<error>Unable to locate a valid CA certificate file. You must set a valid 'cafile' option.</error>"
+ .to_string(),
+ ),
+ true,
+ <dyn IOInterface>::NORMAL,
+ );
+ io.write(
+ PhpMixed::String(
+ "<error>A valid CA certificate file is required for SSL/TLS protection.</error>"
+ .to_string(),
+ ),
+ true,
+ <dyn IOInterface>::NORMAL,
+ );
+ io.write(
+ PhpMixed::String(
+ "<error>You can disable this error, at your own risk, by setting the 'disable-tls' option to true.</error>"
+ .to_string(),
+ ),
+ true,
+ <dyn IOInterface>::NORMAL,
+ );
+ }
+ }
+ return Err(e);
+ }
+ };
+
+ Ok(http_downloader)
+ }
+
+ fn load_composer_auth_env(
+ config: &mut Config,
+ io: Option<&dyn IOInterface>,
+ ) -> anyhow::Result<()> {
+ let composer_auth_env = Platform::get_env("COMPOSER_AUTH");
+ let composer_auth_env_str = match composer_auth_env {
+ Some(s) if !s.is_empty() => s,
+ _ => return Ok(()),
+ };
+
+ let auth_data = json_decode(&composer_auth_env_str, false)?;
+ if matches!(auth_data, PhpMixed::Null) {
+ return Err(anyhow::anyhow!(UnexpectedValueException {
+ message:
+ "COMPOSER_AUTH environment variable is malformed, should be a valid JSON object"
+ .to_string(),
+ code: 0,
+ }));
+ }
+
+ if let Some(io_ref) = io {
+ io_ref.write_error(
+ PhpMixed::String("Loading auth config from COMPOSER_AUTH".to_string()),
+ true,
+ <dyn IOInterface>::DEBUG,
+ );
+ }
+ Self::validate_json_schema(
+ io,
+ ValidateJsonInput::Data(auth_data.clone()),
+ JsonFile::AUTH_SCHEMA,
+ Some("COMPOSER_AUTH"),
+ )?;
+ let auth_data_assoc = json_decode(&composer_auth_env_str, true)?;
+ if !matches!(auth_data_assoc, PhpMixed::Null) {
+ let mut wrapped: IndexMap<String, PhpMixed> = IndexMap::new();
+ wrapped.insert("config".to_string(), auth_data_assoc);
+ config.merge(wrapped, "COMPOSER_AUTH".to_string());
+ }
+ Ok(())
+ }
+
+ fn use_xdg() -> bool {
+ for key in array_keys(&Platform::server_env()) {
+ if strpos(&key, "XDG_") == Some(0) {
+ return true;
+ }
+ }
+
+ Silencer::call(|| Ok::<bool, anyhow::Error>(is_dir("/etc/xdg"))).unwrap_or(false)
+ }
+
+ fn get_user_dir() -> anyhow::Result<String> {
+ let home = Platform::get_env("HOME").unwrap_or_default();
+ if home.is_empty() {
+ return Err(anyhow::anyhow!(RuntimeException {
+ message:
+ "The HOME or COMPOSER_HOME environment variable must be set for composer to run correctly"
+ .to_string(),
+ code: 0,
+ }));
+ }
+
+ Ok(trim(&strtr(&home, "\\", "/"), "/"))
+ }
+
+ fn validate_json_schema(
+ io: Option<&dyn IOInterface>,
+ file_or_data: ValidateJsonInput,
+ schema: i64,
+ source: Option<&str>,
+ ) -> anyhow::Result<()> {
+ if Platform::is_input_completion_process() {
+ return Ok(());
+ }
+
+ let result = match file_or_data {
+ ValidateJsonInput::File(file) => file.validate_schema(schema),
+ ValidateJsonInput::Data(data) => {
+ let source = source.ok_or_else(|| {
+ anyhow::anyhow!(InvalidArgumentException {
+ message:
+ "$source is required to be provided if $fileOrData is arbitrary data"
+ .to_string(),
+ code: 0,
+ })
+ })?;
+ JsonFile::validate_json_schema(source, &data, schema)
+ }
+ };
+
+ if let Err(e) = result {
+ if let Some(jve) = e.downcast_ref::<JsonValidationException>() {
+ let msg = format!(
+ "{}, this may result in errors and should be resolved:{} - {}",
+ jve.get_message(),
+ PHP_EOL,
+ implode(&format!("{} - ", PHP_EOL), jve.get_errors())
+ );
+ if let Some(io_ref) = io {
+ io_ref.write_error(
+ PhpMixed::String(format!("<warning>{}</>", msg)),
+ true,
+ <dyn IOInterface>::NORMAL,
+ );
+ } else {
+ return Err(anyhow::anyhow!(UnexpectedValueException { message: msg, code: 0 }));
+ }
+ } else {
+ return Err(e);
+ }
+ }
+ Ok(())
+ }
+}
+
+enum ValidateJsonInput {
+ File(JsonFile),
+ Data(PhpMixed),
+}
+
+/// `Factory::createComposer` returns either a `Composer` (`$fullLoad=true`) or a `PartialComposer`.
+pub enum PartialComposerOrComposer {
+ Full(Composer),
+ Partial(PartialComposer),
+}
+
+impl PartialComposerOrComposer {
+ fn set_config(&mut self, config: Config) {
+ match self {
+ Self::Full(c) => c.set_config(config),
+ Self::Partial(p) => p.set_config(config),
+ }
+ }
+ fn set_global(&mut self) {
+ match self {
+ Self::Full(c) => c.set_global(),
+ Self::Partial(p) => p.set_global(),
+ }
+ }
+ fn set_loop(&mut self, r#loop: Loop) {
+ match self {
+ Self::Full(c) => c.set_loop(r#loop),
+ Self::Partial(p) => p.set_loop(r#loop),
+ }
+ }
+ fn set_event_dispatcher(&mut self, dispatcher: EventDispatcher) {
+ match self {
+ Self::Full(c) => c.set_event_dispatcher(dispatcher),
+ Self::Partial(p) => p.set_event_dispatcher(dispatcher),
+ }
+ }
+ fn set_repository_manager(&mut self, rm: RepositoryManager) {
+ match self {
+ Self::Full(c) => c.set_repository_manager(rm),
+ Self::Partial(p) => p.set_repository_manager(rm),
+ }
+ }
+ fn set_installation_manager(&mut self, im: InstallationManager) {
+ match self {
+ Self::Full(c) => c.set_installation_manager(im),
+ Self::Partial(p) => p.set_installation_manager(im),
+ }
+ }
+ fn set_package(&mut self, package: Box<dyn RootPackageInterface>) {
+ match self {
+ Self::Full(c) => c.set_package(package),
+ Self::Partial(p) => p.set_package(package),
+ }
+ }
+ fn get_package(&self) -> &dyn RootPackageInterface {
+ match self {
+ Self::Full(c) => c.get_package(),
+ Self::Partial(p) => p.get_package(),
+ }
+ }
+ fn get_config(&self) -> &Config {
+ match self {
+ Self::Full(c) => c.get_config(),
+ Self::Partial(p) => p.get_config(),
+ }
+ }
+ fn get_event_dispatcher_mut(&mut self) -> &mut EventDispatcher {
+ match self {
+ Self::Full(c) => c.get_event_dispatcher_mut(),
+ Self::Partial(p) => p.get_event_dispatcher_mut(),
+ }
+ }
+ fn as_partial(&self) -> PartialComposer {
+ // TODO(phase-b): exact clone semantics differ across Composer/PartialComposer.
+ match self {
+ Self::Full(_) => PartialComposer::default(),
+ Self::Partial(p) => p.clone(),
+ }
+ }
+ fn into_partial(self) -> PartialComposer {
+ match self {
+ Self::Full(_) => PartialComposer::default(),
+ Self::Partial(p) => p,
+ }
+ }
+}