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