From 6643eb8b7d305818f80c910144b3b29b1af34ba7 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 9 Aug 2026 09:14:11 +0900 Subject: refactor(installed-versions): drop the Rust port, which has no readers InstalledVersions is a runtime API for plugins and project code; Composer itself never reads it. Its consumers run in the PHP worker against the copy FilesystemRepository dumps to vendor/composer/InstalledVersions.php, whose static state is already kept in sync by __shirabe_installed_versions_reload. Nothing in Rust read the mirrored statics, so reload() and the reflection setters were no-ops. The tests covered only the Rust port, not the PHP class the worker loads, so they assert nothing about compatibility; they are left as todo!() skeletons. This also removes the shim functions method_exists, php_dir and require_php_file, whose only caller was the deleted module. Co-Authored-By: Claude Opus 5 (1M context) --- crates/shirabe/src/installed_versions.rs | 458 --------------------- crates/shirabe/src/lib.rs | 5 +- .../src/repository/filesystem_repository.rs | 23 +- 3 files changed, 10 insertions(+), 476 deletions(-) delete mode 100644 crates/shirabe/src/installed_versions.rs (limited to 'crates/shirabe/src') diff --git a/crates/shirabe/src/installed_versions.rs b/crates/shirabe/src/installed_versions.rs deleted file mode 100644 index aa934841..00000000 --- a/crates/shirabe/src/installed_versions.rs +++ /dev/null @@ -1,458 +0,0 @@ -//! ref: composer/src/Composer/InstalledVersions.php - -use crate::autoload::ClassLoader; -use indexmap::IndexMap; -use shirabe_php_shim::{ - OutOfBoundsException, PhpMixed, array_flip_strings, array_keys, array_merge, implode, is_file, - method_exists, php_dir, require_php_file, strtr_array, substr, -}; -use shirabe_semver::VersionParser; -use std::sync::Mutex; - -/// This class is copied in every Composer installed project and available to all -/// -/// See also https://getcomposer.org/doc/07-runtime.md#installed-versions -/// -/// To require its presence, you can require `composer-runtime-api ^2.0` -/// -/// @final -pub struct InstalledVersions; - -/// @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to -/// @internal -static SELF_DIR: Mutex> = Mutex::new(None); - -/// @var mixed[]|null -/// @psalm-var array{root: array{...}, versions: array}|array{}|null -static INSTALLED: Mutex>> = Mutex::new(None); - -/// @var bool -static INSTALLED_IS_LOCAL_DIR: Mutex = Mutex::new(false); - -/// @var bool|null -static CAN_GET_VENDORS: Mutex> = Mutex::new(None); - -/// @var array[] -/// @psalm-var array -static INSTALLED_BY_VENDOR: std::sync::LazyLock< - Mutex>>, -> = std::sync::LazyLock::new(|| Mutex::new(IndexMap::new())); - -impl InstalledVersions { - /// Returns a list of all package names which are present, either by being installed, replaced or provided - pub fn get_installed_packages() -> Vec { - let mut packages: Vec> = vec![]; - for installed in Self::get_installed() { - let versions = installed - .get("versions") - .and_then(|v| v.as_array()) - .cloned() - .unwrap_or_default(); - let keys: Vec = array_keys(&versions); - packages.push(keys); - } - - if 1 == packages.len() { - return packages.into_iter().next().unwrap(); - } - - let merged: Vec = packages.into_iter().flatten().collect(); - array_keys(&array_flip_strings(&merged)) - } - - /// Returns a list of all package names with a specific type e.g. 'library' - pub fn get_installed_packages_by_type(r#type: &str) -> Vec { - let mut packages_by_type: Vec = vec![]; - - for installed in Self::get_installed() { - let versions = installed - .get("versions") - .and_then(|v| v.as_array()) - .cloned() - .unwrap_or_default(); - for (name, package) in versions { - if let Some(pkg) = package.as_array() - && let Some(pkg_type) = pkg.get("type").and_then(|v| v.as_string()) - && pkg_type == r#type - { - packages_by_type.push(name); - } - } - } - - packages_by_type - } - - /// Checks whether the given package is installed - /// - /// This also returns true if the package name is provided or replaced by another package - pub fn is_installed(package_name: &str, include_dev_requirements: bool) -> bool { - for installed in Self::get_installed() { - let Some(versions) = installed.get("versions").and_then(|v| v.as_array()) else { - continue; - }; - if let Some(package) = versions.get(package_name) { - let dev_requirement = package - .as_array() - .and_then(|a| a.get("dev_requirement")) - .cloned() - .unwrap_or(PhpMixed::Null); - return include_dev_requirements - || matches!(dev_requirement, PhpMixed::Null) - || matches!(dev_requirement, PhpMixed::Bool(false)); - } - } - - false - } - - /// Checks whether the given package satisfies a version constraint - /// - /// e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: - /// - /// Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') - /// - /// @param VersionParser $parser Install composer/semver to have access to this class and functionality - /// @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package - pub fn satisfies( - parser: &VersionParser, - package_name: &str, - constraint: Option<&str>, - ) -> anyhow::Result { - let constraint = parser.parse_constraints(constraint.unwrap_or(""))?; - let provided = parser.parse_constraints(&Self::get_version_ranges(package_name)?)?; - - Ok(provided.matches(&constraint)) - } - - /// Returns a version constraint representing all the range(s) which are installed for a given package - /// - /// It is easier to use this via isInstalled() with the $constraint argument if you need to check - /// whether a given version of a package is installed, and not just whether it exists - /// - /// @return string Version constraint usable with composer/semver - pub fn get_version_ranges(package_name: &str) -> anyhow::Result { - for installed in Self::get_installed() { - let Some(versions) = installed.get("versions").and_then(|v| v.as_array()) else { - continue; - }; - let Some(pkg) = versions - .get(package_name) - .and_then(|v| v.as_array()) - .cloned() - else { - continue; - }; - - let mut ranges: Vec = vec![]; - if let Some(pretty_version) = pkg.get("pretty_version").and_then(|v| v.as_string()) { - ranges.push(pretty_version.to_string()); - } - if pkg.contains_key("aliases") { - ranges = array_merge( - PhpMixed::List(ranges.iter().map(|s| PhpMixed::String(s.clone())).collect()), - pkg.get("aliases").cloned().unwrap_or(PhpMixed::Null), - ) - .as_list() - .map(|l| { - l.iter() - .filter_map(|v| v.as_string().map(|s| s.to_string())) - .collect() - }) - .unwrap_or_default(); - } - if pkg.contains_key("replaced") { - ranges = array_merge( - PhpMixed::List(ranges.iter().map(|s| PhpMixed::String(s.clone())).collect()), - pkg.get("replaced").cloned().unwrap_or(PhpMixed::Null), - ) - .as_list() - .map(|l| { - l.iter() - .filter_map(|v| v.as_string().map(|s| s.to_string())) - .collect() - }) - .unwrap_or_default(); - } - if pkg.contains_key("provided") { - ranges = array_merge( - PhpMixed::List(ranges.iter().map(|s| PhpMixed::String(s.clone())).collect()), - pkg.get("provided").cloned().unwrap_or(PhpMixed::Null), - ) - .as_list() - .map(|l| { - l.iter() - .filter_map(|v| v.as_string().map(|s| s.to_string())) - .collect() - }) - .unwrap_or_default(); - } - - return Ok(implode(" || ", &ranges)); - } - - Err( - OutOfBoundsException::new(format!("Package \"{}\" is not installed", package_name)) - .into(), - ) - } - - /// @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present - pub fn get_version(package_name: &str) -> anyhow::Result> { - for installed in Self::get_installed() { - let Some(versions) = installed.get("versions").and_then(|v| v.as_array()) else { - continue; - }; - let Some(pkg) = versions.get(package_name).and_then(|v| v.as_array()) else { - continue; - }; - - if !pkg.contains_key("version") { - return Ok(None); - } - - return Ok(pkg - .get("version") - .and_then(|v| v.as_string()) - .map(|s| s.to_string())); - } - - Err( - OutOfBoundsException::new(format!("Package \"{}\" is not installed", package_name)) - .into(), - ) - } - - /// @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present - pub fn get_pretty_version(package_name: &str) -> anyhow::Result> { - for installed in Self::get_installed() { - let Some(versions) = installed.get("versions").and_then(|v| v.as_array()) else { - continue; - }; - let Some(pkg) = versions.get(package_name).and_then(|v| v.as_array()) else { - continue; - }; - - if !pkg.contains_key("pretty_version") { - return Ok(None); - } - - return Ok(pkg - .get("pretty_version") - .and_then(|v| v.as_string()) - .map(|s| s.to_string())); - } - - Err( - OutOfBoundsException::new(format!("Package \"{}\" is not installed", package_name)) - .into(), - ) - } - - /// @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference - pub fn get_reference(package_name: &str) -> anyhow::Result> { - for installed in Self::get_installed() { - let Some(versions) = installed.get("versions").and_then(|v| v.as_array()) else { - continue; - }; - let Some(pkg) = versions.get(package_name).and_then(|v| v.as_array()) else { - continue; - }; - - if !pkg.contains_key("reference") { - return Ok(None); - } - - return Ok(pkg - .get("reference") - .and_then(|v| v.as_string()) - .map(|s| s.to_string())); - } - - Err( - OutOfBoundsException::new(format!("Package \"{}\" is not installed", package_name)) - .into(), - ) - } - - /// @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. - pub fn get_install_path(package_name: &str) -> anyhow::Result> { - for installed in Self::get_installed() { - let Some(versions) = installed.get("versions").and_then(|v| v.as_array()) else { - continue; - }; - let Some(pkg) = versions.get(package_name).and_then(|v| v.as_array()) else { - continue; - }; - - return Ok(if pkg.contains_key("install_path") { - pkg.get("install_path") - .and_then(|v| v.as_string()) - .map(|s| s.to_string()) - } else { - None - }); - } - - Err( - OutOfBoundsException::new(format!("Package \"{}\" is not installed", package_name)) - .into(), - ) - } - - pub fn get_root_package() -> IndexMap { - let installed = Self::get_installed(); - - installed - .into_iter() - .next() - .and_then(|d| d.get("root").and_then(|v| v.as_array()).cloned()) - .unwrap_or_default() - } - - /// Returns the raw data of all installed.php which are currently loaded for custom implementations - /// - /// Returns the first dataset loaded, which may not be what you expect. Use get_all_raw_data - /// instead, which returns all datasets for all autoloaders present in the process. - pub fn get_raw_data() -> IndexMap { - // PHP emits an E_USER_DEPRECATED notice here; there is no Rust equivalent. - let mut installed = INSTALLED.lock().unwrap(); - if installed.is_none() { - // PHP only includes __DIR__/installed.php when loaded from its dumped location; the - // shim is always the source location (PHP's `else` branch), so the data is empty. - *installed = Some(IndexMap::new()); - } - - installed.clone().unwrap() - } - - pub fn get_all_raw_data() -> Vec> { - Self::get_installed() - } - - /// Lets you reload the static array from another file - /// - /// This is only useful for complex integrations in which a project needs to use - /// this class but then also needs to execute another project's autoloader in process, - /// and wants to ensure both projects have access to their version of installed.php. - /// - /// A typical case would be PHPUnit, where it would need to make sure it reads all - /// the data it needs from this class, then call reload() with - /// `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure - /// the project in which it runs can then also use this class safely, without - /// interference between PHPUnit's dependencies and the project's dependencies. - /// - /// @param array[] $data A vendor/composer/installed.php data set - pub fn reload(data: IndexMap) { - *INSTALLED.lock().unwrap() = Some(data); - *INSTALLED_BY_VENDOR.lock().unwrap() = IndexMap::new(); - - // when using reload, we disable the duplicate protection to ensure that self::$installed data is - // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not, - // so we have to assume it does not, and that may result in duplicate data being returned when listing - // all installed packages for example - *INSTALLED_IS_LOCAL_DIR.lock().unwrap() = false; - } - - /// PHP mutates the private static `$selfDir` via Reflection. Rust exposes a mutating function. - pub fn set_self_dir(dir: String) { - *SELF_DIR.lock().unwrap() = Some(dir); - } - - /// PHP mutates the private static `$installedIsLocalDir` via Reflection. Rust exposes a mutating function. - pub fn set_installed_is_local_dir(value: bool) { - *INSTALLED_IS_LOCAL_DIR.lock().unwrap() = value; - } - - fn get_self_dir() -> String { - let mut self_dir = SELF_DIR.lock().unwrap(); - if self_dir.is_none() { - *self_dir = Some(strtr_array(&php_dir(), &{ - let mut m = IndexMap::new(); - m.insert("\\".to_string(), "/".to_string()); - m - })); - } - - self_dir.clone().unwrap() - } - - fn get_installed() -> Vec> { - { - let mut can_get_vendors = CAN_GET_VENDORS.lock().unwrap(); - if can_get_vendors.is_none() { - *can_get_vendors = Some(method_exists( - &PhpMixed::String("Composer\\Autoload\\ClassLoader".to_string()), - "getRegisteredLoaders", - )); - } - } - - let mut installed: Vec> = vec![]; - let mut copied_local_dir = false; - - if CAN_GET_VENDORS.lock().unwrap().unwrap_or(false) { - let self_dir = Self::get_self_dir(); - for (vendor_dir, _loader) in ClassLoader::get_registered_loaders() { - let vendor_dir = strtr_array(&vendor_dir, &{ - let mut m = IndexMap::new(); - m.insert("\\".to_string(), "/".to_string()); - m - }); - let cached = INSTALLED_BY_VENDOR - .lock() - .unwrap() - .get(&vendor_dir) - .cloned(); - if let Some(cached) = cached { - installed.push(cached); - } else if is_file(format!("{}/composer/installed.php", vendor_dir)) { - let required = - require_php_file(&format!("{}/composer/installed.php", vendor_dir)); - let required_map: IndexMap = - required.as_array().cloned().unwrap_or_default(); - INSTALLED_BY_VENDOR - .lock() - .unwrap() - .insert(vendor_dir.clone(), required_map.clone()); - installed.push(required_map.clone()); - let mut installed_static = INSTALLED.lock().unwrap(); - if installed_static.is_none() && format!("{}/composer", vendor_dir) == self_dir - { - *installed_static = Some(required_map); - *INSTALLED_IS_LOCAL_DIR.lock().unwrap() = true; - } - } - if *INSTALLED_IS_LOCAL_DIR.lock().unwrap() - && format!("{}/composer", vendor_dir) == self_dir - { - copied_local_dir = true; - } - } - } - - { - let mut installed_static = INSTALLED.lock().unwrap(); - if installed_static.is_none() { - // only require the installed.php file if this file is loaded from its dumped location, - // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 - if substr(&php_dir(), -8, Some(1)) != "C" { - let required = require_php_file(&format!("{}/installed.php", php_dir())); - *installed_static = required - .as_array() - .cloned() - .map(|m| m.into_iter().collect()); - } else { - *installed_static = Some(IndexMap::new()); - } - } - } - - let installed_static_data = INSTALLED.lock().unwrap().clone().unwrap_or_default(); - if !installed_static_data.is_empty() && !copied_local_dir { - installed.push(installed_static_data); - } - - installed - } -} diff --git a/crates/shirabe/src/lib.rs b/crates/shirabe/src/lib.rs index 4667e7c9..e5624988 100644 --- a/crates/shirabe/src/lib.rs +++ b/crates/shirabe/src/lib.rs @@ -11,7 +11,6 @@ pub mod event_dispatcher; pub mod exception; pub mod factory; pub mod filter; -pub mod installed_versions; pub mod installer; pub mod io; pub mod json; @@ -25,6 +24,10 @@ pub mod script; pub mod self_update; pub mod util; +// InstalledVersions is intentionally unported to Rust. It is a runtime API for plugins and project +// code, never read by Composer itself. Its real state is stored in PHP's InstalledVersions class. +// See also `__shirabe_installed_versions_reload` in crates/shirabe-php-rpc/php/worker.php. + /// ref: composer/bin/composer pub fn run(argv: Vec) -> anyhow::Result { use crate::console::ApplicationHandle; diff --git a/crates/shirabe/src/repository/filesystem_repository.rs b/crates/shirabe/src/repository/filesystem_repository.rs index d9085bfd..b39fe829 100644 --- a/crates/shirabe/src/repository/filesystem_repository.rs +++ b/crates/shirabe/src/repository/filesystem_repository.rs @@ -1,7 +1,6 @@ //! ref: composer/src/Composer/Repository/FilesystemRepository.php use crate::config::is_php_integer_key; -use crate::installed_versions::InstalledVersions; use crate::installer::InstallationManagerInterface; use crate::json::JsonFile; use crate::package::BasePackageHandle; @@ -338,30 +337,20 @@ impl FilesystemRepository { ); // make sure the in memory state is up to date with on disk - // The upstream in-process reload/selfDir/installedIsLocalDir tail is mirrored - // twice: into the Rust-side statics below, and into the PHP worker where the real - // observers (plugins) live. The push is skipped when no worker is running — with no - // child there is nothing that could observe the state; the glue skips it only when - // the class is not even autoloadable there (no Composer PHP runtime = no observer - // code either). + // The upstream in-process reload/selfDir/installedIsLocalDir tail is pushed to the + // PHP worker, where the real observers (plugins) live. The push is skipped when no + // worker is running — with no child there is nothing that could observe the state; + // the glue skips it only when the class is not even autoloadable there (no Composer + // PHP runtime = no observer code either). if shirabe_php_rpc::worker_is_running() { crate::event_dispatcher::unwrap_php_result(shirabe_php_rpc::call_function( "__shirabe_installed_versions_reload", vec![ - shirabe_php_rpc::PluginValue::from_php_mixed(&PhpMixed::Array( - versions.clone(), - )), + shirabe_php_rpc::PluginValue::from_php_mixed(&PhpMixed::Array(versions)), shirabe_php_rpc::PluginValue::string(repo_dir.clone()), ], ))?; } - InstalledVersions::reload(versions); - - // make sure the selfDir matches the expected data at runtime if the class was loaded from the vendor dir, as it may have been - // loaded from the Composer sources, causing packages to appear twice in that case if the installed.php is loaded in addition to the - // in memory loaded data from above - InstalledVersions::set_self_dir(repo_dir.replace('\\', "/")); - InstalledVersions::set_installed_is_local_dir(true); } Ok(()) -- cgit v1.3.1-4-g156e