aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/command/update_command.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe/src/command/update_command.rs')
-rw-r--r--crates/shirabe/src/command/update_command.rs596
1 files changed, 596 insertions, 0 deletions
diff --git a/crates/shirabe/src/command/update_command.rs b/crates/shirabe/src/command/update_command.rs
index 7b1f200..14848fb 100644
--- a/crates/shirabe/src/command/update_command.rs
+++ b/crates/shirabe/src/command/update_command.rs
@@ -1 +1,597 @@
//! ref: composer/src/Composer/Command/UpdateCommand.php
+
+use anyhow::Result;
+use indexmap::IndexMap;
+use shirabe_external_packages::composer::pcre::preg::Preg;
+use shirabe_external_packages::symfony::component::console::helper::table::Table;
+use shirabe_external_packages::symfony::console::input::input_interface::InputInterface;
+use shirabe_external_packages::symfony::console::output::output_interface::OutputInterface;
+use shirabe_php_shim::{
+ array_filter, array_intersect, array_keys, array_merge, array_search, count, empty, in_array,
+ sprintf, strtolower, InvalidArgumentException, PhpMixed, RuntimeException,
+};
+use shirabe_semver::constraint::multi_constraint::MultiConstraint;
+use shirabe_semver::intervals::Intervals;
+
+use crate::advisory::auditor::Auditor;
+use crate::command::base_command::BaseCommand;
+use crate::command::bump_command::BumpCommand;
+use crate::command::completion_trait::CompletionTrait;
+use crate::composer::Composer;
+use crate::console::input::input_argument::InputArgument;
+use crate::console::input::input_option::InputOption;
+use crate::dependency_resolver::request::{
+ self, Request, UpdateAllowTransitiveDeps,
+};
+use crate::installer::Installer;
+use crate::io::io_interface::IOInterface;
+use crate::package::base_package::BasePackage;
+use crate::package::loader::root_package_loader::RootPackageLoader;
+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::platform_repository::PlatformRepository;
+use crate::repository::repository_interface::RepositoryInterface;
+use crate::repository::repository_set::RepositorySet;
+use crate::util::http_downloader::HttpDownloader;
+
+#[derive(Debug)]
+pub struct UpdateCommand {
+ inner: BaseCommand,
+}
+
+impl CompletionTrait for UpdateCommand {}
+
+impl UpdateCommand {
+ pub fn configure(&mut self) {
+ let suggest_installed_package = self.suggest_installed_package(false, true);
+ let suggest_prefer_install = self.suggest_prefer_install();
+ self.inner
+ .set_name("update")
+ .set_aliases(vec!["u".to_string(), "upgrade".to_string()])
+ .set_description("Updates your dependencies to the latest version according to composer.json, and updates the composer.lock file")
+ .set_definition(vec![
+ // TODO(phase-b): InputArgument/InputOption constructors and types
+ todo!("Box<dyn InputDefinitionEntry> for InputArgument::new(\"packages\", IS_ARRAY|OPTIONAL, ..., suggest_installed_package)"),
+ todo!("InputOption::new(\"with\", ..., VALUE_IS_ARRAY|VALUE_REQUIRED, ...)"),
+ todo!("InputOption::new(\"prefer-source\", ..., VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"prefer-dist\", ..., VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"prefer-install\", ..., VALUE_REQUIRED, ..., suggest_prefer_install)"),
+ todo!("InputOption::new(\"dry-run\", ..., VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"dev\", ..., VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"no-dev\", ..., VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"lock\", ..., VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"no-install\", ..., VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"no-audit\", ..., VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"audit-format\", ..., VALUE_REQUIRED, ..., Auditor::FORMAT_SUMMARY, Auditor::FORMATS)"),
+ todo!("InputOption::new(\"no-security-blocking\", ..., VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"no-autoloader\", ..., VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"no-suggest\", ..., VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"no-progress\", ..., VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"with-dependencies\", \"w\", VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"with-all-dependencies\", \"W\", VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"verbose\", \"v|vv|vvv\", VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"optimize-autoloader\", \"o\", VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"classmap-authoritative\", \"a\", VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"apcu-autoloader\", ..., VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"apcu-autoloader-prefix\", ..., VALUE_REQUIRED, ...)"),
+ todo!("InputOption::new(\"ignore-platform-req\", ..., VALUE_REQUIRED|VALUE_IS_ARRAY, ...)"),
+ todo!("InputOption::new(\"ignore-platform-reqs\", ..., VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"prefer-stable\", ..., VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"prefer-lowest\", ..., VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"minimal-changes\", \"m\", VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"patch-only\", ..., VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"interactive\", \"i\", VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"root-reqs\", ..., VALUE_NONE, ...)"),
+ todo!("InputOption::new(\"bump-after-update\", ..., VALUE_OPTIONAL, ..., false, ['dev', 'no-dev', 'all'])"),
+ ])
+ .set_help(
+ "The <info>update</info> command reads the composer.json file from the\n\
+ current directory, processes it, and updates, removes or installs all the\n\
+ dependencies.\n\n\
+ <info>php composer.phar update</info>\n\n\
+ To limit the update operation to a few packages, you can list the package(s)\n\
+ you want to update as such:\n\n\
+ <info>php composer.phar update vendor/package1 foo/mypackage [...]</info>\n\n\
+ You may also use an asterisk (*) pattern to limit the update operation to package(s)\n\
+ from a specific vendor:\n\n\
+ <info>php composer.phar update vendor/package1 foo/* [...]</info>\n\n\
+ To run an update with more restrictive constraints you can use:\n\n\
+ <info>php composer.phar update --with vendor/package:1.0.*</info>\n\n\
+ To run a partial update with more restrictive constraints you can use the shorthand:\n\n\
+ <info>php composer.phar update vendor/package:1.0.*</info>\n\n\
+ To select packages names interactively with auto-completion use <info>-i</info>.\n\n\
+ Read more at https://getcomposer.org/doc/03-cli.md#update-u-upgrade\n",
+ );
+ }
+
+ pub fn execute(
+ &mut self,
+ input: &dyn InputInterface,
+ output: &dyn OutputInterface,
+ ) -> Result<i64> {
+ let io = self.inner.get_io();
+ if input.get_option("dev").as_bool().unwrap_or(false) {
+ io.write_error(
+ PhpMixed::String(
+ "<warning>You are using the deprecated option \"--dev\". It has no effect and will break in Composer 3.</warning>".to_string(),
+ ),
+ true,
+ IOInterface::NORMAL,
+ );
+ }
+ if input.get_option("no-suggest").as_bool().unwrap_or(false) {
+ io.write_error(
+ PhpMixed::String(
+ "<warning>You are using the deprecated option \"--no-suggest\". It has no effect and will break in Composer 3.</warning>".to_string(),
+ ),
+ true,
+ IOInterface::NORMAL,
+ );
+ }
+
+ let composer = self.require_composer(None, None);
+
+ if !HttpDownloader::is_curl_enabled() {
+ io.write_error(
+ PhpMixed::String(
+ "<warning>Composer is operating significantly slower than normal because you do not have the PHP curl extension enabled.</warning>".to_string(),
+ ),
+ true,
+ IOInterface::NORMAL,
+ );
+ }
+
+ let mut packages: Vec<String> = input
+ .get_argument("packages")
+ .as_list()
+ .map(|l| {
+ l.iter()
+ .filter_map(|v| v.as_string().map(|s| s.to_string()))
+ .collect()
+ })
+ .unwrap_or_default();
+ let mut reqs: IndexMap<String, String> = self.inner.format_requirements(
+ input
+ .get_option("with")
+ .as_list()
+ .map(|l| {
+ l.iter()
+ .filter_map(|v| v.as_string().map(|s| s.to_string()))
+ .collect()
+ })
+ .unwrap_or_default(),
+ );
+
+ // extract --with shorthands from the allowlist
+ if packages.len() > 0 {
+ let allowlist_packages_with_requirements: Vec<String> = array_filter(
+ &packages,
+ |pkg: &String| -> bool { Preg::is_match(r"{\S+[ =:]\S+}", pkg) },
+ );
+ for (package, constraint) in self
+ .inner
+ .format_requirements(allowlist_packages_with_requirements.clone())
+ {
+ reqs.insert(package, constraint);
+ }
+
+ // replace the foo/bar:req by foo/bar in the allowlist
+ for package in &allowlist_packages_with_requirements {
+ let package_name = Preg::replace(r"{^([^ =:]+)[ =:].*$}", "$1", package);
+ let index = array_search(
+ package,
+ // TODO(phase-b): array_search expects IndexMap<String, String>; supply a wrapper
+ todo!("packages as IndexMap<String, String>"),
+ );
+ if let Some(idx) = index {
+ // TODO(phase-b): mutate packages[idx] — PHP integer-keyed array
+ let _ = idx;
+ let _ = package_name;
+ }
+ }
+ }
+
+ let root_package = composer.get_package();
+ root_package.set_references(RootPackageLoader::extract_references(
+ &reqs,
+ &root_package.get_references(),
+ ));
+ root_package.set_stability_flags(RootPackageLoader::extract_stability_flags(
+ &reqs,
+ root_package.get_minimum_stability(),
+ root_package.get_stability_flags(),
+ ));
+
+ let parser = VersionParser::new();
+ let mut temporary_constraints: IndexMap<String, _> = IndexMap::new();
+ let root_requirements = array_merge(
+ // TODO(phase-b): array_merge for IndexMap<String, Link>
+ todo!("root_package.get_requires() as PhpMixed"),
+ todo!("root_package.get_dev_requires() as PhpMixed"),
+ );
+ for (package, constraint) in &reqs {
+ let package = strtolower(package);
+ let parsed_constraint = parser.parse_constraints(constraint)?;
+ temporary_constraints.insert(package.clone(), parsed_constraint.clone());
+ // TODO(phase-b): access root_requirements[package].getConstraint()
+ let intersected = todo!("Intervals::haveIntersections check");
+ if let Some(_root_req) = todo!("root_requirements.get(&package)") {
+ if !intersected {
+ io.write_error(
+ PhpMixed::String(format!(
+ "<error>The temporary constraint \"{}\" for \"{}\" must be a subset of the constraint in your composer.json ({})</error>",
+ constraint,
+ package,
+ todo!("root_requirements[package].get_pretty_constraint()"),
+ )),
+ true,
+ IOInterface::NORMAL,
+ );
+ io.write(
+ PhpMixed::String(format!(
+ "<info>Run `composer require {}` or `composer require {}:{}` instead to replace the constraint</info>",
+ package, package, constraint,
+ )),
+ true,
+ IOInterface::NORMAL,
+ );
+
+ return Ok(BaseCommand::FAILURE);
+ }
+ }
+ }
+
+ if input.get_option("patch-only").as_bool().unwrap_or(false) {
+ if !composer.get_locker().is_locked() {
+ return Err(InvalidArgumentException {
+ message: "patch-only can only be used with a lock file present".to_string(),
+ code: 0,
+ }
+ .into());
+ }
+ for package in composer
+ .get_locker()
+ .get_locked_repository(true)?
+ .get_canonical_packages()
+ {
+ if package.is_dev() {
+ continue;
+ }
+ let matches = Preg::is_match_with_indexed_captures(
+ r"{^(\d+\.\d+\.\d+)}",
+ package.get_version(),
+ )?;
+ let Some(matches) = matches else {
+ continue;
+ };
+ let constraint = parser.parse_constraints(&format!("~{}", matches.get(1).cloned().unwrap_or_default()))?;
+ if temporary_constraints.contains_key(package.get_name()) {
+ let existing = temporary_constraints.get(package.get_name()).cloned().unwrap();
+ temporary_constraints.insert(
+ package.get_name().to_string(),
+ // TODO(phase-b): MultiConstraint::create signature
+ todo!("MultiConstraint::create([existing, constraint], true)"),
+ );
+ } else {
+ temporary_constraints.insert(package.get_name().to_string(), constraint);
+ }
+ }
+ }
+
+ if input.get_option("interactive").as_bool().unwrap_or(false) {
+ packages = self.get_packages_interactively(io, input, output, &composer, packages)?;
+ }
+
+ if input.get_option("root-reqs").as_bool().unwrap_or(false) {
+ let mut requires: Vec<String> = array_keys(&root_package.get_requires());
+ if !input.get_option("no-dev").as_bool().unwrap_or(false) {
+ requires = array_merge(
+ // TODO(phase-b): array_merge for Vec<String>
+ todo!("requires as PhpMixed"),
+ todo!("array_keys(&root_package.get_dev_requires()) as PhpMixed"),
+ )
+ .as_list()
+ .map(|l| {
+ l.iter()
+ .filter_map(|v| v.as_string().map(|s| s.to_string()))
+ .collect()
+ })
+ .unwrap_or_default();
+ }
+
+ if !packages.is_empty() {
+ packages = array_intersect(&packages, &requires);
+ } else {
+ packages = requires;
+ }
+ }
+
+ // the arguments lock/nothing/mirrors are not package names but trigger a mirror update instead
+ // they are further mutually exclusive with listing actual package names
+ let filtered_packages: Vec<String> = array_filter(&packages, |package: &String| -> bool {
+ !in_array(
+ PhpMixed::String(package.clone()),
+ &PhpMixed::List(vec![
+ Box::new(PhpMixed::String("lock".to_string())),
+ Box::new(PhpMixed::String("nothing".to_string())),
+ Box::new(PhpMixed::String("mirrors".to_string())),
+ ]),
+ true,
+ )
+ });
+ let update_mirrors = input.get_option("lock").as_bool().unwrap_or(false)
+ || filtered_packages.len() != packages.len();
+ packages = filtered_packages;
+
+ if update_mirrors && !packages.is_empty() {
+ io.write_error(
+ PhpMixed::String(
+ "<error>You cannot simultaneously update only a selection of packages and regenerate the lock file metadata.</error>"
+ .to_string(),
+ ),
+ true,
+ IOInterface::NORMAL,
+ );
+
+ return Ok(-1);
+ }
+
+ let mut command_event = CommandEvent::new(PluginEvents::COMMAND, "update", input, output);
+ composer
+ .get_event_dispatcher()
+ .dispatch(&command_event.get_name(), &mut command_event);
+
+ composer
+ .get_installation_manager()
+ .set_output_progress(!input.get_option("no-progress").as_bool().unwrap_or(false));
+
+ let mut install = Installer::create(io, &composer);
+
+ let config = composer.get_config();
+ let (prefer_source, prefer_dist) =
+ self.inner.get_preferred_install_options(config, input, false);
+
+ let optimize = input.get_option("optimize-autoloader").as_bool().unwrap_or(false)
+ || config.get("optimize-autoloader").as_bool().unwrap_or(false);
+ let authoritative = input
+ .get_option("classmap-authoritative")
+ .as_bool()
+ .unwrap_or(false)
+ || config.get("classmap-authoritative").as_bool().unwrap_or(false);
+ let apcu_prefix = input.get_option("apcu-autoloader-prefix");
+ let apcu = !matches!(apcu_prefix, PhpMixed::Null)
+ || input.get_option("apcu-autoloader").as_bool().unwrap_or(false)
+ || config.get("apcu-autoloader").as_bool().unwrap_or(false);
+ let minimal_changes = input.get_option("minimal-changes").as_bool().unwrap_or(false)
+ || config.get("update-with-minimal-changes").as_bool().unwrap_or(false);
+
+ let mut update_allow_transitive_dependencies = UpdateAllowTransitiveDeps::UpdateOnlyListed;
+ if input
+ .get_option("with-all-dependencies")
+ .as_bool()
+ .unwrap_or(false)
+ {
+ update_allow_transitive_dependencies =
+ UpdateAllowTransitiveDeps::UpdateListedWithTransitiveDeps;
+ } else if input
+ .get_option("with-dependencies")
+ .as_bool()
+ .unwrap_or(false)
+ {
+ update_allow_transitive_dependencies =
+ UpdateAllowTransitiveDeps::UpdateListedWithTransitiveDepsNoRootRequire;
+ }
+
+ install
+ .set_dry_run(input.get_option("dry-run").as_bool().unwrap_or(false))
+ .set_verbose(input.get_option("verbose").as_bool().unwrap_or(false))
+ .set_prefer_source(prefer_source)
+ .set_prefer_dist(prefer_dist)
+ .set_dev_mode(!input.get_option("no-dev").as_bool().unwrap_or(false))
+ .set_dump_autoloader(!input.get_option("no-autoloader").as_bool().unwrap_or(false))
+ .set_optimize_autoloader(optimize)
+ .set_class_map_authoritative(authoritative)
+ .set_apcu_autoloader(apcu, apcu_prefix)
+ .set_update(true)
+ .set_install(!input.get_option("no-install").as_bool().unwrap_or(false))
+ .set_update_mirrors(update_mirrors)
+ .set_update_allow_list(packages.clone())
+ .set_update_allow_transitive_dependencies(update_allow_transitive_dependencies)
+ .set_platform_requirement_filter(self.inner.get_platform_requirement_filter(input))
+ .set_prefer_stable(input.get_option("prefer-stable").as_bool().unwrap_or(false))
+ .set_prefer_lowest(input.get_option("prefer-lowest").as_bool().unwrap_or(false))
+ .set_temporary_constraints(temporary_constraints)
+ .set_audit_config(self.inner.create_audit_config(composer.get_config(), input)?)
+ .set_minimal_update(minimal_changes);
+
+ if input.get_option("no-plugins").as_bool().unwrap_or(false) {
+ install.disable_plugins();
+ }
+
+ let mut result = install.run()?;
+
+ if result == 0 && !input.get_option("lock").as_bool().unwrap_or(false) {
+ let mut bump_after_update = input.get_option("bump-after-update");
+ // PHP: false === $bumpAfterUpdate (strict)
+ if matches!(bump_after_update, PhpMixed::Bool(false)) {
+ bump_after_update = composer.get_config().get("bump-after-update");
+ }
+
+ if !matches!(bump_after_update, PhpMixed::Bool(false)) {
+ io.write_error(
+ PhpMixed::String("<info>Bumping dependencies</info>".to_string()),
+ true,
+ IOInterface::NORMAL,
+ );
+ let mut bump_command = BumpCommand::new();
+ bump_command.set_composer(composer.clone());
+ result = bump_command.do_bump(
+ io,
+ bump_after_update.as_string() == Some("dev"),
+ bump_after_update.as_string() == Some("no-dev"),
+ input.get_option("dry-run").as_bool().unwrap_or(false),
+ input
+ .get_argument("packages")
+ .as_list()
+ .map(|l| {
+ l.iter()
+ .filter_map(|v| v.as_string().map(|s| s.to_string()))
+ .collect()
+ })
+ .unwrap_or_default(),
+ "--bump-after-update=dev".to_string(),
+ )?;
+ }
+ }
+
+ Ok(result)
+ }
+
+ /// @param array<string> $packages
+ /// @return array<string>
+ fn get_packages_interactively(
+ &self,
+ io: &dyn IOInterface,
+ input: &dyn InputInterface,
+ output: &dyn OutputInterface,
+ composer: &Composer,
+ packages: Vec<String>,
+ ) -> Result<Vec<String>> {
+ if !input.is_interactive() {
+ return Err(InvalidArgumentException {
+ message: "--interactive cannot be used in non-interactive terminals.".to_string(),
+ code: 0,
+ }
+ .into());
+ }
+
+ let platform_req_filter = self.inner.get_platform_requirement_filter(input);
+ let stability_flags = composer.get_package().get_stability_flags();
+ let requires = array_merge(
+ // TODO(phase-b): array_merge for IndexMap<String, Link>
+ todo!("composer.get_package().get_requires() as PhpMixed"),
+ todo!("composer.get_package().get_dev_requires() as PhpMixed"),
+ );
+
+ let filter: Option<String> = if packages.len() > 0 {
+ // TODO(phase-b): BasePackage::package_names_to_regexp signature
+ Some(BasePackage::package_names_to_regexp(&packages, "%s"))
+ } else {
+ None
+ };
+
+ io.write_error(
+ PhpMixed::String("<info>Loading packages that can be updated...</info>".to_string()),
+ true,
+ IOInterface::NORMAL,
+ );
+ let mut autocompleter_values: IndexMap<String, String> = IndexMap::new();
+ let installed_packages = if composer.get_locker().is_locked() {
+ composer.get_locker().get_locked_repository(true)?.get_packages()
+ } else {
+ composer.get_repository_manager().get_local_repository().get_packages()
+ };
+ let version_selector = self.create_version_selector(composer);
+ for package in &installed_packages {
+ if let Some(filter) = &filter {
+ if !Preg::is_match(filter, package.get_name()) {
+ continue;
+ }
+ }
+ let current_version = package.get_pretty_version();
+ let constraint = todo!("requires[package.get_name()].get_pretty_constraint() if present");
+ let stability = todo!(
+ "if stabilityFlags[package_name] use array_search(BasePackage::STABILITIES) else minimum_stability"
+ );
+ let latest_version = version_selector.find_best_candidate(
+ package.get_name(),
+ constraint,
+ stability,
+ &*platform_req_filter,
+ );
+ if let Some(latest) = latest_version {
+ if package.get_version() != latest.get_version() || latest.is_dev() {
+ autocompleter_values.insert(
+ package.get_name().to_string(),
+ format!(
+ "<comment>{}</comment> => <comment>{}</comment>",
+ current_version,
+ latest.get_pretty_version(),
+ ),
+ );
+ }
+ }
+ }
+ if 0 == installed_packages.len() {
+ for (req, _constraint) in &requires {
+ if PlatformRepository::is_platform_package(req) {
+ continue;
+ }
+ autocompleter_values.insert(req.clone(), String::new());
+ }
+ }
+
+ if 0 == autocompleter_values.len() {
+ return Err(RuntimeException {
+ message: "Could not find any package with new versions available".to_string(),
+ code: 0,
+ }
+ .into());
+ }
+
+ let packages: Vec<String> = io.select(
+ "Select packages: (Select more than one value separated by comma) ".to_string(),
+ autocompleter_values,
+ false,
+ 1,
+ "No package named \"%s\" is installed.".to_string(),
+ true,
+ );
+
+ let mut table = Table::new(output);
+ table.set_headers(vec!["Selected packages".to_string()]);
+ for package in &packages {
+ table.add_row(vec![package.clone()]);
+ }
+ table.render();
+
+ if io.ask_confirmation(
+ sprintf(
+ "Would you like to continue and update the above package%s [<comment>yes</comment>]? ",
+ &[PhpMixed::String(
+ if 1 == packages.len() { "" } else { "s" }.to_string(),
+ )],
+ ),
+ true,
+ ) {
+ return Ok(packages);
+ }
+
+ Err(RuntimeException {
+ message: "Installation aborted.".to_string(),
+ code: 0,
+ }
+ .into())
+ }
+
+ fn create_version_selector(&self, composer: &Composer) -> VersionSelector {
+ let mut repository_set = RepositorySet::new();
+ repository_set.add_repository(Box::new(CompositeRepository::new(
+ array_filter(
+ &composer.get_repository_manager().get_repositories(),
+ |repository: &Box<dyn RepositoryInterface>| -> bool {
+ // PHP: !$repository instanceof PlatformRepository
+ repository
+ .as_any()
+ .downcast_ref::<PlatformRepository>()
+ .is_none()
+ },
+ ),
+ )));
+
+ VersionSelector::new(repository_set)
+ }
+}