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:
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user