cargo_zigbuild/zig/
locate.rs1use std::env;
2use std::path::PathBuf;
3use std::process::Command;
4use std::str;
5use std::sync::OnceLock;
6
7use anyhow::{Context, Result, bail};
8use serde::Deserialize;
9
10use super::Zig;
11
12impl Zig {
13 pub fn command() -> Result<Command> {
15 let (zig, zig_args) = Self::find_zig()?;
16 let mut cmd = Command::new(zig);
17 cmd.args(zig_args);
18 Ok(cmd)
19 }
20
21 pub(crate) fn zig_version() -> Result<semver::Version> {
22 static ZIG_VERSION: OnceLock<semver::Version> = OnceLock::new();
23
24 if let Some(version) = ZIG_VERSION.get() {
25 return Ok(version.clone());
26 }
27 if let Ok(version_str) = env::var("CARGO_ZIGBUILD_ZIG_VERSION")
29 && let Ok(version) = semver::Version::parse(&version_str)
30 {
31 return Ok(ZIG_VERSION.get_or_init(|| version).clone());
32 }
33 let output = Self::command()?.arg("version").output()?;
34 let version_str =
35 str::from_utf8(&output.stdout).context("`zig version` didn't return utf8 output")?;
36 let version = semver::Version::parse(version_str.trim())?;
37 Ok(ZIG_VERSION.get_or_init(|| version).clone())
38 }
39
40 pub fn find_zig() -> Result<(PathBuf, Vec<String>)> {
42 static ZIG_PATH: OnceLock<(PathBuf, Vec<String>)> = OnceLock::new();
43
44 if let Some(cached) = ZIG_PATH.get() {
45 return Ok(cached.clone());
46 }
47 if let Ok(path) = env::var("CARGO_ZIGBUILD_ZIG_COMMAND")
51 && !path.is_empty()
52 {
53 let path = PathBuf::from(path);
54 if path.exists() {
55 let args = env::var("CARGO_ZIGBUILD_ZIG_COMMAND_ARGS")
56 .map(|s| s.split_whitespace().map(ToString::to_string).collect())
57 .unwrap_or_default();
58 return Ok(ZIG_PATH.get_or_init(|| (path, args)).clone());
59 }
60 }
61 let result = Self::find_zig_python()
62 .or_else(|_| Self::find_zig_bin())
63 .context("Failed to find zig")?;
64 Ok(ZIG_PATH.get_or_init(|| result).clone())
65 }
66
67 fn find_zig_bin() -> Result<(PathBuf, Vec<String>)> {
69 let zig_path = zig_path()?;
70 let output = Command::new(&zig_path).arg("version").output()?;
71
72 let version_str = str::from_utf8(&output.stdout).with_context(|| {
73 format!("`{} version` didn't return utf8 output", zig_path.display())
74 })?;
75 Self::validate_zig_version(version_str)?;
76 Ok((zig_path, Vec::new()))
77 }
78
79 fn find_zig_python() -> Result<(PathBuf, Vec<String>)> {
81 let python_path = python_path()?;
82 let output = Command::new(&python_path)
83 .args(["-m", "ziglang", "version"])
84 .output()?;
85
86 let version_str = str::from_utf8(&output.stdout).with_context(|| {
87 format!(
88 "`{} -m ziglang version` didn't return utf8 output",
89 python_path.display()
90 )
91 })?;
92 Self::validate_zig_version(version_str)?;
93 Ok((python_path, vec!["-m".to_string(), "ziglang".to_string()]))
94 }
95
96 fn validate_zig_version(version: &str) -> Result<()> {
97 let min_ver = semver::Version::new(0, 9, 0);
98 let version = semver::Version::parse(version.trim())?;
99 if version >= min_ver {
100 Ok(())
101 } else {
102 bail!(
103 "zig version {} is too old, need at least {}",
104 version,
105 min_ver
106 )
107 }
108 }
109
110 pub fn lib_dir() -> Result<PathBuf> {
112 static LIB_DIR: OnceLock<PathBuf> = OnceLock::new();
113
114 if let Some(cached) = LIB_DIR.get() {
115 return Ok(cached.clone());
116 }
117 let (zig, zig_args) = Self::find_zig()?;
118 let zig_version = Self::zig_version()?;
119 let output = Command::new(zig).args(zig_args).arg("env").output()?;
120 let parse_zon_lib_dir = || -> Result<PathBuf> {
121 let output_str =
122 str::from_utf8(&output.stdout).context("`zig env` didn't return utf8 output")?;
123 let lib_dir = output_str
124 .find(".lib_dir")
125 .and_then(|idx| {
126 let bytes = output_str.as_bytes();
127 let mut start = idx;
128 while start < bytes.len() && bytes[start] != b'"' {
129 start += 1;
130 }
131 if start >= bytes.len() {
132 return None;
133 }
134 let mut end = start + 1;
135 while end < bytes.len() && bytes[end] != b'"' {
136 end += 1;
137 }
138 if end >= bytes.len() {
139 return None;
140 }
141 Some(&output_str[start + 1..end])
142 })
143 .context("Failed to parse lib_dir from `zig env` ZON output")?;
144 Ok(PathBuf::from(lib_dir))
145 };
146 let lib_dir = if zig_version >= semver::Version::new(0, 15, 0) {
147 parse_zon_lib_dir()?
148 } else {
149 serde_json::from_slice::<ZigEnv>(&output.stdout)
150 .map(|zig_env| PathBuf::from(zig_env.lib_dir))
151 .or_else(|_| parse_zon_lib_dir())?
152 };
153 Ok(LIB_DIR.get_or_init(|| lib_dir).clone())
154 }
155}
156
157#[derive(Debug, Deserialize)]
158struct ZigEnv {
159 lib_dir: String,
160}
161
162fn python_path() -> Result<PathBuf> {
163 let python = env::var("CARGO_ZIGBUILD_PYTHON_PATH").unwrap_or_else(|_| "python3".to_string());
164 Ok(which::which(python)?)
165}
166
167fn zig_path() -> Result<PathBuf> {
168 let zig = env::var("CARGO_ZIGBUILD_ZIG_PATH").unwrap_or_else(|_| "zig".to_string());
169 Ok(which::which(zig)?)
170}
171
172pub(crate) fn cache_dir() -> PathBuf {
173 env::var("CARGO_ZIGBUILD_CACHE_DIR")
174 .ok()
175 .map(|s| s.into())
176 .or_else(dirs::cache_dir)
177 .unwrap_or_else(|| env::current_dir().expect("Failed to get current dir"))
179 .join(env!("CARGO_PKG_NAME"))
180 .join(env!("CARGO_PKG_VERSION"))
181}