//! Shell helper commands exposed to package build/post-install scripts. use crate::builder::{EnvVars, set_env_var}; use anyhow::{Context, Result}; use std::fs; use std::path::{Path, PathBuf}; use tempfile::TempDir; /// Internal staging namespace reserved inside `DESTDIR`. pub const INTERNAL_DEPOT_DIR: &str = ".depot"; /// Internal split-output staging root inside `DESTDIR`. pub const INTERNAL_OUTPUTS_DIR: &str = ".depot/outputs"; /// Ephemeral helper command directory to prepend to PATH while running scripts. pub struct ShellHelpers { _tempdir: TempDir, path_value: String, outputs_dir: PathBuf, } impl ShellHelpers { /// Create helper commands for a given staging tree (`DESTDIR`). pub fn new(destdir: &Path) -> Result { let tempdir = tempfile::tempdir().context("Failed to create shell helper tempdir")?; let bin_dir = tempdir.path().join("bin"); fs::create_dir_all(&bin_dir) .with_context(|| format!("Failed to create helper bin dir: {}", bin_dir.display()))?; let haul_path = bin_dir.join("haul"); fs::write(&haul_path, HAUL_SCRIPT).with_context(|| { format!( "Failed to write shell helper command: {}", haul_path.display() ) })?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let mut perms = fs::metadata(&haul_path) .with_context(|| format!("Failed to stat helper: {}", haul_path.display()))? .permissions(); perms.set_mode(0o755); fs::set_permissions(&haul_path, perms) .with_context(|| format!("Failed to chmod helper: {}", haul_path.display()))?; } let subdestdir_path = bin_dir.join("subdestdir"); fs::write(&subdestdir_path, SUBDESTDIR_SCRIPT).with_context(|| { format!( "Failed to write shell helper command: {}", subdestdir_path.display() ) })?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let mut perms = fs::metadata(&subdestdir_path) .with_context(|| format!("Failed to stat helper: {}", subdestdir_path.display()))? .permissions(); perms.set_mode(0o755); fs::set_permissions(&subdestdir_path, perms).with_context(|| { format!("Failed to chmod helper: {}", subdestdir_path.display()) })?; } let parent_path = std::env::var("PATH").unwrap_or_default(); let helper_path = bin_dir.to_string_lossy().into_owned(); let path_value = if parent_path.is_empty() { helper_path } else { format!("{}:{}", helper_path, parent_path) }; Ok(Self { _tempdir: tempdir, path_value, outputs_dir: destdir.join(INTERNAL_OUTPUTS_DIR), }) } /// Apply helper-related variables to an environment vector used with `prepare_command`. pub fn apply_to_env_vars(&self, env_vars: &mut EnvVars) { set_env_var(env_vars, "PATH", self.path_value.clone()); set_env_var( env_vars, "DEPOT_OUTPUTS_DIR", self.outputs_dir.to_string_lossy().into_owned(), ); set_env_var(env_vars, "DEPOT_INTERNAL_DIR", INTERNAL_DEPOT_DIR); } /// 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); } } /// 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)); for ch in pkg_name.chars() { if ch.is_ascii_alphanumeric() { out.push(ch.to_ascii_uppercase()); } else { out.push('_'); } } if out.is_empty() { out.push('_'); } if out.as_bytes().first().is_some_and(|b| b.is_ascii_digit()) { out.insert(0, '_'); } out } const HAUL_SCRIPT: &str = r#"#!/bin/sh set -eu usage() { echo "Usage: haul [path-pattern ...]" >&2 exit 2 } fail() { echo "haul: $*" >&2 exit 1 } [ "$#" -ge 2 ] || usage out_pkg=$1 shift case "$out_pkg" in ""|.|..|*/*) fail "invalid output package name: $out_pkg" ;; esac [ "${DESTDIR:-}" != "" ] || fail "DESTDIR is not set" [ "${DEPOT_OUTPUTS_DIR:-}" != "" ] || fail "DEPOT_OUTPUTS_DIR is not set" src_root=$DESTDIR out_root=$DEPOT_OUTPUTS_DIR/$out_pkg case "$src_root" in */) src_root=${src_root%/} ;; esac case "$out_root" in */) out_root=${out_root%/} ;; esac mkdir -p "$out_root" tmp_matches=$(mktemp "${TMPDIR:-/tmp}/depot-haul.XXXXXX") trap 'rm -f "$tmp_matches"' EXIT HUP INT TERM : > "$tmp_matches" collect_matches_for_pattern() { pattern=$1 before_count=$(wc -l < "$tmp_matches" | tr -d '[:space:]') case "$pattern" in ""|/|/*|../*|*/../*|..) fail "unsafe pattern: $pattern" ;; .depot|.depot/*) fail "refusing to haul internal staging paths: $pattern" ;; esac if [ ! -d "$src_root" ]; then fail "DESTDIR does not exist: $src_root" fi find "$src_root" -mindepth 1 \ ! -path "$DEPOT_OUTPUTS_DIR" ! -path "$DEPOT_OUTPUTS_DIR/*" \ -print | LC_ALL=C sort | while IFS= read -r abs_path; do rel_path=${abs_path#"$src_root"/} [ "$rel_path" != "$abs_path" ] || continue case "$rel_path" in $pattern) printf '%s\n' "$rel_path" >> "$tmp_matches" ;; esac done after_count=$(wc -l < "$tmp_matches" | tr -d '[:space:]') if [ "$before_count" = "$after_count" ]; then fail "no matches for pattern: $pattern" fi } for pattern in "$@"; do collect_matches_for_pattern "$pattern" done LC_ALL=C sort -u "$tmp_matches" | while IFS= read -r rel_path; do [ -n "$rel_path" ] || continue src_path=$src_root/$rel_path if [ ! -e "$src_path" ] && [ ! -L "$src_path" ]; then # Path may already have been moved because an ancestor directory matched. continue fi dst_path=$out_root/$rel_path dst_parent=$(dirname "$dst_path") mkdir -p "$dst_parent" if [ -e "$dst_path" ] || [ -L "$dst_path" ]; then fail "destination already exists: $dst_path" fi mv "$src_path" "$dst_path" done # Clean empty directories left behind in the primary staging tree, but never # touch the internal depot namespace. find "$src_root" -depth -type d \ ! -path "$src_root" \ ! -path "$src_root/.depot" ! -path "$src_root/.depot/*" \ -empty -exec rmdir {} + 2>/dev/null || true "#; const SUBDESTDIR_SCRIPT: &str = r#"#!/bin/sh set -eu fail() { echo "subdestdir: $*" >&2 exit 1 } [ "$#" -eq 1 ] || fail "usage: subdestdir " [ "${DEPOT_OUTPUTS_DIR:-}" != "" ] || fail "DEPOT_OUTPUTS_DIR is not set" pkg=$1 case "$pkg" in ""|.|..|*/*) fail "invalid output package name: $pkg" ;; esac path=$DEPOT_OUTPUTS_DIR/$pkg mkdir -p "$path" printf '%s\n' "$path" "#; #[cfg(test)] mod tests { use super::shell_ident_suffix; #[test] fn shell_ident_suffix_normalizes_package_names() { assert_eq!(shell_ident_suffix("clang"), "CLANG"); assert_eq!(shell_ident_suffix("llvm-libgcc"), "LLVM_LIBGCC"); assert_eq!(shell_ident_suffix("3foo"), "_3FOO"); assert_eq!(shell_ident_suffix("foo.bar+baz"), "FOO_BAR_BAZ"); } }