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};
9use std::process::{self, Command};
10use std::str;
11use std::sync::OnceLock;
12
13use anyhow::{Context, Result, anyhow, bail};
14use fs_err as fs;
15use path_slash::PathBufExt;
16use serde::Deserialize;
17use target_lexicon::{Architecture, Environment, OperatingSystem, Triple};
18
19use crate::linux::ARM_FEATURES_H;
20use crate::macos::{LIBCHARSET_TBD, LIBICONV_TBD};
21
22#[derive(Clone, Debug, clap::Subcommand)]
24pub enum Zig {
25 #[command(name = "cc")]
27 Cc {
28 #[arg(num_args = 1.., trailing_var_arg = true)]
30 args: Vec<String>,
31 },
32 #[command(name = "c++")]
34 Cxx {
35 #[arg(num_args = 1.., trailing_var_arg = true)]
37 args: Vec<String>,
38 },
39 #[command(name = "ar")]
41 Ar {
42 #[arg(num_args = 1.., trailing_var_arg = true)]
44 args: Vec<String>,
45 },
46 #[command(name = "ranlib")]
48 Ranlib {
49 #[arg(num_args = 1.., trailing_var_arg = true)]
51 args: Vec<String>,
52 },
53 #[command(name = "lib")]
55 Lib {
56 #[arg(num_args = 1.., trailing_var_arg = true)]
58 args: Vec<String>,
59 },
60 #[command(name = "dlltool")]
62 Dlltool {
63 #[arg(num_args = 1.., trailing_var_arg = true)]
65 args: Vec<String>,
66 },
67}
68
69struct TargetInfo {
70 target: Option<String>,
71}
72
73impl TargetInfo {
74 fn new(target: Option<&String>) -> Self {
75 Self {
76 target: target.cloned(),
77 }
78 }
79
80 fn is_arm(&self) -> bool {
82 self.target
83 .as_ref()
84 .map(|x| x.starts_with("arm"))
85 .unwrap_or_default()
86 }
87
88 fn is_aarch64(&self) -> bool {
89 self.target
90 .as_ref()
91 .map(|x| x.starts_with("aarch64"))
92 .unwrap_or_default()
93 }
94
95 fn is_aarch64_be(&self) -> bool {
96 self.target
97 .as_ref()
98 .map(|x| x.starts_with("aarch64_be"))
99 .unwrap_or_default()
100 }
101
102 fn is_i386(&self) -> bool {
103 self.target
104 .as_ref()
105 .map(|x| x.starts_with("i386"))
106 .unwrap_or_default()
107 }
108
109 fn is_i686(&self) -> bool {
110 self.target
111 .as_ref()
112 .map(|x| x.starts_with("i686") || x.starts_with("x86-"))
113 .unwrap_or_default()
114 }
115
116 fn is_riscv64(&self) -> bool {
117 self.target
118 .as_ref()
119 .map(|x| x.starts_with("riscv64"))
120 .unwrap_or_default()
121 }
122
123 fn is_riscv32(&self) -> bool {
124 self.target
125 .as_ref()
126 .map(|x| x.starts_with("riscv32"))
127 .unwrap_or_default()
128 }
129
130 fn is_mips32(&self) -> bool {
131 self.target
132 .as_ref()
133 .map(|x| x.starts_with("mips") && !x.starts_with("mips64"))
134 .unwrap_or_default()
135 }
136
137 fn is_musl(&self) -> bool {
139 self.target
140 .as_ref()
141 .map(|x| x.contains("musl"))
142 .unwrap_or_default()
143 }
144
145 fn is_macos(&self) -> bool {
147 self.target
148 .as_ref()
149 .map(|x| x.contains("macos") || x.contains("maccatalyst"))
150 .unwrap_or_default()
151 }
152
153 fn is_darwin(&self) -> bool {
154 self.target
155 .as_ref()
156 .map(|x| x.contains("darwin"))
157 .unwrap_or_default()
158 }
159
160 fn is_apple_platform(&self) -> bool {
161 self.target
162 .as_ref()
163 .map(|x| {
164 x.contains("macos")
165 || x.contains("darwin")
166 || x.contains("ios")
167 || x.contains("tvos")
168 || x.contains("watchos")
169 || x.contains("visionos")
170 || x.contains("maccatalyst")
171 })
172 .unwrap_or_default()
173 }
174
175 fn is_ios(&self) -> bool {
176 self.target
177 .as_ref()
178 .map(|x| x.contains("ios") && !x.contains("visionos"))
179 .unwrap_or_default()
180 }
181
182 fn is_tvos(&self) -> bool {
183 self.target
184 .as_ref()
185 .map(|x| x.contains("tvos"))
186 .unwrap_or_default()
187 }
188
189 fn is_watchos(&self) -> bool {
190 self.target
191 .as_ref()
192 .map(|x| x.contains("watchos"))
193 .unwrap_or_default()
194 }
195
196 fn is_visionos(&self) -> bool {
197 self.target
198 .as_ref()
199 .map(|x| x.contains("visionos"))
200 .unwrap_or_default()
201 }
202
203 fn apple_cpu(&self) -> &'static str {
205 if self.is_macos() || self.is_darwin() {
206 "apple_m1" } else if self.is_visionos() {
208 "apple_m2" } else if self.is_watchos() {
210 "apple_s5" } else if self.is_ios() || self.is_tvos() {
212 "apple_a14" } else {
214 "generic"
215 }
216 }
217
218 fn is_freebsd(&self) -> bool {
219 self.target
220 .as_ref()
221 .map(|x| x.contains("freebsd"))
222 .unwrap_or_default()
223 }
224
225 fn is_windows_gnu(&self) -> bool {
226 self.target
227 .as_ref()
228 .map(|x| x.contains("windows-gnu"))
229 .unwrap_or_default()
230 }
231
232 fn is_windows_msvc(&self) -> bool {
233 self.target
234 .as_ref()
235 .map(|x| x.contains("windows-msvc"))
236 .unwrap_or_default()
237 }
238
239 fn is_ohos(&self) -> bool {
240 self.target
241 .as_ref()
242 .map(|x| x.contains("ohos"))
243 .unwrap_or_default()
244 }
245}
246
247impl Zig {
248 pub fn execute(&self) -> Result<()> {
250 match self {
251 Zig::Cc { args } => self.execute_compiler("cc", args),
252 Zig::Cxx { args } => self.execute_compiler("c++", args),
253 Zig::Ar { args } => self.execute_tool("ar", args),
254 Zig::Ranlib { args } => self.execute_compiler("ranlib", args),
255 Zig::Lib { args } => self.execute_compiler("lib", args),
256 Zig::Dlltool { args } => self.execute_dlltool(args),
257 }
258 }
259
260 pub fn execute_dlltool(&self, cmd_args: &[String]) -> Result<()> {
263 let zig_version = Zig::zig_version()?;
264 let needs_filtering = zig_version.major == 0 && zig_version.minor < 12;
265
266 if !needs_filtering {
267 return self.execute_tool("dlltool", cmd_args);
268 }
269
270 let mut filtered_args = Vec::with_capacity(cmd_args.len());
273 let mut skip_next = false;
274 for arg in cmd_args {
275 if skip_next {
276 skip_next = false;
277 continue;
278 }
279 if arg == "--no-leading-underscore" {
280 continue;
281 }
282 if arg == "--temp-prefix" || arg == "-t" {
283 skip_next = true;
285 continue;
286 }
287 if arg.starts_with("--temp-prefix=") || arg.starts_with("-t=") {
289 continue;
290 }
291 filtered_args.push(arg.clone());
292 }
293
294 self.execute_tool("dlltool", &filtered_args)
295 }
296
297 pub fn execute_compiler(&self, cmd: &str, cmd_args: &[String]) -> Result<()> {
299 let target = cmd_args
300 .iter()
301 .position(|x| x == "-target")
302 .and_then(|index| cmd_args.get(index + 1));
303 let target_info = TargetInfo::new(target);
304
305 let rustc_ver = match env::var("CARGO_ZIGBUILD_RUSTC_VERSION") {
306 Ok(version) => version.parse()?,
307 Err(_) => rustc_version::version()?,
308 };
309 let zig_version = Zig::zig_version()?;
310
311 let mut new_cmd_args = Vec::with_capacity(cmd_args.len());
312 let mut skip_next_arg = false;
313 let mut seen_target = false;
314 for arg in cmd_args {
315 if skip_next_arg {
316 skip_next_arg = false;
317 continue;
318 }
319 if arg == "-target" {
323 if seen_target {
324 skip_next_arg = true;
325 continue;
326 }
327 seen_target = true;
328 }
329 let args = if arg.starts_with('@') && arg.ends_with("linker-arguments") {
330 vec![self.process_linker_response_file(
331 arg,
332 &rustc_ver,
333 &zig_version,
334 &target_info,
335 )?]
336 } else {
337 match self.filter_linker_arg(arg, &rustc_ver, &zig_version, &target_info) {
338 FilteredArg::Keep(filtered) => filtered,
339 FilteredArg::Skip => continue,
340 FilteredArg::SkipWithNext => {
341 skip_next_arg = true;
342 continue;
343 }
344 }
345 };
346 new_cmd_args.extend(args);
347 }
348
349 if target_info.is_mips32() {
350 new_cmd_args.push("-Wl,-z,notext".to_string());
352 }
353
354 if target_info.is_windows_gnu() && (zig_version.major, zig_version.minor) >= (0, 16) {
355 new_cmd_args.push("-lcompiler_rt".to_string());
356 }
357
358 if self.has_undefined_dynamic_lookup(cmd_args) {
359 new_cmd_args.push("-Wl,-undefined=dynamic_lookup".to_string());
360 }
361 if target_info.is_macos() {
362 if self.should_add_libcharset(cmd_args, &zig_version) {
363 new_cmd_args.push("-lcharset".to_string());
364 }
365 self.add_macos_specific_args(&mut new_cmd_args, &zig_version)?;
366 }
367
368 let mut command = Self::command()?;
371 if (zig_version.major, zig_version.minor) >= (0, 15)
372 && let Some(sdkroot) = Self::macos_sdk_root()
373 {
374 command.env("SDKROOT", sdkroot);
375 }
376
377 let mut child = command
378 .arg(cmd)
379 .args(new_cmd_args)
380 .spawn()
381 .with_context(|| format!("Failed to run `zig {cmd}`"))?;
382 let status = child.wait().expect("Failed to wait on zig child process");
383 if !status.success() {
384 process::exit(status.code().unwrap_or(1));
385 }
386 Ok(())
387 }
388
389 fn process_linker_response_file(
390 &self,
391 arg: &str,
392 rustc_ver: &rustc_version::Version,
393 zig_version: &semver::Version,
394 target_info: &TargetInfo,
395 ) -> Result<String> {
396 let content_bytes = fs::read(arg.trim_start_matches('@'))?;
400 let content = if target_info.is_windows_msvc() {
401 if content_bytes[0..2] != [255, 254] {
402 bail!(
403 "linker response file `{}` didn't start with a utf16 BOM",
404 &arg
405 );
406 }
407 let content_utf16: Vec<u16> = content_bytes[2..]
408 .chunks_exact(2)
409 .map(|a| u16::from_ne_bytes([a[0], a[1]]))
410 .collect();
411 String::from_utf16(&content_utf16).with_context(|| {
412 format!(
413 "linker response file `{}` didn't contain valid utf16 content",
414 &arg
415 )
416 })?
417 } else {
418 String::from_utf8(content_bytes).with_context(|| {
419 format!(
420 "linker response file `{}` didn't contain valid utf8 content",
421 &arg
422 )
423 })?
424 };
425 let mut link_args: Vec<_> = filter_linker_args(
426 content.split('\n').map(|s| s.to_string()),
427 rustc_ver,
428 zig_version,
429 target_info,
430 );
431 if self.has_undefined_dynamic_lookup(&link_args) {
432 link_args.push("-Wl,-undefined=dynamic_lookup".to_string());
433 }
434 if target_info.is_macos() && self.should_add_libcharset(&link_args, zig_version) {
435 link_args.push("-lcharset".to_string());
436 }
437 if target_info.is_windows_msvc() {
438 let new_content = link_args.join("\n");
439 let mut out = Vec::with_capacity((1 + new_content.len()) * 2);
440 for c in std::iter::once(0xFEFF).chain(new_content.encode_utf16()) {
442 out.push(c as u8);
444 out.push((c >> 8) as u8);
445 }
446 fs::write(arg.trim_start_matches('@'), out)?;
447 } else {
448 fs::write(arg.trim_start_matches('@'), link_args.join("\n").as_bytes())?;
449 }
450 Ok(arg.to_string())
451 }
452
453 fn filter_linker_arg(
454 &self,
455 arg: &str,
456 rustc_ver: &rustc_version::Version,
457 zig_version: &semver::Version,
458 target_info: &TargetInfo,
459 ) -> FilteredArg {
460 filter_linker_arg(arg, rustc_ver, zig_version, target_info)
461 }
462}
463
464enum FilteredArg {
465 Keep(Vec<String>),
466 Skip,
467 SkipWithNext,
468}
469
470fn filter_linker_args(
471 args: impl IntoIterator<Item = String>,
472 rustc_ver: &rustc_version::Version,
473 zig_version: &semver::Version,
474 target_info: &TargetInfo,
475) -> Vec<String> {
476 let mut result = Vec::new();
477 let mut skip_next = false;
478 for arg in args {
479 if skip_next {
480 skip_next = false;
481 continue;
482 }
483 match filter_linker_arg(&arg, rustc_ver, zig_version, target_info) {
484 FilteredArg::Keep(filtered) => result.extend(filtered),
485 FilteredArg::Skip => {}
486 FilteredArg::SkipWithNext => {
487 skip_next = true;
488 }
489 }
490 }
491 result
492}
493
494fn filter_linker_arg(
495 arg: &str,
496 rustc_ver: &rustc_version::Version,
497 zig_version: &semver::Version,
498 target_info: &TargetInfo,
499) -> FilteredArg {
500 if arg == "-lgcc_s" {
501 return FilteredArg::Keep(vec!["-lunwind".to_string()]);
502 } else if arg.starts_with("--target=") {
503 return FilteredArg::Skip;
504 } else if arg.starts_with("-e") && arg.len() > 2 && !arg.starts_with("-export") {
505 let entry = &arg[2..];
506 return FilteredArg::Keep(vec![format!("-Wl,--entry={}", entry)]);
507 }
508 if (target_info.is_arm() || target_info.is_windows_gnu())
509 && arg.ends_with(".rlib")
510 && arg.contains("libcompiler_builtins-")
511 {
512 return FilteredArg::Skip;
513 }
514 if target_info.is_windows_gnu() {
515 #[allow(clippy::if_same_then_else)]
516 if arg == "-lgcc_eh"
517 && ((zig_version.major, zig_version.minor) < (0, 14) || target_info.is_i686())
518 {
519 return FilteredArg::Keep(vec!["-lc++".to_string()]);
520 } else if arg.ends_with("rsbegin.o") || arg.ends_with("rsend.o") {
521 if target_info.is_i686() {
522 return FilteredArg::Skip;
523 }
524 } else if arg == "-Wl,-Bdynamic" && (zig_version.major, zig_version.minor) >= (0, 11) {
525 return FilteredArg::Keep(vec!["-Wl,-search_paths_first".to_owned()]);
526 } else if arg == "-lwindows" || arg == "-l:libpthread.a" || arg == "-lgcc" {
527 return FilteredArg::Skip;
528 } else if arg == "-Wl,--disable-auto-image-base"
529 || arg == "-Wl,--dynamicbase"
530 || arg == "-Wl,--large-address-aware"
531 || (arg.starts_with("-Wl,")
532 && (arg.ends_with("/list.def") || arg.ends_with("\\list.def")))
533 {
534 return FilteredArg::Skip;
535 } else if arg == "-lmsvcrt" {
536 return FilteredArg::Skip;
537 }
538 } else if arg == "-Wl,--no-undefined-version"
539 || arg == "-Wl,-znostart-stop-gc"
540 || arg == "-Wl,--fix-cortex-a53-843419"
542 || arg.starts_with("-Wl,-plugin-opt")
543 {
544 return FilteredArg::Skip;
545 }
546 if target_info.is_musl() || target_info.is_ohos() {
547 if (arg.ends_with(".o") && arg.contains("self-contained") && arg.contains("crt"))
548 || arg == "-Wl,-melf_i386"
549 {
550 return FilteredArg::Skip;
551 }
552 if rustc_ver.major == 1
553 && rustc_ver.minor < 59
554 && arg.ends_with(".rlib")
555 && arg.contains("liblibc-")
556 {
557 return FilteredArg::Skip;
558 }
559 if arg == "-lc" {
560 return FilteredArg::Skip;
561 }
562 }
563 if arg.starts_with("-Wp,")
567 && !arg.starts_with("-Wp,-MD")
568 && !arg.starts_with("-Wp,-MMD")
569 && !arg.starts_with("-Wp,-MT")
570 {
571 return FilteredArg::Skip;
572 }
573 if arg.starts_with("-march=") {
574 if target_info.is_arm() || target_info.is_i386() {
575 return FilteredArg::Skip;
576 } else if target_info.is_riscv64() {
577 return FilteredArg::Keep(vec!["-march=generic_rv64".to_string()]);
578 } else if target_info.is_riscv32() {
579 return FilteredArg::Keep(vec!["-march=generic_rv32".to_string()]);
580 } else if arg.starts_with("-march=armv")
581 && (target_info.is_aarch64() || target_info.is_aarch64_be())
582 {
583 let march_value = arg.strip_prefix("-march=").unwrap();
584 let features = if let Some(pos) = march_value.find('+') {
585 &march_value[pos..]
586 } else {
587 ""
588 };
589 let base_cpu = if target_info.is_apple_platform() {
590 target_info.apple_cpu()
591 } else {
592 "generic"
593 };
594 let mut result = vec![format!("-mcpu={}{}", base_cpu, features)];
595 if features.contains("+crypto") {
596 result.append(&mut vec!["-Xassembler".to_owned(), arg.to_string()]);
597 }
598 return FilteredArg::Keep(result);
599 }
600 }
601 if target_info.is_apple_platform() {
602 if (zig_version.major, zig_version.minor) < (0, 16) {
603 if arg.starts_with("-Wl,-exported_symbols_list,") {
604 return FilteredArg::Skip;
605 }
606 if arg == "-Wl,-exported_symbols_list" {
607 return FilteredArg::SkipWithNext;
608 }
609 }
610 if arg == "-Wl,-dylib" {
611 return FilteredArg::Skip;
612 }
613 }
614 if (zig_version.major, zig_version.minor) < (0, 16) {
616 if arg == "-Wl,-exported_symbols_list" || arg == "-Wl,--dynamic-list" {
617 return FilteredArg::SkipWithNext;
618 }
619 if arg.starts_with("-Wl,-exported_symbols_list,") || arg.starts_with("-Wl,--dynamic-list,")
620 {
621 return FilteredArg::Skip;
622 }
623 }
624 if target_info.is_freebsd() {
625 let ignored_libs = ["-lkvm", "-lmemstat", "-lprocstat", "-ldevstat"];
626 if ignored_libs.contains(&arg) {
627 return FilteredArg::Skip;
628 }
629 }
630 FilteredArg::Keep(vec![arg.to_string()])
631}
632
633impl Zig {
634 fn has_undefined_dynamic_lookup(&self, args: &[String]) -> bool {
635 let undefined = args
636 .iter()
637 .position(|x| x == "-undefined")
638 .and_then(|i| args.get(i + 1));
639 matches!(undefined, Some(x) if x == "dynamic_lookup")
640 }
641
642 fn should_add_libcharset(&self, args: &[String], zig_version: &semver::Version) -> bool {
643 if (zig_version.major, zig_version.minor) >= (0, 12) {
645 args.iter().any(|x| x == "-liconv") && !args.iter().any(|x| x == "-lcharset")
646 } else {
647 false
648 }
649 }
650
651 fn add_macos_specific_args(
652 &self,
653 new_cmd_args: &mut Vec<String>,
654 zig_version: &semver::Version,
655 ) -> Result<()> {
656 let sdkroot = Self::macos_sdk_root();
657 if (zig_version.major, zig_version.minor) >= (0, 12) {
658 if let Some(ref sdkroot) = sdkroot
662 && (zig_version.major, zig_version.minor) < (0, 15)
663 {
664 new_cmd_args.push(format!("--sysroot={}", sdkroot.display()));
665 }
666 }
668 if let Some(ref sdkroot) = sdkroot {
669 if (zig_version.major, zig_version.minor) < (0, 15) {
670 new_cmd_args.extend_from_slice(&[
672 "-isystem".to_string(),
673 format!("{}", sdkroot.join("usr").join("include").display()),
674 format!("-L{}", sdkroot.join("usr").join("lib").display()),
675 format!(
676 "-F{}",
677 sdkroot
678 .join("System")
679 .join("Library")
680 .join("Frameworks")
681 .display()
682 ),
683 "-DTARGET_OS_IPHONE=0".to_string(),
684 ]);
685 } else {
686 new_cmd_args.extend_from_slice(&[
689 "-isystem".to_string(),
690 format!("{}", sdkroot.join("usr").join("include").display()),
691 format!("-L{}", sdkroot.join("usr").join("lib").display()),
692 format!(
693 "-F{}",
694 sdkroot
695 .join("System")
696 .join("Library")
697 .join("Frameworks")
698 .display()
699 ),
700 "-iframework".to_string(),
702 format!(
703 "{}",
704 sdkroot
705 .join("System")
706 .join("Library")
707 .join("Frameworks")
708 .display()
709 ),
710 "-DTARGET_OS_IPHONE=0".to_string(),
711 ]);
712 }
713 }
714
715 let cache_dir = cache_dir();
717 let deps_dir = cache_dir.join("deps");
718 fs::create_dir_all(&deps_dir)?;
719 write_tbd_files(&deps_dir)?;
720 new_cmd_args.push("-L".to_string());
721 new_cmd_args.push(format!("{}", deps_dir.display()));
722 Ok(())
723 }
724
725 pub fn execute_tool(&self, cmd: &str, cmd_args: &[String]) -> Result<()> {
727 let mut child = Self::command()?
728 .arg(cmd)
729 .args(cmd_args)
730 .spawn()
731 .with_context(|| format!("Failed to run `zig {cmd}`"))?;
732 let status = child.wait().expect("Failed to wait on zig child process");
733 if !status.success() {
734 process::exit(status.code().unwrap_or(1));
735 }
736 Ok(())
737 }
738
739 pub fn command() -> Result<Command> {
741 let (zig, zig_args) = Self::find_zig()?;
742 let mut cmd = Command::new(zig);
743 cmd.args(zig_args);
744 Ok(cmd)
745 }
746
747 fn zig_version() -> Result<semver::Version> {
748 static ZIG_VERSION: OnceLock<semver::Version> = OnceLock::new();
749
750 if let Some(version) = ZIG_VERSION.get() {
751 return Ok(version.clone());
752 }
753 if let Ok(version_str) = env::var("CARGO_ZIGBUILD_ZIG_VERSION")
755 && let Ok(version) = semver::Version::parse(&version_str)
756 {
757 return Ok(ZIG_VERSION.get_or_init(|| version).clone());
758 }
759 let output = Self::command()?.arg("version").output()?;
760 let version_str =
761 str::from_utf8(&output.stdout).context("`zig version` didn't return utf8 output")?;
762 let version = semver::Version::parse(version_str.trim())?;
763 Ok(ZIG_VERSION.get_or_init(|| version).clone())
764 }
765
766 pub fn find_zig() -> Result<(PathBuf, Vec<String>)> {
768 static ZIG_PATH: OnceLock<(PathBuf, Vec<String>)> = OnceLock::new();
769
770 if let Some(cached) = ZIG_PATH.get() {
771 return Ok(cached.clone());
772 }
773 let result = Self::find_zig_python()
774 .or_else(|_| Self::find_zig_bin())
775 .context("Failed to find zig")?;
776 Ok(ZIG_PATH.get_or_init(|| result).clone())
777 }
778
779 fn find_zig_bin() -> Result<(PathBuf, Vec<String>)> {
781 let zig_path = zig_path()?;
782 let output = Command::new(&zig_path).arg("version").output()?;
783
784 let version_str = str::from_utf8(&output.stdout).with_context(|| {
785 format!("`{} version` didn't return utf8 output", zig_path.display())
786 })?;
787 Self::validate_zig_version(version_str)?;
788 Ok((zig_path, Vec::new()))
789 }
790
791 fn find_zig_python() -> Result<(PathBuf, Vec<String>)> {
793 let python_path = python_path()?;
794 let output = Command::new(&python_path)
795 .args(["-m", "ziglang", "version"])
796 .output()?;
797
798 let version_str = str::from_utf8(&output.stdout).with_context(|| {
799 format!(
800 "`{} -m ziglang version` didn't return utf8 output",
801 python_path.display()
802 )
803 })?;
804 Self::validate_zig_version(version_str)?;
805 Ok((python_path, vec!["-m".to_string(), "ziglang".to_string()]))
806 }
807
808 fn validate_zig_version(version: &str) -> Result<()> {
809 let min_ver = semver::Version::new(0, 9, 0);
810 let version = semver::Version::parse(version.trim())?;
811 if version >= min_ver {
812 Ok(())
813 } else {
814 bail!(
815 "zig version {} is too old, need at least {}",
816 version,
817 min_ver
818 )
819 }
820 }
821
822 pub fn lib_dir() -> Result<PathBuf> {
824 static LIB_DIR: OnceLock<PathBuf> = OnceLock::new();
825
826 if let Some(cached) = LIB_DIR.get() {
827 return Ok(cached.clone());
828 }
829 let (zig, zig_args) = Self::find_zig()?;
830 let zig_version = Self::zig_version()?;
831 let output = Command::new(zig).args(zig_args).arg("env").output()?;
832 let parse_zon_lib_dir = || -> Result<PathBuf> {
833 let output_str =
834 str::from_utf8(&output.stdout).context("`zig env` didn't return utf8 output")?;
835 let lib_dir = output_str
836 .find(".lib_dir")
837 .and_then(|idx| {
838 let bytes = output_str.as_bytes();
839 let mut start = idx;
840 while start < bytes.len() && bytes[start] != b'"' {
841 start += 1;
842 }
843 if start >= bytes.len() {
844 return None;
845 }
846 let mut end = start + 1;
847 while end < bytes.len() && bytes[end] != b'"' {
848 end += 1;
849 }
850 if end >= bytes.len() {
851 return None;
852 }
853 Some(&output_str[start + 1..end])
854 })
855 .context("Failed to parse lib_dir from `zig env` ZON output")?;
856 Ok(PathBuf::from(lib_dir))
857 };
858 let lib_dir = if zig_version >= semver::Version::new(0, 15, 0) {
859 parse_zon_lib_dir()?
860 } else {
861 serde_json::from_slice::<ZigEnv>(&output.stdout)
862 .map(|zig_env| PathBuf::from(zig_env.lib_dir))
863 .or_else(|_| parse_zon_lib_dir())?
864 };
865 Ok(LIB_DIR.get_or_init(|| lib_dir).clone())
866 }
867
868 fn add_env_if_missing<K, V>(command: &mut Command, name: K, value: V)
869 where
870 K: AsRef<OsStr>,
871 V: AsRef<OsStr>,
872 {
873 let command_env_contains_no_key =
874 |name: &K| !command.get_envs().any(|(key, _)| name.as_ref() == key);
875
876 if command_env_contains_no_key(&name) && env::var_os(&name).is_none() {
877 command.env(name, value);
878 }
879 }
880
881 pub(crate) fn apply_command_env(
882 manifest_path: Option<&Path>,
883 release: bool,
884 cargo: &cargo_options::CommonOptions,
885 cmd: &mut Command,
886 enable_zig_ar: bool,
887 ) -> Result<()> {
888 let cargo_config = cargo_config2::Config::load()?;
890 let config_targets;
892 let raw_targets: &[String] = if cargo.target.is_empty() {
893 if let Some(targets) = &cargo_config.build.target {
894 config_targets = targets
895 .iter()
896 .map(|t| t.triple().to_string())
897 .collect::<Vec<_>>();
898 &config_targets
899 } else {
900 &cargo.target
901 }
902 } else {
903 &cargo.target
904 };
905 #[cfg(target_os = "macos")]
906 if !raw_targets.is_empty()
907 && let Err(err) = crate::macos::rlimit::raise_nofile_limit()
908 {
909 eprintln!(
910 "warning: failed to raise the open file limit: {err}; large builds may fail with ProcessFdQuotaExceeded (try `ulimit -n 65536`)"
911 );
912 }
913 let rust_targets = raw_targets
914 .iter()
915 .map(|target| target.split_once('.').map(|(t, _)| t).unwrap_or(target))
916 .collect::<Vec<&str>>();
917 let rustc_meta = rustc_version::version_meta()?;
918 Self::add_env_if_missing(
919 cmd,
920 "CARGO_ZIGBUILD_RUSTC_VERSION",
921 rustc_meta.semver.to_string(),
922 );
923 let host_target = &rustc_meta.host;
924 for (parsed_target, raw_target) in rust_targets.iter().zip(raw_targets) {
925 let env_target = parsed_target.replace('-', "_");
926 let zig_wrapper = prepare_zig_linker(raw_target, &cargo_config)?;
927
928 if is_mingw_shell() {
929 let zig_cc = zig_wrapper.cc.to_slash_lossy();
930 let zig_cxx = zig_wrapper.cxx.to_slash_lossy();
931 Self::add_env_if_missing(cmd, format!("CC_{env_target}"), &*zig_cc);
932 Self::add_env_if_missing(cmd, format!("CXX_{env_target}"), &*zig_cxx);
933 if !parsed_target.contains("wasm") {
934 Self::add_env_if_missing(
935 cmd,
936 format!("CARGO_TARGET_{}_LINKER", env_target.to_uppercase()),
937 &*zig_cc,
938 );
939 }
940 } else {
941 Self::add_env_if_missing(cmd, format!("CC_{env_target}"), &zig_wrapper.cc);
942 Self::add_env_if_missing(cmd, format!("CXX_{env_target}"), &zig_wrapper.cxx);
943 if !parsed_target.contains("wasm") {
944 Self::add_env_if_missing(
945 cmd,
946 format!("CARGO_TARGET_{}_LINKER", env_target.to_uppercase()),
947 &zig_wrapper.cc,
948 );
949 }
950 }
951
952 Self::add_env_if_missing(cmd, format!("RANLIB_{env_target}"), &zig_wrapper.ranlib);
953 if enable_zig_ar {
956 if parsed_target.contains("msvc") {
957 Self::add_env_if_missing(cmd, format!("AR_{env_target}"), &zig_wrapper.lib);
958 } else {
959 Self::add_env_if_missing(cmd, format!("AR_{env_target}"), &zig_wrapper.ar);
960 }
961 }
962
963 Self::setup_os_deps(manifest_path, release, cargo)?;
964
965 let cmake_toolchain_file_env = format!("CMAKE_TOOLCHAIN_FILE_{env_target}");
966 if env::var_os(&cmake_toolchain_file_env).is_none()
967 && env::var_os(format!("CMAKE_TOOLCHAIN_FILE_{parsed_target}")).is_none()
968 && env::var_os("TARGET_CMAKE_TOOLCHAIN_FILE").is_none()
969 && env::var_os("CMAKE_TOOLCHAIN_FILE").is_none()
970 && let Ok(cmake_toolchain_file) =
971 Self::setup_cmake_toolchain(parsed_target, &zig_wrapper, enable_zig_ar)
972 {
973 cmd.env(cmake_toolchain_file_env, cmake_toolchain_file);
974 }
975
976 if cfg!(target_os = "windows")
981 && env::var_os("CMAKE_GENERATOR").is_none()
982 && which::which("ninja").is_ok()
983 {
984 cmd.env("CMAKE_GENERATOR", "Ninja");
985 }
986
987 if raw_target.contains("windows-gnu") {
988 cmd.env("WINAPI_NO_BUNDLED_LIBRARIES", "1");
989 let triple: Triple = parsed_target.parse().unwrap_or_else(|_| Triple::unknown());
993 if !has_system_dlltool(&triple.architecture) {
994 let wrapper_dir = zig_wrapper.ar.parent().unwrap();
996 let existing_path = env::var_os("PATH").unwrap_or_default();
997 let paths = std::iter::once(wrapper_dir.to_path_buf())
998 .chain(env::split_paths(&existing_path));
999 if let Ok(new_path) = env::join_paths(paths) {
1000 cmd.env("PATH", new_path);
1001 }
1002 }
1003 }
1004
1005 if raw_target.contains("apple-darwin")
1006 && let Some(sdkroot) = Self::macos_sdk_root()
1007 && env::var_os("PKG_CONFIG_SYSROOT_DIR").is_none()
1008 {
1009 cmd.env("PKG_CONFIG_SYSROOT_DIR", sdkroot);
1011 }
1012
1013 if host_target == parsed_target {
1016 if !matches!(rustc_meta.channel, rustc_version::Channel::Nightly) {
1017 cmd.env("__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS", "nightly");
1020 }
1021 cmd.env("CARGO_UNSTABLE_TARGET_APPLIES_TO_HOST", "true");
1022 cmd.env("CARGO_TARGET_APPLIES_TO_HOST", "false");
1023 }
1024
1025 let mut options = Self::collect_zig_cc_options(&zig_wrapper, raw_target)
1027 .context("Failed to collect `zig cc` options")?;
1028 if raw_target.contains("apple-darwin") {
1029 options.push("-DTARGET_OS_IPHONE=0".to_string());
1031 }
1032 let escaped_options = shell_words::join(options.iter().map(|s| &s[..]));
1033 let bindgen_env = "BINDGEN_EXTRA_CLANG_ARGS";
1034 let fallback_value = env::var(bindgen_env);
1035 for target in [&env_target[..], parsed_target] {
1036 let name = format!("{bindgen_env}_{target}");
1037 if let Ok(mut value) = env::var(&name).or(fallback_value.clone()) {
1038 if shell_words::split(&value).is_err() {
1039 value = shell_words::quote(&value).into_owned();
1041 }
1042 if !value.is_empty() {
1043 value.push(' ');
1044 }
1045 value.push_str(&escaped_options);
1046 unsafe { env::set_var(name, value) };
1047 } else {
1048 unsafe { env::set_var(name, escaped_options.clone()) };
1049 }
1050 }
1051 }
1052 Ok(())
1053 }
1054
1055 fn collect_zig_cc_options(zig_wrapper: &ZigWrapper, raw_target: &str) -> Result<Vec<String>> {
1059 #[derive(Debug, PartialEq, Eq)]
1060 enum Kind {
1061 Normal,
1062 Framework,
1063 }
1064
1065 #[derive(Debug)]
1066 struct PerLanguageOptions {
1067 glibc_minor_ver: Option<u32>,
1068 include_paths: Vec<(Kind, String)>,
1069 }
1070
1071 fn collect_per_language_options(
1072 program: &Path,
1073 ext: &str,
1074 raw_target: &str,
1075 ) -> Result<PerLanguageOptions> {
1076 let empty_file_path = cache_dir().join(format!(".intentionally-empty-file.{ext}"));
1078 if !empty_file_path.exists() {
1079 fs::write(&empty_file_path, "")?;
1080 }
1081
1082 let output = Command::new(program)
1083 .arg("-E")
1084 .arg(&empty_file_path)
1085 .arg("-v")
1086 .output()?;
1087 let stderr = String::from_utf8(output.stderr)?;
1089 if !output.status.success() {
1090 bail!(
1091 "Failed to run `zig cc -v` with status {}: {}",
1092 output.status,
1093 stderr.trim(),
1094 );
1095 }
1096
1097 let glibc_minor_ver = if let Some(start) = stderr.find("__GLIBC_MINOR__=") {
1101 let stderr = &stderr[start + 16..];
1102 let end = stderr
1103 .find(|c: char| !c.is_ascii_digit())
1104 .unwrap_or(stderr.len());
1105 stderr[..end].parse().ok()
1106 } else {
1107 None
1108 };
1109
1110 let start = stderr
1111 .find("#include <...> search starts here:")
1112 .ok_or_else(|| anyhow!("Failed to parse `zig cc -v` output"))?
1113 + 34;
1114 let end = stderr
1115 .find("End of search list.")
1116 .ok_or_else(|| anyhow!("Failed to parse `zig cc -v` output"))?;
1117
1118 let mut include_paths = Vec::new();
1119 for mut line in stderr[start..end].lines() {
1120 line = line.trim();
1121 let mut kind = Kind::Normal;
1122 if line.ends_with(" (framework directory)") {
1123 line = line[..line.len() - 22].trim();
1124 kind = Kind::Framework;
1125 } else if line.ends_with(" (headermap)") {
1126 bail!("C/C++ search path includes header maps, which are not supported");
1127 }
1128 if !line.is_empty() {
1129 include_paths.push((kind, line.to_owned()));
1130 }
1131 }
1132
1133 if raw_target.contains("ohos") {
1135 let ndk = env::var("OHOS_NDK_HOME").expect("Can't get NDK path");
1136 include_paths.push((Kind::Normal, format!("{}/native/sysroot/usr/include", ndk)));
1137 }
1138
1139 Ok(PerLanguageOptions {
1140 include_paths,
1141 glibc_minor_ver,
1142 })
1143 }
1144
1145 let c_opts = collect_per_language_options(&zig_wrapper.cc, "c", raw_target)?;
1146 let cpp_opts = collect_per_language_options(&zig_wrapper.cxx, "cpp", raw_target)?;
1147
1148 if c_opts.glibc_minor_ver != cpp_opts.glibc_minor_ver {
1150 bail!(
1151 "`zig cc` gives a different glibc minor version for C ({:?}) and C++ ({:?})",
1152 c_opts.glibc_minor_ver,
1153 cpp_opts.glibc_minor_ver,
1154 );
1155 }
1156 let c_paths = c_opts.include_paths;
1157 let mut cpp_paths = cpp_opts.include_paths;
1158 let cpp_pre_len = cpp_paths
1159 .iter()
1160 .position(|p| {
1161 p == c_paths
1162 .iter()
1163 .find(|(kind, _)| *kind == Kind::Normal)
1164 .unwrap()
1165 })
1166 .unwrap_or_default();
1167 let cpp_post_len = cpp_paths.len()
1168 - cpp_paths
1169 .iter()
1170 .position(|p| p == c_paths.last().unwrap())
1171 .unwrap_or_default()
1172 - 1;
1173
1174 let mut args = Vec::new();
1227
1228 args.push("-nostdinc".to_owned());
1231
1232 if raw_target.contains("musl") || raw_target.contains("ohos") {
1240 args.push("-D_LIBCPP_HAS_MUSL_LIBC".to_owned());
1241 args.push("-D_LARGEFILE64_SOURCE".to_owned());
1244 }
1245 args.extend(
1246 [
1247 "-D_LIBCPP_DISABLE_VISIBILITY_ANNOTATIONS",
1248 "-D_LIBCPP_HAS_NO_VENDOR_AVAILABILITY_ANNOTATIONS",
1249 "-D_LIBCXXABI_DISABLE_VISIBILITY_ANNOTATIONS",
1250 "-D_LIBCPP_PSTL_CPU_BACKEND_SERIAL",
1251 "-D_LIBCPP_ABI_VERSION=1",
1252 "-D_LIBCPP_ABI_NAMESPACE=__1",
1253 "-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST",
1254 "-D_LIBCPP_HAS_LOCALIZATION=1",
1256 "-D_LIBCPP_HAS_WIDE_CHARACTERS=1",
1257 "-D_LIBCPP_HAS_UNICODE=1",
1258 "-D_LIBCPP_HAS_THREADS=1",
1259 "-D_LIBCPP_HAS_MONOTONIC_CLOCK",
1260 ]
1261 .into_iter()
1262 .map(ToString::to_string),
1263 );
1264 if let Some(ver) = c_opts.glibc_minor_ver {
1265 args.push(format!("-D__GLIBC_MINOR__={ver}"));
1267 }
1268
1269 for (kind, path) in cpp_paths.drain(..cpp_pre_len) {
1270 if kind != Kind::Normal {
1271 continue;
1273 }
1274 args.push("-cxx-isystem".to_owned());
1280 args.push(path);
1281 }
1282
1283 for (kind, path) in c_paths {
1284 match kind {
1285 Kind::Normal => {
1286 args.push("-Xclang".to_owned());
1288 args.push("-c-isystem".to_owned());
1289 args.push("-Xclang".to_owned());
1290 args.push(path.clone());
1291 args.push("-cxx-isystem".to_owned());
1292 args.push(path);
1293 }
1294 Kind::Framework => {
1295 args.push("-iframework".to_owned());
1296 args.push(path);
1297 }
1298 }
1299 }
1300
1301 for (kind, path) in cpp_paths.drain(cpp_paths.len() - cpp_post_len..) {
1302 assert!(kind == Kind::Normal);
1303 args.push("-cxx-isystem".to_owned());
1304 args.push(path);
1305 }
1306
1307 Ok(args)
1308 }
1309
1310 fn setup_os_deps(
1311 manifest_path: Option<&Path>,
1312 release: bool,
1313 cargo: &cargo_options::CommonOptions,
1314 ) -> Result<()> {
1315 for target in &cargo.target {
1316 if target.contains("apple") {
1317 let target_dir = if let Some(target_dir) = cargo.target_dir.clone() {
1318 target_dir.join(target)
1319 } else {
1320 let manifest_path = manifest_path.unwrap_or_else(|| Path::new("Cargo.toml"));
1321 if !manifest_path.exists() {
1322 continue;
1324 }
1325 let metadata = cargo_metadata::MetadataCommand::new()
1326 .manifest_path(manifest_path)
1327 .no_deps()
1328 .exec()?;
1329 metadata.target_directory.into_std_path_buf().join(target)
1330 };
1331 let profile = match cargo.profile.as_deref() {
1332 Some("dev" | "test") => "debug",
1333 Some("release" | "bench") => "release",
1334 Some(profile) => profile,
1335 None => {
1336 if release {
1337 "release"
1338 } else {
1339 "debug"
1340 }
1341 }
1342 };
1343 let deps_dir = target_dir.join(profile).join("deps");
1344 fs::create_dir_all(&deps_dir)?;
1345 if !target_dir.join("CACHEDIR.TAG").is_file() {
1346 let _ = write_file(
1348 &target_dir.join("CACHEDIR.TAG"),
1349 "Signature: 8a477f597d28d172789f06886806bc55
1350# This file is a cache directory tag created by cargo.
1351# For information about cache directory tags see https://bford.info/cachedir/
1352",
1353 );
1354 }
1355 write_tbd_files(&deps_dir)?;
1356 } else if target.contains("arm") && target.contains("linux") {
1357 if let Ok(lib_dir) = Zig::lib_dir() {
1359 let arm_features_h = lib_dir
1360 .join("libc")
1361 .join("glibc")
1362 .join("sysdeps")
1363 .join("arm")
1364 .join("arm-features.h");
1365 if !arm_features_h.is_file() {
1366 fs::write(arm_features_h, ARM_FEATURES_H)?;
1367 }
1368 }
1369 } else if target.contains("windows-gnu")
1370 && let Ok(lib_dir) = Zig::lib_dir()
1371 {
1372 let lib_common = lib_dir.join("libc").join("mingw").join("lib-common");
1373 let synchronization_def = lib_common.join("synchronization.def");
1374 if !synchronization_def.is_file() {
1375 let api_ms_win_core_synch_l1_2_0_def =
1376 lib_common.join("api-ms-win-core-synch-l1-2-0.def");
1377 fs::copy(api_ms_win_core_synch_l1_2_0_def, synchronization_def).ok();
1379 }
1380 }
1381 }
1382 Ok(())
1383 }
1384
1385 fn setup_cmake_toolchain(
1386 target: &str,
1387 zig_wrapper: &ZigWrapper,
1388 enable_zig_ar: bool,
1389 ) -> Result<PathBuf> {
1390 let wrapper_dir = zig_wrapper.cc.parent().unwrap();
1393 let cmake = wrapper_dir.join("cmake");
1394 fs::create_dir_all(&cmake)?;
1395
1396 let toolchain_file = cmake.join(format!("{target}-toolchain.cmake"));
1397 let triple: Triple = target.parse()?;
1398 let os = triple.operating_system.to_string();
1399 let arch = triple.architecture.to_string();
1400 let (system_name, system_processor) = match (os.as_str(), arch.as_str()) {
1401 ("darwin", "x86_64") => ("Darwin", "x86_64"),
1402 ("darwin", "aarch64") => ("Darwin", "arm64"),
1403 ("linux", arch) => {
1404 let cmake_arch = match arch {
1405 "powerpc" => "ppc",
1406 "powerpc64" => "ppc64",
1407 "powerpc64le" => "ppc64le",
1408 _ => arch,
1409 };
1410 ("Linux", cmake_arch)
1411 }
1412 ("windows", "x86_64") => ("Windows", "AMD64"),
1413 ("windows", "i686") => ("Windows", "X86"),
1414 ("windows", "aarch64") => ("Windows", "ARM64"),
1415 (os, arch) => (os, arch),
1416 };
1417 let mut content = format!(
1418 r#"
1419set(CMAKE_SYSTEM_NAME {system_name})
1420set(CMAKE_SYSTEM_PROCESSOR {system_processor})
1421set(CMAKE_C_COMPILER {cc})
1422set(CMAKE_CXX_COMPILER {cxx})
1423set(CMAKE_RANLIB {ranlib})
1424set(CMAKE_C_LINKER_DEPFILE_SUPPORTED FALSE)
1425set(CMAKE_CXX_LINKER_DEPFILE_SUPPORTED FALSE)"#,
1426 system_name = system_name,
1427 system_processor = system_processor,
1428 cc = zig_wrapper.cc.to_slash_lossy(),
1429 cxx = zig_wrapper.cxx.to_slash_lossy(),
1430 ranlib = zig_wrapper.ranlib.to_slash_lossy(),
1431 );
1432 if enable_zig_ar {
1433 content.push_str(&format!(
1434 "\nset(CMAKE_AR {})\n",
1435 zig_wrapper.ar.to_slash_lossy()
1436 ));
1437 }
1438 if system_name == "Darwin" && !cfg!(target_os = "macos") {
1443 let exe_ext = if cfg!(windows) { ".exe" } else { "" };
1444 let install_name_tool = wrapper_dir.join(format!("install_name_tool{exe_ext}"));
1445 symlink_wrapper(&install_name_tool)?;
1446 content.push_str(&format!(
1447 "\nset(CMAKE_INSTALL_NAME_TOOL {})",
1448 install_name_tool.to_slash_lossy()
1449 ));
1450
1451 if which::which("otool").is_err() {
1452 let script_ext = if cfg!(windows) { "bat" } else { "sh" };
1453 let otool = cmake.join(format!("otool.{script_ext}"));
1454 write_noop_script(&otool)?;
1455 content.push_str(&format!("\nset(CMAKE_OTOOL {})", otool.to_slash_lossy()));
1456 }
1457 }
1458 content.push_str(
1462 r#"
1463set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
1464set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
1465set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
1466set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)"#,
1467 );
1468 write_file(&toolchain_file, &content)?;
1469 Ok(toolchain_file)
1470 }
1471
1472 #[cfg(target_os = "macos")]
1473 fn macos_sdk_root() -> Option<PathBuf> {
1474 static SDK_ROOT: OnceLock<Option<PathBuf>> = OnceLock::new();
1475
1476 SDK_ROOT
1477 .get_or_init(|| match env::var_os("SDKROOT") {
1478 Some(sdkroot) if !sdkroot.is_empty() => Some(sdkroot.into()),
1479 _ => {
1480 let output = Command::new("xcrun")
1481 .args(["--sdk", "macosx", "--show-sdk-path"])
1482 .output()
1483 .ok()?;
1484 if output.status.success() {
1485 let stdout = String::from_utf8(output.stdout).ok()?;
1486 let stdout = stdout.trim();
1487 if !stdout.is_empty() {
1488 return Some(stdout.into());
1489 }
1490 }
1491 None
1492 }
1493 })
1494 .clone()
1495 }
1496
1497 #[cfg(not(target_os = "macos"))]
1498 fn macos_sdk_root() -> Option<PathBuf> {
1499 match env::var_os("SDKROOT") {
1500 Some(sdkroot) if !sdkroot.is_empty() => Some(sdkroot.into()),
1501 _ => None,
1502 }
1503 }
1504}
1505
1506fn write_file(path: &Path, content: &str) -> Result<(), anyhow::Error> {
1507 let existing_content = fs::read_to_string(path).unwrap_or_default();
1508 if existing_content != content {
1509 fs::write(path, content)?;
1510 }
1511 Ok(())
1512}
1513
1514#[cfg(target_family = "unix")]
1518fn write_noop_script(path: &Path) -> Result<()> {
1519 let content = "#!/bin/sh\nexit 0\n";
1520 let existing = fs::read_to_string(path).unwrap_or_default();
1521 if existing != content {
1522 OpenOptions::new()
1523 .create(true)
1524 .write(true)
1525 .truncate(true)
1526 .mode(0o700)
1527 .open(path)?
1528 .write_all(content.as_bytes())?;
1529 }
1530 Ok(())
1531}
1532
1533#[cfg(not(target_family = "unix"))]
1534fn write_noop_script(path: &Path) -> Result<()> {
1535 let content = "@echo off\r\nexit /b 0\r\n";
1536 let existing = fs::read_to_string(path).unwrap_or_default();
1537 if existing != content {
1538 fs::write(path, content)?;
1539 }
1540 Ok(())
1541}
1542
1543fn write_tbd_files(deps_dir: &Path) -> Result<(), anyhow::Error> {
1544 write_file(&deps_dir.join("libiconv.tbd"), LIBICONV_TBD)?;
1545 write_file(&deps_dir.join("libcharset.1.tbd"), LIBCHARSET_TBD)?;
1546 write_file(&deps_dir.join("libcharset.tbd"), LIBCHARSET_TBD)?;
1547 Ok(())
1548}
1549
1550fn cache_dir() -> PathBuf {
1551 env::var("CARGO_ZIGBUILD_CACHE_DIR")
1552 .ok()
1553 .map(|s| s.into())
1554 .or_else(dirs::cache_dir)
1555 .unwrap_or_else(|| env::current_dir().expect("Failed to get current dir"))
1557 .join(env!("CARGO_PKG_NAME"))
1558 .join(env!("CARGO_PKG_VERSION"))
1559}
1560
1561#[derive(Debug, Deserialize)]
1562struct ZigEnv {
1563 lib_dir: String,
1564}
1565
1566#[derive(Debug, Clone)]
1568pub struct ZigWrapper {
1569 pub cc: PathBuf,
1570 pub cxx: PathBuf,
1571 pub ar: PathBuf,
1572 pub ranlib: PathBuf,
1573 pub lib: PathBuf,
1574}
1575
1576#[derive(Debug, Clone, Default, PartialEq)]
1577struct TargetFlags {
1578 pub target_cpu: String,
1579 pub target_feature: String,
1580}
1581
1582impl TargetFlags {
1583 pub fn parse_from_encoded(encoded: &OsStr) -> Result<Self> {
1584 let mut parsed = Self::default();
1585
1586 let f = rustflags::from_encoded(encoded);
1587 for flag in f {
1588 if let rustflags::Flag::Codegen { opt, value } = flag {
1589 let key = opt.replace('-', "_");
1590 match key.as_str() {
1591 "target_cpu" => {
1592 if let Some(value) = value {
1593 parsed.target_cpu = value;
1594 }
1595 }
1596 "target_feature" => {
1597 if let Some(value) = value {
1599 if !parsed.target_feature.is_empty() {
1600 parsed.target_feature.push(',');
1601 }
1602 parsed.target_feature.push_str(&value);
1603 }
1604 }
1605 _ => {}
1606 }
1607 }
1608 }
1609 Ok(parsed)
1610 }
1611}
1612
1613#[allow(clippy::blocks_in_conditions)]
1622pub fn prepare_zig_linker(
1623 target: &str,
1624 cargo_config: &cargo_config2::Config,
1625) -> Result<ZigWrapper> {
1626 let (rust_target, abi_suffix) = target.split_once('.').unwrap_or((target, ""));
1627 let abi_suffix = if abi_suffix.is_empty() {
1628 String::new()
1629 } else {
1630 if abi_suffix
1631 .split_once('.')
1632 .filter(|(x, y)| {
1633 !x.is_empty()
1634 && x.chars().all(|c| c.is_ascii_digit())
1635 && !y.is_empty()
1636 && y.chars().all(|c| c.is_ascii_digit())
1637 })
1638 .is_none()
1639 {
1640 bail!("Malformed zig target abi suffix.")
1641 }
1642 format!(".{abi_suffix}")
1643 };
1644 let triple: Triple = rust_target
1645 .parse()
1646 .with_context(|| format!("Unsupported Rust target '{rust_target}'"))?;
1647 let arch = triple.architecture.to_string();
1648 let target_env = match (triple.architecture, triple.environment) {
1649 (Architecture::Mips32(..), Environment::Gnu) => Environment::Gnueabihf,
1650 (Architecture::Mips32(..), Environment::Musl) => Environment::Musleabi,
1651 (Architecture::Powerpc, Environment::Gnu) => Environment::Gnueabihf,
1652 (_, Environment::GnuLlvm) => Environment::Gnu,
1653 (_, environment) => environment,
1654 };
1655 let file_ext = if cfg!(windows) { "bat" } else { "sh" };
1656 let file_target = target.trim_end_matches('.');
1657
1658 let mut cc_args = vec![
1659 "-g".to_owned(),
1661 "-fno-sanitize=all".to_owned(),
1663 ];
1664
1665 let zig_mcpu_default = match triple.operating_system {
1668 OperatingSystem::Linux => {
1669 match arch.as_str() {
1670 "arm" => match target_env {
1672 Environment::Gnueabi | Environment::Musleabi => "generic+v6+strict_align",
1673 Environment::Gnueabihf | Environment::Musleabihf => {
1674 "generic+v6+strict_align+vfp2-d32"
1675 }
1676 _ => "",
1677 },
1678 "armv5te" => "generic+soft_float+strict_align",
1679 "armv7" => "generic+v7a+vfp3-d32+thumb2-neon",
1680 arch_str @ ("i586" | "i686") => {
1681 if arch_str == "i586" {
1682 "pentium"
1683 } else {
1684 "pentium4"
1685 }
1686 }
1687 "riscv64gc" => "generic_rv64+m+a+f+d+c",
1688 "s390x" => "z10-vector",
1689 _ => "",
1690 }
1691 }
1692 _ => "",
1693 };
1694
1695 let zig_mcpu_override = {
1699 let rust_flags = cargo_config.rustflags(rust_target)?.unwrap_or_default();
1700 let encoded_rust_flags = rust_flags.encode()?;
1701 let target_flags = TargetFlags::parse_from_encoded(OsStr::new(&encoded_rust_flags))?;
1702 target_flags.target_cpu.replace('-', "_")
1705 };
1706
1707 if !zig_mcpu_override.is_empty() {
1708 cc_args.push(format!("-mcpu={zig_mcpu_override}"));
1709 } else if !zig_mcpu_default.is_empty() {
1710 cc_args.push(format!("-mcpu={zig_mcpu_default}"));
1711 }
1712
1713 match triple.operating_system {
1714 OperatingSystem::Linux => {
1715 let zig_arch = match arch.as_str() {
1716 "arm" => "arm",
1718 "armv5te" => "arm",
1719 "armv7" => "arm",
1720 "i586" | "i686" => {
1721 let zig_version = Zig::zig_version()?;
1722 if zig_version.major == 0 && zig_version.minor >= 11 {
1723 "x86"
1724 } else {
1725 "i386"
1726 }
1727 }
1728 "riscv64gc" => "riscv64",
1729 "s390x" => "s390x",
1730 _ => arch.as_str(),
1731 };
1732 let mut zig_target_env = target_env.to_string();
1733
1734 let zig_version = Zig::zig_version()?;
1735
1736 if zig_version >= semver::Version::new(0, 15, 0)
1740 && arch.as_str() == "armv7"
1741 && target_env == Environment::Ohos
1742 {
1743 zig_target_env = "ohoseabi".to_string();
1744 }
1745
1746 cc_args.push("-target".to_string());
1747 cc_args.push(format!("{zig_arch}-linux-{zig_target_env}{abi_suffix}"));
1748 }
1749 OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin(_) => {
1750 let zig_version = Zig::zig_version()?;
1751 if zig_version > semver::Version::new(0, 9, 1) {
1754 cc_args.push("-target".to_string());
1755 cc_args.push(format!("{arch}-macos-none{abi_suffix}"));
1756 } else {
1757 cc_args.push("-target".to_string());
1758 cc_args.push(format!("{arch}-macos-gnu{abi_suffix}"));
1759 }
1760 }
1761 OperatingSystem::Windows => {
1762 let zig_arch = match arch.as_str() {
1763 "i686" => {
1764 let zig_version = Zig::zig_version()?;
1765 if zig_version.major == 0 && zig_version.minor >= 11 {
1766 "x86"
1767 } else {
1768 "i386"
1769 }
1770 }
1771 arch => arch,
1772 };
1773 cc_args.push("-target".to_string());
1774 cc_args.push(format!("{zig_arch}-windows-{target_env}{abi_suffix}"));
1775 }
1776 OperatingSystem::Emscripten => {
1777 cc_args.push("-target".to_string());
1778 cc_args.push(format!("{arch}-emscripten{abi_suffix}"));
1779 }
1780 OperatingSystem::Wasi => {
1781 cc_args.push("-target".to_string());
1782 cc_args.push(format!("{arch}-wasi{abi_suffix}"));
1783 }
1784 OperatingSystem::WasiP1 => {
1785 cc_args.push("-target".to_string());
1786 cc_args.push(format!("{arch}-wasi.0.1.0{abi_suffix}"));
1787 }
1788 OperatingSystem::IOS(_) if triple.environment == Environment::Macabi => {
1789 cc_args.push("-target".to_string());
1792 cc_args.push(format!("{arch}-maccatalyst-none{abi_suffix}"));
1793 }
1794 OperatingSystem::Freebsd => {
1795 let zig_arch = match arch.as_str() {
1796 "i686" => {
1797 let zig_version = Zig::zig_version()?;
1798 if zig_version.major == 0 && zig_version.minor >= 11 {
1799 "x86"
1800 } else {
1801 "i386"
1802 }
1803 }
1804 arch => arch,
1805 };
1806 cc_args.push("-target".to_string());
1807 cc_args.push(format!("{zig_arch}-freebsd"));
1808 }
1809 OperatingSystem::Openbsd => {
1810 cc_args.push("-target".to_string());
1811 cc_args.push(format!("{arch}-openbsd"));
1812 }
1813 OperatingSystem::Unknown => {
1814 if triple.architecture == Architecture::Wasm32
1815 || triple.architecture == Architecture::Wasm64
1816 {
1817 cc_args.push("-target".to_string());
1818 cc_args.push(format!("{arch}-freestanding{abi_suffix}"));
1819 } else {
1820 bail!("unsupported target '{rust_target}'")
1821 }
1822 }
1823 _ => bail!(format!("unsupported target '{rust_target}'")),
1824 };
1825
1826 let zig_linker_dir = cache_dir();
1827 fs::create_dir_all(&zig_linker_dir)?;
1828
1829 if triple.operating_system == OperatingSystem::Linux {
1830 if matches!(
1831 triple.environment,
1832 Environment::Gnu
1833 | Environment::Gnuspe
1834 | Environment::Gnux32
1835 | Environment::Gnueabi
1836 | Environment::Gnuabi64
1837 | Environment::GnuIlp32
1838 | Environment::Gnueabihf
1839 ) {
1840 let glibc_version = if abi_suffix.is_empty() {
1841 (2, 17)
1842 } else {
1843 let mut parts = abi_suffix[1..].split('.');
1844 let major: usize = parts.next().unwrap().parse()?;
1845 let minor: usize = parts.next().unwrap().parse()?;
1846 (major, minor)
1847 };
1848 if glibc_version < (2, 28) {
1850 use crate::linux::{FCNTL_H, FCNTL_MAP};
1851
1852 let zig_version = Zig::zig_version()?;
1853 if zig_version.major == 0 && zig_version.minor < 11 {
1854 let fcntl_map = zig_linker_dir.join("fcntl.map");
1855 let existing_content = fs::read_to_string(&fcntl_map).unwrap_or_default();
1856 if existing_content != FCNTL_MAP {
1857 fs::write(&fcntl_map, FCNTL_MAP)?;
1858 }
1859 let fcntl_h = zig_linker_dir.join("fcntl.h");
1860 let existing_content = fs::read_to_string(&fcntl_h).unwrap_or_default();
1861 if existing_content != FCNTL_H {
1862 fs::write(&fcntl_h, FCNTL_H)?;
1863 }
1864
1865 cc_args.push(format!("-Wl,--version-script={}", fcntl_map.display()));
1866 cc_args.push("-include".to_string());
1867 cc_args.push(fcntl_h.display().to_string());
1868 }
1869 }
1870 } else if matches!(
1871 triple.environment,
1872 Environment::Musl
1873 | Environment::Muslabi64
1874 | Environment::Musleabi
1875 | Environment::Musleabihf
1876 ) {
1877 use crate::linux::MUSL_WEAK_SYMBOLS_MAPPING_SCRIPT;
1878
1879 let zig_version = Zig::zig_version()?;
1880 let rustc_version = rustc_version::version_meta()?.semver;
1881
1882 if (zig_version.major, zig_version.minor) >= (0, 11)
1887 && (rustc_version.major, rustc_version.minor) < (1, 72)
1888 {
1889 let weak_symbols_map = zig_linker_dir.join("musl_weak_symbols_map.ld");
1890 fs::write(&weak_symbols_map, MUSL_WEAK_SYMBOLS_MAPPING_SCRIPT)?;
1891
1892 cc_args.push(format!("-Wl,-T,{}", weak_symbols_map.display()));
1893 }
1894 }
1895 }
1896
1897 let cc_args_str = join_args_for_script(&cc_args);
1900
1901 let current_exe = resolve_current_exe()?;
1906 let exe_hash = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC)
1907 .checksum(current_exe.as_os_str().as_encoded_bytes());
1908 let wrapper_dir = zig_linker_dir
1909 .join("wrappers")
1910 .join(format!("{:x}", exe_hash));
1911 fs::create_dir_all(&wrapper_dir)?;
1912
1913 let hash = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC).checksum(cc_args_str.as_bytes());
1914 let zig_cc = wrapper_dir.join(format!("zigcc-{file_target}-{:x}.{file_ext}", hash));
1915 let zig_cxx = wrapper_dir.join(format!("zigcxx-{file_target}-{:x}.{file_ext}", hash));
1916 let zig_ranlib = wrapper_dir.join(format!("zigranlib.{file_ext}"));
1917 let zig_version = Zig::zig_version()?;
1918 write_linker_wrapper(&zig_cc, "cc", &cc_args_str, &zig_version)?;
1919 write_linker_wrapper(&zig_cxx, "c++", &cc_args_str, &zig_version)?;
1920 write_linker_wrapper(&zig_ranlib, "ranlib", "", &zig_version)?;
1921
1922 let exe_ext = if cfg!(windows) { ".exe" } else { "" };
1923 let zig_ar = wrapper_dir.join(format!("ar{exe_ext}"));
1924 symlink_wrapper(&zig_ar)?;
1925 let zig_lib = wrapper_dir.join(format!("lib{exe_ext}"));
1926 symlink_wrapper(&zig_lib)?;
1927
1928 if matches!(triple.operating_system, OperatingSystem::Windows)
1934 && matches!(triple.environment, Environment::Gnu)
1935 {
1936 if !has_system_dlltool(&triple.architecture) {
1939 let dlltool_name = get_dlltool_name(&triple.architecture);
1940 let zig_dlltool = wrapper_dir.join(format!("{dlltool_name}{exe_ext}"));
1941 symlink_wrapper(&zig_dlltool)?;
1942 }
1943 }
1944
1945 Ok(ZigWrapper {
1946 cc: zig_cc,
1947 cxx: zig_cxx,
1948 ar: zig_ar,
1949 ranlib: zig_ranlib,
1950 lib: zig_lib,
1951 })
1952}
1953
1954fn resolve_current_exe() -> Result<PathBuf> {
1956 if let Ok(exe) = env::var("CARGO_BIN_EXE_cargo-zigbuild") {
1957 Ok(PathBuf::from(exe))
1958 } else {
1959 Ok(env::current_exe()?)
1960 }
1961}
1962
1963fn symlink_wrapper(target: &Path) -> Result<()> {
1964 let current_exe = resolve_current_exe()?;
1965 #[cfg(windows)]
1966 {
1967 if !target.exists() {
1968 if std::fs::hard_link(¤t_exe, target).is_err() {
1970 std::fs::copy(¤t_exe, target)?;
1972 }
1973 }
1974 }
1975
1976 #[cfg(unix)]
1977 {
1978 if !target.exists() {
1979 if fs::read_link(target).is_ok() {
1980 fs::remove_file(target)?;
1982 }
1983 std::os::unix::fs::symlink(current_exe, target)?;
1984 }
1985 }
1986 Ok(())
1987}
1988
1989#[cfg(target_family = "unix")]
1991fn join_args_for_script<I, S>(args: I) -> String
1992where
1993 I: IntoIterator<Item = S>,
1994 S: AsRef<str>,
1995{
1996 shell_words::join(args)
1997}
1998
1999#[cfg(not(target_family = "unix"))]
2005fn quote_for_batch(s: &str) -> String {
2006 let needs_quoting_or_escaping = s.is_empty()
2007 || s.contains(|c: char| {
2008 matches!(
2009 c,
2010 ' ' | '\t' | '"' | '&' | '|' | '<' | '>' | '^' | '%' | '(' | ')' | '!'
2011 )
2012 });
2013
2014 if !needs_quoting_or_escaping {
2015 return s.to_string();
2016 }
2017
2018 let mut out = String::with_capacity(s.len() + 8);
2019 out.push('"');
2020 for c in s.chars() {
2021 match c {
2022 '"' => out.push_str("\"\""),
2023 '%' => out.push_str("%%"),
2024 _ => out.push(c),
2025 }
2026 }
2027 out.push('"');
2028 out
2029}
2030
2031#[cfg(not(target_family = "unix"))]
2033fn join_args_for_script<I, S>(args: I) -> String
2034where
2035 I: IntoIterator<Item = S>,
2036 S: AsRef<str>,
2037{
2038 args.into_iter()
2039 .map(|s| quote_for_batch(s.as_ref()))
2040 .collect::<Vec<_>>()
2041 .join(" ")
2042}
2043
2044#[cfg(target_family = "unix")]
2046fn write_linker_wrapper(
2047 path: &Path,
2048 command: &str,
2049 args: &str,
2050 zig_version: &semver::Version,
2051) -> Result<()> {
2052 let mut buf = Vec::<u8>::new();
2053 let current_exe = resolve_current_exe()?;
2054 writeln!(&mut buf, "#!/bin/sh")?;
2055
2056 writeln!(
2058 &mut buf,
2059 "export CARGO_ZIGBUILD_ZIG_VERSION={}",
2060 zig_version
2061 )?;
2062
2063 writeln!(&mut buf, "if [ -n \"$SDKROOT\" ]; then export SDKROOT; fi")?;
2065
2066 writeln!(
2067 &mut buf,
2068 "exec \"{}\" zig {} -- {} \"$@\"",
2069 current_exe.display(),
2070 command,
2071 args
2072 )?;
2073
2074 let existing_content = fs::read(path).unwrap_or_default();
2078 if existing_content != buf {
2079 OpenOptions::new()
2080 .create(true)
2081 .write(true)
2082 .truncate(true)
2083 .mode(0o700)
2084 .open(path)?
2085 .write_all(&buf)?;
2086 }
2087 Ok(())
2088}
2089
2090#[cfg(not(target_family = "unix"))]
2092fn write_linker_wrapper(
2093 path: &Path,
2094 command: &str,
2095 args: &str,
2096 zig_version: &semver::Version,
2097) -> Result<()> {
2098 let mut buf = Vec::<u8>::new();
2099 let current_exe = resolve_current_exe()?;
2100 let current_exe = if is_mingw_shell() {
2101 current_exe.to_slash_lossy().to_string()
2102 } else {
2103 current_exe.display().to_string()
2104 };
2105 writeln!(&mut buf, "@echo off")?;
2106 writeln!(&mut buf, "setlocal DisableDelayedExpansion")?;
2108 writeln!(&mut buf, "set CARGO_ZIGBUILD_ZIG_VERSION={}", zig_version)?;
2110 writeln!(
2111 &mut buf,
2112 "\"{}\" zig {} -- {} %*",
2113 adjust_canonicalization(current_exe),
2114 command,
2115 args
2116 )?;
2117
2118 let existing_content = fs::read(path).unwrap_or_default();
2119 if existing_content != buf {
2120 fs::write(path, buf)?;
2121 }
2122 Ok(())
2123}
2124
2125pub(crate) fn is_mingw_shell() -> bool {
2126 env::var_os("MSYSTEM").is_some() && env::var_os("SHELL").is_some()
2127}
2128
2129#[cfg(target_os = "windows")]
2131pub fn adjust_canonicalization(p: String) -> String {
2132 const VERBATIM_PREFIX: &str = r#"\\?\"#;
2133 if p.starts_with(VERBATIM_PREFIX) {
2134 p[VERBATIM_PREFIX.len()..].to_string()
2135 } else {
2136 p
2137 }
2138}
2139
2140fn python_path() -> Result<PathBuf> {
2141 let python = env::var("CARGO_ZIGBUILD_PYTHON_PATH").unwrap_or_else(|_| "python3".to_string());
2142 Ok(which::which(python)?)
2143}
2144
2145fn zig_path() -> Result<PathBuf> {
2146 let zig = env::var("CARGO_ZIGBUILD_ZIG_PATH").unwrap_or_else(|_| "zig".to_string());
2147 Ok(which::which(zig)?)
2148}
2149
2150fn get_dlltool_name(arch: &Architecture) -> &'static str {
2154 if cfg!(windows) {
2155 "dlltool"
2156 } else {
2157 match arch {
2158 Architecture::X86_64 => "x86_64-w64-mingw32-dlltool",
2159 Architecture::X86_32(_) => "i686-w64-mingw32-dlltool",
2160 Architecture::Aarch64(_) => "aarch64-w64-mingw32-dlltool",
2161 _ => "dlltool",
2162 }
2163 }
2164}
2165
2166fn has_system_dlltool(arch: &Architecture) -> bool {
2169 which::which(get_dlltool_name(arch)).is_ok()
2170}
2171
2172#[cfg(test)]
2173mod tests {
2174 use super::*;
2175
2176 #[test]
2177 fn test_target_flags() {
2178 let cases = [
2179 ("-C target-feature=-crt-static", "", "-crt-static"),
2181 ("-C target-cpu=native", "native", ""),
2182 (
2183 "--deny warnings --codegen target-feature=+crt-static",
2184 "",
2185 "+crt-static",
2186 ),
2187 ("-C target_cpu=skylake-avx512", "skylake-avx512", ""),
2188 ("-Ctarget_cpu=x86-64-v3", "x86-64-v3", ""),
2189 (
2190 "-C target-cpu=native --cfg foo -C target-feature=-avx512bf16,-avx512bitalg",
2191 "native",
2192 "-avx512bf16,-avx512bitalg",
2193 ),
2194 (
2195 "--target x86_64-unknown-linux-gnu --codegen=target-cpu=x --codegen=target-cpu=x86-64",
2196 "x86-64",
2197 "",
2198 ),
2199 (
2200 "-Ctarget-feature=+crt-static -Ctarget-feature=+avx",
2201 "",
2202 "+crt-static,+avx",
2203 ),
2204 ];
2205
2206 for (input, expected_target_cpu, expected_target_feature) in cases.iter() {
2207 let args = cargo_config2::Flags::from_space_separated(input);
2208 let encoded_rust_flags = args.encode().unwrap();
2209 let flags = TargetFlags::parse_from_encoded(OsStr::new(&encoded_rust_flags)).unwrap();
2210 assert_eq!(flags.target_cpu, *expected_target_cpu, "{}", input);
2211 assert_eq!(flags.target_feature, *expected_target_feature, "{}", input);
2212 }
2213 }
2214
2215 #[test]
2216 fn test_join_args_for_script() {
2217 let args = vec!["-target", "x86_64-linux-gnu"];
2219 let result = join_args_for_script(&args);
2220 assert!(result.contains("-target"));
2221 assert!(result.contains("x86_64-linux-gnu"));
2222 }
2223
2224 #[test]
2225 #[cfg(not(target_family = "unix"))]
2226 fn test_quote_for_batch() {
2227 assert_eq!(quote_for_batch("-target"), "-target");
2229 assert_eq!(quote_for_batch("x86_64-linux-gnu"), "x86_64-linux-gnu");
2230
2231 assert_eq!(
2233 quote_for_batch("C:\\Users\\John Doe\\path"),
2234 "\"C:\\Users\\John Doe\\path\""
2235 );
2236
2237 assert_eq!(quote_for_batch(""), "\"\"");
2239
2240 assert_eq!(quote_for_batch("foo&bar"), "\"foo&bar\"");
2242 assert_eq!(quote_for_batch("foo|bar"), "\"foo|bar\"");
2243 assert_eq!(quote_for_batch("foo<bar"), "\"foo<bar\"");
2244 assert_eq!(quote_for_batch("foo>bar"), "\"foo>bar\"");
2245 assert_eq!(quote_for_batch("foo^bar"), "\"foo^bar\"");
2246 assert_eq!(quote_for_batch("foo%bar"), "\"foo%bar\"");
2247
2248 assert_eq!(quote_for_batch("foo\"bar"), "\"foo\"\"bar\"");
2250 }
2251
2252 #[test]
2253 #[cfg(not(target_family = "unix"))]
2254 fn test_join_args_for_script_windows() {
2255 let args = vec![
2257 "-target",
2258 "x86_64-linux-gnu",
2259 "-L",
2260 "C:\\Users\\John Doe\\path",
2261 ];
2262 let result = join_args_for_script(&args);
2263 assert!(result.contains("\"C:\\Users\\John Doe\\path\""));
2265 assert!(result.contains("-target"));
2267 assert!(!result.contains("\"-target\""));
2268 }
2269
2270 fn make_rustc_ver(major: u64, minor: u64, patch: u64) -> rustc_version::Version {
2271 rustc_version::Version::new(major, minor, patch)
2272 }
2273
2274 fn make_zig_ver(major: u64, minor: u64, patch: u64) -> semver::Version {
2275 semver::Version::new(major, minor, patch)
2276 }
2277
2278 fn run_filter(args: &[&str], target: Option<&str>, zig_ver: (u64, u64)) -> Vec<String> {
2279 let rustc_ver = make_rustc_ver(1, 80, 0);
2280 let zig_version = make_zig_ver(0, zig_ver.0, zig_ver.1);
2281 let target_info = TargetInfo::new(target.map(|s| s.to_string()).as_ref());
2282 filter_linker_args(
2283 args.iter().map(|s| s.to_string()),
2284 &rustc_ver,
2285 &zig_version,
2286 &target_info,
2287 )
2288 }
2289
2290 fn run_filter_one(arg: &str, target: Option<&str>, zig_ver: (u64, u64)) -> Vec<String> {
2291 run_filter(&[arg], target, zig_ver)
2292 }
2293
2294 fn run_filter_one_rustc(
2295 arg: &str,
2296 target: Option<&str>,
2297 zig_ver: (u64, u64),
2298 rustc_minor: u64,
2299 ) -> Vec<String> {
2300 let rustc_ver = make_rustc_ver(1, rustc_minor, 0);
2301 let zig_version = make_zig_ver(0, zig_ver.0, zig_ver.1);
2302 let target_info = TargetInfo::new(target.map(|s| s.to_string()).as_ref());
2303 filter_linker_args(
2304 std::iter::once(arg.to_string()),
2305 &rustc_ver,
2306 &zig_version,
2307 &target_info,
2308 )
2309 }
2310
2311 #[test]
2312 fn test_filter_common_replacements() {
2313 let linux = Some("x86_64-unknown-linux-gnu");
2314 assert_eq!(run_filter_one("-lgcc_s", linux, (13, 0)), vec!["-lunwind"]);
2316 assert!(run_filter_one("--target=x86_64-unknown-linux-gnu", linux, (13, 0)).is_empty());
2318 assert_eq!(
2320 run_filter_one("-emain", linux, (13, 0)),
2321 vec!["-Wl,--entry=main"]
2322 );
2323 assert_eq!(
2325 run_filter_one("-export-dynamic", linux, (13, 0)),
2326 vec!["-export-dynamic"]
2327 );
2328 }
2329
2330 #[test]
2331 fn test_filter_compiler_builtins_removed() {
2332 for target in &["armv7-unknown-linux-gnueabihf", "x86_64-pc-windows-gnu"] {
2333 let result = run_filter_one(
2334 "/path/to/libcompiler_builtins-abc123.rlib",
2335 Some(target),
2336 (13, 0),
2337 );
2338 assert!(
2339 result.is_empty(),
2340 "compiler_builtins should be removed for {target}"
2341 );
2342 }
2343 }
2344
2345 #[test]
2346 fn test_filter_windows_gnu_args() {
2347 let gnu = Some("x86_64-pc-windows-gnu");
2348 let removed: &[&str] = &[
2350 "-lwindows",
2351 "-l:libpthread.a",
2352 "-lgcc",
2353 "-Wl,--disable-auto-image-base",
2354 "-Wl,--dynamicbase",
2355 "-Wl,--large-address-aware",
2356 "-Wl,/path/to/list.def",
2357 "-Wl,C:\\path\\to\\list.def",
2358 "-lmsvcrt",
2359 ];
2360 for arg in removed {
2361 let result = run_filter_one(arg, gnu, (13, 0));
2362 assert!(result.is_empty(), "{arg} should be removed for windows-gnu");
2363 }
2364 let replaced: &[(&str, (u64, u64), &str)] = &[
2366 ("-lgcc_eh", (13, 0), "-lc++"),
2367 ("-Wl,-Bdynamic", (13, 0), "-Wl,-search_paths_first"),
2368 ];
2369 for (arg, zig_ver, expected) in replaced {
2370 let result = run_filter_one(arg, gnu, *zig_ver);
2371 assert_eq!(result, vec![*expected], "filter({arg})");
2372 }
2373 let result = run_filter_one("-lgcc_eh", gnu, (14, 0));
2375 assert_eq!(result, vec!["-lgcc_eh"]);
2376 }
2377
2378 #[test]
2379 fn test_filter_windows_gnu_rsbegin() {
2380 let result = run_filter_one("/path/to/rsbegin.o", Some("i686-pc-windows-gnu"), (13, 0));
2382 assert!(result.is_empty());
2383 let result = run_filter_one("/path/to/rsbegin.o", Some("x86_64-pc-windows-gnu"), (13, 0));
2385 assert_eq!(result, vec!["/path/to/rsbegin.o"]);
2386 }
2387
2388 #[test]
2389 fn test_filter_unsupported_linker_args() {
2390 let linux = Some("x86_64-unknown-linux-gnu");
2391 let removed: &[&str] = &[
2392 "-Wl,--no-undefined-version",
2393 "-Wl,-znostart-stop-gc",
2394 "-Wl,--fix-cortex-a53-843419",
2395 "-Wl,-plugin-opt=O2",
2396 ];
2397 for arg in removed {
2398 let result = run_filter_one(arg, linux, (13, 0));
2399 assert!(result.is_empty(), "{arg} should be removed");
2400 }
2401 }
2402
2403 #[test]
2404 fn test_filter_wp_args() {
2405 let linux = Some("x86_64-unknown-linux-gnu");
2406 for arg in &[
2408 "-Wp,-U_FORTIFY_SOURCE",
2409 "-Wp,-DFOO=1",
2410 "-Wp,-MF,/tmp/t.d",
2411 "-Wp,-MQ,foo",
2412 "-Wp,-MP",
2413 ] {
2414 let result = run_filter_one(arg, linux, (13, 0));
2415 assert!(result.is_empty(), "{arg} should be removed");
2416 }
2417 for arg in &["-Wp,-MD,/tmp/test.d", "-Wp,-MMD,/tmp/test.d", "-Wp,-MT,foo"] {
2419 let result = run_filter_one(arg, linux, (13, 0));
2420 assert_eq!(result, vec![*arg], "{arg} should be kept");
2421 }
2422 let result = run_filter_one("-U_FORTIFY_SOURCE", linux, (13, 0));
2424 assert_eq!(result, vec!["-U_FORTIFY_SOURCE"]);
2425 let result = run_filter_one("-DFOO=1", linux, (13, 0));
2426 assert_eq!(result, vec!["-DFOO=1"]);
2427 }
2428
2429 #[test]
2430 fn test_filter_musl_args() {
2431 let musl = Some("x86_64-unknown-linux-musl");
2432 let removed: &[&str] = &["/path/self-contained/crt1.o", "-lc"];
2433 for arg in removed {
2434 let result = run_filter_one(arg, musl, (13, 0));
2435 assert!(result.is_empty(), "{arg} should be removed for musl");
2436 }
2437 let result = run_filter_one("-Wl,-melf_i386", Some("i686-unknown-linux-musl"), (13, 0));
2439 assert!(result.is_empty());
2440 let result = run_filter_one_rustc("/path/to/liblibc-abc123.rlib", musl, (13, 0), 58);
2442 assert!(result.is_empty());
2443 let result = run_filter_one_rustc("/path/to/liblibc-abc123.rlib", musl, (13, 0), 59);
2444 assert_eq!(result, vec!["/path/to/liblibc-abc123.rlib"]);
2445 }
2446
2447 #[test]
2448 fn test_filter_march_args() {
2449 let cases: &[(&str, &str, &[&str])] = &[
2451 ("-march=armv7-a", "armv7-unknown-linux-gnueabihf", &[]),
2453 (
2455 "-march=rv64gc",
2456 "riscv64gc-unknown-linux-gnu",
2457 &["-march=generic_rv64"],
2458 ),
2459 (
2461 "-march=rv32imac",
2462 "riscv32imac-unknown-none-elf",
2463 &["-march=generic_rv32"],
2464 ),
2465 (
2467 "-march=armv8.4-a",
2468 "aarch64-unknown-linux-gnu",
2469 &["-mcpu=generic"],
2470 ),
2471 (
2473 "-march=armv8.4-a+crypto",
2474 "aarch64-unknown-linux-gnu",
2475 &[
2476 "-mcpu=generic+crypto",
2477 "-Xassembler",
2478 "-march=armv8.4-a+crypto",
2479 ],
2480 ),
2481 (
2483 "-march=armv8.4-a",
2484 "aarch64-apple-darwin",
2485 &["-mcpu=apple_m1"],
2486 ),
2487 ];
2488 for (input, target, expected) in cases {
2489 let result = run_filter_one(input, Some(target), (13, 0));
2490 assert_eq!(&result, expected, "filter({input}, {target})");
2491 }
2492 }
2493
2494 #[test]
2495 fn test_filter_apple_args() {
2496 let darwin = Some("aarch64-apple-darwin");
2497 let result = run_filter_one("-Wl,-dylib", darwin, (13, 0));
2498 assert!(result.is_empty());
2499 }
2500
2501 #[test]
2502 fn test_filter_freebsd_libs_removed() {
2503 for lib in &["-lkvm", "-lmemstat", "-lprocstat", "-ldevstat"] {
2504 let result = run_filter_one(lib, Some("x86_64-unknown-freebsd"), (13, 0));
2505 assert!(result.is_empty(), "{lib} should be removed for freebsd");
2506 }
2507 }
2508
2509 #[test]
2510 fn test_filter_exported_symbols_list_two_arg_apple() {
2511 let result = run_filter(
2512 &[
2513 "-arch",
2514 "arm64",
2515 "-Wl,-exported_symbols_list",
2516 "-Wl,/tmp/rustcXXX/list",
2517 "-o",
2518 "output.dylib",
2519 ],
2520 Some("aarch64-apple-darwin"),
2521 (13, 0),
2522 );
2523 assert_eq!(result, vec!["-arch", "arm64", "-o", "output.dylib"]);
2524 }
2525
2526 #[test]
2527 fn test_filter_exported_symbols_list_two_arg_cross_platform() {
2528 let result = run_filter(
2529 &[
2530 "-arch",
2531 "arm64",
2532 "-Wl,-exported_symbols_list",
2533 "-Wl,C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\rustcXXX\\list",
2534 "-o",
2535 "output.dylib",
2536 ],
2537 None,
2538 (13, 0),
2539 );
2540 assert_eq!(result, vec!["-arch", "arm64", "-o", "output.dylib"]);
2541 }
2542
2543 #[test]
2544 fn test_filter_exported_symbols_list_single_arg_comma() {
2545 let result = run_filter(
2546 &[
2547 "-Wl,-exported_symbols_list,/tmp/rustcXXX/list",
2548 "-o",
2549 "output.dylib",
2550 ],
2551 Some("aarch64-apple-darwin"),
2552 (13, 0),
2553 );
2554 assert_eq!(result, vec!["-o", "output.dylib"]);
2555 }
2556
2557 #[test]
2558 fn test_filter_exported_symbols_list_not_filtered_zig_016() {
2559 let result = run_filter(
2560 &[
2561 "-Wl,-exported_symbols_list",
2562 "-Wl,/tmp/rustcXXX/list",
2563 "-o",
2564 "output.dylib",
2565 ],
2566 Some("aarch64-apple-darwin"),
2567 (16, 0),
2568 );
2569 assert_eq!(
2570 result,
2571 vec![
2572 "-Wl,-exported_symbols_list",
2573 "-Wl,/tmp/rustcXXX/list",
2574 "-o",
2575 "output.dylib"
2576 ]
2577 );
2578 }
2579
2580 #[test]
2581 fn test_filter_dynamic_list_two_arg() {
2582 let result = run_filter(
2583 &[
2584 "-Wl,--dynamic-list",
2585 "-Wl,/tmp/rustcXXX/list",
2586 "-o",
2587 "output.so",
2588 ],
2589 Some("x86_64-unknown-linux-gnu"),
2590 (13, 0),
2591 );
2592 assert_eq!(result, vec!["-o", "output.so"]);
2593 }
2594
2595 #[test]
2596 fn test_filter_dynamic_list_single_arg_comma() {
2597 let result = run_filter(
2598 &["-Wl,--dynamic-list,/tmp/rustcXXX/list", "-o", "output.so"],
2599 Some("x86_64-unknown-linux-gnu"),
2600 (13, 0),
2601 );
2602 assert_eq!(result, vec!["-o", "output.so"]);
2603 }
2604
2605 #[test]
2606 fn test_filter_preserves_normal_args() {
2607 let result = run_filter(
2608 &["-arch", "arm64", "-lSystem", "-lc", "-o", "output"],
2609 Some("aarch64-apple-darwin"),
2610 (13, 0),
2611 );
2612 assert_eq!(
2613 result,
2614 vec!["-arch", "arm64", "-lSystem", "-lc", "-o", "output"]
2615 );
2616 }
2617
2618 #[test]
2619 fn test_filter_skip_next_at_end_of_args() {
2620 let result = run_filter(
2621 &["-o", "output", "-Wl,-exported_symbols_list"],
2622 Some("aarch64-apple-darwin"),
2623 (13, 0),
2624 );
2625 assert_eq!(result, vec!["-o", "output"]);
2626 }
2627}