Skip to main content

zoi_package/
docker.rs

1//! Containerized package builds using Docker.
2//!
3//! This module allows building Zoi packages inside a Docker container.
4//! This is useful for cross-compilation, ensuring a clean and consistent
5//! build environment, and for building packages for different Linux
6//! distributions from a single host.
7
8use std::path::{Path, PathBuf};
9use std::process::Command;
10
11use anyhow::{Result, anyhow};
12use colored::Colorize;
13use zoi_core::{types, utils};
14
15/// Runs the package build process inside a Docker container.
16/// # Errors
17///
18/// Returns an error if the Docker image cannot be built or the container fails
19/// to run.
20pub 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)]
168/// Builds the argument vector forwarded by the Docker bootstrap shell.
169fn 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}