diff --git a/tmpfiles/src/lib.rs b/tmpfiles/src/lib.rs index d41564bb..1db96c3f 100644 --- a/tmpfiles/src/lib.rs +++ b/tmpfiles/src/lib.rs @@ -6,9 +6,11 @@ use std::ffi::{OsStr, OsString}; use std::fmt::Write as WriteFmt; use std::io::{BufRead, BufReader, Write as StdWrite}; use std::iter::Peekable; +use std::num::NonZeroUsize; use std::os::unix::ffi::{OsStrExt, OsStringExt}; use std::path::{Path, PathBuf}; +use camino::Utf8PathBuf; use cap_std::fs::MetadataExt; use cap_std::fs::{Dir, Permissions, PermissionsExt}; use cap_std_ext::cap_std; @@ -18,7 +20,22 @@ use rustix::path::Arg; use thiserror::Error; const TMPFILESD: &str = "usr/lib/tmpfiles.d"; -const BOOTC_GENERATED: &str = "bootc-autogenerated-var.conf"; +/// The path to the file we use for generation +const BOOTC_GENERATED_PREFIX: &str = "bootc-autogenerated-var"; + +/// The number of times we've generated a tmpfiles.d +#[derive(Debug, Default)] +struct BootcTmpfilesGeneration(u32); + +impl BootcTmpfilesGeneration { + fn increment(&self) -> Self { + Self(self.0 + 1) + } + + fn path(&self) -> Utf8PathBuf { + format!("{TMPFILESD}/{BOOTC_GENERATED_PREFIX}-{}.conf", self.0).into() + } +} /// An error when translating tmpfiles.d. #[derive(Debug, Error)] @@ -219,13 +236,22 @@ pub(crate) fn translate_to_tmpfiles_d( Ok(bufwr) } +/// The result of a tmpfiles.d generation run +#[derive(Debug, Default)] +pub struct TmpfilesWrittenResult { + /// Set if we generated entries; this is the count and the path. + pub generated: Option<(NonZeroUsize, Utf8PathBuf)>, + /// Total number of unsupported files that were skipped + pub unsupported: usize, +} + /// Translate the content of `/var` underneath the target root to use tmpfiles.d. pub fn var_to_tmpfiles( rootfs: &Dir, users: &U, groups: &G, -) -> Result<()> { - let existing_tmpfiles = read_tmpfiles(rootfs)?; +) -> Result { + let (existing_tmpfiles, generation) = read_tmpfiles(rootfs)?; // We should never have /var/run as a non-symlink. Don't recurse into it, it's // a hard error. @@ -239,43 +265,55 @@ pub fn var_to_tmpfiles( if !rootfs.try_exists(TMPFILESD)? { return Err(Error::MissingTmpfilesDir {}); } - let mode = Permissions::from_mode(0o644); - rootfs.atomic_replace_with( - Path::new(TMPFILESD).join(BOOTC_GENERATED), - |bufwr| -> Result<()> { - bufwr.get_mut().as_file_mut().set_permissions(mode)?; - let mut prefix = PathBuf::from("/var"); - let mut entries = BTreeSet::new(); - let mut unsupported = Vec::new(); - convert_path_to_tmpfiles_d_recurse( - &mut entries, - &mut unsupported, - users, - groups, - rootfs, - &existing_tmpfiles, - &mut prefix, - false, - )?; - for line in entries { - bufwr.write_all(line.as_bytes())?; - writeln!(bufwr)?; + + let mut entries = BTreeSet::new(); + let mut prefix = PathBuf::from("/var"); + let mut unsupported = Vec::new(); + convert_path_to_tmpfiles_d_recurse( + &mut entries, + &mut unsupported, + users, + groups, + rootfs, + &existing_tmpfiles, + &mut prefix, + false, + )?; + + // If there's no entries, don't write a file + let Some(entries_count) = NonZeroUsize::new(entries.len()) else { + return Ok(TmpfilesWrittenResult::default()); + }; + + let path = generation.path(); + // This should not exist + assert!(!rootfs.try_exists(&path)?); + + rootfs.atomic_replace_with(&path, |bufwr| -> Result<()> { + let mode = Permissions::from_mode(0o644); + bufwr.get_mut().as_file_mut().set_permissions(mode)?; + + for line in entries.iter() { + bufwr.write_all(line.as_bytes())?; + writeln!(bufwr)?; + } + if !unsupported.is_empty() { + let (samples, rest) = bootc_utils::iterator_split(unsupported.iter(), 5); + for elt in samples { + writeln!(bufwr, "# bootc ignored: {elt:?}")?; } - if !unsupported.is_empty() { - let (samples, rest) = bootc_utils::iterator_split(unsupported.iter(), 5); - for elt in samples { - writeln!(bufwr, "# bootc ignored: {elt:?}")?; - } - let rest = rest.count(); - if rest > 0 { - writeln!(bufwr, "# bootc ignored: ...and {rest} more")?; - } + let rest = rest.count(); + if rest > 0 { + writeln!(bufwr, "# bootc ignored: ...and {rest} more")?; } - Ok(()) - }, - )?; + } + Ok(()) + })?; - Ok(()) + Ok(TmpfilesWrittenResult { + generated: Some((entries_count, path)), + unsupported: unsupported.len(), + }) } /// Recursively explore target directory and translate content to tmpfiles.d entries. See @@ -370,7 +408,7 @@ fn convert_path_to_tmpfiles_d_recurse( /// Convert /var for the current root to use systemd tmpfiles.d. #[allow(unsafe_code)] -pub fn convert_var_to_tmpfiles_current_root() -> Result<()> { +pub fn convert_var_to_tmpfiles_current_root() -> Result { let rootfs = Dir::open_ambient_dir("/", cap_std::ambient_authority())?; // See the docs for why this is unsafe @@ -398,7 +436,7 @@ pub fn find_missing_tmpfiles_current_root() -> Result { // See the docs for why this is unsafe let usergroups = unsafe { UsersSnapshot::new() }; - let existing_tmpfiles = read_tmpfiles(&rootfs)?; + let existing_tmpfiles = read_tmpfiles(&rootfs)?.0; let mut prefix = PathBuf::from("/var"); let mut tmpfiles = BTreeSet::new(); @@ -421,20 +459,28 @@ pub fn find_missing_tmpfiles_current_root() -> Result { /// Read all tmpfiles.d entries in the target directory, and return a mapping /// from (file path) => (single tmpfiles.d entry line) -fn read_tmpfiles(rootfs: &Dir) -> Result> { +fn read_tmpfiles(rootfs: &Dir) -> Result<(BTreeMap, BootcTmpfilesGeneration)> { let Some(tmpfiles_dir) = rootfs.open_dir_optional(TMPFILESD)? else { return Ok(Default::default()); }; let mut result = BTreeMap::new(); + let mut generation = BootcTmpfilesGeneration::default(); for entry in tmpfiles_dir.entries()? { let entry = entry?; let name = entry.file_name(); - let Some(extension) = Path::new(&name).extension() else { + let (Some(stem), Some(extension)) = + (Path::new(&name).file_stem(), Path::new(&name).extension()) + else { continue; }; if extension != "conf" { continue; } + if let Ok(s) = stem.as_str() { + if s.starts_with(BOOTC_GENERATED_PREFIX) { + generation = generation.increment(); + } + } let r = BufReader::new(entry.open()?); for line in r.lines() { let line = line?; @@ -445,7 +491,7 @@ fn read_tmpfiles(rootfs: &Dir) -> Result> { result.insert(path.to_owned(), line); } } - Ok(result) + Ok((result, generation)) } fn tmpfiles_entry_get_path(line: &str) -> Result { @@ -541,7 +587,9 @@ mod tests { var_to_tmpfiles(rootfs, userdb, userdb).unwrap(); - let autovar_path = &Path::new(TMPFILESD).join(BOOTC_GENERATED); + // This is the first run + let gen = BootcTmpfilesGeneration(0); + let autovar_path = &gen.path(); assert!(rootfs.try_exists(autovar_path).unwrap()); let entries: Vec = rootfs .read_to_string(autovar_path) @@ -560,6 +608,17 @@ mod tests { similar_asserts::assert_eq!(entries, expected); assert!(!rootfs.try_exists("var/lib").unwrap()); + // Now pretend we're doing a layered container build, and so we need + // a new tmpfiles.d run + rootfs.create_dir_all("var/lib/gen2-test")?; + let w = var_to_tmpfiles(rootfs, userdb, userdb).unwrap(); + let wg = w.generated.as_ref().unwrap(); + assert_eq!(wg.0, NonZeroUsize::new(1).unwrap()); + assert_eq!(w.unsupported, 0); + let gen = gen.increment(); + let autovar_path = &gen.path(); + assert_eq!(autovar_path, &wg.1); + assert!(rootfs.try_exists(autovar_path).unwrap()); Ok(()) } @@ -575,10 +634,9 @@ mod tests { rootfs.create_dir_all("var/log/foo")?; rootfs.write("var/log/foo/foo.log", b"some other log")?; + let gen = BootcTmpfilesGeneration(0); var_to_tmpfiles(rootfs, userdb, userdb).unwrap(); - let tmpfiles = rootfs - .read_to_string(Path::new(TMPFILESD).join(BOOTC_GENERATED)) - .unwrap(); + let tmpfiles = rootfs.read_to_string(&gen.path()).unwrap(); let ignored = tmpfiles .lines() .filter(|line| line.starts_with("# bootc ignored"))