1use std::path::Path;
9
10use anyhow::{Result, anyhow};
11
12pub 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 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 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 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)]
257fn 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}