Skip to main content

cargo_zigbuild/zig/
locate.rs

1use 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    /// Build the zig command line
14    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        // Check for cached version from environment variable first
28        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    /// Search for `python -m ziglang` first and for `zig` second.
41    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        // Trust the zig command resolved when the linker wrapper was generated;
48        // this avoids spawning `python -m ziglang version` and `zig version`
49        // probes on every compiler invocation.
50        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    /// Detect the plain zig binary
68    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    /// Detect the Python ziglang package
80    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    /// Find zig lib directory
111    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        // If the really is no cache dir, cwd will also do
178        .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}