1use std::path::{Path, PathBuf};
9use std::process::Command;
10
11use anyhow::{Result, anyhow};
12use colored::Colorize;
13use zoi_core::{types, utils};
14
15pub fn run(
21 package_file: &Path,
22 build_type: Option<&str>,
23 platforms: &[String],
24 sign_key: Option<String>,
25 sign_mode: types::SignMode,
26 output_dir: Option<&Path>,
27 version_override: Option<&str>,
28 sub_packages: Option<Vec<String>>,
29 image: &str,
30 fakeroot: bool,
31 install_deps: bool,
32 test: bool
33) -> Result<()> {
34 println!("{} Building package using Docker...", "::".bold().blue());
35 println!("Image: {}", image.cyan());
36
37 if !utils::command_exists("docker") {
38 return Err(anyhow!(
39 "Docker is not installed or not in PATH. Please install Docker to \
40 use this method."
41 ));
42 }
43
44 let abs_package_file = package_file.canonicalize()?;
45 let package_dir = abs_package_file.parent().ok_or_else(|| {
46 anyhow!("Could not get parent directory of package file")
47 })?;
48
49 let abs_output_dir = if let Some(dir) = output_dir {
50 if !dir.exists() {
51 std::fs::create_dir_all(dir)?;
52 }
53 dir.canonicalize()?
54 } else {
55 package_dir.to_path_buf()
56 };
57
58 let container_workdir = "/work";
59 let container_output_dir = "/output";
60
61 let mut docker_args = vec![
62 "run".to_string(),
63 "--rm".to_string(),
64 "-v".to_string(),
65 format!("{}:{}", package_dir.display(), container_workdir),
66 "-v".to_string(),
67 format!("{}:{}", abs_output_dir.display(), container_output_dir),
68 "-w".to_string(),
69 container_workdir.to_string(),
70 ];
71
72 if let Ok(user_id) = Command::new("id").arg("-u").output() {
73 let uid = String::from_utf8_lossy(&user_id.stdout).trim().to_string();
74 if let Ok(group_id) = Command::new("id").arg("-g").output() {
75 let gid =
76 String::from_utf8_lossy(&group_id.stdout).trim().to_string();
77 docker_args.push("--user".to_string());
78 docker_args.push(format!("{uid}:{gid}"));
79 }
80 }
81
82 if sign_key.is_some() {
83 let host_gpg_home = std::env::var("GNUPGHOME").map_or_else(
84 |_| {
85 utils::get_user_home()
86 .map(|h| h.join(".gnupg"))
87 .unwrap_or_default()
88 },
89 PathBuf::from
90 );
91
92 if host_gpg_home.exists() {
93 let container_gpg_home = "/gpg_home";
94 docker_args.push("-v".to_string());
95 docker_args.push(format!(
96 "{}:{}",
97 host_gpg_home.display(),
98 container_gpg_home
99 ));
100 docker_args.push("-e".to_string());
101 docker_args.push(format!("GNUPGHOME={container_gpg_home}"));
102 }
103 }
104
105 if let Ok(password) = std::env::var("GPG_PASSWORD") {
106 docker_args.push("-e".to_string());
107 docker_args.push(format!("GPG_PASSWORD={password}"));
108 }
109
110 docker_args.push(image.to_string());
111
112 let package_filename = abs_package_file
113 .file_name()
114 .ok_or_else(|| anyhow!("Invalid package file name"))?
115 .to_string_lossy()
116 .into_owned();
117 let build_args = build_command_args(
118 package_filename,
119 build_type,
120 platforms,
121 sign_key,
122 sign_mode,
123 container_output_dir,
124 version_override,
125 sub_packages,
126 fakeroot,
127 install_deps,
128 test
129 );
130
131 let inner_script = "if ! command -v sudo >/dev/null 2>&1 && [ \"$(id -u)\" -eq 0 ]; then \
132 if command -v pacman >/dev/null 2>&1; then pacman -Sy --noconfirm sudo gnupg; \
133 elif command -v apt-get >/dev/null 2>&1; then apt-get update && apt-get install -y sudo gnupg; \
134 elif command -v dnf >/dev/null 2>&1; then dnf install -y sudo gnupg; \
135 elif command -v apk >/dev/null 2>&1; then apk add --update sudo gnupg; fi; \
136 fi && \
137 if command -v pacman >/dev/null 2>&1; then pacman -Sy --noconfirm base-devel git; \
138 elif command -v apt-get >/dev/null 2>&1; then apt-get update && apt-get install -y build-essential git; \
139 elif command -v dnf >/dev/null 2>&1; then dnf install -y @development-tools git; \
140 elif command -v apk >/dev/null 2>&1; then apk add --update build-base git; fi && \
141 curl -fsSL https://zillowe.pages.dev/scripts/zoi/install.sh | bash && \
142 export PATH=\"$HOME/.local/bin:$PATH\" && \
143 zoi sync && \
144 exec zoi package build \"$@\"";
145
146 docker_args.push("bash".to_string());
147 docker_args.push("-c".to_string());
148 docker_args.push(inner_script.to_string());
149 docker_args.push("zoi-package-build".to_string());
150 docker_args.extend(build_args);
151
152 println!("Running docker command: {}", "docker".cyan());
153 let status = Command::new("docker").args(&docker_args).status()?;
154
155 if !status.success() {
156 return Err(anyhow!(
157 "Docker build failed with exit code {:?}",
158 status.code()
159 ));
160 }
161
162 println!("{}", "Docker build successful!".green());
163
164 Ok(())
165}
166
167#[allow(clippy::too_many_arguments)]
168fn build_command_args(
170 package_filename: String,
171 build_type: Option<&str>,
172 platforms: &[String],
173 sign_key: Option<String>,
174 sign_mode: types::SignMode,
175 output_dir: &str,
176 version_override: Option<&str>,
177 sub_packages: Option<Vec<String>>,
178 fakeroot: bool,
179 install_deps: bool,
180 test: bool
181) -> Vec<String> {
182 let mut args = vec![
183 package_filename,
184 "--output-dir".to_string(),
185 output_dir.to_string(),
186 ];
187
188 if let Some(build_type) = build_type {
189 args.extend(["--type".to_string(), build_type.to_string()]);
190 }
191 for platform in platforms {
192 args.extend(["--platform".to_string(), platform.clone()]);
193 }
194 if let Some(sign_key) = sign_key {
195 args.extend(["--sign".to_string(), sign_key]);
196 if sign_mode == types::SignMode::Embed {
197 args.push("--sign-mode".to_string());
198 args.push("embed".to_string());
199 }
200 }
201 if let Some(version_override) = version_override {
202 args.extend([
203 "--version-override".to_string(),
204 version_override.to_string()
205 ]);
206 }
207 if let Some(sub_packages) = sub_packages {
208 for sub_package in sub_packages {
209 args.extend(["--sub".to_string(), sub_package]);
210 }
211 }
212 if fakeroot {
213 args.push("--fakeroot".to_string());
214 }
215 if install_deps {
216 args.push("--install-deps".to_string());
217 }
218 if test {
219 args.push("--test".to_string());
220 }
221
222 args
223}
224
225#[cfg(test)]
226mod tests {
227 use super::build_command_args;
228
229 #[test]
230 fn build_arguments_keep_shell_syntax_as_data() {
231 let args = build_command_args(
232 "package; touch /tmp/pwned.pkg.lua".to_string(),
233 Some("source; id"),
234 &["linux-amd64; id".to_string()],
235 Some("key; id".to_string()),
236 zoi_core::types::SignMode::Embed,
237 "/output",
238 Some("1.0.0; id"),
239 Some(vec!["sub; id".to_string()]),
240 true,
241 true,
242 true
243 );
244
245 assert!(
246 args.contains(&"package; touch /tmp/pwned.pkg.lua".to_string())
247 );
248 assert!(args.contains(&"source; id".to_string()));
249 assert!(args.contains(&"linux-amd64; id".to_string()));
250 assert!(args.contains(&"key; id".to_string()));
251 assert!(args.contains(&"1.0.0; id".to_string()));
252 assert!(args.contains(&"sub; id".to_string()));
253 }
254}