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 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
20pub 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
31pub 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}