Skip to main content

cargo_zigbuild/zig/
mod.rs

1mod cargo_env;
2mod cli_config;
3mod linker_args;
4mod locate;
5mod target_info;
6mod wrapper;
7
8use std::env;
9use std::process;
10
11use anyhow::{Context, Result, bail};
12use fs_err as fs;
13use target_lexicon::Architecture;
14
15use cargo_env::{write_file, write_tbd_files};
16use linker_args::{FilteredArg, dedup_apple_link_libs, filter_linker_arg, filter_linker_args};
17use locate::cache_dir;
18use target_info::TargetInfo;
19
20pub use cli_config::CliConfig;
21#[cfg(target_os = "windows")]
22pub use wrapper::adjust_canonicalization;
23pub use wrapper::{ZigWrapper, prepare_zig_linker, prepare_zig_linker_with_cli_config};
24
25/// Zig linker wrapper
26#[derive(Clone, Debug, clap::Subcommand)]
27pub enum Zig {
28    /// `zig cc` wrapper
29    #[command(name = "cc")]
30    Cc {
31        /// `zig cc` arguments
32        #[arg(num_args = 1.., trailing_var_arg = true)]
33        args: Vec<String>,
34    },
35    /// `zig c++` wrapper
36    #[command(name = "c++")]
37    Cxx {
38        /// `zig c++` arguments
39        #[arg(num_args = 1.., trailing_var_arg = true)]
40        args: Vec<String>,
41    },
42    /// `zig ar` wrapper
43    #[command(name = "ar")]
44    Ar {
45        /// `zig ar` arguments
46        #[arg(num_args = 1.., trailing_var_arg = true)]
47        args: Vec<String>,
48    },
49    /// `zig ranlib` wrapper
50    #[command(name = "ranlib")]
51    Ranlib {
52        /// `zig ranlib` arguments
53        #[arg(num_args = 1.., trailing_var_arg = true)]
54        args: Vec<String>,
55    },
56    /// `zig lib` wrapper
57    #[command(name = "lib")]
58    Lib {
59        /// `zig lib` arguments
60        #[arg(num_args = 1.., trailing_var_arg = true)]
61        args: Vec<String>,
62    },
63    /// `zig dlltool` wrapper
64    #[command(name = "dlltool")]
65    Dlltool {
66        /// `zig dlltool` arguments
67        #[arg(num_args = 1.., trailing_var_arg = true)]
68        args: Vec<String>,
69    },
70}
71
72impl Zig {
73    /// Execute the underlying zig command
74    pub fn execute(&self) -> Result<()> {
75        match self {
76            Zig::Cc { args } => self.execute_compiler("cc", args),
77            Zig::Cxx { args } => self.execute_compiler("c++", args),
78            Zig::Ar { args } => self.execute_tool("ar", args),
79            Zig::Ranlib { args } => self.execute_compiler("ranlib", args),
80            Zig::Lib { args } => self.execute_compiler("lib", args),
81            Zig::Dlltool { args } => self.execute_dlltool(args),
82        }
83    }
84
85    /// Execute zig dlltool command
86    /// Filter out unsupported options for older zig versions (< 0.12)
87    pub fn execute_dlltool(&self, cmd_args: &[String]) -> Result<()> {
88        let zig_version = Zig::zig_version()?;
89        let needs_filtering = zig_version.major == 0 && zig_version.minor < 12;
90
91        if !needs_filtering {
92            return self.execute_tool("dlltool", cmd_args);
93        }
94
95        // Filter out --no-leading-underscore, --temp-prefix, and -t (short form)
96        // These options are not supported by zig dlltool in versions < 0.12
97        let mut filtered_args = Vec::with_capacity(cmd_args.len());
98        let mut skip_next = false;
99        for arg in cmd_args {
100            if skip_next {
101                skip_next = false;
102                continue;
103            }
104            if arg == "--no-leading-underscore" {
105                continue;
106            }
107            if arg == "--temp-prefix" || arg == "-t" {
108                // Skip this arg and the next one (the value)
109                skip_next = true;
110                continue;
111            }
112            // Handle --temp-prefix=value and -t=value forms
113            if arg.starts_with("--temp-prefix=") || arg.starts_with("-t=") {
114                continue;
115            }
116            filtered_args.push(arg.clone());
117        }
118
119        self.execute_tool("dlltool", &filtered_args)
120    }
121
122    /// Execute zig cc/c++ command
123    pub fn execute_compiler(&self, cmd: &str, cmd_args: &[String]) -> Result<()> {
124        let target = cmd_args
125            .iter()
126            .position(|x| x == "-target")
127            .and_then(|index| cmd_args.get(index + 1));
128        let target_info = TargetInfo::new(target);
129
130        let rustc_ver = match env::var("CARGO_ZIGBUILD_RUSTC_VERSION") {
131            Ok(version) => version.parse()?,
132            Err(_) => rustc_version::version()?,
133        };
134        let zig_version = Zig::zig_version()?;
135
136        let mut new_cmd_args = Vec::with_capacity(cmd_args.len());
137        let mut skip_next_arg = false;
138        let mut seen_target = false;
139        for arg in cmd_args {
140            if skip_next_arg {
141                skip_next_arg = false;
142                continue;
143            }
144            // Our wrapper script already passes the correct -target;
145            // skip any duplicate -target from rustc to avoid conflicts
146            // (e.g. rustc passes arm64 which zig doesn't recognize for some targets)
147            if arg == "-target" {
148                if seen_target {
149                    skip_next_arg = true;
150                    continue;
151                }
152                seen_target = true;
153            }
154            let args = if arg.starts_with('@') && arg.ends_with("linker-arguments") {
155                vec![self.process_linker_response_file(
156                    arg,
157                    &rustc_ver,
158                    &zig_version,
159                    &target_info,
160                )?]
161            } else {
162                match self.filter_linker_arg(arg, &rustc_ver, &zig_version, &target_info) {
163                    FilteredArg::Keep(filtered) => filtered,
164                    FilteredArg::Skip => continue,
165                    FilteredArg::SkipWithNext => {
166                        skip_next_arg = true;
167                        continue;
168                    }
169                }
170            };
171            new_cmd_args.extend(args);
172        }
173
174        if target_info.is_apple_platform() {
175            new_cmd_args = dedup_apple_link_libs(new_cmd_args);
176        }
177
178        if target_info.is_mips32() {
179            // See https://github.com/ziglang/zig/issues/4925#issuecomment-1499823425
180            new_cmd_args.push("-Wl,-z,notext".to_string());
181        }
182
183        // Rust's libstd for strict-align arm targets calls the ARM RTABI
184        // unaligned-access helpers (__aeabi_uread4 etc.), which libgcc
185        // provides but zig's compiler-rt does not; link weak definitions
186        if target_info.is_arm() && !cmd_args.iter().any(|x| x == "-c" || x == "-E" || x == "-S") {
187            let cache_dir = cache_dir();
188            fs::create_dir_all(&cache_dir)?;
189            let shim_path = cache_dir.join("aeabi_unaligned.c");
190            write_file(&shim_path, AEABI_UNALIGNED_C)?;
191            new_cmd_args.push(shim_path.display().to_string());
192        }
193
194        if target_info.is_windows_gnu() && (zig_version.major, zig_version.minor) >= (0, 16) {
195            new_cmd_args.push("-lcompiler_rt".to_string());
196        }
197
198        if self.has_undefined_dynamic_lookup(cmd_args) {
199            new_cmd_args.push("-Wl,-undefined=dynamic_lookup".to_string());
200        }
201        if target_info.is_macos() {
202            if self.should_add_libcharset(cmd_args, &zig_version) {
203                new_cmd_args.push("-lcharset".to_string());
204            }
205            self.add_macos_specific_args(&mut new_cmd_args, &zig_version)?;
206        }
207
208        // For Zig >= 0.15 with macOS, set SDKROOT environment variable
209        // if it exists, instead of passing --sysroot
210        let mut command = Self::command()?;
211        if (zig_version.major, zig_version.minor) >= (0, 15)
212            && let Some(sdkroot) = Self::macos_sdk_root()
213        {
214            command.env("SDKROOT", sdkroot);
215        }
216
217        let mut child = command
218            .arg(cmd)
219            .args(new_cmd_args)
220            .spawn()
221            .with_context(|| format!("Failed to run `zig {cmd}`"))?;
222        let status = child.wait().expect("Failed to wait on zig child process");
223        if !status.success() {
224            process::exit(status.code().unwrap_or(1));
225        }
226        Ok(())
227    }
228
229    fn process_linker_response_file(
230        &self,
231        arg: &str,
232        rustc_ver: &rustc_version::Version,
233        zig_version: &semver::Version,
234        target_info: &TargetInfo,
235    ) -> Result<String> {
236        // rustc passes arguments to linker via an @-file when arguments are too long
237        // See https://github.com/rust-lang/rust/issues/41190
238        // and https://github.com/rust-lang/rust/blob/87937d3b6c302dfedfa5c4b94d0a30985d46298d/compiler/rustc_codegen_ssa/src/back/link.rs#L1373-L1382
239        let content_bytes = fs::read(arg.trim_start_matches('@'))?;
240        let content = if target_info.is_windows_msvc() {
241            if content_bytes[0..2] != [255, 254] {
242                bail!(
243                    "linker response file `{}` didn't start with a utf16 BOM",
244                    &arg
245                );
246            }
247            let content_utf16: Vec<u16> = content_bytes[2..]
248                .chunks_exact(2)
249                .map(|a| u16::from_ne_bytes([a[0], a[1]]))
250                .collect();
251            String::from_utf16(&content_utf16).with_context(|| {
252                format!(
253                    "linker response file `{}` didn't contain valid utf16 content",
254                    &arg
255                )
256            })?
257        } else {
258            String::from_utf8(content_bytes).with_context(|| {
259                format!(
260                    "linker response file `{}` didn't contain valid utf8 content",
261                    &arg
262                )
263            })?
264        };
265        let mut link_args: Vec<_> = filter_linker_args(
266            content.split('\n').map(|s| s.to_string()),
267            rustc_ver,
268            zig_version,
269            target_info,
270        );
271        if self.has_undefined_dynamic_lookup(&link_args) {
272            link_args.push("-Wl,-undefined=dynamic_lookup".to_string());
273        }
274        if target_info.is_macos() && self.should_add_libcharset(&link_args, zig_version) {
275            link_args.push("-lcharset".to_string());
276        }
277        if target_info.is_windows_msvc() {
278            let new_content = link_args.join("\n");
279            let mut out = Vec::with_capacity((1 + new_content.len()) * 2);
280            // start the stream with a UTF-16 BOM
281            for c in std::iter::once(0xFEFF).chain(new_content.encode_utf16()) {
282                // encode in little endian
283                out.push(c as u8);
284                out.push((c >> 8) as u8);
285            }
286            fs::write(arg.trim_start_matches('@'), out)?;
287        } else {
288            fs::write(arg.trim_start_matches('@'), link_args.join("\n").as_bytes())?;
289        }
290        Ok(arg.to_string())
291    }
292
293    fn filter_linker_arg(
294        &self,
295        arg: &str,
296        rustc_ver: &rustc_version::Version,
297        zig_version: &semver::Version,
298        target_info: &TargetInfo,
299    ) -> FilteredArg {
300        filter_linker_arg(arg, rustc_ver, zig_version, target_info)
301    }
302
303    fn has_undefined_dynamic_lookup(&self, args: &[String]) -> bool {
304        let undefined = args
305            .iter()
306            .position(|x| x == "-undefined")
307            .and_then(|i| args.get(i + 1));
308        matches!(undefined, Some(x) if x == "dynamic_lookup")
309    }
310
311    fn should_add_libcharset(&self, args: &[String], zig_version: &semver::Version) -> bool {
312        // See https://github.com/apple-oss-distributions/libiconv/blob/a167071feb7a83a01b27ec8d238590c14eb6faff/xcodeconfig/libiconv.xcconfig
313        if (zig_version.major, zig_version.minor) >= (0, 12) {
314            args.iter().any(|x| x == "-liconv") && !args.iter().any(|x| x == "-lcharset")
315        } else {
316            false
317        }
318    }
319
320    fn add_macos_specific_args(
321        &self,
322        new_cmd_args: &mut Vec<String>,
323        zig_version: &semver::Version,
324    ) -> Result<()> {
325        let sdkroot = Self::macos_sdk_root();
326        if (zig_version.major, zig_version.minor) >= (0, 12) {
327            // Zig 0.12.0+ requires passing `--sysroot`
328            // However, for Zig 0.15+, we should use SDKROOT environment variable instead
329            // to avoid issues with library paths being interpreted relative to sysroot
330            if let Some(ref sdkroot) = sdkroot
331                && (zig_version.major, zig_version.minor) < (0, 15)
332            {
333                new_cmd_args.push(format!("--sysroot={}", sdkroot.display()));
334            }
335            // For Zig >= 0.15, SDKROOT will be set as environment variable
336        }
337        if let Some(ref sdkroot) = sdkroot {
338            if (zig_version.major, zig_version.minor) < (0, 15) {
339                // For zig < 0.15, we need to explicitly add SDK paths with --sysroot
340                new_cmd_args.extend_from_slice(&[
341                    "-isystem".to_string(),
342                    format!("{}", sdkroot.join("usr").join("include").display()),
343                    format!("-L{}", sdkroot.join("usr").join("lib").display()),
344                    format!(
345                        "-F{}",
346                        sdkroot
347                            .join("System")
348                            .join("Library")
349                            .join("Frameworks")
350                            .display()
351                    ),
352                    "-DTARGET_OS_IPHONE=0".to_string(),
353                ]);
354            } else {
355                // For zig >= 0.15 with SDKROOT, we still need to add framework paths
356                // Use -iframework for framework header search
357                new_cmd_args.extend_from_slice(&[
358                    "-isystem".to_string(),
359                    format!("{}", sdkroot.join("usr").join("include").display()),
360                    format!("-L{}", sdkroot.join("usr").join("lib").display()),
361                    format!(
362                        "-F{}",
363                        sdkroot
364                            .join("System")
365                            .join("Library")
366                            .join("Frameworks")
367                            .display()
368                    ),
369                    // Also add the SYSTEM framework search path
370                    "-iframework".to_string(),
371                    format!(
372                        "{}",
373                        sdkroot
374                            .join("System")
375                            .join("Library")
376                            .join("Frameworks")
377                            .display()
378                    ),
379                    "-DTARGET_OS_IPHONE=0".to_string(),
380                ]);
381            }
382        }
383
384        // Add the deps directory that contains `.tbd` files to the library search path
385        let cache_dir = cache_dir();
386        let deps_dir = cache_dir.join("deps");
387        fs::create_dir_all(&deps_dir)?;
388        write_tbd_files(&deps_dir)?;
389        new_cmd_args.push("-L".to_string());
390        new_cmd_args.push(format!("{}", deps_dir.display()));
391        Ok(())
392    }
393
394    /// Execute zig ar/ranlib command
395    pub fn execute_tool(&self, cmd: &str, cmd_args: &[String]) -> Result<()> {
396        let mut child = Self::command()?
397            .arg(cmd)
398            .args(cmd_args)
399            .spawn()
400            .with_context(|| format!("Failed to run `zig {cmd}`"))?;
401        let status = child.wait().expect("Failed to wait on zig child process");
402        if !status.success() {
403            process::exit(status.code().unwrap_or(1));
404        }
405        Ok(())
406    }
407}
408
409/// Weak definitions of the ARM RTABI unaligned-access helpers
410/// (run-time ABI for the ARM architecture, IHI0043, section 4.3.3).
411/// libgcc provides these but LLVM's (and zig's) compiler-rt does not,
412/// and Rust's libstd for strict-align arm targets calls them.
413const AEABI_UNALIGNED_C: &str = r#"
414#ifdef __cplusplus
415extern "C" {
416#endif
417__attribute__((weak)) int __aeabi_uread4(void *address) {
418    int value;
419    __builtin_memcpy(&value, address, 4);
420    return value;
421}
422__attribute__((weak)) int __aeabi_uwrite4(int value, void *address) {
423    __builtin_memcpy(address, &value, 4);
424    return value;
425}
426__attribute__((weak)) long long __aeabi_uread8(void *address) {
427    long long value;
428    __builtin_memcpy(&value, address, 8);
429    return value;
430}
431__attribute__((weak)) long long __aeabi_uwrite8(long long value, void *address) {
432    __builtin_memcpy(address, &value, 8);
433    return value;
434}
435#ifdef __cplusplus
436}
437#endif
438"#;
439
440/// Get the dlltool executable name for the given architecture
441/// On Windows, rustc looks for "dlltool.exe"
442/// On non-Windows hosts, rustc looks for architecture-specific names
443pub(crate) fn get_dlltool_name(arch: &Architecture) -> &'static str {
444    if cfg!(windows) {
445        "dlltool"
446    } else {
447        match arch {
448            Architecture::X86_64 => "x86_64-w64-mingw32-dlltool",
449            Architecture::X86_32(_) => "i686-w64-mingw32-dlltool",
450            Architecture::Aarch64(_) => "aarch64-w64-mingw32-dlltool",
451            _ => "dlltool",
452        }
453    }
454}
455
456/// Check if a dlltool for the given architecture exists in PATH
457/// Returns true if found, false otherwise
458pub(crate) fn has_system_dlltool(arch: &Architecture) -> bool {
459    which::which(get_dlltool_name(arch)).is_ok()
460}