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    sign_mode: zoi_core::types::SignMode,
26    output_dir: Option<&Path>,
27    version_override: Option<&str>,
28    sub_packages: Option<Vec<String>>,
29    fakeroot: bool,
30    install_deps: bool,
31    test: bool
32) -> Result<()> {
33    #[cfg(not(target_os = "linux"))]
34    {
35        let _ = (
36            package_file,
37            build_type,
38            platforms,
39            sign_key,
40            sign_mode,
41            output_dir,
42            version_override,
43            sub_packages,
44            fakeroot,
45            install_deps,
46            test
47        );
48        return Err(anyhow!(
49            "Bubblewrap ('bwrap') is only supported on Linux."
50        ));
51    }
52
53    #[cfg(target_os = "linux")]
54    {
55        use std::path::PathBuf;
56        use std::process::Command;
57
58        use colored::Colorize;
59        use zoi_core::utils;
60
61        println!(
62            "{} Building package using Bubblewrap sandbox...",
63            "::".bold().blue()
64        );
65
66        if !utils::command_exists("bwrap") {
67            return Err(anyhow!(
68                "Bubblewrap ('bwrap') is not installed or not in PATH. Please \
69                 install it to use this method."
70            ));
71        }
72
73        let abs_package_file = package_file.canonicalize()?;
74        let package_dir = abs_package_file.parent().ok_or_else(|| {
75            anyhow!("Could not get parent directory of package file")
76        })?;
77
78        let abs_output_dir = if let Some(dir) = output_dir {
79            if !dir.exists() {
80                std::fs::create_dir_all(dir)?;
81            }
82            dir.canonicalize()?
83        } else {
84            package_dir.to_path_buf()
85        };
86
87        // We use a temporary directory for the build inside the sandbox
88        let container_workdir = "/work";
89        let container_output_dir = "/output";
90
91        let zoi_exe = std::env::current_exe()?;
92        let zoi_exe_dir = zoi_exe
93            .parent()
94            .ok_or_else(|| anyhow!("Could not get zoi executable directory"))?;
95
96        let home_dir = zoi_core::utils::get_user_home()
97            .ok_or_else(|| anyhow!("Could not get home directory"))?;
98        let zoi_data_dir = zoi_core::utils::get_user_data_dir()?;
99
100        let package_filename = abs_package_file
101            .file_name()
102            .ok_or_else(|| anyhow!("Invalid package file name"))?
103            .to_string_lossy()
104            .into_owned();
105        let build_args = build_command_args(
106            package_filename,
107            build_type,
108            platforms,
109            sign_key,
110            sign_mode,
111            container_output_dir,
112            version_override,
113            sub_packages,
114            fakeroot,
115            install_deps,
116            test
117        );
118
119        // Base bwrap arguments
120        let sysroot = zoi_core::sysroot::get_sysroot();
121
122        let mut envs = std::collections::HashMap::new();
123        envs.insert(
124            "PATH".to_string(),
125            "/zoi_bin:/usr/bin:/bin:/usr/sbin:/sbin".to_string()
126        );
127        envs.insert("ZOI_SKIP_LOCK".to_string(), "1".to_string());
128        envs.insert("HOME".to_string(), home_dir.display().to_string());
129
130        let status = if let Some(root) = &sysroot {
131            println!(
132                "{} Isolated build using sysroot: {}",
133                "::".bold().yellow(),
134                root.display()
135            );
136
137            let extra_binds = vec![
138                (package_dir.to_path_buf(), PathBuf::from(container_workdir)),
139                (abs_output_dir.clone(), PathBuf::from(container_output_dir)),
140                (zoi_exe_dir.to_path_buf(), PathBuf::from("/zoi_bin")),
141            ];
142
143            let mut cmd = zoi_sandbox::wrap_command_in_root(
144                root,
145                &PathBuf::from("/zoi_bin/zoi"),
146                &build_args,
147                &envs,
148                &extra_binds,
149                fakeroot
150            )?;
151            cmd.status()?
152        } else {
153            // Base bwrap arguments for non-sysroot build
154            let mut bwrap_args = vec![
155                "--unshare-all".to_string(),
156                "--share-net".to_string(),
157                "--hostname".to_string(),
158                "zoi-build".to_string(),
159                "--dev".to_string(),
160                "/dev".to_string(),
161                "--proc".to_string(),
162                "/proc".to_string(),
163                "--tmpfs".to_string(),
164                "/tmp".to_string(),
165                "--tmpfs".to_string(),
166                "/run".to_string(),
167                "--tmpfs".to_string(),
168                "/var".to_string(),
169                "--ro-bind".to_string(),
170                "/usr".to_string(),
171                "/usr".to_string(),
172                "--symlink".to_string(),
173                "/usr/bin".to_string(),
174                "/bin".to_string(),
175                "--symlink".to_string(),
176                "/usr/lib".to_string(),
177                "/lib".to_string(),
178                "--symlink".to_string(),
179                "/usr/lib64".to_string(),
180                "/lib64".to_string(),
181                "--symlink".to_string(),
182                "/usr/sbin".to_string(),
183                "/sbin".to_string(),
184                "--ro-bind".to_string(),
185                "/etc".to_string(),
186                "/etc".to_string(),
187                "--bind".to_string(),
188                package_dir.display().to_string(),
189                container_workdir.to_string(),
190                "--bind".to_string(),
191                abs_output_dir.display().to_string(),
192                container_output_dir.to_string(),
193                "--ro-bind".to_string(),
194                zoi_exe_dir.display().to_string(),
195                "/zoi_bin".to_string(),
196                "--chdir".to_string(),
197                container_workdir.to_string(),
198                "--setenv".to_string(),
199                "PATH".to_string(),
200                "/zoi_bin:/usr/bin:/bin:/usr/sbin:/sbin".to_string(),
201                "--setenv".to_string(),
202                "ZOI_SKIP_LOCK".to_string(),
203                "1".to_string(),
204            ];
205
206            if zoi_data_dir.exists() {
207                bwrap_args.push("--bind".to_string());
208                bwrap_args.push(zoi_data_dir.display().to_string());
209                bwrap_args.push(zoi_data_dir.display().to_string());
210            }
211
212            let system_zoi = Path::new("/var/lib/zoi");
213            if system_zoi.exists() {
214                bwrap_args.push("--bind".to_string());
215                bwrap_args.push(system_zoi.display().to_string());
216                bwrap_args.push(system_zoi.display().to_string());
217            }
218
219            if fakeroot {
220                bwrap_args.push("--uid".to_string());
221                bwrap_args.push("0".to_string());
222                bwrap_args.push("--gid".to_string());
223                bwrap_args.push("0".to_string());
224            } else {
225                let uid = nix::unistd::getuid().as_raw();
226                let gid = nix::unistd::getgid().as_raw();
227                bwrap_args.push("--uid".to_string());
228                bwrap_args.push(uid.to_string());
229                bwrap_args.push("--gid".to_string());
230                bwrap_args.push(gid.to_string());
231            }
232
233            bwrap_args.push("--setenv".to_string());
234            bwrap_args.push("HOME".to_string());
235            bwrap_args.push(home_dir.display().to_string());
236
237            bwrap_args.push("/zoi_bin/zoi".to_string());
238            bwrap_args.extend(build_args);
239
240            Command::new("bwrap").args(&bwrap_args).status()?
241        };
242
243        if !status.success() {
244            return Err(anyhow!(
245                "Bubblewrap build failed with exit code {:?}",
246                status.code()
247            ));
248        }
249
250        println!("{}", "Bubblewrap build successful!".green());
251
252        Ok(())
253    }
254}
255
256#[cfg(any(target_os = "linux", test))]
257#[allow(clippy::too_many_arguments)]
258/// Builds the argument vector passed directly to `zoi` inside Bubblewrap.
259fn build_command_args(
260    package_filename: String,
261    build_type: Option<&str>,
262    platforms: &[String],
263    sign_key: Option<String>,
264    sign_mode: zoi_core::types::SignMode,
265    output_dir: &str,
266    version_override: Option<&str>,
267    sub_packages: Option<Vec<String>>,
268    fakeroot: bool,
269    install_deps: bool,
270    test: bool
271) -> Vec<String> {
272    let mut args = vec![
273        "package".to_string(),
274        "build".to_string(),
275        package_filename,
276        "--output-dir".to_string(),
277        output_dir.to_string(),
278        "--method".to_string(),
279        "native".to_string(),
280    ];
281
282    if let Some(build_type) = build_type {
283        args.extend(["--type".to_string(), build_type.to_string()]);
284    }
285    for platform in platforms {
286        args.extend(["--platform".to_string(), platform.clone()]);
287    }
288    if let Some(sign_key) = sign_key {
289        args.extend(["--sign".to_string(), sign_key]);
290        if sign_mode == zoi_core::types::SignMode::Embed {
291            args.push("--sign-mode".to_string());
292            args.push("embed".to_string());
293        }
294    }
295    if let Some(version_override) = version_override {
296        args.extend([
297            "--version-override".to_string(),
298            version_override.to_string()
299        ]);
300    }
301    if let Some(sub_packages) = sub_packages {
302        for sub_package in sub_packages {
303            args.extend(["--sub".to_string(), sub_package]);
304        }
305    }
306    if fakeroot {
307        args.push("--fakeroot".to_string());
308    }
309    if install_deps {
310        args.push("--install-deps".to_string());
311    }
312    if test {
313        args.push("--test".to_string());
314    }
315
316    args
317}
318
319#[cfg(test)]
320mod tests {
321    use super::build_command_args;
322
323    #[test]
324    fn build_arguments_keep_shell_syntax_as_data() {
325        let args = build_command_args(
326            "package; touch /tmp/pwned.pkg.lua".to_string(),
327            Some("source; id"),
328            &["linux-amd64; id".to_string()],
329            Some("key; id".to_string()),
330            zoi_core::types::SignMode::Embed,
331            "/output",
332            Some("1.0.0; id"),
333            Some(vec!["sub; id".to_string()]),
334            true,
335            true,
336            true
337        );
338
339        assert!(
340            args.contains(&"package; touch /tmp/pwned.pkg.lua".to_string())
341        );
342        assert!(args.contains(&"source; id".to_string()));
343        assert!(args.contains(&"linux-amd64; id".to_string()));
344        assert!(args.contains(&"key; id".to_string()));
345        assert!(args.contains(&"1.0.0; id".to_string()));
346        assert!(args.contains(&"sub; id".to_string()));
347    }
348}