@@ -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,34 @@ 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+ let efidir = bootdir
953+ . open_dir ( crate :: bootloader:: EFI_DIR )
954+ . context ( "Opening /boot/efi" ) ?;
955+ remove_all_in_dir_no_xdev ( & efidir) ?;
956+ }
957+ Ok ( ( ) )
958+ }
959+
903960/// Implementation of the `bootc install-to-filsystem` CLI command.
904961pub ( crate ) async fn install_to_filesystem ( opts : InstallToFilesystemOpts ) -> Result < ( ) > {
905962 // Gather global state, destructuring the provided options
@@ -909,19 +966,21 @@ pub(crate) async fn install_to_filesystem(opts: InstallToFilesystemOpts) -> Resu
909966 let root_path = & fsopts. root_path ;
910967 let rootfs_fd = Dir :: open_ambient_dir ( root_path, cap_std:: ambient_authority ( ) )
911968 . 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) ?;
969+ match fsopts. replace {
970+ Some ( ReplaceMode :: Wipe ) => {
971+ let rootfs_fd = rootfs_fd. try_clone ( ) ?;
972+ println ! ( "Wiping contents of root" ) ;
973+ tokio:: task:: spawn_blocking ( move || {
974+ for e in rootfs_fd. entries ( ) ? {
975+ let e = e?;
976+ rootfs_fd. remove_all_optional ( e. file_name ( ) ) ?;
977+ }
978+ anyhow:: Ok ( ( ) )
979+ } )
980+ . await ??;
981+ }
982+ Some ( ReplaceMode :: Alongside ) => clean_boot_directories ( & rootfs_fd) ?,
983+ None => require_empty_rootdir ( & rootfs_fd) ?,
925984 }
926985
927986 // Gather data about the root filesystem
@@ -1013,6 +1072,7 @@ pub(crate) async fn install_to_filesystem(opts: InstallToFilesystemOpts) -> Resu
10131072 rootfs_fd,
10141073 boot,
10151074 kargs,
1075+ skip_finalize : matches ! ( fsopts. replace, Some ( ReplaceMode :: Alongside ) ) ,
10161076 } ;
10171077
10181078 install_to_filesystem_impl ( & state, & mut rootfs) . await ?;
0 commit comments