@@ -32,22 +32,111 @@ const SKIP: u8 = 77;
3232const MODULES : & str = "usr/lib/modules" ;
3333/// The default name for the initramfs.
3434const INITRAMFS : & str = "initramfs.img" ;
35- /// The path to the instal.conf that sets layout.
36- const KERNEL_INSTALL_CONF : & str = "usr/lib/kernel/install.conf" ;
35+ /// Config paths per kernel-install(8), checked in priority order.
36+ /// /etc takes precedence over /usr/lib (user/distro config over vendor defaults).
37+ const KERNEL_INSTALL_CONF_ETC : & str = "etc/kernel/install.conf" ;
38+ const KERNEL_INSTALL_CONF_ETC_D : & str = "etc/kernel/install.conf.d" ;
39+ const KERNEL_INSTALL_CONF_USR : & str = "usr/lib/kernel/install.conf" ;
40+ const KERNEL_INSTALL_CONF_USR_D : & str = "usr/lib/kernel/install.conf.d" ;
3741
38- #[ context( "Verifying kernel-install layout file" ) ]
39- pub fn is_ostree_layout ( rootfs : & Dir ) -> Result < bool > {
40- let Some ( conf) = rootfs. open_optional ( KERNEL_INSTALL_CONF ) ? else {
41- return Ok ( false ) ;
42- } ;
43- let buf = BufReader :: new ( conf) ;
44- // Check for "layout=ostree" in the file
42+ /// Parse a config file and return the layout value if found.
43+ fn get_layout_from_file ( file : std:: fs:: File ) -> Result < Option < String > > {
44+ let buf = BufReader :: new ( file) ;
4545 for line in buf. lines ( ) {
4646 let line = line?;
47- if line. trim ( ) == "layout=ostree" {
48- return Ok ( true ) ;
47+ let trimmed = line. trim ( ) ;
48+ if let Some ( value) = trimmed. strip_prefix ( "layout=" ) {
49+ return Ok ( Some ( value. to_string ( ) ) ) ;
50+ }
51+ }
52+ Ok ( None )
53+ }
54+
55+ /// Parse all *.conf files in a drop-in directory and return the layout value.
56+ /// Files are processed in lexicographic order; later files override earlier ones.
57+ fn get_layout_from_dropin_dir ( rootfs : & Dir , dir_path : & str ) -> Result < Option < String > > {
58+ let Some ( dir) = rootfs. open_dir_optional ( dir_path) ? else {
59+ return Ok ( None ) ;
60+ } ;
61+
62+ // Collect and sort entries lexicographically
63+ let mut entries: Vec < _ > = dir
64+ . entries ( ) ?
65+ . filter_map ( |entry| {
66+ let entry = entry. ok ( ) ?;
67+ let file_name = entry. file_name ( ) ;
68+ let path = std:: path:: Path :: new ( & file_name) ;
69+ ( path. extension ( ) == Some ( std:: ffi:: OsStr :: new ( "conf" ) ) ) . then_some ( entry)
70+ } )
71+ . collect ( ) ;
72+ entries. sort_by_key ( |e| e. file_name ( ) ) ;
73+
74+ let mut layout = None ;
75+ for entry in entries {
76+ if let Some ( file) = dir. open_optional ( entry. file_name ( ) ) ? {
77+ if let Some ( value) = get_layout_from_file ( file. into_std ( ) ) ? {
78+ layout = Some ( value) ;
79+ }
4980 }
5081 }
82+ Ok ( layout)
83+ }
84+
85+ /// Get the layout from a config directory level (main conf + drop-ins).
86+ /// Per systemd drop-in semantics, drop-in files are parsed AFTER the main config
87+ /// and can override values from it.
88+ fn get_layout_from_config_dir (
89+ rootfs : & Dir ,
90+ main_conf : & str ,
91+ dropin_dir : & str ,
92+ ) -> Result < Option < String > > {
93+ // Start with the main config file value (if it exists)
94+ let mut layout = None ;
95+ if let Some ( conf) = rootfs. open_optional ( main_conf) ? {
96+ layout = get_layout_from_file ( conf. into_std ( ) ) ?;
97+ }
98+
99+ // Drop-ins override the main config (parsed after, per systemd semantics)
100+ if let Some ( dropin_layout) = get_layout_from_dropin_dir ( rootfs, dropin_dir) ? {
101+ layout = Some ( dropin_layout) ;
102+ }
103+
104+ Ok ( layout)
105+ }
106+
107+ /// Check if the kernel-install layout is configured as "ostree".
108+ ///
109+ /// NOTE: We cannot simply rely on the KERNEL_INSTALL_LAYOUT environment variable
110+ /// because this function is called in contexts where kernel-install is not running:
111+ /// - At compose time (FilesystemScriptPrep, cliwrap_write_wrappers)
112+ /// - During cliwrap interception of direct kernel-install calls
113+ ///
114+ /// The shell hook 05-rpmostree.install can rely on the env var directly since
115+ /// it's invoked by kernel-install itself, which parses the config and exports
116+ /// the variable.
117+ ///
118+ /// Per kernel-install(8) and systemd drop-in semantics:
119+ /// - /etc/kernel/ takes precedence over /usr/lib/kernel/
120+ /// - Within each directory, drop-in files (install.conf.d/*.conf) are parsed
121+ /// AFTER the main config (install.conf) and can override its values
122+ /// - Drop-in files are processed in lexicographic order; later files override earlier ones
123+ #[ context( "Verifying kernel-install layout" ) ]
124+ pub fn is_ostree_layout ( rootfs : & Dir ) -> Result < bool > {
125+ // 1. Check /etc/kernel/ level (main conf + drop-ins merged)
126+ // /etc takes precedence over /usr/lib
127+ if let Some ( layout) =
128+ get_layout_from_config_dir ( rootfs, KERNEL_INSTALL_CONF_ETC , KERNEL_INSTALL_CONF_ETC_D ) ?
129+ {
130+ return Ok ( layout == LAYOUT_OSTREE ) ;
131+ }
132+
133+ // 2. Check /usr/lib/kernel/ level (main conf + drop-ins merged)
134+ if let Some ( layout) =
135+ get_layout_from_config_dir ( rootfs, KERNEL_INSTALL_CONF_USR , KERNEL_INSTALL_CONF_USR_D ) ?
136+ {
137+ return Ok ( layout == LAYOUT_OSTREE ) ;
138+ }
139+
51140 Ok ( false )
52141}
53142
@@ -142,14 +231,14 @@ mod tests {
142231 use super :: * ;
143232
144233 #[ test]
145- fn test_ostree_layout_parse ( ) -> Result < ( ) > {
234+ fn test_ostree_layout_usr_conf ( ) -> Result < ( ) > {
146235 let td = & cap_tempfile:: tempdir ( cap_std:: ambient_authority ( ) ) ?;
147236 assert ! ( !is_ostree_layout( & td) . unwrap( ) ) ;
148- td. create_dir_all ( Path :: new ( KERNEL_INSTALL_CONF ) . parent ( ) . unwrap ( ) ) ?;
149- td. write ( KERNEL_INSTALL_CONF , "" ) ?;
237+ td. create_dir_all ( Path :: new ( KERNEL_INSTALL_CONF_USR ) . parent ( ) . unwrap ( ) ) ?;
238+ td. write ( KERNEL_INSTALL_CONF_USR , "" ) ?;
150239 assert ! ( !is_ostree_layout( & td) . unwrap( ) ) ;
151240 td. write (
152- KERNEL_INSTALL_CONF ,
241+ KERNEL_INSTALL_CONF_USR ,
153242 indoc:: indoc! { r#"
154243 # some comments
155244
@@ -158,7 +247,7 @@ mod tests {
158247 ) ?;
159248 assert ! ( !is_ostree_layout( & td) . unwrap( ) ) ;
160249 td. write (
161- KERNEL_INSTALL_CONF ,
250+ KERNEL_INSTALL_CONF_USR ,
162251 indoc:: indoc! { r#"
163252 # this is an ostree layout
164253 layout=ostree
@@ -170,4 +259,203 @@ mod tests {
170259
171260 Ok ( ( ) )
172261 }
262+
263+ #[ test]
264+ fn test_ostree_layout_usr_dropin ( ) -> Result < ( ) > {
265+ let td = & cap_tempfile:: tempdir ( cap_std:: ambient_authority ( ) ) ?;
266+
267+ // No config at all
268+ assert ! ( !is_ostree_layout( & td) . unwrap( ) ) ;
269+
270+ // Create the drop-in directory
271+ td. create_dir_all ( KERNEL_INSTALL_CONF_USR_D ) ?;
272+
273+ // Drop-in file without layout=ostree
274+ td. write (
275+ format ! ( "{}/00-layout.conf" , KERNEL_INSTALL_CONF_USR_D ) ,
276+ indoc:: indoc! { r#"
277+ # some config
278+ layout=bls
279+ "# } ,
280+ ) ?;
281+ assert ! ( !is_ostree_layout( & td) . unwrap( ) ) ;
282+
283+ // Drop-in file with layout=ostree
284+ td. write (
285+ format ! ( "{}/00-layout.conf" , KERNEL_INSTALL_CONF_USR_D ) ,
286+ indoc:: indoc! { r#"
287+ # kernel-install will not try to run dracut and allow rpm-ostree to
288+ # take over. Rpm-ostree will use this to know that it is responsible
289+ # to run dracut and ensure that there is only one kernel in the image
290+ layout=ostree
291+ "# } ,
292+ ) ?;
293+ assert ! ( is_ostree_layout( & td) . unwrap( ) ) ;
294+
295+ Ok ( ( ) )
296+ }
297+
298+ #[ test]
299+ fn test_ostree_layout_dropin_only ( ) -> Result < ( ) > {
300+ let td = & cap_tempfile:: tempdir ( cap_std:: ambient_authority ( ) ) ?;
301+
302+ // Only drop-in, no main install.conf
303+ td. create_dir_all ( KERNEL_INSTALL_CONF_USR_D ) ?;
304+ td. write (
305+ format ! ( "{}/00-layout.conf" , KERNEL_INSTALL_CONF_USR_D ) ,
306+ "layout=ostree\n " ,
307+ ) ?;
308+ assert ! ( is_ostree_layout( & td) . unwrap( ) ) ;
309+
310+ Ok ( ( ) )
311+ }
312+
313+ #[ test]
314+ fn test_ostree_layout_dropin_ordering ( ) -> Result < ( ) > {
315+ let td = & cap_tempfile:: tempdir ( cap_std:: ambient_authority ( ) ) ?;
316+
317+ // Create the drop-in directory
318+ td. create_dir_all ( KERNEL_INSTALL_CONF_USR_D ) ?;
319+
320+ // First file sets ostree, second file overrides to bls
321+ // Later files (lexicographically) should win
322+ td. write (
323+ format ! ( "{}/00-ostree.conf" , KERNEL_INSTALL_CONF_USR_D ) ,
324+ "layout=ostree\n " ,
325+ ) ?;
326+ td. write (
327+ format ! ( "{}/99-bls.conf" , KERNEL_INSTALL_CONF_USR_D ) ,
328+ "layout=bls\n " ,
329+ ) ?;
330+ assert ! ( !is_ostree_layout( & td) . unwrap( ) ) ;
331+
332+ // Now reverse: bls first, ostree second - ostree should win
333+ td. write (
334+ format ! ( "{}/00-bls.conf" , KERNEL_INSTALL_CONF_USR_D ) ,
335+ "layout=bls\n " ,
336+ ) ?;
337+ td. write (
338+ format ! ( "{}/99-ostree.conf" , KERNEL_INSTALL_CONF_USR_D ) ,
339+ "layout=ostree\n " ,
340+ ) ?;
341+ assert ! ( is_ostree_layout( & td) . unwrap( ) ) ;
342+
343+ Ok ( ( ) )
344+ }
345+
346+ #[ test]
347+ fn test_ostree_layout_etc_takes_precedence ( ) -> Result < ( ) > {
348+ let td = & cap_tempfile:: tempdir ( cap_std:: ambient_authority ( ) ) ?;
349+
350+ // Set up /usr/lib with ostree layout
351+ td. create_dir_all ( Path :: new ( KERNEL_INSTALL_CONF_USR ) . parent ( ) . unwrap ( ) ) ?;
352+ td. write ( KERNEL_INSTALL_CONF_USR , "layout=ostree\n " ) ?;
353+ assert ! ( is_ostree_layout( & td) . unwrap( ) ) ;
354+
355+ // Now /etc overrides to bls - should take precedence
356+ td. create_dir_all ( Path :: new ( KERNEL_INSTALL_CONF_ETC ) . parent ( ) . unwrap ( ) ) ?;
357+ td. write ( KERNEL_INSTALL_CONF_ETC , "layout=bls\n " ) ?;
358+ assert ! ( !is_ostree_layout( & td) . unwrap( ) ) ;
359+
360+ // /etc with ostree should also work
361+ td. write ( KERNEL_INSTALL_CONF_ETC , "layout=ostree\n " ) ?;
362+ assert ! ( is_ostree_layout( & td) . unwrap( ) ) ;
363+
364+ Ok ( ( ) )
365+ }
366+
367+ #[ test]
368+ fn test_ostree_layout_etc_dropin_precedence ( ) -> Result < ( ) > {
369+ let td = & cap_tempfile:: tempdir ( cap_std:: ambient_authority ( ) ) ?;
370+
371+ // Set up /usr/lib/kernel/install.conf.d with ostree
372+ td. create_dir_all ( KERNEL_INSTALL_CONF_USR_D ) ?;
373+ td. write (
374+ format ! ( "{}/00-layout.conf" , KERNEL_INSTALL_CONF_USR_D ) ,
375+ "layout=ostree\n " ,
376+ ) ?;
377+ assert ! ( is_ostree_layout( & td) . unwrap( ) ) ;
378+
379+ // /etc/kernel/install.conf.d overrides - should take precedence
380+ td. create_dir_all ( KERNEL_INSTALL_CONF_ETC_D ) ?;
381+ td. write (
382+ format ! ( "{}/00-layout.conf" , KERNEL_INSTALL_CONF_ETC_D ) ,
383+ "layout=bls\n " ,
384+ ) ?;
385+ assert ! ( !is_ostree_layout( & td) . unwrap( ) ) ;
386+
387+ Ok ( ( ) )
388+ }
389+
390+ /// Test the critical scenario this PR fixes: drop-in overrides main config
391+ /// within the same directory level.
392+ ///
393+ /// Real-world scenario:
394+ /// - systemd-udev installs /usr/lib/kernel/install.conf with layout=bls
395+ /// - bootc adds /usr/lib/kernel/install.conf.d/00-kernel-layout.conf with layout=ostree
396+ /// - Expected: drop-in overrides main conf → layout=ostree
397+ #[ test]
398+ fn test_ostree_layout_dropin_overrides_main_conf ( ) -> Result < ( ) > {
399+ let td = & cap_tempfile:: tempdir ( cap_std:: ambient_authority ( ) ) ?;
400+
401+ // Main conf has layout=bls (simulating systemd-udev default)
402+ td. create_dir_all ( Path :: new ( KERNEL_INSTALL_CONF_USR ) . parent ( ) . unwrap ( ) ) ?;
403+ td. write ( KERNEL_INSTALL_CONF_USR , "layout=bls\n " ) ?;
404+ assert ! ( !is_ostree_layout( & td) . unwrap( ) ) ;
405+
406+ // Drop-in overrides to layout=ostree (simulating bootc config)
407+ td. create_dir_all ( KERNEL_INSTALL_CONF_USR_D ) ?;
408+ td. write (
409+ format ! ( "{}/00-kernel-layout.conf" , KERNEL_INSTALL_CONF_USR_D ) ,
410+ "layout=ostree\n " ,
411+ ) ?;
412+ // Drop-in should override main conf per systemd semantics!
413+ assert ! ( is_ostree_layout( & td) . unwrap( ) ) ;
414+
415+ Ok ( ( ) )
416+ }
417+
418+ /// Test reverse scenario: main conf has ostree, drop-in overrides to bls
419+ #[ test]
420+ fn test_dropin_overrides_main_conf_to_non_ostree ( ) -> Result < ( ) > {
421+ let td = & cap_tempfile:: tempdir ( cap_std:: ambient_authority ( ) ) ?;
422+
423+ // Main conf has layout=ostree
424+ td. create_dir_all ( Path :: new ( KERNEL_INSTALL_CONF_USR ) . parent ( ) . unwrap ( ) ) ?;
425+ td. write ( KERNEL_INSTALL_CONF_USR , "layout=ostree\n " ) ?;
426+ assert ! ( is_ostree_layout( & td) . unwrap( ) ) ;
427+
428+ // Drop-in overrides to layout=bls
429+ td. create_dir_all ( KERNEL_INSTALL_CONF_USR_D ) ?;
430+ td. write (
431+ format ! ( "{}/99-override.conf" , KERNEL_INSTALL_CONF_USR_D ) ,
432+ "layout=bls\n " ,
433+ ) ?;
434+ // Drop-in should override main conf
435+ assert ! ( !is_ostree_layout( & td) . unwrap( ) ) ;
436+
437+ Ok ( ( ) )
438+ }
439+
440+ /// Test /etc drop-in overrides /etc main conf
441+ #[ test]
442+ fn test_etc_dropin_overrides_etc_main_conf ( ) -> Result < ( ) > {
443+ let td = & cap_tempfile:: tempdir ( cap_std:: ambient_authority ( ) ) ?;
444+
445+ // /etc main conf has layout=bls
446+ td. create_dir_all ( Path :: new ( KERNEL_INSTALL_CONF_ETC ) . parent ( ) . unwrap ( ) ) ?;
447+ td. write ( KERNEL_INSTALL_CONF_ETC , "layout=bls\n " ) ?;
448+ assert ! ( !is_ostree_layout( & td) . unwrap( ) ) ;
449+
450+ // /etc drop-in overrides to layout=ostree
451+ td. create_dir_all ( KERNEL_INSTALL_CONF_ETC_D ) ?;
452+ td. write (
453+ format ! ( "{}/50-ostree.conf" , KERNEL_INSTALL_CONF_ETC_D ) ,
454+ "layout=ostree\n " ,
455+ ) ?;
456+ // Drop-in should override main conf
457+ assert ! ( is_ostree_layout( & td) . unwrap( ) ) ;
458+
459+ Ok ( ( ) )
460+ }
173461}
0 commit comments