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
76/// Run the package build command.
77///
78/// # Errors
79///
80/// Returns an error if the build fails, dependencies cannot be installed, or
81/// the pure environment cannot be initialized.
82pub fn run(mut args: BuildCommand) -> Result<()> {
83    let mut _temp_root = None;
84
85    if args.pure {
86        if args.method == "docker" {
87            return Err(anyhow::anyhow!(
88                "--pure is not compatible with --method docker"
89            ));
90        }
91        args.method = "bwrap".to_string();
92
93        let temp = tempfile::Builder::new().prefix("zoi-pure-").tempdir()?;
94        println!(
95            "{} Initializing pure build environment in {}...",
96            "::".bold().blue(),
97            temp.path().display()
98        );
99
100        // Create ZoiOS marker so Zoi uses /usr/bin instead of /usr/local/bin
101        // This ensures the root package and build deps land where bwrap expects
102        // them.
103        let etc_dir = temp.path().join("etc");
104        std::fs::create_dir_all(&etc_dir)?;
105        std::fs::write(
106            etc_dir.join("os-release"),
107            "ID=zoios\nID_LIKE=zoios\n"
108        )?;
109
110        crate::pkg::sysroot::set_sysroot(temp.path().to_path_buf());
111        _temp_root = Some(temp);
112
113        // Install the root base environment package
114        let root_dep = crate::pkg::dependencies::parse_dependency_string(
115            &args.root_package
116        )?;
117        crate::pkg::install::dep_install::install_dependency(
118            &root_dep,
119            "pure-root",
120            crate::pkg::types::Scope::System,
121            true,
122            true,
123            &std::sync::Mutex::new(std::collections::HashSet::new()),
124            &mut Vec::new(),
125            None
126        )?;
127    }
128
129    if args.install_deps {
130        install_dependencies_for_build(&args, args.test)?;
131    }
132
133    if args.test {
134        println!("Running tests before building...");
135        crate::pkg::package::test::run(&args)?;
136        println!("Tests passed, proceeding with build...");
137    }
138
139    crate::pkg::package::build::run(
140        &args.package_file,
141        args.r#type.as_deref(),
142        &args.platform,
143        args.sign,
144        args.output_dir.as_deref(),
145        args.version_override.as_deref(),
146        args.sub,
147        false,
148        &args.method,
149        args.image.as_deref(),
150        args.fakeroot,
151        args.install_deps,
152        args.test
153    )
154}
155
156/// Install dependencies required for building the package.
157///
158/// # Errors
159///
160/// Returns an error if dependency installation fails or the platform cannot be
161/// determined.
162pub fn install_dependencies_for_build(
163    args: &BuildCommand,
164    include_test: bool
165) -> Result<()> {
166    for platform in &args.platform {
167        let current_platform = if platform == "current" {
168            crate::pkg::utils::get_platform()?
169        } else {
170            platform.clone()
171        };
172
173        if let Some(mut dep_strings) =
174            crate::pkg::package::build::get_build_dependencies(
175                &args.package_file,
176                args.r#type.as_deref(),
177                &current_platform,
178                args.version_override.as_deref(),
179                false
180            )?
181        {
182            if include_test
183                && let Some(test_deps) =
184                    crate::pkg::package::build::get_test_dependencies(
185                        &args.package_file,
186                        &current_platform,
187                        args.version_override.as_deref(),
188                        false
189                    )?
190            {
191                dep_strings.extend(test_deps);
192            }
193
194            if !dep_strings.is_empty() {
195                println!(
196                    "{} Installing build/test dependencies...",
197                    "::".bold().blue()
198                );
199                let processed =
200                    std::sync::Mutex::new(std::collections::HashSet::new());
201                let mut installed = Vec::new();
202                for dep_str in dep_strings {
203                    let dep =
204                        crate::pkg::dependencies::parse_dependency_string(
205                            &dep_str
206                        )?;
207                    crate::pkg::install::dep_install::install_dependency(
208                        &dep,
209                        "build",
210                        if crate::pkg::sysroot::get_sysroot().is_some() {
211                            crate::pkg::types::Scope::System
212                        } else {
213                            crate::pkg::types::Scope::User
214                        },
215                        true,
216                        true,
217                        &processed,
218                        &mut installed,
219                        None
220                    )?;
221                }
222            }
223        }
224    }
225    Ok(())
226}