diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 120c71de0..4bc889da4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/crates/lib/src/bootloader.rs b/crates/lib/src/bootloader.rs index 7fce2888a..f6f050fb1 100644 --- a/crates/lib/src/bootloader.rs +++ b/crates/lib/src/bootloader.rs @@ -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; @@ -91,22 +91,67 @@ 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.as_str()) + // Bind mount /boot from the physical target root so bootupctl can find + // the boot partition and install the bootloader there + .bind(boot_path.as_str(), "/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); + } + // // TODO : is it needed ? + // cmd.setenv( + // "PATH", + // "/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin", + // ) + cmd.run(bwrap_args) + } else { + // Running directly without chroot + Command::new("bootupctl") + .args(&bootupd_args) + .log_debug() + .run_inherited_with_cmd_context() + } } #[context("Installing bootloader")] diff --git a/crates/tests-integration/src/install.rs b/crates/tests-integration/src/install.rs index dc9d2afd2..a748763e7 100644 --- a/crates/tests-integration/src/install.rs +++ b/crates/tests-integration/src/install.rs @@ -9,7 +9,17 @@ use fn_error_context::context; use libtest_mimic::Trial; use xshell::{Shell, cmd}; -pub(crate) const BASE_ARGS: &[&str] = &["podman", "run", "--rm", "--privileged", "--pid=host"]; +pub(crate) const BASE_ARGS: &[&str] = &[ + "podman", + "run", + "--rm", + "--privileged", + "--pid=host", + "--env", + "BOOTC_BOOTLOADER_DEBUG=true", + "--env", + "RUST_LOG=debug", +]; // Arbitrary const NON_DEFAULT_STATEROOT: &str = "foo"; diff --git a/crates/utils/src/bwrap.rs b/crates/utils/src/bwrap.rs new file mode 100644 index 000000000..ae6f469f5 --- /dev/null +++ b/crates/utils/src/bwrap.rs @@ -0,0 +1,82 @@ +/// Builder for running commands inside a target os tree using bubblewrap (bwrap). +use std::ffi::OsStr; +use std::process::Command; + +use anyhow::Result; + +use crate::CommandRunExt; + +/// Builder for running commands inside a target directory using bwrap. +#[derive(Debug, Default)] +pub struct BwrapCmd<'a> { + /// The target directory to use as root for the container + chroot_path: &'a str, + /// 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 the given root directory. + pub fn new(path: &'a str) -> Self { + Self { + chroot_path: path, + ..Default::default() + } + } + + /// Add a bind mount from source to target inside the container. + pub fn bind(mut self, source: &'a str, target: &'a str) -> Self { + self.bind_mounts.push((source, target)); + 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>(self, args: impl IntoIterator) -> Result<()> { + let mut cmd = Command::new("bwrap"); + + // Bind the root filesystem + cmd.args(["--bind", self.chroot_path, "/"]); + + // Setup API filesystems + 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 { + 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() + } +} diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index bd9948daa..39ae772c3 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -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";