@@ -162,6 +162,10 @@ use aleph::InstallAleph;
162162use anyhow:: { Context , Result , anyhow, ensure} ;
163163use bootc_kernel_cmdline:: utf8:: { Cmdline , CmdlineOwned } ;
164164use bootc_utils:: CommandRunExt ;
165+ use composefs:: fs:: { read_file, write_to_path} ;
166+ use composefs_boot:: bootloader:: { BootEntry , get_boot_resources} ;
167+ use composefs_boot:: selabel;
168+ use composefs_oci:: image:: create_filesystem as create_composefs_filesystem;
165169use camino:: Utf8Path ;
166170use camino:: Utf8PathBuf ;
167171use canon_json:: CanonJsonSerialize ;
@@ -238,6 +242,9 @@ const DEFAULT_REPO_CONFIG: &[(&str, &str)] = &[
238242/// Kernel argument used to specify we want the rootfs mounted read-write by default
239243pub ( crate ) const RW_KARG : & str = "rw" ;
240244
245+ /// Marker file written to the target root to indicate a flat (non-ostree) install was performed.
246+ pub ( crate ) const FLAT_INSTALL_MARKER : & str = "etc/.bootc-flat" ;
247+
241248#[ derive( clap:: Args , Debug , Clone , Serialize , Deserialize , PartialEq , Eq ) ]
242249pub ( crate ) struct InstallTargetOpts {
243250 // TODO: A size specifier which allocates free space for the root in *addition* to the base container image size
@@ -488,6 +495,15 @@ pub(crate) struct InstallTargetFilesystemOpts {
488495 /// is then the responsibility of the invoking code to perform those operations.
489496 #[ clap( long) ]
490497 pub ( crate ) skip_finalize : bool ,
498+
499+ /// Install in "flat" mode: the container rootfs is written to the target filesystem
500+ /// as a regular writable directory tree (no composefs overlay at boot time). This is
501+ /// experimental. Post-install, bootc day-2 operations (upgrade, rollback, etc.) are
502+ /// unavailable on the installed system. A composefs repository is created at
503+ /// `/sysroot/composefs` as an intermediate step; it can be removed post-install or
504+ /// retained to enable future conversion to immutable mode.
505+ #[ clap( long) ]
506+ pub ( crate ) flat : bool ,
491507}
492508
493509#[ derive( Debug , Clone , clap:: Parser , PartialEq , Eq ) ]
@@ -1286,6 +1302,8 @@ pub(crate) struct RootSetup {
12861302 skip_finalize : bool ,
12871303 boot : Option < MountSpec > ,
12881304 pub ( crate ) kargs : CmdlineOwned ,
1305+ /// If true, perform a flat installation (no ostree/composefs)
1306+ pub ( crate ) flat : bool ,
12891307}
12901308
12911309fn require_boot_uuid ( spec : & MountSpec ) -> Result < & str > {
@@ -1818,16 +1836,17 @@ async fn install_with_sysroot(
18181836
18191837 if cfg ! ( target_arch = "s390x" ) {
18201838 // TODO: Integrate s390x support into install_via_bootupd
1821- crate :: bootloader :: install_via_zipl ( & rootfs. device_info , boot_uuid) ?;
1839+ install_bootloader_via_zipl ( & rootfs. device_info , boot_uuid) ?;
18221840 } else {
18231841 match postfetch. detected_bootloader {
18241842 Bootloader :: Grub => {
1825- crate :: bootloader:: install_via_bootupd (
1843+ let target_root = rootfs
1844+ . target_root_path
1845+ . as_ref ( )
1846+ . unwrap_or ( & rootfs. physical_root_path ) ;
1847+ install_bootloader_via_bootupd (
18261848 & rootfs. device_info ,
1827- & rootfs
1828- . target_root_path
1829- . clone ( )
1830- . unwrap_or ( rootfs. physical_root_path . clone ( ) ) ,
1849+ target_root,
18311850 & state. config_opts ,
18321851 Some ( & deployment_path. as_str ( ) ) ,
18331852 ) ?;
@@ -1863,6 +1882,38 @@ async fn install_with_sysroot(
18631882 Ok ( ( ) )
18641883}
18651884
1885+ /// Install the bootloader using bootupd.
1886+ ///
1887+ /// This is a helper to reduce duplication between ostree and flat installs.
1888+ ///
1889+ /// # Arguments
1890+ /// * `device_info` - Device information for the target
1891+ /// * `target_root` - Path to the target root filesystem
1892+ /// * `config_opts` - Configuration options
1893+ /// * `deployment_path` - Optional deployment path for ostree-based installs
1894+ #[ context( "Installing bootloader via bootupd" ) ]
1895+ fn install_bootloader_via_bootupd (
1896+ device_info : & bootc_blockdev:: Device ,
1897+ target_root : & Utf8PathBuf ,
1898+ config_opts : & InstallConfigOpts ,
1899+ deployment_path : Option < & str > ,
1900+ ) -> Result < ( ) > {
1901+ crate :: bootloader:: install_via_bootupd ( device_info, target_root, config_opts, deployment_path)
1902+ }
1903+
1904+ /// Install the bootloader using zipl (s390x architecture).
1905+ ///
1906+ /// # Arguments
1907+ /// * `device_info` - Device information for the target
1908+ /// * `boot_uuid` - Boot UUID (required for zipl)
1909+ #[ context( "Installing bootloader via zipl" ) ]
1910+ fn install_bootloader_via_zipl (
1911+ device_info : & bootc_blockdev:: Device ,
1912+ boot_uuid : & str ,
1913+ ) -> Result < ( ) > {
1914+ crate :: bootloader:: install_via_zipl ( device_info, boot_uuid)
1915+ }
1916+
18661917enum BoundImages {
18671918 Skip ,
18681919 Resolved ( Vec < ResolvedBoundImage > ) ,
@@ -1899,6 +1950,162 @@ impl BoundImages {
18991950 }
19001951}
19011952
1953+ /// Write a BLS entry for a flat (non-ostree) installation.
1954+ #[ context( "Creating flat BLS entry" ) ]
1955+ fn create_flat_bls_entry (
1956+ rootfs : & RootSetup ,
1957+ kernel_version : & str ,
1958+ vmlinuz_boot_path : & Utf8PathBuf ,
1959+ initramfs_boot_path : & Utf8PathBuf ,
1960+ ) -> Result < ( ) > {
1961+ use crate :: parsers:: bls_config:: { BLSConfig , BLSConfigType } ;
1962+
1963+ let mut cfg = BLSConfig :: default ( ) ;
1964+ cfg. with_title ( format ! ( "Linux {kernel_version}" ) )
1965+ . with_version ( kernel_version. to_string ( ) )
1966+ . with_cfg ( BLSConfigType :: NonEFI {
1967+ linux : vmlinuz_boot_path. clone ( ) ,
1968+ initrd : vec ! [ initramfs_boot_path. clone( ) ] ,
1969+ options : Some ( rootfs. kargs . clone ( ) ) ,
1970+ } ) ;
1971+
1972+ let entry_path = format ! ( "boot/loader/entries/flat-{kernel_version}.conf" ) ;
1973+ rootfs
1974+ . physical_root
1975+ . create_dir_all ( "boot/loader/entries" )
1976+ . context ( "Creating boot/loader/entries" ) ?;
1977+
1978+ let content = format ! ( "{cfg}" ) ;
1979+ rootfs
1980+ . physical_root
1981+ . atomic_write ( & entry_path, content. as_bytes ( ) )
1982+ . with_context ( || format ! ( "Writing BLS entry {entry_path}" ) ) ?;
1983+
1984+ tracing:: debug!( "Wrote BLS entry: {entry_path}" ) ;
1985+ Ok ( ( ) )
1986+ }
1987+
1988+ /// Perform a flat (non-ostree) installation.
1989+ ///
1990+ /// The approach here is to:
1991+ /// 1. Pull the image into a composefs repository at `composefs/` on the target.
1992+ /// This reuses composefs-rs's SELinux labeling support and kernel installation flow.
1993+ /// The composefs repo is preserved at `/sysroot/composefs`; users who want to convert
1994+ /// to immutable composefs mode later can do so, and anyone who doesn't want the extra
1995+ /// metadata can simply `rm -rf /sysroot/composefs`.
1996+ /// 2. Check out the filesystem from the composefs repo to the target using `write_to_path`,
1997+ /// which supports reflink copies on filesystems that enable it (btrfs, XFS).
1998+ /// 3. Write the kernel and initramfs to `/boot/` from the composefs repo objects.
1999+ /// 4. Create a standard BLS entry pointing at `root=UUID=...` (not a composefs overlay).
2000+ #[ context( "Performing flat install" ) ]
2001+ async fn flat_install ( state : & State , rootfs : & RootSetup ) -> Result < ( ) > {
2002+ println ! ( "Installing in flat mode (experimental)" ) ;
2003+
2004+ // Step 1: Pull the image into the composefs repository on the target.
2005+ // allow_missing_fsverity=true because we don't use fsverity at boot time in flat mode.
2006+ let ( image_id, _verity) =
2007+ initialize_composefs_repository ( state, rootfs, true ) . await ?;
2008+
2009+ // Step 2: Build the filesystem tree from the pulled image.
2010+ let repo = crate :: bootc_composefs:: repo:: open_composefs_repo ( & rootfs. physical_root ) ?;
2011+ let mut fs = create_composefs_filesystem ( & repo, & image_id, None ) ?;
2012+
2013+ // Step 3: Apply SELinux labels from the image's file_contexts.
2014+ // Returns true if a policy was found and labels applied, false if no policy was found.
2015+ let _ = selabel:: selabel ( & mut fs, & repo) . context ( "Applying SELinux labels" ) ?;
2016+
2017+ // Step 4: Extract kernel/initramfs boot entries from the composefs tree.
2018+ let boot_entries = get_boot_resources ( & fs, & repo) ?;
2019+ let vmlinuz_entry = boot_entries
2020+ . into_iter ( )
2021+ . find_map ( |e| match e {
2022+ BootEntry :: UsrLibModulesVmLinuz ( v) => Some ( v) ,
2023+ _ => None ,
2024+ } )
2025+ . ok_or_else ( || anyhow ! ( "No vmlinuz kernel found in flat install image" ) ) ?;
2026+ let kernel_version = vmlinuz_entry. kver . as_ref ( ) . to_owned ( ) ;
2027+
2028+ // Step 5: Check out the filesystem to the target directory.
2029+ // On reflink-capable filesystems (btrfs, XFS) this efficiently shares blocks
2030+ // with the composefs object store.
2031+ let target_std = rootfs. physical_root_path . as_std_path ( ) . to_owned ( ) ;
2032+ tokio:: task:: block_in_place ( || write_to_path ( & repo, & fs. root , & target_std) )
2033+ . context ( "Checking out container rootfs to target" ) ?;
2034+
2035+ // Step 6: Write vmlinuz and initramfs to /boot/ on the target.
2036+ let vmlinuz_dest = format ! ( "boot/vmlinuz-{kernel_version}" ) ;
2037+ rootfs
2038+ . physical_root
2039+ . create_dir_all ( "boot" )
2040+ . context ( "Creating boot directory" ) ?;
2041+ rootfs
2042+ . physical_root
2043+ . atomic_write (
2044+ & vmlinuz_dest,
2045+ read_file ( & vmlinuz_entry. vmlinuz , & repo)
2046+ . context ( "Reading vmlinuz from composefs repo" ) ?
2047+ . as_ref ( ) ,
2048+ )
2049+ . with_context ( || format ! ( "Writing {vmlinuz_dest}" ) ) ?;
2050+
2051+ let initramfs_boot_path = Utf8PathBuf :: from ( format ! ( "/boot/initramfs-{kernel_version}.img" ) ) ;
2052+ if let Some ( initramfs) = & vmlinuz_entry. initramfs {
2053+ let initramfs_dest = format ! ( "boot/initramfs-{kernel_version}.img" ) ;
2054+ rootfs
2055+ . physical_root
2056+ . atomic_write (
2057+ & initramfs_dest,
2058+ read_file ( initramfs, & repo)
2059+ . context ( "Reading initramfs from composefs repo" ) ?
2060+ . as_ref ( ) ,
2061+ )
2062+ . with_context ( || format ! ( "Writing {initramfs_dest}" ) ) ?;
2063+ } else {
2064+ crate :: utils:: medium_visibility_warning (
2065+ "No initramfs found in image; boot may require manual initramfs generation" ,
2066+ ) ;
2067+ }
2068+
2069+ // Step 7: Create BLS entry.
2070+ let vmlinuz_boot_path = Utf8PathBuf :: from ( format ! ( "/boot/vmlinuz-{kernel_version}" ) ) ;
2071+ create_flat_bls_entry ( rootfs, & kernel_version, & vmlinuz_boot_path, & initramfs_boot_path) ?;
2072+
2073+ // Step 8: Install bootloader.
2074+ match state. config_opts . bootloader . as_ref ( ) {
2075+ Some ( crate :: spec:: Bootloader :: None ) => {
2076+ tracing:: debug!( "Skipping bootloader installation (bootloader=none)" ) ;
2077+ }
2078+ _ => {
2079+ if cfg ! ( target_arch = "s390x" ) {
2080+ let boot_uuid = rootfs
2081+ . get_boot_uuid ( ) ?
2082+ . or ( rootfs. rootfs_uuid . as_deref ( ) )
2083+ . ok_or_else ( || anyhow ! ( "No uuid for boot/root" ) ) ?;
2084+ install_bootloader_via_zipl ( & rootfs. device_info , boot_uuid) ?;
2085+ } else {
2086+ let target_root = rootfs
2087+ . target_root_path
2088+ . as_ref ( )
2089+ . unwrap_or ( & rootfs. physical_root_path ) ;
2090+ install_bootloader_via_bootupd (
2091+ & rootfs. device_info ,
2092+ target_root,
2093+ & state. config_opts ,
2094+ None ,
2095+ ) ?;
2096+ }
2097+ }
2098+ }
2099+
2100+ // Step 9: Write flat install marker.
2101+ rootfs
2102+ . physical_root
2103+ . atomic_write ( FLAT_INSTALL_MARKER , b"" )
2104+ . context ( "Writing flat install marker" ) ?;
2105+
2106+ Ok ( ( ) )
2107+ }
2108+
19022109async fn ostree_install ( state : & State , rootfs : & RootSetup , cleanup : Cleanup ) -> Result < ( ) > {
19032110 // We verify this upfront because it's currently required by bootupd
19042111 let boot_uuid = rootfs
@@ -1971,7 +2178,9 @@ async fn install_to_filesystem_impl(
19712178 }
19722179 }
19732180
1974- if state. composefs_options . composefs_backend {
2181+ if rootfs. flat {
2182+ flat_install ( state, rootfs) . await ?;
2183+ } else if state. composefs_options . composefs_backend {
19752184 // Load a fd for the mounted target physical root
19762185
19772186 let ( id, verity) = initialize_composefs_repository (
@@ -2608,6 +2817,7 @@ pub(crate) async fn install_to_filesystem(
26082817 boot,
26092818 kargs,
26102819 skip_finalize,
2820+ flat : fsopts. flat ,
26112821 } ;
26122822
26132823 install_to_filesystem_impl ( & state, & mut rootfs, cleanup) . await ?;
@@ -2658,6 +2868,7 @@ pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) ->
26582868 replace : opts. replace ,
26592869 skip_finalize : true ,
26602870 acknowledge_destructive : opts. acknowledge_destructive ,
2871+ flat : false ,
26612872 } ,
26622873 source_opts : opts. source_opts ,
26632874 target_opts : opts. target_opts ,
@@ -3050,4 +3261,59 @@ UUID=boot-uuid /boot ext4 defaults 0 0
30503261
30513262 Ok ( ( ) )
30523263 }
3264+
3265+ #[ test]
3266+ fn test_create_flat_bls_entry ( ) -> Result < ( ) > {
3267+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
3268+ td. create_dir_all ( "boot" ) ?;
3269+
3270+ let rootfs = RootSetup {
3271+ #[ cfg( feature = "install-to-disk" ) ]
3272+ luks_device : None ,
3273+ device_info : bootc_blockdev:: Device {
3274+ name : "vda" . to_string ( ) ,
3275+ serial : None ,
3276+ model : None ,
3277+ partlabel : None ,
3278+ parttype : None ,
3279+ partuuid : None ,
3280+ partn : None ,
3281+ children : None ,
3282+ size : 0 ,
3283+ maj_min : None ,
3284+ start : None ,
3285+ label : None ,
3286+ fstype : None ,
3287+ uuid : None ,
3288+ path : None ,
3289+ pttype : None ,
3290+ } ,
3291+ physical_root_path : Utf8PathBuf :: from ( "/test" ) ,
3292+ physical_root : td. try_clone ( ) ?,
3293+ target_root_path : None ,
3294+ rootfs_uuid : None ,
3295+ skip_finalize : false ,
3296+ boot : None ,
3297+ kargs : CmdlineOwned :: from ( "root=UUID=abc123 rw" ) ,
3298+ flat : true ,
3299+ } ;
3300+
3301+ let kernel_version = "6.12.0-100.fc41.x86_64" ;
3302+ let vmlinuz = Utf8PathBuf :: from ( format ! ( "/boot/vmlinuz-{kernel_version}" ) ) ;
3303+ let initramfs = Utf8PathBuf :: from ( format ! ( "/boot/initramfs-{kernel_version}.img" ) ) ;
3304+
3305+ create_flat_bls_entry ( & rootfs, kernel_version, & vmlinuz, & initramfs) ?;
3306+
3307+ // Read back the BLS entry and verify its contents
3308+ let entry_path = format ! ( "boot/loader/entries/flat-{kernel_version}.conf" ) ;
3309+ let content = String :: from_utf8 ( td. read ( & entry_path) ?) ?;
3310+
3311+ assert ! ( content. contains( & format!( "title Linux {kernel_version}" ) ) ) ;
3312+ assert ! ( content. contains( & format!( "version {kernel_version}" ) ) ) ;
3313+ assert ! ( content. contains( & format!( "linux /boot/vmlinuz-{kernel_version}" ) ) ) ;
3314+ assert ! ( content. contains( & format!( "initrd /boot/initramfs-{kernel_version}.img" ) ) ) ;
3315+ assert ! ( content. contains( "options root=UUID=abc123 rw" ) ) ;
3316+
3317+ Ok ( ( ) )
3318+ }
30533319}
0 commit comments