feat: enhance command execution with shell fallback and wrap helper commands

This commit is contained in:
2026-03-10 22:44:20 -05:00
parent 170025c448
commit 0c90c7189d
5 changed files with 134 additions and 14 deletions
+10 -5
View File
@@ -116,9 +116,14 @@ pub fn build(
build_function_mode_command(spec, destdir, &abs_build_script)?
} else {
let mut cmd = fakeroot::wrap_install_command("sh", destdir);
// Run custom scripts with `-e` so command failures stop the build immediately
// instead of being masked by later shell commands.
cmd.arg("-e").arg(&abs_build_script);
let wrapper = crate::shell_helpers::wrap_shell_command(". \"$1\"");
// Run custom scripts through `sh -c` so helper commands like `haul`
// work even when the helper scripts live on a `noexec` mount.
cmd.arg("-eu")
.arg("-c")
.arg(wrapper)
.arg("sh")
.arg(&abs_build_script);
cmd
};
cmd.current_dir(&build_dir);
@@ -179,8 +184,8 @@ fn build_function_mode_command(
destdir: &Path,
build_script: &Path,
) -> Result<Command> {
let mut wrapper = String::new();
wrapper.push_str("set -eu\n");
let mut wrapper = crate::shell_helpers::wrap_shell_command("");
wrapper.push_str("\nset -eu\n");
wrapper.push_str("depot_has_function() {\n");
wrapper.push_str(" case \"$(type \"$1\" 2>/dev/null || :)\" in\n");
wrapper.push_str(" *function*) return 0 ;;\n");
+55 -4
View File
@@ -59,7 +59,7 @@ pub fn build(
}
crate::builder::prepare_tool_command(&mut configure_cmd, &env_vars);
let status = configure_cmd.status().with_context(|| {
let status = command_status_with_sh_fallback(&mut configure_cmd).with_context(|| {
format!(
"Failed to run perl {} in {}",
configure_script.display(),
@@ -102,7 +102,7 @@ pub fn build(
}
crate::builder::prepare_tool_command(&mut make_cmd, &env_vars);
let status = make_cmd.status().with_context(|| {
let status = command_status_with_sh_fallback(&mut make_cmd).with_context(|| {
format!("Failed to run {} in {}", make_exec, build_dir.display())
})?;
if !status.success() {
@@ -148,7 +148,7 @@ pub fn build(
}
crate::builder::prepare_tool_command(&mut test_cmd, &env_vars);
let status = test_cmd.status().with_context(|| {
let status = command_status_with_sh_fallback(&mut test_cmd).with_context(|| {
format!(
"Failed to run {} {} in {}",
make_exec,
@@ -221,7 +221,7 @@ pub fn build(
));
crate::builder::prepare_tool_command(&mut install_cmd, &install_env);
let status = install_cmd.status().with_context(|| {
let status = command_status_with_sh_fallback(&mut install_cmd).with_context(|| {
format!(
"Failed to run {} {} for {} in {}",
make_exec,
@@ -265,6 +265,57 @@ fn resolve_perl_configure_script(spec: &PackageSpec, actual_src: &Path) -> PathB
}
}
fn command_status_with_sh_fallback(cmd: &mut Command) -> std::io::Result<std::process::ExitStatus> {
match cmd.status() {
Ok(status) => Ok(status),
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => {
let Some(script) = resolved_script_path(cmd) else {
return Err(err);
};
let contents = fs::read(&script);
let is_script = contents.ok().is_some_and(|bytes| bytes.starts_with(b"#!"));
if !is_script {
return Err(err);
}
let mut fallback = Command::new("sh");
fallback.arg(&script);
fallback.args(cmd.get_args());
if let Some(dir) = cmd.get_current_dir() {
fallback.current_dir(dir);
}
fallback.env_clear();
for (key, value) in cmd.get_envs() {
match value {
Some(value) => {
fallback.env(key, value);
}
None => {
fallback.env_remove(key);
}
}
}
fallback.status()
}
Err(err) => Err(err),
}
}
fn resolved_script_path(cmd: &Command) -> Option<PathBuf> {
let program = Path::new(cmd.get_program());
if program.components().count() > 1 || program.is_absolute() {
return Some(program.to_path_buf());
}
let path_value = cmd
.get_envs()
.find_map(|(key, value)| (key == "PATH").then_some(value))
.flatten()?;
std::env::split_paths(path_value)
.map(|dir| dir.join(program))
.find(|candidate| candidate.is_file())
}
fn has_assignment_prefix(args: &[String], name: &str) -> bool {
args.iter().any(|arg| {
let trimmed = arg.trim();
+36 -1
View File
@@ -143,7 +143,7 @@ fn run_install_command_with_program(
cmd.env("DEPOT_DEPCHAIN", dep_chain);
}
let status = cmd.status().with_context(|| {
let status = command_status_with_sh_fallback(&mut cmd).with_context(|| {
format!(
"Failed to spawn child install for {}",
install_request_display(install_requests)
@@ -160,6 +160,41 @@ fn run_install_command_with_program(
}
}
fn command_status_with_sh_fallback(
cmd: &mut std::process::Command,
) -> std::io::Result<std::process::ExitStatus> {
match cmd.status() {
Ok(status) => Ok(status),
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => {
let program = cmd.get_program();
let contents = fs::read(program);
let is_script = contents.ok().is_some_and(|bytes| bytes.starts_with(b"#!"));
if !is_script {
return Err(err);
}
let mut fallback = std::process::Command::new("sh");
fallback.arg(program);
fallback.args(cmd.get_args());
if let Some(dir) = cmd.get_current_dir() {
fallback.current_dir(dir);
}
for (key, value) in cmd.get_envs() {
match value {
Some(value) => {
fallback.env(key, value);
}
None => {
fallback.env_remove(key);
}
}
}
fallback.status()
}
Err(err) => Err(err),
}
}
fn run_child_install_command(
install_requests: &[PathBuf],
rootfs: &Path,
+27 -1
View File
@@ -10,12 +10,16 @@ use tempfile::TempDir;
pub const INTERNAL_DEPOT_DIR: &str = ".depot";
/// Internal split-output staging root inside `DESTDIR`.
pub const INTERNAL_OUTPUTS_DIR: &str = ".depot/outputs";
const DEPOT_HAUL_HELPER_ENV: &str = "DEPOT_HAUL_HELPER";
const DEPOT_SUBDESTDIR_HELPER_ENV: &str = "DEPOT_SUBDESTDIR_HELPER";
/// Ephemeral helper command directory to prepend to PATH while running scripts.
pub struct ShellHelpers {
_tempdir: TempDir,
path_value: String,
outputs_dir: PathBuf,
haul_path: PathBuf,
subdestdir_path: PathBuf,
}
impl ShellHelpers {
@@ -77,6 +81,8 @@ impl ShellHelpers {
_tempdir: tempdir,
path_value,
outputs_dir: destdir.join(INTERNAL_OUTPUTS_DIR),
haul_path,
subdestdir_path,
})
}
@@ -89,16 +95,36 @@ impl ShellHelpers {
self.outputs_dir.to_string_lossy().into_owned(),
);
set_env_var(env_vars, "DEPOT_INTERNAL_DIR", INTERNAL_DEPOT_DIR);
set_env_var(
env_vars,
DEPOT_HAUL_HELPER_ENV,
self.haul_path.to_string_lossy().into_owned(),
);
set_env_var(
env_vars,
DEPOT_SUBDESTDIR_HELPER_ENV,
self.subdestdir_path.to_string_lossy().into_owned(),
);
}
/// Apply helper-related variables directly to a `std::process::Command`.
pub fn apply_to_command(&self, cmd: &mut std::process::Command) {
cmd.env("PATH", &self.path_value)
.env("DEPOT_OUTPUTS_DIR", &self.outputs_dir)
.env("DEPOT_INTERNAL_DIR", INTERNAL_DEPOT_DIR);
.env("DEPOT_INTERNAL_DIR", INTERNAL_DEPOT_DIR)
.env(DEPOT_HAUL_HELPER_ENV, &self.haul_path)
.env(DEPOT_SUBDESTDIR_HELPER_ENV, &self.subdestdir_path);
}
}
/// Wrap a shell command with helper functions that invoke the helper scripts
/// through `/bin/sh`, avoiding direct execution from mounts that may be `noexec`.
pub fn wrap_shell_command(command: &str) -> String {
format!(
"haul() {{ /bin/sh \"${{{DEPOT_HAUL_HELPER_ENV}:?}}\" \"$@\"; }}\nsubdestdir() {{ /bin/sh \"${{{DEPOT_SUBDESTDIR_HELPER_ENV}:?}}\" \"$@\"; }}\n{command}"
)
}
/// Convert a package name into a safe shell identifier suffix.
pub fn shell_ident_suffix(pkg_name: &str) -> String {
let mut out = String::with_capacity(pkg_name.len().max(1));
+6 -3
View File
@@ -125,6 +125,7 @@ pub fn run_post_configure_commands(
for cmd in commands {
let cmd_str = spec.expand_vars(cmd);
crate::log_info!(" post_configure: {}", cmd_str);
let wrapped_cmd = crate::shell_helpers::wrap_shell_command(&cmd_str);
let mut shell_cmd = Command::new("sh");
shell_cmd.current_dir(src_dir);
@@ -136,7 +137,7 @@ pub fn run_post_configure_commands(
.env("CC", &spec.build.flags.cc)
.env("AR", &spec.build.flags.ar)
.arg("-c")
.arg(&cmd_str)
.arg(&wrapped_cmd)
.status()
.with_context(|| format!("Failed to run post_configure command: {}", cmd_str))?;
@@ -161,6 +162,7 @@ pub fn run_post_compile_commands(spec: &PackageSpec, src_dir: &Path, destdir: &P
for cmd in commands {
let cmd_str = spec.expand_vars(cmd);
crate::log_info!(" post_compile: {}", cmd_str);
let wrapped_cmd = crate::shell_helpers::wrap_shell_command(&cmd_str);
let mut shell_cmd = Command::new("sh");
shell_cmd.current_dir(src_dir);
@@ -172,7 +174,7 @@ pub fn run_post_compile_commands(spec: &PackageSpec, src_dir: &Path, destdir: &P
.env("CC", &spec.build.flags.cc)
.env("AR", &spec.build.flags.ar)
.arg("-c")
.arg(&cmd_str)
.arg(&wrapped_cmd)
.status()
.with_context(|| format!("Failed to run post_compile command: {}", cmd_str))?;
@@ -197,6 +199,7 @@ pub fn run_post_install_commands(spec: &PackageSpec, src_dir: &Path, destdir: &P
for cmd in commands {
let cmd_str = spec.expand_vars(cmd);
crate::log_info!(" post_install: {}", cmd_str);
let wrapped_cmd = crate::shell_helpers::wrap_shell_command(&cmd_str);
let mut shell_cmd = Command::new("sh");
shell_cmd.current_dir(src_dir);
@@ -208,7 +211,7 @@ pub fn run_post_install_commands(spec: &PackageSpec, src_dir: &Path, destdir: &P
.env("CC", &spec.build.flags.cc)
.env("AR", &spec.build.flags.ar)
.arg("-c")
.arg(&cmd_str)
.arg(&wrapped_cmd)
.status()
.with_context(|| format!("Failed to run post_install command: {}", cmd_str))?;