Repository: rust-lang/cc-rs
Pull request #836 (2023-07-25 05:54:04)

add armv8r-none-eabihf
Let cc-rs recognize the armv8r-none-eabihf target.

Related to rust-lang/rust#110482.



Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index f85288d..653c6d1 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -11,7 +11,7 @@ jobs:
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
-        build: [stable, beta, nightly, linux32, macos, aarch64-ios, win32, win64, mingw32, mingw64, windows-2019]
+        build: [stable, beta, nightly, linux32, macos, aarch64-macos, aarch64-ios, win32, win64, mingw32, mingw64, windows-2019]
         include:
           - build: stable
             os: ubuntu-latest
@@ -33,6 +33,10 @@ jobs:
             os: macos-latest
             rust: stable
             target: x86_64-apple-darwin
+          - build: aarch64-macos
+            os: macos-14
+            rust: stable
+            target: aarch64-apple-darwin
           - build: aarch64-ios
             os: macos-latest
             rust: stable
@@ -68,7 +72,7 @@ jobs:
             rust: stable-x86_64
             target: x86_64-pc-windows-msvc
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
     - name: Install Rust (rustup)
       run: |
         set -euxo pipefail
@@ -86,17 +90,53 @@ jobs:
         sudo apt-get update
         sudo apt-get install g++-multilib
       if: matrix.build == 'linux32'
-    - run: cargo test ${{ matrix.no_run }}
-    - run: cargo test ${{ matrix.no_run }} --features parallel
-    - run: cargo test ${{ matrix.no_run }} --manifest-path cc-test/Cargo.toml --target ${{ matrix.target }}
-    - run: cargo test ${{ matrix.no_run }} --manifest-path cc-test/Cargo.toml --target ${{ matrix.target }} --features parallel
-    - run: cargo test ${{ matrix.no_run }} --manifest-path cc-test/Cargo.toml --target ${{ matrix.target }} --release
+    - uses: Swatinem/rust-cache@v2
+    - run: cargo test ${{ matrix.no_run }} --workspace --target ${{ matrix.target }}
+    - run: cargo test ${{ matrix.no_run }} --workspace --target ${{ matrix.target }} --release
+    - run: cargo test ${{ matrix.no_run }} --workspace --target ${{ matrix.target }} --features parallel
+
+  # This is separate from the matrix above because there is no prebuilt rust-std component for these targets.
+  check-tvos:
+    name: Test build-std
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        build: [aarch64-tvos, aarch64-tvos-sim, x86_64-tvos]
+        include:
+          - build: aarch64-tvos
+            os: macos-latest
+            rust: nightly
+            target: aarch64-apple-tvos
+            no_run: --no-run
+          - build: aarch64-tvos-sim
+            os: macos-latest
+            rust: nightly
+            target: aarch64-apple-tvos-sim
+            no_run: --no-run
+          - build: x86_64-tvos
+            os: macos-latest
+            rust: nightly
+            target: x86_64-apple-tvos
+            no_run: --no-run
+    steps:
+    - uses: actions/checkout@v4
+    - name: Install Rust (rustup)
+      run: |
+        set -euxo pipefail
+        rustup toolchain install ${{ matrix.rust }} --no-self-update --profile minimal
+        rustup component add rust-src --toolchain ${{ matrix.rust }}
+        rustup default ${{ matrix.rust }}
+      shell: bash
+    - uses: Swatinem/rust-cache@v2
+    - run: cargo test -Z build-std=std ${{ matrix.no_run }} --workspace --target ${{ matrix.target }}
+    - run: cargo test -Z build-std=std ${{ matrix.no_run }} --workspace --target ${{ matrix.target }} --release
+    - run: cargo test -Z build-std=std ${{ matrix.no_run }} --workspace --target ${{ matrix.target }} --features parallel
 
   cuda:
     name: Test CUDA support
     runs-on: ubuntu-20.04
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
     - name: Install cuda-minimal-build-11-8
       shell: bash
       run: |
@@ -105,34 +145,56 @@ jobs:
         sudo dpkg -i cuda-keyring_1.0-1_all.deb
         sudo apt-get update
         sudo apt-get -y install cuda-minimal-build-11-8
+    - uses: Swatinem/rust-cache@v2
     - name: Test 'cudart' feature
       shell: bash
       run: |
-        PATH="/usr/local/cuda/bin:$PATH" cargo test --manifest-path cc-test/Cargo.toml --features test_cuda
+        PATH="/usr/local/cuda/bin:$PATH" cargo test --manifest-path dev-tools/cc-test/Cargo.toml --features test_cuda
 
   msrv:
     name: MSRV
     runs-on: ${{ matrix.os }}
     strategy:
+      fail-fast: false
       matrix:
         os: [ubuntu-latest, windows-latest]
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
+    - name: Install Rust
+      run: |
+        rustup toolchain install 1.53.0 --no-self-update --profile minimal
+        rustup toolchain install nightly --no-self-update --profile minimal
+        rustup default 1.53.0
+      shell: bash
+    - name: Create Cargo.lock with minimal version
+      run: cargo +nightly update -Zminimal-versions
+    - name: Cache downloaded crates since 1.53 is really slow in fetching
+      uses: Swatinem/rust-cache@v2
+    - run: cargo check --lib -p cc
+    - run: cargo check --lib -p cc --all-features
+
+  clippy:
+    name: Clippy
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v4
     - name: Install Rust
       run: |
-        rustup toolchain install 1.46.0 --no-self-update --profile minimal
-        rustup default 1.46.0
+        rustup toolchain install stable --no-self-update --profile minimal --component rustfmt
+        rustup default stable
       shell: bash
-    - run: cargo check --lib
+    - uses: Swatinem/rust-cache@v2
+    - run: cargo clippy
 
   rustfmt:
     name: Rustfmt
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
     - name: Install Rust
       run: |
         rustup toolchain install stable --no-self-update --profile minimal --component rustfmt
         rustup default stable
       shell: bash
+    - uses: Swatinem/rust-cache@v2
     - run: cargo fmt -- --check
diff --git a/Cargo.toml b/Cargo.toml
index bde2eb2..54dead2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "cc"
-version = "1.0.79"
+version = "1.0.86"
 authors = ["Alex Crichton <alex@alexcrichton.com>"]
 license = "MIT OR Apache-2.0"
 repository = "https://github.com/rust-lang/cc-rs"
@@ -14,20 +14,24 @@ code.
 keywords = ["build-dependencies"]
 readme = "README.md"
 categories = ["development-tools::build-utils"]
-exclude = ["/.github"]
+# The binary target is only used by tests.
+exclude = ["/.github", "tests", "src/bin"]
 edition = "2018"
-
-[dependencies]
-jobserver = { version = "0.1.16", optional = true }
+rust-version = "1.53"
 
 [target.'cfg(unix)'.dependencies]
-libc = "0.2.62"
-
-[target.'cfg(windows)'.dependencies]
-windows-sys = { version = "0.48.0", features = ["Win32_Foundation", "Win32_System_Pipes", "Win32_Security", "Win32_System_Com", "Win32_System_Registry"] }
+# Don't turn on the feature "std" for this, see https://github.com/rust-lang/cargo/issues/4866
+# which is still an issue with `resolver = "1"`.
+libc = { version = "0.2.62", default-features = false, optional = true }
 
 [features]
-parallel = ["jobserver"]
+parallel = ["libc"]
 
 [dev-dependencies]
 tempfile = "3"
+
+[workspace]
+members = [
+    "dev-tools/cc-test",
+    "dev-tools/gen-windows-sys-binding",
+]
diff --git a/README.md b/README.md
index a58dc68..33d4bb4 100644
--- a/README.md
+++ b/README.md
@@ -1,219 +1,13 @@
 # cc-rs
 
-A library to compile C/C++/assembly into a Rust library/application.
-
-[Documentation](https://docs.rs/cc)
-
-A simple library meant to be used as a build dependency with Cargo packages in
-order to build a set of C/C++ files into a static archive. This crate calls out
-to the most relevant compiler for a platform, for example using `cl` on MSVC.
-
-## Using cc-rs
-
-First, you'll want to both add a build script for your crate (`build.rs`) and
-also add this crate to your `Cargo.toml` via:
-
-```toml
-[build-dependencies]
-cc = "1.0"
-```
-
-Next up, you'll want to write a build script like so:
-
-```rust,no_run
-// build.rs
-
-fn main() {
-    cc::Build::new()
-        .file("foo.c")
-        .file("bar.c")
-        .compile("foo");
-}
-```
-
-And that's it! Running `cargo build` should take care of the rest and your Rust
-application will now have the C files `foo.c` and `bar.c` compiled into a file
-named `libfoo.a`. If the C files contain
-
-```c
-void foo_function(void) { ... }
-```
-
-and
-
-```c
-int32_t bar_function(int32_t x) { ... }
-```
-
-you can call them from Rust by declaring them in
-your Rust code like so:
-
-```rust,no_run
-extern "C" {
-    fn foo_function();
-    fn bar_function(x: i32) -> i32;
-}
-
-pub fn call() {
-    unsafe {
-        foo_function();
-        bar_function(42);
-    }
-}
-
-fn main() {
-    // ...
-}
-```
-
-See [the Rustonomicon](https://doc.rust-lang.org/nomicon/ffi.html) for more details.
-
-## External configuration via environment variables
-
-To control the programs and flags used for building, the builder can set a
-number of different environment variables.
-
-* `CFLAGS` - a series of space separated flags passed to compilers. Note that
-             individual flags cannot currently contain spaces, so doing
-             something like: `-L=foo\ bar` is not possible.
-* `CC` - the actual C compiler used. Note that this is used as an exact
-         executable name, so (for example) no extra flags can be passed inside
-         this variable, and the builder must ensure that there aren't any
-         trailing spaces. This compiler must understand the `-c` flag. For
-         certain `TARGET`s, it also is assumed to know about other flags (most
-         common is `-fPIC`).
-* `AR` - the `ar` (archiver) executable to use to build the static library.
-* `CRATE_CC_NO_DEFAULTS` - the default compiler flags may cause conflicts in
-                           some cross compiling scenarios. Setting this variable
-                           will disable the generation of default compiler
-                           flags.
-* `CXX...` - see [C++ Support](#c-support).
-
-Furthermore, projects using this crate may specify custom environment variables
-to be inspected, for example via the `Build::try_flags_from_environment`
-function. Consult the project’s own documentation or its use of the `cc` crate
-for any additional variables it may use.
-
-Each of these variables can also be supplied with certain prefixes and suffixes,
-in the following prioritized order:
-
-1. `<var>_<target>` - for example, `CC_x86_64-unknown-linux-gnu`
-2. `<var>_<target_with_underscores>` - for example, `CC_x86_64_unknown_linux_gnu`
-3. `<build-kind>_<var>` - for example, `HOST_CC` or `TARGET_CFLAGS`
-4. `<var>` - a plain `CC`, `AR` as above.
-
-If none of these variables exist, cc-rs uses built-in defaults.
-
-In addition to the above optional environment variables, `cc-rs` has some
-functions with hard requirements on some variables supplied by [cargo's
-build-script driver][cargo] that it has the `TARGET`, `OUT_DIR`, `OPT_LEVEL`,
-and `HOST` variables.
-
-[cargo]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#inputs-to-the-build-script
-
-## Optional features
-
-### Parallel
-
-Currently cc-rs supports parallel compilation (think `make -jN`) but this
-feature is turned off by default. To enable cc-rs to compile C/C++ in parallel,
-you can change your dependency to:
-
-```toml
-[build-dependencies]
-cc = { version = "1.0", features = ["parallel"] }
-```
-
-By default cc-rs will limit parallelism to `$NUM_JOBS`, or if not present it
-will limit it to the number of cpus on the machine. If you are using cargo,
-use `-jN` option of `build`, `test` and `run` commands as `$NUM_JOBS`
-is supplied by cargo.
-
-## Compile-time Requirements
-
-To work properly this crate needs access to a C compiler when the build script
-is being run. This crate does not ship a C compiler with it. The compiler
-required varies per platform, but there are three broad categories:
-
-* Unix platforms require `cc` to be the C compiler. This can be found by
-  installing cc/clang on Linux distributions and Xcode on macOS, for example.
-* Windows platforms targeting MSVC (e.g. your target triple ends in `-msvc`)
-  require Visual Studio to be installed. `cc-rs` attempts to locate it, and
-  if it fails, `cl.exe` is expected to be available in `PATH`. This can be
-  set up by running the appropriate developer tools shell.
-* Windows platforms targeting MinGW (e.g. your target triple ends in `-gnu`)
-  require `cc` to be available in `PATH`. We recommend the
-  [MinGW-w64](https://www.mingw-w64.org/) distribution, which is using the
-  [Win-builds](http://win-builds.org/) installation system.
-  You may also acquire it via
-  [MSYS2](https://www.msys2.org/), as explained [here][msys2-help].  Make sure
-  to install the appropriate architecture corresponding to your installation of
-  rustc. GCC from older [MinGW](http://www.mingw.org/) project is compatible
-  only with 32-bit rust compiler.
-
-[msys2-help]: https://github.com/rust-lang/rust#building-on-windows
-
-## C++ support
-
-`cc-rs` supports C++ libraries compilation by using the `cpp` method on
-`Build`:
-
-```rust,no_run
-fn main() {
-    cc::Build::new()
-        .cpp(true) // Switch to C++ library compilation.
-        .file("foo.cpp")
-        .compile("foo");
-}
-```
-
-For C++ libraries, the `CXX` and `CXXFLAGS` environment variables are used instead of `CC` and `CFLAGS`.
-
-The C++ standard library may be linked to the crate target. By default it's `libc++` for macOS, FreeBSD, and OpenBSD, `libc++_shared` for Android, nothing for MSVC, and `libstdc++` for anything else. It can be changed in one of two ways:
-
-1. by using the `cpp_link_stdlib` method on `Build`:
-    ```rust,no-run
-    fn main() {
-        cc::Build::new()
-            .cpp(true)
-            .file("foo.cpp")
-            .cpp_link_stdlib("stdc++") // use libstdc++
-            .compile("foo");
-    }
-    ```
-2. by setting the `CXXSTDLIB` environment variable.
-
-In particular, for Android you may want to [use `c++_static` if you have at most one shared library](https://developer.android.com/ndk/guides/cpp-support).
-
-Remember that C++ does name mangling so `extern "C"` might be required to enable Rust linker to find your functions.
-
-## CUDA C++ support
-
-`cc-rs` also supports compiling CUDA C++ libraries by using the `cuda` method
-on `Build`:
-
-```rust,no_run
-fn main() {
-    cc::Build::new()
-        // Switch to CUDA C++ library compilation using NVCC.
-        .cuda(true)
-        .cudart("static")
-        // Generate code for Maxwell (GTX 970, 980, 980 Ti, Titan X).
-        .flag("-gencode").flag("arch=compute_52,code=sm_52")
-        // Generate code for Maxwell (Jetson TX1).
-        .flag("-gencode").flag("arch=compute_53,code=sm_53")
-        // Generate code for Pascal (GTX 1070, 1080, 1080 Ti, Titan Xp).
-        .flag("-gencode").flag("arch=compute_61,code=sm_61")
-        // Generate code for Pascal (Tesla P100).
-        .flag("-gencode").flag("arch=compute_60,code=sm_60")
-        // Generate code for Pascal (Jetson TX2).
-        .flag("-gencode").flag("arch=compute_62,code=sm_62")
-        // Generate code in parallel
-        .flag("-t0")
-        .file("bar.cu")
-        .compile("bar");
-}
-```
+A library for [Cargo build scripts](https://doc.rust-lang.org/cargo/reference/build-scripts.html)
+to compile a set of C/C++/assembly/CUDA files into a static archive for Cargo
+to link into the crate being built. This crate does not compile code itself;
+it calls out to the default compiler for the platform. This crate will
+automatically detect situations such as cross compilation and
+various environment variables and will build code appropriately.
+
+Refer to the [documentation](https://docs.rs/cc) for detailed usage instructions.
 
 ## License
 
diff --git a/dev-tools/gen-windows-sys-binding/Cargo.toml b/dev-tools/gen-windows-sys-binding/Cargo.toml
new file mode 100644
index 0000000..9166030
--- /dev/null
+++ b/dev-tools/gen-windows-sys-binding/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "gen-windows-sys-binding"
+version = "0.0.0"
+edition = "2018"
+publish = false
+
+[dependencies]
+windows-bindgen = "0.53"
+tempfile = "3"
diff --git a/dev-tools/gen-windows-sys-binding/src/main.rs b/dev-tools/gen-windows-sys-binding/src/main.rs
new file mode 100644
index 0000000..2a5444f
--- /dev/null
+++ b/dev-tools/gen-windows-sys-binding/src/main.rs
@@ -0,0 +1,64 @@
+//! Adapted from
+//! https://github.com/rust-lang/rust/blob/master/src/tools/generate-windows-sys/src/main.rs
+
+use std::{
+    fs,
+    io::{self, Read, Write},
+};
+
+/// This is printed to the file before the rest of the contents.
+const PRELUDE: &str = r#"// This file is autogenerated.
+//
+// To add bindings, edit windows_sys.lst then run:
+//
+// ```
+// cd generate-windows-sys/
+// cargo run
+// ```
+"#;
+
+fn main() -> io::Result<()> {
+    let manifest_dir = env!("CARGO_MANIFEST_DIR");
+    let temp_file = tempfile::Builder::new()
+        .suffix(".rs")
+        .tempfile()
+        .expect("failed to create temp file");
+
+    // Common args to windows_bindgen.
+    let mut args = vec![
+        "--config",
+        "std",
+        "flatten",
+        "--out",
+        temp_file.path().to_str().unwrap(),
+        "--filter",
+    ];
+
+    // Append the list of APIs
+    let buffer = fs::read_to_string(format!("{manifest_dir}/windows_sys.list"))
+        .expect("failed to read list");
+    args.extend(buffer.lines().filter_map(|line| {
+        let line = line.trim();
+        if line.is_empty() || line.starts_with("//") {
+            None
+        } else {
+            Some(line)
+        }
+    }));
+
+    // Generate bindings.
+    windows_bindgen::bindgen(&args).expect("running bindgen failed");
+
+    let mut bindings = String::new();
+    fs::File::open(temp_file.path())
+        .expect("failed to open temp windows_sys.rs")
+        .read_to_string(&mut bindings)
+        .expect("failed to read temp windows_sys.rs");
+
+    let mut f = fs::File::create(format!("{manifest_dir}/../../src/windows/windows_sys.rs"))
+        .expect("failed to create windows_sys.rs");
+    f.write_all(PRELUDE.as_bytes())?;
+    f.write_all(bindings.as_bytes())?;
+
+    Ok(())
+}
diff --git a/dev-tools/gen-windows-sys-binding/windows_sys.list b/dev-tools/gen-windows-sys-binding/windows_sys.list
new file mode 100644
index 0000000..327bed8
--- /dev/null
+++ b/dev-tools/gen-windows-sys-binding/windows_sys.list
@@ -0,0 +1,47 @@
+Windows.Win32.Foundation.FILETIME
+Windows.Win32.Foundation.ERROR_NO_MORE_ITEMS
+Windows.Win32.Foundation.ERROR_SUCCESS
+Windows.Win32.Foundation.SysFreeString
+Windows.Win32.Foundation.SysStringLen
+Windows.Win32.Foundation.S_FALSE
+Windows.Win32.Foundation.S_OK
+Windows.Win32.Foundation.FALSE
+Windows.Win32.Foundation.HANDLE
+Windows.Win32.Foundation.WAIT_OBJECT_0
+Windows.Win32.Foundation.WAIT_TIMEOUT
+Windows.Win32.Foundation.WAIT_FAILED
+Windows.Win32.Foundation.WAIT_ABANDONED
+Windows.Win32.Foundation.FreeLibrary
+
+Windows.Win32.System.Com.SAFEARRAY
+Windows.Win32.System.Com.SAFEARRAYBOUND
+Windows.Win32.System.Com.CLSCTX_ALL
+Windows.Win32.System.Com.COINIT_MULTITHREADED
+Windows.Win32.System.Com.CoCreateInstance
+Windows.Win32.System.Com.CoInitializeEx
+
+Windows.Win32.System.LibraryLoader.GetProcAddress
+Windows.Win32.System.LibraryLoader.LoadLibraryA
+
+Windows.Win32.System.Pipes.PeekNamedPipe
+
+Windows.Win32.System.Registry.RegCloseKey
+Windows.Win32.System.Registry.RegEnumKeyExW
+Windows.Win32.System.Registry.RegOpenKeyExW
+Windows.Win32.System.Registry.RegQueryValueExW
+Windows.Win32.System.Registry.HKEY
+Windows.Win32.System.Registry.HKEY_LOCAL_MACHINE
+Windows.Win32.System.Registry.KEY_READ
+Windows.Win32.System.Registry.KEY_WOW64_32KEY
+Windows.Win32.System.Registry.REG_SZ
+
+Windows.Win32.System.SystemInformation.IMAGE_FILE_MACHINE_AMD64
+
+Windows.Win32.System.Threading.GetMachineTypeAttributes
+Windows.Win32.System.Threading.ReleaseSemaphore
+Windows.Win32.System.Threading.WaitForSingleObject
+Windows.Win32.System.Threading.SEMAPHORE_MODIFY_STATE
+Windows.Win32.System.Threading.THREAD_SYNCHRONIZE
+Windows.Win32.System.Threading.UserEnabled
+
+Windows.Win32.System.WindowsProgramming.OpenSemaphoreA
diff --git a/src/bin/gcc-shim.rs b/src/bin/gcc-shim.rs
index e5b5372..f0d219f 100644
--- a/src/bin/gcc-shim.rs
+++ b/src/bin/gcc-shim.rs
@@ -1,3 +1,6 @@
+//! This bin target is only used for this crate's tests.
+//! It is not intended for users and is not published with the library code to crates.io.
+
 #![cfg_attr(test, allow(dead_code))]
 
 use std::env;
diff --git a/src/command_helpers.rs b/src/command_helpers.rs
new file mode 100644
index 0000000..1a01815
--- /dev/null
+++ b/src/command_helpers.rs
@@ -0,0 +1,416 @@
+//! Miscellaneous helpers for running commands
+
+use std::{
+    collections::hash_map,
+    ffi::OsString,
+    fmt::Display,
+    fs,
+    hash::Hasher,
+    io::{self, Read, Write},
+    path::Path,
+    process::{Child, ChildStderr, Command, Stdio},
+    sync::Arc,
+};
+
+use crate::{Error, ErrorKind, Object};
+
+#[derive(Clone, Debug)]
+pub(crate) struct CargoOutput {
+    pub(crate) metadata: bool,
+    pub(crate) warnings: bool,
+}
+
+impl CargoOutput {
+    pub(crate) const fn new() -> Self {
+        Self {
+            metadata: true,
+            warnings: true,
+        }
+    }
+
+    pub(crate) fn print_metadata(&self, s: &dyn Display) {
+        if self.metadata {
+            println!("{}", s);
+        }
+    }
+
+    pub(crate) fn print_warning(&self, arg: &dyn Display) {
+        if self.warnings {
+            println!("cargo:warning={}", arg);
+        }
+    }
+
+    fn stdio_for_warnings(&self) -> Stdio {
+        if self.warnings {
+            Stdio::piped()
+        } else {
+            Stdio::null()
+        }
+    }
+}
+
+pub(crate) struct StderrForwarder {
+    inner: Option<(ChildStderr, Vec<u8>)>,
+    #[cfg(feature = "parallel")]
+    is_non_blocking: bool,
+    #[cfg(feature = "parallel")]
+    bytes_available_failed: bool,
+}
+
+const MIN_BUFFER_CAPACITY: usize = 100;
+
+impl StderrForwarder {
+    pub(crate) fn new(child: &mut Child) -> Self {
+        Self {
+            inner: child
+                .stderr
+                .take()
+                .map(|stderr| (stderr, Vec::with_capacity(MIN_BUFFER_CAPACITY))),
+            #[cfg(feature = "parallel")]
+            is_non_blocking: false,
+            #[cfg(feature = "parallel")]
+            bytes_available_failed: false,
+        }
+    }
+
+    #[allow(clippy::uninit_vec)]
+    fn forward_available(&mut self) -> bool {
+        if let Some((stderr, buffer)) = self.inner.as_mut() {
+            loop {
+                let old_data_end = buffer.len();
+
+                // For non-blocking we check to see if there is data available, so we should try to
+                // read at least that much. For blocking, always read at least the minimum amount.
+                #[cfg(not(feature = "parallel"))]
+                let to_reserve = MIN_BUFFER_CAPACITY;
+                #[cfg(feature = "parallel")]
+                let to_reserve = if self.is_non_blocking && !self.bytes_available_failed {
+                    match crate::parallel::stderr::bytes_available(stderr) {
+                        #[cfg(windows)]
+                        Ok(0) => return false,
+                        #[cfg(unix)]
+                        Ok(0) => {
+                            // On Unix, depending on the implementation, we may sometimes get 0 in a
+                            // loop (either there is data available or the pipe is broken), so
+                            // continue with the non-blocking read anyway.
+                            MIN_BUFFER_CAPACITY
+                        }
+                        #[cfg(windows)]
+                        Err(_) => {
+                            // On Windows, if we get an error then the pipe is broken, so flush
+                            // the buffer and bail.
+                            if !buffer.is_empty() {
+                                write_warning(&buffer[..]);
+                            }
+                            self.inner = None;
+                            return true;
+                        }
+                        #[cfg(unix)]
+                        Err(_) => {
+                            // On Unix, depending on the implementation, we may get spurious
+                            // errors so make a note not to use bytes_available again and try
+                            // the non-blocking read anyway.
+                            self.bytes_available_failed = true;
+                            MIN_BUFFER_CAPACITY
+                        }
+                        Ok(bytes_available) => MIN_BUFFER_CAPACITY.max(bytes_available),
+                    }
+                } else {
+                    MIN_BUFFER_CAPACITY
+                };
+                buffer.reserve(to_reserve);
+
+                // SAFETY: 1) the length is set to the capacity, so we are never using memory beyond
+                // the underlying buffer and 2) we always call `truncate` below to set the len back
+                // to the initialized data.
+                unsafe {
+                    buffer.set_len(buffer.capacity());
+                }
+                match stderr.read(&mut buffer[old_data_end..]) {
+                    Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
+                        // No data currently, yield back.
+                        buffer.truncate(old_data_end);
+                        return false;
+                    }
+                    Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {
+                        // Interrupted, try again.
+                        buffer.truncate(old_data_end);
+                    }
+                    Ok(0) | Err(_) => {
+                        // End of stream: flush remaining data and bail.
+                        if old_data_end > 0 {
+                            write_warning(&buffer[..old_data_end]);
+                        }
+                        self.inner = None;
+                        return true;
+                    }
+                    Ok(bytes_read) => {
+                        buffer.truncate(old_data_end + bytes_read);
+                        let mut consumed = 0;
+                        for line in buffer.split_inclusive(|&b| b == b'\n') {
+                            // Only forward complete lines, leave the rest in the buffer.
+                            if let Some((b'\n', line)) = line.split_last() {
+                                consumed += line.len() + 1;
+                                write_warning(line);
+                            }
+                        }
+                        buffer.drain(..consumed);
+                    }
+                }
+            }
+        } else {
+            true
+        }
+    }
+
+    #[cfg(feature = "parallel")]
+    pub(crate) fn set_non_blocking(&mut self) -> Result<(), Error> {
+        assert!(!self.is_non_blocking);
+
+        #[cfg(unix)]
+        if let Some((stderr, _)) = self.inner.as_ref() {
+            crate::parallel::stderr::set_non_blocking(stderr)?;
+        }
+
+        self.is_non_blocking = true;
+        Ok(())
+    }
+
+    #[cfg(feature = "parallel")]
+    fn forward_all(&mut self) {
+        while !self.forward_available() {}
+    }
+
+    #[cfg(not(feature = "parallel"))]
+    fn forward_all(&mut self) {
+        let forward_result = self.forward_available();
+        assert!(forward_result, "Should have consumed all data");
+    }
+}
+
+fn write_warning(line: &[u8]) {
+    let stdout = io::stdout();
+    let mut stdout = stdout.lock();
+    stdout.write_all(b"cargo:warning=").unwrap();
+    stdout.write_all(line).unwrap();
+    stdout.write_all(b"\n").unwrap();
+}
+
+fn wait_on_child(
+    cmd: &Command,
+    program: &str,
+    child: &mut Child,
+    cargo_output: &CargoOutput,
+) -> Result<(), Error> {
+    StderrForwarder::new(child).forward_all();
+
+    let status = match child.wait() {
+        Ok(s) => s,
+        Err(e) => {
+            return Err(Error::new(
+                ErrorKind::ToolExecError,
+                format!(
+                    "Failed to wait on spawned child process, command {:?} with args {:?}: {}.",
+                    cmd, program, e
+                ),
+            ));
+        }
+    };
+
+    cargo_output.print_warning(&status);
+
+    if status.success() {
+        Ok(())
+    } else {
+        Err(Error::new(
+            ErrorKind::ToolExecError,
+            format!(
+                "Command {:?} with args {:?} did not execute successfully (status code {}).",
+                cmd, program, status
+            ),
+        ))
+    }
+}
+
+/// Find the destination object path for each file in the input source files,
+/// and store them in the output Object.
+pub(crate) fn objects_from_files(files: &[Arc<Path>], dst: &Path) -> Result<Vec<Object>, Error> {
+    let mut objects = Vec::with_capacity(files.len());
+    for file in files {
+        let basename = file
+            .file_name()
+            .ok_or_else(|| {
+                Error::new(
+                    ErrorKind::InvalidArgument,
+                    "No file_name for object file path!",
+                )
+            })?
+            .to_string_lossy();
+        let dirname = file
+            .parent()
+            .ok_or_else(|| {
+                Error::new(
+                    ErrorKind::InvalidArgument,
+                    "No parent for object file path!",
+                )
+            })?
+            .to_string_lossy();
+
+        // Hash the dirname. This should prevent conflicts if we have multiple
+        // object files with the same filename in different subfolders.
+        let mut hasher = hash_map::DefaultHasher::new();
+        hasher.write(dirname.to_string().as_bytes());
+        let obj = dst
+            .join(format!("{:016x}-{}", hasher.finish(), basename))
+            .with_extension("o");
+
+        match obj.parent() {
+            Some(s) => fs::create_dir_all(s)?,
+            None => {
+                return Err(Error::new(
+                    ErrorKind::InvalidArgument,
+                    "dst is an invalid path with no parent",
+                ));
+            }
+        };
+
+        objects.push(Object::new(file.to_path_buf(), obj));
+    }
+
+    Ok(objects)
+}
+
+pub(crate) fn run(
+    cmd: &mut Command,
+    program: &str,
+    cargo_output: &CargoOutput,
+) -> Result<(), Error> {
+    let mut child = spawn(cmd, program, cargo_output)?;
+    wait_on_child(cmd, program, &mut child, cargo_output)
+}
+
+pub(crate) fn run_output(
+    cmd: &mut Command,
+    program: &str,
+    cargo_output: &CargoOutput,
+) -> Result<Vec<u8>, Error> {
+    cmd.stdout(Stdio::piped());
+
+    let mut child = spawn(cmd, program, cargo_output)?;
+
+    let mut stdout = vec![];
+    child
+        .stdout
+        .take()
+        .unwrap()
+        .read_to_end(&mut stdout)
+        .unwrap();
+
+    wait_on_child(cmd, program, &mut child, cargo_output)?;
+
+    Ok(stdout)
+}
+
+pub(crate) fn spawn(
+    cmd: &mut Command,
+    program: &str,
+    cargo_output: &CargoOutput,
+) -> Result<Child, Error> {
+    struct ResetStderr<'cmd>(&'cmd mut Command);
+
+    impl Drop for ResetStderr<'_> {
+        fn drop(&mut self) {
+            // Reset stderr to default to release pipe_writer so that print thread will
+            // not block forever.
+            self.0.stderr(Stdio::inherit());
+        }
+    }
+
+    cargo_output.print_warning(&format_args!("running: {:?}", cmd));
+
+    let cmd = ResetStderr(cmd);
+    let child = cmd.0.stderr(cargo_output.stdio_for_warnings()).spawn();
+    match child {
+        Ok(child) => Ok(child),
+        Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
+            let extra = if cfg!(windows) {
+                " (see https://github.com/rust-lang/cc-rs#compile-time-requirements \
+for help)"
+            } else {
+                ""
+            };
+            Err(Error::new(
+                ErrorKind::ToolNotFound,
+                format!("Failed to find tool. Is `{}` installed?{}", program, extra),
+            ))
+        }
+        Err(e) => Err(Error::new(
+            ErrorKind::ToolExecError,
+            format!(
+                "Command {:?} with args {:?} failed to start: {:?}",
+                cmd.0, program, e
+            ),
+        )),
+    }
+}
+
+pub(crate) fn command_add_output_file(
+    cmd: &mut Command,
+    dst: &Path,
+    cuda: bool,
+    msvc: bool,
+    clang: bool,
+    gnu: bool,
+    is_asm: bool,
+    is_arm: bool,
+) {
+    if msvc && !clang && !gnu && !cuda && !(is_asm && is_arm) {
+        let mut s = OsString::from("-Fo");
+        s.push(dst);
+        cmd.arg(s);
+    } else {
+        cmd.arg("-o").arg(dst);
+    }
+}
+
+#[cfg(feature = "parallel")]
+pub(crate) fn try_wait_on_child(
+    cmd: &Command,
+    program: &str,
+    child: &mut Child,
+    stdout: &mut dyn io::Write,
+    stderr_forwarder: &mut StderrForwarder,
+) -> Result<Option<()>, Error> {
+    stderr_forwarder.forward_available();
+
+    match child.try_wait() {
+        Ok(Some(status)) => {
+            stderr_forwarder.forward_all();
+
+            let _ = writeln!(stdout, "{}", status);
+
+            if status.success() {
+                Ok(Some(()))
+            } else {
+                Err(Error::new(
+                    ErrorKind::ToolExecError,
+                    format!(
+                        "Command {:?} with args {:?} did not execute successfully (status code {}).",
+                            cmd, program, status
+                    ),
+                ))
+            }
+        }
+        Ok(None) => Ok(None),
+        Err(e) => {
+            stderr_forwarder.forward_all();
+            Err(Error::new(
+                ErrorKind::ToolExecError,
+                format!(
+                    "Failed to wait on spawned child process, command {:?} with args {:?}: {}.",
+                    cmd, program, e
+                ),
+            ))
+        }
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 2a192df..d77f3f2 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,52 +1,216 @@
-//! A library for build scripts to compile custom C code
+//! A library for [Cargo build scripts](https://doc.rust-lang.org/cargo/reference/build-scripts.html)
+//! to compile a set of C/C++/assembly/CUDA files into a static archive for Cargo
+//! to link into the crate being built. This crate does not compile code itself;
+//! it calls out to the default compiler for the platform. This crate will
+//! automatically detect situations such as cross compilation and
+//! [various environment variables](#external-configuration-via-environment-variables) and will build code appropriately.
 //!
-//! This library is intended to be used as a `build-dependencies` entry in
-//! `Cargo.toml`:
+//! # Example
+//!
+//! First, you'll want to both add a build script for your crate (`build.rs`) and
+//! also add this crate to your `Cargo.toml` via:
 //!
 //! ```toml
 //! [build-dependencies]
 //! cc = "1.0"
 //! ```
 //!
-//! The purpose of this crate is to provide the utility functions necessary to
-//! compile C code into a static archive which is then linked into a Rust crate.
-//! Configuration is available through the `Build` struct.
+//! Next up, you'll want to write a build script like so:
+//!
+//! ```rust,no_run
+//! // build.rs
+//!
+//! fn main() {
+//!     cc::Build::new()
+//!         .file("foo.c")
+//!         .file("bar.c")
+//!         .compile("foo");
+//! }
+//! ```
+//!
+//! And that's it! Running `cargo build` should take care of the rest and your Rust
+//! application will now have the C files `foo.c` and `bar.c` compiled into a file
+//! named `libfoo.a`. If the C files contain
+//!
+//! ```c
+//! void foo_function(void) { ... }
+//! ```
+//!
+//! and
+//!
+//! ```c
+//! int32_t bar_function(int32_t x) { ... }
+//! ```
+//!
+//! you can call them from Rust by declaring them in
+//! your Rust code like so:
+//!
+//! ```rust,no_run
+//! extern "C" {
+//!     fn foo_function();
+//!     fn bar_function(x: i32) -> i32;
+//! }
+//!
+//! pub fn call() {
+//!     unsafe {
+//!         foo_function();
+//!         bar_function(42);
+//!     }
+//! }
+//!
+//! fn main() {
+//!     call();
+//! }
+//! ```
+//!
+//! See [the Rustonomicon](https://doc.rust-lang.org/nomicon/ffi.html) for more details.
+//!
+//! # External configuration via environment variables
+//!
+//! To control the programs and flags used for building, the builder can set a
+//! number of different environment variables.
+//!
+//! * `CFLAGS` - a series of space separated flags passed to compilers. Note that
+//! individual flags cannot currently contain spaces, so doing
+//! something like: `-L=foo\ bar` is not possible.
+//! * `CC` - the actual C compiler used. Note that this is used as an exact
+//! executable name, so (for example) no extra flags can be passed inside
+//! this variable, and the builder must ensure that there aren't any
+//! trailing spaces. This compiler must understand the `-c` flag. For
+//! certain `TARGET`s, it also is assumed to know about other flags (most
+//! common is `-fPIC`).
+//! * `AR` - the `ar` (archiver) executable to use to build the static library.
+//! * `CRATE_CC_NO_DEFAULTS` - the default compiler flags may cause conflicts in
+//! some cross compiling scenarios. Setting this variable
+//! will disable the generation of default compiler
+//! flags.
+//! * `CXX...` - see [C++ Support](#c-support).
 //!
-//! This crate will automatically detect situations such as cross compilation or
-//! other environment variables set by Cargo and will build code appropriately.
+//! Furthermore, projects using this crate may specify custom environment variables
+//! to be inspected, for example via the `Build::try_flags_from_environment`
+//! function. Consult the project’s own documentation or its use of the `cc` crate
+//! for any additional variables it may use.
 //!
-//! The crate is not limited to C code, it can accept any source code that can
-//! be passed to a C or C++ compiler. As such, assembly files with extensions
-//! `.s` (gcc/clang) and `.asm` (MSVC) can also be compiled.
+//! Each of these variables can also be supplied with certain prefixes and suffixes,
+//! in the following prioritized order:
 //!
-//! [`Build`]: struct.Build.html
+//!   1. `<var>_<target>` - for example, `CC_x86_64-unknown-linux-gnu`
+//!   2. `<var>_<target_with_underscores>` - for example, `CC_x86_64_unknown_linux_gnu`
+//!   3. `<build-kind>_<var>` - for example, `HOST_CC` or `TARGET_CFLAGS`
+//!   4. `<var>` - a plain `CC`, `AR` as above.
 //!
-//! # Parallelism
+//! If none of these variables exist, cc-rs uses built-in defaults.
 //!
-//! To parallelize computation, enable the `parallel` feature for the crate.
+//! In addition to the above optional environment variables, `cc-rs` has some
+//! functions with hard requirements on some variables supplied by [cargo's
+//! build-script driver][cargo] that it has the `TARGET`, `OUT_DIR`, `OPT_LEVEL`,
+//! and `HOST` variables.
+//!
+//! [cargo]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#inputs-to-the-build-script
+//!
+//! # Optional features
+//!
+//! ## Parallel
+//!
+//! Currently cc-rs supports parallel compilation (think `make -jN`) but this
+//! feature is turned off by default. To enable cc-rs to compile C/C++ in parallel,
+//! you can change your dependency to:
 //!
 //! ```toml
 //! [build-dependencies]
 //! cc = { version = "1.0", features = ["parallel"] }
 //! ```
-//! To specify the max number of concurrent compilation jobs, set the `NUM_JOBS`
-//! environment variable to the desired amount.
 //!
-//! Cargo will also set this environment variable when executed with the `-jN` flag.
+//! By default cc-rs will limit parallelism to `$NUM_JOBS`, or if not present it
+//! will limit it to the number of cpus on the machine. If you are using cargo,
+//! use `-jN` option of `build`, `test` and `run` commands as `$NUM_JOBS`
+//! is supplied by cargo.
+//!
+//! # Compile-time Requirements
+//!
+//! To work properly this crate needs access to a C compiler when the build script
+//! is being run. This crate does not ship a C compiler with it. The compiler
+//! required varies per platform, but there are three broad categories:
+//!
+//! * Unix platforms require `cc` to be the C compiler. This can be found by
+//! installing cc/clang on Linux distributions and Xcode on macOS, for example.
+//! * Windows platforms targeting MSVC (e.g. your target triple ends in `-msvc`)
+//! require Visual Studio to be installed. `cc-rs` attempts to locate it, and
+//! if it fails, `cl.exe` is expected to be available in `PATH`. This can be
+//! set up by running the appropriate developer tools shell.
+//! * Windows platforms targeting MinGW (e.g. your target triple ends in `-gnu`)
+//! require `cc` to be available in `PATH`. We recommend the
+//! [MinGW-w64](https://www.mingw-w64.org/) distribution, which is using the
+//! [Win-builds](http://win-builds.org/) installation system.
+//! You may also acquire it via
+//! [MSYS2](https://www.msys2.org/), as explained [here][msys2-help].  Make sure
+//! to install the appropriate architecture corresponding to your installation of
+//! rustc. GCC from older [MinGW](http://www.mingw.org/) project is compatible
+//! only with 32-bit rust compiler.
 //!
-//! # Examples
+//! [msys2-help]: https://github.com/rust-lang/rust#building-on-windows
 //!
-//! Use the `Build` struct to compile `src/foo.c`:
+//! # C++ support
 //!
-//! ```no_run
+//! `cc-rs` supports C++ libraries compilation by using the `cpp` method on
+//! `Build`:
+//!
+//! ```rust,no_run
 //! fn main() {
 //!     cc::Build::new()
-//!         .file("src/foo.c")
-//!         .define("FOO", Some("bar"))
-//!         .include("src")
+//!         .cpp(true) // Switch to C++ library compilation.
+//!         .file("foo.cpp")
 //!         .compile("foo");
 //! }
 //! ```
+//!
+//! For C++ libraries, the `CXX` and `CXXFLAGS` environment variables are used instead of `CC` and `CFLAGS`.
+//!
+//! The C++ standard library may be linked to the crate target. By default it's `libc++` for macOS, FreeBSD, and OpenBSD, `libc++_shared` for Android, nothing for MSVC, and `libstdc++` for anything else. It can be changed in one of two ways:
+//!
+//! 1. by using the `cpp_link_stdlib` method on `Build`:
+//! ```rust,no_run
+//! fn main() {
+//!     cc::Build::new()
+//!         .cpp(true)
+//!         .file("foo.cpp")
+//!         .cpp_link_stdlib("stdc++") // use libstdc++
+//!         .compile("foo");
+//! }
+//! ```
+//! 2. by setting the `CXXSTDLIB` environment variable.
+//!
+//! In particular, for Android you may want to [use `c++_static` if you have at most one shared library](https://developer.android.com/ndk/guides/cpp-support).
+//!
+//! Remember that C++ does name mangling so `extern "C"` might be required to enable Rust linker to find your functions.
+//!
+//! # CUDA C++ support
+//!
+//! `cc-rs` also supports compiling CUDA C++ libraries by using the `cuda` method
+//! on `Build`:
+//!
+//! ```rust,no_run
+//! fn main() {
+//!     cc::Build::new()
+//!         // Switch to CUDA C++ library compilation using NVCC.
+//!         .cuda(true)
+//!         .cudart("static")
+//!         // Generate code for Maxwell (GTX 970, 980, 980 Ti, Titan X).
+//!         .flag("-gencode").flag("arch=compute_52,code=sm_52")
+//!         // Generate code for Maxwell (Jetson TX1).
+//!         .flag("-gencode").flag("arch=compute_53,code=sm_53")
+//!         // Generate code for Pascal (GTX 1070, 1080, 1080 Ti, Titan Xp).
+//!         .flag("-gencode").flag("arch=compute_61,code=sm_61")
+//!         // Generate code for Pascal (Tesla P100).
+//!         .flag("-gencode").flag("arch=compute_60,code=sm_60")
+//!         // Generate code for Pascal (Jetson TX2).
+//!         .flag("-gencode").flag("arch=compute_62,code=sm_62")
+//!         // Generate code in parallel
+//!         .flag("-t0")
+//!         .file("bar.cu")
+//!         .compile("bar");
+//! }
+//! ```
 
 #![doc(html_root_url = "https://docs.rs/cc/1.0")]
 #![cfg_attr(test, deny(warnings))]
@@ -54,35 +218,31 @@
 #![deny(missing_docs)]
 
 use std::borrow::Cow;
-use std::collections::{hash_map, HashMap};
+use std::collections::HashMap;
 use std::env;
 use std::ffi::{OsStr, OsString};
 use std::fmt::{self, Display, Formatter};
-use std::fs::{self, File};
-use std::hash::Hasher;
-use std::io::{self, BufRead, BufReader, Read, Write};
+use std::fs;
+use std::io::{self, Write};
 use std::path::{Component, Path, PathBuf};
-use std::process::{Child, Command, Stdio};
+#[cfg(feature = "parallel")]
+use std::process::Child;
+use std::process::Command;
 use std::sync::{Arc, Mutex};
-use std::thread::{self, JoinHandle};
-
-mod os_pipe;
-
-// These modules are all glue to support reading the MSVC version from
-// the registry and from COM interfaces
-#[cfg(windows)]
-mod registry;
-#[cfg(windows)]
-#[macro_use]
-mod winapi;
-#[cfg(windows)]
-mod com;
-#[cfg(windows)]
-mod setup_config;
-#[cfg(windows)]
-mod vs_instances;
-
-pub mod windows_registry;
+
+#[cfg(feature = "parallel")]
+mod parallel;
+mod windows;
+// Regardless of whether this should be in this crate's public API,
+// it has been since 2015, so don't break it.
+pub use windows::find_tools as windows_registry;
+
+mod command_helpers;
+use command_helpers::*;
+
+mod tool;
+pub use tool::Tool;
+use tool::ToolFamily;
 
 /// A builder for compilation of a native library.
 ///
@@ -106,6 +266,7 @@ pub struct Build {
     cpp_set_stdlib: Option<Arc<str>>,
     cuda: bool,
     cudart: Option<Arc<str>>,
+    std: Option<Arc<str>>,
     target: Option<Arc<str>>,
     host: Option<Arc<str>>,
     out_dir: Option<Arc<Path>>,
@@ -116,7 +277,7 @@ pub struct Build {
     compiler: Option<Arc<Path>>,
     archiver: Option<Arc<Path>>,
     ranlib: Option<Arc<Path>>,
-    cargo_metadata: bool,
+    cargo_output: CargoOutput,
     link_lib_modifiers: Vec<Arc<str>>,
     pic: Option<bool>,
     use_plt: Option<bool>,
@@ -128,7 +289,9 @@ pub struct Build {
     extra_warnings: Option<bool>,
     env_cache: Arc<Mutex<HashMap<String, Option<Arc<str>>>>>,
     apple_sdk_root_cache: Arc<Mutex<HashMap<String, OsString>>>,
+    apple_versions_cache: Arc<Mutex<HashMap<String, String>>>,
     emit_rerun_if_env_changed: bool,
+    cached_compiler_family: Arc<Mutex<HashMap<Box<Path>, ToolFamily>>>,
 }
 
 /// Represents the types of errors that may occur while using cc-rs.
@@ -180,97 +343,6 @@ impl Display for Error {
 
 impl std::error::Error for Error {}
 
-/// Configuration used to represent an invocation of a C compiler.
-///
-/// This can be used to figure out what compiler is in use, what the arguments
-/// to it are, and what the environment variables look like for the compiler.
-/// This can be used to further configure other build systems (e.g. forward
-/// along CC and/or CFLAGS) or the `to_command` method can be used to run the
-/// compiler itself.
-#[derive(Clone, Debug)]
-pub struct Tool {
-    path: PathBuf,
-    cc_wrapper_path: Option<PathBuf>,
-    cc_wrapper_args: Vec<OsString>,
-    args: Vec<OsString>,
-    env: Vec<(OsString, OsString)>,
-    family: ToolFamily,
-    cuda: bool,
-    removed_args: Vec<OsString>,
-}
-
-/// Represents the family of tools this tool belongs to.
-///
-/// Each family of tools differs in how and what arguments they accept.
-///
-/// Detection of a family is done on best-effort basis and may not accurately reflect the tool.
-#[derive(Copy, Clone, Debug, PartialEq)]
-enum ToolFamily {
-    /// Tool is GNU Compiler Collection-like.
-    Gnu,
-    /// Tool is Clang-like. It differs from the GCC in a sense that it accepts superset of flags
-    /// and its cross-compilation approach is different.
-    Clang,
-    /// Tool is the MSVC cl.exe.
-    Msvc { clang_cl: bool },
-}
-
-impl ToolFamily {
-    /// What the flag to request debug info for this family of tools look like
-    fn add_debug_flags(&self, cmd: &mut Tool, dwarf_version: Option<u32>) {
-        match *self {
-            ToolFamily::Msvc { .. } => {
-                cmd.push_cc_arg("-Z7".into());
-            }
-            ToolFamily::Gnu | ToolFamily::Clang => {
-                cmd.push_cc_arg(
-                    dwarf_version
-                        .map_or_else(|| "-g".into(), |v| format!("-gdwarf-{}", v))
-                        .into(),
-                );
-            }
-        }
-    }
-
-    /// What the flag to force frame pointers.
-    fn add_force_frame_pointer(&self, cmd: &mut Tool) {
-        match *self {
-            ToolFamily::Gnu | ToolFamily::Clang => {
-                cmd.push_cc_arg("-fno-omit-frame-pointer".into());
-            }
-            _ => (),
-        }
-    }
-
-    /// What the flags to enable all warnings
-    fn warnings_flags(&self) -> &'static str {
-        match *self {
-            ToolFamily::Msvc { .. } => "-W4",
-            ToolFamily::Gnu | ToolFamily::Clang => "-Wall",
-        }
-    }
-
-    /// What the flags to enable extra warnings
-    fn extra_warnings_flags(&self) -> Option<&'static str> {
-        match *self {
-            ToolFamily::Msvc { .. } => None,
-            ToolFamily::Gnu | ToolFamily::Clang => Some("-Wextra"),
-        }
-    }
-
-    /// What the flag to turn warning into errors
-    fn warnings_to_errors_flag(&self) -> &'static str {
-        match *self {
-            ToolFamily::Msvc { .. } => "-WX",
-            ToolFamily::Gnu | ToolFamily::Clang => "-Werror",
-        }
-    }
-
-    fn verbose_stderr(&self) -> bool {
-        *self == ToolFamily::Clang
-    }
-}
-
 /// Represents an object.
 ///
 /// This is a source file -> object file pair.
@@ -283,7 +355,7 @@ struct Object {
 impl Object {
     /// Create a new source file -> object file pair.
     fn new(src: PathBuf, dst: PathBuf) -> Object {
-        Object { src: src, dst: dst }
+        Object { src, dst }
     }
 }
 
@@ -312,6 +384,7 @@ impl Build {
             cpp_set_stdlib: None,
             cuda: false,
             cudart: None,
+            std: None,
             target: None,
             host: None,
             out_dir: None,
@@ -322,7 +395,7 @@ impl Build {
             compiler: None,
             archiver: None,
             ranlib: None,
-            cargo_metadata: true,
+            cargo_output: CargoOutput::new(),
             link_lib_modifiers: Vec::new(),
             pic: None,
             use_plt: None,
@@ -332,7 +405,9 @@ impl Build {
             warnings_into_errors: false,
             env_cache: Arc::new(Mutex::new(HashMap::new())),
             apple_sdk_root_cache: Arc::new(Mutex::new(HashMap::new())),
+            apple_versions_cache: Arc::new(Mutex::new(HashMap::new())),
             emit_rerun_if_env_changed: true,
+            cached_compiler_family: Arc::default(),
         }
     }
 
@@ -423,6 +498,24 @@ impl Build {
         self
     }
 
+    /// Removes a compiler flag that was added by [`Build::flag`].
+    ///
+    /// Will not remove flags added by other means (default flags,
+    /// flags from env, and so on).
+    ///
+    /// # Example
+    /// ```
+    /// cc::Build::new()
+    ///     .file("src/foo.c")
+    ///     .flag("unwanted_flag")
+    ///     .remove_flag("unwanted_flag");
+    /// ```
+
+    pub fn remove_flag(&mut self, flag: &str) -> &mut Build {
+        self.flags.retain(|other_flag| &**other_flag != flag);
+        self
+    }
+
     /// Add a flag to the invocation of the ar
     ///
     /// # Example
@@ -501,6 +594,7 @@ impl Build {
         let host = self.get_host()?;
         let mut cfg = Build::new();
         cfg.flag(flag)
+            .cargo_metadata(self.cargo_output.metadata)
             .target(&target)
             .opt_level(0)
             .host(&host)
@@ -517,6 +611,11 @@ impl Build {
         if compiler.family.verbose_stderr() {
             compiler.remove_arg("-v".into());
         }
+        if compiler.family == ToolFamily::Clang {
+            // Avoid reporting that the arg is unsupported just because the
+            // compiler complains that it wasn't used.
+            compiler.push_cc_arg("-Wno-unused-command-line-argument".into());
+        }
 
         let mut cmd = compiler.to_command();
         let is_arm = target.contains("aarch64") || target.contains("arm");
@@ -533,11 +632,8 @@ impl Build {
             is_arm,
         );
 
-        // We need to explicitly tell msvc not to link and create an exe
-        // in the root directory of the crate
-        if target.contains("msvc") && !self.cuda {
-            cmd.arg("-c");
-        }
+        // Checking for compiler flags does not require linking
+        cmd.arg("-c");
 
         cmd.arg(&src);
 
@@ -665,7 +761,7 @@ impl Build {
     /// `true`.
     ///
     /// The name of the C++ standard library to link is decided by:
-    /// 1. If [cpp_link_stdlib](Build::cpp_link_stdlib) is set, use its value.
+    /// 1. If [`cpp_link_stdlib`](Build::cpp_link_stdlib) is set, use its value.
     /// 2. Else if the `CXXSTDLIB` environment variable is set, use its value.
     /// 3. Else the default is `libc++` for OS X and BSDs, `libc++_shared` for Android,
     /// `None` for MSVC and `libstdc++` for anything else.
@@ -680,7 +776,7 @@ impl Build {
     /// the most common compiler flags, e.g. `-std=c++17`, some project-specific
     /// flags might have to be prefixed with "-Xcompiler" flag, for example as
     /// `.flag("-Xcompiler").flag("-fpermissive")`. See the documentation for
-    /// `nvcc`, the CUDA compiler driver, at https://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/
+    /// `nvcc`, the CUDA compiler driver, at <https://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/>
     /// for more information.
     ///
     /// If enabled, this also implicitly enables C++ support.
@@ -706,6 +802,37 @@ impl Build {
         self
     }
 
+    /// Specify the C or C++ language standard version.
+    ///
+    /// These values are common to modern versions of GCC, Clang and MSVC:
+    /// - `c11` for ISO/IEC 9899:2011
+    /// - `c17` for ISO/IEC 9899:2018
+    /// - `c++14` for ISO/IEC 14882:2014
+    /// - `c++17` for ISO/IEC 14882:2017
+    /// - `c++20` for ISO/IEC 14882:2020
+    ///
+    /// Other values have less broad support, e.g. MSVC does not support `c++11`
+    /// (`c++14` is the minimum), `c89` (omit the flag instead) or `c99`.
+    ///
+    /// For compiling C++ code, you should also set `.cpp(true)`.
+    ///
+    /// The default is that no standard flag is passed to the compiler, so the
+    /// language version will be the compiler's default.
+    ///
+    /// # Example
+    ///
+    /// ```no_run
+    /// cc::Build::new()
+    ///     .file("src/modern.cpp")
+    ///     .cpp(true)
+    ///     .std("c++17")
+    ///     .compile("modern");
+    /// ```
+    pub fn std(&mut self, std: &str) -> &mut Build {
+        self.std = Some(std.into());
+        self
+    }
+
     /// Set warnings into errors flag.
     ///
     /// Disabled by default.
@@ -978,14 +1105,24 @@ impl Build {
     ///  - If `emit_rerun_if_env_changed` is not `false`, `rerun-if-env-changed=`*env*
     ///
     pub fn cargo_metadata(&mut self, cargo_metadata: bool) -> &mut Build {
-        self.cargo_metadata = cargo_metadata;
+        self.cargo_output.metadata = cargo_metadata;
+        self
+    }
+
+    /// Define whether compile warnings should be emitted for cargo. Defaults to
+    /// `true`.
+    ///
+    /// If disabled, compiler messages will not be printed.
+    /// Issues unrelated to the compilation will always produce cargo warnings regardless of this setting.
+    pub fn cargo_warnings(&mut self, cargo_warnings: bool) -> &mut Build {
+        self.cargo_output.warnings = cargo_warnings;
         self
     }
 
     /// Adds a native library modifier that will be added to the
     /// `rustc-link-lib=static:MODIFIERS=LIBRARY_NAME` metadata line
     /// emitted for cargo if `cargo_metadata` is enabled.
-    /// See https://doc.rust-lang.org/rustc/command-line-arguments.html#-l-link-the-generated-crate-to-a-native-library
+    /// See <https://doc.rust-lang.org/rustc/command-line-arguments.html#-l-link-the-generated-crate-to-a-native-library>
     /// for the list of modifiers accepted by rustc.
     pub fn link_lib_modifier(&mut self, link_lib_modifier: &str) -> &mut Build {
         self.link_lib_modifiers.push(link_lib_modifier.into());
@@ -1047,7 +1184,7 @@ impl Build {
 
     /// Run the compiler, generating the file `output`
     ///
-    /// This will return a result instead of panicing; see compile() for the complete description.
+    /// This will return a result instead of panicking; see compile() for the complete description.
     pub fn try_compile(&self, output: &str) -> Result<(), Error> {
         let mut output_components = Path::new(output).components();
         match (output_components.next(), output_components.next()) {
@@ -1065,66 +1202,24 @@ impl Build {
         } else {
             let mut gnu = String::with_capacity(5 + output.len());
             gnu.push_str("lib");
-            gnu.push_str(&output);
+            gnu.push_str(output);
             gnu.push_str(".a");
             (output, gnu)
         };
         let dst = self.get_out_dir()?;
 
-        let mut objects = Vec::new();
-        for file in self.files.iter() {
-            let obj = if file.has_root() || file.components().any(|x| x == Component::ParentDir) {
-                // If `file` is an absolute path or might not be usable directly as a suffix due to
-                // using "..", use the `basename` prefixed with the `dirname`'s hash to ensure name
-                // uniqueness.
-                let basename = file
-                    .file_name()
-                    .ok_or_else(|| Error::new(ErrorKind::InvalidArgument, "file_name() failure"))?
-                    .to_string_lossy();
-                let dirname = file
-                    .parent()
-                    .ok_or_else(|| Error::new(ErrorKind::InvalidArgument, "parent() failure"))?
-                    .to_string_lossy();
-                let mut hasher = hash_map::DefaultHasher::new();
-                hasher.write(dirname.to_string().as_bytes());
-                dst.join(format!("{:016x}-{}", hasher.finish(), basename))
-                    .with_extension("o")
-            } else {
-                dst.join(file).with_extension("o")
-            };
-            let obj = if !obj.starts_with(&dst) {
-                dst.join(obj.file_name().ok_or_else(|| {
-                    Error::new(ErrorKind::IOError, "Getting object file details failed.")
-                })?)
-            } else {
-                obj
-            };
-
-            match obj.parent() {
-                Some(s) => fs::create_dir_all(s)?,
-                None => {
-                    return Err(Error::new(
-                        ErrorKind::IOError,
-                        "Getting object file details failed.",
-                    ));
-                }
-            };
-
-            objects.push(Object::new(file.to_path_buf(), obj));
-        }
-
-        let print = PrintThread::new()?;
+        let objects = objects_from_files(&self.files, &dst)?;
 
-        self.compile_objects(&objects, &print)?;
-        self.assemble(lib_name, &dst.join(gnu_lib_name), &objects, &print)?;
+        self.compile_objects(&objects)?;
+        self.assemble(lib_name, &dst.join(gnu_lib_name), &objects)?;
 
         if self.get_target()?.contains("msvc") {
             let compiler = self.get_base_compiler()?;
             let atlmfc_lib = compiler
                 .env()
                 .iter()
-                .find(|&&(ref var, _)| var.as_os_str() == OsStr::new("LIB"))
-                .and_then(|&(_, ref lib_paths)| {
+                .find(|&(var, _)| var.as_os_str() == OsStr::new("LIB"))
+                .and_then(|(_, lib_paths)| {
                     env::split_paths(lib_paths).find(|path| {
                         let sub = Path::new("atlmfc/lib");
                         path.ends_with(sub) || path.parent().map_or(false, |p| p.ends_with(sub))
@@ -1132,7 +1227,7 @@ impl Build {
                 });
 
             if let Some(atlmfc_lib) = atlmfc_lib {
-                self.print(&format_args!(
+                self.cargo_output.print_metadata(&format_args!(
                     "cargo:rustc-link-search=native={}",
                     atlmfc_lib.display()
                 ));
@@ -1140,15 +1235,16 @@ impl Build {
         }
 
         if self.link_lib_modifiers.is_empty() {
-            self.print(&format_args!("cargo:rustc-link-lib=static={}", lib_name));
+            self.cargo_output
+                .print_metadata(&format_args!("cargo:rustc-link-lib=static={}", lib_name));
         } else {
             let m = self.link_lib_modifiers.join(",");
-            self.print(&format_args!(
+            self.cargo_output.print_metadata(&format_args!(
                 "cargo:rustc-link-lib=static:{}={}",
                 m, lib_name
             ));
         }
-        self.print(&format_args!(
+        self.cargo_output.print_metadata(&format_args!(
             "cargo:rustc-link-search=native={}",
             dst.display()
         ));
@@ -1156,7 +1252,8 @@ impl Build {
         // Add specific C++ libraries, if enabled.
         if self.cpp {
             if let Some(stdlib) = self.get_cpp_link_stdlib()? {
-                self.print(&format_args!("cargo:rustc-link-lib={}", stdlib));
+                self.cargo_output
+                    .print_metadata(&format_args!("cargo:rustc-link-lib={}", stdlib));
             }
         }
 
@@ -1194,10 +1291,10 @@ impl Build {
                     }
                 }
                 if libtst && libdir.is_dir() {
-                    println!(
+                    self.cargo_output.print_metadata(&format_args!(
                         "cargo:rustc-link-search=native={}",
                         libdir.to_str().unwrap()
-                    );
+                    ));
                 }
 
                 // And now the -l flag.
@@ -1206,7 +1303,8 @@ impl Build {
                     "static" => "cudart_static",
                     bad => panic!("unsupported cudart option: {}", bad),
                 };
-                println!("cargo:rustc-link-lib={}", lib);
+                self.cargo_output
+                    .print_metadata(&format_args!("cargo:rustc-link-lib={}", lib));
             }
         }
 
@@ -1256,27 +1354,48 @@ impl Build {
         }
     }
 
+    /// Run the compiler, generating intermediate files, but without linking
+    /// them into an archive file.
+    ///
+    /// This will return a list of compiled object files, in the same order
+    /// as they were passed in as `file`/`files` methods.
+    pub fn compile_intermediates(&self) -> Vec<PathBuf> {
+        match self.try_compile_intermediates() {
+            Ok(v) => v,
+            Err(e) => fail(&e.message),
+        }
+    }
+
+    /// Run the compiler, generating intermediate files, but without linking
+    /// them into an archive file.
+    ///
+    /// This will return a result instead of panicking; see `compile_intermediates()` for the complete description.
+    pub fn try_compile_intermediates(&self) -> Result<Vec<PathBuf>, Error> {
+        let dst = self.get_out_dir()?;
+        let objects = objects_from_files(&self.files, &dst)?;
+
+        self.compile_objects(&objects)?;
+
+        Ok(objects.into_iter().map(|v| v.dst).collect())
+    }
+
     #[cfg(feature = "parallel")]
-    fn compile_objects(&self, objs: &[Object], print: &PrintThread) -> Result<(), Error> {
-        use std::sync::{mpsc::channel, Once};
+    fn compile_objects(&self, objs: &[Object]) -> Result<(), Error> {
+        use std::cell::Cell;
+
+        use parallel::async_executor::{block_on, YieldOnce};
 
         if objs.len() <= 1 {
             for obj in objs {
                 let (mut cmd, name) = self.create_compile_object_cmd(obj)?;
-                run(&mut cmd, &name, print)?;
+                run(&mut cmd, &name, &self.cargo_output)?;
             }
 
             return Ok(());
         }
 
-        // Limit our parallelism globally with a jobserver. Start off by
-        // releasing our own token for this process so we can have a bit of an
-        // easier to write loop below. If this fails, though, then we're likely
-        // on Windows with the main implicit token, so we just have a bit extra
-        // parallelism for a bit and don't reacquire later.
-        let server = jobserver();
-        // Reacquire our process's token on drop
-        let _reacquire = server.release_raw().ok().map(|_| JobserverToken(server));
+        // Limit our parallelism globally with a jobserver.
+        let tokens = parallel::job_token::JobTokenServer::new();
 
         // When compiling objects in parallel we do a few dirty tricks to speed
         // things up:
@@ -1297,78 +1416,108 @@ impl Build {
         // acquire the appropriate tokens, Once all objects have been compiled
         // we wait on all the processes and propagate the results of compilation.
 
-        let (tx, rx) = channel::<(_, String, KillOnDrop, _)>();
-
-        // Since jobserver::Client::acquire can block, waiting
-        // must be done in parallel so that acquire won't block forever.
-        let wait_thread = thread::Builder::new().spawn(move || {
-            for (cmd, program, mut child, _token) in rx {
-                wait_on_child(&cmd, &program, &mut child.0)?;
-            }
-
-            Ok(())
-        })?;
-
-        for obj in objs {
-            let (mut cmd, program) = self.create_compile_object_cmd(obj)?;
-            let token = server.acquire()?;
+        let pendings = Cell::new(Vec::<(
+            Command,
+            String,
+            KillOnDrop,
+            parallel::job_token::JobToken,
+        )>::new());
+        let is_disconnected = Cell::new(false);
+        let has_made_progress = Cell::new(false);
+
+        let wait_future = async {
+            let mut error = None;
+            // Buffer the stdout
+            let mut stdout = io::BufWriter::with_capacity(128, io::stdout());
+
+            loop {
+                // If the other end of the pipe is already disconnected, then we're not gonna get any new jobs,
+                // so it doesn't make sense to reuse the tokens; in fact,
+                // releasing them as soon as possible (once we know that the other end is disconnected) is beneficial.
+                // Imagine that the last file built takes an hour to finish; in this scenario,
+                // by not releasing the tokens before that last file is done we would effectively block other processes from
+                // starting sooner - even though we only need one token for that last file, not N others that were acquired.
+
+                let mut pendings_is_empty = false;
+
+                cell_update(&pendings, |mut pendings| {
+                    // Try waiting on them.
+                    parallel::retain_unordered_mut(
+                        &mut pendings,
+                        |(cmd, program, child, _token)| {
+                            match try_wait_on_child(
+                                cmd,
+                                program,
+                                &mut child.0,
+                                &mut stdout,
+                                &mut child.1,
+                            ) {
+                                Ok(Some(())) => {
+                                    // Task done, remove the entry
+                                    has_made_progress.set(true);
+                                    false
+                                }
+                                Ok(None) => true, // Task still not finished, keep the entry
+                                Err(err) => {
+                                    // Task fail, remove the entry.
+                                    // Since we can only return one error, log the error to make
+                                    // sure users always see all the compilation failures.
+                                    has_made_progress.set(true);
+
+                                    if self.cargo_output.warnings {
+                                        let _ = writeln!(stdout, "cargo:warning={}", err);
+                                    }
+                                    error = Some(err);
+
+                                    false
+                                }
+                            }
+                        },
+                    );
+                    pendings_is_empty = pendings.is_empty();
+                    pendings
+                });
 
-            let child = spawn(&mut cmd, &program, print.pipe_writer_cloned()?.unwrap())?;
+                if pendings_is_empty && is_disconnected.get() {
+                    break if let Some(err) = error {
+                        Err(err)
+                    } else {
+                        Ok(())
+                    };
+                }
 
-            if tx.send((cmd, program, KillOnDrop(child), token)).is_err() {
-                break;
+                YieldOnce::default().await;
             }
-        }
-        drop(tx);
-
-        return wait_thread.join().expect("wait_thread panics");
-
-        /// Returns a suitable `jobserver::Client` used to coordinate
-        /// parallelism between build scripts.
-        fn jobserver() -> &'static jobserver::Client {
-            static INIT: Once = Once::new();
-            static mut JOBSERVER: Option<jobserver::Client> = None;
-
-            fn _assert_sync<T: Sync>() {}
-            _assert_sync::<jobserver::Client>();
+        };
+        let spawn_future = async {
+            for obj in objs {
+                let (mut cmd, program) = self.create_compile_object_cmd(obj)?;
+                let token = loop {
+                    if let Some(token) = tokens.try_acquire()? {
+                        break token;
+                    } else {
+                        YieldOnce::default().await
+                    }
+                };
+                let mut child = spawn(&mut cmd, &program, &self.cargo_output)?;
+                let mut stderr_forwarder = StderrForwarder::new(&mut child);
+                stderr_forwarder.set_non_blocking()?;
 
-            unsafe {
-                INIT.call_once(|| {
-                    let server = default_jobserver();
-                    JOBSERVER = Some(server);
+                cell_update(&pendings, |mut pendings| {
+                    pendings.push((cmd, program, KillOnDrop(child, stderr_forwarder), token));
+                    pendings
                 });
-                JOBSERVER.as_ref().unwrap()
-            }
-        }
 
-        unsafe fn default_jobserver() -> jobserver::Client {
-            // Try to use the environmental jobserver which Cargo typically
-            // initializes for us...
-            if let Some(client) = jobserver::Client::from_env() {
-                return client;
+                has_made_progress.set(true);
             }
+            is_disconnected.set(true);
 
-            // ... but if that fails for whatever reason select something
-            // reasonable and crate a new jobserver. Use `NUM_JOBS` if set (it's
-            // configured by Cargo) and otherwise just fall back to a
-            // semi-reasonable number. Note that we could use `num_cpus` here
-            // but it's an extra dependency that will almost never be used, so
-            // it's generally not too worth it.
-            let mut parallelism = 4;
-            if let Ok(amt) = env::var("NUM_JOBS") {
-                if let Ok(amt) = amt.parse() {
-                    parallelism = amt;
-                }
-            }
+            Ok::<_, Error>(())
+        };
 
-            // If we create our own jobserver then be sure to reserve one token
-            // for ourselves.
-            let client = jobserver::Client::new(parallelism).expect("failed to create jobserver");
-            client.acquire_raw().expect("failed to acquire initial");
-            return client;
-        }
+        return block_on(wait_future, spawn_future, &has_made_progress);
 
-        struct KillOnDrop(Child);
+        struct KillOnDrop(Child, StderrForwarder);
 
         impl Drop for KillOnDrop {
             fn drop(&mut self) {
@@ -1378,19 +1527,22 @@ impl Build {
             }
         }
 
-        struct JobserverToken(&'static jobserver::Client);
-        impl Drop for JobserverToken {
-            fn drop(&mut self) {
-                let _ = self.0.acquire_raw();
-            }
+        fn cell_update<T, F>(cell: &Cell<T>, f: F)
+        where
+            T: Default,
+            F: FnOnce(T) -> T,
+        {
+            let old = cell.take();
+            let new = f(old);
+            cell.set(new);
         }
     }
 
     #[cfg(not(feature = "parallel"))]
-    fn compile_objects(&self, objs: &[Object], print: &PrintThread) -> Result<(), Error> {
+    fn compile_objects(&self, objs: &[Object]) -> Result<(), Error> {
         for obj in objs {
             let (mut cmd, name) = self.create_compile_object_cmd(obj)?;
-            run(&mut cmd, &name, print)?;
+            run(&mut cmd, &name, &self.cargo_output)?;
         }
 
         Ok(())
@@ -1405,11 +1557,12 @@ impl Build {
         let clang = compiler.family == ToolFamily::Clang;
         let gnu = compiler.family == ToolFamily::Gnu;
 
-        let (mut cmd, name) = if msvc && asm_ext == Some(AsmFileExt::DotAsm) {
+        let is_assembler_msvc = msvc && asm_ext == Some(AsmFileExt::DotAsm);
+        let (mut cmd, name) = if is_assembler_msvc {
             self.msvc_macro_assembler()?
         } else {
             let mut cmd = compiler.to_command();
-            for &(ref a, ref b) in self.env.iter() {
+            for (a, b) in self.env.iter() {
                 cmd.env(a, b);
             }
             (
@@ -1427,7 +1580,7 @@ impl Build {
             &mut cmd, &obj.dst, self.cuda, msvc, clang, gnu, is_asm, is_arm,
         );
         // armasm and armasm64 don't requrie -c option
-        if !msvc || !is_asm || !is_arm {
+        if !is_assembler_msvc || !is_arm {
             cmd.arg("-c");
         }
         if self.cuda && self.cuda_file_count() > 1 {
@@ -1436,7 +1589,7 @@ impl Build {
         if is_asm {
             cmd.args(self.asm_flags.iter().map(std::ops::Deref::deref));
         }
-        if compiler.family == (ToolFamily::Msvc { clang_cl: true }) && !is_asm {
+        if compiler.family == (ToolFamily::Msvc { clang_cl: true }) && !is_assembler_msvc {
             // #513: For `clang-cl`, separate flags/options from the input file.
             // When cross-compiling macOS -> Windows, this avoids interpreting
             // common `/Users/...` paths as the `/U` flag and triggering
@@ -1451,11 +1604,11 @@ impl Build {
         Ok((cmd, name))
     }
 
-    /// This will return a result instead of panicing; see expand() for the complete description.
+    /// This will return a result instead of panicking; see expand() for the complete description.
     pub fn try_expand(&self) -> Result<Vec<u8>, Error> {
         let compiler = self.try_get_compiler()?;
         let mut cmd = compiler.to_command();
-        for &(ref a, ref b) in self.env.iter() {
+        for (a, b) in self.env.iter() {
             cmd.env(a, b);
         }
         cmd.arg("-E");
@@ -1465,6 +1618,21 @@ impl Build {
             "Expand may only be called for a single file"
         );
 
+        let is_asm = self
+            .files
+            .iter()
+            .map(std::ops::Deref::deref)
+            .find_map(AsmFileExt::from_path)
+            .is_some();
+
+        if compiler.family == (ToolFamily::Msvc { clang_cl: true }) && !is_asm {
+            // #513: For `clang-cl`, separate flags/options from the input file.
+            // When cross-compiling macOS -> Windows, this avoids interpreting
+            // common `/Users/...` paths as the `/U` flag and triggering
+            // `-Wslash-u-filename` warning.
+            cmd.arg("--");
+        }
+
         cmd.args(self.files.iter().map(std::ops::Deref::deref));
 
         let name = compiler
@@ -1474,7 +1642,7 @@ impl Build {
             .to_string_lossy()
             .into_owned();
 
-        Ok(run_output(&mut cmd, &name)?)
+        Ok(run_output(&mut cmd, &name, &self.cargo_output)?)
     }
 
     /// Run the compiler, returning the macro-expanded version of the input files.
@@ -1523,7 +1691,8 @@ impl Build {
 
     /// Get the compiler that's in use for this configuration.
     ///
-    /// This will return a result instead of panicing; see get_compiler() for the complete description.
+    /// This will return a result instead of panicking; see
+    /// [`get_compiler()`](Self::get_compiler) for the complete description.
     pub fn try_get_compiler(&self) -> Result<Tool, Error> {
         let opt_level = self.get_opt_level()?;
         let target = self.get_target()?;
@@ -1539,6 +1708,14 @@ impl Build {
             println!("Info: default compiler flags are disabled");
         }
 
+        if let Some(ref std) = self.std {
+            let separator = match cmd.family {
+                ToolFamily::Msvc { .. } => ':',
+                ToolFamily::Gnu | ToolFamily::Clang => '=',
+            };
+            cmd.push_cc_arg(format!("-std{}{}", separator, std).into());
+        }
+
         if let Ok(flags) = self.envflags(if self.cpp { "CXXFLAGS" } else { "CFLAGS" }) {
             for arg in flags {
                 cmd.push_cc_arg(arg.into());
@@ -1547,7 +1724,7 @@ impl Build {
 
         for directory in self.include_directories.iter() {
             cmd.args.push("-I".into());
-            cmd.args.push((**directory).into());
+            cmd.args.push(directory.as_os_str().into());
         }
 
         // If warnings and/or extra_warnings haven't been explicitly set,
@@ -1555,18 +1732,12 @@ impl Build {
         // CFLAGS/CXXFLAGS, since those variables presumably already contain
         // the desired set of warnings flags.
 
-        if self
-            .warnings
-            .unwrap_or(if self.has_flags() { false } else { true })
-        {
+        if self.warnings.unwrap_or(!self.has_flags()) {
             let wflags = cmd.family.warnings_flags().into();
             cmd.push_cc_arg(wflags);
         }
 
-        if self
-            .extra_warnings
-            .unwrap_or(if self.has_flags() { false } else { true })
-        {
+        if self.extra_warnings.unwrap_or(!self.has_flags()) {
             if let Some(wflags) = cmd.family.extra_warnings_flags() {
                 cmd.push_cc_arg(wflags.into());
             }
@@ -1582,7 +1753,7 @@ impl Build {
             }
         }
 
-        for &(ref key, ref value) in self.definitions.iter() {
+        for (key, value) in self.definitions.iter() {
             if let Some(ref value) = *value {
                 cmd.args.push(format!("-D{}={}", key, value).into());
             } else {
@@ -1658,7 +1829,10 @@ impl Build {
                     cmd.push_opt_unless_duplicate("-DANDROID".into());
                 }
 
-                if !target.contains("apple-ios") && !target.contains("apple-watchos") {
+                if !target.contains("apple-ios")
+                    && !target.contains("apple-watchos")
+                    && !target.contains("apple-tvos")
+                {
                     cmd.push_cc_arg("-ffunction-sections".into());
                     cmd.push_cc_arg("-fdata-sections".into());
                 }
@@ -1692,12 +1866,20 @@ impl Build {
             family.add_force_frame_pointer(cmd);
         }
 
+        if !cmd.is_like_msvc() {
+            if target.contains("i686") || target.contains("i586") {
+                cmd.args.push("-m32".into());
+            } else if target == "x86_64-unknown-linux-gnux32" {
+                cmd.args.push("-mx32".into());
+            } else if target.contains("x86_64") || target.contains("powerpc64") {
+                cmd.args.push("-m64".into());
+            }
+        }
+
         // Target flags
         match cmd.family {
             ToolFamily::Clang => {
-                if !(target.contains("android")
-                    && android_clang_compiler_uses_target_arg_internally(&cmd.path))
-                {
+                if !(target.contains("android") && cmd.has_internal_target_arg) {
                     if target.contains("darwin") {
                         if let Some(arch) =
                             map_darwin_target_from_rust_to_compiler_architecture(target)
@@ -1716,8 +1898,10 @@ impl Build {
                         if let Some(arch) =
                             map_darwin_target_from_rust_to_compiler_architecture(target)
                         {
-                            let deployment_target = env::var("IPHONEOS_DEPLOYMENT_TARGET")
-                                .unwrap_or_else(|_| "7.0".into());
+                            let sdk_details =
+                                apple_os_sdk_parts(AppleOs::Ios, &AppleArchSpec::Simulator(""));
+                            let deployment_target =
+                                self.apple_deployment_version(AppleOs::Ios, None, &sdk_details.sdk);
                             cmd.args.push(
                                 format!(
                                     "--target={}-apple-ios{}-simulator",
@@ -1730,8 +1914,13 @@ impl Build {
                         if let Some(arch) =
                             map_darwin_target_from_rust_to_compiler_architecture(target)
                         {
-                            let deployment_target = env::var("WATCHOS_DEPLOYMENT_TARGET")
-                                .unwrap_or_else(|_| "5.0".into());
+                            let sdk_details =
+                                apple_os_sdk_parts(AppleOs::WatchOs, &AppleArchSpec::Simulator(""));
+                            let deployment_target = self.apple_deployment_version(
+                                AppleOs::WatchOs,
+                                None,
+                                &sdk_details.sdk,
+                            );
                             cmd.args.push(
                                 format!(
                                     "--target={}-apple-watchos{}-simulator",
@@ -1740,6 +1929,40 @@ impl Build {
                                 .into(),
                             );
                         }
+                    } else if target.contains("tvos-sim") || target.contains("x86_64-apple-tvos") {
+                        if let Some(arch) =
+                            map_darwin_target_from_rust_to_compiler_architecture(target)
+                        {
+                            let sdk_details =
+                                apple_os_sdk_parts(AppleOs::TvOs, &AppleArchSpec::Simulator(""));
+                            let deployment_target = self.apple_deployment_version(
+                                AppleOs::TvOs,
+                                None,
+                                &sdk_details.sdk,
+                            );
+                            cmd.args.push(
+                                format!(
+                                    "--target={}-apple-tvos{}-simulator",
+                                    arch, deployment_target
+                                )
+                                .into(),
+                            );
+                        }
+                    } else if target.contains("aarch64-apple-tvos") {
+                        if let Some(arch) =
+                            map_darwin_target_from_rust_to_compiler_architecture(target)
+                        {
+                            let sdk_details =
+                                apple_os_sdk_parts(AppleOs::TvOs, &AppleArchSpec::Device(""));
+                            let deployment_target = self.apple_deployment_version(
+                                AppleOs::TvOs,
+                                None,
+                                &sdk_details.sdk,
+                            );
+                            cmd.args.push(
+                                format!("--target={}-apple-tvos{}", arch, deployment_target).into(),
+                            );
+                        }
                     } else if target.starts_with("riscv64gc-") {
                         cmd.args.push(
                             format!("--target={}", target.replace("riscv64gc", "riscv64")).into(),
@@ -1803,6 +2026,8 @@ impl Build {
                 } else {
                     if target.contains("i586") {
                         cmd.push_cc_arg("-arch:IA32".into());
+                    } else if target.contains("arm64ec") {
+                        cmd.push_cc_arg("-arm64EC".into());
                     }
                 }
 
@@ -1821,14 +2046,6 @@ impl Build {
                 }
             }
             ToolFamily::Gnu => {
-                if target.contains("i686") || target.contains("i586") {
-                    cmd.args.push("-m32".into());
-                } else if target == "x86_64-unknown-linux-gnux32" {
-                    cmd.args.push("-mx32".into());
-                } else if target.contains("x86_64") || target.contains("powerpc64") {
-                    cmd.args.push("-m64".into());
-                }
-
                 if target.contains("darwin") {
                     if let Some(arch) = map_darwin_target_from_rust_to_compiler_architecture(target)
                     {
@@ -1997,36 +2214,36 @@ impl Build {
                     let mut parts = target.split('-');
                     if let Some(arch) = parts.next() {
                         let arch = &arch[5..];
-                        if target.contains("linux") && arch.starts_with("64") {
-                            cmd.args.push(("-march=rv64gc").into());
-                            cmd.args.push("-mabi=lp64d".into());
-                        } else if target.contains("freebsd") && arch.starts_with("64") {
-                            cmd.args.push(("-march=rv64gc").into());
-                            cmd.args.push("-mabi=lp64d".into());
-                        } else if target.contains("netbsd") && arch.starts_with("64") {
-                            cmd.args.push(("-march=rv64gc").into());
-                            cmd.args.push("-mabi=lp64d".into());
-                        } else if target.contains("openbsd") && arch.starts_with("64") {
-                            cmd.args.push(("-march=rv64gc").into());
-                            cmd.args.push("-mabi=lp64d".into());
-                        } else if target.contains("linux") && arch.starts_with("32") {
-                            cmd.args.push(("-march=rv32gc").into());
-                            cmd.args.push("-mabi=ilp32d".into());
-                        } else if arch.starts_with("64") {
-                            cmd.args.push(("-march=rv".to_owned() + arch).into());
-                            cmd.args.push("-mabi=lp64".into());
+                        if arch.starts_with("64") {
+                            if target.contains("linux")
+                                | target.contains("freebsd")
+                                | target.contains("netbsd")
+                                | target.contains("linux")
+                            {
+                                cmd.args.push(("-march=rv64gc").into());
+                                cmd.args.push("-mabi=lp64d".into());
+                            } else {
+                                cmd.args.push(("-march=rv".to_owned() + arch).into());
+                                cmd.args.push("-mabi=lp64".into());
+                            }
+                        } else if arch.starts_with("32") {
+                            if target.contains("linux") {
+                                cmd.args.push(("-march=rv32gc").into());
+                                cmd.args.push("-mabi=ilp32d".into());
+                            } else {
+                                cmd.args.push(("-march=rv".to_owned() + arch).into());
+                                cmd.args.push("-mabi=ilp32".into());
+                            }
                         } else {
-                            cmd.args.push(("-march=rv".to_owned() + arch).into());
-                            cmd.args.push("-mabi=ilp32".into());
+                            cmd.args.push("-mcmodel=medany".into());
                         }
-                        cmd.args.push("-mcmodel=medany".into());
                     }
                 }
             }
         }
 
-        if target.contains("apple-ios") || target.contains("apple-watchos") {
-            self.ios_watchos_flags(cmd)?;
+        if target.contains("-apple-") {
+            self.apple_flags(cmd)?;
         }
 
         if self.static_flag.unwrap_or(false) {
@@ -2043,11 +2260,7 @@ impl Build {
                     cmd.push_cc_arg(format!("-stdlib=lib{}", stdlib).into());
                 }
                 _ => {
-                    println!(
-                        "cargo:warning=cpp_set_stdlib is specified, but the {:?} compiler \
-                         does not support this option, ignored",
-                        cmd.family
-                    );
+                    self.cargo_output.print_warning(&format_args!("cpp_set_stdlib is specified, but the {:?} compiler does not support this option, ignored", cmd.family));
                 }
             }
         }
@@ -2086,13 +2299,26 @@ impl Build {
                 cmd.arg("-g");
             }
 
-            println!("cargo:warning=The MSVC ARM assemblers do not support -D flags");
+            for (key, value) in self.definitions.iter() {
+                cmd.arg("-PreDefine");
+                if let Some(ref value) = *value {
+                    if let Ok(i) = value.parse::<i32>() {
+                        cmd.arg(&format!("{} SETA {}", key, i));
+                    } else if value.starts_with('"') && value.ends_with('"') {
+                        cmd.arg(&format!("{} SETS {}", key, value));
+                    } else {
+                        cmd.arg(&format!("{} SETS \"{}\"", key, value));
+                    }
+                } else {
+                    cmd.arg(&format!("{} SETL {}", key, "{TRUE}"));
+                }
+            }
         } else {
             if self.get_debug() {
                 cmd.arg("-Zi");
             }
 
-            for &(ref key, ref value) in self.definitions.iter() {
+            for (key, value) in self.definitions.iter() {
                 if let Some(ref value) = *value {
                     cmd.arg(&format!("-D{}={}", key, value));
                 } else {
@@ -2104,20 +2330,11 @@ impl Build {
         if target.contains("i686") || target.contains("i586") {
             cmd.arg("-safeseh");
         }
-        for flag in self.flags.iter() {
-            cmd.arg(&**flag);
-        }
 
         Ok((cmd, tool.to_string()))
     }
 
-    fn assemble(
-        &self,
-        lib_name: &str,
-        dst: &Path,
-        objs: &[Object],
-        print: &PrintThread,
-    ) -> Result<(), Error> {
+    fn assemble(&self, lib_name: &str, dst: &Path, objs: &[Object]) -> Result<(), Error> {
         // Delete the destination if it exists as we want to
         // create on the first iteration instead of appending.
         let _ = fs::remove_file(dst);
@@ -2131,7 +2348,7 @@ impl Build {
             .chain(self.objects.iter().map(std::ops::Deref::deref))
             .collect();
         for chunk in objs.chunks(100) {
-            self.assemble_progressive(dst, chunk, print)?;
+            self.assemble_progressive(dst, chunk)?;
         }
 
         if self.cuda && self.cuda_file_count() > 0 {
@@ -2142,8 +2359,8 @@ impl Build {
             let dlink = out_dir.join(lib_name.to_owned() + "_dlink.o");
             let mut nvcc = self.get_compiler().to_command();
             nvcc.arg("--device-link").arg("-o").arg(&dlink).arg(dst);
-            run(&mut nvcc, "nvcc", print)?;
-            self.assemble_progressive(dst, &[dlink.as_path()], print)?;
+            run(&mut nvcc, "nvcc", &self.cargo_output)?;
+            self.assemble_progressive(dst, &[dlink.as_path()])?;
         }
 
         let target = self.get_target()?;
@@ -2154,9 +2371,9 @@ impl Build {
 
             let lib_dst = dst.with_file_name(format!("{}.lib", lib_name));
             let _ = fs::remove_file(&lib_dst);
-            match fs::hard_link(&dst, &lib_dst).or_else(|_| {
+            match fs::hard_link(dst, &lib_dst).or_else(|_| {
                 // if hard-link fails, just copy (ignoring the number of bytes written)
-                fs::copy(&dst, &lib_dst).map(|_| ())
+                fs::copy(dst, &lib_dst).map(|_| ())
             }) {
                 Ok(_) => (),
                 Err(_) => {
@@ -2175,18 +2392,13 @@ impl Build {
             // NOTE: We add `s` even if flags were passed using $ARFLAGS/ar_flag, because `s`
             // here represents a _mode_, not an arbitrary flag. Further discussion of this choice
             // can be seen in https://github.com/rust-lang/cc-rs/pull/763.
-            run(ar.arg("s").arg(dst), &cmd, print)?;
+            run(ar.arg("s").arg(dst), &cmd, &self.cargo_output)?;
         }
 
         Ok(())
     }
 
-    fn assemble_progressive(
-        &self,
-        dst: &Path,
-        objs: &[&Path],
-        print: &PrintThread,
-    ) -> Result<(), Error> {
+    fn assemble_progressive(&self, dst: &Path, objs: &[&Path]) -> Result<(), Error> {
         let target = self.get_target()?;
 
         if target.contains("msvc") {
@@ -2207,7 +2419,7 @@ impl Build {
                 cmd.arg(dst);
             }
             cmd.args(objs);
-            run(&mut cmd, &program, print)?;
+            run(&mut cmd, &program, &self.cargo_output)?;
         } else {
             let (mut ar, cmd, _any_flags) = self.get_ar()?;
 
@@ -2238,43 +2450,32 @@ impl Build {
             // NOTE: We add cq here regardless of whether $ARFLAGS/ar_flag have been used because
             // it dictates the _mode_ ar runs in, which the setter of $ARFLAGS/ar_flag can't
             // dictate. See https://github.com/rust-lang/cc-rs/pull/763 for further discussion.
-            run(ar.arg("cq").arg(dst).args(objs), &cmd, print)?;
+            run(ar.arg("cq").arg(dst).args(objs), &cmd, &self.cargo_output)?;
         }
 
         Ok(())
     }
 
-    fn ios_watchos_flags(&self, cmd: &mut Tool) -> Result<(), Error> {
-        enum ArchSpec {
-            Device(&'static str),
-            Simulator(&'static str),
-            Catalyst(&'static str),
-        }
-
-        enum Os {
-            Ios,
-            WatchOs,
-        }
-        impl Display for Os {
-            fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
-                match self {
-                    Os::Ios => f.write_str("iOS"),
-                    Os::WatchOs => f.write_str("WatchOS"),
-                }
-            }
-        }
-
+    fn apple_flags(&self, cmd: &mut Tool) -> Result<(), Error> {
         let target = self.get_target()?;
-        let os = if target.contains("-watchos") {
-            Os::WatchOs
+        let os = if target.contains("-darwin") {
+            AppleOs::MacOs
+        } else if target.contains("-watchos") {
+            AppleOs::WatchOs
+        } else if target.contains("-tvos") {
+            AppleOs::TvOs
         } else {
-            Os::Ios
+            AppleOs::Ios
+        };
+        let is_mac = match os {
+            AppleOs::MacOs => true,
+            _ => false,
         };
 
-        let arch = target.split('-').nth(0).ok_or_else(|| {
+        let arch_str = target.split('-').nth(0).ok_or_else(|| {
             Error::new(
                 ErrorKind::ArchitectureInvalid,
-                format!("Unknown architecture for {} target.", os),
+                format!("Unknown architecture for {:?} target.", os),
             )
         })?;
 
@@ -2283,16 +2484,27 @@ impl Build {
             None => false,
         };
 
-        let is_sim = match target.split('-').nth(3) {
+        let is_arm_sim = match target.split('-').nth(3) {
             Some(v) => v == "sim",
             None => false,
         };
 
-        let arch = if is_catalyst {
-            match arch {
-                "arm64e" => ArchSpec::Catalyst("arm64e"),
-                "arm64" | "aarch64" => ArchSpec::Catalyst("arm64"),
-                "x86_64" => ArchSpec::Catalyst("-m64"),
+        let arch = if is_mac {
+            match arch_str {
+                "i686" => AppleArchSpec::Device("-m32"),
+                "x86_64" | "x86_64h" | "aarch64" => AppleArchSpec::Device("-m64"),
+                _ => {
+                    return Err(Error::new(
+                        ErrorKind::ArchitectureInvalid,
+                        "Unknown architecture for macOS target.",
+                    ));
+                }
+            }
+        } else if is_catalyst {
+            match arch_str {
+                "arm64e" => AppleArchSpec::Catalyst("arm64e"),
+                "arm64" | "aarch64" => AppleArchSpec::Catalyst("arm64"),
+                "x86_64" | "x86_64h" => AppleArchSpec::Catalyst("-m64"),
                 _ => {
                     return Err(Error::new(
                         ErrorKind::ArchitectureInvalid,
@@ -2300,58 +2512,52 @@ impl Build {
                     ));
                 }
             }
-        } else if is_sim {
-            match arch {
-                "arm64" | "aarch64" => ArchSpec::Simulator("arm64"),
-                "x86_64" => ArchSpec::Simulator("-m64"),
+        } else if is_arm_sim {
+            match arch_str {
+                "arm64" | "aarch64" => AppleArchSpec::Simulator("arm64"),
+                "x86_64" | "x86_64h" => AppleArchSpec::Simulator("-m64"),
                 _ => {
                     return Err(Error::new(
                         ErrorKind::ArchitectureInvalid,
-                        "Unknown architecture for iOS simulator target.",
+                        "Unknown architecture for simulator target.",
                     ));
                 }
             }
         } else {
-            match arch {
-                "arm" | "armv7" | "thumbv7" => ArchSpec::Device("armv7"),
-                "armv7k" => ArchSpec::Device("armv7k"),
-                "armv7s" | "thumbv7s" => ArchSpec::Device("armv7s"),
-                "arm64e" => ArchSpec::Device("arm64e"),
-                "arm64" | "aarch64" => ArchSpec::Device("arm64"),
-                "arm64_32" => ArchSpec::Device("arm64_32"),
-                "i386" | "i686" => ArchSpec::Simulator("-m32"),
-                "x86_64" => ArchSpec::Simulator("-m64"),
+            match arch_str {
+                "arm" | "armv7" | "thumbv7" => AppleArchSpec::Device("armv7"),
+                "armv7k" => AppleArchSpec::Device("armv7k"),
+                "armv7s" | "thumbv7s" => AppleArchSpec::Device("armv7s"),
+                "arm64e" => AppleArchSpec::Device("arm64e"),
+                "arm64" | "aarch64" => AppleArchSpec::Device("arm64"),
+                "arm64_32" => AppleArchSpec::Device("arm64_32"),
+                "i386" | "i686" => AppleArchSpec::Simulator("-m32"),
+                "x86_64" | "x86_64h" => AppleArchSpec::Simulator("-m64"),
                 _ => {
                     return Err(Error::new(
                         ErrorKind::ArchitectureInvalid,
-                        format!("Unknown architecture for {} target.", os),
+                        format!("Unknown architecture for {:?} target.", os),
                     ));
                 }
             }
         };
 
-        let (sdk_prefix, sim_prefix, min_version) = match os {
-            Os::Ios => (
-                "iphone",
-                "ios-",
-                std::env::var("IPHONEOS_DEPLOYMENT_TARGET").unwrap_or_else(|_| "7.0".into()),
-            ),
-            Os::WatchOs => (
-                "watch",
-                "watch",
-                std::env::var("WATCHOS_DEPLOYMENT_TARGET").unwrap_or_else(|_| "2.0".into()),
-            ),
-        };
+        let sdk_details = apple_os_sdk_parts(os, &arch);
+        let min_version = self.apple_deployment_version(os, Some(arch_str), &sdk_details.sdk);
 
-        let sdk = match arch {
-            ArchSpec::Device(arch) => {
+        match arch {
+            AppleArchSpec::Device(_) if is_mac => {
+                cmd.args
+                    .push(format!("-mmacosx-version-min={}", min_version).into());
+            }
+            AppleArchSpec::Device(arch) => {
                 cmd.args.push("-arch".into());
                 cmd.args.push(arch.into());
-                cmd.args
-                    .push(format!("-m{}os-version-min={}", sdk_prefix, min_version).into());
-                format!("{}os", sdk_prefix)
+                cmd.args.push(
+                    format!("-m{}os-version-min={}", sdk_details.sdk_prefix, min_version).into(),
+                );
             }
-            ArchSpec::Simulator(arch) => {
+            AppleArchSpec::Simulator(arch) => {
                 if arch.starts_with('-') {
                     // -m32 or -m64
                     cmd.args.push(arch.into());
@@ -2359,31 +2565,39 @@ impl Build {
                     cmd.args.push("-arch".into());
                     cmd.args.push(arch.into());
                 }
-                cmd.args
-                    .push(format!("-m{}simulator-version-min={}", sim_prefix, min_version).into());
-                format!("{}simulator", sdk_prefix)
+                cmd.args.push(
+                    format!(
+                        "-m{}simulator-version-min={}",
+                        sdk_details.sim_prefix, min_version
+                    )
+                    .into(),
+                );
             }
-            ArchSpec::Catalyst(_) => "macosx".to_owned(),
+            AppleArchSpec::Catalyst(_) => {}
         };
 
-        self.print(&format_args!("Detecting {} SDK path for {}", os, sdk));
-        let sdk_path = if let Some(sdkroot) = env::var_os("SDKROOT") {
-            sdkroot
-        } else {
-            self.apple_sdk_root(sdk.as_str())?
-        };
+        // AppleClang sometimes requires sysroot even for darwin
+        if cmd.is_xctoolchain_clang() || !target.ends_with("-darwin") {
+            self.cargo_output.print_metadata(&format_args!(
+                "Detecting {:?} SDK path for {}",
+                os, sdk_details.sdk
+            ));
+            let sdk_path = if let Some(sdkroot) = env::var_os("SDKROOT") {
+                sdkroot
+            } else {
+                self.apple_sdk_root(&sdk_details.sdk)?
+            };
 
-        cmd.args.push("-isysroot".into());
-        cmd.args.push(sdk_path);
-        // TODO: Remove this once Apple stops accepting apps built with Xcode 13
-        cmd.args.push("-fembed-bitcode".into());
+            cmd.args.push("-isysroot".into());
+            cmd.args.push(sdk_path);
+        }
 
         Ok(())
     }
 
     fn cmd<P: AsRef<OsStr>>(&self, prog: P) -> Command {
         let mut cmd = Command::new(prog);
-        for &(ref a, ref b) in self.env.iter() {
+        for (a, b) in self.env.iter() {
             cmd.env(a, b);
         }
         cmd
@@ -2391,7 +2605,11 @@ impl Build {
 
     fn get_base_compiler(&self) -> Result<Tool, Error> {
         if let Some(c) = &self.compiler {
-            return Ok(Tool::new((**c).to_owned()));
+            return Ok(Tool::new(
+                (**c).to_owned(),
+                &self.cached_compiler_family,
+                &self.cargo_output,
+            ));
         }
         let host = self.get_host()?;
         let target = self.get_target()?;
@@ -2412,7 +2630,7 @@ impl Build {
             traditional
         };
 
-        let cl_exe = windows_registry::find_tool(&target, "cl.exe");
+        let cl_exe = windows_registry::find_tool(target, "cl.exe");
 
         let tool_opt: Option<Tool> = self
             .env_tool(env)
@@ -2427,7 +2645,12 @@ impl Build {
                 // semi-buggy build scripts which are shared in
                 // makefiles/configure scripts (where spaces are far more
                 // lenient)
-                let mut t = Tool::with_clang_driver(tool, driver_mode);
+                let mut t = Tool::with_clang_driver(
+                    tool,
+                    driver_mode,
+                    &self.cached_compiler_family,
+                    &self.cargo_output,
+                );
                 if let Some(cc_wrapper) = wrapper {
                     t.cc_wrapper_path = Some(PathBuf::from(cc_wrapper));
                 }
@@ -2441,12 +2664,20 @@ impl Build {
                     let tool = if self.cpp { "em++" } else { "emcc" };
                     // Windows uses bat file so we have to be a bit more specific
                     if cfg!(windows) {
-                        let mut t = Tool::new(PathBuf::from("cmd"));
+                        let mut t = Tool::new(
+                            PathBuf::from("cmd"),
+                            &self.cached_compiler_family,
+                            &self.cargo_output,
+                        );
                         t.args.push("/c".into());
                         t.args.push(format!("{}.bat", tool).into());
                         Some(t)
                     } else {
-                        Some(Tool::new(PathBuf::from(tool)))
+                        Some(Tool::new(
+                            PathBuf::from(tool),
+                            &self.cached_compiler_family,
+                            &self.cargo_output,
+                        ))
                     }
                 } else {
                     None
@@ -2464,12 +2695,13 @@ impl Build {
                         let cc = if target.contains("llvm") { clang } else { gnu };
                         format!("{}.exe", cc)
                     }
-                } else if target.contains("apple-ios") {
-                    clang.to_string()
-                } else if target.contains("apple-watchos") {
+                } else if target.contains("apple-ios")
+                    | target.contains("apple-watchos")
+                    | target.contains("apple-tvos")
+                {
                     clang.to_string()
                 } else if target.contains("android") {
-                    autodetect_android_compiler(&target, &host, gnu, clang)
+                    autodetect_android_compiler(target, &host, gnu, clang)
                 } else if target.contains("cloudabi") {
                     format!("{}-{}", target, traditional)
                 } else if target == "wasm32-wasi"
@@ -2488,7 +2720,7 @@ impl Build {
                 } else if target.starts_with("aarch64-kmc-solid_") {
                     format!("aarch64-kmc-elf-{}", gnu)
                 } else if &*self.get_host()? != target {
-                    let prefix = self.prefix_for_target(&target);
+                    let prefix = self.prefix_for_target(target);
                     match prefix {
                         Some(prefix) => {
                             let cc = if target.contains("llvm") { clang } else { gnu };
@@ -2500,7 +2732,11 @@ impl Build {
                     default.to_string()
                 };
 
-                let mut t = Tool::new(PathBuf::from(compiler));
+                let mut t = Tool::new(
+                    PathBuf::from(compiler),
+                    &self.cached_compiler_family,
+                    &self.cargo_output,
+                );
                 if let Some(cc_wrapper) = Self::rustc_wrapper_fallback() {
                     t.cc_wrapper_path = Some(PathBuf::from(cc_wrapper));
                 }
@@ -2517,7 +2753,13 @@ impl Build {
                 Err(_) => PathBuf::from("nvcc"),
                 Ok(nvcc) => PathBuf::from(&*nvcc),
             };
-            let mut nvcc_tool = Tool::with_features(nvcc, None, self.cuda);
+            let mut nvcc_tool = Tool::with_features(
+                nvcc,
+                None,
+                self.cuda,
+                &self.cached_compiler_family,
+                &self.cargo_output,
+            );
             nvcc_tool
                 .args
                 .push(format!("-ccbin={}", tool.path.display()).into());
@@ -2542,16 +2784,17 @@ impl Build {
         {
             if let Some(path) = tool.path.file_name() {
                 let file_name = path.to_str().unwrap().to_owned();
-                let (target, clang) = file_name.split_at(file_name.rfind("-").unwrap());
+                let (target, clang) = file_name.split_at(file_name.rfind('-').unwrap());
 
-                tool.path.set_file_name(clang.trim_start_matches("-"));
+                tool.has_internal_target_arg = true;
+                tool.path.set_file_name(clang.trim_start_matches('-'));
                 tool.path.set_extension("exe");
                 tool.args.push(format!("--target={}", target).into());
 
                 // Additionally, shell scripts for target i686-linux-android versions 16 to 24
                 // pass the `mstackrealign` option so we do that here as well.
                 if target.contains("i686-linux-android") {
-                    let (_, version) = target.split_at(target.rfind("d").unwrap() + 1);
+                    let (_, version) = target.split_at(target.rfind('d').unwrap() + 1);
                     if let Ok(version) = version.parse::<u32>() {
                         if version > 15 && version < 25 {
                             tool.args.push("-mstackrealign".into());
@@ -2576,14 +2819,15 @@ impl Build {
                 && tool.env.len() == 0
                 && target.contains("msvc")
             {
-                for &(ref k, ref v) in cl_exe.env.iter() {
+                for (k, v) in cl_exe.env.iter() {
                     tool.env.push((k.to_owned(), v.to_owned()));
                 }
             }
         }
 
         if target.contains("msvc") && tool.family == ToolFamily::Gnu {
-            println!("cargo:warning=GNU compiler is not supported for this target");
+            self.cargo_output
+                .print_warning(&"GNU compiler is not supported for this target");
         }
 
         Ok(tool)
@@ -2618,7 +2862,11 @@ impl Build {
         // interpretation at all, just pass it on through. This'll hopefully get
         // us to support spaces-in-paths.
         if Path::new(&*tool).exists() {
-            return Some((PathBuf::from(&*tool), None, Vec::new()));
+            return Some((
+                PathBuf::from(&*tool),
+                Self::rustc_wrapper_fallback(),
+                Vec::new(),
+            ));
         }
 
         // Ok now we want to handle a couple of scenarios. We'll assume from
@@ -2672,7 +2920,7 @@ impl Build {
     }
 
     /// Returns the C++ standard library:
-    /// 1. If [cpp_link_stdlib](cc::Build::cpp_link_stdlib) is set, uses its value.
+    /// 1. If [`cpp_link_stdlib`](cc::Build::cpp_link_stdlib) is set, uses its value.
     /// 2. Else if the `CXXSTDLIB` environment variable is set, uses its value.
     /// 3. Else the default is `libc++` for OS X and BSDs, `libc++_shared` for Android,
     /// `None` for MSVC and `libstdc++` for anything else.
@@ -2690,13 +2938,12 @@ impl Build {
                     let target = self.get_target()?;
                     if target.contains("msvc") {
                         Ok(None)
-                    } else if target.contains("apple") {
-                        Ok(Some("c++".to_string()))
-                    } else if target.contains("freebsd") {
-                        Ok(Some("c++".to_string()))
-                    } else if target.contains("openbsd") {
-                        Ok(Some("c++".to_string()))
-                    } else if target.contains("aix") {
+                    } else if target.contains("apple")
+                        | target.contains("freebsd")
+                        | target.contains("openbsd")
+                        | target.contains("aix")
+                        | target.contains("linux-ohos")
+                    {
                         Ok(Some("c++".to_string()))
                     } else if target.contains("android") {
                         Ok(Some("c++_shared".to_string()))
@@ -2735,8 +2982,8 @@ impl Build {
 
     /// Get the archiver that's in use for this configuration.
     ///
-    /// This will return a result instead of panicing;
-    /// see [`get_archiver()`] for the complete description.
+    /// This will return a result instead of panicking;
+    /// see [`Self::get_archiver`] for the complete description.
     pub fn try_get_archiver(&self) -> Result<Command, Error> {
         Ok(self.try_get_archiver_and_flags()?.0)
     }
@@ -2786,8 +3033,8 @@ impl Build {
 
     /// Get the ranlib that's in use for this configuration.
     ///
-    /// This will return a result instead of panicing;
-    /// see [`get_ranlib()`] for the complete description.
+    /// This will return a result instead of panicking;
+    /// see [`Self::get_ranlib`] for the complete description.
     pub fn try_get_ranlib(&self) -> Result<Command, Error> {
         let mut cmd = self.get_base_ranlib()?;
         if let Ok(flags) = self.envflags("RANLIBFLAGS") {
@@ -2830,13 +3077,13 @@ impl Build {
                     // Formally speaking one should be able to use this approach,
                     // parsing -print-search-dirs output, to cover all clang targets,
                     // including Android SDKs and other cross-compilation scenarios...
-                    // And even extend it to gcc targets by seaching for "ar" instead
+                    // And even extend it to gcc targets by searching for "ar" instead
                     // of "llvm-ar"...
                     let compiler = self.get_base_compiler().ok()?;
                     if compiler.family == ToolFamily::Clang {
                         name = format!("llvm-{}", tool);
-                        search_programs(&mut self.cmd(&compiler.path), &name)
-                            .map(|name| self.cmd(&name))
+                        search_programs(&mut self.cmd(&compiler.path), &name, &self.cargo_output)
+                            .map(|name| self.cmd(name))
                     } else {
                         None
                     }
@@ -2877,10 +3124,14 @@ impl Build {
 
                     if lib.is_empty() {
                         name = String::from("lib.exe");
-                        match windows_registry::find(&target, "lib.exe") {
+                        let mut cmd = match windows_registry::find(&target, "lib.exe") {
                             Some(t) => t,
                             None => self.cmd("lib.exe"),
+                        };
+                        if target.contains("arm64ec") {
+                            cmd.arg("/machine:arm64ec");
                         }
+                        cmd
                     } else {
                         name = lib;
                         self.cmd(&name)
@@ -2897,7 +3148,7 @@ impl Build {
                         Some(p) => {
                             // GCC uses $target-gcc-ar, whereas binutils uses $target-ar -- try both.
                             // Prefer -ar if it exists, as builds of `-gcc-ar` have been observed to be
-                            // outright broken (such as when targetting freebsd with `--disable-lto`
+                            // outright broken (such as when targeting freebsd with `--disable-lto`
                             // toolchain where the archiver attempts to load the LTO plugin anyway but
                             // fails to find one).
                             //
@@ -2994,6 +3245,7 @@ impl Build {
                 "riscv64-unknown-elf",
                 "riscv-none-embed",
             ]),
+            "riscv32imac-esp-espidf" => Some("riscv32-esp-elf"),
             "riscv32imac-unknown-none-elf" => self.find_working_gnu_prefix(&[
                 "riscv32-unknown-elf",
                 "riscv64-unknown-elf",
@@ -3036,6 +3288,7 @@ impl Build {
             "armebv7r-none-eabihf" => Some("arm-none-eabi"),
             "armv7r-none-eabi" => Some("arm-none-eabi"),
             "armv7r-none-eabihf" => Some("arm-none-eabi"),
+            "armv8r-none-eabihf" => Some("arm-none-eabi"),
             "thumbv6m-none-eabi" => Some("arm-none-eabi"),
             "thumbv7em-none-eabi" => Some("arm-none-eabi"),
             "thumbv7em-none-eabihf" => Some("arm-none-eabi"),
@@ -3175,10 +3428,12 @@ impl Build {
             return val.clone();
         }
         if self.emit_rerun_if_env_changed && !provided_by_cargo(v) {
-            self.print(&format_args!("cargo:rerun-if-env-changed={}", v));
+            self.cargo_output
+                .print_metadata(&format_args!("cargo:rerun-if-env-changed={}", v));
         }
         let r = env::var(v).ok().map(Arc::from);
-        self.print(&format_args!("{} = {:?}", v, r));
+        self.cargo_output
+            .print_metadata(&format_args!("{} = {:?}", v, r));
         cache.insert(v.to_string(), r.clone());
         r
     }
@@ -3197,7 +3452,7 @@ impl Build {
         let target = self.get_target()?;
         let host = self.get_host()?;
         let kind = if host == target { "HOST" } else { "TARGET" };
-        let target_u = target.replace("-", "_");
+        let target_u = target.replace('-', "_");
         let res = self
             .getenv(&format!("{}_{}", var_base, target))
             .or_else(|| self.getenv(&format!("{}_{}", var_base, target_u)))
@@ -3221,29 +3476,10 @@ impl Build {
             .collect())
     }
 
-    fn print(&self, s: &dyn Display) {
-        if self.cargo_metadata {
-            println!("{}", s);
-        }
-    }
-
     fn fix_env_for_apple_os(&self, cmd: &mut Command) -> Result<(), Error> {
         let target = self.get_target()?;
         let host = self.get_host()?;
         if host.contains("apple-darwin") && target.contains("apple-darwin") {
-            // If, for example, `cargo` runs during the build of an XCode project, then `SDKROOT` environment variable
-            // would represent the current target, and this is the problem for us, if we want to compile something
-            // for the host, when host != target.
-            // We can not just remove `SDKROOT`, because, again, for example, XCode add to PATH
-            // /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
-            // and `cc` from this path can not find system include files, like `pthread.h`, if `SDKROOT`
-            // is not set
-            if let Ok(sdkroot) = env::var("SDKROOT") {
-                if !sdkroot.contains("MacOSX") {
-                    let macos_sdk = self.apple_sdk_root("macosx")?;
-                    cmd.env("SDKROOT", macos_sdk);
-                }
-            }
             // Additionally, `IPHONEOS_DEPLOYMENT_TARGET` must not be set when using the Xcode linker at
             // "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld",
             // although this is apparently ignored when using the linker at "/usr/bin/ld".
@@ -3267,6 +3503,7 @@ impl Build {
                 .arg("--sdk")
                 .arg(sdk),
             "xcrun",
+            &self.cargo_output,
         )?;
 
         let sdk_path = match String::from_utf8(sdk_path) {
@@ -3283,357 +3520,199 @@ impl Build {
         Ok(ret)
     }
 
-    fn cuda_file_count(&self) -> usize {
-        self.files
-            .iter()
-            .filter(|file| file.extension() == Some(OsStr::new("cu")))
-            .count()
-    }
-}
+    fn apple_deployment_version(&self, os: AppleOs, arch_str: Option<&str>, sdk: &str) -> String {
+        let default_deployment_from_sdk = || {
+            let mut cache = self
+                .apple_versions_cache
+                .lock()
+                .expect("apple_versions_cache lock failed");
 
-impl Default for Build {
-    fn default() -> Build {
-        Build::new()
-    }
-}
+            if let Some(ret) = cache.get(sdk) {
+                return Some(ret.clone());
+            }
 
-impl Tool {
-    fn new(path: PathBuf) -> Self {
-        Tool::with_features(path, None, false)
-    }
+            let version = run_output(
+                self.cmd("xcrun")
+                    .arg("--show-sdk-platform-version")
+                    .arg("--sdk")
+                    .arg(sdk),
+                "xcrun",
+                &self.cargo_output,
+            )
+            .ok()?;
 
-    fn with_clang_driver(path: PathBuf, clang_driver: Option<&str>) -> Self {
-        Self::with_features(path, clang_driver, false)
-    }
+            let version = std::str::from_utf8(&version).ok()?.trim().to_owned();
 
-    #[cfg(windows)]
-    /// Explicitly set the `ToolFamily`, skipping name-based detection.
-    fn with_family(path: PathBuf, family: ToolFamily) -> Self {
-        Self {
-            path: path,
-            cc_wrapper_path: None,
-            cc_wrapper_args: Vec::new(),
-            args: Vec::new(),
-            env: Vec::new(),
-            family: family,
-            cuda: false,
-            removed_args: Vec::new(),
-        }
-    }
+            cache.insert(sdk.into(), version.clone());
+            Some(version)
+        };
 
-    fn with_features(path: PathBuf, clang_driver: Option<&str>, cuda: bool) -> Self {
-        // Try to detect family of the tool from its name, falling back to Gnu.
-        let family = if let Some(fname) = path.file_name().and_then(|p| p.to_str()) {
-            if fname.contains("clang-cl") {
-                ToolFamily::Msvc { clang_cl: true }
-            } else if fname.ends_with("cl") || fname == "cl.exe" {
-                ToolFamily::Msvc { clang_cl: false }
-            } else if fname.contains("clang") {
-                match clang_driver {
-                    Some("cl") => ToolFamily::Msvc { clang_cl: true },
-                    _ => ToolFamily::Clang,
-                }
+        let deployment_from_env = |name: &str| {
+            // note this isn't hit in production codepaths, its mostly just for tests which don't
+            // set the real env
+            if let Some((_, v)) = self.env.iter().find(|(k, _)| &**k == OsStr::new(name)) {
+                Some(v.to_str().unwrap().to_string())
             } else {
-                ToolFamily::Gnu
+                env::var(name).ok()
             }
-        } else {
-            ToolFamily::Gnu
         };
 
-        Tool {
-            path: path,
-            cc_wrapper_path: None,
-            cc_wrapper_args: Vec::new(),
-            args: Vec::new(),
-            env: Vec::new(),
-            family: family,
-            cuda: cuda,
-            removed_args: Vec::new(),
-        }
-    }
-
-    /// Add an argument to be stripped from the final command arguments.
-    fn remove_arg(&mut self, flag: OsString) {
-        self.removed_args.push(flag);
-    }
-
-    /// Push an "exotic" flag to the end of the compiler's arguments list.
-    ///
-    /// Nvidia compiler accepts only the most common compiler flags like `-D`,
-    /// `-I`, `-c`, etc. Options meant specifically for the underlying
-    /// host C++ compiler have to be prefixed with '-Xcompiler`.
-    /// [Another possible future application for this function is passing
-    /// clang-specific flags to clang-cl, which otherwise accepts only
-    /// MSVC-specific options.]
-    fn push_cc_arg(&mut self, flag: OsString) {
-        if self.cuda {
-            self.args.push("-Xcompiler".into());
-        }
-        self.args.push(flag);
-    }
-
-    /// Checks if an argument or flag has already been specified or conflicts.
-    ///
-    /// Currently only checks optimization flags.
-    fn is_duplicate_opt_arg(&self, flag: &OsString) -> bool {
-        let flag = flag.to_str().unwrap();
-        let mut chars = flag.chars();
-
-        // Only duplicate check compiler flags
-        if self.is_like_msvc() {
-            if chars.next() != Some('/') {
-                return false;
-            }
-        } else if self.is_like_gnu() || self.is_like_clang() {
-            if chars.next() != Some('-') {
-                return false;
+        // Determines if the acquired deployment target is too low to support modern C++ on some Apple platform.
+        //
+        // A long time ago they used libstdc++, but since macOS 10.9 and iOS 7 libc++ has been the library the SDKs provide to link against.
+        // If a `cc`` config wants to use C++, we round up to these versions as the baseline.
+        let maybe_cpp_version_baseline = |deployment_target_ver: String| -> Option<String> {
+            if !self.cpp {
+                return Some(deployment_target_ver);
             }
-        }
 
-        // Check for existing optimization flags (-O, /O)
-        if chars.next() == Some('O') {
-            return self
-                .args()
-                .iter()
-                .any(|ref a| a.to_str().unwrap_or("").chars().nth(1) == Some('O'));
-        }
-
-        // TODO Check for existing -m..., -m...=..., /arch:... flags
-        return false;
-    }
-
-    /// Don't push optimization arg if it conflicts with existing args.
-    fn push_opt_unless_duplicate(&mut self, flag: OsString) {
-        if self.is_duplicate_opt_arg(&flag) {
-            println!("Info: Ignoring duplicate arg {:?}", &flag);
-        } else {
-            self.push_cc_arg(flag);
-        }
-    }
-
-    /// Converts this compiler into a `Command` that's ready to be run.
-    ///
-    /// This is useful for when the compiler needs to be executed and the
-    /// command returned will already have the initial arguments and environment
-    /// variables configured.
-    pub fn to_command(&self) -> Command {
-        let mut cmd = match self.cc_wrapper_path {
-            Some(ref cc_wrapper_path) => {
-                let mut cmd = Command::new(&cc_wrapper_path);
-                cmd.arg(&self.path);
-                cmd
+            let mut deployment_target = deployment_target_ver
+                .split('.')
+                .map(|v| v.parse::<u32>().expect("integer version"));
+
+            match os {
+                AppleOs::MacOs => {
+                    let major = deployment_target.next().unwrap_or(0);
+                    let minor = deployment_target.next().unwrap_or(0);
+
+                    // If below 10.9, we ignore it and let the SDK's target definitions handle it.
+                    if major == 10 && minor < 9 {
+                        self.cargo_output.print_warning(&format_args!(
+                            "macOS deployment target ({}) too low, it will be increased",
+                            deployment_target_ver
+                        ));
+                        return None;
+                    }
+                }
+                AppleOs::Ios => {
+                    let major = deployment_target.next().unwrap_or(0);
+
+                    // If below 10.7, we ignore it and let the SDK's target definitions handle it.
+                    if major < 7 {
+                        self.cargo_output.print_warning(&format_args!(
+                            "iOS deployment target ({}) too low, it will be increased",
+                            deployment_target_ver
+                        ));
+                        return None;
+                    }
+                }
+                // watchOS, tvOS, and others are all new enough that libc++ is their baseline.
+                _ => {}
             }
-            None => Command::new(&self.path),
-        };
-        cmd.args(&self.cc_wrapper_args);
-
-        let value = self
-            .args
-            .iter()
-            .filter(|a| !self.removed_args.contains(a))
-            .collect::<Vec<_>>();
-        cmd.args(&value);
 
-        for &(ref k, ref v) in self.env.iter() {
-            cmd.env(k, v);
-        }
-        cmd
-    }
-
-    /// Returns the path for this compiler.
-    ///
-    /// Note that this may not be a path to a file on the filesystem, e.g. "cc",
-    /// but rather something which will be resolved when a process is spawned.
-    pub fn path(&self) -> &Path {
-        &self.path
-    }
+            // If the deployment target met or exceeded the C++ baseline
+            Some(deployment_target_ver)
+        };
 
-    /// Returns the default set of arguments to the compiler needed to produce
-    /// executables for the target this compiler generates.
-    pub fn args(&self) -> &[OsString] {
-        &self.args
-    }
+        // The hardcoded minimums here are subject to change in a future compiler release,
+        // and only exist as last resort fallbacks. Don't consider them stable.
+        // `cc` doesn't use rustc's `--print deployment-target`` because the compiler's defaults
+        // don't align well with Apple's SDKs and other third-party libraries that require ~generally~ higher
+        // deployment targets. rustc isn't interested in those by default though so its fine to be different here.
+        //
+        // If no explicit target is passed, `cc` defaults to the current Xcode SDK's `DefaultDeploymentTarget` for better
+        // compatibility. This is also the crate's historical behavior and what has become a relied-on value.
+        //
+        // The ordering of env -> XCode SDK -> old rustc defaults is intentional for performance when using
+        // an explicit target.
+        match os {
+            AppleOs::MacOs => deployment_from_env("MACOSX_DEPLOYMENT_TARGET")
+                .and_then(maybe_cpp_version_baseline)
+                .or_else(default_deployment_from_sdk)
+                .unwrap_or_else(|| {
+                    if arch_str == Some("aarch64") {
+                        "11.0".into()
+                    } else {
+                        let default = "10.7";
+                        maybe_cpp_version_baseline(default.into()).unwrap_or_else(|| default.into())
+                    }
+                }),
 
-    /// Returns the set of environment variables needed for this compiler to
-    /// operate.
-    ///
-    /// This is typically only used for MSVC compilers currently.
-    pub fn env(&self) -> &[(OsString, OsString)] {
-        &self.env
-    }
+            AppleOs::Ios => deployment_from_env("IPHONEOS_DEPLOYMENT_TARGET")
+                .and_then(maybe_cpp_version_baseline)
+                .or_else(default_deployment_from_sdk)
+                .unwrap_or_else(|| "7.0".into()),
 
-    /// Returns the compiler command in format of CC environment variable.
-    /// Or empty string if CC env was not present
-    ///
-    /// This is typically used by configure script
-    pub fn cc_env(&self) -> OsString {
-        match self.cc_wrapper_path {
-            Some(ref cc_wrapper_path) => {
-                let mut cc_env = cc_wrapper_path.as_os_str().to_owned();
-                cc_env.push(" ");
-                cc_env.push(self.path.to_path_buf().into_os_string());
-                for arg in self.cc_wrapper_args.iter() {
-                    cc_env.push(" ");
-                    cc_env.push(arg);
-                }
-                cc_env
-            }
-            None => OsString::from(""),
-        }
-    }
+            AppleOs::WatchOs => deployment_from_env("WATCHOS_DEPLOYMENT_TARGET")
+                .or_else(default_deployment_from_sdk)
+                .unwrap_or_else(|| "5.0".into()),
 
-    /// Returns the compiler flags in format of CFLAGS environment variable.
-    /// Important here - this will not be CFLAGS from env, its internal gcc's flags to use as CFLAGS
-    /// This is typically used by configure script
-    pub fn cflags_env(&self) -> OsString {
-        let mut flags = OsString::new();
-        for (i, arg) in self.args.iter().enumerate() {
-            if i > 0 {
-                flags.push(" ");
-            }
-            flags.push(arg);
+            AppleOs::TvOs => deployment_from_env("TVOS_DEPLOYMENT_TARGET")
+                .or_else(default_deployment_from_sdk)
+                .unwrap_or_else(|| "9.0".into()),
         }
-        flags
-    }
-
-    /// Whether the tool is GNU Compiler Collection-like.
-    pub fn is_like_gnu(&self) -> bool {
-        self.family == ToolFamily::Gnu
     }
 
-    /// Whether the tool is Clang-like.
-    pub fn is_like_clang(&self) -> bool {
-        self.family == ToolFamily::Clang
-    }
-
-    /// Whether the tool is MSVC-like.
-    pub fn is_like_msvc(&self) -> bool {
-        match self.family {
-            ToolFamily::Msvc { .. } => true,
-            _ => false,
-        }
+    fn cuda_file_count(&self) -> usize {
+        self.files
+            .iter()
+            .filter(|file| file.extension() == Some(OsStr::new("cu")))
+            .count()
     }
 }
 
-fn wait_on_child(cmd: &Command, program: &str, child: &mut Child) -> Result<(), Error> {
-    let status = match child.wait() {
-        Ok(s) => s,
-        Err(e) => {
-            return Err(Error::new(
-                ErrorKind::ToolExecError,
-                format!(
-                    "Failed to wait on spawned child process, command {:?} with args {:?}: {}.",
-                    cmd, program, e
-                ),
-            ));
-        }
-    };
-    println!("{}", status);
-
-    if status.success() {
-        Ok(())
-    } else {
-        Err(Error::new(
-            ErrorKind::ToolExecError,
-            format!(
-                "Command {:?} with args {:?} did not execute successfully (status code {}).",
-                cmd, program, status
-            ),
-        ))
+impl Default for Build {
+    fn default() -> Build {
+        Build::new()
     }
 }
 
-fn run_inner(cmd: &mut Command, program: &str, pipe_writer: File) -> Result<(), Error> {
-    let mut child = spawn(cmd, program, pipe_writer)?;
-    wait_on_child(cmd, program, &mut child)
-}
-
-fn run(cmd: &mut Command, program: &str, print: &PrintThread) -> Result<(), Error> {
-    run_inner(cmd, program, print.pipe_writer_cloned()?.unwrap())?;
-
-    Ok(())
+fn fail(s: &str) -> ! {
+    eprintln!("\n\nerror occurred: {}\n\n", s);
+    std::process::exit(1);
 }
 
-fn run_output(cmd: &mut Command, program: &str) -> Result<Vec<u8>, Error> {
-    cmd.stdout(Stdio::piped());
-
-    let mut print = PrintThread::new()?;
-    let mut child = spawn(cmd, program, print.pipe_writer().take().unwrap())?;
-
-    let mut stdout = vec![];
-    child
-        .stdout
-        .take()
-        .unwrap()
-        .read_to_end(&mut stdout)
-        .unwrap();
-
-    wait_on_child(cmd, program, &mut child)?;
-
-    Ok(stdout)
+#[derive(Clone, Copy, PartialEq)]
+enum AppleOs {
+    MacOs,
+    Ios,
+    WatchOs,
+    TvOs,
 }
-
-fn spawn(cmd: &mut Command, program: &str, pipe_writer: File) -> Result<Child, Error> {
-    struct ResetStderr<'cmd>(&'cmd mut Command);
-
-    impl Drop for ResetStderr<'_> {
-        fn drop(&mut self) {
-            // Reset stderr to default to release pipe_writer so that print thread will
-            // not block forever.
-            self.0.stderr(Stdio::inherit());
+impl std::fmt::Debug for AppleOs {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        match self {
+            AppleOs::MacOs => f.write_str("macOS"),
+            AppleOs::Ios => f.write_str("iOS"),
+            AppleOs::WatchOs => f.write_str("WatchOS"),
+            AppleOs::TvOs => f.write_str("AppleTVOS"),
         }
     }
+}
 
-    println!("running: {:?}", cmd);
+struct AppleSdkTargetParts {
+    sdk_prefix: &'static str,
+    sim_prefix: &'static str,
+    sdk: Cow<'static, str>,
+}
 
-    let cmd = ResetStderr(cmd);
+fn apple_os_sdk_parts(os: AppleOs, arch: &AppleArchSpec) -> AppleSdkTargetParts {
+    let (sdk_prefix, sim_prefix) = match os {
+        AppleOs::MacOs => ("macosx", ""),
+        AppleOs::Ios => ("iphone", "ios-"),
+        AppleOs::WatchOs => ("watch", "watch"),
+        AppleOs::TvOs => ("appletv", "appletv"),
+    };
+    let sdk = match arch {
+        AppleArchSpec::Device(_) if os == AppleOs::MacOs => Cow::Borrowed("macosx"),
+        AppleArchSpec::Device(_) => format!("{}os", sdk_prefix).into(),
+        AppleArchSpec::Simulator(_) => format!("{}simulator", sdk_prefix).into(),
+        AppleArchSpec::Catalyst(_) => Cow::Borrowed("macosx"),
+    };
 
-    match cmd.0.stderr(pipe_writer).spawn() {
-        Ok(child) => Ok(child),
-        Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
-            let extra = if cfg!(windows) {
-                " (see https://github.com/rust-lang/cc-rs#compile-time-requirements \
-                 for help)"
-            } else {
-                ""
-            };
-            Err(Error::new(
-                ErrorKind::ToolNotFound,
-                format!("Failed to find tool. Is `{}` installed?{}", program, extra),
-            ))
-        }
-        Err(e) => Err(Error::new(
-            ErrorKind::ToolExecError,
-            format!(
-                "Command {:?} with args {:?} failed to start: {:?}",
-                cmd.0, program, e
-            ),
-        )),
+    AppleSdkTargetParts {
+        sdk_prefix,
+        sim_prefix,
+        sdk,
     }
 }
 
-fn fail(s: &str) -> ! {
-    eprintln!("\n\nerror occurred: {}\n\n", s);
-    std::process::exit(1);
-}
-
-fn command_add_output_file(
-    cmd: &mut Command,
-    dst: &Path,
-    cuda: bool,
-    msvc: bool,
-    clang: bool,
-    gnu: bool,
-    is_asm: bool,
-    is_arm: bool,
-) {
-    if msvc && !clang && !gnu && !cuda && !(is_asm && is_arm) {
-        let mut s = OsString::from("-Fo");
-        s.push(&dst);
-        cmd.arg(s);
-    } else {
-        cmd.arg("-o").arg(&dst);
-    }
+#[allow(dead_code)]
+enum AppleArchSpec {
+    Device(&'static str),
+    Simulator(&'static str),
+    #[allow(dead_code)]
+    Catalyst(&'static str),
 }
 
 // Use by default minimum available API level
@@ -3657,7 +3736,7 @@ static NEW_STANDALONE_ANDROID_COMPILERS: [&str; 4] = [
 fn android_clang_compiler_uses_target_arg_internally(clang_path: &Path) -> bool {
     if let Some(filename) = clang_path.file_name() {
         if let Some(filename_str) = filename.to_str() {
-            if let Some(idx) = filename_str.rfind("-") {
+            if let Some(idx) = filename_str.rfind('-') {
                 return filename_str.split_at(idx).0.contains("android");
             }
         }
@@ -3735,7 +3814,9 @@ fn autodetect_android_compiler(target: &str, host: &str, gnu: &str, clang: &str)
 
 // Rust and clang/cc don't agree on how to name the target.
 fn map_darwin_target_from_rust_to_compiler_architecture(target: &str) -> Option<&'static str> {
-    if target.contains("x86_64") {
+    if target.contains("x86_64h") {
+        Some("x86_64h")
+    } else if target.contains("x86_64") {
         Some("x86_64")
     } else if target.contains("arm64e") {
         Some("arm64e")
@@ -3768,13 +3849,23 @@ fn which(tool: &Path, path_entries: Option<OsString>) -> Option<PathBuf> {
     let path_entries = path_entries.or(env::var_os("PATH"))?;
     env::split_paths(&path_entries).find_map(|path_entry| {
         let mut exe = path_entry.join(tool);
-        return if check_exe(&mut exe) { Some(exe) } else { None };
+        if check_exe(&mut exe) {
+            Some(exe)
+        } else {
+            None
+        }
     })
 }
 
 // search for |prog| on 'programs' path in '|cc| -print-search-dirs' output
-fn search_programs(cc: &mut Command, prog: &str) -> Option<PathBuf> {
-    let search_dirs = run_output(cc.arg("-print-search-dirs"), "cc").ok()?;
+fn search_programs(cc: &mut Command, prog: &str, cargo_output: &CargoOutput) -> Option<PathBuf> {
+    let search_dirs = run_output(
+        cc.arg("-print-search-dirs"),
+        "cc",
+        // this doesn't concern the compilation so we always want to show warnings.
+        cargo_output,
+    )
+    .ok()?;
     // clang driver appears to be forcing UTF-8 output even on Windows,
     // hence from_utf8 is assumed to be usable in all cases.
     let search_dirs = std::str::from_utf8(&search_dirs).ok()?;
@@ -3810,58 +3901,3 @@ impl AsmFileExt {
         None
     }
 }
-
-struct PrintThread {
-    handle: Option<JoinHandle<()>>,
-    pipe_writer: Option<File>,
-}
-
-impl PrintThread {
-    fn new() -> Result<Self, Error> {
-        let (pipe_reader, pipe_writer) = os_pipe::pipe()?;
-
-        // Capture the standard error coming from compilation, and write it out
-        // with cargo:warning= prefixes. Note that this is a bit wonky to avoid
-        // requiring the output to be UTF-8, we instead just ship bytes from one
-        // location to another.
-        let print = thread::spawn(move || {
-            let mut stderr = BufReader::with_capacity(4096, pipe_reader);
-            let mut line = String::with_capacity(20);
-            let mut stdout = io::stdout();
-
-            // read_line returns 0 on Eof
-            while stderr.read_line(&mut line).unwrap() != 0 {
-                writeln!(&mut stdout, "cargo:warning={}", line).ok();
-
-                // read_line does not clear the buffer
-                line.clear();
-            }
-        });
-
-        Ok(Self {
-            handle: Some(print),
-            pipe_writer: Some(pipe_writer),
-        })
-    }
-
-    fn pipe_writer(&mut self) -> &mut Option<File> {
-        &mut self.pipe_writer
-    }
-
-    fn pipe_writer_cloned(&self) -> Result<Option<File>, Error> {
-        self.pipe_writer
-            .as_ref()
-            .map(File::try_clone)
-            .transpose()
-            .map_err(From::from)
-    }
-}
-
-impl Drop for PrintThread {
-    fn drop(&mut self) {
-        // Drop pipe_writer first to avoid deadlock
-        self.pipe_writer.take();
-
-        self.handle.take().unwrap().join().unwrap();
-    }
-}
diff --git a/src/os_pipe.rs b/src/os_pipe.rs
deleted file mode 100644
index 218c9bc..0000000
--- a/src/os_pipe.rs
+++ /dev/null
@@ -1,28 +0,0 @@
-//! Adapted from:
-//!  - https://doc.rust-lang.org/src/std/sys/unix/pipe.rs.html
-//!  - https://doc.rust-lang.org/src/std/sys/unix/fd.rs.html#385
-//!  - https://github.com/rust-lang/rust/blob/master/library/std/src/sys/mod.rs#L57
-//!  - https://github.com/oconnor663/os_pipe.rs
-use std::fs::File;
-
-/// Open a new pipe and return a pair of [`File`] objects for the reader and writer.
-///
-/// This corresponds to the `pipe2` library call on Posix and the
-/// `CreatePipe` library call on Windows (though these implementation
-/// details might change). These pipes are non-inheritable, so new child
-/// processes won't receive a copy of them unless they're explicitly
-/// passed as stdin/stdout/stderr.
-pub fn pipe() -> std::io::Result<(File, File)> {
-    sys::pipe()
-}
-
-#[cfg(unix)]
-#[path = "os_pipe/unix.rs"]
-mod sys;
-
-#[cfg(windows)]
-#[path = "os_pipe/windows.rs"]
-mod sys;
-
-#[cfg(all(not(unix), not(windows)))]
-compile_error!("Only unix and windows support os_pipe!");
diff --git a/src/os_pipe/unix.rs b/src/os_pipe/unix.rs
deleted file mode 100644
index ec4e547..0000000
--- a/src/os_pipe/unix.rs
+++ /dev/null
@@ -1,121 +0,0 @@
-use std::{
-    fs::File,
-    io,
-    os::{raw::c_int, unix::io::FromRawFd},
-};
-
-pub(super) fn pipe() -> io::Result<(File, File)> {
-    let mut fds = [0; 2];
-
-    // The only known way right now to create atomically set the CLOEXEC flag is
-    // to use the `pipe2` syscall. This was added to Linux in 2.6.27, glibc 2.9
-    // and musl 0.9.3, and some other targets also have it.
-    #[cfg(any(
-        target_os = "dragonfly",
-        target_os = "freebsd",
-        target_os = "linux",
-        target_os = "netbsd",
-        target_os = "openbsd",
-        target_os = "redox"
-    ))]
-    {
-        unsafe {
-            cvt(libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC))?;
-        }
-    }
-
-    #[cfg(not(any(
-        target_os = "dragonfly",
-        target_os = "freebsd",
-        target_os = "linux",
-        target_os = "netbsd",
-        target_os = "openbsd",
-        target_os = "redox"
-    )))]
-    {
-        unsafe {
-            cvt(libc::pipe(fds.as_mut_ptr()))?;
-        }
-
-        cloexec::set_cloexec(fds[0])?;
-        cloexec::set_cloexec(fds[1])?;
-    }
-
-    unsafe { Ok((File::from_raw_fd(fds[0]), File::from_raw_fd(fds[1]))) }
-}
-
-fn cvt(t: c_int) -> io::Result<c_int> {
-    if t == -1 {
-        Err(io::Error::last_os_error())
-    } else {
-        Ok(t)
-    }
-}
-
-#[cfg(not(any(
-    target_os = "dragonfly",
-    target_os = "freebsd",
-    target_os = "linux",
-    target_os = "netbsd",
-    target_os = "openbsd",
-    target_os = "redox"
-)))]
-mod cloexec {
-    use super::{c_int, cvt, io};
-
-    #[cfg(not(any(
-        target_env = "newlib",
-        target_os = "solaris",
-        target_os = "illumos",
-        target_os = "emscripten",
-        target_os = "fuchsia",
-        target_os = "l4re",
-        target_os = "linux",
-        target_os = "haiku",
-        target_os = "redox",
-        target_os = "vxworks",
-        target_os = "nto",
-    )))]
-    pub(super) fn set_cloexec(fd: c_int) -> io::Result<()> {
-        unsafe {
-            cvt(libc::ioctl(fd, libc::FIOCLEX))?;
-        }
-
-        Ok(())
-    }
-
-    #[cfg(any(
-        all(
-            target_env = "newlib",
-            not(any(target_os = "espidf", target_os = "horizon"))
-        ),
-        target_os = "solaris",
-        target_os = "illumos",
-        target_os = "emscripten",
-        target_os = "fuchsia",
-        target_os = "l4re",
-        target_os = "linux",
-        target_os = "haiku",
-        target_os = "redox",
-        target_os = "vxworks",
-        target_os = "nto",
-    ))]
-    pub(super) fn set_cloexec(fd: c_int) -> io::Result<()> {
-        unsafe {
-            let previous = cvt(libc::fcntl(fd, libc::F_GETFD))?;
-            let new = previous | libc::FD_CLOEXEC;
-            if new != previous {
-                cvt(libc::fcntl(fd, libc::F_SETFD, new))?;
-            }
-        }
-
-        Ok(())
-    }
-
-    // FD_CLOEXEC is not supported in ESP-IDF and Horizon OS but there's no need to,
-    // because neither supports spawning processes.
-    #[cfg(any(target_os = "espidf", target_os = "horizon"))]
-    pub(super) fn set_cloexec(_fd: c_int) -> io::Result<()> {
-        Ok(())
-    }
-}
diff --git a/src/os_pipe/windows.rs b/src/os_pipe/windows.rs
deleted file mode 100644
index 4f7a570..0000000
--- a/src/os_pipe/windows.rs
+++ /dev/null
@@ -1,24 +0,0 @@
-use std::{fs::File, io, os::windows::prelude::*, ptr};
-use windows_sys::Win32::{Foundation::INVALID_HANDLE_VALUE, System::Pipes::CreatePipe};
-
-/// NOTE: These pipes do not support IOCP.
-///
-/// If IOCP is needed, then you might want to emulate
-/// anonymous pipes with CreateNamedPipe, as Rust's stdlib does.
-pub(super) fn pipe() -> io::Result<(File, File)> {
-    let mut read_pipe = INVALID_HANDLE_VALUE;
-    let mut write_pipe = INVALID_HANDLE_VALUE;
-
-    let ret = unsafe { CreatePipe(&mut read_pipe, &mut write_pipe, ptr::null_mut(), 0) };
-
-    if ret == 0 {
-        Err(io::Error::last_os_error())
-    } else {
-        unsafe {
-            Ok((
-                File::from_raw_handle(read_pipe as RawHandle),
-                File::from_raw_handle(write_pipe as RawHandle),
-            ))
-        }
-    }
-}
diff --git a/src/parallel/async_executor.rs b/src/parallel/async_executor.rs
new file mode 100644
index 0000000..9ebd1ad
--- /dev/null
+++ b/src/parallel/async_executor.rs
@@ -0,0 +1,118 @@
+use std::{
+    cell::Cell,
+    future::Future,
+    pin::Pin,
+    ptr,
+    task::{Context, Poll, RawWaker, RawWakerVTable, Waker},
+    thread,
+    time::Duration,
+};
+
+use crate::Error;
+
+const NOOP_WAKER_VTABLE: RawWakerVTable = RawWakerVTable::new(
+    // Cloning just returns a new no-op raw waker
+    |_| NOOP_RAW_WAKER,
+    // `wake` does nothing
+    |_| {},
+    // `wake_by_ref` does nothing
+    |_| {},
+    // Dropping does nothing as we don't allocate anything
+    |_| {},
+);
+const NOOP_RAW_WAKER: RawWaker = RawWaker::new(ptr::null(), &NOOP_WAKER_VTABLE);
+
+#[derive(Default)]
+pub(crate) struct YieldOnce(bool);
+
+impl Future for YieldOnce {
+    type Output = ();
+
+    fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<()> {
+        let flag = &mut std::pin::Pin::into_inner(self).0;
+        if !*flag {
+            *flag = true;
+            Poll::Pending
+        } else {
+            Poll::Ready(())
+        }
+    }
+}
+
+/// Execute the futures and return when they are all done.
+///
+/// Here we use our own homebrew async executor since cc is used in the build
+/// script of many popular projects, pulling in additional dependencies would
+/// significantly slow down its compilation.
+pub(crate) fn block_on<Fut1, Fut2>(
+    mut fut1: Fut1,
+    mut fut2: Fut2,
+    has_made_progress: &Cell<bool>,
+) -> Result<(), Error>
+where
+    Fut1: Future<Output = Result<(), Error>>,
+    Fut2: Future<Output = Result<(), Error>>,
+{
+    // Shadows the future so that it can never be moved and is guaranteed
+    // to be pinned.
+    //
+    // The same trick used in `pin!` macro.
+    //
+    // TODO: Once MSRV is bumped to 1.68, replace this with `std::pin::pin!`
+    let mut fut1 = Some(unsafe { Pin::new_unchecked(&mut fut1) });
+    let mut fut2 = Some(unsafe { Pin::new_unchecked(&mut fut2) });
+
+    // TODO: Once `Waker::noop` stablised and our MSRV is bumped to the version
+    // which it is stablised, replace this with `Waker::noop`.
+    let waker = unsafe { Waker::from_raw(NOOP_RAW_WAKER) };
+    let mut context = Context::from_waker(&waker);
+
+    let mut backoff_cnt = 0;
+
+    loop {
+        has_made_progress.set(false);
+
+        if let Some(fut) = fut2.as_mut() {
+            if let Poll::Ready(res) = fut.as_mut().poll(&mut context) {
+                fut2 = None;
+                res?;
+            }
+        }
+
+        if let Some(fut) = fut1.as_mut() {
+            if let Poll::Ready(res) = fut.as_mut().poll(&mut context) {
+                fut1 = None;
+                res?;
+            }
+        }
+
+        if fut1.is_none() && fut2.is_none() {
+            return Ok(());
+        }
+
+        if !has_made_progress.get() {
+            if backoff_cnt > 3 {
+                // We have yielded at least three times without making'
+                // any progress, so we will sleep for a while.
+                let duration = Duration::from_millis(100 * (backoff_cnt - 3).min(10));
+                thread::sleep(duration);
+            } else {
+                // Given that we spawned a lot of compilation tasks, it is unlikely
+                // that OS cannot find other ready task to execute.
+                //
+                // If all of them are done, then we will yield them and spawn more,
+                // or simply return.
+                //
+                // Thus this will not be turned into a busy-wait loop and it will not
+                // waste CPU resource.
+                thread::yield_now();
+            }
+        }
+
+        backoff_cnt = if has_made_progress.get() {
+            0
+        } else {
+            backoff_cnt + 1
+        };
+    }
+}
diff --git a/src/parallel/job_token/mod.rs b/src/parallel/job_token/mod.rs
new file mode 100644
index 0000000..fda097d
--- /dev/null
+++ b/src/parallel/job_token/mod.rs
@@ -0,0 +1,195 @@
+use std::{mem::MaybeUninit, sync::Once};
+
+use crate::Error;
+
+#[cfg(unix)]
+#[path = "unix.rs"]
+mod sys;
+
+#[cfg(windows)]
+#[path = "windows.rs"]
+mod sys;
+
+pub(crate) struct JobToken();
+
+impl Drop for JobToken {
+    fn drop(&mut self) {
+        match JobTokenServer::new() {
+            JobTokenServer::Inherited(jobserver) => jobserver.release_token_raw(),
+            JobTokenServer::InProcess(jobserver) => jobserver.release_token_raw(),
+        }
+    }
+}
+
+pub(crate) enum JobTokenServer {
+    Inherited(inherited_jobserver::JobServer),
+    InProcess(inprocess_jobserver::JobServer),
+}
+
+impl JobTokenServer {
+    /// This function returns a static reference to the jobserver because
+    ///  - creating a jobserver from env is a bit fd-unsafe (e.g. the fd might
+    ///    be closed by other jobserver users in the process) and better do it
+    ///    at the start of the program.
+    ///  - in case a jobserver cannot be created from env (e.g. it's not
+    ///    present), we will create a global in-process only jobserver
+    ///    that has to be static so that it will be shared by all cc
+    ///    compilation.
+    pub(crate) fn new() -> &'static Self {
+        static INIT: Once = Once::new();
+        static mut JOBSERVER: MaybeUninit<JobTokenServer> = MaybeUninit::uninit();
+
+        unsafe {
+            INIT.call_once(|| {
+                let server = inherited_jobserver::JobServer::from_env()
+                    .map(Self::Inherited)
+                    .unwrap_or_else(|| Self::InProcess(inprocess_jobserver::JobServer::new()));
+                JOBSERVER = MaybeUninit::new(server);
+            });
+            // TODO: Poor man's assume_init_ref, as that'd require a MSRV of 1.55.
+            &*JOBSERVER.as_ptr()
+        }
+    }
+
+    pub(crate) fn try_acquire(&self) -> Result<Option<JobToken>, Error> {
+        match self {
+            Self::Inherited(jobserver) => jobserver.try_acquire(),
+            Self::InProcess(jobserver) => Ok(jobserver.try_acquire()),
+        }
+    }
+}
+
+mod inherited_jobserver {
+    use super::{sys, Error, JobToken};
+
+    use std::{
+        env::var_os,
+        sync::atomic::{
+            AtomicBool,
+            Ordering::{AcqRel, Acquire},
+        },
+    };
+
+    pub(crate) struct JobServer {
+        /// Implicit token for this process which is obtained and will be
+        /// released in parent. Since JobTokens only give back what they got,
+        /// there should be at most one global implicit token in the wild.
+        ///
+        /// Since Rust does not execute any `Drop` for global variables,
+        /// we can't just put it back to jobserver and then re-acquire it at
+        /// the end of the process.
+        global_implicit_token: AtomicBool,
+        inner: sys::JobServerClient,
+    }
+
+    impl JobServer {
+        pub(super) unsafe fn from_env() -> Option<Self> {
+            let var = var_os("CARGO_MAKEFLAGS")
+                .or_else(|| var_os("MAKEFLAGS"))
+                .or_else(|| var_os("MFLAGS"))?;
+
+            #[cfg(unix)]
+            let var = std::os::unix::ffi::OsStrExt::as_bytes(var.as_os_str());
+            #[cfg(not(unix))]
+            let var = var.to_str()?.as_bytes();
+
+            let makeflags = var.split(u8::is_ascii_whitespace);
+
+            // `--jobserver-auth=` is the only documented makeflags.
+            // `--jobserver-fds=` is actually an internal only makeflags, so we should
+            // always prefer `--jobserver-auth=`.
+            //
+            // Also, according to doc of makeflags, if there are multiple `--jobserver-auth=`
+            // the last one is used
+            if let Some(flag) = makeflags
+                .clone()
+                .filter_map(|s| s.strip_prefix(b"--jobserver-auth="))
+                .last()
+            {
+                sys::JobServerClient::open(flag)
+            } else {
+                sys::JobServerClient::open(
+                    makeflags
+                        .filter_map(|s| s.strip_prefix(b"--jobserver-fds="))
+                        .last()?,
+                )
+            }
+            .map(|inner| Self {
+                inner,
+                global_implicit_token: AtomicBool::new(true),
+            })
+        }
+
+        pub(super) fn try_acquire(&self) -> Result<Option<JobToken>, Error> {
+            if !self.global_implicit_token.swap(false, AcqRel) {
+                // Cold path, no global implicit token, obtain one
+                if self.inner.try_acquire()?.is_none() {
+                    return Ok(None);
+                }
+            }
+            Ok(Some(JobToken()))
+        }
+
+        pub(super) fn release_token_raw(&self) {
+            // All tokens will be put back into the jobserver immediately
+            // and they cannot be cached, since Rust does not call `Drop::drop`
+            // on global variables.
+            if self
+                .global_implicit_token
+                .compare_exchange(false, true, AcqRel, Acquire)
+                .is_err()
+            {
+                // There's already a global implicit token, so this token must
+                // be released back into jobserver
+                let _ = self.inner.release();
+            }
+        }
+    }
+}
+
+mod inprocess_jobserver {
+    use super::JobToken;
+
+    use std::{
+        env::var,
+        sync::atomic::{
+            AtomicU32,
+            Ordering::{AcqRel, Acquire},
+        },
+    };
+
+    pub(crate) struct JobServer(AtomicU32);
+
+    impl JobServer {
+        pub(super) fn new() -> Self {
+            // Use `NUM_JOBS` if set (it's configured by Cargo) and otherwise
+            // just fall back to a semi-reasonable number.
+            //
+            // Note that we could use `num_cpus` here but it's an extra
+            // dependency that will almost never be used, so
+            // it's generally not too worth it.
+            let mut parallelism = 4;
+            // TODO: Use std::thread::available_parallelism as an upper bound
+            // when MSRV is bumped.
+            if let Ok(amt) = var("NUM_JOBS") {
+                if let Ok(amt) = amt.parse() {
+                    parallelism = amt;
+                }
+            }
+
+            Self(AtomicU32::new(parallelism))
+        }
+
+        pub(super) fn try_acquire(&self) -> Option<JobToken> {
+            let res = self
+                .0
+                .fetch_update(AcqRel, Acquire, |tokens| tokens.checked_sub(1));
+
+            res.ok().map(|_| JobToken())
+        }
+
+        pub(super) fn release_token_raw(&self) {
+            self.0.fetch_add(1, AcqRel);
+        }
+    }
+}
diff --git a/src/parallel/job_token/unix.rs b/src/parallel/job_token/unix.rs
new file mode 100644
index 0000000..95dcdc8
--- /dev/null
+++ b/src/parallel/job_token/unix.rs
@@ -0,0 +1,156 @@
+use std::{
+    ffi::OsStr,
+    fs::{self, File},
+    io::{self, Read, Write},
+    mem::ManuallyDrop,
+    os::{raw::c_int, unix::prelude::*},
+    path::Path,
+};
+
+use crate::parallel::stderr::set_non_blocking;
+
+pub(super) struct JobServerClient {
+    read: File,
+    write: Option<File>,
+}
+
+impl JobServerClient {
+    pub(super) unsafe fn open(var: &[u8]) -> Option<Self> {
+        if let Some(fifo) = var.strip_prefix(b"fifo:") {
+            Self::from_fifo(Path::new(OsStr::from_bytes(fifo)))
+        } else {
+            Self::from_pipe(OsStr::from_bytes(var).to_str()?)
+        }
+    }
+
+    /// `--jobserver-auth=fifo:PATH`
+    fn from_fifo(path: &Path) -> Option<Self> {
+        let file = fs::OpenOptions::new()
+            .read(true)
+            .write(true)
+            .open(path)
+            .ok()?;
+
+        if is_pipe(&file)? {
+            // File in Rust is always closed-on-exec as long as it's opened by
+            // `File::open` or `fs::OpenOptions::open`.
+            set_non_blocking(&file).ok()?;
+
+            Some(Self {
+                read: file,
+                write: None,
+            })
+        } else {
+            None
+        }
+    }
+
+    /// `--jobserver-auth=fd-for-R,fd-for-W`
+    unsafe fn from_pipe(s: &str) -> Option<Self> {
+        let (read, write) = s.split_once(',')?;
+
+        let read = read.parse().ok()?;
+        let write = write.parse().ok()?;
+
+        let read = ManuallyDrop::new(File::from_raw_fd(read));
+        let write = ManuallyDrop::new(File::from_raw_fd(write));
+
+        // Ok so we've got two integers that look like file descriptors, but
+        // for extra sanity checking let's see if they actually look like
+        // instances of a pipe before we return the client.
+        //
+        // If we're called from `make` *without* the leading + on our rule
+        // then we'll have `MAKEFLAGS` env vars but won't actually have
+        // access to the file descriptors.
+        match (
+            is_pipe(&read),
+            is_pipe(&write),
+            get_access_mode(&read),
+            get_access_mode(&write),
+        ) {
+            (
+                Some(true),
+                Some(true),
+                Some(libc::O_RDONLY) | Some(libc::O_RDWR),
+                Some(libc::O_WRONLY) | Some(libc::O_RDWR),
+            ) => {
+                let read = read.try_clone().ok()?;
+                let write = write.try_clone().ok()?;
+
+                // Set read and write end to nonblocking
+                set_non_blocking(&read).ok()?;
+                set_non_blocking(&write).ok()?;
+
+                Some(Self {
+                    read,
+                    write: Some(write),
+                })
+            }
+            _ => None,
+        }
+    }
+
+    pub(super) fn try_acquire(&self) -> io::Result<Option<()>> {
+        let mut fds = [libc::pollfd {
+            fd: self.read.as_raw_fd(),
+            events: libc::POLLIN,
+            revents: 0,
+        }];
+
+        let ret = cvt(unsafe { libc::poll(fds.as_mut_ptr(), 1, 0) })?;
+        if ret == 1 {
+            let mut buf = [0];
+            match (&self.read).read(&mut buf) {
+                Ok(1) => Ok(Some(())),
+                Ok(_) => Ok(None), // 0, eof
+                Err(e)
+                    if e.kind() == io::ErrorKind::Interrupted
+                        || e.kind() == io::ErrorKind::WouldBlock =>
+                {
+                    Ok(None)
+                }
+                Err(e) => Err(e),
+            }
+        } else {
+            Ok(None)
+        }
+    }
+
+    pub(super) fn release(&self) -> io::Result<()> {
+        // For write to block, this would mean that pipe is full.
+        // If all every release are pair with an acquire, then this cannot
+        // happen.
+        //
+        // If it does happen, it is likely a bug in the program using this
+        // crate or some other programs that use the same jobserver have a
+        // bug in their code.
+        //
+        // If that turns out to not be the case we'll get an error anyway!
+        let mut write = self.write.as_ref().unwrap_or(&self.read);
+        match write.write(&[b'+'])? {
+            1 => Ok(()),
+            _ => Err(io::Error::from(io::ErrorKind::UnexpectedEof)),
+        }
+    }
+}
+
+fn cvt(t: c_int) -> io::Result<c_int> {
+    if t == -1 {
+        Err(io::Error::last_os_error())
+    } else {
+        Ok(t)
+    }
+}
+
+fn is_pipe(file: &File) -> Option<bool> {
+    Some(file.metadata().ok()?.file_type().is_fifo())
+}
+
+fn get_access_mode(file: &File) -> Option<c_int> {
+    let ret = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GETFL) };
+    if ret == -1 {
+        return None;
+    }
+
+    Some(ret & libc::O_ACCMODE)
+}
diff --git a/src/parallel/job_token/windows.rs b/src/parallel/job_token/windows.rs
new file mode 100644
index 0000000..434fe16
--- /dev/null
+++ b/src/parallel/job_token/windows.rs
@@ -0,0 +1,68 @@
+use std::{ffi::CString, io, ptr, str};
+
+use crate::windows::windows_sys::{
+    OpenSemaphoreA, ReleaseSemaphore, WaitForSingleObject, FALSE, HANDLE, SEMAPHORE_MODIFY_STATE,
+    THREAD_SYNCHRONIZE, WAIT_ABANDONED, WAIT_FAILED, WAIT_OBJECT_0, WAIT_TIMEOUT,
+};
+
+pub(super) struct JobServerClient {
+    sem: HANDLE,
+}
+
+unsafe impl Sync for JobServerClient {}
+unsafe impl Send for JobServerClient {}
+
+impl JobServerClient {
+    pub(super) unsafe fn open(var: &[u8]) -> Option<Self> {
+        let var = str::from_utf8(var).ok()?;
+        if !var.is_ascii() {
+            // `OpenSemaphoreA` only accepts ASCII, not utf-8.
+            //
+            // Upstream implementation jobserver and jobslot also uses the
+            // same function and they works without problem, so there's no
+            // motivation to support utf-8 here using `OpenSemaphoreW`
+            // which only makes the code harder to maintain by making it more
+            // different than upstream.
+            return None;
+        }
+
+        let name = CString::new(var).ok()?;
+
+        let sem = OpenSemaphoreA(
+            THREAD_SYNCHRONIZE | SEMAPHORE_MODIFY_STATE,
+            FALSE,
+            name.as_bytes().as_ptr(),
+        );
+        if sem != ptr::null_mut() {
+            Some(Self { sem })
+        } else {
+            None
+        }
+    }
+
+    pub(super) fn try_acquire(&self) -> io::Result<Option<()>> {
+        match unsafe { WaitForSingleObject(self.sem, 0) } {
+            WAIT_OBJECT_0 => Ok(Some(())),
+            WAIT_TIMEOUT => Ok(None),
+            WAIT_FAILED => Err(io::Error::last_os_error()),
+            // We believe this should be impossible for a semaphore, but still
+            // check the error code just in case it happens.
+            WAIT_ABANDONED => Err(io::Error::new(
+                io::ErrorKind::Other,
+                "Wait on jobserver semaphore returned WAIT_ABANDONED",
+            )),
+            _ => unreachable!("Unexpected return value from WaitForSingleObject"),
+        }
+    }
+
+    pub(super) fn release(&self) -> io::Result<()> {
+        // SAFETY: ReleaseSemaphore will write to prev_count is it is Some
+        // and release semaphore self.sem by 1.
+        let r = unsafe { ReleaseSemaphore(self.sem, 1, ptr::null_mut()) };
+        if r != 0 {
+            Ok(())
+        } else {
+            Err(io::Error::last_os_error())
+        }
+    }
+}
diff --git a/src/parallel/mod.rs b/src/parallel/mod.rs
new file mode 100644
index 0000000..d69146d
--- /dev/null
+++ b/src/parallel/mod.rs
@@ -0,0 +1,20 @@
+pub(crate) mod async_executor;
+pub(crate) mod job_token;
+pub(crate) mod stderr;
+
+/// Remove all element in `vec` which `f(element)` returns `false`.
+///
+/// TODO: Remove this once the MSRV is bumped to v1.61
+pub(crate) fn retain_unordered_mut<T, F>(vec: &mut Vec<T>, mut f: F)
+where
+    F: FnMut(&mut T) -> bool,
+{
+    let mut i = 0;
+    while i < vec.len() {
+        if f(&mut vec[i]) {
+            i += 1;
+        } else {
+            vec.swap_remove(i);
+        }
+    }
+}
diff --git a/src/parallel/stderr.rs b/src/parallel/stderr.rs
new file mode 100644
index 0000000..8bdfd40
--- /dev/null
+++ b/src/parallel/stderr.rs
@@ -0,0 +1,78 @@
+/// Helpers functions for [ChildStderr].
+use std::{convert::TryInto, process::ChildStderr};
+
+use crate::{Error, ErrorKind};
+
+#[cfg(all(not(unix), not(windows)))]
+compile_error!("Only unix and windows support non-blocking pipes! For other OSes, disable the parallel feature.");
+
+#[cfg(unix)]
+pub fn set_non_blocking(pipe: &impl std::os::unix::io::AsRawFd) -> Result<(), Error> {
+    // On Unix, switch the pipe to non-blocking mode.
+    // On Windows, we have a different way to be non-blocking.
+    let fd = pipe.as_raw_fd();
+    let flags = unsafe { libc::fcntl(fd, libc::F_GETFL, 0) };
+    if flags == -1 {
+        return Err(Error::new(
+            ErrorKind::IOError,
+            format!(
+                "Failed to get flags for pipe {}: {}",
+                fd,
+                std::io::Error::last_os_error()
+            ),
+        ));
+    }
+
+    if unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) } == -1 {
+        return Err(Error::new(
+            ErrorKind::IOError,
+            format!(
+                "Failed to set flags for pipe {}: {}",
+                fd,
+                std::io::Error::last_os_error()
+            ),
+        ));
+    }
+
+    Ok(())
+}
+
+pub fn bytes_available(stderr: &mut ChildStderr) -> Result<usize, Error> {
+    let mut bytes_available = 0;
+    #[cfg(windows)]
+    {
+        use crate::windows::windows_sys::PeekNamedPipe;
+        use std::os::windows::io::AsRawHandle;
+        use std::ptr::null_mut;
+        if unsafe {
+            PeekNamedPipe(
+                stderr.as_raw_handle(),
+                null_mut(),
+                0,
+                null_mut(),
+                &mut bytes_available,
+                null_mut(),
+            )
+        } == 0
+        {
+            return Err(Error::new(
+                ErrorKind::IOError,
+                format!(
+                    "PeekNamedPipe failed with {}",
+                    std::io::Error::last_os_error()
+                ),
+            ));
+        }
+    }
+    #[cfg(unix)]
+    {
+        use std::os::unix::io::AsRawFd;
+        if unsafe { libc::ioctl(stderr.as_raw_fd(), libc::FIONREAD, &mut bytes_available) } != 0 {
+            return Err(Error::new(
+                ErrorKind::IOError,
+                format!("ioctl failed with {}", std::io::Error::last_os_error()),
+            ));
+        }
+    }
+    Ok(bytes_available.try_into().unwrap())
+}
diff --git a/src/tool.rs b/src/tool.rs
new file mode 100644
index 0000000..39131ea
--- /dev/null
+++ b/src/tool.rs
@@ -0,0 +1,400 @@
+use std::{
+    collections::HashMap,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    process::Command,
+    sync::Mutex,
+};
+
+use crate::command_helpers::{run_output, CargoOutput};
+
+/// Configuration used to represent an invocation of a C compiler.
+///
+/// This can be used to figure out what compiler is in use, what the arguments
+/// to it are, and what the environment variables look like for the compiler.
+/// This can be used to further configure other build systems (e.g. forward
+/// along CC and/or CFLAGS) or the `to_command` method can be used to run the
+/// compiler itself.
+#[derive(Clone, Debug)]
+#[allow(missing_docs)]
+pub struct Tool {
+    pub(crate) path: PathBuf,
+    pub(crate) cc_wrapper_path: Option<PathBuf>,
+    pub(crate) cc_wrapper_args: Vec<OsString>,
+    pub(crate) args: Vec<OsString>,
+    pub(crate) env: Vec<(OsString, OsString)>,
+    pub(crate) family: ToolFamily,
+    pub(crate) cuda: bool,
+    pub(crate) removed_args: Vec<OsString>,
+    pub(crate) has_internal_target_arg: bool,
+}
+
+impl Tool {
+    pub(crate) fn new(
+        path: PathBuf,
+        cached_compiler_family: &Mutex<HashMap<Box<Path>, ToolFamily>>,
+        cargo_output: &CargoOutput,
+    ) -> Self {
+        Self::with_features(path, None, false, cached_compiler_family, cargo_output)
+    }
+
+    pub(crate) fn with_clang_driver(
+        path: PathBuf,
+        clang_driver: Option<&str>,
+        cached_compiler_family: &Mutex<HashMap<Box<Path>, ToolFamily>>,
+        cargo_output: &CargoOutput,
+    ) -> Self {
+        Self::with_features(
+            path,
+            clang_driver,
+            false,
+            cached_compiler_family,
+            cargo_output,
+        )
+    }
+
+    #[cfg(windows)]
+    /// Explicitly set the `ToolFamily`, skipping name-based detection.
+    pub(crate) fn with_family(path: PathBuf, family: ToolFamily) -> Self {
+        Self {
+            path,
+            cc_wrapper_path: None,
+            cc_wrapper_args: Vec::new(),
+            args: Vec::new(),
+            env: Vec::new(),
+            family,
+            cuda: false,
+            removed_args: Vec::new(),
+            has_internal_target_arg: false,
+        }
+    }
+
+    pub(crate) fn with_features(
+        path: PathBuf,
+        clang_driver: Option<&str>,
+        cuda: bool,
+        cached_compiler_family: &Mutex<HashMap<Box<Path>, ToolFamily>>,
+        cargo_output: &CargoOutput,
+    ) -> Self {
+        fn detect_family_inner(path: &Path, cargo_output: &CargoOutput) -> ToolFamily {
+            let mut cmd = Command::new(path);
+            cmd.arg("--version");
+
+            let stdout = match run_output(
+                &mut cmd,
+                &path.to_string_lossy(),
+                // tool detection issues should always be shown as warnings
+                cargo_output,
+            )
+            .ok()
+            .and_then(|o| String::from_utf8(o).ok())
+            {
+                Some(s) => s,
+                None => {
+                    // --version failed. fallback to gnu
+                    cargo_output.print_warning(&format_args!("Failed to run: {:?}", cmd));
+                    return ToolFamily::Gnu;
+                }
+            };
+            if stdout.contains("clang") {
+                ToolFamily::Clang
+            } else if stdout.contains("GCC") {
+                ToolFamily::Gnu
+            } else {
+                // --version doesn't include clang for GCC
+                cargo_output.print_warning(&format_args!(
+                    "Compiler version doesn't include clang or GCC: {:?}",
+                    cmd
+                ));
+                ToolFamily::Gnu
+            }
+        }
+        let detect_family = |path: &Path| -> ToolFamily {
+            if let Some(family) = cached_compiler_family.lock().unwrap().get(path) {
+                return *family;
+            }
+
+            let family = detect_family_inner(path, cargo_output);
+            cached_compiler_family
+                .lock()
+                .unwrap()
+                .insert(path.into(), family);
+            family
+        };
+
+        // Try to detect family of the tool from its name, falling back to Gnu.
+        let family = if let Some(fname) = path.file_name().and_then(|p| p.to_str()) {
+            if fname.contains("clang-cl") {
+                ToolFamily::Msvc { clang_cl: true }
+            } else if fname.ends_with("cl") || fname == "cl.exe" {
+                ToolFamily::Msvc { clang_cl: false }
+            } else if fname.contains("clang") {
+                match clang_driver {
+                    Some("cl") => ToolFamily::Msvc { clang_cl: true },
+                    _ => ToolFamily::Clang,
+                }
+            } else {
+                detect_family(&path)
+            }
+        } else {
+            detect_family(&path)
+        };
+
+        Tool {
+            path,
+            cc_wrapper_path: None,
+            cc_wrapper_args: Vec::new(),
+            args: Vec::new(),
+            env: Vec::new(),
+            family,
+            cuda,
+            removed_args: Vec::new(),
+            has_internal_target_arg: false,
+        }
+    }
+
+    /// Add an argument to be stripped from the final command arguments.
+    pub(crate) fn remove_arg(&mut self, flag: OsString) {
+        self.removed_args.push(flag);
+    }
+
+    /// Push an "exotic" flag to the end of the compiler's arguments list.
+    ///
+    /// Nvidia compiler accepts only the most common compiler flags like `-D`,
+    /// `-I`, `-c`, etc. Options meant specifically for the underlying
+    /// host C++ compiler have to be prefixed with `-Xcompiler`.
+    /// [Another possible future application for this function is passing
+    /// clang-specific flags to clang-cl, which otherwise accepts only
+    /// MSVC-specific options.]
+    pub(crate) fn push_cc_arg(&mut self, flag: OsString) {
+        if self.cuda {
+            self.args.push("-Xcompiler".into());
+        }
+        self.args.push(flag);
+    }
+
+    /// Checks if an argument or flag has already been specified or conflicts.
+    ///
+    /// Currently only checks optimization flags.
+    pub(crate) fn is_duplicate_opt_arg(&self, flag: &OsString) -> bool {
+        let flag = flag.to_str().unwrap();
+        let mut chars = flag.chars();
+
+        // Only duplicate check compiler flags
+        if self.is_like_msvc() {
+            if chars.next() != Some('/') {
+                return false;
+            }
+        } else if self.is_like_gnu() || self.is_like_clang() {
+            if chars.next() != Some('-') {
+                return false;
+            }
+        }
+
+        // Check for existing optimization flags (-O, /O)
+        if chars.next() == Some('O') {
+            return self
+                .args()
+                .iter()
+                .any(|a| a.to_str().unwrap_or("").chars().nth(1) == Some('O'));
+        }
+
+        // TODO Check for existing -m..., -m...=..., /arch:... flags
+        false
+    }
+
+    /// Don't push optimization arg if it conflicts with existing args.
+    pub(crate) fn push_opt_unless_duplicate(&mut self, flag: OsString) {
+        if self.is_duplicate_opt_arg(&flag) {
+            println!("Info: Ignoring duplicate arg {:?}", &flag);
+        } else {
+            self.push_cc_arg(flag);
+        }
+    }
+
+    /// Converts this compiler into a `Command` that's ready to be run.
+    ///
+    /// This is useful for when the compiler needs to be executed and the
+    /// command returned will already have the initial arguments and environment
+    /// variables configured.
+    pub fn to_command(&self) -> Command {
+        let mut cmd = match self.cc_wrapper_path {
+            Some(ref cc_wrapper_path) => {
+                let mut cmd = Command::new(cc_wrapper_path);
+                cmd.arg(&self.path);
+                cmd
+            }
+            None => Command::new(&self.path),
+        };
+        cmd.args(&self.cc_wrapper_args);
+
+        let value = self
+            .args
+            .iter()
+            .filter(|a| !self.removed_args.contains(a))
+            .collect::<Vec<_>>();
+        cmd.args(&value);
+
+        for (k, v) in self.env.iter() {
+            cmd.env(k, v);
+        }
+        cmd
+    }
+
+    /// Returns the path for this compiler.
+    ///
+    /// Note that this may not be a path to a file on the filesystem, e.g. "cc",
+    /// but rather something which will be resolved when a process is spawned.
+    pub fn path(&self) -> &Path {
+        &self.path
+    }
+
+    /// Returns the default set of arguments to the compiler needed to produce
+    /// executables for the target this compiler generates.
+    pub fn args(&self) -> &[OsString] {
+        &self.args
+    }
+
+    /// Returns the set of environment variables needed for this compiler to
+    /// operate.
+    ///
+    /// This is typically only used for MSVC compilers currently.
+    pub fn env(&self) -> &[(OsString, OsString)] {
+        &self.env
+    }
+
+    /// Returns the compiler command in format of CC environment variable.
+    /// Or empty string if CC env was not present
+    ///
+    /// This is typically used by configure script
+    pub fn cc_env(&self) -> OsString {
+        match self.cc_wrapper_path {
+            Some(ref cc_wrapper_path) => {
+                let mut cc_env = cc_wrapper_path.as_os_str().to_owned();
+                cc_env.push(" ");
+                cc_env.push(self.path.to_path_buf().into_os_string());
+                for arg in self.cc_wrapper_args.iter() {
+                    cc_env.push(" ");
+                    cc_env.push(arg);
+                }
+                cc_env
+            }
+            None => OsString::from(""),
+        }
+    }
+
+    /// Returns the compiler flags in format of CFLAGS environment variable.
+    /// Important here - this will not be CFLAGS from env, its internal gcc's flags to use as CFLAGS
+    /// This is typically used by configure script
+    pub fn cflags_env(&self) -> OsString {
+        let mut flags = OsString::new();
+        for (i, arg) in self.args.iter().enumerate() {
+            if i > 0 {
+                flags.push(" ");
+            }
+            flags.push(arg);
+        }
+        flags
+    }
+
+    /// Whether the tool is GNU Compiler Collection-like.
+    pub fn is_like_gnu(&self) -> bool {
+        self.family == ToolFamily::Gnu
+    }
+
+    /// Whether the tool is Clang-like.
+    pub fn is_like_clang(&self) -> bool {
+        self.family == ToolFamily::Clang
+    }
+
+    /// Whether the tool is AppleClang under .xctoolchain
+    #[cfg(target_vendor = "apple")]
+    pub(crate) fn is_xctoolchain_clang(&self) -> bool {
+        let path = self.path.to_string_lossy();
+        path.contains(".xctoolchain/")
+    }
+    #[cfg(not(target_vendor = "apple"))]
+    pub(crate) fn is_xctoolchain_clang(&self) -> bool {
+        false
+    }
+
+    /// Whether the tool is MSVC-like.
+    pub fn is_like_msvc(&self) -> bool {
+        match self.family {
+            ToolFamily::Msvc { .. } => true,
+            _ => false,
+        }
+    }
+}
+
+/// Represents the family of tools this tool belongs to.
+///
+/// Each family of tools differs in how and what arguments they accept.
+///
+/// Detection of a family is done on best-effort basis and may not accurately reflect the tool.
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub enum ToolFamily {
+    /// Tool is GNU Compiler Collection-like.
+    Gnu,
+    /// Tool is Clang-like. It differs from the GCC in a sense that it accepts superset of flags
+    /// and its cross-compilation approach is different.
+    Clang,
+    /// Tool is the MSVC cl.exe.
+    Msvc { clang_cl: bool },
+}
+
+impl ToolFamily {
+    /// What the flag to request debug info for this family of tools look like
+    pub(crate) fn add_debug_flags(&self, cmd: &mut Tool, dwarf_version: Option<u32>) {
+        match *self {
+            ToolFamily::Msvc { .. } => {
+                cmd.push_cc_arg("-Z7".into());
+            }
+            ToolFamily::Gnu | ToolFamily::Clang => {
+                cmd.push_cc_arg(
+                    dwarf_version
+                        .map_or_else(|| "-g".into(), |v| format!("-gdwarf-{}", v))
+                        .into(),
+                );
+            }
+        }
+    }
+
+    /// What the flag to force frame pointers.
+    pub(crate) fn add_force_frame_pointer(&self, cmd: &mut Tool) {
+        match *self {
+            ToolFamily::Gnu | ToolFamily::Clang => {
+                cmd.push_cc_arg("-fno-omit-frame-pointer".into());
+            }
+            _ => (),
+        }
+    }
+
+    /// What the flags to enable all warnings
+    pub(crate) fn warnings_flags(&self) -> &'static str {
+        match *self {
+            ToolFamily::Msvc { .. } => "-W4",
+            ToolFamily::Gnu | ToolFamily::Clang => "-Wall",
+        }
+    }
+
+    /// What the flags to enable extra warnings
+    pub(crate) fn extra_warnings_flags(&self) -> Option<&'static str> {
+        match *self {
+            ToolFamily::Msvc { .. } => None,
+            ToolFamily::Gnu | ToolFamily::Clang => Some("-Wextra"),
+        }
+    }
+
+    /// What the flag to turn warning into errors
+    pub(crate) fn warnings_to_errors_flag(&self) -> &'static str {
+        match *self {
+            ToolFamily::Msvc { .. } => "-WX",
+            ToolFamily::Gnu | ToolFamily::Clang => "-Werror",
+        }
+    }
+
+    pub(crate) fn verbose_stderr(&self) -> bool {
+        *self == ToolFamily::Clang
+    }
+}
diff --git a/src/com.rs b/src/windows/com.rs
similarity index 93%
rename from src/com.rs
rename to src/windows/com.rs
index 6231412..e81bb1d 100644
--- a/src/com.rs
+++ b/src/windows/com.rs
@@ -7,8 +7,15 @@
 
 #![allow(unused)]
 
-use crate::winapi::{IUnknown, Interface};
+use crate::windows::{
+    winapi::{IUnknown, Interface},
+    windows_sys::{
+        CoInitializeEx, SysFreeString, SysStringLen, BSTR, COINIT_MULTITHREADED, HRESULT, S_FALSE,
+        S_OK,
+    },
+};
 use std::{
+    convert::TryInto,
     ffi::{OsStr, OsString},
     mem::ManuallyDrop,
     ops::Deref,
@@ -16,16 +23,9 @@ use std::{
     ptr::{null, null_mut},
     slice::from_raw_parts,
 };
-use windows_sys::{
-    core::{BSTR, HRESULT},
-    Win32::{
-        Foundation::{SysFreeString, SysStringLen, S_FALSE, S_OK},
-        System::Com::{CoInitializeEx, COINIT_MULTITHREADED},
-    },
-};
 
 pub fn initialize() -> Result<(), HRESULT> {
-    let err = unsafe { CoInitializeEx(null(), COINIT_MULTITHREADED) };
+    let err = unsafe { CoInitializeEx(null(), COINIT_MULTITHREADED.try_into().unwrap()) };
     if err != S_OK && err != S_FALSE {
         // S_FALSE just means COM is already initialized
         Err(err)
@@ -65,7 +65,7 @@ where
     fn as_unknown(&self) -> &IUnknown {
         unsafe { &*(self.0 as *mut IUnknown) }
     }
-    /// Performs QueryInterface fun.
+    /// Performs `QueryInterface` fun.
     pub fn cast<U>(&self) -> Result<ComPtr<U>, i32>
     where
         U: Interface,
diff --git a/src/windows_registry.rs b/src/windows/find_tools.rs
similarity index 73%
rename from src/windows_registry.rs
rename to src/windows/find_tools.rs
index 07a9c4d..9c65115 100644
--- a/src/windows_registry.rs
+++ b/src/windows/find_tools.rs
@@ -11,6 +11,8 @@
 //! A helper module to probe the Windows Registry when looking for
 //! windows-specific tools.
 
+#![allow(clippy::upper_case_acronyms)]
+
 use std::process::Command;
 
 use crate::Tool;
@@ -53,6 +55,9 @@ pub fn find_tool(target: &str, tool: &str) -> Option<Tool> {
         return None;
     }
 
+    // Split the target to get the arch.
+    let target = impl_::TargetArch(target.split_once('-')?.0);
+
     // Looks like msbuild isn't located in the same location as other tools like
     // cl.exe and lib.exe. To handle this we probe for it manually with
     // dedicated registry keys.
@@ -71,11 +76,11 @@ pub fn find_tool(target: &str, tool: &str) -> Option<Tool> {
     // environment variables like `LIB`, `INCLUDE`, and `PATH` to ensure that
     // the tool is actually usable.
 
-    return impl_::find_msvc_environment(tool, target)
+    impl_::find_msvc_environment(tool, target)
         .or_else(|| impl_::find_msvc_15plus(tool, target))
         .or_else(|| impl_::find_msvc_14(tool, target))
         .or_else(|| impl_::find_msvc_12(tool, target))
-        .or_else(|| impl_::find_msvc_11(tool, target));
+        .or_else(|| impl_::find_msvc_11(tool, target))
 }
 
 /// A version of Visual Studio
@@ -154,10 +159,14 @@ pub fn find_vs_version() -> Result<VsVers, String> {
 
 #[cfg(windows)]
 mod impl_ {
-    use crate::com;
-    use crate::registry::{RegistryKey, LOCAL_MACHINE};
-    use crate::setup_config::SetupConfiguration;
-    use crate::vs_instances::{VsInstances, VswhereInstance};
+    use crate::windows::com;
+    use crate::windows::registry::{RegistryKey, LOCAL_MACHINE};
+    use crate::windows::setup_config::SetupConfiguration;
+    use crate::windows::vs_instances::{VsInstances, VswhereInstance};
+    use crate::windows::windows_sys::{
+        FreeLibrary, GetMachineTypeAttributes, GetProcAddress, LoadLibraryA, UserEnabled, HMODULE,
+        IMAGE_FILE_MACHINE_AMD64, MACHINE_ATTRIBUTES, S_OK,
+    };
     use std::convert::TryFrom;
     use std::env;
     use std::ffi::OsString;
@@ -168,10 +177,27 @@ mod impl_ {
     use std::path::{Path, PathBuf};
     use std::process::Command;
     use std::str::FromStr;
+    use std::sync::atomic::{AtomicBool, Ordering};
+    use std::sync::Once;
 
     use super::MSVC_FAMILY;
     use crate::Tool;
 
+    #[derive(Copy, Clone)]
+    pub struct TargetArch<'a>(pub &'a str);
+
+    impl PartialEq<&str> for TargetArch<'_> {
+        fn eq(&self, other: &&str) -> bool {
+            self.0 == *other
+        }
+    }
+
+    impl<'a> From<TargetArch<'a>> for &'a str {
+        fn from(target: TargetArch<'a>) -> Self {
+            target.0
+        }
+    }
+
     struct MsvcTool {
         tool: PathBuf,
         libs: Vec<PathBuf>,
@@ -179,10 +205,75 @@ mod impl_ {
         include: Vec<PathBuf>,
     }
 
+    struct LibraryHandle(HMODULE);
+
+    impl LibraryHandle {
+        fn new(name: &[u8]) -> Option<Self> {
+            let handle = unsafe { LoadLibraryA(name.as_ptr() as _) };
+            (!handle.is_null()).then(|| Self(handle))
+        }
+
+        /// Get a function pointer to a function in the library.
+        /// SAFETY: The caller must ensure that the function signature matches the actual function.
+        /// The easiest way to do this is to add an entry to windows_sys_no_link.list and use the
+        /// generated function for `func_signature`.
+        unsafe fn get_proc_address<F>(&self, name: &[u8]) -> Option<F> {
+            let symbol = unsafe { GetProcAddress(self.0, name.as_ptr() as _) };
+            symbol.map(|symbol| unsafe { mem::transmute_copy(&symbol) })
+        }
+    }
+
+    impl Drop for LibraryHandle {
+        fn drop(&mut self) {
+            unsafe { FreeLibrary(self.0) };
+        }
+    }
+
+    type GetMachineTypeAttributesFuncType =
+        unsafe extern "system" fn(u16, *mut MACHINE_ATTRIBUTES) -> i32;
+    const _: () = {
+        // Ensure that our hand-written signature matches the actual function signature.
+        // We can't use `GetMachineTypeAttributes` outside of a const scope otherwise we'll end up statically linking to
+        // it, which will fail to load on older versions of Windows.
+        let _: GetMachineTypeAttributesFuncType = GetMachineTypeAttributes;
+    };
+
+    fn is_amd64_emulation_supported_inner() -> Option<bool> {
+        // GetMachineTypeAttributes is only available on Win11 22000+, so dynamically load it.
+        let kernel32 = LibraryHandle::new(b"kernel32.dll\0")?;
+        // SAFETY: GetMachineTypeAttributesFuncType is checked to match the real function signature.
+        let get_machine_type_attributes = unsafe {
+            kernel32
+                .get_proc_address::<GetMachineTypeAttributesFuncType>(b"GetMachineTypeAttributes\0")
+        }?;
+        let mut attributes = Default::default();
+        if unsafe { get_machine_type_attributes(IMAGE_FILE_MACHINE_AMD64, &mut attributes) } == S_OK
+        {
+            Some((attributes & UserEnabled) != 0)
+        } else {
+            Some(false)
+        }
+    }
+
+    fn is_amd64_emulation_supported() -> bool {
+        // TODO: Replace with a OnceLock once MSRV is 1.70.
+        static LOAD_VALUE: Once = Once::new();
+        static IS_SUPPORTED: AtomicBool = AtomicBool::new(false);
+
+        // Using Relaxed ordering since the Once is providing synchronization.
+        LOAD_VALUE.call_once(|| {
+            IS_SUPPORTED.store(
+                is_amd64_emulation_supported_inner().unwrap_or(false),
+                Ordering::Relaxed,
+            );
+        });
+        IS_SUPPORTED.load(Ordering::Relaxed)
+    }
+
     impl MsvcTool {
         fn new(tool: PathBuf) -> MsvcTool {
             MsvcTool {
-                tool: tool,
+                tool,
                 libs: Vec::new(),
                 path: Vec::new(),
                 include: Vec::new(),
@@ -196,7 +287,7 @@ mod impl_ {
                 path,
                 include,
             } = self;
-            let mut tool = Tool::with_family(tool.into(), MSVC_FAMILY);
+            let mut tool = Tool::with_family(tool, MSVC_FAMILY);
             add_env(&mut tool, "LIB", libs);
             add_env(&mut tool, "PATH", path);
             add_env(&mut tool, "INCLUDE", include);
@@ -206,15 +297,14 @@ mod impl_ {
 
     /// Checks to see if the `VSCMD_ARG_TGT_ARCH` environment variable matches the
     /// given target's arch. Returns `None` if the variable does not exist.
-    #[cfg(windows)]
-    fn is_vscmd_target(target: &str) -> Option<bool> {
+    fn is_vscmd_target(target: TargetArch<'_>) -> Option<bool> {
         let vscmd_arch = env::var("VSCMD_ARG_TGT_ARCH").ok()?;
         // Convert the Rust target arch to its VS arch equivalent.
-        let arch = match target.split("-").next() {
-            Some("x86_64") => "x64",
-            Some("aarch64") => "arm64",
-            Some("i686") | Some("i586") => "x86",
-            Some("thumbv7a") => "arm",
+        let arch = match target.into() {
+            "x86_64" => "x64",
+            "aarch64" | "arm64ec" => "arm64",
+            "i686" | "i586" => "x86",
+            "thumbv7a" => "arm",
             // An unrecognized arch.
             _ => return Some(false),
         };
@@ -222,7 +312,7 @@ mod impl_ {
     }
 
     /// Attempt to find the tool using environment variables set by vcvars.
-    pub fn find_msvc_environment(tool: &str, target: &str) -> Option<Tool> {
+    pub fn find_msvc_environment(tool: &str, target: TargetArch<'_>) -> Option<Tool> {
         // Early return if the environment doesn't contain a VC install.
         if env::var_os("VCINSTALLDIR").is_none() {
             return None;
@@ -242,16 +332,19 @@ mod impl_ {
                         .map(|p| p.join(tool))
                         .find(|p| p.exists())
                 })
-                .map(|path| Tool::with_family(path.into(), MSVC_FAMILY))
+                .map(|path| Tool::with_family(path, MSVC_FAMILY))
         }
     }
 
-    fn find_msbuild_vs17(target: &str) -> Option<Tool> {
+    fn find_msbuild_vs17(target: TargetArch<'_>) -> Option<Tool> {
         find_tool_in_vs16plus_path(r"MSBuild\Current\Bin\MSBuild.exe", target, "17")
     }
 
     #[allow(bare_trait_objects)]
-    fn vs16plus_instances(target: &str, version: &'static str) -> Box<Iterator<Item = PathBuf>> {
+    fn vs16plus_instances(
+        target: TargetArch<'_>,
+        version: &'static str,
+    ) -> Box<Iterator<Item = PathBuf>> {
         let instances = if let Some(instances) = vs15plus_instances(target) {
             instances
         } else {
@@ -269,7 +362,11 @@ mod impl_ {
         }))
     }
 
-    fn find_tool_in_vs16plus_path(tool: &str, target: &str, version: &'static str) -> Option<Tool> {
+    fn find_tool_in_vs16plus_path(
+        tool: &str,
+        target: TargetArch<'_>,
+        version: &'static str,
+    ) -> Option<Tool> {
         vs16plus_instances(target, version)
             .filter_map(|path| {
                 let path = path.join(tool);
@@ -277,10 +374,10 @@ mod impl_ {
                     return None;
                 }
                 let mut tool = Tool::with_family(path, MSVC_FAMILY);
-                if target.contains("x86_64") {
+                if target == "x86_64" {
                     tool.env.push(("Platform".into(), "X64".into()));
                 }
-                if target.contains("aarch64") {
+                if target == "aarch64" || target == "arm64ec" {
                     tool.env.push(("Platform".into(), "ARM64".into()));
                 }
                 Some(tool)
@@ -288,7 +385,7 @@ mod impl_ {
             .next()
     }
 
-    fn find_msbuild_vs16(target: &str) -> Option<Tool> {
+    fn find_msbuild_vs16(target: TargetArch<'_>) -> Option<Tool> {
         find_tool_in_vs16plus_path(r"MSBuild\Current\Bin\MSBuild.exe", target, "16")
     }
 
@@ -304,7 +401,7 @@ mod impl_ {
     //
     // However, on ARM64 this method doesn't work because VS Installer fails to register COM component on ARM64.
     // Hence, as the last resort we try to use vswhere.exe to list available instances.
-    fn vs15plus_instances(target: &str) -> Option<VsInstances> {
+    fn vs15plus_instances(target: TargetArch<'_>) -> Option<VsInstances> {
         vs15plus_instances_using_com().or_else(|| vs15plus_instances_using_vswhere(target))
     }
 
@@ -317,7 +414,7 @@ mod impl_ {
         Some(VsInstances::ComBased(enum_setup_instances))
     }
 
-    fn vs15plus_instances_using_vswhere(target: &str) -> Option<VsInstances> {
+    fn vs15plus_instances_using_vswhere(target: TargetArch<'_>) -> Option<VsInstances> {
         let program_files_path: PathBuf = env::var("ProgramFiles(x86)")
             .or_else(|_| env::var("ProgramFiles"))
             .ok()?
@@ -330,11 +427,10 @@ mod impl_ {
             return None;
         }
 
-        let arch = target.split('-').next().unwrap();
-        let tools_arch = match arch {
+        let tools_arch = match target.into() {
             "i586" | "i686" | "x86_64" => Some("x86.x64"),
             "arm" | "thumbv7a" => Some("ARM"),
-            "aarch64" => Some("ARM64"),
+            "aarch64" | "arm64ec" => Some("ARM64"),
             _ => None,
         };
 
@@ -368,7 +464,7 @@ mod impl_ {
             .collect()
     }
 
-    pub fn find_msvc_15plus(tool: &str, target: &str) -> Option<Tool> {
+    pub fn find_msvc_15plus(tool: &str, target: TargetArch<'_>) -> Option<Tool> {
         let iter = vs15plus_instances(target)?;
         iter.into_iter()
             .filter_map(|instance| {
@@ -388,13 +484,13 @@ mod impl_ {
     // we keep the registry method as a fallback option.
     //
     // [more reliable]: https://github.com/rust-lang/cc-rs/pull/331
-    fn find_tool_in_vs15_path(tool: &str, target: &str) -> Option<Tool> {
+    fn find_tool_in_vs15_path(tool: &str, target: TargetArch<'_>) -> Option<Tool> {
         let mut path = match vs15plus_instances(target) {
             Some(instances) => instances
                 .into_iter()
                 .filter_map(|instance| instance.installation_path())
                 .map(|path| path.join(tool))
-                .find(|ref path| path.is_file()),
+                .find(|path| path.is_file()),
             None => None,
         };
 
@@ -410,10 +506,9 @@ mod impl_ {
 
         path.map(|path| {
             let mut tool = Tool::with_family(path, MSVC_FAMILY);
-            if target.contains("x86_64") {
+            if target == "x86_64" {
                 tool.env.push(("Platform".into(), "X64".into()));
-            }
-            if target.contains("aarch64") {
+            } else if target == "aarch64" {
                 tool.env.push(("Platform".into(), "ARM64".into()));
             }
             tool
@@ -422,10 +517,10 @@ mod impl_ {
 
     fn tool_from_vs15plus_instance(
         tool: &str,
-        target: &str,
+        target: TargetArch<'_>,
         instance_path: &PathBuf,
     ) -> Option<Tool> {
-        let (root_path, bin_path, host_dylib_path, lib_path, include_path) =
+        let (root_path, bin_path, host_dylib_path, lib_path, alt_lib_path, include_path) =
             vs15plus_vc_paths(target, instance_path)?;
         let tool_path = bin_path.join(tool);
         if !tool_path.exists() {
@@ -435,6 +530,9 @@ mod impl_ {
         let mut tool = MsvcTool::new(tool_path);
         tool.path.push(bin_path.clone());
         tool.path.push(host_dylib_path);
+        if let Some(alt_lib_path) = alt_lib_path {
+            tool.libs.push(alt_lib_path);
+        }
         tool.libs.push(lib_path);
         tool.include.push(include_path);
 
@@ -449,45 +547,97 @@ mod impl_ {
     }
 
     fn vs15plus_vc_paths(
-        target: &str,
-        instance_path: &PathBuf,
-    ) -> Option<(PathBuf, PathBuf, PathBuf, PathBuf, PathBuf)> {
-        let version_path =
-            instance_path.join(r"VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt");
-        let mut version_file = File::open(version_path).ok()?;
-        let mut version = String::new();
-        version_file.read_to_string(&mut version).ok()?;
-        let version = version.trim();
-        let host = match host_arch() {
-            X86 => "X86",
-            X86_64 => "X64",
-            // There is no natively hosted compiler on ARM64.
-            // Instead, use the x86 toolchain under emulation (there is no x64 emulation).
-            AARCH64 => "X86",
+        target: TargetArch<'_>,
+        instance_path: &Path,
+    ) -> Option<(PathBuf, PathBuf, PathBuf, PathBuf, Option<PathBuf>, PathBuf)> {
+        let version = vs15plus_vc_read_version(instance_path)?;
+
+        let hosts = match host_arch() {
+            X86 => &["X86"],
+            X86_64 => &["X64"],
+            // Starting with VS 17.4, there is a natively hosted compiler on ARM64:
+            // https://devblogs.microsoft.com/visualstudio/arm64-visual-studio-is-officially-here/
+            // On older versions of VS, we use x64 if running under emulation is supported,
+            // otherwise use x86.
+            AARCH64 => {
+                if is_amd64_emulation_supported() {
+                    &["ARM64", "X64", "X86"][..]
+                } else {
+                    &["ARM64", "X86"]
+                }
+            }
             _ => return None,
         };
         let target = lib_subdir(target)?;
         // The directory layout here is MSVC/bin/Host$host/$target/
         let path = instance_path.join(r"VC\Tools\MSVC").join(version);
+        // We use the first available host architecture that can build for the target
+        let (host_path, host) = hosts.iter().find_map(|&x| {
+            let candidate = path.join("bin").join(format!("Host{}", x));
+            if candidate.join(target).exists() {
+                Some((candidate, x))
+            } else {
+                None
+            }
+        })?;
         // This is the path to the toolchain for a particular target, running
         // on a given host
-        let bin_path = path
-            .join("bin")
-            .join(&format!("Host{}", host))
-            .join(&target);
+        let bin_path = host_path.join(target);
         // But! we also need PATH to contain the target directory for the host
         // architecture, because it contains dlls like mspdb140.dll compiled for
         // the host architecture.
-        let host_dylib_path = path
-            .join("bin")
-            .join(&format!("Host{}", host))
-            .join(&host.to_lowercase());
-        let lib_path = path.join("lib").join(&target);
+        let host_dylib_path = host_path.join(host.to_lowercase());
+        let lib_path = path.join("lib").join(target);
+        let alt_lib_path = (target == "arm64ec").then(|| path.join("lib").join("arm64ec"));
         let include_path = path.join("include");
-        Some((path, bin_path, host_dylib_path, lib_path, include_path))
+        Some((
+            path,
+            bin_path,
+            host_dylib_path,
+            lib_path,
+            alt_lib_path,
+            include_path,
+        ))
     }
 
-    fn atl_paths(target: &str, path: &Path) -> Option<(PathBuf, PathBuf)> {
+    fn vs15plus_vc_read_version(dir: &Path) -> Option<String> {
+        // Try to open the default version file.
+        let mut version_path: PathBuf =
+            dir.join(r"VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt");
+        let mut version_file = if let Ok(f) = File::open(&version_path) {
+            f
+        } else {
+            // If the default doesn't exist, search for other version files.
+            // These are in the form Microsoft.VCToolsVersion.v143.default.txt
+            // where `143` is any three decimal digit version number.
+            // This sorts versions by lexical order and selects the highest version.
+            let mut version_file = String::new();
+            version_path.pop();
+            for file in version_path.read_dir().ok()? {
+                let name = file.ok()?.file_name();
+                let name = name.to_str()?;
+                if name.starts_with("Microsoft.VCToolsVersion.v")
+                    && name.ends_with(".default.txt")
+                    && name > &version_file
+                {
+                    version_file.replace_range(.., name);
+                }
+            }
+            if version_file.is_empty() {
+                return None;
+            }
+            version_path.push(version_file);
+            File::open(version_path).ok()?
+        };
+
+        // Get the version string from the file we found.
+        let mut version = String::new();
+        version_file.read_to_string(&mut version).ok()?;
+        version.truncate(version.trim_end().len());
+        Some(version)
+    }
+
+    fn atl_paths(target: TargetArch<'_>, path: &Path) -> Option<(PathBuf, PathBuf)> {
         let atl_path = path.join("atlmfc");
         let sub = lib_subdir(target)?;
         if atl_path.exists() {
@@ -499,14 +649,14 @@ mod impl_ {
 
     // For MSVC 14 we need to find the Universal CRT as well as either
     // the Windows 10 SDK or Windows 8.1 SDK.
-    pub fn find_msvc_14(tool: &str, target: &str) -> Option<Tool> {
+    pub fn find_msvc_14(tool: &str, target: TargetArch<'_>) -> Option<Tool> {
         let vcdir = get_vc_dir("14.0")?;
         let mut tool = get_tool(tool, &vcdir, target)?;
         add_sdks(&mut tool, target)?;
         Some(tool.into_tool())
     }
 
-    fn add_sdks(tool: &mut MsvcTool, target: &str) -> Option<()> {
+    fn add_sdks(tool: &mut MsvcTool, target: TargetArch<'_>) -> Option<()> {
         let sub = lib_subdir(target)?;
         let (ucrt, ucrt_version) = get_ucrt_dir()?;
 
@@ -549,7 +699,7 @@ mod impl_ {
     }
 
     // For MSVC 12 we need to find the Windows 8.1 SDK.
-    pub fn find_msvc_12(tool: &str, target: &str) -> Option<Tool> {
+    pub fn find_msvc_12(tool: &str, target: TargetArch<'_>) -> Option<Tool> {
         let vcdir = get_vc_dir("12.0")?;
         let mut tool = get_tool(tool, &vcdir, target)?;
         let sub = lib_subdir(target)?;
@@ -565,7 +715,7 @@ mod impl_ {
     }
 
     // For MSVC 11 we need to find the Windows 8 SDK.
-    pub fn find_msvc_11(tool: &str, target: &str) -> Option<Tool> {
+    pub fn find_msvc_11(tool: &str, target: TargetArch<'_>) -> Option<Tool> {
         let vcdir = get_vc_dir("11.0")?;
         let mut tool = get_tool(tool, &vcdir, target)?;
         let sub = lib_subdir(target)?;
@@ -590,7 +740,7 @@ mod impl_ {
 
     // Given a possible MSVC installation directory, we look for the linker and
     // then add the MSVC library path.
-    fn get_tool(tool: &str, path: &Path, target: &str) -> Option<MsvcTool> {
+    fn get_tool(tool: &str, path: &Path, target: TargetArch<'_>) -> Option<MsvcTool> {
         bin_subdir(target)
             .into_iter()
             .map(|(sub, host)| {
@@ -599,7 +749,7 @@ mod impl_ {
                     path.join("bin").join(host),
                 )
             })
-            .filter(|&(ref path, _)| path.is_file())
+            .filter(|(path, _)| path.is_file())
             .map(|(path, host)| {
                 let mut tool = MsvcTool::new(path);
                 tool.path.push(host);
@@ -728,9 +878,8 @@ mod impl_ {
     // linkers that can target the architecture we desire. The 64-bit host
     // linker is preferred, and hence first, due to 64-bit allowing it more
     // address space to work with and potentially being faster.
-    fn bin_subdir(target: &str) -> Vec<(&'static str, &'static str)> {
-        let arch = target.split('-').next().unwrap();
-        match (arch, host_arch()) {
+    fn bin_subdir(target: TargetArch<'_>) -> Vec<(&'static str, &'static str)> {
+        match (target.into(), host_arch()) {
             ("i586", X86) | ("i686", X86) => vec![("", "")],
             ("i586", X86_64) | ("i686", X86_64) => vec![("amd64_x86", "amd64"), ("", "")],
             ("x86_64", X86) => vec![("x86_amd64", "")],
@@ -741,21 +890,19 @@ mod impl_ {
         }
     }
 
-    fn lib_subdir(target: &str) -> Option<&'static str> {
-        let arch = target.split('-').next().unwrap();
-        match arch {
+    fn lib_subdir(target: TargetArch<'_>) -> Option<&'static str> {
+        match target.into() {
             "i586" | "i686" => Some("x86"),
             "x86_64" => Some("x64"),
             "arm" | "thumbv7a" => Some("arm"),
-            "aarch64" => Some("arm64"),
+            "aarch64" | "arm64ec" => Some("arm64"),
             _ => None,
         }
     }
 
     // MSVC's x86 libraries are not in a subfolder
-    fn vc_lib_subdir(target: &str) -> Option<&'static str> {
-        let arch = target.split('-').next().unwrap();
-        match arch {
+    fn vc_lib_subdir(target: TargetArch<'_>) -> Option<&'static str> {
+        match target.into() {
             "i586" | "i686" => Some(""),
             "x86_64" => Some("amd64"),
             "arm" | "thumbv7a" => Some("arm"),
@@ -807,7 +954,7 @@ mod impl_ {
         for subkey in key.iter().filter_map(|k| k.ok()) {
             let val = subkey
                 .to_str()
-                .and_then(|s| s.trim_left_matches("v").replace(".", "").parse().ok());
+                .and_then(|s| s.trim_left_matches("v").replace('.', "").parse().ok());
             let val = match val {
                 Some(s) => s,
                 None => continue,
@@ -825,19 +972,19 @@ mod impl_ {
     pub fn has_msbuild_version(version: &str) -> bool {
         match version {
             "17.0" => {
-                find_msbuild_vs17("x86_64-pc-windows-msvc").is_some()
-                    || find_msbuild_vs17("i686-pc-windows-msvc").is_some()
-                    || find_msbuild_vs17("aarch64-pc-windows-msvc").is_some()
+                find_msbuild_vs17(TargetArch("x86_64")).is_some()
+                    || find_msbuild_vs17(TargetArch("i686")).is_some()
+                    || find_msbuild_vs17(TargetArch("aarch64")).is_some()
             }
             "16.0" => {
-                find_msbuild_vs16("x86_64-pc-windows-msvc").is_some()
-                    || find_msbuild_vs16("i686-pc-windows-msvc").is_some()
-                    || find_msbuild_vs16("aarch64-pc-windows-msvc").is_some()
+                find_msbuild_vs16(TargetArch("x86_64")).is_some()
+                    || find_msbuild_vs16(TargetArch("i686")).is_some()
+                    || find_msbuild_vs16(TargetArch("aarch64")).is_some()
             }
             "15.0" => {
-                find_msbuild_vs15("x86_64-pc-windows-msvc").is_some()
-                    || find_msbuild_vs15("i686-pc-windows-msvc").is_some()
-                    || find_msbuild_vs15("aarch64-pc-windows-msvc").is_some()
+                find_msbuild_vs15(TargetArch("x86_64")).is_some()
+                    || find_msbuild_vs15(TargetArch("i686")).is_some()
+                    || find_msbuild_vs15(TargetArch("aarch64")).is_some()
             }
             "12.0" | "14.0" => LOCAL_MACHINE
                 .open(&OsString::from(format!(
@@ -849,19 +996,19 @@ mod impl_ {
         }
     }
 
-    pub fn find_devenv(target: &str) -> Option<Tool> {
-        find_devenv_vs15(&target)
+    pub fn find_devenv(target: TargetArch<'_>) -> Option<Tool> {
+        find_devenv_vs15(target)
     }
 
-    fn find_devenv_vs15(target: &str) -> Option<Tool> {
+    fn find_devenv_vs15(target: TargetArch<'_>) -> Option<Tool> {
         find_tool_in_vs15_path(r"Common7\IDE\devenv.exe", target)
     }
 
     // see http://stackoverflow.com/questions/328017/path-to-msbuild
-    pub fn find_msbuild(target: &str) -> Option<Tool> {
+    pub fn find_msbuild(target: TargetArch<'_>) -> Option<Tool> {
         // VS 15 (2017) changed how to locate msbuild
         if let Some(r) = find_msbuild_vs17(target) {
-            return Some(r);
+            Some(r)
         } else if let Some(r) = find_msbuild_vs16(target) {
             return Some(r);
         } else if let Some(r) = find_msbuild_vs15(target) {
@@ -871,11 +1018,11 @@ mod impl_ {
         }
     }
 
-    fn find_msbuild_vs15(target: &str) -> Option<Tool> {
+    fn find_msbuild_vs15(target: TargetArch<'_>) -> Option<Tool> {
         find_tool_in_vs15_path(r"MSBuild\15.0\Bin\MSBuild.exe", target)
     }
 
-    fn find_old_msbuild(target: &str) -> Option<Tool> {
+    fn find_old_msbuild(target: TargetArch<'_>) -> Option<Tool> {
         let key = r"SOFTWARE\Microsoft\MSBuild\ToolsVersions";
         LOCAL_MACHINE
             .open(key.as_ref())
@@ -887,7 +1034,7 @@ mod impl_ {
                 let mut path = PathBuf::from(path);
                 path.push("MSBuild.exe");
                 let mut tool = Tool::with_family(path, MSVC_FAMILY);
-                if target.contains("x86_64") {
+                if target == "x86_64" {
                     tool.env.push(("Platform".into(), "X64".into()));
                 }
                 tool
diff --git a/src/windows/mod.rs b/src/windows/mod.rs
new file mode 100644
index 0000000..9b6f297
--- /dev/null
+++ b/src/windows/mod.rs
@@ -0,0 +1,20 @@
+//! These modules are all glue to support reading the MSVC version from
+//! the registry and from COM interfaces.
+
+// This is used in the crate's public API, so don't use #[cfg(windows)]
+pub mod find_tools;
+
+#[cfg(windows)]
+pub(crate) mod windows_sys;
+
+#[cfg(windows)]
+mod registry;
+#[cfg(windows)]
+#[macro_use]
+mod winapi;
+#[cfg(windows)]
+mod com;
+#[cfg(windows)]
+mod setup_config;
+#[cfg(windows)]
+mod vs_instances;
diff --git a/src/registry.rs b/src/windows/registry.rs
similarity index 95%
rename from src/registry.rs
rename to src/windows/registry.rs
index bd5fbb2..8398303 100644
--- a/src/registry.rs
+++ b/src/windows/registry.rs
@@ -8,6 +8,10 @@
 // option. This file may not be copied, modified, or distributed
 // except according to those terms.
 
+use crate::windows::windows_sys::{
+    RegCloseKey, RegEnumKeyExW, RegOpenKeyExW, RegQueryValueExW, ERROR_NO_MORE_ITEMS,
+    ERROR_SUCCESS, HKEY, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_32KEY, REG_SZ,
+};
 use std::{
     ffi::{OsStr, OsString},
     io,
@@ -15,17 +19,11 @@ use std::{
     os::windows::prelude::*,
     ptr::null_mut,
 };
-use windows_sys::Win32::{
-    Foundation::{ERROR_NO_MORE_ITEMS, ERROR_SUCCESS},
-    System::Registry::{
-        RegCloseKey, RegEnumKeyExW, RegOpenKeyExW, RegQueryValueExW, HKEY, HKEY_LOCAL_MACHINE,
-        KEY_READ, KEY_WOW64_32KEY, REG_SZ,
-    },
-};
 
 /// Must never be `HKEY_PERFORMANCE_DATA`.
 pub(crate) struct RegistryKey(Repr);
 
+#[allow(clippy::upper_case_acronyms)]
 type DWORD = u32;
 
 struct OwnedKey(HKEY);
@@ -59,7 +57,7 @@ impl RegistryKey {
     /// Open a sub-key of `self`.
     pub fn open(&self, key: &OsStr) -> io::Result<RegistryKey> {
         let key = key.encode_wide().chain(Some(0)).collect::<Vec<_>>();
-        let mut ret = 0;
+        let mut ret = null_mut();
         let err = unsafe {
             RegOpenKeyExW(
                 self.raw(),
@@ -150,7 +148,7 @@ impl RegistryKey {
             if !v.is_empty() && v[v.len() - 1] == 0 {
                 v.pop();
             }
-            return Ok(OsString::from_wide(&v));
+            Ok(OsString::from_wide(&v))
         }
     }
 }
diff --git a/src/setup_config.rs b/src/windows/setup_config.rs
similarity index 98%
rename from src/setup_config.rs
rename to src/windows/setup_config.rs
index a738b96..5739ecf 100644
--- a/src/setup_config.rs
+++ b/src/windows/setup_config.rs
@@ -8,25 +8,19 @@
 #![allow(bad_style)]
 #![allow(unused)]
 
-use crate::{
+use crate::windows::{
     com::{BStr, ComPtr},
     winapi::{
         IUnknown, IUnknownVtbl, Interface, LCID, LPCOLESTR, LPCWSTR, LPFILETIME, LPSAFEARRAY,
         PULONGLONG, ULONG,
     },
+    windows_sys::{CoCreateInstance, BSTR, CLSCTX_ALL, HRESULT, S_FALSE},
 };
 
 use std::{
     ffi::OsString,
     ptr::{null, null_mut},
 };
-use windows_sys::{
-    core::{BSTR, HRESULT},
-    Win32::{
-        Foundation::S_FALSE,
-        System::Com::{CoCreateInstance, CLSCTX_ALL},
-    },
-};
 
 // Bindings to the Setup.Configuration stuff
 pub type InstanceState = u32;
diff --git a/src/vs_instances.rs b/src/windows/vs_instances.rs
similarity index 98%
rename from src/vs_instances.rs
rename to src/windows/vs_instances.rs
index 31d3dd1..e863dad 100644
--- a/src/vs_instances.rs
+++ b/src/windows/vs_instances.rs
@@ -4,7 +4,7 @@ use std::convert::TryFrom;
 use std::io::BufRead;
 use std::path::PathBuf;
 
-use crate::setup_config::{EnumSetupInstances, SetupInstance};
+use crate::windows::setup_config::{EnumSetupInstances, SetupInstance};
 
 pub enum VsInstance {
     Com(SetupInstance),
diff --git a/src/winapi.rs b/src/windows/winapi.rs
similarity index 91%
rename from src/winapi.rs
rename to src/windows/winapi.rs
index 93bea3c..09965da 100644
--- a/src/winapi.rs
+++ b/src/windows/winapi.rs
@@ -5,25 +5,18 @@
 // All files in the project carrying such notice may not be copied, modified, or distributed
 // except according to those terms.
 
-#![allow(bad_style)]
+#![allow(bad_style, clippy::upper_case_acronyms)]
 
 use std::os::raw;
 
 pub type wchar_t = u16;
 
-pub use windows_sys::{
-    core::GUID,
-    Win32::{
-        Foundation::FILETIME,
-        System::Com::{SAFEARRAY, SAFEARRAYBOUND},
-    },
-};
+pub use crate::windows::windows_sys::{FILETIME, GUID, HRESULT, SAFEARRAY};
 
 pub type REFIID = *const IID;
 pub type IID = GUID;
 pub type ULONG = raw::c_ulong;
 pub type DWORD = u32;
-pub type HRESULT = raw::c_long;
 pub type LPFILETIME = *mut FILETIME;
 pub type OLECHAR = WCHAR;
 pub type WCHAR = wchar_t;
@@ -44,7 +37,7 @@ macro_rules! DEFINE_GUID {
         $name:ident, $l:expr, $w1:expr, $w2:expr,
         $b1:expr, $b2:expr, $b3:expr, $b4:expr, $b5:expr, $b6:expr, $b7:expr, $b8:expr
     ) => {
-        pub const $name: $crate::winapi::GUID = $crate::winapi::GUID {
+        pub const $name: $crate::windows::winapi::GUID = $crate::windows::winapi::GUID {
             data1: $l,
             data2: $w1,
             data3: $w2,
@@ -128,10 +121,10 @@ macro_rules! RIDL {
         $l:expr, $w1:expr, $w2:expr,
         $b1:expr, $b2:expr, $b3:expr, $b4:expr, $b5:expr, $b6:expr, $b7:expr, $b8:expr
     ) => (
-        impl $crate::winapi::Interface for $interface {
+        impl $crate::windows::winapi::Interface for $interface {
             #[inline]
-            fn uuidof() -> $crate::winapi::GUID {
-                $crate::winapi::GUID {
+            fn uuidof() -> $crate::windows::winapi::GUID {
+                $crate::windows::winapi::GUID {
                     data1: $l,
                     data2: $w1,
                     data3: $w2,
diff --git a/src/windows/windows_sys.rs b/src/windows/windows_sys.rs
new file mode 100644
index 0000000..8b98ce9
--- /dev/null
+++ b/src/windows/windows_sys.rs
@@ -0,0 +1,223 @@
+// This file is autogenerated.
+//
+// To add bindings, edit windows_sys.lst then run:
+//
+// ```
+// cd generate-windows-sys/
+// cargo run
+// ```
+// Bindings generated by `windows-bindgen` 0.53.0
+
+#![allow(
+    non_snake_case,
+    non_upper_case_globals,
+    non_camel_case_types,
+    dead_code,
+    clippy::all
+)]
+#[link(name = "advapi32")]
+extern "system" {
+    pub fn RegCloseKey(hkey: HKEY) -> WIN32_ERROR;
+}
+#[link(name = "advapi32")]
+extern "system" {
+    pub fn RegEnumKeyExW(
+        hkey: HKEY,
+        dwindex: u32,
+        lpname: PWSTR,
+        lpcchname: *mut u32,
+        lpreserved: *const u32,
+        lpclass: PWSTR,
+        lpcchclass: *mut u32,
+        lpftlastwritetime: *mut FILETIME,
+    ) -> WIN32_ERROR;
+}
+#[link(name = "advapi32")]
+extern "system" {
+    pub fn RegOpenKeyExW(
+        hkey: HKEY,
+        lpsubkey: PCWSTR,
+        uloptions: u32,
+        samdesired: REG_SAM_FLAGS,
+        phkresult: *mut HKEY,
+    ) -> WIN32_ERROR;
+}
+#[link(name = "advapi32")]
+extern "system" {
+    pub fn RegQueryValueExW(
+        hkey: HKEY,
+        lpvaluename: PCWSTR,
+        lpreserved: *const u32,
+        lptype: *mut REG_VALUE_TYPE,
+        lpdata: *mut u8,
+        lpcbdata: *mut u32,
+    ) -> WIN32_ERROR;
+}
+#[link(name = "kernel32")]
+extern "system" {
+    pub fn FreeLibrary(hlibmodule: HMODULE) -> BOOL;
+}
+#[link(name = "kernel32")]
+extern "system" {
+    pub fn GetMachineTypeAttributes(
+        machine: u16,
+        machinetypeattributes: *mut MACHINE_ATTRIBUTES,
+    ) -> HRESULT;
+}
+#[link(name = "kernel32")]
+extern "system" {
+    pub fn GetProcAddress(hmodule: HMODULE, lpprocname: PCSTR) -> FARPROC;
+}
+#[link(name = "kernel32")]
+extern "system" {
+    pub fn LoadLibraryA(lplibfilename: PCSTR) -> HMODULE;
+}
+#[link(name = "kernel32")]
+extern "system" {
+    pub fn OpenSemaphoreA(dwdesiredaccess: u32, binherithandle: BOOL, lpname: PCSTR) -> HANDLE;
+}
+#[link(name = "kernel32")]
+extern "system" {
+    pub fn PeekNamedPipe(
+        hnamedpipe: HANDLE,
+        lpbuffer: *mut ::core::ffi::c_void,
+        nbuffersize: u32,
+        lpbytesread: *mut u32,
+        lptotalbytesavail: *mut u32,
+        lpbytesleftthismessage: *mut u32,
+    ) -> BOOL;
+}
+#[link(name = "kernel32")]
+extern "system" {
+    pub fn ReleaseSemaphore(
+        hsemaphore: HANDLE,
+        lreleasecount: i32,
+        lppreviouscount: *mut i32,
+    ) -> BOOL;
+}
+#[link(name = "kernel32")]
+extern "system" {
+    pub fn WaitForSingleObject(hhandle: HANDLE, dwmilliseconds: u32) -> WAIT_EVENT;
+}
+#[link(name = "ole32")]
+extern "system" {
+    pub fn CoCreateInstance(
+        rclsid: *const GUID,
+        punkouter: *mut ::core::ffi::c_void,
+        dwclscontext: CLSCTX,
+        riid: *const GUID,
+        ppv: *mut *mut ::core::ffi::c_void,
+    ) -> HRESULT;
+}
+#[link(name = "ole32")]
+extern "system" {
+    pub fn CoInitializeEx(pvreserved: *const ::core::ffi::c_void, dwcoinit: u32) -> HRESULT;
+}
+#[link(name = "oleaut32")]
+extern "system" {
+    pub fn SysFreeString(bstrstring: BSTR);
+}
+#[link(name = "oleaut32")]
+extern "system" {
+    pub fn SysStringLen(pbstr: BSTR) -> u32;
+}
+pub type ADVANCED_FEATURE_FLAGS = u16;
+pub type BOOL = i32;
+pub type BSTR = *const u16;
+pub type CLSCTX = u32;
+pub const CLSCTX_ALL: CLSCTX = 23u32;
+pub type COINIT = i32;
+pub const COINIT_MULTITHREADED: COINIT = 0i32;
+pub const ERROR_NO_MORE_ITEMS: WIN32_ERROR = 259u32;
+pub const ERROR_SUCCESS: WIN32_ERROR = 0u32;
+pub const FALSE: BOOL = 0i32;
+pub type FARPROC = ::core::option::Option<unsafe extern "system" fn() -> isize>;
+#[repr(C)]
+pub struct FILETIME {
+    pub dwLowDateTime: u32,
+    pub dwHighDateTime: u32,
+}
+impl ::core::marker::Copy for FILETIME {}
+impl ::core::clone::Clone for FILETIME {
+    fn clone(&self) -> Self {
+        *self
+    }
+}
+#[repr(C)]
+pub struct GUID {
+    pub data1: u32,
+    pub data2: u16,
+    pub data3: u16,
+    pub data4: [u8; 8],
+}
+impl ::core::marker::Copy for GUID {}
+impl ::core::clone::Clone for GUID {
+    fn clone(&self) -> Self {
+        *self
+    }
+}
+impl GUID {
+    pub const fn from_u128(uuid: u128) -> Self {
+        Self {
+            data1: (uuid >> 96) as u32,
+            data2: (uuid >> 80 & 0xffff) as u16,
+            data3: (uuid >> 64 & 0xffff) as u16,
+            data4: (uuid as u64).to_be_bytes(),
+        }
+    }
+}
+pub type HANDLE = *mut ::core::ffi::c_void;
+pub type HKEY = *mut ::core::ffi::c_void;
+pub const HKEY_LOCAL_MACHINE: HKEY = -2147483646i32 as _;
+pub type HMODULE = *mut ::core::ffi::c_void;
+pub type HRESULT = i32;
+pub type IMAGE_FILE_MACHINE = u16;
+pub const IMAGE_FILE_MACHINE_AMD64: IMAGE_FILE_MACHINE = 34404u16;
+pub const KEY_READ: REG_SAM_FLAGS = 131097u32;
+pub const KEY_WOW64_32KEY: REG_SAM_FLAGS = 512u32;
+pub type MACHINE_ATTRIBUTES = i32;
+pub type PCSTR = *const u8;
+pub type PCWSTR = *const u16;
+pub type PWSTR = *mut u16;
+pub type REG_SAM_FLAGS = u32;
+pub const REG_SZ: REG_VALUE_TYPE = 1u32;
+pub type REG_VALUE_TYPE = u32;
+#[repr(C)]
+pub struct SAFEARRAY {
+    pub cDims: u16,
+    pub fFeatures: ADVANCED_FEATURE_FLAGS,
+    pub cbElements: u32,
+    pub cLocks: u32,
+    pub pvData: *mut ::core::ffi::c_void,
+    pub rgsabound: [SAFEARRAYBOUND; 1],
+}
+impl ::core::marker::Copy for SAFEARRAY {}
+impl ::core::clone::Clone for SAFEARRAY {
+    fn clone(&self) -> Self {
+        *self
+    }
+}
+#[repr(C)]
+pub struct SAFEARRAYBOUND {
+    pub cElements: u32,
+    pub lLbound: i32,
+}
+impl ::core::marker::Copy for SAFEARRAYBOUND {}
+impl ::core::clone::Clone for SAFEARRAYBOUND {
+    fn clone(&self) -> Self {
+        *self
+    }
+}
+pub const SEMAPHORE_MODIFY_STATE: SYNCHRONIZATION_ACCESS_RIGHTS = 2u32;
+pub type SYNCHRONIZATION_ACCESS_RIGHTS = u32;
+pub const S_FALSE: HRESULT = 0x1_u32 as _;
+pub const S_OK: HRESULT = 0x0_u32 as _;
+pub type THREAD_ACCESS_RIGHTS = u32;
+pub const THREAD_SYNCHRONIZE: THREAD_ACCESS_RIGHTS = 1048576u32;
+pub const UserEnabled: MACHINE_ATTRIBUTES = 1i32;
+pub const WAIT_ABANDONED: WAIT_EVENT = 128u32;
+pub type WAIT_EVENT = u32;
+pub const WAIT_FAILED: WAIT_EVENT = 4294967295u32;
+pub const WAIT_OBJECT_0: WAIT_EVENT = 0u32;
+pub const WAIT_TIMEOUT: WAIT_EVENT = 258u32;
+pub type WIN32_ERROR = u32;


Test patch:
diff --git a/cc-test/Cargo.toml b/dev-tools/cc-test/Cargo.toml
similarity index 86%
rename from cc-test/Cargo.toml
rename to dev-tools/cc-test/Cargo.toml
index 2b2013e..cc7747e 100644
--- a/cc-test/Cargo.toml
+++ b/dev-tools/cc-test/Cargo.toml
@@ -3,6 +3,7 @@ name = "cc-test"
 version = "0.1.0"
 authors = ["Alex Crichton <alex@alexcrichton.com>"]
 edition = "2018"
+publish = false
 
 [lib]
 name = "cc_test"
@@ -10,7 +11,7 @@ doctest = false
 test = false
 
 [build-dependencies]
-cc = { path = ".." }
+cc = { path = "../.." }
 which = "^4.0"
 
 [features]
diff --git a/cc-test/build.rs b/dev-tools/cc-test/build.rs
similarity index 55%
rename from cc-test/build.rs
rename to dev-tools/cc-test/build.rs
index 695c754..8968eb5 100644
--- a/cc-test/build.rs
+++ b/dev-tools/cc-test/build.rs
@@ -1,12 +1,35 @@
 use std::env;
 use std::fs;
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
+use std::process::Command;
 
 fn main() {
+    // if we are being executed from a `fork_run_action` call (i.e. this is a
+    // "fork"), perform the requested action and then return.
+    if run_action_if_forked() {
+        return;
+    }
+
     let out = PathBuf::from(env::var_os("OUT_DIR").unwrap());
     fs::remove_dir_all(&out).unwrap();
     fs::create_dir(&out).unwrap();
 
+    // The following are builds where we want to capture the output (i.e. stdout and
+    // stderr). We do that by re-running _this_ executable and passing in the
+    // action as the first argument.
+    run_forked_capture_output(&out, "metadata-on");
+    run_forked_capture_output(&out, "metadata-off");
+
+    run_forked_capture_output(&out, "warnings-off");
+    if cc::Build::new().get_compiler().is_like_msvc() {
+        // MSVC doesn't output warnings to stderr, so we can't capture them.
+        // the test will use this env var to know whether to run the test.
+        println!("cargo:rustc-env=TEST_WARNINGS_ON=0");
+    } else {
+        println!("cargo:rustc-env=TEST_WARNINGS_ON=1");
+        run_forked_capture_output(&out, "warnings-on");
+    }
+
     cc::Build::new()
         .file("src/foo.c")
         .flag_if_supported("-Wall")
@@ -22,7 +45,7 @@ fn main() {
         .compile("bar");
 
     let target = std::env::var("TARGET").unwrap();
-    let file = target.split("-").next().unwrap();
+    let file = target.split('-').next().unwrap();
     let file = format!(
         "src/{}.{}",
         file,
@@ -104,3 +127,48 @@ fn main() {
     let out = String::from_utf8(out).unwrap();
     assert!(out.contains("hello world"));
 }
+
+#[track_caller]
+fn run_forked_capture_output(out: &Path, action: &str) {
+    let program = env::current_exe().unwrap();
+    let output = Command::new(&program).arg(action).output().unwrap();
+    assert!(output.status.success(), "output: {:#?}", output);
+    // we've captured the output and now we write it to a dedicated directory in the
+    // build output so our tests can access the output.
+    let action_dir = out.join(action);
+    fs::create_dir_all(&action_dir).unwrap();
+    fs::write(action_dir.join("stdout"), output.stdout).unwrap();
+    fs::write(action_dir.join("stderr"), output.stderr).unwrap();
+}
+
+fn run_action_if_forked() -> bool {
+    let mut args = env::args();
+    let _program = args.next().unwrap();
+    let action = args.next();
+    match action.as_deref() {
+        Some("metadata-on") => build_cargo_metadata(true),
+        Some("metadata-off") => build_cargo_metadata(false),
+        Some("warnings-on") => build_cargo_warnings(true),
+        Some("warnings-off") => build_cargo_warnings(false),
+        // No action requested, we're being called from cargo. Proceed with build.
+        _ => return false,
+    }
+    true
+}
+
+fn build_cargo_warnings(warnings: bool) {
+    cc::Build::new()
+        .cargo_metadata(false)
+        .cargo_warnings(warnings)
+        .file("src/compile_error.c")
+        .try_compile("compile_error")
+        .unwrap_err();
+}
+
+fn build_cargo_metadata(metadata: bool) {
+    cc::Build::new()
+        .cargo_metadata(metadata)
+        .file("src/dummy.c")
+        .try_compile("dummy")
+        .unwrap();
+}
diff --git a/cc-test/src/NMakefile b/dev-tools/cc-test/src/NMakefile
similarity index 100%
rename from cc-test/src/NMakefile
rename to dev-tools/cc-test/src/NMakefile
diff --git a/cc-test/src/aarch64.S b/dev-tools/cc-test/src/aarch64.S
similarity index 100%
rename from cc-test/src/aarch64.S
rename to dev-tools/cc-test/src/aarch64.S
diff --git a/cc-test/src/aarch64.asm b/dev-tools/cc-test/src/aarch64.asm
similarity index 100%
rename from cc-test/src/aarch64.asm
rename to dev-tools/cc-test/src/aarch64.asm
diff --git a/cc-test/src/armv7.S b/dev-tools/cc-test/src/armv7.S
similarity index 100%
rename from cc-test/src/armv7.S
rename to dev-tools/cc-test/src/armv7.S
diff --git a/cc-test/src/bar1.c b/dev-tools/cc-test/src/bar1.c
similarity index 100%
rename from cc-test/src/bar1.c
rename to dev-tools/cc-test/src/bar1.c
diff --git a/cc-test/src/bar2.c b/dev-tools/cc-test/src/bar2.c
similarity index 100%
rename from cc-test/src/bar2.c
rename to dev-tools/cc-test/src/bar2.c
diff --git a/cc-test/src/baz.cpp b/dev-tools/cc-test/src/baz.cpp
similarity index 100%
rename from cc-test/src/baz.cpp
rename to dev-tools/cc-test/src/baz.cpp
diff --git a/dev-tools/cc-test/src/compile_error.c b/dev-tools/cc-test/src/compile_error.c
new file mode 100644
index 0000000..535ae34
--- /dev/null
+++ b/dev-tools/cc-test/src/compile_error.c
@@ -0,0 +1,1 @@
+#error "if you see this, cargo_warnings(false) didn't do its job"
diff --git a/cc-test/src/cuda.cu b/dev-tools/cc-test/src/cuda.cu
similarity index 100%
rename from cc-test/src/cuda.cu
rename to dev-tools/cc-test/src/cuda.cu
diff --git a/dev-tools/cc-test/src/dummy.c b/dev-tools/cc-test/src/dummy.c
new file mode 100644
index 0000000..0edb14b
--- /dev/null
+++ b/dev-tools/cc-test/src/dummy.c
@@ -0,0 +1,3 @@
+/* just an empty file */
+
+void dummy(void) {}
diff --git a/cc-test/src/expand.c b/dev-tools/cc-test/src/expand.c
similarity index 100%
rename from cc-test/src/expand.c
rename to dev-tools/cc-test/src/expand.c
diff --git a/cc-test/src/foo.c b/dev-tools/cc-test/src/foo.c
similarity index 100%
rename from cc-test/src/foo.c
rename to dev-tools/cc-test/src/foo.c
diff --git a/cc-test/src/i686.S b/dev-tools/cc-test/src/i686.S
similarity index 100%
rename from cc-test/src/i686.S
rename to dev-tools/cc-test/src/i686.S
diff --git a/cc-test/src/i686.asm b/dev-tools/cc-test/src/i686.asm
similarity index 100%
rename from cc-test/src/i686.asm
rename to dev-tools/cc-test/src/i686.asm
diff --git a/cc-test/src/include/foo.h b/dev-tools/cc-test/src/include/foo.h
similarity index 100%
rename from cc-test/src/include/foo.h
rename to dev-tools/cc-test/src/include/foo.h
diff --git a/cc-test/src/lib.rs b/dev-tools/cc-test/src/lib.rs
similarity index 100%
rename from cc-test/src/lib.rs
rename to dev-tools/cc-test/src/lib.rs
diff --git a/cc-test/src/msvc.c b/dev-tools/cc-test/src/msvc.c
similarity index 100%
rename from cc-test/src/msvc.c
rename to dev-tools/cc-test/src/msvc.c
diff --git a/cc-test/src/opt_linkage.c b/dev-tools/cc-test/src/opt_linkage.c
similarity index 100%
rename from cc-test/src/opt_linkage.c
rename to dev-tools/cc-test/src/opt_linkage.c
diff --git a/cc-test/src/riscv64gc.S b/dev-tools/cc-test/src/riscv64gc.S
similarity index 100%
rename from cc-test/src/riscv64gc.S
rename to dev-tools/cc-test/src/riscv64gc.S
diff --git a/cc-test/src/windows.c b/dev-tools/cc-test/src/windows.c
similarity index 100%
rename from cc-test/src/windows.c
rename to dev-tools/cc-test/src/windows.c
diff --git a/cc-test/src/x86_64.S b/dev-tools/cc-test/src/x86_64.S
similarity index 100%
rename from cc-test/src/x86_64.S
rename to dev-tools/cc-test/src/x86_64.S
diff --git a/cc-test/src/x86_64.asm b/dev-tools/cc-test/src/x86_64.asm
similarity index 100%
rename from cc-test/src/x86_64.asm
rename to dev-tools/cc-test/src/x86_64.asm
diff --git a/cc-test/tests/all.rs b/dev-tools/cc-test/tests/all.rs
similarity index 100%
rename from cc-test/tests/all.rs
rename to dev-tools/cc-test/tests/all.rs
diff --git a/dev-tools/cc-test/tests/output.rs b/dev-tools/cc-test/tests/output.rs
new file mode 100644
index 0000000..9bfe61d
--- /dev/null
+++ b/dev-tools/cc-test/tests/output.rs
@@ -0,0 +1,68 @@
+use std::fs;
+use std::path::PathBuf;
+
+#[test]
+fn cargo_warnings_on() {
+    if env!("TEST_WARNINGS_ON") == "0" {
+        // in some cases we don't catch compiler warnings and turn them into cargo
+        // instructions.
+        return;
+    }
+    let (stdout, stderr) = load_output("warnings-on");
+    assert!(stderr.is_empty());
+    assert!(stdout.contains("cargo:warning="));
+}
+
+#[test]
+fn cargo_warnings_off() {
+    let (stdout, stderr) = load_output("warnings-off");
+    assert!(stderr.is_empty());
+    assert!(!stdout.contains("cargo:warning="));
+}
+
+#[test]
+fn cargo_metadata_on() {
+    let (stdout, stderr) = load_output("metadata-on");
+    assert!(stderr.is_empty());
+    assert!(stdout.contains("cargo:rustc-link-lib="));
+    assert!(stdout.contains("cargo:rustc-link-search="));
+}
+
+#[test]
+fn cargo_metadata_off() {
+    let (stdout, stderr) = load_output("metadata-off");
+    assert!(stderr.is_empty());
+
+    // most of the instructions aren't currently used
+    const INSTRUCTIONS: &[&str] = &[
+        "cargo:rerun-if-changed=",
+        "cargo:rerun-if-env-changed=",
+        "cargo:rustc-cdylib-link-arg=",
+        "cargo:rustc-cfg=",
+        "cargo:rustc-env=",
+        "cargo:rustc-flags=",
+        "cargo:rustc-link-arg-benches=",
+        "cargo:rustc-link-arg-bin=",
+        "cargo:rustc-link-arg-bins=",
+        "cargo:rustc-link-arg-examples=",
+        "cargo:rustc-link-arg-tests=",
+        "cargo:rustc-link-arg=",
+        "cargo:rustc-link-lib=",
+        "cargo:rustc-link-search=",
+    ];
+    for instr in INSTRUCTIONS {
+        assert!(!stdout.contains(instr), "instruction present: {}", instr);
+    }
+}
+
+#[track_caller]
+fn load_output(action: &str) -> (String, String) {
+    // these files are written by the `run_forked_capture_output` function in the
+    // build script.
+    let action_dir = PathBuf::from(env!("OUT_DIR")).join(action);
+    let stdout = fs::read_to_string(action_dir.join("stdout")).unwrap();
+    let stderr = fs::read_to_string(action_dir.join("stderr")).unwrap();
+    println!("compile stdout: {:?}", stdout);
+    println!("compile stderr: {:?}", stderr);
+    (stdout, stderr)
+}
diff --git a/tests/support/mod.rs b/tests/support/mod.rs
index f3c0440..a17fe43 100644
--- a/tests/support/mod.rs
+++ b/tests/support/mod.rs
@@ -17,7 +17,7 @@ pub struct Test {
 }
 
 pub struct Execution {
-    args: Vec<String>,
+    pub args: Vec<String>,
 }
 
 impl Test {
@@ -34,7 +34,7 @@ impl Test {
         // lesser of the two evils.
         env::remove_var("RUSTC_WRAPPER");
 
-        let mut gcc = PathBuf::from(env::current_exe().unwrap());
+        let mut gcc = env::current_exe().unwrap();
         gcc.pop();
         if gcc.ends_with("deps") {
             gcc.pop();
@@ -42,8 +42,8 @@ impl Test {
         let td = Builder::new().prefix("gcc-test").tempdir_in(&gcc).unwrap();
         gcc.push(format!("gcc-shim{}", env::consts::EXE_SUFFIX));
         Test {
-            td: td,
-            gcc: gcc,
+            td,
+            gcc,
             msvc: false,
         }
     }
@@ -61,6 +61,12 @@ impl Test {
         t
     }
 
+    pub fn clang() -> Test {
+        let t = Test::new();
+        t.shim("clang").shim("clang++").shim("ar");
+        t
+    }
+
     pub fn shim(&self, name: &str) -> &Test {
         let name = if name.ends_with(env::consts::EXE_SUFFIX) {
             name.to_string()
@@ -76,7 +82,11 @@ impl Test {
         let target = if self.msvc {
             "x86_64-pc-windows-msvc"
         } else {
-            "x86_64-unknown-linux-gnu"
+            if cfg!(target_os = "macos") {
+                "x86_64-apple-darwin"
+            } else {
+                "x86_64-unknown-linux-gnu"
+            }
         };
 
         cfg.target(target)
diff --git a/tests/test.rs b/tests/test.rs
index 161abd8..f8c950d 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -24,7 +24,8 @@ fn gnu_smoke() {
         .must_have("-c")
         .must_have("-ffunction-sections")
         .must_have("-fdata-sections");
-    test.cmd(1).must_have(test.td.path().join("foo.o"));
+    test.cmd(1)
+        .must_have(test.td.path().join("d1fba762150c532c-foo.o"));
 }
 
 #[test]
@@ -55,7 +56,11 @@ fn gnu_opt_level_s() {
 #[test]
 fn gnu_debug() {
     let test = Test::gnu();
-    test.gcc().debug(true).file("foo.c").compile("foo");
+    test.gcc()
+        .target("x86_64-unknown-linux")
+        .debug(true)
+        .file("foo.c")
+        .compile("foo");
     test.cmd(0).must_have("-gdwarf-4");
 
     let test = Test::gnu();
@@ -70,7 +75,11 @@ fn gnu_debug() {
 #[test]
 fn gnu_debug_fp_auto() {
     let test = Test::gnu();
-    test.gcc().debug(true).file("foo.c").compile("foo");
+    test.gcc()
+        .target("x86_64-unknown-linux")
+        .debug(true)
+        .file("foo.c")
+        .compile("foo");
     test.cmd(0).must_have("-gdwarf-4");
     test.cmd(0).must_have("-fno-omit-frame-pointer");
 }
@@ -78,7 +87,11 @@ fn gnu_debug_fp_auto() {
 #[test]
 fn gnu_debug_fp() {
     let test = Test::gnu();
-    test.gcc().debug(true).file("foo.c").compile("foo");
+    test.gcc()
+        .target("x86_64-unknown-linux")
+        .debug(true)
+        .file("foo.c")
+        .compile("foo");
     test.cmd(0).must_have("-gdwarf-4");
     test.cmd(0).must_have("-fno-omit-frame-pointer");
 }
@@ -89,6 +102,7 @@ fn gnu_debug_nofp() {
 
     let test = Test::gnu();
     test.gcc()
+        .target("x86_64-unknown-linux")
         .debug(true)
         .force_frame_pointer(false)
         .file("foo.c")
@@ -98,6 +112,7 @@ fn gnu_debug_nofp() {
 
     let test = Test::gnu();
     test.gcc()
+        .target("x86_64-unknown-linux")
         .force_frame_pointer(false)
         .debug(true)
         .file("foo.c")
@@ -245,8 +260,8 @@ fn gnu_x86_64_no_plt() {
     test.gcc()
         .pic(true)
         .use_plt(false)
-        .target(&target)
-        .host(&target)
+        .target(target)
+        .host(target)
         .file("foo.c")
         .compile("foo");
     test.cmd(0).must_have("-fno-plt");
@@ -329,11 +344,9 @@ fn gnu_flag_if_supported() {
         .must_not_have("-std=c++11");
 }
 
+#[cfg(not(windows))]
 #[test]
 fn gnu_flag_if_supported_cpp() {
-    if cfg!(windows) {
-        return;
-    }
     let test = Test::gnu();
     test.gcc()
         .cpp(true)
@@ -366,6 +379,14 @@ fn gnu_no_dash_dash() {
     test.cmd(0).must_not_have("--");
 }
 
+#[test]
+fn gnu_std_c() {
+    let test = Test::gnu();
+    test.gcc().file("foo.c").std("c11").compile("foo");
+
+    test.cmd(0).must_have("-std=c11");
+}
+
 #[test]
 fn msvc_smoke() {
     reset_env();
@@ -379,7 +400,8 @@ fn msvc_smoke() {
         .must_not_have("-Z7")
         .must_have("-c")
         .must_have("-MD");
-    test.cmd(1).must_have(test.td.path().join("foo.o"));
+    test.cmd(1)
+        .must_have(test.td.path().join("d1fba762150c532c-foo.o"));
 }
 
 #[test]
@@ -443,6 +465,14 @@ fn msvc_no_dash_dash() {
     test.cmd(0).must_not_have("--");
 }
 
+#[test]
+fn msvc_std_c() {
+    let test = Test::msvc();
+    test.gcc().file("foo.c").std("c11").compile("foo");
+
+    test.cmd(0).must_have("-std:c11");
+}
+
 // Disable this test with the parallel feature because the execution
 // order is not deterministic.
 #[cfg(not(feature = "parallel"))]
@@ -459,3 +489,131 @@ fn asm_flags() {
     test.cmd(1).must_have("--abc");
     test.cmd(2).must_have("--abc");
 }
+
+#[test]
+fn gnu_apple_darwin() {
+    for (arch, version) in &[("x86_64", "10.7"), ("aarch64", "11.0")] {
+        let target = format!("{}-apple-darwin", arch);
+        let test = Test::gnu();
+        test.gcc()
+            .target(&target)
+            .host(&target)
+            // Avoid test maintenance when minimum supported OSes change.
+            .__set_env("MACOSX_DEPLOYMENT_TARGET", version)
+            .file("foo.c")
+            .compile("foo");
+
+        let cmd = test.cmd(0);
+        test.cmd(0)
+            .must_have(format!("-mmacosx-version-min={}", version));
+        cmd.must_not_have("-isysroot");
+    }
+}
+
+#[cfg(target_os = "macos")]
+#[test]
+fn macos_cpp_minimums() {
+    let versions = &[
+        // Too low
+        ("10.7", (10, 9)),
+        // Minimum
+        ("10.9", (10, 9)),
+        // Higher
+        ("11.0", (11, 0)),
+    ];
+
+    let target = "x86_64-apple-darwin";
+    for (deployment_target, set_version) in versions {
+        let test = Test::gnu();
+        test.gcc()
+            .target(target)
+            .host(target)
+            .cpp(true)
+            .__set_env("MACOSX_DEPLOYMENT_TARGET", deployment_target)
+            .file("foo.c")
+            .compile("foo");
+
+        let exec = test.cmd(0);
+        let deployment_arg = exec
+            .args
+            .iter()
+            .find_map(|arg| arg.strip_prefix("-mmacosx-version-min="))
+            .expect("no deployment target argument was set");
+
+        let mut deployment_parts = deployment_arg.split('.').map(|v| v.parse::<u32>().unwrap());
+
+        let major = deployment_parts.next().unwrap();
+        let minor = deployment_parts.next().unwrap();
+
+        // Check that we are on at least our minimums since this test reads from system
+        // SDK state, and that can vary per-system. It should never go lower then the deployment
+        // target we pass.
+        assert!(major >= set_version.0);
+
+        // If still on 10.x make sure `x` didn't go lower.
+        if major == set_version.0 {
+            assert!(minor >= set_version.1);
+        }
+    }
+
+    let test = Test::gnu();
+    test.gcc()
+        .target(target)
+        .host(target)
+        .__set_env("MACOSX_DEPLOYMENT_TARGET", "10.7")
+        .file("foo.c")
+        .compile("foo");
+
+    // No C++ leaves it untouched
+    test.cmd(0).must_have("-mmacosx-version-min=10.7");
+}
+
+#[cfg(target_os = "macos")]
+#[test]
+fn clang_apple_tvos() {
+    for target in &["aarch64-apple-tvos"] {
+        let test = Test::clang();
+        test.gcc()
+            .__set_env("TVOS_DEPLOYMENT_TARGET", "9.0")
+            .target(&target)
+            .host(&target)
+            .file("foo.c")
+            .compile("foo");
+
+        test.cmd(0).must_have("-mappletvos-version-min=9.0");
+    }
+}
+
+#[cfg(target_os = "macos")]
+#[test]
+fn clang_apple_tvsimulator() {
+    for target in &["x86_64-apple-tvos"] {
+        let test = Test::clang();
+        test.gcc()
+            .__set_env("TVOS_DEPLOYMENT_TARGET", "9.0")
+            .target(&target)
+            .host(&target)
+            .file("foo.c")
+            .compile("foo");
+
+        test.cmd(0).must_have("-mappletvsimulator-version-min=9.0");
+    }
+}
+
+#[test]
+fn compile_intermediates() {
+    let test = Test::gnu();
+    let intermediates = test
+        .gcc()
+        .file("foo.c")
+        .file("x86_64.asm")
+        .file("x86_64.S")
+        .asm_flag("--abc")
+        .compile_intermediates();
+
+    assert_eq!(intermediates.len(), 3);
+
+    assert!(intermediates[0].display().to_string().contains("foo"));
+    assert!(intermediates[1].display().to_string().contains("x86_64"));
+    assert!(intermediates[2].display().to_string().contains("x86_64"));
+}