cargo_lambda_build/
zig.rs1use 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
20pub 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
38pub 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}