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