aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-15 07:37:45 +0900
committernsfisis <nsfisis@gmail.com>2026-08-15 07:37:45 +0900
commit548463bad1f72c97f68b47f54a263a4f4ad87b3a (patch)
tree1285d740611be3ad175f9e57c61d880b500dd818 /crates/shirabe
parented8694f89eb7702eb7e617af29f8f444f32b8d3c (diff)
downloadphp-shirabe-548463bad1f72c97f68b47f54a263a4f4ad87b3a.tar.gz
php-shirabe-548463bad1f72c97f68b47f54a263a4f4ad87b3a.tar.zst
php-shirabe-548463bad1f72c97f68b47f54a263a4f4ad87b3a.zip
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) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe')
-rw-r--r--crates/shirabe/src/event_dispatcher/event_dispatcher.rs36
-rw-r--r--crates/shirabe/tests/common/php_worker.rs4
-rw-r--r--crates/shirabe/tests/installed_versions_test.rs9
3 files changed, 26 insertions, 23 deletions
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<String> {
+ /// 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<String> {
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",
);
}