Skip to main content

zoi_package/
bwrap.rs

1//! Linux isolation using Bubblewrap.
2//!
3//! This module provides a way to run Zoi package builds inside a
4//! Bubblewrap (`bwrap`) sandbox on Linux. This ensures that the build
5//! process is isolated from the host system and has restricted access
6//! to the filesystem and network.
7
8use std::path::Path;
9
10use anyhow::{Result, anyhow};
11
12/// Runs the package build process inside a Bubblewrap sandbox.
13///
14/// # Errors
15///
16/// Returns an error if:
17/// - Not running on Linux.
18/// - Bubblewrap is not installed.
19/// - The build process fails inside the sandbox.
20pub fn run(
21    package_file: &Path,
22    build_type: Option<&str>,
23    platforms: &[String],
24    sign_key: Option<String>,
25    output_dir: Option<&Path>,
26    version_override: Option<&str>,
27    sub_packages: Option<Vec<String>>,
28    fakeroot: bool,
29    install_deps: bool,
30    test: bool
31) -> Result<()> {
32    #[cfg(not(target_os = "linux"))]
33    {
34        let _ = (
35            package_file,
36            build_type,
37            platforms,
38            sign_key,
39            output_dir,
40            version_override,
41            sub_packages,
42            fakeroot,
43            install_deps,
44            test
45        );
46        return Err(anyhow!(
47            "Bubblewrap ('bwrap') is only supported on Linux."
48        ));
49    }
50
51    #[cfg(target_os = "linux")]
52    {
53        use std::fmt::Write;
54        use std::path::PathBuf;
55        use std::process::Command;
56
57        use colored::Colorize;
58        use zoi_core::utils;
59
60        println!(
61            "{} Building package using Bubblewrap sandbox...",
62            "::".bold().blue()
63        );
64
65        if !utils::command_exists("bwrap") {
66            return Err(anyhow!(
67                "Bubblewrap ('bwrap') is not installed or not in PATH. Please \
68                 install it to use this method."
69            ));
70        }
71
72        let abs_package_file = package_file.canonicalize()?;
73        let package_dir = abs_package_file.parent().ok_or_else(|| {
74            anyhow!("Could not get parent directory of package file")
75        })?;
76
77        let abs_output_dir = if let Some(dir) = output_dir {
78            if !dir.exists() {
79                std::fs::create_dir_all(dir)?;
80            }
81            dir.canonicalize()?
82        } else {
83            package_dir.to_path_buf()
84        };
85
86        // We use a temporary directory for the build inside the sandbox
87        let container_workdir = "/work";
88        let container_output_dir = "/output";
89
90        let zoi_exe = std::env::current_exe()?;
91        let zoi_exe_dir = zoi_exe
92            .parent()
93            .ok_or_else(|| anyhow!("Could not get zoi executable directory"))?;
94
95        let home_dir = zoi_core::utils::get_user_home()
96            .ok_or_else(|| anyhow!("Could not get home directory"))?;
97        let zoi_home = home_dir.join(".zoi");
98
99        let package_filename = abs_package_file
100            .file_name()
101            .ok_or_else(|| anyhow!("Invalid package file name"))?
102            .to_string_lossy();
103
104        // Construct the inner zoi command
105        let mut inner_cmd = format!(
106            "zoi package build {package_filename} --output-dir \
107             {container_output_dir} --method native"
108        );
109
110        if let Some(bt) = build_type {
111            let _ = write!(inner_cmd, " --type {bt}");
112        }
113
114        for p in platforms {
115            let _ = write!(inner_cmd, " --platform {p}");
116        }
117
118        if let Some(sk) = sign_key {
119            let _ = write!(inner_cmd, " --sign {sk}");
120        }
121
122        if let Some(v) = version_override {
123            let _ = write!(inner_cmd, " --version-override {v}");
124        }
125
126        if let Some(subs) = sub_packages {
127            for s in subs {
128                let _ = write!(inner_cmd, " --sub {s}");
129            }
130        }
131
132        if fakeroot {
133            inner_cmd.push_str(" --fakeroot");
134        }
135
136        if install_deps {
137            inner_cmd.push_str(" --install-deps");
138        }
139
140        if test {
141            inner_cmd.push_str(" --test");
142        }
143
144        // Base bwrap arguments
145        let sysroot = zoi_core::sysroot::get_sysroot();
146
147        let mut envs = std::collections::HashMap::new();
148        envs.insert(
149            "PATH".to_string(),
150            "/zoi_bin:/usr/bin:/bin:/usr/sbin:/sbin".to_string()
151        );
152        envs.insert("ZOI_SKIP_LOCK".to_string(), "1".to_string());
153        envs.insert("HOME".to_string(), home_dir.display().to_string());
154
155        let status = if let Some(root) = &sysroot {
156            println!(
157                "{} Isolated build using sysroot: {}",
158                "::".bold().yellow(),
159                root.display()
160            );
161
162            let extra_binds = vec![
163                (package_dir.to_path_buf(), PathBuf::from(container_workdir)),
164                (abs_output_dir.clone(), PathBuf::from(container_output_dir)),
165                (zoi_exe_dir.to_path_buf(), PathBuf::from("/zoi_bin")),
166            ];
167
168            let mut cmd = zoi_sandbox::wrap_command_in_root(
169                root,
170                &PathBuf::from("/bin/bash"),
171                &["-c".to_string(), inner_cmd],
172                &envs,
173                &extra_binds,
174                fakeroot
175            )?;
176            cmd.status()?
177        } else {
178            // Base bwrap arguments for non-sysroot build
179            let mut bwrap_args = vec![
180                "--unshare-all".to_string(),
181                "--share-net".to_string(),
182                "--hostname".to_string(),
183                "zoi-build".to_string(),
184                "--dev".to_string(),
185                "/dev".to_string(),
186                "--proc".to_string(),
187                "/proc".to_string(),
188                "--tmpfs".to_string(),
189                "/tmp".to_string(),
190                "--tmpfs".to_string(),
191                "/run".to_string(),
192                "--tmpfs".to_string(),
193                "/var".to_string(),
194                "--ro-bind".to_string(),
195                "/usr".to_string(),
196                "/usr".to_string(),
197                "--symlink".to_string(),
198                "/usr/bin".to_string(),
199                "/bin".to_string(),
200                "--symlink".to_string(),
201                "/usr/lib".to_string(),
202                "/lib".to_string(),
203                "--symlink".to_string(),
204                "/usr/lib64".to_string(),
205                "/lib64".to_string(),
206                "--symlink".to_string(),
207                "/usr/sbin".to_string(),
208                "/sbin".to_string(),
209                "--ro-bind".to_string(),
210                "/etc".to_string(),
211                "/etc".to_string(),
212                "--bind".to_string(),
213                package_dir.display().to_string(),
214                container_workdir.to_string(),
215                "--bind".to_string(),
216                abs_output_dir.display().to_string(),
217                container_output_dir.to_string(),
218                "--ro-bind".to_string(),
219                zoi_exe_dir.display().to_string(),
220                "/zoi_bin".to_string(),
221                "--chdir".to_string(),
222                container_workdir.to_string(),
223                "--setenv".to_string(),
224                "PATH".to_string(),
225                "/zoi_bin:/usr/bin:/bin:/usr/sbin:/sbin".to_string(),
226                "--setenv".to_string(),
227                "ZOI_SKIP_LOCK".to_string(),
228                "1".to_string(),
229            ];
230
231            if zoi_home.exists() {
232                bwrap_args.push("--bind".to_string());
233                bwrap_args.push(zoi_home.display().to_string());
234                bwrap_args.push(zoi_home.display().to_string());
235            }
236
237            let system_zoi = Path::new("/var/lib/zoi");
238            if system_zoi.exists() {
239                bwrap_args.push("--bind".to_string());
240                bwrap_args.push(system_zoi.display().to_string());
241                bwrap_args.push(system_zoi.display().to_string());
242            }
243
244            if fakeroot {
245                bwrap_args.push("--uid".to_string());
246                bwrap_args.push("0".to_string());
247                bwrap_args.push("--gid".to_string());
248                bwrap_args.push("0".to_string());
249            } else {
250                let uid = nix::unistd::getuid().as_raw();
251                let gid = nix::unistd::getgid().as_raw();
252                bwrap_args.push("--uid".to_string());
253                bwrap_args.push(uid.to_string());
254                bwrap_args.push("--gid".to_string());
255                bwrap_args.push(gid.to_string());
256            }
257
258            bwrap_args.push("--setenv".to_string());
259            bwrap_args.push("HOME".to_string());
260            bwrap_args.push(home_dir.display().to_string());
261
262            bwrap_args.push("bash".to_string());
263            bwrap_args.push("-c".to_string());
264            bwrap_args.push(inner_cmd);
265
266            Command::new("bwrap").args(&bwrap_args).status()?
267        };
268
269        if !status.success() {
270            return Err(anyhow!(
271                "Bubblewrap build failed with exit code {:?}",
272                status.code()
273            ));
274        }
275
276        println!("{}", "Bubblewrap build successful!".green());
277
278        Ok(())
279    }
280}