@@ -21,6 +21,7 @@ use camino::Utf8PathBuf;
21
21
use cap_std:: fs:: Dir ;
22
22
use cap_std_ext:: cap_std;
23
23
use cap_std_ext:: prelude:: CapStdExtDirExt ;
24
+ use clap:: ValueEnum ;
24
25
use rustix:: fs:: MetadataExt ;
25
26
26
27
use fn_error_context:: context;
@@ -44,6 +45,7 @@ const BOOT: &str = "boot";
44
45
const RUN_BOOTC : & str = "/run/bootc" ;
45
46
/// This is an ext4 special directory we need to ignore.
46
47
const LOST_AND_FOUND : & str = "lost+found" ;
48
+ pub ( crate ) const ARCH_USES_EFI : bool = cfg ! ( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ;
47
49
48
50
/// Kernel argument used to specify we want the rootfs mounted read-write by default
49
51
const RW_KARG : & str = "rw" ;
@@ -117,6 +119,28 @@ pub(crate) struct InstallOpts {
117
119
pub ( crate ) config_opts : InstallConfigOpts ,
118
120
}
119
121
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
+
120
144
/// Options for installing to a filesystem
121
145
#[ derive( Debug , Clone , clap:: Args ) ]
122
146
pub ( crate ) struct InstallTargetFilesystemOpts {
@@ -141,9 +165,10 @@ pub(crate) struct InstallTargetFilesystemOpts {
141
165
#[ clap( long) ]
142
166
pub ( crate ) boot_mount_spec : Option < String > ,
143
167
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.
145
170
#[ clap( long) ]
146
- pub ( crate ) wipe : bool ,
171
+ pub ( crate ) replace : Option < ReplaceMode > ,
147
172
}
148
173
149
174
/// Perform an installation to a mounted filesystem.
@@ -592,6 +617,8 @@ pub(crate) struct RootSetup {
592
617
device : Utf8PathBuf ,
593
618
rootfs : Utf8PathBuf ,
594
619
rootfs_fd : Dir ,
620
+ /// If true, do not try to remount the root read-only and flush the journal, etc.
621
+ skip_finalize : bool ,
595
622
boot : MountSpec ,
596
623
kargs : Vec < String > ,
597
624
}
@@ -826,9 +853,11 @@ async fn install_to_filesystem_impl(state: &State, rootfs: &mut RootSetup) -> Re
826
853
. run ( ) ?;
827
854
828
855
// 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
+ }
832
861
}
833
862
834
863
Ok ( ( ) )
@@ -900,6 +929,36 @@ fn require_empty_rootdir(rootfs_fd: &Dir) -> Result<()> {
900
929
Ok ( ( ) )
901
930
}
902
931
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
+
903
962
/// Implementation of the `bootc install-to-filsystem` CLI command.
904
963
pub ( crate ) async fn install_to_filesystem ( opts : InstallToFilesystemOpts ) -> Result < ( ) > {
905
964
// Gather global state, destructuring the provided options
@@ -909,35 +968,44 @@ pub(crate) async fn install_to_filesystem(opts: InstallToFilesystemOpts) -> Resu
909
968
let root_path = & fsopts. root_path ;
910
969
let rootfs_fd = Dir :: open_ambient_dir ( root_path, cap_std:: ambient_authority ( ) )
911
970
. 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) ?,
925
986
}
926
987
927
988
// Gather data about the root filesystem
928
989
let inspect = crate :: mount:: inspect_filesystem ( & fsopts. root_path ) ?;
929
990
930
991
// We support overriding the mount specification for root (i.e. LABEL vs UUID versus
931
992
// 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 )
934
995
} else {
935
996
let mut uuid = inspect
936
997
. uuid
937
998
. ok_or_else ( || anyhow ! ( "No filesystem uuid found in target root" ) ) ?;
938
999
uuid. insert_str ( 0 , "UUID=" ) ;
939
1000
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)
941
1009
} ;
942
1010
tracing:: debug!( "Root mount spec: {root_mount_spec}" ) ;
943
1011
@@ -967,15 +1035,6 @@ pub(crate) async fn install_to_filesystem(opts: InstallToFilesystemOpts) -> Resu
967
1035
// for GRUB (BIOS) and in the future zipl (I think).
968
1036
let backing_device = {
969
1037
let mut dev = inspect. source ;
970
- // Hack: trim bind mount information from source
971
- if dev. contains ( '[' ) {
972
- dev = inspect
973
- . sources
974
- . into_iter ( )
975
- . flatten ( )
976
- . next ( )
977
- . ok_or_else ( || anyhow ! ( "Expected `sources` in findmnt output" ) ) ?;
978
- }
979
1038
loop {
980
1039
tracing:: debug!( "Finding parents for {dev}" ) ;
981
1040
let mut parents = crate :: blockdev:: find_parent_devices ( & dev) ?. into_iter ( ) ;
@@ -1004,7 +1063,11 @@ pub(crate) async fn install_to_filesystem(opts: InstallToFilesystemOpts) -> Resu
1004
1063
// By default, we inject a boot= karg because things like FIPS compliance currently
1005
1064
// require checking in the initramfs.
1006
1065
let bootarg = format ! ( "boot={}" , & boot. source) ;
1007
- 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 < _ > > ( ) ;
1008
1071
1009
1072
let mut rootfs = RootSetup {
1010
1073
luks_device : None ,
@@ -1013,6 +1076,7 @@ pub(crate) async fn install_to_filesystem(opts: InstallToFilesystemOpts) -> Resu
1013
1076
rootfs_fd,
1014
1077
boot,
1015
1078
kargs,
1079
+ skip_finalize : matches ! ( fsopts. replace, Some ( ReplaceMode :: Alongside ) ) ,
1016
1080
} ;
1017
1081
1018
1082
install_to_filesystem_impl ( & state, & mut rootfs) . await ?;
0 commit comments