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