1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
|
//! The Composer PHP runtime the worker loads.
//!
//! See `docs/dev/composer-runtime-bundle.md`.
use crate::PluginValue;
use crate::call_function;
const BUNDLE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/composer-runtime-bundle.phar"));
/// Identifies the bundle by its contents; the sentinel entry holds the same string.
const BUNDLE_ID: &str = env!("SHIRABE_COMPOSER_RUNTIME_BUNDLE_ID");
/// The entry the worker reads back to tell a bundle it can use from one it cannot.
const SENTINEL_PATH: &str = "shirabe/bundle-id";
/// The alias `Phar::loadPhar` maps the bundle to in the worker. The bundle stores no alias of
/// its own, so this name is the only one its stream paths answer to.
const ALIAS: &str = "shirabe-composer-runtime.phar";
/// The path the Composer PHP runtime's files sit under in the worker, either inside this
/// executable or in the directory the bundle was extracted to.
pub fn base_path() -> anyhow::Result<String> {
static BASE: std::sync::OnceLock<Result<String, String>> = std::sync::OnceLock::new();
BASE.get_or_init(|| resolve().map_err(|e| format!("{e:#}")))
.clone()
.map_err(|e| anyhow::anyhow!(e))
}
fn resolve() -> anyhow::Result<String> {
if worker_opens_bundle()? {
return Ok(format!("phar://{ALIAS}"));
}
let directory = extract()?;
directory.into_os_string().into_string().map_err(|path| {
anyhow::anyhow!("the extracted Composer PHP runtime path {path:?} is not valid UTF-8")
})
}
/// Whether the worker can read the bundle straight out of this executable. It cannot when its
/// PHP has no phar extension, no zlib to inflate the entries, or a restriction on the phar stream
/// wrapper.
fn worker_opens_bundle() -> anyhow::Result<bool> {
let executable = std::env::current_exe()?;
let executable = executable.to_str().ok_or_else(|| {
anyhow::anyhow!("the path of this executable, {executable:?}, is not valid UTF-8")
})?;
let opened = call_function(
"__shirabe_open_runtime_bundle",
vec![
PluginValue::string(executable),
PluginValue::string(ALIAS),
PluginValue::string(SENTINEL_PATH),
PluginValue::string(BUNDLE_ID),
],
)?
.map_err(|throw| anyhow::anyhow!("{throw}"))?;
match opened {
PluginValue::Bool(opened) => Ok(opened),
other => Err(anyhow::anyhow!(
"opening the Composer PHP runtime bundle did not answer with a bool: {other:?}"
)),
}
}
/// Unpacks the bundle into a content-addressed directory, so that a worker that cannot read the
/// bundle in place gets the same files from the filesystem.
fn extract() -> anyhow::Result<std::path::PathBuf> {
extract_into(&cache_directory()?.join("shirabe").join("runtime"))
}
fn extract_into(root: &std::path::Path) -> anyhow::Result<std::path::PathBuf> {
let destination = root.join(BUNDLE_ID);
if destination.is_dir() {
return Ok(destination);
}
std::fs::create_dir_all(root)?;
let staging = tempfile::tempdir_in(root)?;
let archive = staging.path().join("bundle.phar");
std::fs::write(&archive, BUNDLE)?;
let unpacked = staging.path().join("unpacked");
shirabe_php_shim::Phar::new(&archive)?.extract_to(&unpacked, None, true)?;
std::fs::remove_file(&archive)?;
if let Err(error) = std::fs::rename(&unpacked, &destination) {
// Losing the race against another process that unpacked the same bundle is not a
// failure: the directory is named after the contents that went into it.
if !destination.is_dir() {
return Err(error.into());
}
}
Ok(destination)
}
fn cache_directory() -> anyhow::Result<std::path::PathBuf> {
if let Some(directory) = std::env::var_os("XDG_CACHE_HOME")
&& !directory.is_empty()
{
return Ok(std::path::PathBuf::from(directory));
}
let home = std::env::var_os("HOME")
.ok_or_else(|| anyhow::anyhow!("neither XDG_CACHE_HOME nor HOME is set"))?;
Ok(std::path::Path::new(&home).join(".cache"))
}
#[cfg(test)]
mod tests {
use super::*;
use shirabe_symfony_process::PhpExecutableFinder;
fn sentinel_of(directory: &std::path::Path) -> String {
std::fs::read_to_string(directory.join(SENTINEL_PATH)).expect("no sentinel in the bundle")
}
/// PHP takes the first occurrence of the stub token in the file it opens, so no other copy of
/// it may precede the bundle in the executable.
#[test]
fn the_first_phar_in_this_executable_is_the_bundle() {
let executable = std::env::current_exe().expect("no path for this executable");
let unpacked = tempfile::tempdir().unwrap();
shirabe_php_shim::Phar::new(&executable)
.expect("this executable does not read back as a phar")
.extract_to(unpacked.path(), None, true)
.unwrap();
assert_eq!(sentinel_of(unpacked.path()), BUNDLE_ID);
}
#[test]
fn extracting_the_bundle_names_the_directory_after_it() {
let root = tempfile::tempdir().unwrap();
let directory = extract_into(root.path()).unwrap();
assert_eq!(directory, root.path().join(BUNDLE_ID));
assert_eq!(sentinel_of(&directory), BUNDLE_ID);
assert!(directory.join("vendor/autoload.php").is_file());
// A second call finds the unpacked bundle and leaves it alone.
assert_eq!(extract_into(root.path()).unwrap(), directory);
}
#[test]
fn the_worker_reads_the_bundle_out_of_this_executable() {
if PhpExecutableFinder::new().find(false).is_none() {
// No PHP in this environment; the worker cannot start.
return;
}
assert_eq!(base_path().unwrap(), format!("phar://{ALIAS}"));
}
/// The other half of `base_path`: a worker whose PHP cannot open the bundle is handed the
/// unpacked tree, and has to reach the same classes through it.
#[test]
fn the_worker_loads_the_composer_runtime_from_an_unpacked_bundle() {
if PhpExecutableFinder::new().find(false).is_none() {
// No PHP in this environment; the worker cannot start.
return;
}
let root = tempfile::tempdir().unwrap();
let autoload = extract_into(root.path())
.unwrap()
.join("vendor/autoload.php");
call_function(
"__shirabe_require",
vec![PluginValue::string(autoload.to_str().unwrap())],
)
.unwrap()
.unwrap();
// A class with no proxy stub, so that the answer is about the runtime and not about the
// stub autoloader.
let exists = call_function(
"class_exists",
vec![PluginValue::string(r"Composer\Util\Filesystem")],
)
.unwrap()
.unwrap();
assert_eq!(exists, PluginValue::Bool(true));
}
}
|