From 427e059e4ffc6ce5ff130c803f9e533b7c125089 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Fri, 7 Aug 2026 20:51:59 +0900 Subject: feat(plugin): cross alias packages over RPC as proxy stubs AliasPackage, CompleteAliasPackage and RootAliasPackage now have generated proxy stubs, so a package handed to a plugin no longer has to be a real package: it crosses as the stub matching its concrete variant, answers getAliasOf / setRootPackageAlias / isRootPackageAlias / hasSelfVersionRequires, and can be constructed from plugin code. The setters RootPackageInterface declares are routed through that interface for every root package instead of through the base Package state. Only the alias variant needs it -- RootAliasPackage overrides all nine to write through to the package it aliases -- but a real RootPackage delegates to the same base state either way, so both take one path. An alias of an alias has no representation here, so narrowing the constructor argument to a real package is an explicit error rather than a silent demotion. Co-Authored-By: Claude Opus 5 (1M context) --- .../php/stubs/Composer/Package/AliasPackage.php | 35 +++++ .../Composer/Package/CompleteAliasPackage.php | 140 ++++++++++++++++++ .../stubs/Composer/Package/RootAliasPackage.php | 120 ++++++++++++++++ crates/shirabe-php-rpc/src/lib.rs | 12 ++ crates/shirabe/src/package/handle.rs | 20 +++ crates/shirabe/src/plugin/php_plugin_proxy.rs | 158 +++++++++++++++------ crates/shirabe/tests/plugin/alias_package_test.rs | 89 ++++++++++++ .../plugin/fixtures/alias-v1/Alias/Plugin.php | 63 ++++++++ .../tests/plugin/fixtures/alias-v1/composer.json | 12 ++ crates/shirabe/tests/plugin/main.rs | 1 + 10 files changed, 610 insertions(+), 40 deletions(-) create mode 100644 crates/shirabe-php-rpc/php/stubs/Composer/Package/AliasPackage.php create mode 100644 crates/shirabe-php-rpc/php/stubs/Composer/Package/CompleteAliasPackage.php create mode 100644 crates/shirabe-php-rpc/php/stubs/Composer/Package/RootAliasPackage.php create mode 100644 crates/shirabe/tests/plugin/alias_package_test.rs create mode 100644 crates/shirabe/tests/plugin/fixtures/alias-v1/Alias/Plugin.php create mode 100644 crates/shirabe/tests/plugin/fixtures/alias-v1/composer.json (limited to 'crates') diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Package/AliasPackage.php b/crates/shirabe-php-rpc/php/stubs/Composer/Package/AliasPackage.php new file mode 100644 index 00000000..f7e2e404 --- /dev/null +++ b/crates/shirabe-php-rpc/php/stubs/Composer/Package/AliasPackage.php @@ -0,0 +1,35 @@ +__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$aliasOf, $version, $prettyVersion]]); + \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this); + } + + public function getAliasOf() + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getAliasOf', []); + } + + public function setRootPackageAlias(bool $value): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setRootPackageAlias', [$value]); + } + + public function isRootPackageAlias(): bool + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'isRootPackageAlias', []); + } + + public function hasSelfVersionRequires(): bool + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'hasSelfVersionRequires', []); + } +} diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Package/CompleteAliasPackage.php b/crates/shirabe-php-rpc/php/stubs/Composer/Package/CompleteAliasPackage.php new file mode 100644 index 00000000..19555915 --- /dev/null +++ b/crates/shirabe-php-rpc/php/stubs/Composer/Package/CompleteAliasPackage.php @@ -0,0 +1,140 @@ +__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$aliasOf, $version, $prettyVersion]]); + \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this); + } + + public function getScripts(): array + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getScripts', []); + } + + public function setScripts(array $scripts): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setScripts', [$scripts]); + } + + public function getRepositories(): array + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getRepositories', []); + } + + public function setRepositories(array $repositories): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setRepositories', [$repositories]); + } + + public function getLicense(): array + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getLicense', []); + } + + public function setLicense(array $license): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setLicense', [$license]); + } + + public function getKeywords(): array + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getKeywords', []); + } + + public function setKeywords(array $keywords): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setKeywords', [$keywords]); + } + + public function getDescription(): ?string + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getDescription', []); + } + + public function setDescription(?string $description): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setDescription', [$description]); + } + + public function getHomepage(): ?string + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getHomepage', []); + } + + public function setHomepage(?string $homepage): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setHomepage', [$homepage]); + } + + public function getAuthors(): array + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getAuthors', []); + } + + public function setAuthors(array $authors): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setAuthors', [$authors]); + } + + public function getSupport(): array + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getSupport', []); + } + + public function setSupport(array $support): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setSupport', [$support]); + } + + public function getFunding(): array + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getFunding', []); + } + + public function setFunding(array $funding): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setFunding', [$funding]); + } + + public function isAbandoned(): bool + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'isAbandoned', []); + } + + public function getReplacementPackage(): ?string + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getReplacementPackage', []); + } + + public function setAbandoned($abandoned): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setAbandoned', [$abandoned]); + } + + public function getArchiveName(): ?string + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getArchiveName', []); + } + + public function setArchiveName(?string $name): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setArchiveName', [$name]); + } + + public function getArchiveExcludes(): array + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getArchiveExcludes', []); + } + + public function setArchiveExcludes(array $excludes): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setArchiveExcludes', [$excludes]); + } +} diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Package/RootAliasPackage.php b/crates/shirabe-php-rpc/php/stubs/Composer/Package/RootAliasPackage.php new file mode 100644 index 00000000..9c4a294f --- /dev/null +++ b/crates/shirabe-php-rpc/php/stubs/Composer/Package/RootAliasPackage.php @@ -0,0 +1,120 @@ +__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$aliasOf, $version, $prettyVersion]]); + \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this); + } + + public function getAliases(): array + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getAliases', []); + } + + public function getMinimumStability(): string + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getMinimumStability', []); + } + + public function getStabilityFlags(): array + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getStabilityFlags', []); + } + + public function getReferences(): array + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getReferences', []); + } + + public function getPreferStable(): bool + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getPreferStable', []); + } + + public function getConfig(): array + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getConfig', []); + } + + public function setRequires(array $requires): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setRequires', [$requires]); + } + + public function setDevRequires(array $devRequires): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setDevRequires', [$devRequires]); + } + + public function setConflicts(array $conflicts): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setConflicts', [$conflicts]); + } + + public function setProvides(array $provides): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setProvides', [$provides]); + } + + public function setReplaces(array $replaces): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setReplaces', [$replaces]); + } + + public function setAutoload(array $autoload): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setAutoload', [$autoload]); + } + + public function setDevAutoload(array $devAutoload): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setDevAutoload', [$devAutoload]); + } + + public function setStabilityFlags(array $stabilityFlags): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setStabilityFlags', [$stabilityFlags]); + } + + public function setMinimumStability(string $minimumStability): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setMinimumStability', [$minimumStability]); + } + + public function setPreferStable(bool $preferStable): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setPreferStable', [$preferStable]); + } + + public function setConfig(array $config): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setConfig', [$config]); + } + + public function setReferences(array $references): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setReferences', [$references]); + } + + public function setAliases(array $aliases): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setAliases', [$aliases]); + } + + public function setSuggests(array $suggests): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setSuggests', [$suggests]); + } + + public function setExtra(array $extra): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'setExtra', [$extra]); + } +} diff --git a/crates/shirabe-php-rpc/src/lib.rs b/crates/shirabe-php-rpc/src/lib.rs index e2f5fd1e..1b501caf 100644 --- a/crates/shirabe-php-rpc/src/lib.rs +++ b/crates/shirabe-php-rpc/src/lib.rs @@ -875,6 +875,18 @@ const STUB_FILES: &[(&str, &str)] = &[ "Composer/Package/RootPackage.php", include_str!("../php/stubs/Composer/Package/RootPackage.php"), ), + ( + "Composer/Package/AliasPackage.php", + include_str!("../php/stubs/Composer/Package/AliasPackage.php"), + ), + ( + "Composer/Package/CompleteAliasPackage.php", + include_str!("../php/stubs/Composer/Package/CompleteAliasPackage.php"), + ), + ( + "Composer/Package/RootAliasPackage.php", + include_str!("../php/stubs/Composer/Package/RootAliasPackage.php"), + ), ( "Composer/Repository/ArrayRepository.php", include_str!("../php/stubs/Composer/Repository/ArrayRepository.php"), diff --git a/crates/shirabe/src/package/handle.rs b/crates/shirabe/src/package/handle.rs index aed8be0f..45d46f49 100644 --- a/crates/shirabe/src/package/handle.rs +++ b/crates/shirabe/src/package/handle.rs @@ -76,6 +76,26 @@ impl AnyPackage { } } + /// For `AliasPackage`'s own methods, which `PackageInterface` does not carry and the + /// alias subclasses inherit rather than redeclare. + pub fn as_alias_package(&self) -> Option<&AliasPackage> { + match self { + Self::AliasPackage(p) => Some(p), + Self::CompleteAliasPackage(p) => Some(&p.inner), + Self::RootAliasPackage(p) => Some(&p.inner.inner), + _ => None, + } + } + + pub fn as_alias_package_mut(&mut self) -> Option<&mut AliasPackage> { + match self { + Self::AliasPackage(p) => Some(p), + Self::CompleteAliasPackage(p) => Some(&mut p.inner), + Self::RootAliasPackage(p) => Some(&mut p.inner.inner), + _ => None, + } + } + pub fn as_root_package_interface(&self) -> Option<&dyn RootPackageInterface> { match self { Self::RootPackage(p) => Some(p), diff --git a/crates/shirabe/src/plugin/php_plugin_proxy.rs b/crates/shirabe/src/plugin/php_plugin_proxy.rs index 354b89aa..bd40b358 100644 --- a/crates/shirabe/src/plugin/php_plugin_proxy.rs +++ b/crates/shirabe/src/plugin/php_plugin_proxy.rs @@ -143,19 +143,14 @@ pub(crate) fn rust_handle_value(rhandle: u64, class: &str) -> PluginValue { } /// The proxy stub class matching a package's concrete variant. -fn package_stub_class( - package: &std::rc::Rc>, -) -> Result<&'static str, PhpThrow> { +fn package_stub_class(package: &std::rc::Rc>) -> &'static str { match &*package.borrow() { - AnyPackage::Package(_) => Ok("Composer\\Package\\Package"), - AnyPackage::CompletePackage(_) => Ok("Composer\\Package\\CompletePackage"), - AnyPackage::RootPackage(_) => Ok("Composer\\Package\\RootPackage"), - // TODO(plugin): alias packages need proxy stubs of their own before they can cross. - AnyPackage::AliasPackage(_) - | AnyPackage::CompleteAliasPackage(_) - | AnyPackage::RootAliasPackage(_) => Err(runtime_throw( - "alias packages are not available over RPC yet".to_string(), - )), + AnyPackage::Package(_) => "Composer\\Package\\Package", + AnyPackage::CompletePackage(_) => "Composer\\Package\\CompletePackage", + AnyPackage::RootPackage(_) => "Composer\\Package\\RootPackage", + AnyPackage::AliasPackage(_) => "Composer\\Package\\AliasPackage", + AnyPackage::CompleteAliasPackage(_) => "Composer\\Package\\CompleteAliasPackage", + AnyPackage::RootAliasPackage(_) => "Composer\\Package\\RootAliasPackage", } } @@ -177,10 +172,10 @@ fn repository_stub_class(repository: &RepositoryInterfaceHandle) -> Result<&'sta /// Registers a package and returns its wire descriptor. pub(crate) fn package_handle_value( package: &std::rc::Rc>, -) -> Result { - let class = package_stub_class(package)?; +) -> PluginValue { + let class = package_stub_class(package); let rhandle = register_entity(RustEntity::Package(package.clone())); - Ok(rust_handle_value(rhandle, class)) + rust_handle_value(rhandle, class) } /// The PHP class name (= proxy stub class) of a Rust IO instance, for the `__class` field of @@ -376,6 +371,42 @@ pub(crate) fn construct_entity(args: &[PluginValue]) -> Result AnyPackage::CompletePackage( crate::package::CompletePackage::new(string_arg(0)?, string_arg(1)?, string_arg(2)?), ), + // The alias target has to be a package that already lives on the Rust side; an alias of + // an alias has no Rust representation, so its narrowing is an explicit error too. + "Composer\\Package\\AliasPackage" => { + let alias_of = package_from_arg(&class, ctor_args.first())? + .as_package() + .ok_or_else(|| runtime_throw(format!("{class} expects a real Package to alias")))?; + AnyPackage::AliasPackage(crate::package::AliasPackage::new( + alias_of, + string_arg(1)?, + string_arg(2)?, + )) + } + "Composer\\Package\\CompleteAliasPackage" => { + let alias_of = package_from_arg(&class, ctor_args.first())? + .as_complete_package() + .ok_or_else(|| { + runtime_throw(format!("{class} expects a real CompletePackage to alias")) + })?; + AnyPackage::CompleteAliasPackage(crate::package::CompleteAliasPackage::new( + alias_of, + string_arg(1)?, + string_arg(2)?, + )) + } + "Composer\\Package\\RootAliasPackage" => { + let alias_of = package_from_arg(&class, ctor_args.first())? + .as_root_package() + .ok_or_else(|| { + runtime_throw(format!("{class} expects a real RootPackage to alias")) + })?; + AnyPackage::RootAliasPackage(crate::package::RootAliasPackage::new( + alias_of, + string_arg(1)?, + string_arg(2)?, + )) + } // TODO(plugin): the remaining proxied classes get a construction story on demand, // driven by explicit errors from real plugins. Each one has to decide what a // plugin-built instance means for the Rust-side graph, which is why none of them is @@ -450,7 +481,7 @@ fn dispatch_composer_method( } "getPackage" => { let package = composer.borrow().get_package().as_rc().clone(); - package_handle_value(&package) + Ok(package_handle_value(&package)) } "getEventDispatcher" => { let dispatcher = composer.borrow().get_event_dispatcher(); @@ -791,7 +822,7 @@ fn dispatch_repository_method( })?; let mut items = Vec::with_capacity(packages.len()); for package in packages { - items.push(package_handle_value(package.as_rc())?); + items.push(package_handle_value(package.as_rc())); } Ok(PluginValue::List(items)) } @@ -1340,6 +1371,31 @@ fn dispatch_package_method( .set_transport_options(options); return Ok(PluginValue::Null); } + // `RootAliasPackage` overrides each of these to write through to its alias target, and + // `RootPackage` reaches the same base state either way, so both go through the interface. + "setRequires" | "setDevRequires" | "setConflicts" | "setProvides" | "setReplaces" + | "setAutoload" | "setDevAutoload" | "setSuggests" | "setExtra" + if package.borrow().is_root() => + { + let mut borrowed = package.borrow_mut(); + let package = borrowed + .as_root_package_interface_mut() + .expect("a root package exposes RootPackageInterface"); + match method_name { + "setRequires" => package.set_requires(link_map_arg(method_name, args.first())?), + "setDevRequires" => { + package.set_dev_requires(link_map_arg(method_name, args.first())?) + } + "setConflicts" => package.set_conflicts(link_map_arg(method_name, args.first())?), + "setProvides" => package.set_provides(link_map_arg(method_name, args.first())?), + "setReplaces" => package.set_replaces(link_map_arg(method_name, args.first())?), + "setAutoload" => package.set_autoload(map_arg(method_name, args.first())?), + "setDevAutoload" => package.set_dev_autoload(map_arg(method_name, args.first())?), + "setSuggests" => package.set_suggests(string_map_arg(method_name, args.first())?), + _ => package.set_extra(map_arg(method_name, args.first())?), + } + return Ok(PluginValue::Null); + } // `Package`'s own setters. The concrete subclasses inherit them (their PHP overrides in // `RootPackage` delegate to the same base state), so the base package answers for every // real variant. @@ -1527,6 +1583,35 @@ fn dispatch_package_method( let this = PackageInterfaceHandle::from_rc_unchecked(package.clone()); return Ok(PluginValue::Bool(this.equals(&other))); } + // The subclasses narrow `getAliasOf`'s return type to their own alias target, but every + // variant holds the one entity. + "getAliasOf" | "isRootPackageAlias" | "hasSelfVersionRequires" => { + let borrowed = package.borrow(); + let alias = borrowed.as_alias_package().ok_or_else(|| { + runtime_throw(format!( + "`{method_name}` is not available on this package over RPC" + )) + })?; + return Ok(match method_name { + "getAliasOf" => package_handle_value(alias.get_alias_of().as_rc()), + "isRootPackageAlias" => PluginValue::Bool(alias.is_root_package_alias()), + _ => PluginValue::Bool(alias.has_self_version_requires()), + }); + } + "setRootPackageAlias" => { + let value = bool_arg(method_name, args.first())?; + package + .borrow_mut() + .as_alias_package_mut() + .ok_or_else(|| { + runtime_throw( + "`setRootPackageAlias` is not available on this package over RPC" + .to_string(), + ) + })? + .set_root_package_alias(value); + return Ok(PluginValue::Null); + } _ => {} } @@ -2138,16 +2223,14 @@ impl PhpInstallerProxy { )) } - fn package_arg(package: &PackageInterfaceHandle) -> anyhow::Result { - Ok(package_handle_value(package.as_rc())?) + fn package_arg(package: &PackageInterfaceHandle) -> PluginValue { + package_handle_value(package.as_rc()) } - fn optional_package_arg( - package: &Option, - ) -> anyhow::Result { + fn optional_package_arg(package: &Option) -> PluginValue { match package { Some(package) => Self::package_arg(package), - None => Ok(PluginValue::Null), + None => PluginValue::Null, } } @@ -2209,7 +2292,7 @@ impl crate::installer::InstallerInterface for PhpInstallerProxy { repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result { - let args = vec![Self::repo_arg(repo)?, Self::package_arg(&package)?]; + let args = vec![Self::repo_arg(repo)?, Self::package_arg(&package)]; match self.call("isInstalled", args)? { PluginValue::Bool(installed) => Ok(installed), other => Err(self.unsupported_shape("isInstalled", &other)), @@ -2222,8 +2305,8 @@ impl crate::installer::InstallerInterface for PhpInstallerProxy { prev_package: Option, ) -> anyhow::Result> { let args = vec![ - Self::package_arg(&package)?, - Self::optional_package_arg(&prev_package)?, + Self::package_arg(&package), + Self::optional_package_arg(&prev_package), ]; let value = self.call("download", args)?; self.promise_result("download", value) @@ -2237,8 +2320,8 @@ impl crate::installer::InstallerInterface for PhpInstallerProxy { ) -> anyhow::Result> { let args = vec![ PluginValue::string(r#type), - Self::package_arg(&package)?, - Self::optional_package_arg(&prev_package)?, + Self::package_arg(&package), + Self::optional_package_arg(&prev_package), ]; let value = self.call("prepare", args)?; self.promise_result("prepare", value) @@ -2249,7 +2332,7 @@ impl crate::installer::InstallerInterface for PhpInstallerProxy { repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result> { - let args = vec![Self::repo_arg(repo)?, Self::package_arg(&package)?]; + let args = vec![Self::repo_arg(repo)?, Self::package_arg(&package)]; let value = self.call("install", args)?; self.promise_result("install", value) } @@ -2262,8 +2345,8 @@ impl crate::installer::InstallerInterface for PhpInstallerProxy { ) -> anyhow::Result> { let args = vec![ Self::repo_arg(repo)?, - Self::package_arg(&initial)?, - Self::package_arg(&target)?, + Self::package_arg(&initial), + Self::package_arg(&target), ]; let value = self.call("update", args)?; self.promise_result("update", value) @@ -2274,7 +2357,7 @@ impl crate::installer::InstallerInterface for PhpInstallerProxy { repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result> { - let args = vec![Self::repo_arg(repo)?, Self::package_arg(&package)?]; + let args = vec![Self::repo_arg(repo)?, Self::package_arg(&package)]; let value = self.call("uninstall", args)?; self.promise_result("uninstall", value) } @@ -2287,8 +2370,8 @@ impl crate::installer::InstallerInterface for PhpInstallerProxy { ) -> anyhow::Result> { let args = vec![ PluginValue::string(r#type), - Self::package_arg(&package)?, - Self::optional_package_arg(&prev_package)?, + Self::package_arg(&package), + Self::optional_package_arg(&prev_package), ]; let value = self.call("cleanup", args)?; self.promise_result("cleanup", value) @@ -2298,12 +2381,7 @@ impl crate::installer::InstallerInterface for PhpInstallerProxy { // PHP declares `getInstallPath(): string`; a failure here is a plugin error the // infallible signature cannot carry, so it aborts rather than answering a path that // would silently install the package in the wrong place. - let args = vec![Self::package_arg(&package).unwrap_or_else(|error| { - panic!( - "{}::getInstallPath argument failed: {error:#}", - self.handle.class - ) - })]; + let args = vec![Self::package_arg(&package)]; let value = self.call("getInstallPath", args).unwrap_or_else(|error| { panic!( "{}::getInstallPath failed over RPC: {error:#}", diff --git a/crates/shirabe/tests/plugin/alias_package_test.rs b/crates/shirabe/tests/plugin/alias_package_test.rs new file mode 100644 index 00000000..b32d2b67 --- /dev/null +++ b/crates/shirabe/tests/plugin/alias_package_test.rs @@ -0,0 +1,89 @@ +//! Shirabe-specific integration tests for the alias package proxy stubs. Upstream Composer has +//! no test for this (its plugins run in-process), so the fixture `fixtures/alias-v1` is +//! Shirabe-owned. + +use crate::async_runtime::run; +use crate::plugin_installer_test::{lock_php_worker, new_installer, php_runtime_available, set_up}; +use shirabe::installer::InstallerInterface; +use shirabe::package::loader::{ArrayLoader, JsonLoader, JsonLoaderInput}; +use shirabe::package::{ + CompleteAliasPackageHandle, CompletePackageHandle, PackageInterfaceHandle, RootPackageHandle, +}; +use shirabe_php_shim::PhpMixed; + +fn alias_fixture_package() -> PackageInterfaceHandle { + let loader = JsonLoader::new(Box::new(ArrayLoader::new(None, false))); + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/plugin/fixtures/alias-v1/composer.json"); + loader + .load(JsonLoaderInput::String( + path.canonicalize().unwrap().to_str().unwrap().to_string(), + )) + .unwrap() +} + +#[test] +fn test_alias_packages_cross_the_plugin_boundary() { + if !php_runtime_available() { + return; + } + let _worker = lock_php_worker(); + let set_up = set_up(); + + let aliased = CompletePackageHandle::new( + "vendor/aliased".to_string(), + "1.0.0.0".to_string(), + "1.0.0".to_string(), + ); + aliased.set_description("an aliased package".to_string()); + let alias = CompleteAliasPackageHandle::new( + aliased.clone(), + "2.0.0.0".to_string(), + "2.0.0".to_string(), + ); + set_up + .repository + .borrow_mut() + .add_package(alias.clone().into()) + .unwrap(); + + let real_root = RootPackageHandle::new( + "dummy/root".to_string(), + "1.0.0.0".to_string(), + "1.0.0".to_string(), + ); + let root_alias = shirabe::package::RootAliasPackageHandle::new( + real_root.clone(), + "1.1.0.0".to_string(), + "1.1.0".to_string(), + ); + set_up.composer.borrow_mut().set_package(root_alias.into()); + + let installer = new_installer(&set_up); + set_up.pm.borrow_mut().load_installed_plugins().unwrap(); + run(installer.install(&set_up.repository, alias_fixture_package())).unwrap(); + + assert_eq!( + "alias: Composer\\Package\\CompleteAliasPackage vendor/aliased 2.0.0 of Composer\\Package\\CompletePackage 1.0.0\n\ + alias description: an aliased package\n\ + alias self-version: no\n\ + alias root-flag: no\n\ + alias root-flag: yes\n\ + root: Composer\\Package\\RootAliasPackage dummy/root 1.1.0 of Composer\\Package\\RootPackage 1.0.0\n\ + root aliasOf identity: same\n\ + root minimum stability: stable\n\ + built: Composer\\Package\\CompleteAliasPackage vendor/aliased 9.9.9 of Composer\\Package\\CompletePackage 1.0.0\n", + set_up.io.borrow().get_output() + ); + + // The flag the plugin set landed on the Rust-side entity, not on a child-side copy. + let alias: shirabe::package::AliasPackageHandle = alias.into(); + assert!(alias.is_root_package_alias()); + + // `RootAliasPackage`'s setters write through to the package it aliases. + assert_eq!("dev", real_root.get_minimum_stability()); + assert_eq!( + Some(&PhpMixed::String("alias-v1".to_string())), + real_root.get_extra().get("seen-by") + ); +} diff --git a/crates/shirabe/tests/plugin/fixtures/alias-v1/Alias/Plugin.php b/crates/shirabe/tests/plugin/fixtures/alias-v1/Alias/Plugin.php new file mode 100644 index 00000000..373348e8 --- /dev/null +++ b/crates/shirabe/tests/plugin/fixtures/alias-v1/Alias/Plugin.php @@ -0,0 +1,63 @@ +getRepositoryManager()->getLocalRepository()->getPackages() as $package) { + if (!$package instanceof AliasPackage) { + continue; + } + $aliased = $package->getAliasOf(); + $io->write('alias: ' . self::describe($package)); + $io->write('alias description: ' . $package->getDescription()); + $io->write('alias self-version: ' . ($package->hasSelfVersionRequires() ? 'yes' : 'no')); + $io->write('alias root-flag: ' . ($package->isRootPackageAlias() ? 'yes' : 'no')); + $package->setRootPackageAlias(true); + $io->write('alias root-flag: ' . ($package->isRootPackageAlias() ? 'yes' : 'no')); + } + + $root = $composer->getPackage(); + if (!$root instanceof RootAliasPackage) { + throw new \RuntimeException('not a RootAliasPackage: ' . get_class($root)); + } + $io->write('root: ' . self::describe($root)); + $io->write('root aliasOf identity: ' . ($root->getAliasOf() === $root->getAliasOf() ? 'same' : 'distinct')); + $io->write('root minimum stability: ' . $root->getMinimumStability()); + $root->setMinimumStability('dev'); + $root->setExtra(['seen-by' => 'alias-v1']); + + $built = new CompleteAliasPackage($aliased, '9.9.9.9', '9.9.9'); + $io->write('built: ' . self::describe($built)); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + } + + public function uninstall(Composer $composer, IOInterface $io) + { + } + + private static function describe(AliasPackage $package): string + { + return sprintf( + '%s %s %s of %s %s', + get_class($package), + $package->getName(), + $package->getPrettyVersion(), + get_class($package->getAliasOf()), + $package->getAliasOf()->getPrettyVersion() + ); + } +} diff --git a/crates/shirabe/tests/plugin/fixtures/alias-v1/composer.json b/crates/shirabe/tests/plugin/fixtures/alias-v1/composer.json new file mode 100644 index 00000000..ac831b6e --- /dev/null +++ b/crates/shirabe/tests/plugin/fixtures/alias-v1/composer.json @@ -0,0 +1,12 @@ +{ + "name": "alias-v1", + "version": "1.0.0", + "type": "composer-plugin", + "autoload": { "psr-0": { "Alias": "" } }, + "extra": { + "class": "Alias\\Plugin" + }, + "require": { + "composer-plugin-api": "^2.0" + } +} diff --git a/crates/shirabe/tests/plugin/main.rs b/crates/shirabe/tests/plugin/main.rs index f5a29ba6..7e32a616 100644 --- a/crates/shirabe/tests/plugin/main.rs +++ b/crates/shirabe/tests/plugin/main.rs @@ -3,6 +3,7 @@ mod async_runtime; #[path = "../common/config_stub.rs"] mod config_stub; +mod alias_package_test; mod e2e_command_provider_test; mod e2e_extension_installer_test; mod e2e_installer_test; -- cgit v1.3.1