1use std::env;
2use std::ffi::OsStr;
3#[cfg(target_family = "unix")]
4use std::fs::OpenOptions;
5use std::io::Write;
6#[cfg(target_family = "unix")]
7use std::os::unix::fs::OpenOptionsExt;
8use std::path::{Path, PathBuf};
9
10use anyhow::{Context, Result, bail};
11use fs_err as fs;
12#[cfg(not(target_family = "unix"))]
13use path_slash::PathBufExt;
14use target_lexicon::{Architecture, Environment, OperatingSystem, Triple};
15
16use super::cli_config::CliConfig;
17use super::locate::cache_dir;
18use super::{Zig, get_dlltool_name, has_system_dlltool};
19
20#[derive(Debug, Clone)]
22pub struct ZigWrapper {
23 pub cc: PathBuf,
24 pub cxx: PathBuf,
25 pub ar: PathBuf,
26 pub ranlib: PathBuf,
27 pub lib: PathBuf,
28}
29
30#[derive(Debug, Clone, Default, PartialEq)]
31struct TargetFlags {
32 pub target_cpu: String,
33 pub target_feature: String,
34}
35
36impl TargetFlags {
37 pub fn parse_from_encoded(encoded: &OsStr) -> Result<Self> {
38 let mut parsed = Self::default();
39
40 let f = rustflags::from_encoded(encoded);
41 for flag in f {
42 if let rustflags::Flag::Codegen { opt, value } = flag {
43 let key = opt.replace('-', "_");
44 match key.as_str() {
45 "target_cpu" => {
46 if let Some(value) = value {
47 parsed.target_cpu = value;
48 }
49 }
50 "target_feature" => {
51 if let Some(value) = value {
53 if !parsed.target_feature.is_empty() {
54 parsed.target_feature.push(',');
55 }
56 parsed.target_feature.push_str(&value);
57 }
58 }
59 _ => {}
60 }
61 }
62 }
63 Ok(parsed)
64 }
65}
66
67#[allow(clippy::blocks_in_conditions)]
76pub fn prepare_zig_linker(
77 target: &str,
78 cargo_config: &cargo_config2::Config,
79) -> Result<ZigWrapper> {
80 prepare_zig_linker_with_cli_config(target, cargo_config, &[])
81}
82
83pub fn prepare_zig_linker_with_cli_config(
87 target: &str,
88 cargo_config: &cargo_config2::Config,
89 config_args: &[String],
90) -> Result<ZigWrapper> {
91 let (rust_target, abi_suffix) = target.split_once('.').unwrap_or((target, ""));
92 let abi_suffix = if abi_suffix.is_empty() {
93 String::new()
94 } else {
95 if abi_suffix
96 .split_once('.')
97 .filter(|(x, y)| {
98 !x.is_empty()
99 && x.chars().all(|c| c.is_ascii_digit())
100 && !y.is_empty()
101 && y.chars().all(|c| c.is_ascii_digit())
102 })
103 .is_none()
104 {
105 bail!("Malformed zig target abi suffix.")
106 }
107 format!(".{abi_suffix}")
108 };
109 let triple: Triple = rust_target
110 .parse()
111 .with_context(|| format!("Unsupported Rust target '{rust_target}'"))?;
112 let arch = triple.architecture.to_string();
113 let target_env = match (triple.architecture, triple.environment) {
114 (Architecture::Mips32(..), Environment::Gnu) => Environment::Gnueabihf,
115 (Architecture::Mips32(..), Environment::Musl) => Environment::Musleabi,
116 (Architecture::Powerpc, Environment::Gnu) => Environment::Gnueabihf,
117 (_, Environment::GnuLlvm) => Environment::Gnu,
118 (_, environment) => environment,
119 };
120 let file_ext = if cfg!(windows) { "bat" } else { "sh" };
121 let file_target = target.trim_end_matches('.');
122
123 let mut cc_args = vec![
124 "-g".to_owned(),
126 "-fno-sanitize=all".to_owned(),
128 ];
129
130 let zig_mcpu_default = match triple.operating_system {
133 OperatingSystem::Linux => {
134 match arch.as_str() {
135 "arm" => match target_env {
137 Environment::Gnueabi | Environment::Musleabi => "generic+v6+strict_align",
138 Environment::Gnueabihf | Environment::Musleabihf => {
139 "generic+v6+strict_align+vfp2-d32"
140 }
141 _ => "",
142 },
143 "armv5te" => "generic+soft_float+strict_align",
144 "armv7" => "generic+v7a+vfp3-d32+thumb2-neon",
145 arch_str @ ("i586" | "i686") => {
146 if arch_str == "i586" {
147 "pentium"
148 } else {
149 "pentium4"
150 }
151 }
152 "riscv64gc" => "generic_rv64+m+a+f+d+c",
153 "s390x" => "z10-vector",
154 _ => "",
155 }
156 }
157 _ => "",
158 };
159
160 let zig_mcpu_override = {
164 let cli_config = CliConfig::parse(config_args)?;
165 let rust_flags = cli_config
166 .rustflags(cargo_config, rust_target)?
167 .unwrap_or_default();
168 let encoded_rust_flags = rust_flags.encode()?;
169 let target_flags = TargetFlags::parse_from_encoded(OsStr::new(&encoded_rust_flags))?;
170 target_flags.target_cpu.replace('-', "_")
173 };
174
175 if !zig_mcpu_override.is_empty() {
176 cc_args.push(format!("-mcpu={zig_mcpu_override}"));
177 } else if !zig_mcpu_default.is_empty() {
178 cc_args.push(format!("-mcpu={zig_mcpu_default}"));
179 }
180
181 match triple.operating_system {
182 OperatingSystem::Linux => {
183 let zig_arch = match arch.as_str() {
184 "arm" => "arm",
186 "armv5te" => "arm",
187 "armv7" => "arm",
188 "i586" | "i686" => {
189 let zig_version = Zig::zig_version()?;
190 if zig_version.major == 0 && zig_version.minor >= 11 {
191 "x86"
192 } else {
193 "i386"
194 }
195 }
196 "riscv64gc" => "riscv64",
197 "s390x" => "s390x",
198 _ => arch.as_str(),
199 };
200 let mut zig_target_env = target_env.to_string();
201
202 let zig_version = Zig::zig_version()?;
203
204 if zig_version >= semver::Version::new(0, 15, 0)
208 && arch.as_str() == "armv7"
209 && target_env == Environment::Ohos
210 {
211 zig_target_env = "ohoseabi".to_string();
212 }
213
214 cc_args.push("-target".to_string());
215 cc_args.push(format!("{zig_arch}-linux-{zig_target_env}{abi_suffix}"));
216 }
217 OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin(_) => {
218 let zig_version = Zig::zig_version()?;
219 if zig_version > semver::Version::new(0, 9, 1) {
222 cc_args.push("-target".to_string());
223 cc_args.push(format!("{arch}-macos-none{abi_suffix}"));
224 } else {
225 cc_args.push("-target".to_string());
226 cc_args.push(format!("{arch}-macos-gnu{abi_suffix}"));
227 }
228 }
229 OperatingSystem::Windows => {
230 let zig_arch = match arch.as_str() {
231 "i686" => {
232 let zig_version = Zig::zig_version()?;
233 if zig_version.major == 0 && zig_version.minor >= 11 {
234 "x86"
235 } else {
236 "i386"
237 }
238 }
239 arch => arch,
240 };
241 cc_args.push("-target".to_string());
242 cc_args.push(format!("{zig_arch}-windows-{target_env}{abi_suffix}"));
243 }
244 OperatingSystem::Emscripten => {
245 cc_args.push("-target".to_string());
246 cc_args.push(format!("{arch}-emscripten{abi_suffix}"));
247 }
248 OperatingSystem::Wasi => {
249 cc_args.push("-target".to_string());
250 cc_args.push(format!("{arch}-wasi{abi_suffix}"));
251 }
252 OperatingSystem::WasiP1 => {
253 cc_args.push("-target".to_string());
254 cc_args.push(format!("{arch}-wasi.0.1.0{abi_suffix}"));
255 }
256 OperatingSystem::IOS(_) if triple.environment == Environment::Macabi => {
257 cc_args.push("-target".to_string());
260 cc_args.push(format!("{arch}-maccatalyst-none{abi_suffix}"));
261 }
262 OperatingSystem::Freebsd => {
263 let zig_arch = match arch.as_str() {
264 "i686" => {
265 let zig_version = Zig::zig_version()?;
266 if zig_version.major == 0 && zig_version.minor >= 11 {
267 "x86"
268 } else {
269 "i386"
270 }
271 }
272 arch => arch,
273 };
274 cc_args.push("-target".to_string());
275 cc_args.push(format!("{zig_arch}-freebsd"));
276 }
277 OperatingSystem::Openbsd => {
278 cc_args.push("-target".to_string());
279 cc_args.push(format!("{arch}-openbsd"));
280 }
281 OperatingSystem::Unknown => {
282 if triple.architecture == Architecture::Wasm32
283 || triple.architecture == Architecture::Wasm64
284 {
285 cc_args.push("-target".to_string());
286 cc_args.push(format!("{arch}-freestanding{abi_suffix}"));
287 } else {
288 bail!("unsupported target '{rust_target}'")
289 }
290 }
291 _ => bail!(format!("unsupported target '{rust_target}'")),
292 };
293
294 let zig_linker_dir = cache_dir();
295 fs::create_dir_all(&zig_linker_dir)?;
296
297 if triple.operating_system == OperatingSystem::Linux {
298 if matches!(
299 triple.environment,
300 Environment::Gnu
301 | Environment::Gnuspe
302 | Environment::Gnux32
303 | Environment::Gnueabi
304 | Environment::Gnuabi64
305 | Environment::GnuIlp32
306 | Environment::Gnueabihf
307 ) {
308 let glibc_version = if abi_suffix.is_empty() {
309 (2, 17)
310 } else {
311 let mut parts = abi_suffix[1..].split('.');
312 let major: usize = parts.next().unwrap().parse()?;
313 let minor: usize = parts.next().unwrap().parse()?;
314 (major, minor)
315 };
316 if glibc_version < (2, 28) {
318 use crate::linux::{FCNTL_H, FCNTL_MAP};
319
320 let zig_version = Zig::zig_version()?;
321 if zig_version.major == 0 && zig_version.minor < 11 {
322 let fcntl_map = zig_linker_dir.join("fcntl.map");
323 let existing_content = fs::read_to_string(&fcntl_map).unwrap_or_default();
324 if existing_content != FCNTL_MAP {
325 fs::write(&fcntl_map, FCNTL_MAP)?;
326 }
327 let fcntl_h = zig_linker_dir.join("fcntl.h");
328 let existing_content = fs::read_to_string(&fcntl_h).unwrap_or_default();
329 if existing_content != FCNTL_H {
330 fs::write(&fcntl_h, FCNTL_H)?;
331 }
332
333 cc_args.push(format!("-Wl,--version-script={}", fcntl_map.display()));
334 cc_args.push("-include".to_string());
335 cc_args.push(fcntl_h.display().to_string());
336 }
337 }
338 } else if matches!(
339 triple.environment,
340 Environment::Musl
341 | Environment::Muslabi64
342 | Environment::Musleabi
343 | Environment::Musleabihf
344 ) {
345 use crate::linux::MUSL_WEAK_SYMBOLS_MAPPING_SCRIPT;
346
347 let zig_version = Zig::zig_version()?;
348 let rustc_version = rustc_version::version_meta()?.semver;
349
350 if (zig_version.major, zig_version.minor) >= (0, 11)
355 && (rustc_version.major, rustc_version.minor) < (1, 72)
356 {
357 let weak_symbols_map = zig_linker_dir.join("musl_weak_symbols_map.ld");
358 fs::write(&weak_symbols_map, MUSL_WEAK_SYMBOLS_MAPPING_SCRIPT)?;
359
360 cc_args.push(format!("-Wl,-T,{}", weak_symbols_map.display()));
361 }
362 }
363 }
364
365 let cc_args_str = join_args_for_script(&cc_args);
368
369 let current_exe = resolve_current_exe()?;
374 let exe_hash = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC)
375 .checksum(current_exe.as_os_str().as_encoded_bytes());
376 let wrapper_dir = zig_linker_dir
377 .join("wrappers")
378 .join(format!("{:x}", exe_hash));
379 fs::create_dir_all(&wrapper_dir)?;
380
381 let hash = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC).checksum(cc_args_str.as_bytes());
382 let zig_cc = wrapper_dir.join(format!("zigcc-{file_target}-{:x}.{file_ext}", hash));
383 let zig_cxx = wrapper_dir.join(format!("zigcxx-{file_target}-{:x}.{file_ext}", hash));
384 let zig_ranlib = wrapper_dir.join(format!("zigranlib.{file_ext}"));
385 let zig_version = Zig::zig_version()?;
386 let zig_command = Zig::find_zig()?;
387 write_linker_wrapper(&zig_cc, "cc", &cc_args_str, &zig_version, &zig_command)?;
388 write_linker_wrapper(&zig_cxx, "c++", &cc_args_str, &zig_version, &zig_command)?;
389 write_linker_wrapper(&zig_ranlib, "ranlib", "", &zig_version, &zig_command)?;
390
391 let exe_ext = if cfg!(windows) { ".exe" } else { "" };
392 let zig_ar = wrapper_dir.join(format!("ar{exe_ext}"));
393 symlink_wrapper(&zig_ar)?;
394 let zig_lib = wrapper_dir.join(format!("lib{exe_ext}"));
395 symlink_wrapper(&zig_lib)?;
396
397 if matches!(triple.operating_system, OperatingSystem::Windows)
403 && matches!(triple.environment, Environment::Gnu)
404 {
405 if !has_system_dlltool(&triple.architecture) {
408 let dlltool_name = get_dlltool_name(&triple.architecture);
409 let zig_dlltool = wrapper_dir.join(format!("{dlltool_name}{exe_ext}"));
410 symlink_wrapper(&zig_dlltool)?;
411 }
412 }
413
414 Ok(ZigWrapper {
415 cc: zig_cc,
416 cxx: zig_cxx,
417 ar: zig_ar,
418 ranlib: zig_ranlib,
419 lib: zig_lib,
420 })
421}
422
423fn resolve_current_exe() -> Result<PathBuf> {
425 if let Ok(exe) = env::var("CARGO_BIN_EXE_cargo-zigbuild") {
426 Ok(PathBuf::from(exe))
427 } else {
428 Ok(env::current_exe()?)
429 }
430}
431
432pub(crate) fn symlink_wrapper(target: &Path) -> Result<()> {
433 let current_exe = resolve_current_exe()?;
434 #[cfg(windows)]
435 {
436 if !target.exists() {
437 if std::fs::hard_link(¤t_exe, target).is_err() {
439 std::fs::copy(¤t_exe, target)?;
441 }
442 }
443 }
444
445 #[cfg(unix)]
446 {
447 if !target.exists() {
448 if fs::read_link(target).is_ok() {
449 fs::remove_file(target)?;
451 }
452 std::os::unix::fs::symlink(current_exe, target)?;
453 }
454 }
455 Ok(())
456}
457
458#[cfg(target_family = "unix")]
460fn join_args_for_script<I, S>(args: I) -> String
461where
462 I: IntoIterator<Item = S>,
463 S: AsRef<str>,
464{
465 shell_words::join(args)
466}
467
468#[cfg(not(target_family = "unix"))]
474fn quote_for_batch(s: &str) -> String {
475 let needs_quoting_or_escaping = s.is_empty()
476 || s.contains(|c: char| {
477 matches!(
478 c,
479 ' ' | '\t' | '"' | '&' | '|' | '<' | '>' | '^' | '%' | '(' | ')' | '!'
480 )
481 });
482
483 if !needs_quoting_or_escaping {
484 return s.to_string();
485 }
486
487 let mut out = String::with_capacity(s.len() + 8);
488 out.push('"');
489 for c in s.chars() {
490 match c {
491 '"' => out.push_str("\"\""),
492 '%' => out.push_str("%%"),
493 _ => out.push(c),
494 }
495 }
496 out.push('"');
497 out
498}
499
500#[cfg(not(target_family = "unix"))]
502fn join_args_for_script<I, S>(args: I) -> String
503where
504 I: IntoIterator<Item = S>,
505 S: AsRef<str>,
506{
507 args.into_iter()
508 .map(|s| quote_for_batch(s.as_ref()))
509 .collect::<Vec<_>>()
510 .join(" ")
511}
512
513#[cfg(target_family = "unix")]
515fn write_linker_wrapper(
516 path: &Path,
517 command: &str,
518 args: &str,
519 zig_version: &semver::Version,
520 zig_command: &(PathBuf, Vec<String>),
521) -> Result<()> {
522 let mut buf = Vec::<u8>::new();
523 let current_exe = resolve_current_exe()?;
524 writeln!(&mut buf, "#!/bin/sh")?;
525
526 writeln!(
528 &mut buf,
529 "export CARGO_ZIGBUILD_ZIG_VERSION={}",
530 zig_version
531 )?;
532 writeln!(
535 &mut buf,
536 "export CARGO_ZIGBUILD_ZIG_COMMAND={}",
537 shell_words::quote(&zig_command.0.to_string_lossy())
538 )?;
539 if !zig_command.1.is_empty() {
540 writeln!(
541 &mut buf,
542 "export CARGO_ZIGBUILD_ZIG_COMMAND_ARGS={}",
543 shell_words::quote(&zig_command.1.join(" "))
544 )?;
545 }
546
547 writeln!(&mut buf, "if [ -n \"$SDKROOT\" ]; then export SDKROOT; fi")?;
549
550 writeln!(
551 &mut buf,
552 "exec \"{}\" zig {} -- {} \"$@\"",
553 current_exe.display(),
554 command,
555 args
556 )?;
557
558 let existing_content = fs::read(path).unwrap_or_default();
562 if existing_content != buf {
563 OpenOptions::new()
564 .create(true)
565 .write(true)
566 .truncate(true)
567 .mode(0o700)
568 .open(path)?
569 .write_all(&buf)?;
570 }
571 Ok(())
572}
573
574#[cfg(not(target_family = "unix"))]
576fn write_linker_wrapper(
577 path: &Path,
578 command: &str,
579 args: &str,
580 zig_version: &semver::Version,
581 zig_command: &(PathBuf, Vec<String>),
582) -> Result<()> {
583 let mut buf = Vec::<u8>::new();
584 let current_exe = resolve_current_exe()?;
585 let current_exe = if is_mingw_shell() {
586 current_exe.to_slash_lossy().to_string()
587 } else {
588 current_exe.display().to_string()
589 };
590 writeln!(&mut buf, "@echo off")?;
591 writeln!(&mut buf, "setlocal DisableDelayedExpansion")?;
593 writeln!(&mut buf, "set CARGO_ZIGBUILD_ZIG_VERSION={}", zig_version)?;
595 writeln!(
598 &mut buf,
599 "set \"CARGO_ZIGBUILD_ZIG_COMMAND={}\"",
600 zig_command.0.display()
601 )?;
602 if !zig_command.1.is_empty() {
603 writeln!(
604 &mut buf,
605 "set \"CARGO_ZIGBUILD_ZIG_COMMAND_ARGS={}\"",
606 zig_command.1.join(" ")
607 )?;
608 }
609 writeln!(
610 &mut buf,
611 "\"{}\" zig {} -- {} %*",
612 adjust_canonicalization(current_exe),
613 command,
614 args
615 )?;
616
617 let existing_content = fs::read(path).unwrap_or_default();
618 if existing_content != buf {
619 fs::write(path, buf)?;
620 }
621 Ok(())
622}
623
624pub(crate) fn is_mingw_shell() -> bool {
625 env::var_os("MSYSTEM").is_some() && env::var_os("SHELL").is_some()
626}
627
628#[cfg(target_os = "windows")]
630pub fn adjust_canonicalization(p: String) -> String {
631 const VERBATIM_PREFIX: &str = r#"\\?\"#;
632 if p.starts_with(VERBATIM_PREFIX) {
633 p[VERBATIM_PREFIX.len()..].to_string()
634 } else {
635 p
636 }
637}
638
639#[cfg(test)]
640mod tests {
641 use super::*;
642
643 #[test]
644 fn test_target_flags() {
645 let cases = [
646 ("-C target-feature=-crt-static", "", "-crt-static"),
648 ("-C target-cpu=native", "native", ""),
649 (
650 "--deny warnings --codegen target-feature=+crt-static",
651 "",
652 "+crt-static",
653 ),
654 ("-C target_cpu=skylake-avx512", "skylake-avx512", ""),
655 ("-Ctarget_cpu=x86-64-v3", "x86-64-v3", ""),
656 (
657 "-C target-cpu=native --cfg foo -C target-feature=-avx512bf16,-avx512bitalg",
658 "native",
659 "-avx512bf16,-avx512bitalg",
660 ),
661 (
662 "--target x86_64-unknown-linux-gnu --codegen=target-cpu=x --codegen=target-cpu=x86-64",
663 "x86-64",
664 "",
665 ),
666 (
667 "-Ctarget-feature=+crt-static -Ctarget-feature=+avx",
668 "",
669 "+crt-static,+avx",
670 ),
671 ];
672
673 for (input, expected_target_cpu, expected_target_feature) in cases.iter() {
674 let args = cargo_config2::Flags::from_space_separated(input);
675 let encoded_rust_flags = args.encode().unwrap();
676 let flags = TargetFlags::parse_from_encoded(OsStr::new(&encoded_rust_flags)).unwrap();
677 assert_eq!(flags.target_cpu, *expected_target_cpu, "{}", input);
678 assert_eq!(flags.target_feature, *expected_target_feature, "{}", input);
679 }
680 }
681
682 #[test]
683 fn test_join_args_for_script() {
684 let args = vec!["-target", "x86_64-linux-gnu"];
686 let result = join_args_for_script(&args);
687 assert!(result.contains("-target"));
688 assert!(result.contains("x86_64-linux-gnu"));
689 }
690
691 #[test]
692 #[cfg(not(target_family = "unix"))]
693 fn test_quote_for_batch() {
694 assert_eq!(quote_for_batch("-target"), "-target");
696 assert_eq!(quote_for_batch("x86_64-linux-gnu"), "x86_64-linux-gnu");
697
698 assert_eq!(
700 quote_for_batch("C:\\Users\\John Doe\\path"),
701 "\"C:\\Users\\John Doe\\path\""
702 );
703
704 assert_eq!(quote_for_batch(""), "\"\"");
706
707 assert_eq!(quote_for_batch("foo&bar"), "\"foo&bar\"");
709 assert_eq!(quote_for_batch("foo|bar"), "\"foo|bar\"");
710 assert_eq!(quote_for_batch("foo<bar"), "\"foo<bar\"");
711 assert_eq!(quote_for_batch("foo>bar"), "\"foo>bar\"");
712 assert_eq!(quote_for_batch("foo^bar"), "\"foo^bar\"");
713 assert_eq!(quote_for_batch("foo%bar"), "\"foo%bar\"");
714
715 assert_eq!(quote_for_batch("foo\"bar"), "\"foo\"\"bar\"");
717 }
718
719 #[test]
720 #[cfg(not(target_family = "unix"))]
721 fn test_join_args_for_script_windows() {
722 let args = vec![
724 "-target",
725 "x86_64-linux-gnu",
726 "-L",
727 "C:\\Users\\John Doe\\path",
728 ];
729 let result = join_args_for_script(&args);
730 assert!(result.contains("\"C:\\Users\\John Doe\\path\""));
732 assert!(result.contains("-target"));
734 assert!(!result.contains("\"-target\""));
735 }
736}