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 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 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 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 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)]
254fn 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}