@@ -21,6 +21,7 @@ use camino::Utf8PathBuf;
2121use cap_std:: fs:: Dir ;
2222use cap_std_ext:: cap_std;
2323use cap_std_ext:: prelude:: CapStdExtDirExt ;
24+ use clap:: ValueEnum ;
2425use rustix:: fs:: MetadataExt ;
2526
2627use fn_error_context:: context;
@@ -44,6 +45,7 @@ const BOOT: &str = "boot";
4445const RUN_BOOTC : & str = "/run/bootc" ;
4546/// This is an ext4 special directory we need to ignore.
4647const LOST_AND_FOUND : & str = "lost+found" ;
48+ pub ( crate ) const ARCH_USES_EFI : bool = cfg ! ( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ;
4749
4850/// Kernel argument used to specify we want the rootfs mounted read-write by default
4951const RW_KARG : & str = "rw" ;
@@ -117,6 +119,28 @@ pub(crate) struct InstallOpts {
117119 pub ( crate ) config_opts : InstallConfigOpts ,
118120}
119121
122+ #[ derive( ValueEnum , Debug , Copy , Clone , PartialEq , Eq , Serialize , Deserialize ) ]
123+ #[ serde( rename_all = "kebab-case" ) ]
124+ pub ( crate ) enum ReplaceMode {
125+ /// Completely wipe the contents of the target filesystem. This cannot
126+ /// be done if the target filesystem is the one the system is booted from.
127+ Wipe ,
128+ /// This is a destructive operation in the sense that the bootloader state
129+ /// will have its contents wiped and replaced. However,
130+ /// the running system (and all files) will remain in place until reboot.
131+ ///
132+ /// As a corollary to this, you will also need to remove all the old operating
133+ /// system binaries after the reboot into the target system; this can be done
134+ /// with code in the new target system, or manually.
135+ Alongside ,
136+ }
137+
138+ impl std:: fmt:: Display for ReplaceMode {
139+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
140+ self . to_possible_value ( ) . unwrap ( ) . get_name ( ) . fmt ( f)
141+ }
142+ }
143+
120144/// Options for installing to a filesystem
121145#[ derive( Debug , Clone , clap:: Args ) ]
122146pub ( crate ) struct InstallTargetFilesystemOpts {
@@ -141,9 +165,10 @@ pub(crate) struct InstallTargetFilesystemOpts {
141165 #[ clap( long) ]
142166 pub ( crate ) boot_mount_spec : Option < String > ,
143167
144- /// Automatically wipe existing data on the filesystems.
168+ /// Initialize the system in-place; at the moment, only one mode for this is implemented.
169+ /// In the future, it may also be supported to set up an explicit "dual boot" system.
145170 #[ clap( long) ]
146- pub ( crate ) wipe : bool ,
171+ pub ( crate ) replace : Option < ReplaceMode > ,
147172}
148173
149174/// Perform an installation to a mounted filesystem.
@@ -592,6 +617,8 @@ pub(crate) struct RootSetup {
592617 device : Utf8PathBuf ,
593618 rootfs : Utf8PathBuf ,
594619 rootfs_fd : Dir ,
620+ /// If true, do not try to remount the root read-only and flush the journal, etc.
621+ skip_finalize : bool ,
595622 boot : MountSpec ,
596623 kargs : Vec < String > ,
597624}
@@ -826,9 +853,11 @@ async fn install_to_filesystem_impl(state: &State, rootfs: &mut RootSetup) -> Re
826853 . run ( ) ?;
827854
828855 // Finalize mounted filesystems
829- let bootfs = rootfs. rootfs . join ( "boot" ) ;
830- for fs in [ bootfs. as_path ( ) , rootfs. rootfs . as_path ( ) ] {
831- finalize_filesystem ( fs) ?;
856+ if !rootfs. skip_finalize {
857+ let bootfs = rootfs. rootfs . join ( "boot" ) ;
858+ for fs in [ bootfs. as_path ( ) , rootfs. rootfs . as_path ( ) ] {
859+ finalize_filesystem ( fs) ?;
860+ }
832861 }
833862
834863 Ok ( ( ) )
@@ -900,6 +929,36 @@ fn require_empty_rootdir(rootfs_fd: &Dir) -> Result<()> {
900929 Ok ( ( ) )
901930}
902931
932+ /// Remove all entries in a directory, but do not traverse across distinct devices.
933+ #[ context( "Removing entries (noxdev" ) ]
934+ fn remove_all_in_dir_no_xdev ( d : & Dir ) -> Result < ( ) > {
935+ let parent_dev = d. dir_metadata ( ) ?. dev ( ) ;
936+ for entry in d. entries ( ) ? {
937+ let entry = entry?;
938+ let entry_dev = entry. metadata ( ) ?. dev ( ) ;
939+ if entry_dev == parent_dev {
940+ d. remove_all_optional ( entry. file_name ( ) ) ?;
941+ }
942+ }
943+ anyhow:: Ok ( ( ) )
944+ }
945+
946+ #[ context( "Removing boot directory content" ) ]
947+ fn clean_boot_directories ( rootfs : & Dir ) -> Result < ( ) > {
948+ let bootdir = rootfs. open_dir ( BOOT ) . context ( "Opening /boot" ) ?;
949+ // This should not remove /boot/efi note.
950+ remove_all_in_dir_no_xdev ( & bootdir) ?;
951+ if ARCH_USES_EFI {
952+ if let Some ( efidir) = bootdir
953+ . open_dir_optional ( crate :: bootloader:: EFI_DIR )
954+ . context ( "Opening /boot/efi" ) ?
955+ {
956+ remove_all_in_dir_no_xdev ( & efidir) ?;
957+ }
958+ }
959+ Ok ( ( ) )
960+ }
961+
903962/// Implementation of the `bootc install-to-filsystem` CLI command.
904963pub ( crate ) async fn install_to_filesystem ( opts : InstallToFilesystemOpts ) -> Result < ( ) > {
905964 // Gather global state, destructuring the provided options
@@ -909,35 +968,44 @@ pub(crate) async fn install_to_filesystem(opts: InstallToFilesystemOpts) -> Resu
909968 let root_path = & fsopts. root_path ;
910969 let rootfs_fd = Dir :: open_ambient_dir ( root_path, cap_std:: ambient_authority ( ) )
911970 . with_context ( || format ! ( "Opening target root directory {root_path}" ) ) ?;
912- if fsopts. wipe {
913- let rootfs_fd = rootfs_fd. try_clone ( ) ?;
914- println ! ( "Wiping contents of root" ) ;
915- tokio:: task:: spawn_blocking ( move || {
916- for e in rootfs_fd. entries ( ) ? {
917- let e = e?;
918- rootfs_fd. remove_all_optional ( e. file_name ( ) ) ?;
919- }
920- anyhow:: Ok ( ( ) )
921- } )
922- . await ??;
923- } else {
924- require_empty_rootdir ( & rootfs_fd) ?;
971+ match fsopts. replace {
972+ Some ( ReplaceMode :: Wipe ) => {
973+ let rootfs_fd = rootfs_fd. try_clone ( ) ?;
974+ println ! ( "Wiping contents of root" ) ;
975+ tokio:: task:: spawn_blocking ( move || {
976+ for e in rootfs_fd. entries ( ) ? {
977+ let e = e?;
978+ rootfs_fd. remove_all_optional ( e. file_name ( ) ) ?;
979+ }
980+ anyhow:: Ok ( ( ) )
981+ } )
982+ . await ??;
983+ }
984+ Some ( ReplaceMode :: Alongside ) => clean_boot_directories ( & rootfs_fd) ?,
985+ None => require_empty_rootdir ( & rootfs_fd) ?,
925986 }
926987
927988 // Gather data about the root filesystem
928989 let inspect = crate :: mount:: inspect_filesystem ( & fsopts. root_path ) ?;
929990
930991 // We support overriding the mount specification for root (i.e. LABEL vs UUID versus
931992 // raw paths).
932- let root_mount_spec = if let Some ( s) = fsopts. root_mount_spec {
933- s
993+ let ( root_mount_spec, root_extra ) = if let Some ( s) = fsopts. root_mount_spec {
994+ ( s , None )
934995 } else {
935996 let mut uuid = inspect
936997 . uuid
937998 . ok_or_else ( || anyhow ! ( "No filesystem uuid found in target root" ) ) ?;
938999 uuid. insert_str ( 0 , "UUID=" ) ;
9391000 tracing:: debug!( "root {uuid}" ) ;
940- uuid
1001+ let opts = match inspect. fstype . as_str ( ) {
1002+ "btrfs" => {
1003+ let subvol = crate :: utils:: find_mount_option ( & inspect. options , "subvol" ) ;
1004+ subvol. map ( |vol| format ! ( "rootflags=subvol={vol}" ) )
1005+ }
1006+ _ => None ,
1007+ } ;
1008+ ( uuid, opts)
9411009 } ;
9421010 tracing:: debug!( "Root mount spec: {root_mount_spec}" ) ;
9431011
@@ -995,7 +1063,11 @@ pub(crate) async fn install_to_filesystem(opts: InstallToFilesystemOpts) -> Resu
9951063 // By default, we inject a boot= karg because things like FIPS compliance currently
9961064 // require checking in the initramfs.
9971065 let bootarg = format ! ( "boot={}" , & boot. source) ;
998- let kargs = vec ! [ rootarg, RW_KARG . to_string( ) , bootarg] ;
1066+ let kargs = [ rootarg]
1067+ . into_iter ( )
1068+ . chain ( root_extra)
1069+ . chain ( [ RW_KARG . to_string ( ) , bootarg] )
1070+ . collect :: < Vec < _ > > ( ) ;
9991071
10001072 let mut rootfs = RootSetup {
10011073 luks_device : None ,
@@ -1004,6 +1076,7 @@ pub(crate) async fn install_to_filesystem(opts: InstallToFilesystemOpts) -> Resu
10041076 rootfs_fd,
10051077 boot,
10061078 kargs,
1079+ skip_finalize : matches ! ( fsopts. replace, Some ( ReplaceMode :: Alongside ) ) ,
10071080 } ;
10081081
10091082 install_to_filesystem_impl ( & state, & mut rootfs) . await ?;
0 commit comments