diff --git a/rust/lanzatool/src/gc.rs b/rust/lanzatool/src/gc.rs index 6238d99..24ad270 100644 --- a/rust/lanzatool/src/gc.rs +++ b/rust/lanzatool/src/gc.rs @@ -35,10 +35,30 @@ impl Roots { } pub fn collect_garbage(&self, directory: impl AsRef) -> Result<()> { + self.collect_garbage_with_filter(directory, |_| true) + } + + /// Collect garbage with an additional filter. + /// + /// The filter function takes a &Path and returns a bool. The paths for which the filter + /// function returns true are considered for garbage collection. This means that _only_ files + /// that are unused AND for which the filter function returns true are deleted. + pub fn collect_garbage_with_filter

( + &self, + directory: impl AsRef, + mut predicate: P, + ) -> Result<()> + where + P: FnMut(&Path) -> bool, + { // Find all the paths not used anymore. let entries_not_in_use = WalkDir::new(directory.as_ref()) .into_iter() - .filter(|e| !self.in_use(e.as_ref().ok())); + .filter(|e| !self.in_use(e.as_ref().ok())) + .filter(|e| match e.as_ref().ok() { + Some(e) => predicate(e.path()), + None => false, + }); // Remove all entries not in use. for e in entries_not_in_use { @@ -148,6 +168,27 @@ mod tests { Ok(()) } + #[test] + fn only_delete_filtered_unused_files() -> Result<()> { + let tmpdir = tempfile::tempdir()?; + let rootdir = create_dir(tmpdir.path().join("root"))?; + + let unused_file = create_file(rootdir.join("unused_file"))?; + let unused_file_with_prefix = create_file(rootdir.join("prefix_unused_file"))?; + + let mut roots = Roots::new(); + roots.extend(vec![&rootdir]); + roots.collect_garbage_with_filter(&rootdir, |p| { + p.file_name() + .and_then(|n| n.to_str()) + .map_or(false, |n| n.starts_with("prefix_")) + })?; + + assert!(unused_file.exists()); + assert!(!unused_file_with_prefix.exists()); + Ok(()) + } + fn create_file(path: PathBuf) -> Result { fs::File::create(&path)?; Ok(path) diff --git a/rust/lanzatool/src/install.rs b/rust/lanzatool/src/install.rs index 705aee7..07f07ff 100644 --- a/rust/lanzatool/src/install.rs +++ b/rust/lanzatool/src/install.rs @@ -61,7 +61,20 @@ impl Installer { }; self.install_links(links)?; - self.gc_roots.collect_garbage(&self.esp)?; + // Only collect garbage in these two directories. This way, no files that do not belong to + // the NixOS installation are deleted. Lanzatool takes full control over the esp/EFI/nixos + // directory and deletes ALL files that it doesn't know about. Dual- or multiboot setups + // that need files in this directory will NOT work. + self.gc_roots.collect_garbage(self.esp.join("EFI/nixos"))?; + // The esp/EFI/Linux directory is assumed to be potentially shared with other distros. + // Thus, only files that start with "nixos-" are garbage collected (i.e. potentially + // deleted). + self.gc_roots + .collect_garbage_with_filter(self.esp.join("EFI/Linux"), |p| { + p.file_name() + .and_then(|n| n.to_str()) + .map_or(false, |n| n.starts_with("nixos-")) + })?; Ok(()) } diff --git a/rust/lanzatool/tests/gc.rs b/rust/lanzatool/tests/gc.rs index 6aa94d9..ad9cfc6 100644 --- a/rust/lanzatool/tests/gc.rs +++ b/rust/lanzatool/tests/gc.rs @@ -44,6 +44,41 @@ fn keep_only_configured_number_of_generations() -> Result<()> { Ok(()) } +#[test] +fn keep_unrelated_files_on_esp() -> Result<()> { + let esp_mountpoint = tempdir()?; + let tmpdir = tempdir()?; + let profiles = tempdir()?; + let generation_links: Vec = [1, 2, 3] + .into_iter() + .map(|v| { + common::setup_generation_link(tmpdir.path(), profiles.path(), v) + .expect("Failed to setup generation link") + }) + .collect(); + + // Install all 3 generations. + let output0 = common::lanzaboote_install(0, esp_mountpoint.path(), generation_links.clone())?; + assert!(output0.status.success()); + + let unrelated_uki = esp_mountpoint.path().join("EFI/Linux/ubuntu.efi"); + let unrelated_os = esp_mountpoint.path().join("EFI/windows"); + let unrelated_firmware = esp_mountpoint.path().join("dell"); + fs::File::create(&unrelated_uki)?; + fs::create_dir(&unrelated_os)?; + fs::create_dir(&unrelated_firmware)?; + + // Call `lanzatool install` again with a config limit of 2. + let output1 = common::lanzaboote_install(2, esp_mountpoint.path(), generation_links)?; + assert!(output1.status.success()); + + assert!(unrelated_uki.exists()); + assert!(unrelated_os.exists()); + assert!(unrelated_firmware.exists()); + + Ok(()) +} + fn count_files(path: &Path) -> Result { Ok(fs::read_dir(path)?.count()) }