aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-25 03:20:08 +0900
committernsfisis <nsfisis@gmail.com>2026-06-25 03:33:09 +0900
commitc65cab710bb3d79db20862c9361d4dbbac8ac5d7 (patch)
treeb4d192237bbb8dbaa80166f1ffa24703e57243f4
parentf73e9ab05f8d94937d2437919199d9d9681c8cde (diff)
downloadphp-shirabe-c65cab710bb3d79db20862c9361d4dbbac8ac5d7.tar.gz
php-shirabe-c65cab710bb3d79db20862c9361d4dbbac8ac5d7.tar.zst
php-shirabe-c65cab710bb3d79db20862c9361d4dbbac8ac5d7.zip
feat(php-shim): model $_ENV/$_SERVER as OsString snapshots
Rework the environment shim around getenv/putenv on the real environment and $_ENV/$_SERVER as startup snapshots, all over OsString. Migrate every caller off the old server()/server_argv() helpers and force the snapshots in main() before any putenv() runs. Document the porting rules in docs/dev/env-vars-porting.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/color.rs4
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/command/command.rs8
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/command/complete_command.rs25
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs27
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style.rs10
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/output/console_output.rs5
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/output/stream_output.rs19
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs3
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/terminal.rs8
-rw-r--r--crates/shirabe-external-packages/src/symfony/process/process.rs25
-rw-r--r--crates/shirabe-php-shim/src/env.rs144
-rw-r--r--crates/shirabe/src/autoload/autoload_generator.rs7
-rw-r--r--crates/shirabe/src/command/init_command.rs52
-rw-r--r--crates/shirabe/src/console/application.rs28
-rw-r--r--crates/shirabe/src/main.rs7
-rw-r--r--crates/shirabe/src/util/platform.rs32
-rw-r--r--crates/shirabe/tests/command/init_command_test.rs14
-rw-r--r--crates/shirabe/tests/config_test.rs6
-rw-r--r--crates/shirabe/tests/util/ini_helper_test.rs10
-rw-r--r--docs/dev/env-vars-porting.md44
20 files changed, 327 insertions, 151 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/color.rs b/crates/shirabe-external-packages/src/symfony/console/color.rs
index 3d5a7c4..fb7cc9f 100644
--- a/crates/shirabe-external-packages/src/symfony/console/color.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/color.rs
@@ -196,7 +196,9 @@ impl Color {
let b = color & 255;
// see https://github.com/termstandard/colors/ for more information about true color support
- if shirabe_php_shim::getenv("COLORTERM").as_deref() != Some("truecolor") {
+ if shirabe_php_shim::getenv("COLORTERM").as_deref()
+ != Some(std::ffi::OsStr::new("truecolor"))
+ {
return Self::degrade_hex_color_to_ansi(r, g, b).to_string();
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/command/command.rs b/crates/shirabe-external-packages/src/symfony/console/command/command.rs
index 840f11a..89f4ae2 100644
--- a/crates/shirabe-external-packages/src/symfony/console/command/command.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/command/command.rs
@@ -731,7 +731,13 @@ impl Command for CommandData {
"%command.name%".to_string(),
"%command.full_name%".to_string(),
];
- let php_self = shirabe_php_shim::server("PHP_SELF");
+ let php_self = shirabe_php_shim::PHP_SERVER
+ .lock()
+ .unwrap()
+ .php_self()
+ .unwrap_or_default()
+ .to_string_lossy()
+ .into_owned();
let replacements = [
name.clone().unwrap_or_default(),
if is_single_command {
diff --git a/crates/shirabe-external-packages/src/symfony/console/command/complete_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/complete_command.rs
index cc700dc..da59a75 100644
--- a/crates/shirabe-external-packages/src/symfony/console/command/complete_command.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/command/complete_command.rs
@@ -125,7 +125,15 @@ impl CompleteCommand {
return;
}
- let command_name = shirabe_php_shim::basename(&shirabe_php_shim::server_argv()[0]);
+ let command_name = shirabe_php_shim::basename(
+ &shirabe_php_shim::PHP_SERVER
+ .lock()
+ .unwrap()
+ .argv()
+ .next()
+ .unwrap_or_default()
+ .to_string_lossy(),
+ );
shirabe_php_shim::file_put_contents3(
&format!(
"{}/sf_{}.log",
@@ -206,7 +214,9 @@ impl Command for CompleteCommand {
) -> anyhow::Result<()> {
let _ = (input, output);
self.is_debug.set(shirabe_php_shim::filter_var_boolean(
- &shirabe_php_shim::getenv("SYMFONY_COMPLETION_DEBUG").unwrap_or_default(),
+ &shirabe_php_shim::getenv("SYMFONY_COMPLETION_DEBUG")
+ .unwrap_or_default()
+ .to_string_lossy(),
));
Ok(())
@@ -268,7 +278,16 @@ impl Command for CompleteCommand {
"<info>Input:</> <comment>(\"|\" indicates the cursor position)</>".to_string(),
format!(" {}", completion_input.to_string()),
"<info>Command:</>".to_string(),
- format!(" {}", shirabe_php_shim::server_argv().join(" ")),
+ format!(
+ " {}",
+ shirabe_php_shim::PHP_SERVER
+ .lock()
+ .unwrap()
+ .argv()
+ .map(|a| a.to_string_lossy().into_owned())
+ .collect::<Vec<_>>()
+ .join(" ")
+ ),
"<info>Messages:</>".to_string(),
]);
diff --git a/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs
index 306bd59..588b868 100644
--- a/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs
@@ -73,7 +73,14 @@ impl DumpCompletionCommand {
}
fn guess_shell() -> String {
- shirabe_php_shim::basename(&shirabe_php_shim::server_shell().unwrap_or_default())
+ shirabe_php_shim::basename(
+ &shirabe_php_shim::PHP_SERVER
+ .lock()
+ .unwrap()
+ .get("SHELL")
+ .unwrap_or_default()
+ .to_string_lossy(),
+ )
}
fn tail_debug_log(&self, command_name: &str, _output: &dyn OutputInterface) {
@@ -112,7 +119,13 @@ impl DumpCompletionCommand {
impl Command for DumpCompletionCommand {
fn configure(&self) -> anyhow::Result<()> {
- let full_command = shirabe_php_shim::server_php_self();
+ let full_command = shirabe_php_shim::PHP_SERVER
+ .lock()
+ .unwrap()
+ .php_self()
+ .unwrap_or_default()
+ .to_string_lossy()
+ .into_owned();
let command_name = shirabe_php_shim::basename(&full_command);
// @realpath($fullCommand) ?: $fullCommand
let full_command = match shirabe_php_shim::realpath(&full_command) {
@@ -170,7 +183,15 @@ impl Command for DumpCompletionCommand {
input: Rc<RefCell<dyn InputInterface>>,
output: Rc<RefCell<dyn OutputInterface>>,
) -> anyhow::Result<i64> {
- let command_name = shirabe_php_shim::basename(&shirabe_php_shim::server_argv()[0]);
+ let command_name = shirabe_php_shim::basename(
+ &shirabe_php_shim::PHP_SERVER
+ .lock()
+ .unwrap()
+ .argv()
+ .next()
+ .unwrap_or_default()
+ .to_string_lossy(),
+ );
if input.borrow().get_option("debug")?.to_bool() {
self.tail_debug_log(&command_name, &*output.borrow());
diff --git a/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style.rs b/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style.rs
index 39d694d..c421edf 100644
--- a/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style.rs
@@ -77,13 +77,17 @@ impl OutputFormatterStyleInterface for OutputFormatterStyle {
if self.handles_href_gracefully.is_none() {
self.handles_href_gracefully = Some(
shirabe_php_shim::getenv("TERMINAL_EMULATOR").as_deref()
- != Some("JetBrains-JediTerm")
+ != Some(std::ffi::OsStr::new("JetBrains-JediTerm"))
&& (shirabe_php_shim::getenv("KONSOLE_VERSION").is_none_or(|v| v.is_empty())
|| shirabe_php_shim::getenv("KONSOLE_VERSION")
- .map(|v| v.parse::<i64>().unwrap_or(0))
+ .map(|v| v.to_string_lossy().parse::<i64>().unwrap_or(0))
.unwrap_or(0)
> 201100)
- && !shirabe_php_shim::server_contains_key("IDEA_INITIAL_DIRECTORY"),
+ && shirabe_php_shim::PHP_SERVER
+ .lock()
+ .unwrap()
+ .get("IDEA_INITIAL_DIRECTORY")
+ .is_none(),
);
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/output/console_output.rs b/crates/shirabe-external-packages/src/symfony/console/output/console_output.rs
index dd6840d..254ec2c 100644
--- a/crates/shirabe-external-packages/src/symfony/console/output/console_output.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/output/console_output.rs
@@ -103,7 +103,10 @@ impl ConsoleOutput {
} else {
String::new()
},
- shirabe_php_shim::getenv("OSTYPE").unwrap_or_default(),
+ shirabe_php_shim::getenv("OSTYPE")
+ .unwrap_or_default()
+ .to_string_lossy()
+ .into_owned(),
shirabe_php_shim::PHP_OS.to_string(),
];
diff --git a/crates/shirabe-external-packages/src/symfony/console/output/stream_output.rs b/crates/shirabe-external-packages/src/symfony/console/output/stream_output.rs
index f517cbe..ac85f3a 100644
--- a/crates/shirabe-external-packages/src/symfony/console/output/stream_output.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/output/stream_output.rs
@@ -91,7 +91,9 @@ impl StreamOutput {
if !shirabe_php_shim::stream_isatty_resource(stream)
&& !["MINGW32", "MINGW64"].contains(
&shirabe_php_shim::strtoupper(
- &shirabe_php_shim::getenv("MSYSTEM").unwrap_or_default(),
+ &shirabe_php_shim::getenv("MSYSTEM")
+ .unwrap_or_default()
+ .to_string_lossy(),
)
.as_str(),
)
@@ -105,15 +107,19 @@ impl StreamOutput {
return true;
}
- if Some("Hyper".to_string()) == shirabe_php_shim::getenv("TERM_PROGRAM")
+ if shirabe_php_shim::getenv("TERM_PROGRAM").as_deref()
+ == Some(std::ffi::OsStr::new("Hyper"))
|| shirabe_php_shim::getenv("COLORTERM").is_some()
|| shirabe_php_shim::getenv("ANSICON").is_some()
- || Some("ON".to_string()) == shirabe_php_shim::getenv("ConEmuANSI")
+ || shirabe_php_shim::getenv("ConEmuANSI").as_deref() == Some(std::ffi::OsStr::new("ON"))
{
return true;
}
- let term = shirabe_php_shim::getenv("TERM").unwrap_or_default();
+ let term = shirabe_php_shim::getenv("TERM")
+ .unwrap_or_default()
+ .to_string_lossy()
+ .into_owned();
if "dumb" == term {
return false;
}
@@ -130,7 +136,10 @@ impl StreamOutput {
/// PHP: `(($_SERVER['NO_COLOR'] ?? getenv('NO_COLOR'))[0] ?? '')`.
fn no_color_first_char() -> String {
- let value = shirabe_php_shim::getenv("NO_COLOR").unwrap_or_default();
+ let value = shirabe_php_shim::getenv("NO_COLOR")
+ .unwrap_or_default()
+ .to_string_lossy()
+ .into_owned();
value
.chars()
.next()
diff --git a/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs b/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs
index 92e80b2..d7b2ef6 100644
--- a/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs
@@ -277,7 +277,8 @@ impl SymfonyStyle {
let mut progress_bar = self.inner.create_progress_bar(max);
if std::path::MAIN_SEPARATOR != '\\'
- || shirabe_php_shim::getenv("TERM_PROGRAM").as_deref() == Some("Hyper")
+ || shirabe_php_shim::getenv("TERM_PROGRAM").as_deref()
+ == Some(std::ffi::OsStr::new("Hyper"))
{
progress_bar.set_empty_bar_character("░"); // light shade character ░
progress_bar.set_progress_character("");
diff --git a/crates/shirabe-external-packages/src/symfony/console/terminal.rs b/crates/shirabe-external-packages/src/symfony/console/terminal.rs
index c02b87f..cc8f812 100644
--- a/crates/shirabe-external-packages/src/symfony/console/terminal.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/terminal.rs
@@ -28,7 +28,8 @@ impl Terminal {
let width = shirabe_php_shim::getenv("COLUMNS");
if let Some(width) = width {
return shirabe_php_shim::intval(&PhpMixed::String(shirabe_php_shim::trim(
- &width, None,
+ &width.to_string_lossy(),
+ None,
)));
}
@@ -44,7 +45,8 @@ impl Terminal {
let height = shirabe_php_shim::getenv("LINES");
if let Some(height) = height {
return shirabe_php_shim::intval(&PhpMixed::String(shirabe_php_shim::trim(
- &height, None,
+ &height.to_string_lossy(),
+ None,
)));
}
@@ -85,7 +87,7 @@ impl Terminal {
if let Some(ansicon) = &ansicon
&& shirabe_php_shim::preg_match(
"/^(\\d+)x(\\d+)(?: \\((\\d+)x(\\d+)\\))?$/",
- &shirabe_php_shim::trim(ansicon, None),
+ &shirabe_php_shim::trim(&ansicon.to_string_lossy(), None),
&mut matches,
)
{
diff --git a/crates/shirabe-external-packages/src/symfony/process/process.rs b/crates/shirabe-external-packages/src/symfony/process/process.rs
index caf8aa1..3550f09 100644
--- a/crates/shirabe-external-packages/src/symfony/process/process.rs
+++ b/crates/shirabe-external-packages/src/symfony/process/process.rs
@@ -1670,13 +1670,20 @@ impl Process {
}
fn get_default_env(&self) -> IndexMap<String, PhpMixed> {
- let env = php::getenv_all();
- let server = php::php_server();
+ let env: IndexMap<String, String> = php::getenv_all()
+ .map(|(k, v)| {
+ (
+ k.to_string_lossy().into_owned(),
+ v.to_string_lossy().into_owned(),
+ )
+ })
+ .collect();
+ let server = php::PHP_SERVER.lock().unwrap();
// non-Windows: array_intersect_key($env, $_SERVER) ?: $env
let mut intersect: IndexMap<String, PhpMixed> = IndexMap::new();
for (k, v) in &env {
- if server.contains_key(k) {
+ if server.get(k).is_some() {
intersect.insert(k.clone(), PhpMixed::String(v.clone()));
}
}
@@ -1689,7 +1696,17 @@ impl Process {
};
// $_ENV + env_map
- let mut result = php::php_env();
+ let mut result: IndexMap<String, PhpMixed> = php::PHP_ENV
+ .lock()
+ .unwrap()
+ .get_all()
+ .map(|(k, v)| {
+ (
+ k.to_string_lossy().into_owned(),
+ PhpMixed::String(v.to_string_lossy().into_owned()),
+ )
+ })
+ .collect();
for (k, v) in env_map {
result.entry(k).or_insert(v);
}
diff --git a/crates/shirabe-php-shim/src/env.rs b/crates/shirabe-php-shim/src/env.rs
index 62dc666..cf0c659 100644
--- a/crates/shirabe-php-shim/src/env.rs
+++ b/crates/shirabe-php-shim/src/env.rs
@@ -1,100 +1,90 @@
-use crate::PhpMixed;
-use indexmap::IndexMap;
+//! Porting rules for PHP environment variables: see `docs/dev/env-vars-porting.md`.
-pub fn getenv(_name: &str) -> Option<String> {
- std::env::var(_name).ok()
+pub fn getenv_all() -> std::env::VarsOs {
+ std::env::vars_os()
}
-// TODO(phase-c): only the simple `^(\w+)(=(.+))?$` form is supported.
-pub fn putenv(setting: &str) -> bool {
- let is_word =
- |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_');
- // A setting without `=` deletes the variable, mirroring PHP's putenv('NAME').
- match setting.split_once('=') {
- Some((name, value)) => {
- if !is_word(name) {
- panic!("putenv: unsupported setting format: {setting:?}");
- }
- unsafe {
- std::env::set_var(name, value);
- }
- }
- None => {
- if !is_word(setting) {
- panic!("putenv: unsupported setting format: {setting:?}");
- }
- unsafe {
- std::env::remove_var(setting);
- }
- }
- }
- true
+pub fn getenv<K: AsRef<std::ffi::OsStr>>(key: K) -> Option<std::ffi::OsString> {
+ std::env::var_os(key)
}
-/// PHP superglobal $_SERVER access. In the CLI SAPI $_SERVER is populated from
-/// the environment, which is the only source modeled here.
-pub fn server_get(name: &str) -> Option<String> {
- // TODO: is var_os() better?
- std::env::var(name).ok()
+pub unsafe fn putenv<K: AsRef<std::ffi::OsStr>, V: AsRef<std::ffi::OsStr>>(key: K, value: V) {
+ // TODO: validate key and value format to avoid panic?
+ unsafe { std::env::set_var(key, value) }
}
-// TODO(php-runtime): modify the real PHP's $_SERVER.
-pub fn server_set(_name: &str, _value: String) {}
-
-// TODO(php-runtime): modify the real PHP's $_SERVER.
-pub fn server_unset(_name: &str) {}
-
-pub fn server_contains_key(name: &str) -> bool {
- std::env::var_os(name).is_some()
+pub unsafe fn putenv_clear<K: AsRef<std::ffi::OsStr>>(key: K) {
+ // TODO: validate key and value format to avoid panic?
+ unsafe { std::env::remove_var(key) }
}
-/// PHP superglobal $_ENV access.
-pub fn env_get(name: &str) -> Option<String> {
- // TODO: is var_os() better?
- std::env::var(name).ok()
+pub struct Superglobal {
+ vars: indexmap::IndexMap<std::ffi::OsString, std::ffi::OsString>,
}
-// TODO(php-runtime): modify the real PHP's $_ENV.
-pub fn env_set(_name: &str, _value: String) {}
+pub struct SuperglobalServer(Superglobal);
-// TODO(php-runtime): modify the real PHP's $_ENV.
-pub fn env_unset(_name: &str) {}
+impl Superglobal {
+ fn from_env_vars() -> Self {
+ let vars = std::env::vars_os().collect();
+ Self { vars }
+ }
-pub fn env_contains_key(name: &str) -> bool {
- std::env::var_os(name).is_some()
-}
+ pub fn get_all(&self) -> impl Iterator<Item = (std::ffi::OsString, std::ffi::OsString)> + '_ {
+ self.vars.iter().map(|(k, v)| (k.clone(), v.clone()))
+ }
-/// PHP `getenv()` with no argument: all environment variables.
-pub fn getenv_all() -> IndexMap<String, String> {
- todo!()
-}
+ pub fn get<K: AsRef<std::ffi::OsStr>>(&self, key: K) -> Option<std::ffi::OsString> {
+ self.vars.get(key.as_ref()).cloned()
+ }
-/// PHP superglobal `$_ENV`.
-pub fn php_env() -> IndexMap<String, PhpMixed> {
- todo!()
-}
+ pub fn put(&mut self, key: std::ffi::OsString, value: std::ffi::OsString) {
+ self.vars.insert(key, value);
+ }
-/// PHP superglobal `$_SERVER`.
-pub fn php_server() -> IndexMap<String, PhpMixed> {
- todo!()
+ pub fn clear<K: AsRef<std::ffi::OsStr>>(&mut self, key: K) {
+ self.vars.shift_remove(key.as_ref());
+ }
}
-pub fn server(key: &str) -> String {
- if key == "PHP_SELF" {
- return server_php_self();
+impl SuperglobalServer {
+ fn from_env_vars() -> Self {
+ Self(Superglobal::from_env_vars())
}
- server_get(key).unwrap_or_default()
-}
-pub fn server_argv() -> Vec<String> {
- std::env::args().collect()
-}
+ pub fn get_all(&self) -> impl Iterator<Item = (std::ffi::OsString, std::ffi::OsString)> + '_ {
+ self.0.get_all()
+ }
-pub fn server_php_self() -> String {
- // CLI SAPI: $_SERVER['PHP_SELF'] is the path of the executed script, i.e. argv[0].
- std::env::args().next().unwrap_or_default()
-}
+ pub fn get<K: AsRef<std::ffi::OsStr>>(&self, key: K) -> Option<std::ffi::OsString> {
+ self.0.get(key)
+ }
+
+ pub fn put(&mut self, key: std::ffi::OsString, value: std::ffi::OsString) {
+ self.0.put(key, value)
+ }
+
+ pub fn clear<K: AsRef<std::ffi::OsStr>>(&mut self, key: K) {
+ self.0.clear(key)
+ }
+
+ pub fn argv(&self) -> std::env::ArgsOs {
+ std::env::args_os()
+ }
-pub fn server_shell() -> Option<String> {
- todo!()
+ pub fn php_self(&self) -> Option<std::ffi::OsString> {
+ self.argv().next()
+ }
}
+
+/// PHP superglobal $_SERVER. $_SERVER is a snapshot at startup. Modifying it does not affect the
+/// real environment variables, while putenv() does.
+/// TODO(php-runtime): modify the real PHP's $_SERVER.
+pub static PHP_SERVER: std::sync::LazyLock<std::sync::Mutex<SuperglobalServer>> =
+ std::sync::LazyLock::new(|| std::sync::Mutex::new(SuperglobalServer::from_env_vars()));
+
+/// PHP superglobal $_ENV. $_ENV is a snapshot at startup. Modifying it does not affect the real
+/// environment variables, while putenv() does.
+/// TODO(php-runtime): modify the real PHP's $_ENV.
+pub static PHP_ENV: std::sync::LazyLock<std::sync::Mutex<Superglobal>> =
+ std::sync::LazyLock::new(|| std::sync::Mutex::new(Superglobal::from_env_vars()));
diff --git a/crates/shirabe/src/autoload/autoload_generator.rs b/crates/shirabe/src/autoload/autoload_generator.rs
index 7b02841..ded7cee 100644
--- a/crates/shirabe/src/autoload/autoload_generator.rs
+++ b/crates/shirabe/src/autoload/autoload_generator.rs
@@ -148,7 +148,12 @@ impl AutoloadGenerator {
if self.run_scripts {
// set COMPOSER_DEV_MODE in case not set yet so it is available in the dump-autoload event listeners
- if shirabe_php_shim::server_get("COMPOSER_DEV_MODE").is_none() {
+ if shirabe_php_shim::PHP_SERVER
+ .lock()
+ .unwrap()
+ .get("COMPOSER_DEV_MODE")
+ .is_none()
+ {
Platform::put_env(
"COMPOSER_DEV_MODE",
if self.dev_mode.unwrap_or(false) {
diff --git a/crates/shirabe/src/command/init_command.rs b/crates/shirabe/src/command/init_command.rs
index 9683b89..557ebca 100644
--- a/crates/shirabe/src/command/init_command.rs
+++ b/crates/shirabe/src/command/init_command.rs
@@ -10,11 +10,11 @@ use shirabe_external_packages::symfony::console::input::ArrayInput;
use shirabe_external_packages::symfony::console::input::InputInterface;
use shirabe_external_packages::symfony::console::output::OutputInterface;
use shirabe_php_shim::{
- FILE_IGNORE_NEW_LINES, InvalidArgumentException, PHP_EOL, PhpMixed, array_filter, array_flip,
- array_flip_strings, array_intersect_key, array_keys, array_map, basename, empty, explode, file,
- file_exists, file_get_contents, file_put_contents, function_exists, get_current_user, implode,
- is_dir, is_string, preg_quote, realpath, server_get, sprintf, str_replace, strpos, strtolower,
- trim, ucwords,
+ FILE_IGNORE_NEW_LINES, InvalidArgumentException, PHP_EOL, PHP_SERVER, PhpMixed, array_filter,
+ array_flip, array_flip_strings, array_intersect_key, array_keys, array_map, basename, empty,
+ explode, file, file_exists, file_get_contents, file_put_contents, function_exists,
+ get_current_user, implode, is_dir, is_string, preg_quote, realpath, sprintf, str_replace,
+ strpos, strtolower, trim, ucwords,
};
use shirabe_spdx_licenses::SpdxLicenses;
use std::cell::RefCell;
@@ -723,7 +723,11 @@ impl Command for InitCommand {
.as_string()
.map(|s| s.to_string());
if license.is_none() {
- let default_license = server_get("COMPOSER_DEFAULT_LICENSE");
+ let default_license = PHP_SERVER
+ .lock()
+ .unwrap()
+ .get("COMPOSER_DEFAULT_LICENSE")
+ .map(|value| value.to_string_lossy().into_owned());
if !empty(
&default_license
.clone()
@@ -1163,7 +1167,21 @@ impl InitCommand {
let name = self.sanitize_package_name_component(&name);
let mut vendor = name.clone();
- let composer_default_vendor = server_get("COMPOSER_DEFAULT_VENDOR");
+ let composer_default_vendor = PHP_SERVER
+ .lock()
+ .unwrap()
+ .get("COMPOSER_DEFAULT_VENDOR")
+ .map(|value| value.to_string_lossy().into_owned());
+ let server_username = PHP_SERVER
+ .lock()
+ .unwrap()
+ .get("USERNAME")
+ .map(|value| value.to_string_lossy().into_owned());
+ let server_user = PHP_SERVER
+ .lock()
+ .unwrap()
+ .get("USER")
+ .map(|value| value.to_string_lossy().into_owned());
if !empty(
&composer_default_vendor
.clone()
@@ -1174,19 +1192,19 @@ impl InitCommand {
} else if git.contains_key("github.user") {
vendor = git.get("github.user").cloned().unwrap_or_default();
} else if !empty(
- &server_get("USERNAME")
+ &server_username
.clone()
.map(PhpMixed::String)
.unwrap_or(PhpMixed::Null),
) {
- vendor = server_get("USERNAME").unwrap_or_default();
+ vendor = server_username.unwrap_or_default();
} else if !empty(
- &server_get("USER")
+ &server_user
.clone()
.map(PhpMixed::String)
.unwrap_or(PhpMixed::Null),
) {
- vendor = server_get("USER").unwrap_or_default();
+ vendor = server_user.unwrap_or_default();
} else if !get_current_user().is_empty() {
vendor = get_current_user();
}
@@ -1200,7 +1218,11 @@ impl InitCommand {
let git = self.get_git_config();
let mut author_name: Option<String> = None;
- let composer_default_author = server_get("COMPOSER_DEFAULT_AUTHOR");
+ let composer_default_author = PHP_SERVER
+ .lock()
+ .unwrap()
+ .get("COMPOSER_DEFAULT_AUTHOR")
+ .map(|value| value.to_string_lossy().into_owned());
if !empty(
&composer_default_author
.clone()
@@ -1213,7 +1235,11 @@ impl InitCommand {
}
let mut author_email: Option<String> = None;
- let composer_default_email = server_get("COMPOSER_DEFAULT_EMAIL");
+ let composer_default_email = PHP_SERVER
+ .lock()
+ .unwrap()
+ .get("COMPOSER_DEFAULT_EMAIL")
+ .map(|value| value.to_string_lossy().into_owned());
if !empty(
&composer_default_email
.clone()
diff --git a/crates/shirabe/src/console/application.rs b/crates/shirabe/src/console/application.rs
index 251fc67..319e8dd 100644
--- a/crates/shirabe/src/console/application.rs
+++ b/crates/shirabe/src/console/application.rs
@@ -1402,7 +1402,10 @@ impl Application {
input.borrow_mut().set_interactive(false);
}
- let mut shell_verbosity = shirabe_php_shim::getenv("SHELL_VERBOSITY").unwrap_or_default();
+ let shell_verbosity = shirabe_php_shim::getenv("SHELL_VERBOSITY")
+ .unwrap_or_default()
+ .to_string_lossy()
+ .into_owned();
let shell_verbosity_int: i64 = shell_verbosity.parse().unwrap_or(0);
let mut shell_verbosity: i64 = shell_verbosity_int;
match shell_verbosity_int {
@@ -1500,10 +1503,16 @@ impl Application {
}
if shirabe_php_shim::function_exists("putenv") {
- shirabe_php_shim::putenv(&format!("SHELL_VERBOSITY={}", shell_verbosity));
+ unsafe { shirabe_php_shim::putenv("SHELL_VERBOSITY", shell_verbosity.to_string()) };
}
- shirabe_php_shim::env_set("SHELL_VERBOSITY", shell_verbosity.to_string());
- shirabe_php_shim::server_set("SHELL_VERBOSITY", shell_verbosity.to_string());
+ shirabe_php_shim::PHP_ENV
+ .lock()
+ .unwrap()
+ .put("SHELL_VERBOSITY".into(), shell_verbosity.to_string().into());
+ shirabe_php_shim::PHP_SERVER
+ .lock()
+ .unwrap()
+ .put("SHELL_VERBOSITY".into(), shell_verbosity.to_string().into());
let _ = &mut shell_verbosity;
@@ -2285,7 +2294,12 @@ impl ApplicationHandle {
{
io.write_error(&format!(
"<warning>Warning: This development build of Composer is over 60 days old. It is recommended to update it by running \"{} self-update\" to get the latest version.</warning>",
- shirabe_php_shim::server_get("PHP_SELF").unwrap_or_default(),
+ shirabe_php_shim::PHP_SERVER
+ .lock()
+ .unwrap()
+ .get("PHP_SELF")
+ .unwrap_or_default()
+ .to_string_lossy(),
));
}
@@ -2598,8 +2612,8 @@ impl ApplicationHandle {
let app = application.borrow();
(app.terminal.get_height(), app.terminal.get_width())
};
- shirabe_php_shim::putenv(&format!("LINES={}", height));
- shirabe_php_shim::putenv(&format!("COLUMNS={}", width));
+ unsafe { shirabe_php_shim::putenv("LINES", height.to_string()) };
+ unsafe { shirabe_php_shim::putenv("COLUMNS", width.to_string()) };
}
let input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>> = match input {
diff --git a/crates/shirabe/src/main.rs b/crates/shirabe/src/main.rs
index 3e3623d..538d89d 100644
--- a/crates/shirabe/src/main.rs
+++ b/crates/shirabe/src/main.rs
@@ -1,8 +1,13 @@
//! ref: composer/bin/composer
-use shirabe_php_shim::run_shutdown_functions;
+use shirabe_php_shim::{PHP_ENV, PHP_SERVER, run_shutdown_functions};
fn main() {
+ // Take the $_ENV / $_SERVER snapshots before any putenv() mutates the real environment.
+ // See `docs/dev/env-vars-porting.md` for details.
+ std::sync::LazyLock::force(&PHP_ENV);
+ std::sync::LazyLock::force(&PHP_SERVER);
+
let result = shirabe::run(std::env::args().collect());
run_shutdown_functions();
let mut exit_code = match result {
diff --git a/crates/shirabe/src/util/platform.rs b/crates/shirabe/src/util/platform.rs
index 72fe5f9..889da6e 100644
--- a/crates/shirabe/src/util/platform.rs
+++ b/crates/shirabe/src/util/platform.rs
@@ -5,11 +5,11 @@ use std::sync::Mutex;
use anyhow::Result;
use shirabe_external_packages::composer::pcre::Preg;
use shirabe_php_shim::{
- PhpMixed, PhpResource, RuntimeException, defined, env_contains_key, env_get, env_set,
- env_unset, file_exists, file_get_contents, fstat, function_exists, getcwd, getenv, in_array,
- ini_get, is_array, is_readable, mb_strlen, php_os_family, posix_geteuid, posix_getpwuid,
- posix_getuid, posix_isatty, putenv, realpath, server_contains_key, server_get, server_set,
- server_unset, stream_isatty, stripos, strlen, strtoupper, substr, usleep,
+ PHP_ENV, PHP_SERVER, PhpMixed, PhpResource, RuntimeException, defined, file_exists,
+ file_get_contents, fstat, function_exists, getcwd, getenv, in_array, ini_get, is_array,
+ is_readable, mb_strlen, php_os_family, posix_geteuid, posix_getpwuid, posix_getuid,
+ posix_isatty, putenv, putenv_clear, realpath, stream_isatty, stripos, strlen, strtoupper,
+ substr, usleep,
};
use crate::util::ProcessExecutor;
@@ -66,28 +66,28 @@ impl Platform {
///
/// @return string|false
pub fn get_env(name: &str) -> Option<String> {
- if server_contains_key(name) {
- return Some(server_get(name).unwrap_or_default());
+ if let Some(value) = PHP_SERVER.lock().unwrap().get(name) {
+ return Some(value.to_string_lossy().into_owned());
}
- if env_contains_key(name) {
- return Some(env_get(name).unwrap_or_default());
+ if let Some(value) = PHP_ENV.lock().unwrap().get(name) {
+ return Some(value.to_string_lossy().into_owned());
}
- getenv(name)
+ getenv(name).map(|value| value.to_string_lossy().into_owned())
}
/// putenv() equivalent but updates the runtime global variables too
pub fn put_env(name: &str, value: &str) {
- putenv(&format!("{}={}", name, value));
- server_set(name, value.to_string());
- env_set(name, value.to_string());
+ unsafe { putenv(name, value) };
+ PHP_SERVER.lock().unwrap().put(name.into(), value.into());
+ PHP_ENV.lock().unwrap().put(name.into(), value.into());
}
/// putenv('X') equivalent but updates the runtime global variables too
pub fn clear_env(name: &str) {
- putenv(name);
- server_unset(name);
- env_unset(name);
+ unsafe { putenv_clear(name) };
+ PHP_SERVER.lock().unwrap().clear(name);
+ PHP_ENV.lock().unwrap().clear(name);
}
/// Parses tildes and environment variables in paths.
diff --git a/crates/shirabe/tests/command/init_command_test.rs b/crates/shirabe/tests/command/init_command_test.rs
index 1c2fca6..0c38bf4 100644
--- a/crates/shirabe/tests/command/init_command_test.rs
+++ b/crates/shirabe/tests/command/init_command_test.rs
@@ -4,12 +4,13 @@ use crate::test_case::{RunOptions, get_application_tester, init_temp_composer};
use serial_test::serial;
use shirabe::command::init_command::InitCommand;
use shirabe::json::JsonFile;
-use shirabe_php_shim::{PhpMixed, server_set, server_unset};
+use shirabe_php_shim::{PHP_SERVER, PhpMixed};
use tempfile::TempDir;
fn set_up() {
- server_set("COMPOSER_DEFAULT_AUTHOR", "John Smith".to_string());
- server_set("COMPOSER_DEFAULT_EMAIL", "john@example.com".to_string());
+ let mut server = PHP_SERVER.lock().unwrap();
+ server.put("COMPOSER_DEFAULT_AUTHOR".into(), "John Smith".into());
+ server.put("COMPOSER_DEFAULT_EMAIL".into(), "john@example.com".into());
}
/// const DEFAULT_AUTHORS in PHP.
@@ -497,12 +498,15 @@ fn test_run_guess_name_from_dir_sanitizes_dir() {
std::fs::create_dir(dir_name).unwrap();
std::env::set_current_dir(dir_name).unwrap();
- server_set("COMPOSER_DEFAULT_VENDOR", ".vendorName".to_string());
+ PHP_SERVER
+ .lock()
+ .unwrap()
+ .put("COMPOSER_DEFAULT_VENDOR".into(), ".vendorName".into());
let mut app_tester = get_application_tester();
let result = app_tester.run(non_interactive_input(vec![]), RunOptions::default());
- server_unset("COMPOSER_DEFAULT_VENDOR");
+ PHP_SERVER.lock().unwrap().clear("COMPOSER_DEFAULT_VENDOR");
result.unwrap();
assert_eq!(0, app_tester.get_status_code());
diff --git a/crates/shirabe/tests/config_test.rs b/crates/shirabe/tests/config_test.rs
index 9c2f80b..df29281 100644
--- a/crates/shirabe/tests/config_test.rs
+++ b/crates/shirabe/tests/config_test.rs
@@ -1,6 +1,7 @@
//! ref: composer/tests/Composer/Test/ConfigTest.php
use indexmap::IndexMap;
+use serial_test::serial;
use shirabe::advisory::Auditor;
use shirabe::config::Config;
use shirabe::util::Platform;
@@ -509,6 +510,7 @@ fn test_disable_tls_can_be_overridden() {
}
#[test]
+#[serial]
fn test_process_timeout() {
Platform::put_env("COMPOSER_PROCESS_TIMEOUT", "0");
let config = Config::new(true, None);
@@ -520,6 +522,7 @@ fn test_process_timeout() {
#[ignore]
#[test]
+#[serial]
fn test_htaccess_protect() {
Platform::put_env("COMPOSER_HTACCESS_PROTECT", "0");
let config = Config::new(true, None);
@@ -530,6 +533,7 @@ fn test_htaccess_protect() {
}
#[test]
+#[serial]
fn test_get_source_of_value() {
Platform::clear_env("COMPOSER_PROCESS_TIMEOUT");
@@ -552,6 +556,7 @@ fn test_get_source_of_value() {
}
#[test]
+#[serial]
fn test_get_source_of_value_env_variables() {
Platform::put_env("COMPOSER_HTACCESS_PROTECT", "0");
let mut config = Config::new(true, None);
@@ -562,6 +567,7 @@ fn test_get_source_of_value_env_variables() {
}
#[test]
+#[serial]
fn test_audit() {
let mut config = Config::new(true, None);
let result = config.get("audit");
diff --git a/crates/shirabe/tests/util/ini_helper_test.rs b/crates/shirabe/tests/util/ini_helper_test.rs
index f511d76..d1e2071 100644
--- a/crates/shirabe/tests/util/ini_helper_test.rs
+++ b/crates/shirabe/tests/util/ini_helper_test.rs
@@ -12,7 +12,8 @@ fn set_up() -> TearDown {
// XdebugHandler is a unit struct with no name-registration API, so this
// step is a no-op here.
// Save current state
- let env_original = getenv("COMPOSER_ORIGINAL_INIS");
+ let env_original =
+ getenv("COMPOSER_ORIGINAL_INIS").map(|value| value.to_string_lossy().into_owned());
TearDown { env_original }
}
@@ -20,7 +21,7 @@ fn set_up() -> TearDown {
fn tear_down(env_original: &Option<String>) {
// Restore original state
if let Some(env_original) = env_original {
- putenv(&format!("COMPOSER_ORIGINAL_INIS={env_original}"));
+ unsafe { putenv("COMPOSER_ORIGINAL_INIS", env_original) };
} else {
Platform::clear_env("COMPOSER_ORIGINAL_INIS");
}
@@ -38,10 +39,7 @@ impl Drop for TearDown {
}
fn set_env(paths: &[&str]) {
- putenv(&format!(
- "COMPOSER_ORIGINAL_INIS={}",
- paths.join(PATH_SEPARATOR)
- ));
+ unsafe { putenv("COMPOSER_ORIGINAL_INIS", paths.join(PATH_SEPARATOR)) };
}
#[test]
diff --git a/docs/dev/env-vars-porting.md b/docs/dev/env-vars-porting.md
new file mode 100644
index 0000000..fbee870
--- /dev/null
+++ b/docs/dev/env-vars-porting.md
@@ -0,0 +1,44 @@
+# Environment variable porting
+
+PHP exposes three distinct ways to read process environment variables: the `$_ENV` and `$_SERVER`
+superglobals, and `getenv()`/`putenv()`. They look interchangeable but are not, so Shirabe ports
+each to a separate construct in `crates/shirabe-php-shim/src/env.rs`. Choose the porting target by
+matching the exact PHP construct used in Composer; do not substitute one for another.
+
+## The three are not interchangeable
+
+The three differ in what they observe and when:
+
+* `getenv()`/`putenv()` read and write the real environment variables. `putenv()` mutates the
+ live process environment, so a value set with `putenv()` is inherited by child processes spawned
+ afterwards.
+* `$_ENV` and `$_SERVER` are snapshots taken at startup. They are populated once when the
+ process starts and are not kept in sync afterwards. A later `putenv()` does *not* appear in
+ `$_ENV`/`$_SERVER`, and assigning to `$_ENV`/`$_SERVER` does *not* change the real environment.
+* `$_ENV` and `$_SERVER` are not shared with child processes. Launching an external program via `system()`
+ and friends passes along the real environment (as mutated by `putenv()`), not the
+ `$_ENV`/`$_SERVER` snapshot.
+* Also, `$_ENV` and `$_SERVER` have their own storage; they are not shared.
+
+Because of these differences, porting must preserve which of the three Composer actually used at
+each call site.
+
+## Mapping
+
+| PHP | Shirabe |
+| --- | --- |
+| `getenv()` | `getenv()`/`getenv_all()` |
+| `putenv("K=V")` | `putenv()` |
+| `putenv("K")` (unset) | `putenv_clear()` |
+| `$_ENV` | `PHP_ENV` |
+| `$_SERVER` | `PHP_SERVER` |
+
+## TODOs
+
+The current implementation only models the Rust side. Two things remain unimplemented:
+
+* Propagating `$_ENV`/`$_SERVER` into the real PHP runtime. When Shirabe hands control to PHP
+ (for the plugin API), the PHP side needs to see the same `$_ENV`/`$_SERVER` snapshot Shirabe
+ holds. This is marked `TODO(php-runtime)` in `env.rs` and is not yet wired up.
+* Reflecting PHP-side mutations of `$_ENV`/`$_SERVER` back into Shirabe. How to handle the case
+ where PHP code rewrites `$_ENV` or `$_SERVER` is still TBD.