diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-10 00:32:08 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-10 00:32:08 +0900 |
| commit | 8cc1ba8a02c0318b65658f1634de378c780392b9 (patch) | |
| tree | fdd5cb61e488018891a486b25991b87c84220bb8 /crates/mozart-core/src/vcs/process.rs | |
| parent | 72b2e877c01e67ba7edd37e34ac2eadb7a1c62c4 (diff) | |
| download | php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.tar.gz php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.tar.zst php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.zip | |
refactor(workspace): consolidate crates into mozart-core
Merged mozart-archiver, mozart-autoload, mozart-registry,
mozart-sat-resolver, and mozart-vcs into mozart-core to align
the source layout with Composer's structure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-core/src/vcs/process.rs')
| -rw-r--r-- | crates/mozart-core/src/vcs/process.rs | 142 |
1 files changed, 142 insertions, 0 deletions
diff --git a/crates/mozart-core/src/vcs/process.rs b/crates/mozart-core/src/vcs/process.rs new file mode 100644 index 0000000..8ccc11d --- /dev/null +++ b/crates/mozart-core/src/vcs/process.rs @@ -0,0 +1,142 @@ +use indexmap::IndexMap; +use std::path::Path; +use std::process::Command; +use std::time::{Duration, Instant}; + +use anyhow::{Result, bail}; + +/// Output from a process execution. +#[derive(Debug, Clone)] +pub struct ProcessOutput { + pub status: i32, + pub stdout: String, + pub stderr: String, +} + +/// Wrapper around `std::process::Command` for executing external programs. +/// +/// Corresponds to Composer's `ProcessExecutor`. +pub struct ProcessExecutor { + timeout: Option<Duration>, + env_overrides: IndexMap<String, Option<String>>, +} + +impl Default for ProcessExecutor { + fn default() -> Self { + Self::new() + } +} + +impl ProcessExecutor { + pub fn new() -> Self { + Self { + timeout: None, + env_overrides: IndexMap::new(), + } + } + + pub fn with_timeout(secs: u64) -> Self { + Self { + timeout: Some(Duration::from_secs(secs)), + env_overrides: IndexMap::new(), + } + } + + /// Set an environment variable override for all subsequent executions. + pub fn set_env(&mut self, key: impl Into<String>, value: impl Into<String>) { + self.env_overrides.insert(key.into(), Some(value.into())); + } + + /// Remove an environment variable for all subsequent executions. + pub fn remove_env(&mut self, key: impl Into<String>) { + self.env_overrides.insert(key.into(), None); + } + + /// Execute a command. Does not error on non-zero exit status. + pub fn execute(&self, args: &[&str], cwd: Option<&Path>) -> Result<ProcessOutput> { + if args.is_empty() { + bail!("No command specified"); + } + + let mut cmd = Command::new(args[0]); + if args.len() > 1 { + cmd.args(&args[1..]); + } + if let Some(dir) = cwd { + cmd.current_dir(dir); + } + + for (key, value) in &self.env_overrides { + match value { + Some(v) => { + cmd.env(key, v); + } + None => { + cmd.env_remove(key); + } + } + } + + if let Some(timeout) = self.timeout { + let mut child = cmd + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn()?; + + let start = Instant::now(); + loop { + match child.try_wait()? { + Some(status) => { + let mut stdout = String::new(); + let mut stderr = String::new(); + if let Some(ref mut out) = child.stdout { + std::io::Read::read_to_string(out, &mut stdout)?; + } + if let Some(ref mut err) = child.stderr { + std::io::Read::read_to_string(err, &mut stderr)?; + } + return Ok(ProcessOutput { + status: status.code().unwrap_or(-1), + stdout, + stderr, + }); + } + None => { + if start.elapsed() > timeout { + let _ = child.kill(); + bail!("Process timed out after {} seconds", timeout.as_secs()); + } + std::thread::sleep(Duration::from_millis(50)); + } + } + } + } else { + let output = cmd.output()?; + Ok(ProcessOutput { + status: output.status.code().unwrap_or(-1), + stdout: String::from_utf8_lossy(&output.stdout).into_owned(), + stderr: String::from_utf8_lossy(&output.stderr).into_owned(), + }) + } + } + + /// Execute a command, returning an error if the exit status is non-zero. + pub fn execute_checked(&self, args: &[&str], cwd: Option<&Path>) -> Result<ProcessOutput> { + let output = self.execute(args, cwd)?; + if output.status != 0 { + bail!( + "Command `{}` failed with exit code {}\nstdout: {}\nstderr: {}", + args.join(" "), + output.status, + output.stdout.trim(), + output.stderr.trim(), + ); + } + Ok(output) + } + + /// Split output into non-empty lines. + pub fn split_lines(output: &str) -> Vec<&str> { + output.lines().filter(|l| !l.is_empty()).collect() + } +} |
