@@ -6,9 +6,11 @@ use std::ffi::{OsStr, OsString};
6
6
use std:: fmt:: Write as WriteFmt ;
7
7
use std:: io:: { BufRead , BufReader , Write as StdWrite } ;
8
8
use std:: iter:: Peekable ;
9
+ use std:: num:: NonZeroUsize ;
9
10
use std:: os:: unix:: ffi:: { OsStrExt , OsStringExt } ;
10
11
use std:: path:: { Path , PathBuf } ;
11
12
13
+ use camino:: Utf8PathBuf ;
12
14
use cap_std:: fs:: MetadataExt ;
13
15
use cap_std:: fs:: { Dir , Permissions , PermissionsExt } ;
14
16
use cap_std_ext:: cap_std;
@@ -18,7 +20,22 @@ use rustix::path::Arg;
18
20
use thiserror:: Error ;
19
21
20
22
const 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
+ }
22
39
23
40
/// An error when translating tmpfiles.d.
24
41
#[ derive( Debug , Error ) ]
@@ -219,13 +236,22 @@ pub(crate) fn translate_to_tmpfiles_d(
219
236
Ok ( bufwr)
220
237
}
221
238
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
+
222
248
/// Translate the content of `/var` underneath the target root to use tmpfiles.d.
223
249
pub fn var_to_tmpfiles < U : uzers:: Users , G : uzers:: Groups > (
224
250
rootfs : & Dir ,
225
251
users : & U ,
226
252
groups : & G ,
227
- ) -> Result < ( ) > {
228
- let existing_tmpfiles = read_tmpfiles ( rootfs) ?;
253
+ ) -> Result < TmpfilesWrittenResult > {
254
+ let ( existing_tmpfiles, generation ) = read_tmpfiles ( rootfs) ?;
229
255
230
256
// We should never have /var/run as a non-symlink. Don't recurse into it, it's
231
257
// a hard error.
@@ -239,43 +265,55 @@ pub fn var_to_tmpfiles<U: uzers::Users, G: uzers::Groups>(
239
265
if !rootfs. try_exists ( TMPFILESD ) ? {
240
266
return Err ( Error :: MissingTmpfilesDir { } ) ;
241
267
}
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:?}" ) ?;
263
304
}
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" ) ?;
273
308
}
274
- Ok ( ( ) )
275
- } ,
276
- ) ?;
309
+ }
310
+ Ok ( ( ) )
311
+ } ) ?;
277
312
278
- Ok ( ( ) )
313
+ Ok ( TmpfilesWrittenResult {
314
+ generated : Some ( ( entries_count, path) ) ,
315
+ unsupported : unsupported. len ( ) ,
316
+ } )
279
317
}
280
318
281
319
/// 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>(
370
408
371
409
/// Convert /var for the current root to use systemd tmpfiles.d.
372
410
#[ allow( unsafe_code) ]
373
- pub fn convert_var_to_tmpfiles_current_root ( ) -> Result < ( ) > {
411
+ pub fn convert_var_to_tmpfiles_current_root ( ) -> Result < TmpfilesWrittenResult > {
374
412
let rootfs = Dir :: open_ambient_dir ( "/" , cap_std:: ambient_authority ( ) ) ?;
375
413
376
414
// See the docs for why this is unsafe
@@ -398,7 +436,7 @@ pub fn find_missing_tmpfiles_current_root() -> Result<TmpfilesResult> {
398
436
// See the docs for why this is unsafe
399
437
let usergroups = unsafe { UsersSnapshot :: new ( ) } ;
400
438
401
- let existing_tmpfiles = read_tmpfiles ( & rootfs) ?;
439
+ let existing_tmpfiles = read_tmpfiles ( & rootfs) ?. 0 ;
402
440
403
441
let mut prefix = PathBuf :: from ( "/var" ) ;
404
442
let mut tmpfiles = BTreeSet :: new ( ) ;
@@ -421,20 +459,28 @@ pub fn find_missing_tmpfiles_current_root() -> Result<TmpfilesResult> {
421
459
422
460
/// Read all tmpfiles.d entries in the target directory, and return a mapping
423
461
/// 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 ) > {
425
463
let Some ( tmpfiles_dir) = rootfs. open_dir_optional ( TMPFILESD ) ? else {
426
464
return Ok ( Default :: default ( ) ) ;
427
465
} ;
428
466
let mut result = BTreeMap :: new ( ) ;
467
+ let mut generation = BootcTmpfilesGeneration :: default ( ) ;
429
468
for entry in tmpfiles_dir. entries ( ) ? {
430
469
let entry = entry?;
431
470
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 {
433
474
continue ;
434
475
} ;
435
476
if extension != "conf" {
436
477
continue ;
437
478
}
479
+ if let Ok ( s) = stem. as_str ( ) {
480
+ if s. starts_with ( BOOTC_GENERATED_PREFIX ) {
481
+ generation = generation. increment ( ) ;
482
+ }
483
+ }
438
484
let r = BufReader :: new ( entry. open ( ) ?) ;
439
485
for line in r. lines ( ) {
440
486
let line = line?;
@@ -445,7 +491,7 @@ fn read_tmpfiles(rootfs: &Dir) -> Result<BTreeMap<PathBuf, String>> {
445
491
result. insert ( path. to_owned ( ) , line) ;
446
492
}
447
493
}
448
- Ok ( result)
494
+ Ok ( ( result, generation ) )
449
495
}
450
496
451
497
fn tmpfiles_entry_get_path ( line : & str ) -> Result < PathBuf > {
@@ -541,7 +587,9 @@ mod tests {
541
587
542
588
var_to_tmpfiles ( rootfs, userdb, userdb) . unwrap ( ) ;
543
589
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 ( ) ;
545
593
assert ! ( rootfs. try_exists( autovar_path) . unwrap( ) ) ;
546
594
let entries: Vec < String > = rootfs
547
595
. read_to_string ( autovar_path)
@@ -560,6 +608,17 @@ mod tests {
560
608
similar_asserts:: assert_eq!( entries, expected) ;
561
609
assert ! ( !rootfs. try_exists( "var/lib" ) . unwrap( ) ) ;
562
610
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( ) ) ;
563
622
Ok ( ( ) )
564
623
}
565
624
@@ -575,10 +634,9 @@ mod tests {
575
634
rootfs. create_dir_all ( "var/log/foo" ) ?;
576
635
rootfs. write ( "var/log/foo/foo.log" , b"some other log" ) ?;
577
636
637
+ let gen = BootcTmpfilesGeneration ( 0 ) ;
578
638
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 ( ) ;
582
640
let ignored = tmpfiles
583
641
. lines ( )
584
642
. filter ( |line| line. starts_with ( "# bootc ignored" ) )
0 commit comments