@@ -161,7 +161,7 @@ use std::time::Duration;
161161use aleph:: InstallAleph ;
162162use anyhow:: { Context , Result , anyhow, ensure} ;
163163use bootc_kernel_cmdline:: utf8:: { Cmdline , CmdlineOwned } ;
164- use bootc_utils:: CommandRunExt ;
164+ use bootc_utils:: { BwrapCmd , CommandRunExt } ;
165165use camino:: Utf8Path ;
166166use camino:: Utf8PathBuf ;
167167use canon_json:: CanonJsonSerialize ;
@@ -238,6 +238,9 @@ const DEFAULT_REPO_CONFIG: &[(&str, &str)] = &[
238238/// Kernel argument used to specify we want the rootfs mounted read-write by default
239239pub ( crate ) const RW_KARG : & str = "rw" ;
240240
241+ /// Marker file written to the target root to indicate a flat (non-ostree) install was performed.
242+ pub ( crate ) const FLAT_INSTALL_MARKER : & str = "etc/.bootc-flat" ;
243+
241244#[ derive( clap:: Args , Debug , Clone , Serialize , Deserialize , PartialEq , Eq ) ]
242245pub ( crate ) struct InstallTargetOpts {
243246 // TODO: A size specifier which allocates free space for the root in *addition* to the base container image size
@@ -488,6 +491,13 @@ pub(crate) struct InstallTargetFilesystemOpts {
488491 /// is then the responsibility of the invoking code to perform those operations.
489492 #[ clap( long) ]
490493 pub ( crate ) skip_finalize : bool ,
494+
495+ /// Install in "flat" mode: the container rootfs is copied directly to the target filesystem
496+ /// without ostree or composefs layering. This is experimental. Post-install, bootc day-2
497+ /// operations (upgrade, rollback, etc.) are unavailable on the installed system.
498+ /// Requires running inside the container to install (self-install mode).
499+ #[ clap( long) ]
500+ pub ( crate ) flat : bool ,
491501}
492502
493503#[ derive( Debug , Clone , clap:: Parser , PartialEq , Eq ) ]
@@ -1286,6 +1296,8 @@ pub(crate) struct RootSetup {
12861296 skip_finalize : bool ,
12871297 boot : Option < MountSpec > ,
12881298 pub ( crate ) kargs : CmdlineOwned ,
1299+ /// If true, perform a flat installation (no ostree/composefs)
1300+ pub ( crate ) flat : bool ,
12891301}
12901302
12911303fn require_boot_uuid ( spec : & MountSpec ) -> Result < & str > {
@@ -1899,6 +1911,197 @@ impl BoundImages {
18991911 }
19001912}
19011913
1914+ /// Copy the running container's rootfs to a target directory using `cp --archive`.
1915+ async fn copy_container_rootfs_to_target ( target_path : & Utf8PathBuf ) -> Result < ( ) > {
1916+ let target_path_str = target_path. to_string ( ) ;
1917+ crate :: utils:: async_task_with_spinner (
1918+ "Copying container rootfs to target" ,
1919+ tokio:: task:: spawn_blocking ( move || {
1920+ Task :: new ( "Copying rootfs" , "cp" )
1921+ . args ( [
1922+ "--archive" ,
1923+ "--one-file-system" ,
1924+ "--no-target-directory" ,
1925+ "/" ,
1926+ & target_path_str,
1927+ ] )
1928+ . run ( )
1929+ } ) ,
1930+ )
1931+ . await ?
1932+ }
1933+
1934+ /// Copy vmlinuz and initramfs from `usr/lib/modules/<version>/` to `boot/` in the target root.
1935+ ///
1936+ /// Returns the absolute-path strings suitable for use in a BLS config entry, e.g.
1937+ /// (`/boot/vmlinuz-<ver>`, `/boot/initramfs-<ver>.img`).
1938+ #[ context( "Copying kernel to /boot" ) ]
1939+ fn copy_kernel_to_boot (
1940+ root : & Dir ,
1941+ kernel : & crate :: kernel:: KernelInternal ,
1942+ ) -> Result < ( Utf8PathBuf , Utf8PathBuf ) > {
1943+ let version = & kernel. kernel . version ;
1944+ match & kernel. k_type {
1945+ crate :: kernel:: KernelType :: Vmlinuz { path, initramfs } => {
1946+ let vmlinuz_dest = format ! ( "boot/vmlinuz-{version}" ) ;
1947+ let initramfs_dest = format ! ( "boot/initramfs-{version}.img" ) ;
1948+
1949+ // Copy vmlinuz
1950+ let vmlinuz_data = root
1951+ . read ( path. as_str ( ) )
1952+ . with_context ( || format ! ( "Reading kernel {path}" ) ) ?;
1953+ root. atomic_write ( & vmlinuz_dest, & vmlinuz_data)
1954+ . with_context ( || format ! ( "Writing {vmlinuz_dest}" ) ) ?;
1955+
1956+ // Copy initramfs (it may not exist; dracut will regenerate it if missing)
1957+ if root. try_exists ( initramfs. as_str ( ) ) ? {
1958+ let initramfs_data = root
1959+ . read ( initramfs. as_str ( ) )
1960+ . with_context ( || format ! ( "Reading initramfs {initramfs}" ) ) ?;
1961+ root. atomic_write ( & initramfs_dest, & initramfs_data)
1962+ . with_context ( || format ! ( "Writing {initramfs_dest}" ) ) ?;
1963+ }
1964+
1965+ // Return absolute paths for use in BLS entry
1966+ Ok ( (
1967+ Utf8PathBuf :: from ( format ! ( "/boot/vmlinuz-{version}" ) ) ,
1968+ Utf8PathBuf :: from ( format ! ( "/boot/initramfs-{version}.img" ) ) ,
1969+ ) )
1970+ }
1971+ crate :: kernel:: KernelType :: Uki { .. } => {
1972+ anyhow:: bail!( "Flat install with UKI kernels is not yet supported" )
1973+ }
1974+ }
1975+ }
1976+
1977+ /// Run dracut inside the target to regenerate the initramfs, omitting ostree-specific modules.
1978+ ///
1979+ /// If dracut is not found in the target the warning is emitted and we continue.
1980+ fn regenerate_initramfs_for_flat (
1981+ target_path : & Utf8PathBuf ,
1982+ kernel_version : & str ,
1983+ initramfs_boot_path : & Utf8PathBuf ,
1984+ ) -> Result < ( ) > {
1985+ // Check if dracut is available in the target
1986+ let has_dracut = target_path. join ( "usr/bin/dracut" ) . try_exists ( ) ?
1987+ || target_path. join ( "usr/sbin/dracut" ) . try_exists ( ) ?;
1988+ if !has_dracut {
1989+ crate :: utils:: medium_visibility_warning (
1990+ "dracut not found in target; initramfs may require manual regeneration" ,
1991+ ) ;
1992+ return Ok ( ( ) ) ;
1993+ }
1994+
1995+ println ! ( "Regenerating initramfs (omitting ostree modules)" ) ;
1996+ BwrapCmd :: new ( target_path)
1997+ . run ( [
1998+ "dracut" ,
1999+ "--force" ,
2000+ "--no-hostonly" ,
2001+ "--omit" ,
2002+ "ostree" ,
2003+ initramfs_boot_path. as_str ( ) ,
2004+ kernel_version,
2005+ ] )
2006+ . context ( "Regenerating initramfs via dracut" )
2007+ }
2008+
2009+ /// Write a BLS entry for a flat (non-ostree) installation.
2010+ #[ context( "Creating flat BLS entry" ) ]
2011+ fn create_flat_bls_entry (
2012+ rootfs : & RootSetup ,
2013+ kernel_version : & str ,
2014+ vmlinuz_boot_path : & Utf8PathBuf ,
2015+ initramfs_boot_path : & Utf8PathBuf ,
2016+ ) -> Result < ( ) > {
2017+ use crate :: parsers:: bls_config:: { BLSConfig , BLSConfigType } ;
2018+
2019+ let mut cfg = BLSConfig :: default ( ) ;
2020+ cfg. with_title ( format ! ( "Linux {kernel_version}" ) )
2021+ . with_version ( kernel_version. to_string ( ) )
2022+ . with_cfg ( BLSConfigType :: NonEFI {
2023+ linux : vmlinuz_boot_path. clone ( ) ,
2024+ initrd : vec ! [ initramfs_boot_path. clone( ) ] ,
2025+ options : Some ( rootfs. kargs . clone ( ) ) ,
2026+ } ) ;
2027+
2028+ let entry_path = format ! ( "boot/loader/entries/flat-{kernel_version}.conf" ) ;
2029+ rootfs
2030+ . physical_root
2031+ . create_dir_all ( "boot/loader/entries" )
2032+ . context ( "Creating boot/loader/entries" ) ?;
2033+
2034+ let content = format ! ( "{cfg}" ) ;
2035+ rootfs
2036+ . physical_root
2037+ . atomic_write ( & entry_path, content. as_bytes ( ) )
2038+ . with_context ( || format ! ( "Writing BLS entry {entry_path}" ) ) ?;
2039+
2040+ tracing:: debug!( "Wrote BLS entry: {entry_path}" ) ;
2041+ Ok ( ( ) )
2042+ }
2043+
2044+ /// Perform a flat (non-ostree) installation: copy the container rootfs directly to the target
2045+ /// filesystem, then set up kernel, initramfs, BLS entry, and bootloader.
2046+ #[ context( "Performing flat install" ) ]
2047+ async fn flat_install ( state : & State , rootfs : & RootSetup ) -> Result < ( ) > {
2048+ if !state. source . in_host_mountns {
2049+ anyhow:: bail!(
2050+ "--flat mode requires running inside the container to install; \
2051+ --source-imgref is not yet supported with --flat"
2052+ ) ;
2053+ }
2054+
2055+ let target_path = rootfs. physical_root_path . clone ( ) ;
2056+ println ! ( "Installing in flat mode (experimental)" ) ;
2057+
2058+ // Step 1: Copy container rootfs to target
2059+ copy_container_rootfs_to_target ( & target_path) . await ?;
2060+
2061+ // Step 2: Find kernel in the target root
2062+ let kernel = crate :: kernel:: find_kernel ( & rootfs. physical_root ) ?
2063+ . ok_or_else ( || anyhow ! ( "No kernel found in flat install target" ) ) ?;
2064+ let kernel_version = kernel. kernel . version . clone ( ) ;
2065+
2066+ // Step 3: Copy kernel and initramfs to /boot/<ver>
2067+ let ( vmlinuz_boot_path, initramfs_boot_path) =
2068+ copy_kernel_to_boot ( & rootfs. physical_root , & kernel) ?;
2069+
2070+ // Step 4: Regenerate initramfs via dracut (omit ostree modules)
2071+ regenerate_initramfs_for_flat ( & target_path, & kernel_version, & initramfs_boot_path) ?;
2072+
2073+ // Step 5: Create BLS entry
2074+ create_flat_bls_entry ( rootfs, & kernel_version, & vmlinuz_boot_path, & initramfs_boot_path) ?;
2075+
2076+ // Step 6: Install bootloader
2077+ if cfg ! ( target_arch = "s390x" ) {
2078+ let boot_uuid = rootfs
2079+ . get_boot_uuid ( ) ?
2080+ . or ( rootfs. rootfs_uuid . as_deref ( ) )
2081+ . ok_or_else ( || anyhow ! ( "No uuid for boot/root" ) ) ?;
2082+ crate :: bootloader:: install_via_zipl ( & rootfs. device_info , boot_uuid) ?;
2083+ } else {
2084+ let target_root = rootfs
2085+ . target_root_path
2086+ . as_ref ( )
2087+ . unwrap_or ( & rootfs. physical_root_path ) ;
2088+ crate :: bootloader:: install_via_bootupd (
2089+ & rootfs. device_info ,
2090+ target_root,
2091+ & state. config_opts ,
2092+ None , // No deployment path for flat installs
2093+ ) ?;
2094+ }
2095+
2096+ // Step 7: Write flat install marker
2097+ rootfs
2098+ . physical_root
2099+ . atomic_write ( FLAT_INSTALL_MARKER , b"" )
2100+ . context ( "Writing flat install marker" ) ?;
2101+
2102+ Ok ( ( ) )
2103+ }
2104+
19022105async fn ostree_install ( state : & State , rootfs : & RootSetup , cleanup : Cleanup ) -> Result < ( ) > {
19032106 // We verify this upfront because it's currently required by bootupd
19042107 let boot_uuid = rootfs
@@ -1971,7 +2174,9 @@ async fn install_to_filesystem_impl(
19712174 }
19722175 }
19732176
1974- if state. composefs_options . composefs_backend {
2177+ if rootfs. flat {
2178+ flat_install ( state, rootfs) . await ?;
2179+ } else if state. composefs_options . composefs_backend {
19752180 // Load a fd for the mounted target physical root
19762181
19772182 let ( id, verity) = initialize_composefs_repository (
@@ -2608,6 +2813,7 @@ pub(crate) async fn install_to_filesystem(
26082813 boot,
26092814 kargs,
26102815 skip_finalize,
2816+ flat : fsopts. flat ,
26112817 } ;
26122818
26132819 install_to_filesystem_impl ( & state, & mut rootfs, cleanup) . await ?;
@@ -2658,6 +2864,7 @@ pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) ->
26582864 replace : opts. replace ,
26592865 skip_finalize : true ,
26602866 acknowledge_destructive : opts. acknowledge_destructive ,
2867+ flat : false ,
26612868 } ,
26622869 source_opts : opts. source_opts ,
26632870 target_opts : opts. target_opts ,
@@ -3050,4 +3257,59 @@ UUID=boot-uuid /boot ext4 defaults 0 0
30503257
30513258 Ok ( ( ) )
30523259 }
3260+
3261+ #[ test]
3262+ fn test_create_flat_bls_entry ( ) -> Result < ( ) > {
3263+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
3264+ td. create_dir_all ( "boot" ) ?;
3265+
3266+ let rootfs = RootSetup {
3267+ #[ cfg( feature = "install-to-disk" ) ]
3268+ luks_device : None ,
3269+ device_info : bootc_blockdev:: Device {
3270+ name : "vda" . to_string ( ) ,
3271+ serial : None ,
3272+ model : None ,
3273+ partlabel : None ,
3274+ parttype : None ,
3275+ partuuid : None ,
3276+ partn : None ,
3277+ children : None ,
3278+ size : 0 ,
3279+ maj_min : None ,
3280+ start : None ,
3281+ label : None ,
3282+ fstype : None ,
3283+ uuid : None ,
3284+ path : None ,
3285+ pttype : None ,
3286+ } ,
3287+ physical_root_path : Utf8PathBuf :: from ( "/test" ) ,
3288+ physical_root : td. try_clone ( ) ?,
3289+ target_root_path : None ,
3290+ rootfs_uuid : None ,
3291+ skip_finalize : false ,
3292+ boot : None ,
3293+ kargs : CmdlineOwned :: from ( "root=UUID=abc123 rw" ) ,
3294+ flat : true ,
3295+ } ;
3296+
3297+ let kernel_version = "6.12.0-100.fc41.x86_64" ;
3298+ let vmlinuz = Utf8PathBuf :: from ( format ! ( "/boot/vmlinuz-{kernel_version}" ) ) ;
3299+ let initramfs = Utf8PathBuf :: from ( format ! ( "/boot/initramfs-{kernel_version}.img" ) ) ;
3300+
3301+ create_flat_bls_entry ( & rootfs, kernel_version, & vmlinuz, & initramfs) ?;
3302+
3303+ // Read back the BLS entry and verify its contents
3304+ let entry_path = format ! ( "boot/loader/entries/flat-{kernel_version}.conf" ) ;
3305+ let content = String :: from_utf8 ( td. read ( & entry_path) ?) ?;
3306+
3307+ assert ! ( content. contains( & format!( "title Linux {kernel_version}" ) ) ) ;
3308+ assert ! ( content. contains( & format!( "version {kernel_version}" ) ) ) ;
3309+ assert ! ( content. contains( & format!( "linux /boot/vmlinuz-{kernel_version}" ) ) ) ;
3310+ assert ! ( content. contains( & format!( "initrd /boot/initramfs-{kernel_version}.img" ) ) ) ;
3311+ assert ! ( content. contains( "options root=UUID=abc123 rw" ) ) ;
3312+
3313+ Ok ( ( ) )
3314+ }
30533315}
0 commit comments