Skip to main content

zoi_cli/cmd/package/
build.rs

1//! Logic for the `package build` command.
2//!
3//! This module provides the functionality to build Zoi packages from their
4//! definition files, handling dependencies, platforms, and isolation.
5
6use std::path::PathBuf;
7
8use anyhow::Result;
9use clap::{Parser, ValueEnum};
10use colored::Colorize;
11
12/// How to attach a signature to a built artifact.
13#[derive(ValueEnum, Clone, Copy, Debug)]
14pub enum SignModeArg {
15    /// Store the signature inside the archive.
16    Embed,
17    /// Write a legacy `.sig` sidecar next to the archive.
18    File
19}
20
21impl From<SignModeArg> for zoi_core::types::SignMode {
22    fn from(mode: SignModeArg) -> Self {
23        match mode {
24            SignModeArg::Embed => Self::Embed,
25            SignModeArg::File => Self::File
26        }
27    }
28}
29
30/// Arguments for the `package build` command.
31#[derive(Parser, Debug)]
32pub struct BuildCommand {
33    /// Path to the package file (e.g. path/to/name.pkg.lua)
34    #[arg(required = true)]
35    pub package_file: PathBuf,
36
37    /// The type of package to build (e.g. 'source', 'pre-compiled').
38    #[arg(long)]
39    pub r#type: Option<String>,
40
41    /// The platform to build for (e.g. 'linux-amd64', 'windows-arm64', 'all',
42    /// 'current'). Can be specified multiple times.
43    #[arg(long, short, num_args = 1.., default_values_t = vec!["current".to_string()])]
44    pub platform: Vec<String>,
45
46    /// The sub-packages to build.
47    #[arg(long, num_args = 1..)]
48    pub sub: Option<Vec<String>>,
49
50    /// Sign the package with the given PGP key (name or fingerprint)
51    #[arg(long)]
52    pub sign: Option<String>,
53
54    /// How to attach the signature when --sign is used. `embed` stores the
55    /// signature inside the archive; `file` writes a legacy `.sig` sidecar.
56    #[arg(long, value_enum, default_value_t = SignModeArg::Embed)]
57    pub sign_mode: SignModeArg,
58
59    /// Run tests before building
60    #[arg(long)]
61    pub test: bool,
62
63    /// Directory to output the built package to
64    #[arg(long, short = 'o')]
65    pub output_dir: Option<PathBuf>,
66
67    /// Automatically install build-time dependencies
68    #[arg(long)]
69    pub install_deps: bool,
70
71    /// Override the package version
72    #[arg(long)]
73    pub version_override: Option<String>,
74
75    /// Method to use for building ('native', 'bwrap', or 'docker')
76    #[arg(long, default_value = "native")]
77    pub method: String,
78
79    /// Docker image to use when method is 'docker'
80    #[arg(long)]
81    pub image: Option<String>,
82
83    /// Force root ownership (UID/GID 0) in the built archive
84    #[arg(long)]
85    pub fakeroot: bool,
86
87    /// Build in a clean, isolated sysroot.
88    /// Requires a base package (specified by --root-package) to be installed
89    /// into the sysroot.
90    #[arg(long)]
91    pub pure: bool,
92
93    /// The base package to install when using --pure (default:
94    /// @core/base:dev).
95    #[arg(long, default_value = "@core/base:dev")]
96    pub root_package: String,
97
98    /// Skip creating an intermediate source bundle (.zsa) and build directly
99    /// from the .pkg.lua file.
100    #[arg(long)]
101    pub no_zsa: bool
102}
103
104/// Run the package build command.
105///
106/// # Errors
107///
108/// Returns an error if the build fails, dependencies cannot be installed, or
109/// the pure environment cannot be initialized.
110pub fn run(mut args: BuildCommand) -> Result<()> {
111    let mut _temp_root = None;
112
113    if args.pure {
114        if args.method == "docker" {
115            return Err(anyhow::anyhow!(
116                "--pure is not compatible with --method docker"
117            ));
118        }
119        args.method = "bwrap".to_string();
120
121        let temp = tempfile::Builder::new().prefix("zoi-pure-").tempdir()?;
122        println!(
123            "{} Initializing pure build environment in {}...",
124            "::".bold().blue(),
125            temp.path().display()
126        );
127
128        // Create ZoiOS marker so Zoi uses /usr/bin instead of /usr/local/bin
129        // This ensures the root package and build deps land where bwrap expects
130        // them.
131        let etc_dir = temp.path().join("etc");
132        std::fs::create_dir_all(&etc_dir)?;
133        std::fs::write(
134            etc_dir.join("os-release"),
135            "ID=zoios\nID_LIKE=zoios\n"
136        )?;
137
138        crate::pkg::sysroot::set_sysroot(temp.path().to_path_buf());
139        _temp_root = Some(temp);
140
141        // Install the root base environment package
142        let root_dep = crate::pkg::dependencies::parse_dependency_string(
143            &args.root_package
144        )?;
145        crate::pkg::install::dep_install::install_dependency(
146            &root_dep,
147            "pure-root",
148            crate::pkg::types::Scope::System,
149            true,
150            true,
151            &std::sync::Mutex::new(std::collections::HashSet::new()),
152            &mut Vec::new(),
153            None
154        )?;
155    }
156
157    if args.install_deps {
158        install_dependencies_for_build(&args, args.test)?;
159    }
160
161    if args.test {
162        println!("Running tests before building...");
163        crate::pkg::package::test::run(&args)?;
164        println!("Tests passed, proceeding with build...");
165    }
166
167    // Two-stage build: bundle the package into a .zsa source archive first,
168    // then build the distributable .zpa from that archive. The bundle is
169    // temporary and discarded after the build. --no-zsa opts out and keeps
170    // the legacy direct-from-source behavior.
171    let mut _temp_zsa_bundle = None;
172    if !args.no_zsa && args.package_file.to_string_lossy().ends_with(".pkg.lua")
173    {
174        let temp = tempfile::Builder::new()
175            .prefix("zoi-build-zsa-")
176            .tempdir()?;
177        println!("{} Bundling source archive...", "::".bold().blue());
178        let zsa_path = crate::pkg::package::bundle::run(
179            &args.package_file,
180            Some(temp.path()),
181            None,
182            args.sign_mode.into(),
183            args.version_override.as_deref(),
184            args.r#type.as_deref()
185        )?;
186
187        // Pin the output dir to the original package's directory when unset.
188        // Otherwise the build would default its output to the temp bundle
189        // directory, which gets deleted right after this function returns.
190        if args.output_dir.is_none() {
191            args.output_dir = args.package_file.parent().map(PathBuf::from);
192        }
193
194        println!("{} Building from source bundle...", "::".bold().blue());
195        args.package_file = zsa_path;
196        _temp_zsa_bundle = Some(temp);
197    }
198
199    crate::pkg::package::build::run(
200        &args.package_file,
201        args.r#type.as_deref(),
202        &args.platform,
203        args.sign,
204        args.sign_mode.into(),
205        args.output_dir.as_deref(),
206        args.version_override.as_deref(),
207        args.sub,
208        false,
209        &args.method,
210        args.image.as_deref(),
211        args.fakeroot,
212        args.install_deps,
213        args.test
214    )
215}
216
217/// Install dependencies required for building the package.
218///
219/// # Errors
220///
221/// Returns an error if dependency installation fails or the platform cannot be
222/// determined.
223pub fn install_dependencies_for_build(
224    args: &BuildCommand,
225    include_test: bool
226) -> Result<()> {
227    for platform in &args.platform {
228        let current_platform = if platform == "current" {
229            crate::pkg::utils::get_platform()?
230        } else {
231            platform.clone()
232        };
233
234        if let Some(mut dep_strings) =
235            crate::pkg::package::build::get_build_dependencies(
236                &args.package_file,
237                args.r#type.as_deref(),
238                &current_platform,
239                args.version_override.as_deref(),
240                false
241            )?
242        {
243            if include_test
244                && let Some(test_deps) =
245                    crate::pkg::package::build::get_test_dependencies(
246                        &args.package_file,
247                        &current_platform,
248                        args.version_override.as_deref(),
249                        false
250                    )?
251            {
252                dep_strings.extend(test_deps);
253            }
254
255            if !dep_strings.is_empty() {
256                println!(
257                    "{} Installing build/test dependencies...",
258                    "::".bold().blue()
259                );
260                let processed =
261                    std::sync::Mutex::new(std::collections::HashSet::new());
262                let mut installed = Vec::new();
263                for dep_str in dep_strings {
264                    let dep =
265                        crate::pkg::dependencies::parse_dependency_string(
266                            &dep_str
267                        )?;
268                    crate::pkg::install::dep_install::install_dependency(
269                        &dep,
270                        "build",
271                        if crate::pkg::sysroot::get_sysroot().is_some() {
272                            crate::pkg::types::Scope::System
273                        } else {
274                            crate::pkg::types::Scope::User
275                        },
276                        true,
277                        true,
278                        &processed,
279                        &mut installed,
280                        None
281                    )?;
282                }
283            }
284        }
285    }
286    Ok(())
287}