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::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    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
116    let mut inner_cmd = format!(
117        "if ! command -v sudo >/dev/null 2>&1 && [ \"$(id -u)\" -eq 0 ]; then \
118            if command -v pacman >/dev/null 2>&1; then pacman -Sy --noconfirm sudo gnupg; \
119            elif command -v apt-get >/dev/null 2>&1; then apt-get update && apt-get install -y sudo gnupg; \
120            elif command -v dnf >/dev/null 2>&1; then dnf install -y sudo gnupg; \
121            elif command -v apk >/dev/null 2>&1; then apk add --update sudo gnupg; fi; \
122         fi && \
123         if command -v pacman >/dev/null 2>&1; then pacman -Sy --noconfirm base-devel git; \
124         elif command -v apt-get >/dev/null 2>&1; then apt-get update && apt-get install -y build-essential git; \
125         elif command -v dnf >/dev/null 2>&1; then dnf install -y @development-tools git; \
126         elif command -v apk >/dev/null 2>&1; then apk add --update build-base git; fi && \
127         curl -fsSL https://zillowe.pages.dev/scripts/zoi/install.sh | bash && \
128         export PATH=\"$HOME/.local/bin:$PATH\" && \
129         zoi sync && \
130         zoi package build {package_filename} --output-dir {container_output_dir}",
131    );
132
133    if let Some(bt) = build_type {
134        use std::fmt::Write;
135        let _ = write!(inner_cmd, " --type {bt}");
136    }
137
138    for p in platforms {
139        use std::fmt::Write;
140        let _ = write!(inner_cmd, " --platform {p}");
141    }
142
143    if let Some(sk) = sign_key {
144        use std::fmt::Write;
145        let _ = write!(inner_cmd, " --sign {sk}");
146    }
147
148    if let Some(v) = version_override {
149        use std::fmt::Write;
150        let _ = write!(inner_cmd, " --version-override {v}");
151    }
152
153    if let Some(subs) = sub_packages {
154        for s in subs {
155            use std::fmt::Write;
156            let _ = write!(inner_cmd, " --sub {s}");
157        }
158    }
159
160    if fakeroot {
161        inner_cmd.push_str(" --fakeroot");
162    }
163
164    if install_deps {
165        inner_cmd.push_str(" --install-deps");
166    }
167
168    if test {
169        inner_cmd.push_str(" --test");
170    }
171
172    docker_args.push("bash".to_string());
173    docker_args.push("-c".to_string());
174    docker_args.push(inner_cmd);
175
176    println!("Running docker command: {}", "docker".cyan());
177    let status = Command::new("docker").args(&docker_args).status()?;
178
179    if !status.success() {
180        return Err(anyhow!(
181            "Docker build failed with exit code {:?}",
182            status.code()
183        ));
184    }
185
186    println!("{}", "Docker build successful!".green());
187
188    Ok(())
189}