cargo_lambda_build/
zig.rs

1use crate::error::BuildError;
2use cargo_lambda_interactive::{
3    choose_option, command::silent_command, is_stdin_tty, progress::Progress,
4};
5use cargo_zigbuild::Zig;
6use miette::{IntoDiagnostic, Result};
7use serde::Serialize;
8use std::{path::PathBuf, process::Command};
9
10#[derive(Debug, Default, Serialize)]
11pub struct ZigInfo {
12    #[serde(default, skip_serializing_if = "Option::is_none")]
13    path: Option<PathBuf>,
14    #[serde(default, skip_serializing_if = "Option::is_none")]
15    install_options: Option<Vec<InstallOption>>,
16    #[serde(default, skip_serializing_if = "Option::is_none")]
17    version: Option<String>,
18}
19
20/// Print information about the Zig installation.
21pub fn print_install_options(options: &[InstallOption]) {
22    println!("Zig is not installed in your system.");
23    if is_stdin_tty() {
24        println!("Run `cargo lambda system --install-zig` to install Zig.")
25    }
26
27    if !options.is_empty() {
28        println!("You can use any of the following options to install it:");
29        for option in options {
30            println!("\t* {}: `{}`", option, option.usage());
31        }
32    }
33    println!(
34        "\t* Download Zig 0.13.0 or newer from https://ziglang.org/download/ and add it to your PATH"
35    );
36}
37
38/// Install Zig using a choice prompt.
39pub async fn install_zig(options: Vec<InstallOption>, install_non_interactive: bool) -> Result<()> {
40    if install_non_interactive {
41        let Some(choice) = options.first().cloned() else {
42            return Err(BuildError::ZigMissing.into());
43        };
44
45        return choice.install().await;
46    }
47
48    let choice = choose_option(
49        "Zig is not installed in your system.\nHow do you want to install Zig?",
50        options,
51    );
52
53    match choice {
54        Ok(choice) => choice.install().await.map(|_| ()),
55        Err(err) => Err(err).into_diagnostic(),
56    }
57}
58
59pub async fn check_installation(install_non_interactive: bool) -> Result<ZigInfo> {
60    let zig_info = get_zig_info();
61    if zig_info.is_ok() {
62        return zig_info;
63    }
64
65    let options = install_options();
66    if !options.is_empty() && (is_stdin_tty() || install_non_interactive) {
67        install_zig(options, install_non_interactive).await?;
68        return get_zig_info();
69    }
70
71    print_install_options(&options);
72    Err(BuildError::ZigMissing.into())
73}
74
75pub fn get_zig_info() -> Result<ZigInfo> {
76    let Ok((path, run_modifiers)) = Zig::find_zig() else {
77        let options = install_options();
78        return Ok(ZigInfo {
79            install_options: Some(options),
80            ..Default::default()
81        });
82    };
83
84    let mut cmd = Command::new(&path);
85    cmd.args(&run_modifiers);
86    cmd.arg("version");
87    let output = cmd.output().into_diagnostic()?;
88    let version = String::from_utf8(output.stdout)
89        .into_diagnostic()?
90        .trim()
91        .to_string();
92
93    Ok(ZigInfo {
94        path: Some(path),
95        version: Some(version),
96        ..Default::default()
97    })
98}
99
100#[derive(Clone, Debug)]
101pub enum InstallOption {
102    #[cfg(not(windows))]
103    Brew,
104    #[cfg(windows)]
105    Choco,
106    #[cfg(not(windows))]
107    Nix,
108    #[cfg(not(windows))]
109    Npm,
110    Pip3,
111    #[cfg(windows)]
112    Scoop,
113    #[cfg(windows)]
114    Winget,
115}
116
117impl serde::Serialize for InstallOption {
118    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
119    where
120        S: serde::Serializer,
121    {
122        serializer.serialize_str(self.usage())
123    }
124}
125
126impl std::fmt::Display for InstallOption {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        match self {
129            #[cfg(not(windows))]
130            InstallOption::Brew => write!(f, "Install with Homebrew"),
131            #[cfg(windows)]
132            InstallOption::Choco => write!(f, "Install with Chocolatey"),
133            #[cfg(not(windows))]
134            InstallOption::Nix => write!(f, "Install with Nix"),
135            #[cfg(not(windows))]
136            InstallOption::Npm => write!(f, "Install with NPM"),
137            InstallOption::Pip3 => write!(f, "Install with Pip3 (Python 3)"),
138            #[cfg(windows)]
139            InstallOption::Scoop => write!(f, "Install with Scoop"),
140            #[cfg(windows)]
141            InstallOption::Winget => write!(f, "Install with Winget"),
142        }
143    }
144}
145
146impl InstallOption {
147    pub fn usage(&self) -> &'static str {
148        match self {
149            #[cfg(not(windows))]
150            InstallOption::Brew => "brew install zig",
151            #[cfg(windows)]
152            InstallOption::Choco => "choco install zig",
153            #[cfg(not(windows))]
154            InstallOption::Nix => "nix-env -iA nixpkgs.zig",
155            #[cfg(not(windows))]
156            InstallOption::Npm => "npm install -g @ziglang/cli",
157            InstallOption::Pip3 => "pip3 install ziglang",
158            #[cfg(windows)]
159            InstallOption::Scoop => "scoop install zig",
160            #[cfg(windows)]
161            InstallOption::Winget => "winget install zig.zig",
162        }
163    }
164
165    pub async fn install(self) -> Result<()> {
166        let pb = Progress::start("Installing Zig...");
167        let usage = self.usage().split(' ').collect::<Vec<_>>();
168        let usage = usage.as_slice();
169        let result = silent_command(usage[0], &usage[1..usage.len()]).await;
170
171        let finish = if result.is_ok() {
172            "Zig installed"
173        } else {
174            "Failed to install Zig"
175        };
176        pb.finish(finish);
177
178        result
179    }
180}
181
182pub fn install_options() -> Vec<InstallOption> {
183    let mut options = Vec::new();
184
185    #[cfg(not(windows))]
186    if which::which("brew").is_ok() {
187        options.push(InstallOption::Brew);
188    }
189
190    #[cfg(windows)]
191    if which::which("choco").is_ok() {
192        options.push(InstallOption::Choco);
193    }
194
195    #[cfg(not(windows))]
196    if which::which("nix-env").is_ok() {
197        options.push(InstallOption::Nix);
198    }
199
200    #[cfg(not(windows))]
201    if which::which("npm").is_ok() {
202        options.push(InstallOption::Npm);
203    }
204
205    if which::which("pip3").is_ok() {
206        options.push(InstallOption::Pip3);
207    }
208
209    #[cfg(windows)]
210    if which::which("scoop").is_ok() {
211        options.push(InstallOption::Scoop);
212    }
213
214    #[cfg(windows)]
215    if which::which("winget").is_ok() {
216        options.push(InstallOption::Winget);
217    }
218
219    options
220}