aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--crates/shirabe/src/event_dispatcher/event_dispatcher.rs29
-rw-r--r--crates/shirabe/src/plugin/php_plugin_proxy.rs93
-rw-r--r--crates/shirabe/tests/plugin/e2e_script_event_test.rs90
-rw-r--r--crates/shirabe/tests/plugin/fixtures/e2e-script-event/project/composer.json17
-rw-r--r--crates/shirabe/tests/plugin/fixtures/e2e-script-event/project/src/ScriptEventRecorder.php22
-rw-r--r--crates/shirabe/tests/plugin/main.rs1
6 files changed, 200 insertions, 52 deletions
diff --git a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
index b22c7c0a..ece83533 100644
--- a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
+++ b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
@@ -1150,6 +1150,16 @@ try {{
.into()
})?;
+ // The event stub the script receives hands out stubs of the Composer object graph, and
+ // those extend and implement the real Composer contracts (`PackageInterface` and the
+ // rest), which only the Composer PHP runtime carries.
+ let cache_dir = self
+ .composer()
+ .borrow_partial()
+ .get_config()
+ .borrow()
+ .get_str("cache-dir")?;
+ Self::ensure_composer_php_runtime(std::path::Path::new(&cache_dir))?;
Self::ensure_script_autoloader()?;
let rhandle = shirabe_php_rpc::alloc_rhandle();
let mut dispatcher = ScriptRpcDispatcher {
@@ -1640,12 +1650,12 @@ try {{
/// Serves `CallRustMethod` requests issued by the PHP worker while a script-related call is in
/// flight: Rust handle 0 is the runtime service endpoint (autoload lookups against the
-/// Rust-side [`ClassLoader`]), and at most one live event handle is exposed per dispatched
-/// call.
+/// Rust-side [`ClassLoader`]), every other rhandle resolves through the R table, except the
+/// one live event handle exposed per dispatched call.
///
-/// TODO(plugin): this per-call scope stands in for persistent R-table registration; a stub
-/// retained by the script beyond the call observes an unknown handle error instead of the
-/// live object.
+/// TODO(plugin): a script that stores the event stub beyond its own call observes an unknown
+/// handle error afterwards; keeping events in the R table needs full proxying of the object
+/// graph an event exposes, which does not exist yet.
struct ScriptRpcDispatcher<'a> {
loader: Option<ClassLoader>,
event: Option<(u64, &'a dyn EventInterface)>,
@@ -1691,10 +1701,11 @@ impl RustMethodDispatcher for ScriptRpcDispatcher<'_> {
Some((event_rhandle, event)) if event_rhandle == rhandle => {
dispatch_event_method(event, method_name)
}
- _ => Err(runtime_throw(format!(
- "unknown Rust handle {rhandle} (script-event handles are scoped to a single \
- dispatched call)"
- ))),
+ _ => crate::plugin::php_plugin_proxy::dispatch_r_table_method(
+ rhandle,
+ method_name,
+ &args,
+ ),
}
}
}
diff --git a/crates/shirabe/src/plugin/php_plugin_proxy.rs b/crates/shirabe/src/plugin/php_plugin_proxy.rs
index 0cd36b18..9ebdbbff 100644
--- a/crates/shirabe/src/plugin/php_plugin_proxy.rs
+++ b/crates/shirabe/src/plugin/php_plugin_proxy.rs
@@ -340,52 +340,59 @@ impl RustMethodDispatcher for PluginRpcDispatcher<'_> {
return dispatch_event_method(event, method_name);
}
- // The entity is cloned out so no table borrow is held while the handler runs (a
- // handler that re-enters register_*_entity would otherwise panic on the RefCell).
- let entity = R_TABLE.with(|table| table.borrow().get(&rhandle).cloned());
- if method_name == "__shirabeClone" {
- return match entity {
- Some(entity) => clone_entity(&entity),
- None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))),
- };
+ dispatch_r_table_method(rhandle, method_name, &args)
+ }
+}
+
+/// Serves a method call on an R-table entity, shared by every dispatcher: the child holds a
+/// stub for an entity registered by an earlier call, and the handle outlives the call that
+/// minted it.
+pub(crate) fn dispatch_r_table_method(
+ rhandle: u64,
+ method_name: &str,
+ args: &[PluginValue],
+) -> Result<PluginValue, PhpThrow> {
+ // The entity is cloned out so no table borrow is held while the handler runs (a
+ // handler that re-enters register_*_entity would otherwise panic on the RefCell).
+ let entity = R_TABLE.with(|table| table.borrow().get(&rhandle).cloned());
+ if method_name == "__shirabeClone" {
+ return match entity {
+ Some(entity) => clone_entity(&entity),
+ None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))),
+ };
+ }
+ if matches!(method_name, "__get" | "__set" | "__isset" | "__unset") {
+ return match entity {
+ Some(entity) => dispatch_property_access(&entity, method_name, args),
+ None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))),
+ };
+ }
+ match entity {
+ Some(RustEntity::Io(io)) => dispatch_io_method(&io, method_name, args),
+ Some(RustEntity::Composer(composer)) => dispatch_composer_method(&composer, method_name),
+ Some(RustEntity::Config(config)) => dispatch_config_method(&config, method_name, args),
+ Some(RustEntity::DownloadManager(dm)) => {
+ dispatch_download_manager_method(&dm, method_name, args)
}
- if matches!(method_name, "__get" | "__set" | "__isset" | "__unset") {
- return match entity {
- Some(entity) => dispatch_property_access(&entity, method_name, &args),
- None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))),
- };
+ Some(RustEntity::Filesystem(fs)) => dispatch_filesystem_method(&fs, method_name, args),
+ Some(RustEntity::InstallationManager(im)) => {
+ dispatch_installation_manager_method(&im, method_name, args)
}
- match entity {
- Some(RustEntity::Io(io)) => dispatch_io_method(&io, method_name, &args),
- Some(RustEntity::Composer(composer)) => {
- dispatch_composer_method(&composer, method_name)
- }
- Some(RustEntity::Config(config)) => dispatch_config_method(&config, method_name, &args),
- Some(RustEntity::DownloadManager(dm)) => {
- dispatch_download_manager_method(&dm, method_name, &args)
- }
- Some(RustEntity::Filesystem(fs)) => dispatch_filesystem_method(&fs, method_name, &args),
- Some(RustEntity::InstallationManager(im)) => {
- dispatch_installation_manager_method(&im, method_name, &args)
- }
- Some(RustEntity::RepositoryManager(rm)) => {
- dispatch_repository_manager_method(&rm, method_name)
- }
- Some(RustEntity::Repository(repository)) => {
- dispatch_repository_method(&repository, method_name, &args)
- }
- Some(RustEntity::Package(package)) => {
- dispatch_package_method(&package, method_name, &args)
- }
- Some(RustEntity::EventDispatcher(dispatcher)) => {
- dispatch_event_dispatcher_method(&dispatcher, method_name, &args)
- }
- Some(RustEntity::Operation(operation)) => {
- dispatch_operation_method(&operation, method_name, &args)
- }
- Some(RustEntity::Plugin(plugin)) => dispatch_plugin_method(&plugin, method_name),
- None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))),
+ Some(RustEntity::RepositoryManager(rm)) => {
+ dispatch_repository_manager_method(&rm, method_name)
+ }
+ Some(RustEntity::Repository(repository)) => {
+ dispatch_repository_method(&repository, method_name, args)
+ }
+ Some(RustEntity::Package(package)) => dispatch_package_method(&package, method_name, args),
+ Some(RustEntity::EventDispatcher(dispatcher)) => {
+ dispatch_event_dispatcher_method(&dispatcher, method_name, args)
+ }
+ Some(RustEntity::Operation(operation)) => {
+ dispatch_operation_method(&operation, method_name, args)
}
+ Some(RustEntity::Plugin(plugin)) => dispatch_plugin_method(&plugin, method_name),
+ None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))),
}
}
diff --git a/crates/shirabe/tests/plugin/e2e_script_event_test.rs b/crates/shirabe/tests/plugin/e2e_script_event_test.rs
new file mode 100644
index 00000000..5a0267d7
--- /dev/null
+++ b/crates/shirabe/tests/plugin/e2e_script_event_test.rs
@@ -0,0 +1,90 @@
+//! Script event E2E compatibility check: upstream Composer and Shirabe each run `dump-autoload`
+//! in a fixture project whose `post-autoload-dump` script is a PHP static method, and the method
+//! records what the `Composer\Script\Event` it receives exposes. Upstream has no test that reads
+//! the Composer object graph out of an event from a script, so the whole fixture is
+//! Shirabe-authored (`fixtures/e2e-script-event/`) and nothing has to be fetched; the test skips
+//! only while the PHP runtime or the Composer checkout is missing.
+
+use crate::e2e_extension_installer_test::{copy_dir, upstream_composer_bin};
+use crate::php_worker::{lock_php_worker, php_runtime_available};
+use std::path::{Path, PathBuf};
+use tempfile::TempDir;
+
+fn fixture_dir() -> PathBuf {
+ Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/plugin/fixtures/e2e-script-event")
+}
+
+struct Run {
+ exit_code: i32,
+ trace: String,
+ stdout: String,
+}
+
+/// Runs `dump-autoload` in a fresh copy of the fixture and returns the exit code with what the
+/// script wrote to its trace file and through the event's IO.
+fn dump_autoload(program: &str, prefix_args: &[&str]) -> Run {
+ let work = TempDir::new().unwrap();
+ copy_dir(&fixture_dir(), work.path());
+ let project = work.path().join("project");
+ let output = std::process::Command::new(program)
+ .args(prefix_args)
+ .arg("dump-autoload")
+ .current_dir(&project)
+ .env("COMPOSER_HOME", work.path().join("home"))
+ .env("COMPOSER_CACHE_DIR", work.path().join("cache"))
+ .env("COMPOSER_NO_INTERACTION", "1")
+ .env("COLUMNS", "120")
+ .env("LINES", "30")
+ .output()
+ .unwrap();
+ Run {
+ exit_code: output.status.code().unwrap_or(-1),
+ trace: std::fs::read_to_string(project.join("script-event-trace.txt")).unwrap_or_default(),
+ stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
+ }
+}
+
+#[test]
+fn test_script_event_object_graph_matches_upstream_composer() {
+ if !php_runtime_available() {
+ return;
+ }
+ let Some(composer_bin) = upstream_composer_bin() else {
+ return;
+ };
+ let _worker = lock_php_worker();
+ let composer_bin = composer_bin.to_str().unwrap().to_string();
+
+ let upstream = dump_autoload("php", &[composer_bin.as_str()]);
+ let shirabe = dump_autoload(env!("CARGO_BIN_EXE_shirabe"), &[]);
+
+ assert_eq!(0, upstream.exit_code, "upstream dump-autoload must succeed");
+ assert_eq!(upstream.exit_code, shirabe.exit_code);
+ assert_eq!(upstream.trace, shirabe.trace);
+
+ // Pinned as well as compared, so a run where neither side dispatches the script cannot pass.
+ assert_eq!(
+ "\
+event=post-autoload-dump
+dev=0
+root=shirabe/e2e-script-event
+vendor-dir=vendor
+bin-dir=bin
+",
+ upstream.trace
+ );
+
+ let io_line = |run: &Run| -> String {
+ run.stdout
+ .lines()
+ .find(|line| line.starts_with("script-event: "))
+ .unwrap_or_default()
+ .to_string()
+ };
+ assert_eq!(io_line(&upstream), io_line(&shirabe));
+ assert_eq!(
+ "script-event: event=post-autoload-dump dev=0 root=shirabe/e2e-script-event \
+ vendor-dir=vendor bin-dir=bin",
+ io_line(&upstream)
+ );
+}
diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-script-event/project/composer.json b/crates/shirabe/tests/plugin/fixtures/e2e-script-event/project/composer.json
new file mode 100644
index 00000000..59fc8f13
--- /dev/null
+++ b/crates/shirabe/tests/plugin/fixtures/e2e-script-event/project/composer.json
@@ -0,0 +1,17 @@
+{
+ "name": "shirabe/e2e-script-event",
+ "description": "E2E fixture project: record what a post-autoload-dump PHP script sees on its Event.",
+ "repositories": [
+ {
+ "packagist.org": false
+ }
+ ],
+ "autoload": {
+ "psr-4": {
+ "Shirabe\\E2e\\": "src/"
+ }
+ },
+ "scripts": {
+ "post-autoload-dump": "Shirabe\\E2e\\ScriptEventRecorder::postAutoloadDump"
+ }
+}
diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-script-event/project/src/ScriptEventRecorder.php b/crates/shirabe/tests/plugin/fixtures/e2e-script-event/project/src/ScriptEventRecorder.php
new file mode 100644
index 00000000..84fa0da9
--- /dev/null
+++ b/crates/shirabe/tests/plugin/fixtures/e2e-script-event/project/src/ScriptEventRecorder.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Shirabe\E2e;
+
+use Composer\Script\Event;
+
+class ScriptEventRecorder
+{
+ public static function postAutoloadDump(Event $event): void
+ {
+ $composer = $event->getComposer();
+ $lines = [
+ 'event=' . $event->getName(),
+ 'dev=' . ($event->isDevMode() ? '1' : '0'),
+ 'root=' . $composer->getPackage()->getName(),
+ 'vendor-dir=' . basename($composer->getConfig()->get('vendor-dir')),
+ 'bin-dir=' . basename($composer->getConfig()->get('bin-dir')),
+ ];
+ $event->getIO()->write('script-event: ' . implode(' ', $lines));
+ file_put_contents(__DIR__ . '/../script-event-trace.txt', implode("\n", $lines) . "\n");
+ }
+}
diff --git a/crates/shirabe/tests/plugin/main.rs b/crates/shirabe/tests/plugin/main.rs
index d8defcbc..000ca3a9 100644
--- a/crates/shirabe/tests/plugin/main.rs
+++ b/crates/shirabe/tests/plugin/main.rs
@@ -13,6 +13,7 @@ mod e2e_installers_test;
mod e2e_normalize_test;
mod e2e_package_event_test;
mod e2e_script_command_test;
+mod e2e_script_event_test;
mod plugin_installer_test;
mod subscriber_test;
mod value_round_trip_test;