aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/package/loader
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-16 13:05:05 +0900
committernsfisis <nsfisis@gmail.com>2026-05-16 13:05:05 +0900
commitcb5e9a2e65b87cc50c4f80f99bd7a36220b76536 (patch)
tree1779e1351160f6c607d01e9783a51914fa992baa /crates/shirabe/src/package/loader
parent6754d2435b0a1d2d48d1c969a027e9665b71e1e3 (diff)
downloadphp-shirabe-cb5e9a2e65b87cc50c4f80f99bd7a36220b76536.tar.gz
php-shirabe-cb5e9a2e65b87cc50c4f80f99bd7a36220b76536.tar.zst
php-shirabe-cb5e9a2e65b87cc50c4f80f99bd7a36220b76536.zip
feat(port): port RootPackageLoader.php
Diffstat (limited to 'crates/shirabe/src/package/loader')
-rw-r--r--crates/shirabe/src/package/loader/root_package_loader.rs429
1 files changed, 429 insertions, 0 deletions
diff --git a/crates/shirabe/src/package/loader/root_package_loader.rs b/crates/shirabe/src/package/loader/root_package_loader.rs
index 7e8e940..eef7d70 100644
--- a/crates/shirabe/src/package/loader/root_package_loader.rs
+++ b/crates/shirabe/src/package/loader/root_package_loader.rs
@@ -1 +1,430 @@
//! ref: composer/src/Composer/Package/Loader/RootPackageLoader.php
+
+use indexmap::IndexMap;
+use shirabe_external_packages::composer::pcre::preg::Preg;
+use shirabe_php_shim::{strtolower, ucfirst, LogicException, RuntimeException, UnexpectedValueException};
+
+use crate::config::Config;
+use crate::io::io_interface::IOInterface;
+use crate::package::base_package::{BasePackage, STABILITIES, SUPPORTED_LINK_TYPES};
+use crate::package::loader::array_loader::ArrayLoader;
+use crate::package::loader::validating_array_loader::ValidatingArrayLoader;
+use crate::package::package_interface::PackageInterface;
+use crate::package::root_alias_package::RootAliasPackage;
+use crate::package::root_package::RootPackage;
+use crate::package::version::version_guesser::VersionGuesser;
+use crate::package::version::version_parser::VersionParser;
+use crate::repository::repository_factory::RepositoryFactory;
+use crate::repository::repository_manager::RepositoryManager;
+use crate::util::platform::Platform;
+use crate::util::process_executor::ProcessExecutor;
+
+#[derive(Debug)]
+pub struct RootPackageLoader {
+ inner: ArrayLoader,
+ manager: RepositoryManager,
+ config: Config,
+ version_guesser: VersionGuesser,
+ io: Option<Box<dyn IOInterface>>,
+}
+
+impl RootPackageLoader {
+ pub fn new(
+ manager: RepositoryManager,
+ config: Config,
+ parser: Option<VersionParser>,
+ version_guesser: Option<VersionGuesser>,
+ io: Option<Box<dyn IOInterface>>,
+ ) -> Self {
+ let inner = ArrayLoader::new(parser);
+ let version_guesser = version_guesser.unwrap_or_else(|| {
+ let mut process_executor = ProcessExecutor::new(io.as_deref());
+ process_executor.enable_async();
+ VersionGuesser::new(&config, process_executor, inner.version_parser.clone())
+ });
+ Self {
+ inner,
+ manager,
+ config,
+ version_guesser,
+ io,
+ }
+ }
+
+ pub fn load(
+ &mut self,
+ config: IndexMap<String, Box<shirabe_php_shim::PhpMixed>>,
+ class: &str,
+ cwd: Option<&str>,
+ ) -> anyhow::Result<Box<dyn PackageInterface>> {
+ if class != "Composer\\Package\\RootPackage" {
+ shirabe_php_shim::trigger_error(
+ "The $class arg is deprecated, please reach out to Composer maintainers ASAP if you still need this.",
+ shirabe_php_shim::E_USER_DEPRECATED,
+ );
+ }
+
+ let mut config = config;
+
+ if !config.contains_key("name") {
+ config.insert(
+ "name".to_string(),
+ Box::new(shirabe_php_shim::PhpMixed::String("__root__".to_string())),
+ );
+ } else if let Some(err) = ValidatingArrayLoader::has_package_naming_error(
+ config["name"].as_string().unwrap_or(""),
+ false,
+ ) {
+ return Err(anyhow::anyhow!(RuntimeException {
+ message: format!("Your package name {}", err),
+ code: 0,
+ }));
+ }
+
+ let mut auto_versioned = false;
+ if !config.contains_key("version") {
+ let mut commit: Option<String> = None;
+
+ if Platform::get_env("COMPOSER_ROOT_VERSION").is_some() {
+ let version = self.version_guesser.get_root_version_from_env();
+ config.insert(
+ "version".to_string(),
+ Box::new(shirabe_php_shim::PhpMixed::String(version)),
+ );
+ } else {
+ let cwd_str = cwd.map(|s| s.to_string()).unwrap_or_else(|| Platform::get_cwd(true));
+ let version_data = self.version_guesser.guess_version(&config, &cwd_str);
+ if let Some(data) = version_data {
+ config.insert(
+ "version".to_string(),
+ Box::new(shirabe_php_shim::PhpMixed::String(data.pretty_version.clone())),
+ );
+ config.insert(
+ "version_normalized".to_string(),
+ Box::new(shirabe_php_shim::PhpMixed::String(data.version.clone())),
+ );
+ commit = data.commit;
+ }
+ }
+
+ if !config.contains_key("version") {
+ if let Some(ref io) = self.io {
+ let name = config["name"].as_string().unwrap_or("");
+ let package_type = config
+ .get("type")
+ .and_then(|v| v.as_string())
+ .unwrap_or("");
+ if name != "__root__" && package_type != "project" {
+ io.warning(&format!(
+ "Composer could not detect the root package ({}) version, defaulting to '1.0.0'. See https://getcomposer.org/root-version",
+ name
+ ));
+ }
+ }
+ config.insert(
+ "version".to_string(),
+ Box::new(shirabe_php_shim::PhpMixed::String("1.0.0".to_string())),
+ );
+ auto_versioned = true;
+ }
+
+ if let Some(commit_hash) = commit {
+ let mut source = IndexMap::new();
+ source.insert(
+ "type".to_string(),
+ Box::new(shirabe_php_shim::PhpMixed::String(String::new())),
+ );
+ source.insert(
+ "url".to_string(),
+ Box::new(shirabe_php_shim::PhpMixed::String(String::new())),
+ );
+ source.insert(
+ "reference".to_string(),
+ Box::new(shirabe_php_shim::PhpMixed::String(commit_hash.clone())),
+ );
+ config.insert(
+ "source".to_string(),
+ Box::new(shirabe_php_shim::PhpMixed::Array(source)),
+ );
+
+ let mut dist = IndexMap::new();
+ dist.insert(
+ "type".to_string(),
+ Box::new(shirabe_php_shim::PhpMixed::String(String::new())),
+ );
+ dist.insert(
+ "url".to_string(),
+ Box::new(shirabe_php_shim::PhpMixed::String(String::new())),
+ );
+ dist.insert(
+ "reference".to_string(),
+ Box::new(shirabe_php_shim::PhpMixed::String(commit_hash)),
+ );
+ config.insert(
+ "dist".to_string(),
+ Box::new(shirabe_php_shim::PhpMixed::Array(dist)),
+ );
+ }
+ }
+
+ let package = self.inner.load(config.clone(), "Composer\\Package\\RootPackage")?;
+
+ let real_package: &mut RootPackage = if let Some(alias_pkg) =
+ package.as_any_mut().downcast_mut::<RootAliasPackage>()
+ {
+ alias_pkg
+ .get_alias_of_mut()
+ .as_any_mut()
+ .downcast_mut::<RootPackage>()
+ .ok_or_else(|| {
+ anyhow::anyhow!(LogicException {
+ message: "Expecting a Composer\\Package\\RootPackage at this point"
+ .to_string(),
+ code: 0,
+ })
+ })?
+ } else if let Some(root_pkg) = package.as_any_mut().downcast_mut::<RootPackage>() {
+ root_pkg
+ } else {
+ return Err(anyhow::anyhow!(LogicException {
+ message: "Expecting a Composer\\Package\\RootPackage at this point".to_string(),
+ code: 0,
+ }));
+ };
+
+ if auto_versioned {
+ real_package
+ .replace_version(real_package.get_version().to_string(), RootPackage::DEFAULT_PRETTY_VERSION.to_string());
+ }
+
+ if let Some(min_stability) = config
+ .get("minimum-stability")
+ .and_then(|v| v.as_string())
+ {
+ real_package
+ .set_minimum_stability(VersionParser::normalize_stability(min_stability).to_string());
+ }
+
+ let mut aliases: Vec<IndexMap<String, String>> = vec![];
+ let mut stability_flags: IndexMap<String, i64> = IndexMap::new();
+ let mut references: IndexMap<String, String> = IndexMap::new();
+
+ for link_type in ["require", "require-dev"] {
+ if config.contains_key(link_type) {
+ let link_info = &SUPPORTED_LINK_TYPES[link_type];
+ let method = format!("get_{}", link_info.method);
+ let links: IndexMap<String, String> = real_package
+ .call_get_links_method(&method)
+ .iter()
+ .map(|(target, link)| {
+ (
+ target.clone(),
+ link.get_constraint()
+ .map(|c| c.get_pretty_string().to_string())
+ .unwrap_or_default(),
+ )
+ })
+ .collect();
+ aliases = self.extract_aliases(&links, aliases);
+ stability_flags = Self::extract_stability_flags(
+ &links,
+ real_package.get_minimum_stability(),
+ stability_flags,
+ );
+ references = Self::extract_references(&links, references);
+
+ let package_name = config["name"].as_string().unwrap_or("").to_string();
+ if links.contains_key(&package_name) {
+ return Err(anyhow::anyhow!(RuntimeException {
+ message: format!(
+ "Root package '{}' cannot require itself in its composer.json\nDid you accidentally name your root package after an external package?",
+ package_name
+ ),
+ code: 0,
+ }));
+ }
+ }
+ }
+
+ for link_type in SUPPORTED_LINK_TYPES.keys() {
+ if let Some(section) = config.get(*link_type) {
+ if let Some(section_map) = section.as_array() {
+ for (link_name, _constraint) in section_map {
+ if let Some(err) =
+ ValidatingArrayLoader::has_package_naming_error(link_name, true)
+ {
+ return Err(anyhow::anyhow!(RuntimeException {
+ message: format!("{}.{}", link_type, err),
+ code: 0,
+ }));
+ }
+ }
+ }
+ }
+ }
+
+ real_package.set_aliases(aliases);
+ real_package.set_stability_flags(stability_flags);
+ real_package.set_references(references);
+
+ if let Some(prefer_stable) = config.get("prefer-stable").and_then(|v| v.as_bool()) {
+ real_package.set_prefer_stable(prefer_stable);
+ }
+
+ if let Some(pkg_config) = config.get("config").and_then(|v| v.as_array()) {
+ real_package.set_config(
+ pkg_config
+ .iter()
+ .map(|(k, v)| (k.clone(), (**v).clone()))
+ .collect(),
+ );
+ }
+
+ let repos = RepositoryFactory::default_repos(None, &self.config, &mut self.manager)?;
+ for repo in repos {
+ self.manager.add_repository(repo);
+ }
+ real_package.set_repositories(self.config.get_repositories());
+
+ Ok(package)
+ }
+
+ fn extract_aliases(
+ &self,
+ requires: &IndexMap<String, String>,
+ mut aliases: Vec<IndexMap<String, String>>,
+ ) -> Vec<IndexMap<String, String>> {
+ for (req_name, req_version) in requires {
+ if let Some(m) = Preg::is_match_strict_groups(
+ r"(?:^|\| *|, *)([^,\s#|]+)(?:#[^ ]+)? +as +([^,\s|]+)(?:$| *\|| *,)",
+ req_version,
+ )
+ .unwrap_or(None)
+ {
+ let mut alias = IndexMap::new();
+ alias.insert("package".to_string(), strtolower(req_name));
+ alias.insert(
+ "version".to_string(),
+ self.inner
+ .version_parser
+ .normalize(&m[1], req_version)
+ .unwrap_or_default(),
+ );
+ alias.insert("alias".to_string(), m[2].clone());
+ alias.insert(
+ "alias_normalized".to_string(),
+ self.inner
+ .version_parser
+ .normalize(&m[2], req_version)
+ .unwrap_or_default(),
+ );
+ aliases.push(alias);
+ } else if req_version.contains(" as ") {
+ return {
+ panic!(
+ "{}",
+ UnexpectedValueException {
+ message: format!(
+ "Invalid alias definition in \"{}\": \"{}\". Aliases should be in the form \"exact-version as other-exact-version\".",
+ req_name, req_version
+ ),
+ code: 0,
+ }
+ .message
+ )
+ };
+ }
+ }
+ aliases
+ }
+
+ pub fn extract_stability_flags(
+ requires: &IndexMap<String, String>,
+ minimum_stability: &str,
+ mut stability_flags: IndexMap<String, i64>,
+ ) -> IndexMap<String, i64> {
+ let stabilities = &*STABILITIES;
+ let minimum_stability_val = stabilities[minimum_stability];
+
+ for (req_name, req_version) in requires {
+ let mut constraints: Vec<String> = vec![];
+
+ let or_split = Preg::split(r"\s*\|\|?\s*", req_version.trim()).unwrap_or_default();
+ for or_constraint in &or_split {
+ let and_split = Preg::split(
+ r"(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)",
+ or_constraint,
+ )
+ .unwrap_or_default();
+ for and_constraint in and_split {
+ constraints.push(and_constraint);
+ }
+ }
+
+ let stability_names: Vec<&str> = stabilities.keys().copied().collect();
+ let pattern = format!(
+ "^[^@]*?@({})$",
+ stability_names.join("|")
+ );
+
+ let mut matched = false;
+ for constraint in &constraints {
+ if let Some(Some(m)) =
+ Preg::is_match_strict_groups(&pattern, constraint).ok()
+ {
+ let name = strtolower(req_name);
+ let stability = stabilities[VersionParser::normalize_stability(&m[1])];
+
+ if stability_flags.get(&name).copied().unwrap_or(i64::MAX) > stability {
+ continue;
+ }
+ stability_flags.insert(name, stability);
+ matched = true;
+ }
+ }
+
+ if matched {
+ continue;
+ }
+
+ for constraint in &constraints {
+ let req_version_stripped =
+ Preg::replace(r"^([^,\s@]+) as .+$", "$1", constraint).unwrap_or_default();
+ if Preg::is_match(r"^[^,\s@]+$", &req_version_stripped).unwrap_or(false) {
+ let stability_name = VersionParser::parse_stability(&req_version_stripped);
+ if stability_name != "stable" {
+ let name = strtolower(req_name);
+ let stability = stabilities[stability_name];
+ if stability_flags.get(&name).copied().unwrap_or(i64::MAX) > stability
+ || minimum_stability_val > stability
+ {
+ continue;
+ }
+ stability_flags.insert(name, stability);
+ }
+ }
+ }
+ }
+
+ stability_flags
+ }
+
+ pub fn extract_references(
+ requires: &IndexMap<String, String>,
+ mut references: IndexMap<String, String>,
+ ) -> IndexMap<String, String> {
+ for (req_name, req_version) in requires {
+ let req_version =
+ Preg::replace(r"^([^,\s@]+) as .+$", "$1", req_version).unwrap_or_default();
+ if let Some(Some(m)) =
+ Preg::is_match_strict_groups(r"^[^,\s@]+?#([a-f0-9]+)$", &req_version).ok()
+ {
+ if VersionParser::parse_stability(&req_version) == "dev" {
+ let name = strtolower(req_name);
+ references.insert(name, m[1].clone());
+ }
+ }
+ }
+ references
+ }
+}