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
+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 {