Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 78 additions & 29 deletions crates/lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,8 @@ pub(crate) struct RootSetup {
pub(crate) physical_root_path: Utf8PathBuf,
/// Directory file descriptor for the above physical root.
pub(crate) physical_root: Dir,
/// Target root path /target.
pub(crate) target_root_path: Option<Utf8PathBuf>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I would like if we could go to having most things be file descriptor relative; it's unfortunate to need to add a new absolute path here.

But I understand that the bootupd install is using it today.

What I would find helpful is to understand a bit more strictly the meaing of "target" here. I think we're effectively using it to be "what the user provided as an install target" right?

And for ostree/bootc systems, that's always the mounted deployment root conventionally today when they say -v /:/target.

But that's just conventional - what would happen if they passed -v /sysroot:/target say?

The more I think about this the more I feel what we should do is detect when the passed root filesystem refers to either a mounted deployment root or the physical root, and automatically canonicalize internally to "physical root" and "deployment root" (which for non-ostree systems are the same thing), and then discard the user provided input path.

Copy link
Contributor Author

@HuijingHei HuijingHei Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I would find helpful is to understand a bit more strictly the meaing of "target" here. I think we're effectively using it to be "what the user provided as an install target" right?

And for ostree/bootc systems, that's always the mounted deployment root conventionally today when they say -v /:/target.

But that's just conventional - what would happen if they passed -v /sysroot:/target say?

The more I think about this the more I feel what we should do is detect when the passed root filesystem refers to either a mounted deployment root or the physical root, and automatically canonicalize internally to "physical root" and "deployment root" (which for non-ostree systems are the same thing), and then discard the user provided input path.

This sounds better and more sane, will try this later.

Copy link
Contributor Author

@HuijingHei HuijingHei Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what would happen if they passed -v /sysroot:/target say?

We still need /boot/efi (or separate /boot) to update the bootloader, if we pass -v /sysroot:/target, then how could we get the "physical root" like / (to get /boot/efi)?

pub(crate) rootfs_uuid: Option<String>,
/// True if we should skip finalizing
skip_finalize: bool,
Expand Down Expand Up @@ -1575,7 +1577,10 @@ async fn install_with_sysroot(
Bootloader::Grub => {
crate::bootloader::install_via_bootupd(
&rootfs.device_info,
&rootfs.physical_root_path,
&rootfs
.target_root_path
.clone()
.unwrap_or(rootfs.physical_root_path.clone()),
&state.config_opts,
Some(&deployment_path.as_str()),
)?;
Expand Down Expand Up @@ -1922,30 +1927,58 @@ fn remove_all_in_dir_no_xdev(d: &Dir, mount_err: bool) -> Result<()> {
anyhow::Ok(())
}

#[context("Removing boot directory content except loader dir on ostree")]
fn remove_all_except_loader_dirs(bootdir: &Dir, is_ostree: bool) -> Result<()> {
let entries = bootdir
.entries()
.context("Reading boot directory entries")?;

for entry in entries {
let entry = entry.context("Reading directory entry")?;
let file_name = entry.file_name();
let file_name = if let Some(n) = file_name.to_str() {
n
} else {
anyhow::bail!("Invalid non-UTF8 filename: {file_name:?} in /boot");
};

// Only preserve loader on ostree
if is_ostree && file_name.starts_with("loader") {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also relates to an issue (TODO link) where we should support preserving basically everything (including the bootloader entries on non-ostree) by default until the very end of the install. And ideally make the "commit" phase an optional step after.

This one also textually conflicts with #1727

Copy link
Contributor Author

@HuijingHei HuijingHei Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also relates to an issue (TODO link) where we should support preserving basically everything (including the bootloader entries on non-ostree) by default until the very end of the install. And ideally make the "commit" phase an optional step after.

I think you meant preserve everything under /boot & /boot/efi, then clean them before install bootloader, is this right?

This one also textually conflicts with #1727

A little confused, this requires empty rootfs when --replace is not set, but when using to-existing-root it is using ReplaceMode::Alongside by default, maybe I misunderstood this?

continue;
}

let etype = entry.file_type()?;
if etype == FileType::dir() {
// Open the directory and remove its contents
if let Some(subdir) = bootdir.open_dir_noxdev(&file_name)? {
remove_all_in_dir_no_xdev(&subdir, false)
.with_context(|| format!("Removing directory contents: {}", file_name))?;
}
} else {
bootdir
.remove_file_optional(&file_name)
.with_context(|| format!("Removing file: {}", file_name))?;
}
}
Ok(())
}

#[context("Removing boot directory content")]
fn clean_boot_directories(rootfs: &Dir, is_ostree: bool) -> Result<()> {
let bootdir =
crate::utils::open_dir_remount_rw(rootfs, BOOT.into()).context("Opening /boot")?;

if is_ostree {
// On ostree systems, the boot directory already has our desired format, we should only
// remove the bootupd-state.json file to avoid bootupctl complaining it already exists.
bootdir
.remove_file_optional("bootupd-state.json")
.context("removing bootupd-state.json")?;
} else {
// This should not remove /boot/efi note.
remove_all_in_dir_no_xdev(&bootdir, false).context("Emptying /boot")?;
// TODO: Discover the ESP the same way bootupd does it; we should also
// support not wiping the ESP.
if ARCH_USES_EFI {
if let Some(efidir) = bootdir
.open_dir_optional(crate::bootloader::EFI_DIR)
.context("Opening /boot/efi")?
{
remove_all_in_dir_no_xdev(&efidir, false)
.context("Emptying EFI system partition")?;
}
// This should not remove /boot/efi note.
remove_all_except_loader_dirs(&bootdir, is_ostree).context("Emptying /boot")?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm looks like this is going to conflict with #1727


// TODO: Discover the ESP the same way bootupd does it; we should also
// support not wiping the ESP.
if ARCH_USES_EFI {
if let Some(efidir) = bootdir
.open_dir_optional(crate::bootloader::EFI_DIR)
.context("Opening /boot/efi")?
{
remove_all_in_dir_no_xdev(&efidir, false).context("Emptying EFI system partition")?;
}
}

Expand Down Expand Up @@ -2082,6 +2115,18 @@ pub(crate) async fn install_to_filesystem(
.context("Mounting host / to {ALONGSIDE_ROOT_MOUNT}")?;
}

let target_root_path = fsopts.root_path.clone();
// Get a file descriptor for the root path /target
let target_rootfs_fd =
Dir::open_ambient_dir(&target_root_path, cap_std::ambient_authority())
.with_context(|| format!("Opening target root directory {target_root_path}"))?;

tracing::debug!("Target root filesystem: {target_root_path}");

if let Some(false) = target_rootfs_fd.is_mountpoint(".")? {
anyhow::bail!("Not a mountpoint: {target_root_path}");
}

// Check that the target is a directory
{
let root_path = &fsopts.root_path;
Expand All @@ -2095,10 +2140,7 @@ pub(crate) async fn install_to_filesystem(

// Check to see if this happens to be the real host root
if !fsopts.acknowledge_destructive {
let root_path = &fsopts.root_path;
let rootfs_fd = Dir::open_ambient_dir(root_path, cap_std::ambient_authority())
.with_context(|| format!("Opening target root directory {root_path}"))?;
warn_on_host_root(&rootfs_fd)?;
warn_on_host_root(&target_rootfs_fd)?;
}

// If we're installing to an ostree root, then find the physical root from
Expand All @@ -2114,7 +2156,8 @@ pub(crate) async fn install_to_filesystem(
};

// Get a file descriptor for the root path
let rootfs_fd = {
// It will be /target/sysroot on ostree OS, or will be /target
let rootfs_fd = if is_already_ostree {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I know you're just changing existing code, I think it'd be cleaner to fold this conditional with the one above.

I had to read this whole logic a few times to notice that a key bit here is we're mutating the user input fsopts.root_path = possible_physical_root just above, and that's an important aspect.

let root_path = &fsopts.root_path;
let rootfs_fd = Dir::open_ambient_dir(&fsopts.root_path, cap_std::ambient_authority())
.with_context(|| format!("Opening target root directory {root_path}"))?;
Expand All @@ -2125,6 +2168,8 @@ pub(crate) async fn install_to_filesystem(
anyhow::bail!("Not a mountpoint: {root_path}");
}
rootfs_fd
} else {
target_rootfs_fd.try_clone()?
};

match fsopts.replace {
Expand All @@ -2134,7 +2179,9 @@ pub(crate) async fn install_to_filesystem(
tokio::task::spawn_blocking(move || remove_all_in_dir_no_xdev(&rootfs_fd, true))
.await??;
}
Some(ReplaceMode::Alongside) => clean_boot_directories(&rootfs_fd, is_already_ostree)?,
Some(ReplaceMode::Alongside) => {
clean_boot_directories(&target_rootfs_fd, is_already_ostree)?
}
None => require_empty_rootdir(&rootfs_fd)?,
}

Expand Down Expand Up @@ -2179,7 +2226,7 @@ pub(crate) async fn install_to_filesystem(

let boot_is_mount = {
let root_dev = rootfs_fd.dir_metadata()?.dev();
let boot_dev = rootfs_fd
let boot_dev = target_rootfs_fd
.symlink_metadata_optional(BOOT)?
.ok_or_else(|| {
anyhow!("No /{BOOT} directory found in root; this is is currently required")
Expand All @@ -2190,9 +2237,10 @@ pub(crate) async fn install_to_filesystem(
};
// Find the UUID of /boot because we need it for GRUB.
let boot_uuid = if boot_is_mount {
let boot_path = fsopts.root_path.join(BOOT);
let boot_path = target_root_path.join(BOOT);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be a key change here. I guess we needed this because in the ostree case we'd mutated fsopts.root_path to refer to the physical root, and when /boot is actually a separate mount point that was broken on ostree setups?

But this relates to what I mean above - we could instead just know to look at the deployment's /boot explicitly.

Copy link
Contributor Author

@HuijingHei HuijingHei Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be a key change here. I guess we needed this because in the ostree case we'd mutated fsopts.root_path to refer to the physical root, and when /boot is actually a separate mount point that was broken on ostree setups?

I am a little confused, do you mean "physical root" is /target, and "deployment root" is /target/sysroot?

From following log (copy from bug), we passed /target/sysroot, but we should pass /target.

> bootupctl backend install --write-uuid -vvvv --update-firmware --auto --device /dev/vda /target/sysroot
...
[DEBUG bootupd::efi] Mounted at "/target/sysroot/boot"
...
Could not find /boot/efi/EFI when installing "redhat/grub.cfg"

And I do not understand /sysroot/boot and /boot correctly, are they the same?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For non-bootc/ostree setups, "physical root == root", so the user always passes -v /:/target - in other words /target inside there is always the filesystem root.

For bootc/ostree setups, they are not the same. What I am saying though is we should support being passed either thing (/sysroot or /) for /target and automatically do the right thing, and resolve the right roots.

And I do not understand /sysroot/boot and /boot correctly, are they the same?

Typically we only mount the separate boot partition in the target root, not the physical root.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood, thank you for the detail info.

For bootc/ostree setups, they are not the same. What I am saying though is we should support being passed either thing (/sysroot or /) for /target and automatically do the right thing, and resolve the right roots.

Try to catch your meaning, for bootc/ostree setups, if passed -v /sysroot:/target, my concern is how to get ESP part, but looks can get /target part, then get its parent disk, then get separate boot part if there is (maybe need to mount it to /target/boot?), and ESP.

podman run --rm -it --privileged -v /dev:/dev -v /var/lib/containers:/var/lib/containers -v /sysroot:/target \
--pid=host --security-opt label=type:unconfined_t \
quay.io/fedora/fedora-bootc:rawhide bash
bash-5.3# cd /target
bash-5.3# findmnt .
TARGET  SOURCE    FSTYPE OPTIONS
/target /dev/vda4 xfs    rw,relatime,seclabel,attr2,inode64,logbufs=8,logbsize=32k,prjquota

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will try this later.

tracing::debug!("boot_path={boot_path}");
let u = bootc_mount::inspect_filesystem(&boot_path)
.context("Inspecting /{BOOT}")?
.with_context(|| format!("Inspecting /{BOOT}"))?
.uuid
.ok_or_else(|| anyhow!("No UUID found for /{BOOT}"))?;
Some(u)
Expand Down Expand Up @@ -2279,6 +2327,7 @@ pub(crate) async fn install_to_filesystem(
device_info,
physical_root_path: fsopts.root_path,
physical_root: rootfs_fd,
target_root_path: Some(target_root_path.clone()),
rootfs_uuid: inspect.uuid.clone(),
boot,
kargs,
Expand Down
1 change: 1 addition & 0 deletions crates/lib/src/install/baseline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,7 @@ pub(crate) fn install_create_rootfs(
device_info,
physical_root_path,
physical_root,
target_root_path: None,
rootfs_uuid: Some(root_uuid.to_string()),
boot,
kargs,
Expand Down