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