feat: add cleanup option for auto-installed dependencies in build command

- Introduced a new `cleanup_deps` flag in the build command to remove dependencies that were automatically installed during the build process.
- Implemented an `InterruptWatcher` to handle Ctrl-C interruptions gracefully during the build process.
- Enhanced the `AutoInstalledDependencyTracker` to track and manage auto-installed dependencies, allowing for cleanup after the build.
- Updated the CLI to parse the new `cleanup_deps` flag and integrated it into the build workflow.
- Added tests to ensure the correct parsing of the `cleanup_deps` flag and the functionality of the dependency tracking and cleanup process.
- Improved logging for package removal operations to include a verbose mode controlled by the `DEPOT_VERBOSE_REMOVE` environment variable.
This commit is contained in:
2026-03-12 18:06:38 -05:00
parent 2c3369d32a
commit 8d9e13ed71
9 changed files with 859 additions and 377 deletions
+60 -11
View File
@@ -111,10 +111,33 @@ fn apply_replacement_rules(current: &mut [String], replacements: &[String], labe
}
}
fn sanitize_flag_list(values: Vec<String>, label: &str) -> Vec<String> {
let mut sanitized = Vec::with_capacity(values.len());
for value in values {
let trimmed = value.trim();
if trimmed.is_empty() {
continue;
}
if trimmed == "-" {
crate::log_warn!(
"Dropping invalid {} entry '-'; compiler/linker flag lists cannot contain a bare dash",
label
);
continue;
}
sanitized.push(trimmed.to_string());
}
sanitized
}
fn replaced_flags(values: &[String], replacements: &[String], label: &str) -> Vec<String> {
let mut current = values.to_vec();
apply_replacement_rules(&mut current, replacements, label);
current
apply_replacement_rules(
&mut current,
replacements,
&format!("{label} replacement rules"),
);
sanitize_flag_list(current, label)
}
pub(crate) fn install_dirs(flags: &crate::package::BuildFlags) -> InstallDirs {
@@ -145,25 +168,21 @@ pub(crate) fn install_dirs(flags: &crate::package::BuildFlags) -> InstallDirs {
fn compiler_flag_sets(
flags: &crate::package::BuildFlags,
) -> (Vec<String>, Vec<String>, Vec<String>, Vec<String>) {
let mut cflags = replaced_flags(
&flags.cflags,
&flags.replace_cflags,
"build.flags.replace_cflags",
);
let mut cflags = replaced_flags(&flags.cflags, &flags.replace_cflags, "build.flags.cflags");
let mut cxxflags = replaced_flags(
&flags.cxxflags,
&flags.replace_cxxflags,
"build.flags.replace_cxxflags",
"build.flags.cxxflags",
);
let mut ldflags = replaced_flags(
&flags.ldflags,
&flags.replace_ldflags,
"build.flags.replace_ldflags",
"build.flags.ldflags",
);
let ltoflags = replaced_flags(
&flags.ltoflags,
&flags.replace_ltoflags,
"build.flags.replace_ltoflags",
"build.flags.ltoflags",
);
if flags.use_lto && !ltoflags.is_empty() {
@@ -179,7 +198,7 @@ pub(crate) fn effective_rustflags(flags: &crate::package::BuildFlags) -> Vec<Str
replaced_flags(
&flags.rustflags,
&flags.replace_rustflags,
"build.flags.replace_rustflags",
"build.flags.rustflags",
)
}
@@ -663,6 +682,36 @@ mod tests {
);
}
#[test]
fn test_standard_build_env_drops_bare_dash_flags() {
let mut spec = mk_spec(vec!["-O2", "-", ""], vec!["-Wl,--as-needed", " "]);
spec.build.flags.cxxflags = vec!["-O2".into(), "-".into(), "-fno-exceptions".into()];
spec.build.flags.ltoflags = vec!["-".into(), "-flto=thin".into()];
spec.build.flags.use_lto = true;
let env = standard_build_env(&spec, None, true, true);
assert!(
env.iter()
.any(|(k, v)| k == "CFLAGS" && v == "-O2 -flto=thin"),
"expected bare dash entries to be removed from CFLAGS"
);
assert!(
env.iter()
.any(|(k, v)| k == "CXXFLAGS" && v == "-O2 -fno-exceptions -flto=thin"),
"expected bare dash entries to be removed from CXXFLAGS"
);
assert!(
env.iter()
.any(|(k, v)| k == "LDFLAGS" && v == "-Wl,--as-needed -flto=thin"),
"expected blank and bare dash entries to be removed from LDFLAGS"
);
assert!(
env.iter()
.any(|(k, v)| k == "LTOFLAGS" && v == "-flto=thin"),
"expected bare dash entries to be removed from LTOFLAGS"
);
}
#[test]
fn test_standard_build_env_skips_lto_injection_when_disabled() {
let mut spec = mk_spec(vec!["-O2"], vec!["-Wl,--as-needed"]);
+13
View File
@@ -97,6 +97,10 @@ pub enum Commands {
/// Automatically install missing dependencies before building
#[arg(long)]
install_deps: bool,
/// Remove dependencies auto-installed for this build after the command finishes
#[arg(long)]
cleanup_deps: bool,
},
/// Update installed packages from configured repositories
Update {
@@ -360,4 +364,13 @@ mod tests {
_ => panic!("expected build command"),
}
}
#[test]
fn build_cleanup_deps_flag_is_parsed() {
let cli = Cli::try_parse_from(["depot", "build", "--cleanup-deps", "pkg.toml"]).unwrap();
match cli.command {
Commands::Build { cleanup_deps, .. } => assert!(cleanup_deps),
_ => panic!("expected build command"),
}
}
}
+753 -346
View File
File diff suppressed because it is too large Load Diff
+13 -4
View File
@@ -14,6 +14,10 @@ fn format_licenses(licenses: &[String]) -> String {
licenses.join(", ")
}
fn verbose_remove_output() -> bool {
std::env::var_os("DEPOT_VERBOSE_REMOVE").is_some()
}
/// Installed package row from the local package database.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstalledPackageRecord {
@@ -268,7 +272,9 @@ pub fn remove_package(db_path: &Path, name: &str, rootfs: &Path) -> Result<()> {
let path = rootfs.join(file);
match fs::remove_file(&path) {
Ok(()) => {
crate::log_info!(" Removed file: {}", file);
if verbose_remove_output() {
crate::log_info!(" Removed file: {}", file);
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
// Already gone, keep going.
@@ -297,14 +303,15 @@ pub fn remove_package(db_path: &Path, name: &str, rootfs: &Path) -> Result<()> {
)?;
if other_owners > 0 {
crate::log_info!(" Keeping directory (owned by other package): {}", dir);
continue;
}
// Try to remove (will fail if not empty, which is fine)
match fs::remove_dir(&path) {
Ok(()) => {
crate::log_info!(" Removed directory: {}", dir);
if verbose_remove_output() {
crate::log_info!(" Removed directory: {}", dir);
}
dirs_removed += 1;
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
@@ -312,7 +319,9 @@ pub fn remove_package(db_path: &Path, name: &str, rootfs: &Path) -> Result<()> {
}
Err(e) if e.raw_os_error() == Some(39) || e.raw_os_error() == Some(66) => {
// ENOTEMPTY (39 on Linux, 66 on macOS) - directory not empty
crate::log_info!(" Keeping directory (not empty): {}", dir);
if verbose_remove_output() {
crate::log_info!(" Keeping directory (not empty): {}", dir);
}
}
Err(_) => {
// Other errors (permission, etc.) - just skip silently
+4
View File
@@ -96,6 +96,10 @@ pub fn set_assume_yes(assume_yes: bool) {
ASSUME_YES.store(assume_yes, Ordering::Relaxed);
}
pub fn assume_yes_enabled() -> bool {
ASSUME_YES.load(Ordering::Relaxed)
}
pub fn prompt_yes_no(prompt: &str, default_yes: bool) -> Result<bool> {
if ASSUME_YES.load(Ordering::Relaxed) {
info(format!("{} [auto-yes]", prompt));