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