diff options
Diffstat (limited to 'crates/shirabe')
| -rw-r--r-- | crates/shirabe/src/package/handle.rs | 20 | ||||
| -rw-r--r-- | crates/shirabe/src/plugin/php_plugin_proxy.rs | 158 | ||||
| -rw-r--r-- | crates/shirabe/tests/plugin/alias_package_test.rs | 89 | ||||
| -rw-r--r-- | crates/shirabe/tests/plugin/fixtures/alias-v1/Alias/Plugin.php | 63 | ||||
| -rw-r--r-- | crates/shirabe/tests/plugin/fixtures/alias-v1/composer.json | 12 | ||||
| -rw-r--r-- | crates/shirabe/tests/plugin/main.rs | 1 |
6 files changed, 303 insertions, 40 deletions
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<std::cell::RefCell<AnyPackage>>, -) -> Result<&'static str, PhpThrow> { +fn package_stub_class(package: &std::rc::Rc<std::cell::RefCell<AnyPackage>>) -> &'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<std::cell::RefCell<AnyPackage>>, -) -> Result<PluginValue, PhpThrow> { - 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<PluginValue, PhpT "Composer\\Package\\CompletePackage" => 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<PluginValue> { - 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<PackageInterfaceHandle>, - ) -> anyhow::Result<PluginValue> { + fn optional_package_arg(package: &Option<PackageInterfaceHandle>) -> 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<bool> { - 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<PackageInterfaceHandle>, ) -> anyhow::Result<Option<PhpMixed>> { 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<Option<PhpMixed>> { 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<Option<PhpMixed>> { - 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<Option<PhpMixed>> { 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<Option<PhpMixed>> { - 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<Option<PhpMixed>> { 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 @@ +<?php + +namespace Alias; + +use Composer\Composer; +use Composer\IO\IOInterface; +use Composer\Package\AliasPackage; +use Composer\Package\CompleteAliasPackage; +use Composer\Package\RootAliasPackage; +use Composer\Plugin\PluginInterface; + +class Plugin implements PluginInterface +{ + public function activate(Composer $composer, IOInterface $io) + { + $aliased = null; + foreach ($composer->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; |
