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#[allow(clippy::too_many_arguments)]
257/// Builds the argument vector passed directly to `zoi` inside Bubblewrap.
258fn build_command_args(
259    package_filename: String,
260    build_type: Option<&str>,
261    platforms: &[String],
262    sign_key: Option<String>,
263    sign_mode: zoi_core::types::SignMode,
264    output_dir: &str,
265    version_override: Option<&str>,
266    sub_packages: Option<Vec<String>>,
267    fakeroot: bool,
268    install_deps: bool,
269    test: bool
270) -> Vec<String> {
271    let mut args = vec![
272        "package".to_string(),
273        "build".to_string(),
274        package_filename,
275        "--output-dir".to_string(),
276        output_dir.to_string(),
277        "--method".to_string(),
278        "native".to_string(),
279    ];
280
281    if let Some(build_type) = build_type {
282        args.extend(["--type".to_string(), build_type.to_string()]);
283    }
284    for platform in platforms {
285        args.extend(["--platform".to_string(), platform.clone()]);
286    }
287    if let Some(sign_key) = sign_key {
288        args.extend(["--sign".to_string(), sign_key]);
289        if sign_mode == zoi_core::types::SignMode::Embed {
290            args.push("--sign-mode".to_string());
291            args.push("embed".to_string());
292        }
293    }
294    if let Some(version_override) = version_override {
295        args.extend([
296            "--version-override".to_string(),
297            version_override.to_string()
298        ]);
299    }
300    if let Some(sub_packages) = sub_packages {
301        for sub_package in sub_packages {
302            args.extend(["--sub".to_string(), sub_package]);
303        }
304    }
305    if fakeroot {
306        args.push("--fakeroot".to_string());
307    }
308    if install_deps {
309        args.push("--install-deps".to_string());
310    }
311    if test {
312        args.push("--test".to_string());
313    }
314
315    args
316}
317
318#[cfg(test)]
319mod tests {
320    use super::build_command_args;
321
322    #[test]
323    fn build_arguments_keep_shell_syntax_as_data() {
324        let args = build_command_args(
325            "package; touch /tmp/pwned.pkg.lua".to_string(),
326            Some("source; id"),
327            &["linux-amd64; id".to_string()],
328            Some("key; id".to_string()),
329            zoi_core::types::SignMode::Embed,
330            "/output",
331            Some("1.0.0; id"),
332            Some(vec!["sub; id".to_string()]),
333            true,
334            true,
335            true
336        );
337
338        assert!(
339            args.contains(&"package; touch /tmp/pwned.pkg.lua".to_string())
340        );
341        assert!(args.contains(&"source; id".to_string()));
342        assert!(args.contains(&"linux-amd64; id".to_string()));
343        assert!(args.contains(&"key; id".to_string()));
344        assert!(args.contains(&"1.0.0; id".to_string()));
345        assert!(args.contains(&"sub; id".to_string()));
346    }
347}