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            new_cmd_args.push(shim_path.display().to_string());
195        }
196
197        if target_info.is_windows_gnu() && (zig_version.major, zig_version.minor) >= (0, 16) {
198            new_cmd_args.push("-lcompiler_rt".to_string());
199        }
200
201        if self.has_undefined_dynamic_lookup(cmd_args) {
202            new_cmd_args.push("-Wl,-undefined=dynamic_lookup".to_string());
203        }
204        if target_info.is_macos() {
205            if self.should_add_libcharset(cmd_args, &zig_version) {
206                new_cmd_args.push("-lcharset".to_string());
207            }
208            self.add_macos_specific_args(&mut new_cmd_args, &zig_version)?;
209        }
210
211        // For Zig >= 0.15 with macOS, set SDKROOT environment variable
212        // if it exists, instead of passing --sysroot
213        let mut command = Self::command()?;
214        if (zig_version.major, zig_version.minor) >= (0, 15)
215            && let Some(sdkroot) = Self::macos_sdk_root()
216        {
217            command.env("SDKROOT", sdkroot);
218        }
219
220        let mut child = command
221            .arg(cmd)
222            .args(new_cmd_args)
223            .spawn()
224            .with_context(|| format!("Failed to run `zig {cmd}`"))?;
225        let status = child.wait().expect("Failed to wait on zig child process");
226        if !status.success() {
227            process::exit(status.code().unwrap_or(1));
228        }
229        Ok(())
230    }
231
232    fn process_linker_response_file(
233        &self,
234        arg: &str,
235        rustc_ver: &rustc_version::Version,
236        zig_version: &semver::Version,
237        target_info: &TargetInfo,
238        linker_filter: &mut LinkerArgFilter,
239    ) -> Result<String> {
240        // rustc passes arguments to linker via an @-file when arguments are too long
241        // See https://github.com/rust-lang/rust/issues/41190
242        // and https://github.com/rust-lang/rust/blob/87937d3b6c302dfedfa5c4b94d0a30985d46298d/compiler/rustc_codegen_ssa/src/back/link.rs#L1373-L1382
243        let content_bytes = fs::read(arg.trim_start_matches('@'))?;
244        let content = if target_info.is_windows_msvc() {
245            if content_bytes[0..2] != [255, 254] {
246                bail!(
247                    "linker response file `{}` didn't start with a utf16 BOM",
248                    &arg
249                );
250            }
251            let content_utf16: Vec<u16> = content_bytes[2..]
252                .chunks_exact(2)
253                .map(|a| u16::from_ne_bytes([a[0], a[1]]))
254                .collect();
255            String::from_utf16(&content_utf16).with_context(|| {
256                format!(
257                    "linker response file `{}` didn't contain valid utf16 content",
258                    &arg
259                )
260            })?
261        } else {
262            String::from_utf8(content_bytes).with_context(|| {
263                format!(
264                    "linker response file `{}` didn't contain valid utf8 content",
265                    &arg
266                )
267            })?
268        };
269        // For a non-MSVC linker, rustc escapes each `\` and each space with a
270        // backslash (the GNU ld convention), while zig reads response files
271        // with the Windows command line rules, so unescape before filtering
272        // and requote for zig below.
273        let unescape = !target_info.is_windows_msvc();
274        let mut link_args = linker_filter.filter_args(
275            // A final newline must not consume an operand expected after this file.
276            content.split_terminator('\n').map(|line| {
277                if unescape {
278                    unescape_linker_response_line(line)
279                } else {
280                    line.to_string()
281                }
282            }),
283            rustc_ver,
284            zig_version,
285            target_info,
286        );
287        if self.has_undefined_dynamic_lookup(&link_args) {
288            link_args.push("-Wl,-undefined=dynamic_lookup".to_string());
289        }
290        if target_info.is_macos() && self.should_add_libcharset(&link_args, zig_version) {
291            link_args.push("-lcharset".to_string());
292        }
293        if target_info.is_windows_msvc() {
294            let new_content = link_args.join("\n");
295            let mut out = Vec::with_capacity((1 + new_content.len()) * 2);
296            // start the stream with a UTF-16 BOM
297            for c in std::iter::once(0xFEFF).chain(new_content.encode_utf16()) {
298                // encode in little endian
299                out.push(c as u8);
300                out.push((c >> 8) as u8);
301            }
302            fs::write(arg.trim_start_matches('@'), out)?;
303        } else {
304            // zig parses the response file with the Windows command line
305            // rules, not rustc's GNU ld escaping, so rewrite each argument
306            // in that form; one argument per line, as rustc writes it
307            let new_content = link_args
308                .iter()
309                .map(|arg| quote_linker_response_arg(arg))
310                .collect::<Vec<_>>()
311                .join("\n");
312            fs::write(arg.trim_start_matches('@'), new_content.as_bytes())?;
313        }
314        Ok(arg.to_string())
315    }
316
317    fn has_undefined_dynamic_lookup(&self, args: &[String]) -> bool {
318        let undefined = args
319            .iter()
320            .position(|x| x == "-undefined")
321            .and_then(|i| args.get(i + 1));
322        matches!(undefined, Some(x) if x == "dynamic_lookup")
323    }
324
325    fn should_add_libcharset(&self, args: &[String], zig_version: &semver::Version) -> bool {
326        // See https://github.com/apple-oss-distributions/libiconv/blob/a167071feb7a83a01b27ec8d238590c14eb6faff/xcodeconfig/libiconv.xcconfig
327        if (zig_version.major, zig_version.minor) >= (0, 12) {
328            args.iter().any(|x| x == "-liconv") && !args.iter().any(|x| x == "-lcharset")
329        } else {
330            false
331        }
332    }
333
334    fn add_macos_specific_args(
335        &self,
336        new_cmd_args: &mut Vec<String>,
337        zig_version: &semver::Version,
338    ) -> Result<()> {
339        let sdkroot = Self::macos_sdk_root();
340        if (zig_version.major, zig_version.minor) >= (0, 12) {
341            // Zig 0.12.0+ requires passing `--sysroot`
342            // However, for Zig 0.15+, we should use SDKROOT environment variable instead
343            // to avoid issues with library paths being interpreted relative to sysroot
344            if let Some(ref sdkroot) = sdkroot
345                && (zig_version.major, zig_version.minor) < (0, 15)
346            {
347                new_cmd_args.push(format!("--sysroot={}", sdkroot.display()));
348            }
349            // For Zig >= 0.15, SDKROOT will be set as environment variable
350        }
351        if let Some(ref sdkroot) = sdkroot {
352            if (zig_version.major, zig_version.minor) < (0, 15) {
353                // For zig < 0.15, we need to explicitly add SDK paths with --sysroot
354                new_cmd_args.extend_from_slice(&[
355                    "-isystem".to_string(),
356                    format!("{}", sdkroot.join("usr").join("include").display()),
357                    format!("-L{}", sdkroot.join("usr").join("lib").display()),
358                    format!(
359                        "-F{}",
360                        sdkroot
361                            .join("System")
362                            .join("Library")
363                            .join("Frameworks")
364                            .display()
365                    ),
366                    "-DTARGET_OS_IPHONE=0".to_string(),
367                ]);
368            } else {
369                // For zig >= 0.15 with SDKROOT, we still need to add framework paths
370                // Use -iframework for framework header search
371                new_cmd_args.extend_from_slice(&[
372                    "-isystem".to_string(),
373                    format!("{}", sdkroot.join("usr").join("include").display()),
374                    format!("-L{}", sdkroot.join("usr").join("lib").display()),
375                    format!(
376                        "-F{}",
377                        sdkroot
378                            .join("System")
379                            .join("Library")
380                            .join("Frameworks")
381                            .display()
382                    ),
383                    // Also add the SYSTEM framework search path
384                    "-iframework".to_string(),
385                    format!(
386                        "{}",
387                        sdkroot
388                            .join("System")
389                            .join("Library")
390                            .join("Frameworks")
391                            .display()
392                    ),
393                    "-DTARGET_OS_IPHONE=0".to_string(),
394                ]);
395            }
396        }
397
398        // Add the deps directory that contains `.tbd` files to the library search path
399        let cache_dir = cache_dir();
400        let deps_dir = cache_dir.join("deps");
401        fs::create_dir_all(&deps_dir)?;
402        write_tbd_files(&deps_dir)?;
403        new_cmd_args.push("-L".to_string());
404        new_cmd_args.push(format!("{}", deps_dir.display()));
405        Ok(())
406    }
407
408    /// Execute zig ar/ranlib command
409    pub fn execute_tool(&self, cmd: &str, cmd_args: &[String]) -> Result<()> {
410        let mut child = Self::command()?
411            .arg(cmd)
412            .args(cmd_args)
413            .spawn()
414            .with_context(|| format!("Failed to run `zig {cmd}`"))?;
415        let status = child.wait().expect("Failed to wait on zig child process");
416        if !status.success() {
417            process::exit(status.code().unwrap_or(1));
418        }
419        Ok(())
420    }
421}
422
423/// Weak definitions of the ARM RTABI unaligned-access helpers
424/// (run-time ABI for the ARM architecture, IHI0043, section 4.3.3).
425/// libgcc provides these but LLVM's (and zig's) compiler-rt does not,
426/// and Rust's libstd for strict-align arm targets calls them.
427const AEABI_UNALIGNED_C: &str = r#"
428#ifdef __cplusplus
429extern "C" {
430#endif
431__attribute__((weak)) int __aeabi_uread4(void *address) {
432    int value;
433    __builtin_memcpy(&value, address, 4);
434    return value;
435}
436__attribute__((weak)) int __aeabi_uwrite4(int value, void *address) {
437    __builtin_memcpy(address, &value, 4);
438    return value;
439}
440__attribute__((weak)) long long __aeabi_uread8(void *address) {
441    long long value;
442    __builtin_memcpy(&value, address, 8);
443    return value;
444}
445__attribute__((weak)) long long __aeabi_uwrite8(long long value, void *address) {
446    __builtin_memcpy(address, &value, 8);
447    return value;
448}
449#ifdef __cplusplus
450}
451#endif
452"#;
453
454/// Get the dlltool executable name for the given architecture
455/// On Windows, rustc looks for "dlltool.exe"
456/// On non-Windows hosts, rustc looks for architecture-specific names
457pub(crate) fn get_dlltool_name(arch: &Architecture) -> &'static str {
458    if cfg!(windows) {
459        "dlltool"
460    } else {
461        match arch {
462            Architecture::X86_64 => "x86_64-w64-mingw32-dlltool",
463            Architecture::X86_32(_) => "i686-w64-mingw32-dlltool",
464            Architecture::Aarch64(_) => "aarch64-w64-mingw32-dlltool",
465            _ => "dlltool",
466        }
467    }
468}
469
470/// Check if a dlltool for the given architecture exists in PATH
471/// Returns true if found, false otherwise
472pub(crate) fn has_system_dlltool(arch: &Architecture) -> bool {
473    which::which(get_dlltool_name(arch)).is_ok()
474}