From 548463bad1f72c97f68b47f54a263a4f4ad87b3a Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 15 Aug 2026 07:37:45 +0900 Subject: feat(php-rpc): embed the Composer PHP runtime in the executable Plugins and scripts need the real `Composer\` classes and the packages Composer depends on, which so far came from a checkout found through SHIRABE_COMPOSER_PHP_DIR or a path next to the workspace. Neither exists for a distributed binary. The build script now archives those PHP sources into a phar the way Compiler.php does and the executable carries it. The worker maps it with Phar::loadPhar and reads a content-addressed sentinel back to tell a bundle it can use from one it cannot; where its PHP cannot open the phar, the bundle is unpacked once into the cache directory and autoloaded from there. SHIRABE_COMPOSER_PHP_DIR still overrides both for development. PHP locates a phar's manifest by the first __HALT_COMPILER(); token in the file, so the executable must hold no other copy of it: phar.rs builds the token at run time, and a linter keeps further literals out of the sources that reach the binary. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/event_dispatcher/event_dispatcher.rs | 36 ++++++++++------------ crates/shirabe/tests/common/php_worker.rs | 4 +-- crates/shirabe/tests/installed_versions_test.rs | 9 +++++- 3 files changed, 26 insertions(+), 23 deletions(-) (limited to 'crates/shirabe') diff --git a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs index 368e2227..8f0b54be 100644 --- a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs +++ b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs @@ -1566,16 +1566,7 @@ try {{ /// Loads the Composer PHP runtime (symfony/console and friends) into the worker, needed /// before a `scripts` Command class can be autoloaded and hosted. pub(crate) fn ensure_composer_php_runtime() -> anyhow::Result<()> { - // TODO(plugin): the real PHP classes are taken from a Composer checkout for now; how - // they ship with a released Shirabe binary is part of the plugin distribution work. - let autoload = Self::composer_php_runtime_autoload().ok_or_else(|| -> anyhow::Error { - RuntimeException::new( - "unable to locate the Composer PHP runtime; set SHIRABE_COMPOSER_PHP_DIR \ - to a Composer checkout with its vendor directory installed" - .to_string(), - ) - .into() - })?; + let autoload = Self::composer_php_runtime_autoload()?; unwrap_php_result(call_function( "__shirabe_require", vec![PluginValue::string(autoload)], @@ -1590,22 +1581,27 @@ try {{ Self::ensure_composer_php_runtime() } - fn composer_php_runtime_autoload() -> Option { + /// The `vendor/autoload.php` of the Composer PHP runtime: the checkout `SHIRABE_COMPOSER_PHP_DIR` + /// points at, or else the runtime bundle the executable carries. + fn composer_php_runtime_autoload() -> anyhow::Result { if let Some(dir) = Platform::get_env("SHIRABE_COMPOSER_PHP_DIR") { let path = std::path::Path::new(&dir) .join("vendor") .join("autoload.php"); - if path.is_file() { - return path.to_str().map(|s| s.to_string()); + if !path.is_file() { + return Err(RuntimeException::new(format!( + "SHIRABE_COMPOSER_PHP_DIR points at {dir}, which has no \ + vendor/autoload.php; install the checkout's dependencies or unset it to use \ + the runtime the executable carries" + )) + .into()); } + return Ok(path.display().to_string()); } - // Development fallback: the Composer checkout sitting next to this workspace. - let dev = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../composer/vendor/autoload.php"); - if dev.is_file() { - return dev.canonicalize().ok()?.to_str().map(|s| s.to_string()); - } - None + Ok(format!( + "{}/vendor/autoload.php", + shirabe_php_rpc::composer_runtime::base_path()? + )) } /// Runs a boolean runtime query (`class_exists`, `is_a`, ...) inside the PHP worker, with diff --git a/crates/shirabe/tests/common/php_worker.rs b/crates/shirabe/tests/common/php_worker.rs index 4be064e4..af1b6b44 100644 --- a/crates/shirabe/tests/common/php_worker.rs +++ b/crates/shirabe/tests/common/php_worker.rs @@ -24,8 +24,8 @@ pub fn lock_php_worker() -> std::sync::MutexGuard<'static, ()> { .unwrap_or_else(|poisoned| poisoned.into_inner()) } -/// Requires the Composer PHP runtime (`composer/vendor/autoload.php`) into the worker, which is -/// what makes the real `Composer\` classes autoloadable there. +/// Requires the Composer PHP runtime's `vendor/autoload.php` into the worker, which is what makes +/// the real `Composer\` classes autoloadable there. pub fn load_composer_php_runtime() { shirabe::event_dispatcher::EventDispatcher::__ensure_composer_php_runtime().unwrap(); } diff --git a/crates/shirabe/tests/installed_versions_test.rs b/crates/shirabe/tests/installed_versions_test.rs index 9b81c3c8..b8c65485 100644 --- a/crates/shirabe/tests/installed_versions_test.rs +++ b/crates/shirabe/tests/installed_versions_test.rs @@ -512,12 +512,19 @@ fn test_worker_loads_the_installed_versions_file_shirabe_dumps() { let _worker = lock_php_worker(); load_composer_php_runtime(); + // Read in the worker rather than from Rust: the file it autoloads lives inside the runtime + // bundle, which only the PHP side has a stream wrapper for. let loaded = string_of(&php_eval( r"return (new \ReflectionClass(\Composer\InstalledVersions::class))->getFileName();", )); + let contents = string_of(&php_eval( + r"return file_get_contents( + (new \ReflectionClass(\Composer\InstalledVersions::class))->getFileName() + );", + )); assert_eq!( include_str!("../../../composer/src/Composer/InstalledVersions.php"), - std::fs::read_to_string(&loaded).unwrap(), + contents, "the worker autoloads {loaded}, which must match the file Shirabe dumps", ); } -- cgit v1.3.1-4-g156e