Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ jobs:
# Install tests
sudo bootc-integration-tests install-alongside localhost/bootc-install

# inspect system state after the install tests.
sudo lsblk
sudo mount

# system-reinstall-bootc tests
cargo build --release -p system-reinstall-bootc

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

77 changes: 63 additions & 14 deletions crates/lib/src/bootloader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::fs::create_dir_all;
use std::process::Command;

use anyhow::{Context, Result, anyhow, bail};
use bootc_utils::CommandRunExt;
use bootc_utils::{BwrapCmd, CommandRunExt};
use camino::Utf8Path;
use cap_std_ext::cap_std::fs::Dir;
use cap_std_ext::dirext::CapStdExtDirExt;
Expand Down Expand Up @@ -91,22 +91,71 @@ pub(crate) fn install_via_bootupd(
// bootc defaults to only targeting the platform boot method.
let bootupd_opts = (!configopts.generic_image).then_some(["--update-firmware", "--auto"]);

let abs_deployment_path = deployment_path.map(|v| rootfs.join(v));
let src_root_arg = if let Some(p) = abs_deployment_path.as_deref() {
vec!["--src-root", p.as_str()]
// When not running inside the target container (through `--src-imgref`) we use
// will bwrap as a chroot to run bootupctl from the deployment.
// This makes sure we use binaries from the target image rather than the buildroot.
// In that case, the target rootfs is replaced with `/` because this is just used by
// bootupd to find the backing device.
let rootfs_mount = if deployment_path.is_none() {
rootfs.as_str()
} else {
vec![]
"/"
};
let devpath = device.path();

println!("Installing bootloader via bootupd");
Command::new("bootupctl")
.args(["backend", "install", "--write-uuid"])
.args(verbose)
.args(bootupd_opts.iter().copied().flatten())
.args(src_root_arg)
.args(["--device", devpath.as_str(), rootfs.as_str()])
.log_debug()
.run_inherited_with_cmd_context()

// Build the bootupctl arguments
let mut bootupd_args: Vec<&str> = vec!["backend", "install", "--write-uuid"];
if let Some(v) = verbose {
bootupd_args.push(v);
}

if let Some(ref opts) = bootupd_opts {
bootupd_args.extend(opts.iter().copied());
}
bootupd_args.extend(["--device", device.path().as_str(), rootfs_mount]);

// Run inside a bwrap container. It takes care of mounting and creating
// the necessary API filesystems in the target deployment and acts as
// a nicer `chroot`.
if let Some(deploy) = deployment_path {
let target_root = rootfs.join(deploy);
let boot_path = rootfs.join("boot");

tracing::debug!("Running bootupctl via bwrap in {}", target_root);

// Prepend "bootupctl" to the args for bwrap
let mut bwrap_args = vec!["bootupctl"];
bwrap_args.extend(bootupd_args);

let mut cmd = BwrapCmd::new(&target_root)
// Bind mount /boot from the physical target root so bootupctl can find
// the boot partition and install the bootloader there
.bind(&boot_path, &"/boot")
// Bind the target block device inside the bwrap container so bootupctl can access it
.bind_device(device.path().as_str());

// Also bind all partitions of the tafet block device
for partition in &device.partitions {
cmd = cmd.bind_device(&partition.node);
}

// The $PATH in the bwrap env is not complete enough for some images
// so we inject a reasonnable default.
// This is causing bootupctl and/or sfdisk binaries
// to be not found with fedora 43.
cmd.setenv(
"PATH",
"/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin",
)
.run(bwrap_args)
} else {
// Running directly without chroot
Command::new("bootupctl")
.args(&bootupd_args)
.log_debug()
.run_inherited_with_cmd_context()
}
}

#[context("Installing bootloader")]
Expand Down
1 change: 1 addition & 0 deletions crates/utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ repository = "https://github.com/bootc-dev/bootc"
# Workspace dependencies
anstream = { workspace = true }
anyhow = { workspace = true }
cap-std-ext = {workspace = true, features = ["fs_utf8"] }
chrono = { workspace = true, features = ["std"] }
owo-colors = { workspace = true }
rustix = { workspace = true }
Expand Down
106 changes: 106 additions & 0 deletions crates/utils/src/bwrap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/// Builder for running commands inside a target os tree using bubblewrap (bwrap).
use std::borrow::Cow;
use std::ffi::OsStr;
use std::os::fd::AsRawFd;
use std::process::Command;

use anyhow::Result;
use cap_std_ext::camino::{Utf8Path, Utf8PathBuf};
use cap_std_ext::cap_std::fs::Dir;

use crate::CommandRunExt;

/// Builder for running commands inside a target directory using bwrap.
#[derive(Debug)]
pub struct BwrapCmd<'a> {
/// The target directory to use as root for the container
chroot_path: Cow<'a, Utf8Path>,
/// Bind mounts in format (source, target)
bind_mounts: Vec<(&'a str, &'a str)>,
/// Device nodes to bind into the container
devices: Vec<&'a str>,
/// Environment variables to set
env_vars: Vec<(&'a str, &'a str)>,
}

impl<'a> BwrapCmd<'a> {
/// Create a new BwrapCmd builder with a root directory as a File Descriptor.
#[allow(dead_code)]
pub fn new_with_dir(path: &'a Dir) -> Self {
let fd_path: String = format!("/proc/self/fd/{}", path.as_raw_fd());
Self {
chroot_path: Cow::Owned(Utf8PathBuf::from(&fd_path)),
bind_mounts: Vec::new(),
devices: Vec::new(),
env_vars: Vec::new(),
}
}

/// Create a new BwrapCmd builder with a root directory
pub fn new(path: &'a Utf8Path) -> Self {
Self {
chroot_path: Cow::Borrowed(path),
bind_mounts: Vec::new(),
devices: Vec::new(),
env_vars: Vec::new(),
}
}

/// Add a bind mount from source to target inside the container.
pub fn bind(
mut self,
source: &'a impl AsRef<Utf8Path>,
target: &'a impl AsRef<Utf8Path>,
) -> Self {
self.bind_mounts
.push((source.as_ref().as_str(), target.as_ref().as_str()));
self
}

/// Bind a device node into the container.
pub fn bind_device(mut self, device: &'a str) -> Self {
self.devices.push(device);
self
}

/// Set an environment variable for the command.
pub fn setenv(mut self, key: &'a str, value: &'a str) -> Self {
self.env_vars.push((key, value));
self
}

/// Run the specified command inside the container.
pub fn run<S: AsRef<OsStr>>(self, args: impl IntoIterator<Item = S>) -> Result<()> {
let mut cmd = Command::new("bwrap");

// Bind the root filesystem
cmd.args(["--bind", self.chroot_path.as_str(), "/"]);

// Setup API filesystems
// See https://systemd.io/API_FILE_SYSTEMS/
cmd.args(["--proc", "/proc"]);
cmd.args(["--dev", "/dev"]);
cmd.args(["--ro-bind", "/sys", "/sys"]);

// Add bind mounts
for (source, target) in &self.bind_mounts {
cmd.args(["--bind", source, target]);
}

// Add device bind mounts
for device in self.devices {
Copy link
Collaborator

Choose a reason for hiding this comment

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

What would actually probably work most reliably in general is for us to pass the block device as a file descriptor.

Copy link
Contributor Author

@jbtrystram jbtrystram Jan 28, 2026

Choose a reason for hiding this comment

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

IIUC that would require bootupd to accept the device as a FD rather than a path. so we can't change that here for now, correct ?

Copy link
Contributor Author

@jbtrystram jbtrystram Jan 29, 2026

Choose a reason for hiding this comment

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

Oh, or we can pass /proc/ns/fd/{rawFd} I guess ?

Copy link
Contributor

@HuijingHei HuijingHei Jan 30, 2026

Choose a reason for hiding this comment

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

If passing /proc/ns/fd/{rawFd}, when mount ESP, bootupd will be updated to get uuid instead of node, as the node will not exist.
E.g.

[root@cosa-devsh core]# exec 3</dev/vda
[root@cosa-devsh core]# sfdisk -J /proc/self/fd/3
{
   "partitiontable": {
      "label": "gpt",
      "id": "77B17F5E-C5DA-419A-B200-BCD3BAE39D35",
      "device": "/proc/self/fd/3",
      "unit": "sectors",
      "firstlba": 2048,
      "lastlba": 33554398,
      "sectorsize": 512,
      "partitions": [
         {
            "node": "/proc/self/fd/3p1",
            "start": 2048,
            "size": 2048,
            "type": "21686148-6449-6E6F-744E-656564454649",
            "uuid": "431E16C0-3582-4C42-B8A5-38FF61E588BA",
            "name": "BIOS-BOOT"
         },{
            "node": "/proc/self/fd/3p2",
            "start": 4096,
            "size": 260096,
            "type": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
            "uuid": "7295F72A-E852-CC4F-8606-444EDA295B72",
            "name": "EFI-SYSTEM"
         },{
            "node": "/proc/self/fd/3p3",
            "start": 264192,
            "size": 786432,
            "type": "BC13C2FF-59E6-4262-A352-B275FD6F7172",
            "uuid": "B2EFF31A-187F-B14F-BCDF-A8C07BDEFD04",
            "name": "boot"
         },{
            "node": "/proc/self/fd/3p4",
            "start": 1050624,
            "size": 32503775,
            "type": "4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709",
            "uuid": "57D12571-0713-4A48-92BA-6010C1A59C10",
            "name": "root"
         }
      ]
   }
}
[root@cosa-devsh core]# ls -l /proc/self/fd/3p2
ls: cannot access '/proc/self/fd/3p2': No such file or directory

cmd.args(["--dev-bind", device, device]);
}

// Add environment variables
for (key, value) in &self.env_vars {
cmd.args(["--setenv", key, value]);
}

// Command to run
cmd.arg("--");
cmd.args(args);

cmd.log_debug().run_inherited_with_cmd_context()
}
}
14 changes: 8 additions & 6 deletions crates/utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@
//! things here that only depend on the standard library and
//! "core" crates.
//!
mod bwrap;
pub use bwrap::*;
mod command;
pub use command::*;
mod path;
pub use path::*;
mod iterators;
pub use iterators::*;
mod timestamp;
pub use timestamp::*;
mod tracing_util;
pub use tracing_util::*;
mod path;
pub use path::*;
/// Re-execute the current process
pub mod reexec;
mod result_ext;
pub use result_ext::*;
mod timestamp;
pub use timestamp::*;
mod tracing_util;
pub use tracing_util::*;

/// The name of our binary
pub const NAME: &str = "bootc";
Expand Down