bootc_internal_utils/
bwrap.rs

1/// Builder for running commands inside a target os tree using bubblewrap (bwrap).
2use std::borrow::Cow;
3use std::ffi::OsStr;
4use std::os::fd::AsRawFd;
5use std::process::Command;
6
7use anyhow::Result;
8use cap_std_ext::camino::{Utf8Path, Utf8PathBuf};
9use cap_std_ext::cap_std::fs::Dir;
10
11use crate::CommandRunExt;
12
13/// Builder for running commands inside a target directory using bwrap.
14#[derive(Debug)]
15pub struct BwrapCmd<'a> {
16    /// The target directory to use as root for the container
17    chroot_path: Cow<'a, Utf8Path>,
18    /// Bind mounts in format (source, target)
19    bind_mounts: Vec<(&'a str, &'a str)>,
20    /// Device nodes to bind into the container
21    devices: Vec<&'a str>,
22    /// Environment variables to set
23    env_vars: Vec<(&'a str, &'a str)>,
24}
25
26impl<'a> BwrapCmd<'a> {
27    /// Create a new BwrapCmd builder with a root directory as a File Descriptor.
28    #[allow(dead_code)]
29    pub fn new_with_dir(path: &'a Dir) -> Self {
30        let fd_path: String = format!("/proc/self/fd/{}", path.as_raw_fd());
31        Self {
32            chroot_path: Cow::Owned(Utf8PathBuf::from(&fd_path)),
33            bind_mounts: Vec::new(),
34            devices: Vec::new(),
35            env_vars: Vec::new(),
36        }
37    }
38
39    /// Create a new BwrapCmd builder with a root directory
40    pub fn new(path: &'a Utf8Path) -> Self {
41        Self {
42            chroot_path: Cow::Borrowed(path),
43            bind_mounts: Vec::new(),
44            devices: Vec::new(),
45            env_vars: Vec::new(),
46        }
47    }
48
49    /// Add a bind mount from source to target inside the container.
50    pub fn bind(
51        mut self,
52        source: &'a impl AsRef<Utf8Path>,
53        target: &'a impl AsRef<Utf8Path>,
54    ) -> Self {
55        self.bind_mounts
56            .push((source.as_ref().as_str(), target.as_ref().as_str()));
57        self
58    }
59
60    /// Bind a device node into the container.
61    pub fn bind_device(mut self, device: &'a str) -> Self {
62        self.devices.push(device);
63        self
64    }
65
66    /// Set an environment variable for the command.
67    pub fn setenv(mut self, key: &'a str, value: &'a str) -> Self {
68        self.env_vars.push((key, value));
69        self
70    }
71
72    /// Run the specified command inside the container.
73    pub fn run<S: AsRef<OsStr>>(self, args: impl IntoIterator<Item = S>) -> Result<()> {
74        let mut cmd = Command::new("bwrap");
75
76        // Bind the root filesystem
77        cmd.args(["--bind", self.chroot_path.as_str(), "/"]);
78
79        // Setup API filesystems
80        // See https://systemd.io/API_FILE_SYSTEMS/
81        cmd.args(["--proc", "/proc"]);
82        cmd.args(["--dev", "/dev"]);
83        cmd.args(["--bind", "/sys", "/sys"]);
84
85        // Add bind mounts
86        for (source, target) in &self.bind_mounts {
87            cmd.args(["--bind", source, target]);
88        }
89
90        // Add device bind mounts
91        for device in self.devices {
92            cmd.args(["--dev-bind", device, device]);
93        }
94
95        // Add environment variables
96        for (key, value) in &self.env_vars {
97            cmd.args(["--setenv", key, value]);
98        }
99
100        // Command to run
101        cmd.arg("--");
102        cmd.args(args);
103
104        cmd.log_debug().run_inherited_with_cmd_context()
105    }
106}