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