@@ -6,9 +6,11 @@ use std::ffi::{OsStr, OsString};
66use std:: fmt:: Write as WriteFmt ;
77use std:: io:: { BufRead , BufReader , Write as StdWrite } ;
88use std:: iter:: Peekable ;
9+ use std:: num:: NonZeroUsize ;
910use std:: os:: unix:: ffi:: { OsStrExt , OsStringExt } ;
1011use std:: path:: { Path , PathBuf } ;
1112
13+ use camino:: Utf8PathBuf ;
1214use cap_std:: fs:: MetadataExt ;
1315use cap_std:: fs:: { Dir , Permissions , PermissionsExt } ;
1416use cap_std_ext:: cap_std;
@@ -18,7 +20,22 @@ use rustix::path::Arg;
1820use thiserror:: Error ;
1921
2022const TMPFILESD : & str = "usr/lib/tmpfiles.d" ;
21- const BOOTC_GENERATED : & str = "bootc-autogenerated-var.conf" ;
23+ /// The path to the file we use for generation
24+ const BOOTC_GENERATED_PREFIX : & str = "bootc-autogenerated-var" ;
25+
26+ /// The number of times we've generated a tmpfiles.d
27+ #[ derive( Debug , Default ) ]
28+ struct BootcTmpfilesGeneration ( u32 ) ;
29+
30+ impl BootcTmpfilesGeneration {
31+ fn increment ( & self ) -> Self {
32+ Self ( self . 0 + 1 )
33+ }
34+
35+ fn path ( & self ) -> Utf8PathBuf {
36+ format ! ( "{TMPFILESD}/{BOOTC_GENERATED_PREFIX}-{}.conf" , self . 0 ) . into ( )
37+ }
38+ }
2239
2340/// An error when translating tmpfiles.d.
2441#[ derive( Debug , Error ) ]
@@ -219,13 +236,22 @@ pub(crate) fn translate_to_tmpfiles_d(
219236 Ok ( bufwr)
220237}
221238
239+ /// The result of a tmpfiles.d generation run
240+ #[ derive( Debug , Default ) ]
241+ pub struct TmpfilesWrittenResult {
242+ /// Set if we generated entries; this is the count and the path.
243+ pub generated : Option < ( NonZeroUsize , Utf8PathBuf ) > ,
244+ /// Total number of unsupported files that were skipped
245+ pub unsupported : usize ,
246+ }
247+
222248/// Translate the content of `/var` underneath the target root to use tmpfiles.d.
223249pub fn var_to_tmpfiles < U : uzers:: Users , G : uzers:: Groups > (
224250 rootfs : & Dir ,
225251 users : & U ,
226252 groups : & G ,
227- ) -> Result < ( ) > {
228- let existing_tmpfiles = read_tmpfiles ( rootfs) ?;
253+ ) -> Result < TmpfilesWrittenResult > {
254+ let ( existing_tmpfiles, generation ) = read_tmpfiles ( rootfs) ?;
229255
230256 // We should never have /var/run as a non-symlink. Don't recurse into it, it's
231257 // a hard error.
@@ -239,43 +265,55 @@ pub fn var_to_tmpfiles<U: uzers::Users, G: uzers::Groups>(
239265 if !rootfs. try_exists ( TMPFILESD ) ? {
240266 return Err ( Error :: MissingTmpfilesDir { } ) ;
241267 }
242- let mode = Permissions :: from_mode ( 0o644 ) ;
243- rootfs. atomic_replace_with (
244- Path :: new ( TMPFILESD ) . join ( BOOTC_GENERATED ) ,
245- |bufwr| -> Result < ( ) > {
246- bufwr. get_mut ( ) . as_file_mut ( ) . set_permissions ( mode) ?;
247- let mut prefix = PathBuf :: from ( "/var" ) ;
248- let mut entries = BTreeSet :: new ( ) ;
249- let mut unsupported = Vec :: new ( ) ;
250- convert_path_to_tmpfiles_d_recurse (
251- & mut entries,
252- & mut unsupported,
253- users,
254- groups,
255- rootfs,
256- & existing_tmpfiles,
257- & mut prefix,
258- false ,
259- ) ?;
260- for line in entries {
261- bufwr. write_all ( line. as_bytes ( ) ) ?;
262- writeln ! ( bufwr) ?;
268+
269+ let mut entries = BTreeSet :: new ( ) ;
270+ let mut prefix = PathBuf :: from ( "/var" ) ;
271+ let mut unsupported = Vec :: new ( ) ;
272+ convert_path_to_tmpfiles_d_recurse (
273+ & mut entries,
274+ & mut unsupported,
275+ users,
276+ groups,
277+ rootfs,
278+ & existing_tmpfiles,
279+ & mut prefix,
280+ false ,
281+ ) ?;
282+
283+ // If there's no entries, don't write a file
284+ let Some ( entries_count) = NonZeroUsize :: new ( entries. len ( ) ) else {
285+ return Ok ( TmpfilesWrittenResult :: default ( ) ) ;
286+ } ;
287+
288+ let path = generation. path ( ) ;
289+ // This should not exist
290+ assert ! ( !rootfs. try_exists( & path) ?) ;
291+
292+ rootfs. atomic_replace_with ( & path, |bufwr| -> Result < ( ) > {
293+ let mode = Permissions :: from_mode ( 0o644 ) ;
294+ bufwr. get_mut ( ) . as_file_mut ( ) . set_permissions ( mode) ?;
295+
296+ for line in entries. iter ( ) {
297+ bufwr. write_all ( line. as_bytes ( ) ) ?;
298+ writeln ! ( bufwr) ?;
299+ }
300+ if !unsupported. is_empty ( ) {
301+ let ( samples, rest) = bootc_utils:: iterator_split ( unsupported. iter ( ) , 5 ) ;
302+ for elt in samples {
303+ writeln ! ( bufwr, "# bootc ignored: {elt:?}" ) ?;
263304 }
264- if !unsupported. is_empty ( ) {
265- let ( samples, rest) = bootc_utils:: iterator_split ( unsupported. iter ( ) , 5 ) ;
266- for elt in samples {
267- writeln ! ( bufwr, "# bootc ignored: {elt:?}" ) ?;
268- }
269- let rest = rest. count ( ) ;
270- if rest > 0 {
271- writeln ! ( bufwr, "# bootc ignored: ...and {rest} more" ) ?;
272- }
305+ let rest = rest. count ( ) ;
306+ if rest > 0 {
307+ writeln ! ( bufwr, "# bootc ignored: ...and {rest} more" ) ?;
273308 }
274- Ok ( ( ) )
275- } ,
276- ) ?;
309+ }
310+ Ok ( ( ) )
311+ } ) ?;
277312
278- Ok ( ( ) )
313+ Ok ( TmpfilesWrittenResult {
314+ generated : Some ( ( entries_count, path) ) ,
315+ unsupported : unsupported. len ( ) ,
316+ } )
279317}
280318
281319/// Recursively explore target directory and translate content to tmpfiles.d entries. See
@@ -370,7 +408,7 @@ fn convert_path_to_tmpfiles_d_recurse<U: uzers::Users, G: uzers::Groups>(
370408
371409/// Convert /var for the current root to use systemd tmpfiles.d.
372410#[ allow( unsafe_code) ]
373- pub fn convert_var_to_tmpfiles_current_root ( ) -> Result < ( ) > {
411+ pub fn convert_var_to_tmpfiles_current_root ( ) -> Result < TmpfilesWrittenResult > {
374412 let rootfs = Dir :: open_ambient_dir ( "/" , cap_std:: ambient_authority ( ) ) ?;
375413
376414 // See the docs for why this is unsafe
@@ -398,7 +436,7 @@ pub fn find_missing_tmpfiles_current_root() -> Result<TmpfilesResult> {
398436 // See the docs for why this is unsafe
399437 let usergroups = unsafe { UsersSnapshot :: new ( ) } ;
400438
401- let existing_tmpfiles = read_tmpfiles ( & rootfs) ?;
439+ let existing_tmpfiles = read_tmpfiles ( & rootfs) ?. 0 ;
402440
403441 let mut prefix = PathBuf :: from ( "/var" ) ;
404442 let mut tmpfiles = BTreeSet :: new ( ) ;
@@ -421,20 +459,28 @@ pub fn find_missing_tmpfiles_current_root() -> Result<TmpfilesResult> {
421459
422460/// Read all tmpfiles.d entries in the target directory, and return a mapping
423461/// from (file path) => (single tmpfiles.d entry line)
424- fn read_tmpfiles ( rootfs : & Dir ) -> Result < BTreeMap < PathBuf , String > > {
462+ fn read_tmpfiles ( rootfs : & Dir ) -> Result < ( BTreeMap < PathBuf , String > , BootcTmpfilesGeneration ) > {
425463 let Some ( tmpfiles_dir) = rootfs. open_dir_optional ( TMPFILESD ) ? else {
426464 return Ok ( Default :: default ( ) ) ;
427465 } ;
428466 let mut result = BTreeMap :: new ( ) ;
467+ let mut generation = BootcTmpfilesGeneration :: default ( ) ;
429468 for entry in tmpfiles_dir. entries ( ) ? {
430469 let entry = entry?;
431470 let name = entry. file_name ( ) ;
432- let Some ( extension) = Path :: new ( & name) . extension ( ) else {
471+ let ( Some ( stem) , Some ( extension) ) =
472+ ( Path :: new ( & name) . file_stem ( ) , Path :: new ( & name) . extension ( ) )
473+ else {
433474 continue ;
434475 } ;
435476 if extension != "conf" {
436477 continue ;
437478 }
479+ if let Ok ( s) = stem. as_str ( ) {
480+ if s. starts_with ( BOOTC_GENERATED_PREFIX ) {
481+ generation = generation. increment ( ) ;
482+ }
483+ }
438484 let r = BufReader :: new ( entry. open ( ) ?) ;
439485 for line in r. lines ( ) {
440486 let line = line?;
@@ -445,7 +491,7 @@ fn read_tmpfiles(rootfs: &Dir) -> Result<BTreeMap<PathBuf, String>> {
445491 result. insert ( path. to_owned ( ) , line) ;
446492 }
447493 }
448- Ok ( result)
494+ Ok ( ( result, generation ) )
449495}
450496
451497fn tmpfiles_entry_get_path ( line : & str ) -> Result < PathBuf > {
@@ -541,7 +587,9 @@ mod tests {
541587
542588 var_to_tmpfiles ( rootfs, userdb, userdb) . unwrap ( ) ;
543589
544- let autovar_path = & Path :: new ( TMPFILESD ) . join ( BOOTC_GENERATED ) ;
590+ // This is the first run
591+ let gen = BootcTmpfilesGeneration ( 0 ) ;
592+ let autovar_path = & gen. path ( ) ;
545593 assert ! ( rootfs. try_exists( autovar_path) . unwrap( ) ) ;
546594 let entries: Vec < String > = rootfs
547595 . read_to_string ( autovar_path)
@@ -560,6 +608,17 @@ mod tests {
560608 similar_asserts:: assert_eq!( entries, expected) ;
561609 assert ! ( !rootfs. try_exists( "var/lib" ) . unwrap( ) ) ;
562610
611+ // Now pretend we're doing a layered container build, and so we need
612+ // a new tmpfiles.d run
613+ rootfs. create_dir_all ( "var/lib/gen2-test" ) ?;
614+ let w = var_to_tmpfiles ( rootfs, userdb, userdb) . unwrap ( ) ;
615+ let wg = w. generated . as_ref ( ) . unwrap ( ) ;
616+ assert_eq ! ( wg. 0 , NonZeroUsize :: new( 1 ) . unwrap( ) ) ;
617+ assert_eq ! ( w. unsupported, 0 ) ;
618+ let gen = gen. increment ( ) ;
619+ let autovar_path = & gen. path ( ) ;
620+ assert_eq ! ( autovar_path, & wg. 1 ) ;
621+ assert ! ( rootfs. try_exists( autovar_path) . unwrap( ) ) ;
563622 Ok ( ( ) )
564623 }
565624
@@ -575,10 +634,9 @@ mod tests {
575634 rootfs. create_dir_all ( "var/log/foo" ) ?;
576635 rootfs. write ( "var/log/foo/foo.log" , b"some other log" ) ?;
577636
637+ let gen = BootcTmpfilesGeneration ( 0 ) ;
578638 var_to_tmpfiles ( rootfs, userdb, userdb) . unwrap ( ) ;
579- let tmpfiles = rootfs
580- . read_to_string ( Path :: new ( TMPFILESD ) . join ( BOOTC_GENERATED ) )
581- . unwrap ( ) ;
639+ let tmpfiles = rootfs. read_to_string ( & gen. path ( ) ) . unwrap ( ) ;
582640 let ignored = tmpfiles
583641 . lines ( )
584642 . filter ( |line| line. starts_with ( "# bootc ignored" ) )
0 commit comments