feat: improve depot UX and packaging workflow

- add shared terminal UI helpers for colored info/warn/success logs and yes/no prompts
- add Python build backend (PEP 517/setup.py wheel build + install) and expose python in interactive spec creation
- expand spec/build flags (post_configure, make phase targets/dirs, configure_file, no_strip, no_compress_man)
- support split-output staging with internal .depot output dirs, shell helpers (haul/subdestdir), and per-output deps/provides overrides
- improve staging with ELF auto-strip and zstd manpage compression (with opt-out flags)
- enforce runtime deps before install and support legacy lifecycle hook script names
- improve manual source handling with files/urls lists and stricter validation
This commit is contained in:
2026-02-22 14:52:45 -06:00
parent 0c676f6743
commit d63ad03e98
28 changed files with 4639 additions and 412 deletions
+259
View File
@@ -0,0 +1,259 @@
//! 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<Self> {
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 <output-package> <path-pattern> [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 <output-package>"
[ "${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");
}
}