From 0c90c7189dae5bf918e33f049d34777555594459 Mon Sep 17 00:00:00 2001 From: SFG545 Date: Tue, 10 Mar 2026 22:44:20 -0500 Subject: [PATCH] feat: enhance command execution with shell fallback and wrap helper commands --- src/builder/custom.rs | 15 +++++++---- src/builder/perl.rs | 59 ++++++++++++++++++++++++++++++++++++++++--- src/commands.rs | 37 ++++++++++++++++++++++++++- src/shell_helpers.rs | 28 +++++++++++++++++++- src/source/hooks.rs | 9 ++++--- 5 files changed, 134 insertions(+), 14 deletions(-) diff --git a/src/builder/custom.rs b/src/builder/custom.rs index ecf2424..4326a73 100755 --- a/src/builder/custom.rs +++ b/src/builder/custom.rs @@ -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 { - 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"); diff --git a/src/builder/perl.rs b/src/builder/perl.rs index 4e1015f..8b68139 100644 --- a/src/builder/perl.rs +++ b/src/builder/perl.rs @@ -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 { + 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 { + 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(); diff --git a/src/commands.rs b/src/commands.rs index f046a35..5d09a8b 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -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 { + 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, diff --git a/src/shell_helpers.rs b/src/shell_helpers.rs index d0b173a..36d5b2d 100644 --- a/src/shell_helpers.rs +++ b/src/shell_helpers.rs @@ -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)); diff --git a/src/source/hooks.rs b/src/source/hooks.rs index aaba6b3..99c3d24 100755 --- a/src/source/hooks.rs +++ b/src/source/hooks.rs @@ -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))?;