Implement interrupt handling for long-running commands and archive extraction

- Added an `interrupts` module to manage Ctrl-C signals and propagate them to child processes.
- Refactored archive extraction functions to check for interrupts and handle them gracefully.
- Introduced `copy_interruptibly` function to allow for interruptible copying of data.
- Updated various source files to utilize the new interrupt handling functions, ensuring that commands and archive extractions can be interrupted by the user.
- Added tests to verify the interrupt functionality during copying and tar extraction.
- Introduced a new Meson option for generating HTML CLI documentation.
- Enhanced the `PackageSpec` struct to support additional Rust LTO flags.
This commit is contained in:
2026-03-15 14:48:01 -05:00
parent 108c989695
commit a774c61e5a
23 changed files with 1117 additions and 145 deletions
Generated
+1 -1
View File
@@ -418,7 +418,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2"
[[package]]
name = "depot"
version = "0.22.1"
version = "0.23.0"
dependencies = [
"anyhow",
"ar",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "depot"
version = "0.22.1"
version = "0.23.0"
edition = "2024"
[lints.rust]
+2
View File
@@ -17,6 +17,8 @@ ldflags = ["-Wl,-O1"]
# Default LTOFLAGS and automatic injection into CFLAGS/CXXFLAGS/LDFLAGS.
ltoflags = ["-flto=auto"]
# Rust-specific LTO flags appended only to RUSTFLAGS when use_lto is enabled.
#RUSTLTOFLAGS = ["-Clinker-plugin-lto"]
use_lto = true
# CARCH short name (can be overridden per-package)
+20 -1
View File
@@ -1,6 +1,6 @@
project(
'depot',
version: '0.22.1',
version: '0.23.0',
meson_version: '>=0.60.0',
default_options: ['buildtype=release'],
)
@@ -10,6 +10,8 @@ fs = import('fs')
cargo = find_program('cargo', required: true)
find_prog = find_program('find', required: true)
sh = find_program('sh', required: true)
build_html_doc = get_option('build-html-doc')
cli_doc = find_program('cli_doc', required: build_html_doc)
src_root = meson.project_source_root()
build_root = meson.project_build_root()
@@ -72,6 +74,23 @@ depot_bin = custom_target(
install_dir: get_option('bindir'),
)
if build_html_doc
custom_target(
'depot-html-doc',
input: depot_bin,
output: 'depot.html',
command: [
cli_doc,
'--output-filename',
'@OUTPUT@',
join_paths(build_root, 'depot'),
],
build_by_default: true,
install: true,
install_dir: join_paths(get_option('datadir'), 'doc', meson.project_name()),
)
endif
test(
'cargo-test',
sh,
+6
View File
@@ -0,0 +1,6 @@
option(
'build-html-doc',
type: 'boolean',
value: false,
description: 'Generate HTML CLI documentation with cli_doc and install it',
)
+21 -20
View File
@@ -128,8 +128,7 @@ pub fn build(
configure_cmd.arg(expanded);
}
let status = configure_cmd
.status()
let status = crate::interrupts::command_status(&mut configure_cmd)
.with_context(|| format!("Failed to run configure in {}", build_dir.display()))?;
if !status.success() {
@@ -164,7 +163,7 @@ pub fn build(
crate::builder::prepare_tool_command(&mut make_cmd, &env_vars);
let status = make_cmd.status().with_context(|| {
let status = crate::interrupts::command_status(&mut make_cmd).with_context(|| {
format!("Failed to run {} in {}", make_exec, make_dir.display())
})?;
@@ -216,14 +215,15 @@ pub fn build(
crate::builder::prepare_tool_command(&mut test_cmd, &env_vars);
let test_targets_display = test_targets.join(" ");
let status = test_cmd.status().with_context(|| {
format!(
"Failed to run {} {} in {}",
make_exec,
test_targets_display,
test_dir.display()
)
})?;
let status =
crate::interrupts::command_status(&mut test_cmd).with_context(|| {
format!(
"Failed to run {} {} in {}",
make_exec,
test_targets_display,
test_dir.display()
)
})?;
if !status.success() {
anyhow::bail!(
"{} {} failed with status: {} (dir: {})",
@@ -344,15 +344,16 @@ pub fn build(
));
crate::builder::prepare_tool_command(&mut install_cmd, &install_env);
let status = install_cmd.status().with_context(|| {
format!(
"Failed to run {} {} for {} in {}",
make_exec,
install_targets.join(" "),
spec.package.name,
install_dir.display()
)
})?;
let status =
crate::interrupts::command_status(&mut install_cmd).with_context(|| {
format!(
"Failed to run {} {} for {} in {}",
make_exec,
install_targets.join(" "),
spec.package.name,
install_dir.display()
)
})?;
if !status.success() {
anyhow::bail!(
+117 -13
View File
@@ -63,6 +63,9 @@ pub fn build(
for arg in cmake_install_dir_args(flags) {
cmake_cmd.arg(arg);
}
for arg in cmake_lib32_target_args(flags, cross) {
cmake_cmd.arg(arg);
}
// Add toolchain file for cross-compilation
if let Some(ref tf) = toolchain_file {
@@ -90,7 +93,8 @@ pub fn build(
crate::builder::prepare_tool_command(&mut cmake_cmd, &env_vars);
let status = cmake_cmd.status().context("Failed to run cmake")?;
let status =
crate::interrupts::command_status(&mut cmake_cmd).context("Failed to run cmake")?;
if !status.success() {
anyhow::bail!("cmake configure failed");
}
@@ -117,8 +121,7 @@ pub fn build(
crate::builder::prepare_tool_command(&mut build_cmd, &env_vars);
let status = build_cmd
.status()
let status = crate::interrupts::command_status(&mut build_cmd)
.with_context(|| format!("Failed to run cmake build for {}", spec.package.name))?;
if !status.success() {
anyhow::bail!("cmake build failed");
@@ -139,12 +142,13 @@ pub fn build(
}
crate::builder::prepare_tool_command(&mut test_cmd, &env_vars);
let status = test_cmd.status().with_context(|| {
format!(
"Failed to run cmake build target(s) '{}' for {}",
joined, spec.package.name
)
})?;
let status =
crate::interrupts::command_status(&mut test_cmd).with_context(|| {
format!(
"Failed to run cmake build target(s) '{}' for {}",
joined, spec.package.name
)
})?;
if !status.success() {
anyhow::bail!("cmake test target(s) '{}' failed", joined);
}
@@ -157,8 +161,7 @@ pub fn build(
test_cmd.arg("--output-on-failure");
crate::builder::prepare_tool_command(&mut test_cmd, &env_vars);
let status = test_cmd
.status()
let status = crate::interrupts::command_status(&mut test_cmd)
.with_context(|| format!("Failed to run ctest for {}", spec.package.name))?;
if !status.success() {
anyhow::bail!("ctest failed");
@@ -203,8 +206,7 @@ pub fn build(
));
crate::builder::prepare_tool_command(&mut install_cmd, &install_env);
let status = install_cmd
.status()
let status = crate::interrupts::command_status(&mut install_cmd)
.with_context(|| format!("Failed to run cmake install for {}", spec.package.name))?;
if !status.success() {
if !install_targets.is_empty() {
@@ -378,6 +380,55 @@ fn cmake_install_dir_args(flags: &crate::package::BuildFlags) -> Vec<String> {
.collect()
}
fn cmake_lib32_target_args(
flags: &crate::package::BuildFlags,
cross: Option<&CrossConfig>,
) -> Vec<String> {
if !flags.lib32_variant {
return Vec::new();
}
let target = match lib32_target_triple(flags, cross) {
Some(target) => target,
None => return Vec::new(),
};
let arch = crate::cross::target_arch_from_triple(&target);
let defaults = [
("CMAKE_SYSTEM_PROCESSOR", arch.to_string()),
("CMAKE_C_COMPILER_TARGET", target.clone()),
("CMAKE_CXX_COMPILER_TARGET", target.clone()),
("CMAKE_ASM_COMPILER_TARGET", target),
];
defaults
.into_iter()
.filter(|(variable, _)| cmake_cache_entry_value(&flags.configure, variable).is_none())
.map(|(variable, value)| format!("-D{variable}={value}"))
.collect()
}
fn lib32_target_triple(
flags: &crate::package::BuildFlags,
cross: Option<&CrossConfig>,
) -> Option<String> {
let host = if let Some(cc_cfg) = cross {
Some(cc_cfg.host_triple().to_string())
} else if !flags.chost.trim().is_empty() {
Some(flags.chost.trim().to_string())
} else {
let detected = CrossConfig::build_triple();
if let Err(err) = &detected {
crate::log_warn!(
"Failed to detect native build triple for lib32 CMake target flags: {}",
err
);
}
detected.ok()
};
host.map(|host| crate::cross::lib32_target_triple(&host))
}
fn cmake_install_dir_value(prefix: &str, value: &str) -> String {
let trimmed_prefix = prefix.trim();
let trimmed_value = value.trim();
@@ -610,6 +661,59 @@ mod tests {
);
}
#[test]
fn test_cmake_lib32_target_args_include_compiler_target_defaults() {
let flags = BuildFlags {
lib32_variant: true,
chost: "x86_64-sfg-linux-gnu".into(),
..BuildFlags::default()
};
let args = cmake_lib32_target_args(&flags, None);
assert!(args.iter().any(|a| a == "-DCMAKE_SYSTEM_PROCESSOR=i686"));
assert!(
args.iter()
.any(|a| a == "-DCMAKE_C_COMPILER_TARGET=i686-sfg-linux-gnu")
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_CXX_COMPILER_TARGET=i686-sfg-linux-gnu")
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_ASM_COMPILER_TARGET=i686-sfg-linux-gnu")
);
}
#[test]
fn test_cmake_lib32_target_args_respect_explicit_overrides() {
let flags = BuildFlags {
lib32_variant: true,
chost: "x86_64-sfg-linux-gnu".into(),
configure: vec![
"-DCMAKE_C_COMPILER_TARGET=i686-custom-linux-gnu".into(),
"-DCMAKE_SYSTEM_PROCESSOR=i686".into(),
],
..BuildFlags::default()
};
let args = cmake_lib32_target_args(&flags, None);
assert!(
!args
.iter()
.any(|a| a.starts_with("-DCMAKE_C_COMPILER_TARGET="))
);
assert!(
!args
.iter()
.any(|a| a.starts_with("-DCMAKE_SYSTEM_PROCESSOR="))
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_CXX_COMPILER_TARGET=i686-sfg-linux-gnu")
);
}
#[test]
fn test_cmake_install_dir_args_respect_explicit_user_overrides() {
let flags = BuildFlags {
+1 -1
View File
@@ -131,7 +131,7 @@ pub fn build(
crate::builder::prepare_tool_command(&mut cmd, &env_vars);
// Run the command and include the OS error on spawn failures for clearer diagnostics
let status = cmd.status().map_err(|e| {
let status = crate::interrupts::command_status(&mut cmd).map_err(|e| {
anyhow::anyhow!(
"Failed to run build script {}: {}",
build_script.display(),
+2 -4
View File
@@ -48,8 +48,7 @@ pub fn build(
cmd.current_dir(src_dir);
crate::builder::prepare_tool_command(&mut cmd, &env_vars);
let status = cmd
.status()
let status = crate::interrupts::command_status(&mut cmd)
.with_context(|| format!("Failed to run build command: {}", cmd_str))?;
if !status.success() {
anyhow::bail!("Build command failed: {}", cmd_str);
@@ -89,8 +88,7 @@ pub fn build(
));
crate::builder::prepare_tool_command(&mut cmd, &install_env);
let status = cmd
.status()
let status = crate::interrupts::command_status(&mut cmd)
.with_context(|| format!("Failed to run install command: {}", cmd_str))?;
if !status.success() {
anyhow::bail!("Install command failed: {}", cmd_str);
+154 -8
View File
@@ -29,9 +29,12 @@ pub fn build(
// Environment variables
let env_vars = crate::builder::standard_build_env(spec, cross, true, export_compiler_flags);
// Generate cross file if cross-compiling
// Generate cross file if cross-compiling, or when the lib32 variant needs
// Meson to treat the build as x86 instead of the native x86_64 host.
let cross_file = if let Some(cc_cfg) = cross {
Some(cc_cfg.generate_meson_cross_file(&build_dir)?)
} else if flags.lib32_variant {
Some(generate_lib32_meson_cross_file(flags, &build_dir)?)
} else {
None
};
@@ -56,7 +59,8 @@ pub fn build(
crate::builder::prepare_tool_command(&mut meson_cmd, &env_vars);
let status = meson_cmd.status().context("Failed to run meson setup")?;
let status = crate::interrupts::command_status(&mut meson_cmd)
.context("Failed to run meson setup")?;
if !status.success() {
anyhow::bail!("meson setup failed");
}
@@ -76,8 +80,7 @@ pub fn build(
crate::builder::prepare_tool_command(&mut ninja_cmd, &env_vars);
let status = ninja_cmd
.status()
let status = crate::interrupts::command_status(&mut ninja_cmd)
.with_context(|| format!("Failed to run ninja for {}", spec.package.name))?;
if !status.success() {
anyhow::bail!("ninja build failed");
@@ -105,8 +108,7 @@ pub fn build(
crate::builder::prepare_tool_command(&mut test_cmd, &env_vars);
let status = test_cmd
.status()
let status = crate::interrupts::command_status(&mut test_cmd)
.with_context(|| format!("Failed to run meson test for {}", spec.package.name))?;
if !status.success() {
anyhow::bail!("meson test failed");
@@ -141,8 +143,7 @@ pub fn build(
));
crate::builder::prepare_tool_command(&mut install_cmd, &install_env);
let status = install_cmd
.status()
let status = crate::interrupts::command_status(&mut install_cmd)
.with_context(|| format!("Failed to run meson install for {}", spec.package.name))?;
if !status.success() {
anyhow::bail!("meson install failed");
@@ -258,6 +259,109 @@ fn meson_setup_args(
args
}
fn generate_lib32_meson_cross_file(
flags: &crate::package::BuildFlags,
build_dir: &Path,
) -> Result<PathBuf> {
let target = lib32_target_triple(flags);
let arch = crate::cross::target_arch_from_triple(&target);
let cpu_family = crate::cross::cpu_family_for_arch(arch);
let c = meson_binary_value(
&compiler_command_with_lib32_target(&flags.cc, &target),
"C compiler",
)?;
let cpp = meson_binary_value(
&compiler_command_with_lib32_target(&flags.cxx, &target),
"C++ compiler",
)?;
let ar = meson_binary_value(&command_words(&flags.ar), "archiver")?;
let mut content = format!(
"# Meson cross file for lib32 builds\n# Generated by depot for target: {target}\n\n[binaries]\nc = {c}\ncpp = {cpp}\nar = {ar}\n"
);
if !flags.ld.trim().is_empty() {
let ld = meson_binary_value(&command_words(&flags.ld), "linker")?;
content.push_str(&format!("ld = {ld}\n"));
}
content.push_str(&format!(
"\n[host_machine]\nsystem = 'linux'\ncpu_family = '{cpu_family}'\ncpu = '{arch}'\nendian = 'little'\n"
));
fs::create_dir_all(build_dir)?;
let cross_path = build_dir.join("lib32-cross-file.ini");
fs::write(&cross_path, content)
.with_context(|| format!("Failed to write {}", cross_path.display()))?;
Ok(cross_path)
}
fn lib32_target_triple(flags: &crate::package::BuildFlags) -> String {
let host = if !flags.chost.trim().is_empty() {
flags.chost.trim().to_string()
} else {
match CrossConfig::build_triple() {
Ok(triple) => triple,
Err(err) => {
crate::log_warn!(
"Failed to detect native build triple for lib32 Meson target file: {}",
err
);
"x86_64-unknown-linux-gnu".to_string()
}
}
};
crate::cross::lib32_target_triple(&host)
}
fn compiler_command_with_lib32_target(command: &str, target: &str) -> Vec<String> {
let mut parts = command_words(command);
if compiler_command_supports_target(&parts) && !compiler_command_has_target(&parts) {
parts.push(format!("--target={target}"));
}
parts
}
fn command_words(command: &str) -> Vec<String> {
command
.split_whitespace()
.filter(|part| !part.is_empty())
.map(ToString::to_string)
.collect()
}
fn compiler_command_supports_target(parts: &[String]) -> bool {
parts.first().is_some_and(|tool| {
Path::new(tool)
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.contains("clang"))
})
}
fn compiler_command_has_target(parts: &[String]) -> bool {
parts.iter().any(|part| {
part == "--target"
|| part == "-target"
|| part.starts_with("--target=")
|| part.starts_with("-target=")
})
}
fn meson_binary_value(parts: &[String], label: &str) -> Result<String> {
if parts.is_empty() {
anyhow::bail!("Missing {} command for lib32 Meson cross file", label);
}
let rendered = parts
.iter()
.map(|part| format!("'{}'", part.replace('\\', "\\\\").replace('\'', "\\'")))
.collect::<Vec<_>>();
if rendered.len() == 1 {
Ok(rendered[0].clone())
} else {
Ok(format!("[{}]", rendered.join(", ")))
}
}
/// Expand environment variables in a string (e.g., $DEPOT_SYSROOT)
fn expand_env_vars(input: &str) -> String {
let mut result = input.to_string();
@@ -439,6 +543,48 @@ mod tests {
assert!(!args.iter().any(|a| a == "-Dcpp_ld=ld.lld"));
}
#[test]
fn test_compiler_command_with_lib32_target_adds_clang_target() {
let parts = compiler_command_with_lib32_target("clang -m32", "i686-sfg-linux-gnu");
assert_eq!(
parts,
vec![
"clang".to_string(),
"-m32".to_string(),
"--target=i686-sfg-linux-gnu".to_string()
]
);
}
#[test]
fn test_compiler_command_with_lib32_target_skips_non_clang_compilers() {
let parts = compiler_command_with_lib32_target("gcc -m32", "i686-sfg-linux-gnu");
assert_eq!(parts, vec!["gcc".to_string(), "-m32".to_string()]);
}
#[test]
fn test_generate_lib32_meson_cross_file_sets_x86_host_machine() -> Result<()> {
let tmp = tempdir()?;
let flags = BuildFlags {
lib32_variant: true,
chost: "x86_64-sfg-linux-gnu".to_string(),
cc: "clang -m32".to_string(),
cxx: "clang++ -m32".to_string(),
ar: "llvm-ar".to_string(),
ld: "ld.lld".to_string(),
..BuildFlags::default()
};
let path = generate_lib32_meson_cross_file(&flags, tmp.path())?;
let content = fs::read_to_string(path)?;
assert!(content.contains("Generated by depot for target: i686-sfg-linux-gnu"));
assert!(content.contains("c = ['clang', '-m32', '--target=i686-sfg-linux-gnu']"));
assert!(content.contains("cpp = ['clang++', '-m32', '--target=i686-sfg-linux-gnu']"));
assert!(content.contains("cpu_family = 'x86'"));
assert!(content.contains("cpu = 'i686'"));
Ok(())
}
#[test]
fn test_resolve_build_dir_uses_flag() {
let flags = BuildFlags {
+40 -2
View File
@@ -194,12 +194,21 @@ fn compiler_flag_sets(
(cflags, cxxflags, ldflags, ltoflags)
}
fn rust_ltoflags(flags: &crate::package::BuildFlags) -> Vec<String> {
sanitize_flag_list(flags.rustltoflags.clone(), "build.flags.rustltoflags")
}
pub(crate) fn effective_rustflags(flags: &crate::package::BuildFlags) -> Vec<String> {
replaced_flags(
let mut rustflags = replaced_flags(
&flags.rustflags,
&flags.replace_rustflags,
"build.flags.rustflags",
)
);
let rust_ltoflags = rust_ltoflags(flags);
if flags.use_lto && !rust_ltoflags.is_empty() {
rustflags.extend(rust_ltoflags);
}
rustflags
}
pub fn standard_build_env(
@@ -224,6 +233,10 @@ pub fn standard_build_env(
if !ltoflags.is_empty() {
set_env_var(&mut env_vars, "LTOFLAGS", ltoflags.join(" "));
}
let rust_ltoflags = rust_ltoflags(flags);
if !rust_ltoflags.is_empty() {
set_env_var(&mut env_vars, "RUSTLTOFLAGS", rust_ltoflags.join(" "));
}
let ldflags = if !ldflags.is_empty() || !flags.libc.is_empty() {
if flags.libc.is_empty() {
@@ -717,6 +730,7 @@ mod tests {
let mut spec = mk_spec(vec!["-O2"], vec!["-Wl,--as-needed"]);
spec.build.flags.cxxflags = vec!["-O2".into()];
spec.build.flags.ltoflags = vec!["-flto=auto".into()];
spec.build.flags.rustltoflags = vec!["-Clinker-plugin-lto".into()];
spec.build.flags.use_lto = false;
let env = standard_build_env(&spec, None, true, true);
@@ -738,6 +752,12 @@ mod tests {
.any(|(k, v)| k == "LTOFLAGS" && v == "-flto=auto"),
"expected LTOFLAGS variable to be exported even when use_lto is false"
);
assert!(
env.iter()
.any(|(k, v)| k == "RUSTLTOFLAGS" && v == "-Clinker-plugin-lto"),
"expected RUSTLTOFLAGS variable to be exported even when use_lto is false"
);
assert_eq!(effective_rustflags(&spec.build.flags), Vec::<String>::new());
}
#[test]
@@ -778,6 +798,24 @@ mod tests {
assert_eq!(effective_rustflags(&flags), vec!["-C", "opt-level=2"]);
}
#[test]
fn test_effective_rustflags_appends_rustltoflags_when_enabled() {
let mut flags = BuildFlags::default();
flags.rustflags = vec!["-C".into(), "opt-level=3".into()];
flags.rustltoflags = vec!["-Clinker-plugin-lto".into(), "-Cembed-bitcode=yes".into()];
flags.use_lto = true;
assert_eq!(
effective_rustflags(&flags),
vec![
"-C",
"opt-level=3",
"-Clinker-plugin-lto",
"-Cembed-bitcode=yes"
]
);
}
#[test]
fn test_standard_build_env_passthrough_does_not_override_default_vars() {
let mut spec = mk_spec(Vec::new(), Vec::new());
+2 -2
View File
@@ -267,7 +267,7 @@ 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() {
match crate::interrupts::command_status(cmd) {
Ok(status) => Ok(status),
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => {
let Some(script) = resolved_script_path(cmd) else {
@@ -296,7 +296,7 @@ fn command_status_with_sh_fallback(cmd: &mut Command) -> std::io::Result<std::pr
}
}
}
fallback.status()
crate::interrupts::command_status(&mut fallback)
}
Err(err) => Err(err),
}
+6 -9
View File
@@ -273,8 +273,7 @@ fn build_wheel_setup_py(
.arg(dist_dir);
crate::builder::prepare_tool_command(&mut cmd, &env_vars.to_vec());
let status = cmd
.status()
let status = crate::interrupts::command_status(&mut cmd)
.with_context(|| format!("Failed to run setup.py in {}", src_dir.display()))?;
if !status.success() {
bail!("setup.py bdist_wheel failed with status {}", status);
@@ -315,25 +314,23 @@ fn build_wheel_pep517(
);
crate::builder::prepare_command(&mut cmd, &build_env);
let output = cmd.output().with_context(|| {
let status = crate::interrupts::command_status(&mut cmd).with_context(|| {
format!(
"Failed to run python3 for PEP 517 build in {}",
src_dir.display()
)
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if !status.success() {
let requires = if cfg.requires.is_empty() {
"(none declared)".to_string()
} else {
cfg.requires.join(", ")
};
bail!(
"PEP 517 wheel build failed with status {}. Backend: {}. Declared build requirements: {}. stderr: {}",
output.status,
"PEP 517 wheel build failed with status {}. Backend: {}. Declared build requirements: {}",
status,
cfg.backend,
requires,
stderr.trim()
requires
);
}
+1 -2
View File
@@ -102,8 +102,7 @@ pub fn build(
// Set environment
crate::builder::prepare_tool_command(&mut cargo_cmd, &env_vars);
let status = cargo_cmd
.status()
let status = crate::interrupts::command_status(&mut cargo_cmd)
.with_context(|| format!("Failed to run cargo build for {}", spec.package.name))?;
if !status.success() {
anyhow::bail!("cargo build failed");
+214 -25
View File
@@ -9,14 +9,11 @@ use crate::{
use anyhow::{Context, Result};
use git2::Direction;
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
use signal_hook::consts::SIGINT;
use std::cmp::Ordering;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::fs;
use std::io::IsTerminal;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
use url::Url;
use walkdir::WalkDir;
@@ -105,6 +102,7 @@ fn should_delegate_live_rootfs_installs(rootfs: &Path) -> bool {
const DEPOT_INSTALL_CONTEXT_ENV: &str = "DEPOT_INSTALL_CONTEXT";
const INSTALL_CONTEXT_UPDATE: &str = "update";
const INSTALL_CONTEXT_PLANNED: &str = "planned";
const DEPOT_PACKAGE_NAME: &str = "depot";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum InstallInvocationContext {
@@ -132,6 +130,55 @@ fn install_test_deps_enabled(cli_test_deps: bool, config: &config::Config) -> bo
cli_test_deps || config.install_test_deps
}
fn current_argv0() -> String {
std::env::args_os()
.next()
.filter(|arg| !arg.is_empty())
.map(|arg| PathBuf::from(arg).display().to_string())
.or_else(|| {
std::env::current_exe()
.ok()
.map(|path| path.display().to_string())
})
.unwrap_or_else(|| DEPOT_PACKAGE_NAME.to_string())
}
fn is_explicit_depot_self_update_request(packages: &[String]) -> bool {
packages.len() == 1
&& packages
.first()
.is_some_and(|package| package == DEPOT_PACKAGE_NAME)
}
fn ensure_depot_self_update_not_required(config: &config::Config, rootfs: &Path) -> Result<()> {
let db_path = config.installed_db_path(rootfs);
if db::get_package_version(&db_path, DEPOT_PACKAGE_NAME)
.with_context(|| {
format!(
"Failed to query installed package database at {}",
db_path.display()
)
})?
.is_none()
{
return Ok(());
}
let requested = [DEPOT_PACKAGE_NAME.to_string()];
let updates = collect_update_candidates(config, rootfs, &requested)
.context("Failed to check for pending depot self-update")?;
if updates.is_empty() {
return Ok(());
}
anyhow::bail!(
"An update for '{}' is available. Run '{} update {}' before continuing.",
DEPOT_PACKAGE_NAME,
current_argv0(),
DEPOT_PACKAGE_NAME
);
}
fn maybe_reexec_with_sudo(cli: &Cli) -> Result<bool> {
if !should_reexec_with_sudo(cli) {
return Ok(false);
@@ -235,7 +282,7 @@ 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() {
match crate::interrupts::command_status(cmd) {
Ok(status) => Ok(status),
Err(err)
if err.kind() == std::io::ErrorKind::PermissionDenied
@@ -264,7 +311,7 @@ fn command_status_with_sh_fallback(
}
}
}
fallback.status()
crate::interrupts::command_status(&mut fallback)
}
Err(err) => Err(err),
}
@@ -295,27 +342,21 @@ fn run_child_install_command(
}
#[derive(Clone)]
struct InterruptWatcher {
interrupted: Arc<AtomicBool>,
}
struct InterruptWatcher;
impl InterruptWatcher {
fn install() -> Result<Self> {
let interrupted = Arc::new(AtomicBool::new(false));
signal_hook::flag::register(SIGINT, interrupted.clone())
.context("Failed to register Ctrl-C handler")?;
Ok(Self { interrupted })
crate::interrupts::install()?;
crate::interrupts::reset();
Ok(Self)
}
fn was_interrupted(&self) -> bool {
self.interrupted.load(AtomicOrdering::Relaxed)
crate::interrupts::was_interrupted()
}
fn check(&self) -> Result<()> {
if self.was_interrupted() {
anyhow::bail!("Interrupted by Ctrl-C");
}
Ok(())
crate::interrupts::check()
}
}
@@ -533,6 +574,7 @@ fn current_process_env_vars() -> Vec<(String, String)> {
"PYTHONNOUSERSITE",
"RANLIB",
"RUSTFLAGS",
"RUSTLTOFLAGS",
"SETUPTOOLS_USE_DISTUTILS",
"STRIP",
];
@@ -767,6 +809,7 @@ fn load_package_archive_into_staging(
archive_path.display()
)
})? {
crate::interrupts::check()?;
let mut entry = entry.with_context(|| {
format!(
"Failed to read archive entry from {}",
@@ -800,7 +843,7 @@ fn load_package_archive_into_staging(
.with_context(|| format!("Failed to read zstd stream {}", archive_path.display()))?;
let mut archive = tar::Archive::new(zstd_decoder);
archive.set_preserve_permissions(true);
archive.unpack(&extract_dir).with_context(|| {
crate::interrupts::unpack_tar_archive(&mut archive, &extract_dir).with_context(|| {
format!(
"Failed to extract package archive {} into {}",
archive_path.display(),
@@ -836,7 +879,7 @@ fn extract_package_archive_to_staging(
.with_context(|| format!("Failed to read zstd stream {}", archive_path.display()))?;
let mut archive = tar::Archive::new(zstd_decoder);
archive.set_preserve_permissions(true);
archive.unpack(&extract_dir).with_context(|| {
crate::interrupts::unpack_tar_archive(&mut archive, &extract_dir).with_context(|| {
format!(
"Failed to extract package archive {} into {}",
archive_path.display(),
@@ -2514,8 +2557,6 @@ fn run_update_command(
config: &config::Config,
options: UpdateCommandOptions<'_>,
) -> Result<()> {
sync_source_repositories_for_update(config)?;
let updates = collect_update_candidates(config, options.rootfs, packages)?;
if updates.is_empty() {
ui::info("All installed packages are up to date.");
@@ -3487,8 +3528,7 @@ fn execute_install_plan_with_child_commands(
}
cmd.arg("install").arg(path);
let status = cmd
.status()
let status = crate::interrupts::command_status(&mut cmd)
.context("Failed to spawn planned install step")?;
if !status.success() {
anyhow::bail!("Planned install step for '{}' failed", step.package);
@@ -4080,6 +4120,8 @@ fn run_direct_install_request(
}
pub fn run(cli: Cli) -> Result<()> {
crate::interrupts::install()?;
crate::interrupts::reset();
ui::set_assume_yes(command_assume_yes(&cli.command));
if maybe_reexec_with_sudo(&cli)? {
return Ok(());
@@ -4126,6 +4168,7 @@ pub fn run(cli: Cli) -> Result<()> {
// Load configuration early so we can use configured repos/paths.
let config = config::Config::for_rootfs(&rootfs);
ensure_depot_self_update_not_required(&config, &rootfs)?;
let install_test_deps = install_test_deps_enabled(cli_test_deps, &config);
let mut planned_targets = Vec::new();
let mut planned_spec_paths = Vec::new();
@@ -4266,11 +4309,11 @@ pub fn run(cli: Cli) -> Result<()> {
let dry_run = build_exec_args.dry_run;
let cli_lib32_only = lib32_args.lib32_only;
warn_if_running_as_root_for_build("build", &rootfs);
let config = config::Config::for_rootfs(&rootfs);
ensure_depot_self_update_not_required(&config, &rootfs)?;
let spec_path = spec.or(spec_pos).context("No spec file provided")?;
ui::info(format!("Building package from: {}", spec_path.display()));
let mut pkg_spec = package::PackageSpec::from_file(&spec_path)?;
let config = config::Config::for_rootfs(&rootfs);
let install_test_deps = install_test_deps_enabled(cli_test_deps, &config);
let interrupt_watcher = if cleanup_deps {
Some(InterruptWatcher::install()?)
@@ -4783,6 +4826,10 @@ pub fn run(cli: Cli) -> Result<()> {
let clean = build_exec_args.clean;
let dry_run = build_exec_args.dry_run;
let config = config::Config::for_rootfs(&rootfs);
sync_source_repositories_for_update(&config)?;
if !is_explicit_depot_self_update_request(&packages) {
ensure_depot_self_update_not_required(&config, &rootfs)?;
}
run_update_command(
&packages,
&config,
@@ -5422,6 +5469,57 @@ mod tests {
spec_dir: PathBuf::from("."),
}
}
fn register_installed_test_package(
config: &config::Config,
rootfs: &Path,
name: &str,
version: &str,
) -> Result<()> {
let mut spec = test_package_spec(package::BuildType::Bin, None, &[]);
spec.package.name = name.to_string();
spec.package.version = version.to_string();
let dest = rootfs.join("dest").join(name);
fs::create_dir_all(dest.join("usr/bin"))?;
fs::write(dest.join("usr/bin").join(name), name)?;
db::register_package(&config.installed_db_path(rootfs), &spec, &dest)?;
Ok(())
}
fn write_test_repo_spec(path: &Path, name: &str, version: &str) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(
path,
format!(
r#"[package]
name = "{name}"
version = "{version}"
revision = 1
description = "{name}"
homepage = "https://example.test/{name}"
license = "MIT"
[[source]]
url = "https://example.test/{name}-{version}.tar.gz"
sha256 = "skip"
extract_dir = "{name}-{version}"
[build]
type = "custom"
[dependencies]
build = []
runtime = []
optional = []
"#
),
)
.with_context(|| format!("Failed to write {}", path.display()))?;
Ok(())
}
use anyhow::Context;
use std::io::Write;
@@ -6154,6 +6252,97 @@ optional = []
Ok(())
}
#[test]
fn explicit_depot_self_update_request_requires_only_depot() {
assert!(is_explicit_depot_self_update_request(&[
DEPOT_PACKAGE_NAME.to_string()
]));
assert!(!is_explicit_depot_self_update_request(&[]));
assert!(!is_explicit_depot_self_update_request(&["pkg".to_string()]));
assert!(!is_explicit_depot_self_update_request(&[
DEPOT_PACKAGE_NAME.to_string(),
"pkg".to_string()
]));
}
#[test]
fn depot_self_update_check_blocks_when_update_is_available() -> Result<()> {
let temp = tempfile::tempdir().context("Failed to create temp dir")?;
let rootfs = temp.path().join("rootfs");
let repo_clones = temp.path().join("repos");
let build_dir = temp.path().join("build");
let db_dir = rootfs.join("var/lib/depot");
fs::create_dir_all(&db_dir)?;
fs::create_dir_all(&repo_clones)?;
fs::create_dir_all(&build_dir)?;
let mut config = config::Config::for_rootfs(&rootfs);
config.repo_clone_dir = repo_clones.clone();
config.build_dir = build_dir;
config.db_dir = db_dir;
config.repo_settings.prefer_binary = false;
config.binary_repos.clear();
config.source_repos.insert(
"core".into(),
config::SourceRepo {
url: "https://example.test/core.git".into(),
enabled: true,
priority: 0,
subdirs: Vec::new(),
},
);
register_installed_test_package(&config, &rootfs, DEPOT_PACKAGE_NAME, "1.0.0")?;
write_test_repo_spec(
&repo_clones.join("core").join("depot.toml"),
DEPOT_PACKAGE_NAME,
"1.1.0",
)?;
let err = ensure_depot_self_update_not_required(&config, &rootfs)
.expect_err("outdated depot should block command execution");
assert!(err.to_string().contains("update depot"));
Ok(())
}
#[test]
fn depot_self_update_check_allows_when_depot_is_current() -> Result<()> {
let temp = tempfile::tempdir().context("Failed to create temp dir")?;
let rootfs = temp.path().join("rootfs");
let repo_clones = temp.path().join("repos");
let build_dir = temp.path().join("build");
let db_dir = rootfs.join("var/lib/depot");
fs::create_dir_all(&db_dir)?;
fs::create_dir_all(&repo_clones)?;
fs::create_dir_all(&build_dir)?;
let mut config = config::Config::for_rootfs(&rootfs);
config.repo_clone_dir = repo_clones.clone();
config.build_dir = build_dir;
config.db_dir = db_dir;
config.repo_settings.prefer_binary = false;
config.binary_repos.clear();
config.source_repos.insert(
"core".into(),
config::SourceRepo {
url: "https://example.test/core.git".into(),
enabled: true,
priority: 0,
subdirs: Vec::new(),
},
);
register_installed_test_package(&config, &rootfs, DEPOT_PACKAGE_NAME, "1.1.0")?;
write_test_repo_spec(
&repo_clones.join("core").join("depot.toml"),
DEPOT_PACKAGE_NAME,
"1.1.0",
)?;
ensure_depot_self_update_not_required(&config, &rootfs)?;
Ok(())
}
#[test]
fn collect_missing_update_dependencies_skips_planned_provides_and_installed_deps() -> Result<()>
{
+59 -22
View File
@@ -119,7 +119,7 @@ set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
"#,
self.prefix,
self.target_arch(),
target_arch_from_triple(&self.prefix),
self.cc,
self.cxx,
self.ar,
@@ -169,8 +169,8 @@ endian = 'little'
self.objcopy,
self.objdump,
self.readelf,
self.cpu_family(),
self.target_arch(),
cpu_family_for_arch(target_arch_from_triple(&self.prefix)),
target_arch_from_triple(&self.prefix),
);
fs::create_dir_all(build_dir)?;
@@ -179,28 +179,65 @@ endian = 'little'
Ok(cross_path)
}
}
/// Extract target architecture from prefix
fn target_arch(&self) -> &str {
self.prefix.split('-').next().unwrap_or("unknown")
/// Extract the leading architecture component from a target triple.
pub fn target_arch_from_triple(triple: &str) -> &str {
triple.split('-').next().unwrap_or("unknown")
}
/// Return the Meson CPU family for an architecture token.
pub fn cpu_family_for_arch(arch: &str) -> &str {
match arch {
"x86_64" | "amd64" => "x86_64",
"i686" | "i586" | "i486" | "i386" => "x86",
"aarch64" | "arm64" => "aarch64",
"arm" | "armv7" | "armv7l" => "arm",
"riscv64" => "riscv64",
"riscv32" => "riscv32",
"powerpc64" | "ppc64" => "ppc64",
"powerpc" | "ppc" => "ppc",
"mips64" => "mips64",
"mips" => "mips",
_ => arch,
}
}
/// Get CPU family for Meson
fn cpu_family(&self) -> &str {
let arch = self.target_arch();
match arch {
"x86_64" | "amd64" => "x86_64",
"i686" | "i586" | "i486" | "i386" => "x86",
"aarch64" | "arm64" => "aarch64",
"arm" | "armv7" | "armv7l" => "arm",
"riscv64" => "riscv64",
"riscv32" => "riscv32",
"powerpc64" | "ppc64" => "ppc64",
"powerpc" | "ppc" => "ppc",
"mips64" => "mips64",
"mips" => "mips",
_ => arch,
}
/// Derive the lib32 target triple while preserving the explicit x86 variant.
pub fn lib32_target_triple(host: &str) -> String {
let mut parts = host.splitn(2, '-');
let arch = parts.next().unwrap_or(host);
let rest = parts.next();
let lib32_arch = match arch {
"x86_64" | "amd64" => "i686",
"i686" | "i586" | "i486" | "i386" => arch,
_ => arch,
};
match rest {
Some(rest) if !rest.is_empty() => format!("{lib32_arch}-{rest}"),
_ => lib32_arch.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::lib32_target_triple;
#[test]
fn lib32_target_triple_preserves_explicit_x86_variant() {
assert_eq!(
lib32_target_triple("x86_64-sfg-linux-gnu"),
"i686-sfg-linux-gnu"
);
assert_eq!(
lib32_target_triple("i686-sfg-linux-gnu"),
"i686-sfg-linux-gnu"
);
assert_eq!(
lib32_target_triple("i586-sfg-linux-gnu"),
"i586-sfg-linux-gnu"
);
}
}
+273
View File
@@ -0,0 +1,273 @@
use anyhow::{Context, Result, bail};
use signal_hook::consts::SIGINT;
use std::fs;
use std::io::{self, ErrorKind, Read, Write};
use std::path::Path;
use std::process::{Child, Command, ExitStatus};
use std::sync::Once;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::{Duration, Instant};
static INTERRUPTED: AtomicBool = AtomicBool::new(false);
static INSTALL_HANDLER: Once = Once::new();
static INSTALL_ERROR: OnceLock<String> = OnceLock::new();
pub(crate) fn install() -> Result<()> {
INSTALL_HANDLER.call_once(|| {
if let Err(err) = unsafe {
signal_hook::low_level::register(SIGINT, || {
INTERRUPTED.store(true, Ordering::Relaxed);
})
} {
let _ = INSTALL_ERROR.set(err.to_string());
};
});
if let Some(err) = INSTALL_ERROR.get() {
bail!("Failed to register Ctrl-C handler: {}", err);
}
Ok(())
}
pub(crate) fn reset() {
INTERRUPTED.store(false, Ordering::Relaxed);
}
pub(crate) fn was_interrupted() -> bool {
INTERRUPTED.load(Ordering::Relaxed)
}
pub(crate) fn check() -> Result<()> {
install()?;
check_with(was_interrupted)
}
pub(crate) fn command_status(cmd: &mut Command) -> io::Result<ExitStatus> {
install().map_err(io::Error::other)?;
configure_child_process_group(cmd);
let mut child = cmd.spawn()?;
wait_for_child_with(&mut child, was_interrupted)
}
pub(crate) fn copy_interruptibly<R: Read, W: Write>(
reader: &mut R,
writer: &mut W,
) -> io::Result<u64> {
install().map_err(io::Error::other)?;
copy_interruptibly_with(reader, writer, was_interrupted)
}
pub(crate) fn unpack_tar_archive<R: Read>(
archive: &mut tar::Archive<R>,
dest: &Path,
) -> Result<()> {
install()?;
unpack_tar_archive_with(archive, dest, was_interrupted)
}
fn check_with(interrupted: impl Fn() -> bool) -> Result<()> {
if interrupted() {
bail!("Interrupted by Ctrl-C");
}
Ok(())
}
fn copy_interruptibly_with<R: Read, W: Write>(
reader: &mut R,
writer: &mut W,
interrupted: impl Fn() -> bool,
) -> io::Result<u64> {
let mut copied = 0u64;
let mut buf = [0u8; 64 * 1024];
loop {
if interrupted() {
return Err(io::Error::new(
ErrorKind::Interrupted,
"Interrupted by Ctrl-C",
));
}
let n = reader.read(&mut buf)?;
if n == 0 {
return Ok(copied);
}
writer.write_all(&buf[..n])?;
copied += n as u64;
}
}
fn unpack_tar_archive_with<R: Read>(
archive: &mut tar::Archive<R>,
dest: &Path,
interrupted: impl Fn() -> bool,
) -> Result<()> {
fs::create_dir_all(dest)
.with_context(|| format!("Failed to create extraction dir {}", dest.display()))?;
for entry in archive
.entries()
.context("Failed to iterate tar archive entries")?
{
check_with(&interrupted)?;
let mut entry = entry.context("Failed to read tar archive entry")?;
let path = entry
.path()
.context("Failed to inspect tar archive entry path")?
.into_owned();
if !entry
.unpack_in(dest)
.with_context(|| format!("Failed to unpack tar archive entry {}", path.display()))?
{
bail!(
"Refusing to extract unsafe tar archive entry outside destination: {}",
path.display()
);
}
}
Ok(())
}
#[cfg(unix)]
fn configure_child_process_group(cmd: &mut Command) {
use std::os::unix::process::CommandExt;
// Put the child in its own process group so Ctrl-C can be forwarded to the
// whole build/extract pipeline without killing the depot parent process.
unsafe {
cmd.pre_exec(|| {
if nix::libc::setpgid(0, 0) != 0 {
return Err(io::Error::last_os_error());
}
Ok(())
});
}
}
#[cfg(not(unix))]
fn configure_child_process_group(_cmd: &mut Command) {}
fn wait_for_child_with(
child: &mut Child,
interrupted: impl Fn() -> bool,
) -> io::Result<ExitStatus> {
let mut interrupted_at: Option<Instant> = None;
let mut sent_term = false;
let mut sent_kill = false;
loop {
if let Some(status) = child.try_wait()? {
return Ok(status);
}
if interrupted() {
if interrupted_at.is_none() {
interrupted_at = Some(Instant::now());
signal_child_process_group(child, nix::libc::SIGINT);
} else if interrupted_at
.is_some_and(|started| started.elapsed() >= Duration::from_secs(2) && !sent_term)
{
sent_term = true;
signal_child_process_group(child, nix::libc::SIGTERM);
} else if interrupted_at
.is_some_and(|started| started.elapsed() >= Duration::from_secs(4) && !sent_kill)
{
sent_kill = true;
signal_child_process_group(child, nix::libc::SIGKILL);
}
}
thread::sleep(Duration::from_millis(50));
}
}
#[cfg(unix)]
fn signal_child_process_group(child: &Child, signal: i32) {
let pgid = -(child.id() as i32);
let rc = unsafe { nix::libc::kill(pgid, signal) };
if rc == 0 {
return;
}
let err = io::Error::last_os_error();
if err.raw_os_error() != Some(nix::libc::ESRCH) {
crate::log_warn!(
"Failed to forward signal {} to child process group {}: {}",
signal,
child.id(),
err
);
}
}
#[cfg(not(unix))]
fn signal_child_process_group(_child: &Child, _signal: i32) {}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
use std::sync::Arc;
#[test]
fn copy_interruptibly_returns_interrupted_error() {
let mut reader = Cursor::new(b"hello".to_vec());
let mut writer = Vec::new();
let interrupted = AtomicBool::new(true);
let err = copy_interruptibly_with(&mut reader, &mut writer, || {
interrupted.load(Ordering::Relaxed)
})
.expect_err("copy should stop once interrupted");
assert_eq!(err.kind(), ErrorKind::Interrupted);
}
#[test]
fn unpack_tar_archive_stops_when_interrupted() {
let mut tar_data = Vec::new();
{
let mut builder = tar::Builder::new(&mut tar_data);
let mut header = tar::Header::new_gnu();
header.set_path("hello.txt").unwrap();
header.set_size(5);
header.set_mode(0o644);
header.set_cksum();
builder
.append(&header, Cursor::new(b"hello".to_vec()))
.unwrap();
builder.finish().unwrap();
}
let temp = tempfile::tempdir().unwrap();
let mut archive = tar::Archive::new(Cursor::new(tar_data));
let interrupted = AtomicBool::new(true);
let err = unpack_tar_archive_with(&mut archive, temp.path(), || {
interrupted.load(Ordering::Relaxed)
})
.expect_err("tar unpack should stop once interrupted");
assert!(err.to_string().contains("Interrupted by Ctrl-C"));
}
#[test]
#[cfg(unix)]
fn command_status_interrupts_child_process() {
use std::os::unix::process::ExitStatusExt;
let interrupted = Arc::new(AtomicBool::new(false));
let trigger_flag = interrupted.clone();
let trigger = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(100));
trigger_flag.store(true, Ordering::Relaxed);
});
let start = Instant::now();
let mut cmd = Command::new("sh");
cmd.arg("-c").arg("sleep 10");
configure_child_process_group(&mut cmd);
let mut child = cmd.spawn().expect("sleep command should spawn");
let status = wait_for_child_with(&mut child, || interrupted.load(Ordering::Relaxed))
.expect("sleep command should be interruptible");
trigger.join().unwrap();
assert!(start.elapsed() < Duration::from_secs(3));
assert_eq!(status.signal(), Some(SIGINT));
}
}
+1
View File
@@ -12,6 +12,7 @@ mod deps;
mod fakeroot;
mod index;
mod install;
mod interrupts;
mod locking;
mod metadata_time;
mod package;
+13
View File
@@ -942,6 +942,19 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
),
);
}
if !spec.build.flags.rustltoflags.is_empty() {
flags_tbl.insert(
"rustltoflags".into(),
Value::Array(
spec.build
.flags
.rustltoflags
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
if !spec.build.flags.keep.is_empty() {
flags_tbl.insert(
"keep".into(),
+43
View File
@@ -472,6 +472,17 @@ impl PackageSpec {
self.build.flags.ltoflags = vec![s.to_string()];
}
}
"rustltoflags" | "rust_ltoflags" | "rust-ltoflags" => {
if let Some(arr) = v.as_array() {
self.build.flags.rustltoflags = arr
.iter()
.filter_map(|x| x.as_str())
.map(String::from)
.collect();
} else if let Some(s) = v.as_str() {
self.build.flags.rustltoflags = vec![s.to_string()];
}
}
"replace_ltoflags" | "replace_lto-flags" | "replace_lto_flags" => {
if let Some(arr) = v.as_array() {
self.build.flags.replace_ltoflags = arr
@@ -1067,6 +1078,18 @@ impl PackageSpec {
}
}
}
"rustltoflags" | "rust_ltoflags" | "rust-ltoflags" => {
for v in values {
if let Some(arr) = v.as_array() {
self.build
.flags
.rustltoflags
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
} else if let Some(s) = v.as_str() {
self.build.flags.rustltoflags.push(s.to_string());
}
}
}
"replace_ltoflags" | "replace_lto-flags" | "replace_lto_flags" => {
for v in values {
if let Some(arr) = v.as_array() {
@@ -2316,6 +2339,7 @@ make_dirs = ["lib"]
make_test_dirs = ["tests"]
make_install_dirs = ["lib"]
ltoflags = ["-flto=auto"]
RUSTLTOFLAGS = ["-Clinker-plugin-lto"]
replace_ltoflags = ["auto=>thin"]
rustflags = ["-C", "debuginfo=2"]
replace_rustflags = ["debuginfo=2=>opt-level=2"]
@@ -2518,6 +2542,12 @@ post_configure = ["echo configured"]
.ltoflags
.contains(&"-flto=auto".to_string())
);
assert!(
spec.build
.flags
.rustltoflags
.contains(&"-Clinker-plugin-lto".to_string())
);
assert!(
spec.build
.flags
@@ -3868,6 +3898,18 @@ pub struct BuildFlags {
deserialize_with = "deserialize_string_or_array"
)]
pub ltoflags: Vec<String>,
/// Rust LTO flags exported to `RUSTLTOFLAGS`.
///
/// When `use_lto` is true (default), these flags are also appended to
/// `RUSTFLAGS`.
#[serde(
default,
alias = "rust-ltoflags",
alias = "rust_ltoflags",
alias = "RUSTLTOFLAGS",
deserialize_with = "deserialize_string_or_array"
)]
pub rustltoflags: Vec<String>,
/// Ordered replacement rules applied to `ltoflags` before export/injection.
#[serde(
default,
@@ -4236,6 +4278,7 @@ impl Default for BuildFlags {
ldflags: Vec::new(),
replace_ldflags: Vec::new(),
ltoflags: Vec::new(),
rustltoflags: Vec::new(),
replace_ltoflags: Vec::new(),
keep: Vec::new(),
split_docs: false,
+123 -11
View File
@@ -9,6 +9,7 @@ use lzma_rust2::{LzipReader, LzmaReader};
use std::fs::{self, File};
use std::io::{Cursor, Read, Write};
use std::os::unix::fs as unix_fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use tempfile::{NamedTempFile, tempdir};
@@ -42,6 +43,7 @@ pub fn extract_archive(
source: &Source,
build_dir: &Path,
) -> Result<PathBuf> {
crate::interrupts::install()?;
let extract_dir_name = spec.expand_vars(&source.extract_dir);
let extract_path = build_dir.join(&extract_dir_name);
@@ -157,7 +159,7 @@ fn extract_zip(path: &Path, dest: &Path) -> Result<()> {
let tmp = tempdir()?;
let file = File::open(path)?;
let mut archive = zip::ZipArchive::new(file)?;
archive.extract(tmp.path())?;
extract_zip_archive(&mut archive, tmp.path())?;
finalize_extracted_tree(tmp.path(), dest)
}
@@ -190,6 +192,20 @@ fn extract_tar_compress(path: &Path, dest: &Path) -> Result<()> {
let mut child = Command::new("gzip");
child.arg("-cd").arg(path);
child.stdout(Stdio::piped());
crate::interrupts::check()?;
// Run gzip in its own process group so Ctrl-C can interrupt decompression too.
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
unsafe {
child.pre_exec(|| {
if nix::libc::setpgid(0, 0) != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
}
let mut child = child.spawn().with_context(|| {
format!(
"Failed to spawn gzip for .Z decompression: {}",
@@ -201,13 +217,12 @@ fn extract_tar_compress(path: &Path, dest: &Path) -> Result<()> {
.take()
.context("Failed to capture gzip stdout")?;
let mut archive = tar::Archive::new(stdout);
archive
.unpack(tmp.path())
.with_context(|| format!("Failed to unpack .tar.Z archive {}", path.display()))?;
let unpack_result = crate::interrupts::unpack_tar_archive(&mut archive, tmp.path())
.with_context(|| format!("Failed to unpack .tar.Z archive {}", path.display()));
drop(archive);
let status = child
.wait()
let status = wait_for_child_interruptibly(&mut child)
.with_context(|| format!("Failed waiting for gzip on {}", path.display()))?;
unpack_result?;
if !status.success() {
bail!("gzip failed while decompressing {}", path.display());
}
@@ -224,7 +239,7 @@ fn extract_cpio(path: &Path, dest: &Path) -> Result<()> {
fn extract_tar_reader<R: Read>(reader: R, dest: &Path) -> Result<()> {
let tmp = tempdir()?;
let mut archive = tar::Archive::new(reader);
archive.unpack(tmp.path())?;
crate::interrupts::unpack_tar_archive(&mut archive, tmp.path())?;
finalize_extracted_tree(tmp.path(), dest)
}
@@ -281,7 +296,7 @@ fn extract_gz_file(path: &Path, dest: &Path) -> Result<()> {
fs::create_dir_all(dest)?;
let mut out = File::create(dest.join(out_name))?;
std::io::copy(&mut decoder, &mut out)?;
crate::interrupts::copy_interruptibly(&mut decoder, &mut out)?;
Ok(())
}
@@ -302,7 +317,7 @@ fn extract_xz_file(path: &Path, dest: &Path) -> Result<()> {
fs::create_dir_all(dest)?;
let mut out = File::create(dest.join(out_name))?;
std::io::copy(&mut decoder, &mut out)?;
crate::interrupts::copy_interruptibly(&mut decoder, &mut out)?;
Ok(())
}
@@ -323,7 +338,7 @@ fn extract_zst_file(path: &Path, dest: &Path) -> Result<()> {
fs::create_dir_all(dest)?;
let mut out = File::create(dest.join(out_name))?;
std::io::copy(&mut decoder, &mut out)?;
crate::interrupts::copy_interruptibly(&mut decoder, &mut out)?;
Ok(())
}
@@ -333,13 +348,14 @@ fn extract_deb(path: &Path, dest: &Path) -> Result<()> {
let mut ar = ar::Archive::new(file);
while let Some(entry_result) = ar.next_entry() {
crate::interrupts::check()?;
let mut entry = entry_result?;
let id = String::from_utf8_lossy(entry.header().identifier()).to_string();
let lower = id.to_ascii_lowercase();
if lower.starts_with("data.tar") {
// write the inner member to a temporary file and reuse tar extraction logic
let mut tmpf = NamedTempFile::new()?;
std::io::copy(&mut entry, &mut tmpf)?;
crate::interrupts::copy_interruptibly(&mut entry, &mut tmpf)?;
let tmp_path = tmpf.path().to_path_buf();
if lower.ends_with(".gz") {
@@ -364,6 +380,7 @@ fn extract_deb(path: &Path, dest: &Path) -> Result<()> {
fn extract_cpio_newc_from_reader<R: Read>(mut r: R, dest: &Path) -> Result<()> {
use std::str;
loop {
crate::interrupts::check()?;
// read 6-byte magic
let mut magic = [0u8; 6];
if let Err(e) = r.read_exact(&mut magic) {
@@ -437,6 +454,7 @@ fn extract_cpio_newc_from_reader<R: Read>(mut r: R, dest: &Path) -> Result<()> {
let mut remaining = filesize;
let mut buf = [0u8; 8192];
while remaining > 0 {
crate::interrupts::check()?;
let to_read = std::cmp::min(remaining, buf.len());
let n = r.read(&mut buf[..to_read])?;
if n == 0 {
@@ -466,6 +484,7 @@ fn extract_cpio_newc_from_reader<R: Read>(mut r: R, dest: &Path) -> Result<()> {
fn extract_rpm(path: &Path, dest: &Path) -> Result<()> {
// Read entire file and search for compression/cpio magic
crate::interrupts::check()?;
let data = std::fs::read(path)?;
// search for known signatures
@@ -520,6 +539,7 @@ fn find_subslice(hay: &[u8], needle: &[u8]) -> Option<usize> {
fn move_dir_contents(src: &Path, dest: &Path) -> Result<()> {
fs::create_dir_all(dest)?;
for entry in fs::read_dir(src)? {
crate::interrupts::check()?;
let entry = entry?;
let src_path = entry.path();
let dest_path = dest.join(entry.file_name());
@@ -580,6 +600,7 @@ fn copy_file_preserve_metadata(src: &Path, dst: &Path) -> Result<()> {
fn copy_dir_recursive_local(src: &Path, dst: &Path) -> Result<()> {
for entry in WalkDir::new(src) {
crate::interrupts::check()?;
let entry = entry?;
let rel = entry.path().strip_prefix(src).unwrap();
let target = dst.join(rel);
@@ -598,6 +619,97 @@ fn copy_dir_recursive_local(src: &Path, dst: &Path) -> Result<()> {
Ok(())
}
fn extract_zip_archive<R: Read + std::io::Seek>(
archive: &mut zip::ZipArchive<R>,
dest: &Path,
) -> Result<()> {
for index in 0..archive.len() {
crate::interrupts::check()?;
let mut entry = archive.by_index(index)?;
let enclosed = entry
.enclosed_name()
.with_context(|| format!("Zip archive entry has unsafe path: {}", entry.name()))?;
let out_path = dest.join(enclosed);
if entry.is_dir() {
fs::create_dir_all(&out_path)?;
continue;
}
let mode = entry.unix_mode().unwrap_or(0);
if (mode & 0o170000) == 0o120000 {
if let Some(parent) = out_path.parent() {
fs::create_dir_all(parent)?;
}
let mut target = Vec::new();
crate::interrupts::copy_interruptibly(&mut entry, &mut target)?;
let target = String::from_utf8(target).context("Zip symlink target was not UTF-8")?;
let _ = fs::remove_file(&out_path);
unix_fs::symlink(target, &out_path)?;
continue;
}
if let Some(parent) = out_path.parent() {
fs::create_dir_all(parent)?;
}
let mut out = File::create(&out_path)?;
crate::interrupts::copy_interruptibly(&mut entry, &mut out)?;
if mode != 0 {
fs::set_permissions(&out_path, fs::Permissions::from_mode(mode & 0o7777))?;
}
}
Ok(())
}
fn wait_for_child_interruptibly(
child: &mut std::process::Child,
) -> std::io::Result<std::process::ExitStatus> {
let mut interrupted_at = None;
let mut sent_term = false;
let mut sent_kill = false;
loop {
if let Some(status) = child.try_wait()? {
return Ok(status);
}
if crate::interrupts::was_interrupted() {
if interrupted_at.is_none() {
interrupted_at = Some(std::time::Instant::now());
signal_child_group(child, nix::libc::SIGINT);
} else if interrupted_at.is_some_and(|started| {
started.elapsed() >= std::time::Duration::from_secs(2) && !sent_term
}) {
sent_term = true;
signal_child_group(child, nix::libc::SIGTERM);
} else if interrupted_at.is_some_and(|started| {
started.elapsed() >= std::time::Duration::from_secs(4) && !sent_kill
}) {
sent_kill = true;
signal_child_group(child, nix::libc::SIGKILL);
}
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
}
fn signal_child_group(child: &std::process::Child, signal: i32) {
let rc = unsafe { nix::libc::kill(-(child.id() as i32), signal) };
if rc != 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() != Some(nix::libc::ESRCH) {
crate::log_warn!(
"Failed to forward signal {} to extractor child process group {}: {}",
signal,
child.id(),
err
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
+15 -23
View File
@@ -68,13 +68,13 @@ fn apply_patches(
crate::log_info!(" patch: {}", patch_path.display());
// Apply with patch(1). Keep it simple: -p1 is the common case.
let status = Command::new("patch")
.current_dir(src_dir)
.env("PATH", crate::runtime_env::safe_script_path())
.arg("-p1")
.arg("-i")
.arg(&patch_path)
.status()
let mut patch_cmd = Command::new("patch");
patch_cmd.current_dir(src_dir);
patch_cmd.env("PATH", crate::runtime_env::safe_script_path());
patch_cmd.arg("-p1");
patch_cmd.arg("-i");
patch_cmd.arg(&patch_path);
let status = crate::interrupts::command_status(&mut patch_cmd)
.with_context(|| format!("Failed to execute patch for {}", patch_path.display()))?;
if !status.success() {
@@ -107,10 +107,8 @@ fn run_post_extract_commands(spec: &PackageSpec, source: &Source, src_dir: &Path
let mut shell_cmd = Command::new("sh");
shell_cmd.current_dir(src_dir);
crate::builder::prepare_command(&mut shell_cmd, &env_vars);
let status = shell_cmd
.arg("-c")
.arg(&wrapped_cmd)
.status()
shell_cmd.arg("-c").arg(&wrapped_cmd);
let status = crate::interrupts::command_status(&mut shell_cmd)
.with_context(|| format!("Failed to run post_extract command: {}", cmd_str))?;
if !status.success() {
@@ -149,10 +147,8 @@ pub fn run_post_configure_commands(
let mut shell_cmd = Command::new("sh");
shell_cmd.current_dir(src_dir);
crate::builder::prepare_command(&mut shell_cmd, &env_vars);
let status = shell_cmd
.arg("-c")
.arg(&wrapped_cmd)
.status()
shell_cmd.arg("-c").arg(&wrapped_cmd);
let status = crate::interrupts::command_status(&mut shell_cmd)
.with_context(|| format!("Failed to run post_configure command: {}", cmd_str))?;
if !status.success() {
@@ -187,10 +183,8 @@ pub fn run_post_compile_commands(spec: &PackageSpec, src_dir: &Path, destdir: &P
let mut shell_cmd = Command::new("sh");
shell_cmd.current_dir(src_dir);
crate::builder::prepare_command(&mut shell_cmd, &env_vars);
let status = shell_cmd
.arg("-c")
.arg(&wrapped_cmd)
.status()
shell_cmd.arg("-c").arg(&wrapped_cmd);
let status = crate::interrupts::command_status(&mut shell_cmd)
.with_context(|| format!("Failed to run post_compile command: {}", cmd_str))?;
if !status.success() {
@@ -229,10 +223,8 @@ pub fn run_post_install_commands_in_dir(
let mut shell_cmd = Command::new("sh");
shell_cmd.current_dir(work_dir);
crate::builder::prepare_command(&mut shell_cmd, &env_vars);
let status = shell_cmd
.arg("-c")
.arg(&wrapped_cmd)
.status()
shell_cmd.arg("-c").arg(&wrapped_cmd);
let status = crate::interrupts::command_status(&mut shell_cmd)
.with_context(|| format!("Failed to run post_install command: {}", cmd_str))?;
if !status.success() {
+2
View File
@@ -296,6 +296,7 @@ pub fn prepare(spec: &PackageSpec, cache_dir: &Path, build_dir: &Path) -> Result
let mut primary: Option<PathBuf> = None;
for (idx, src) in spec.sources().iter().enumerate() {
crate::interrupts::check()?;
let src_dir = prepare_one(spec, src, cache_dir, build_dir)
.with_context(|| format!("Failed to prepare source #{}", idx + 1))?;
if idx == 0 {
@@ -409,6 +410,7 @@ fn prepare_one(
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
fs::create_dir_all(dst)?;
for entry in WalkDir::new(src) {
crate::interrupts::check()?;
let entry = entry?;
let rel = entry.path().strip_prefix(src).unwrap();
let target = dst.join(rel);