diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7377d3759..e211b1888 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,5 +1,11 @@ version: 2 updates: +- package-ecosystem: "github-actions" + # Workflow files stored in the + # default location of `.github/workflows` + directory: "/" + schedule: + interval: "daily" - package-ecosystem: cargo directory: "/" schedule: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 42f358875..1fafe834e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,13 +1,17 @@ name: CI on: [push, pull_request] +env: + CARGO_INCREMENTAL: 0 + CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse + jobs: test: name: Test runs-on: ${{ matrix.os }} strategy: matrix: - build: [stable, beta, nightly, linux32, macos, aarch64-ios, win32, win64, mingw32, mingw64, windows-2016] + build: [stable, beta, nightly, linux32, macos, aarch64-ios, win32, win64, mingw32, mingw64, windows-2019] include: - build: stable os: ubuntu-latest @@ -40,7 +44,7 @@ jobs: target: aarch64-pc-windows-msvc no_run: --no-run - build: win32 - os: windows-2016 + os: windows-2019 rust: stable-i686-msvc target: i686-pc-windows-msvc - build: win64 @@ -48,27 +52,29 @@ jobs: rust: stable target: x86_64-pc-windows-msvc - build: mingw32 - os: windows-latest + # windows-latest, a.k.a. windows-2022, runner is equipped with + # a newer mingw toolchain, which appears to produce unexecutable + # mixed-language binaries in debug builds. Fall back to + # windows-2019 for now and revisit it later... + os: windows-2019 rust: stable-i686-gnu target: i686-pc-windows-gnu - build: mingw64 os: windows-latest rust: stable-x86_64-gnu target: x86_64-pc-windows-gnu - - build: windows-2016 - os: windows-2016 + - build: windows-2019 + os: windows-2019 rust: stable-x86_64 target: x86_64-pc-windows-msvc steps: - - uses: actions/checkout@master - - name: Update Rustup (temporary workaround) - run: rustup self update - shell: bash - if: startsWith(matrix.os, 'windows') + - uses: actions/checkout@v4 - name: Install Rust (rustup) - run: rustup update ${{ matrix.rust }} --no-self-update && rustup default ${{ matrix.rust }} + run: | + set -euxo pipefail + rustup toolchain install ${{ matrix.rust }} --no-self-update --profile minimal --target ${{ matrix.target }} + rustup default ${{ matrix.rust }} shell: bash - - run: rustup target add ${{ matrix.target }} - name: Install g++-multilib run: | set -e @@ -80,68 +86,78 @@ jobs: sudo apt-get update sudo apt-get install g++-multilib if: matrix.build == 'linux32' - - run: cargo build - 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 + check-tvos: + name: Test aarch64-apple-tvos + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Install Rust (rustup) + run: | + set -euxo pipefail + rustup toolchain install nightly --no-self-update --profile minimal + rustup component add rust-src --toolchain nightly + rustup default nightly + shell: bash + - run: cargo test -Z build-std=std --no-run --target aarch64-apple-tvos + - run: cargo test -Z build-std=std --no-run --features parallel --target aarch64-apple-tvos + - run: cargo test -Z build-std=std --no-run --manifest-path cc-test/Cargo.toml --target aarch64-apple-tvos + - run: cargo test -Z build-std=std --no-run --manifest-path cc-test/Cargo.toml --target aarch64-apple-tvos --features parallel + - run: cargo test -Z build-std=std --no-run --manifest-path cc-test/Cargo.toml --target aarch64-apple-tvos --release + cuda: name: Test CUDA support runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@master - - name: Install cuda-minimal-build-11-4 + - uses: actions/checkout@v4 + - name: Install cuda-minimal-build-11-8 shell: bash run: | # https://developer.nvidia.com/cuda-downloads?target_os=Linux&target_arch=x86_64&Distribution=Ubuntu&target_version=20.04&target_type=deb_network - wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/cuda-ubuntu2004.pin - sudo mv cuda-ubuntu2004.pin /etc/apt/preferences.d/cuda-repository-pin-600 - sudo apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/7fa2af80.pub - sudo add-apt-repository "deb https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/ /" + wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/cuda-keyring_1.0-1_all.deb + sudo dpkg -i cuda-keyring_1.0-1_all.deb sudo apt-get update - sudo apt-get -y install cuda-minimal-build-11-4 + sudo apt-get -y install cuda-minimal-build-11-8 - name: Test 'cudart' feature shell: bash - run: env PATH=/usr/local/cuda/bin:$PATH cargo test --manifest-path cc-test/Cargo.toml --features test_cuda + run: | + PATH="/usr/local/cuda/bin:$PATH" cargo test --manifest-path 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@master + - uses: actions/checkout@v4 - name: Install Rust - run: rustup update 1.34.0 --no-self-update && rustup default 1.34.0 + 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 - - run: cargo build + - 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 + - run: cargo check --lib --all-features rustfmt: name: Rustfmt runs-on: ubuntu-latest steps: - - uses: actions/checkout@master + - uses: actions/checkout@v4 - name: Install Rust - run: rustup update stable && rustup default stable && rustup component add rustfmt + run: | + rustup toolchain install stable --no-self-update --profile minimal --component rustfmt + rustup default stable + shell: bash - run: cargo fmt -- --check - - publish_docs: - name: Publish Documentation - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: Install Rust - run: rustup update stable && rustup default stable - - name: Build documentation - run: cargo doc --no-deps --all-features - - name: Publish documentation - run: | - cd target/doc - git init - git add . - git -c user.name='ci' -c user.email='ci' commit -m init - git push -f -q https://git:${{ secrets.github_token }}@github.com/${{ github.repository }} HEAD:gh-pages - if: github.event_name == 'push' && github.event.ref == 'refs/heads/main' diff --git a/Cargo.toml b/Cargo.toml index 227e2569e..9e646aa64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "cc" -version = "1.0.73" +version = "1.0.84" authors = ["Alex Crichton "] license = "MIT OR Apache-2.0" -repository = "https://github.com/alexcrichton/cc-rs" -homepage = "https://github.com/alexcrichton/cc-rs" +repository = "https://github.com/rust-lang/cc-rs" +homepage = "https://github.com/rust-lang/cc-rs" documentation = "https://docs.rs/cc" description = """ A build-time dependency for Cargo build scripts to assist in invoking the native @@ -16,12 +16,15 @@ readme = "README.md" categories = ["development-tools::build-utils"] exclude = ["/.github"] edition = "2018" +rust-version = "1.53" -[dependencies] -jobserver = { version = "0.1.16", optional = true } +[target.'cfg(unix)'.dependencies] +# 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 } [features] -parallel = ["jobserver"] +parallel = [] [dev-dependencies] tempfile = "3" diff --git a/README.md b/README.md index b52e095b9..a58dc68d0 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ you can call them from Rust by declaring them in your Rust code like so: ```rust,no_run -extern { +extern "C" { fn foo_function(); fn bar_function(x: i32) -> i32; } @@ -83,9 +83,17 @@ number of different environment variables. 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. +* `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: @@ -94,7 +102,7 @@ in the following prioritized order: 3. `_` - for example, `HOST_CC` or `TARGET_CFLAGS` 4. `` - a plain `CC`, `AR` as above. -If none of these variables exist, cc-rs uses built-in defaults +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 @@ -130,17 +138,17 @@ 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 `cl.exe` to be available and in `PATH`. This is typically found in - standard Visual Studio installations and the `PATH` can be set up by running - the appropriate developer tools shell. + 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](http://mingw-w64.org) distribution, which is using the - [Win-builds](http://win-builds.org) installation system. + [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 + 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 @@ -155,7 +163,7 @@ fn main() { cc::Build::new() .cpp(true) // Switch to C++ library compilation. .file("foo.cpp") - .compile("libfoo.a"); + .compile("foo"); } ``` @@ -170,7 +178,7 @@ The C++ standard library may be linked to the crate target. By default it's `lib .cpp(true) .file("foo.cpp") .cpp_link_stdlib("stdc++") // use libstdc++ - .compile("libfoo.a"); + .compile("foo"); } ``` 2. by setting the `CXXSTDLIB` environment variable. @@ -182,7 +190,7 @@ Remember that C++ does name mangling so `extern "C"` might be required to enable ## CUDA C++ support `cc-rs` also supports compiling CUDA C++ libraries by using the `cuda` method -on `Build` (currently for GNU/Clang toolchains only): +on `Build`: ```rust,no_run fn main() { @@ -200,8 +208,10 @@ fn main() { .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("libbar.a"); + .compile("bar"); } ``` diff --git a/gen-windows-sys-binding/Cargo.toml b/gen-windows-sys-binding/Cargo.toml new file mode 100644 index 000000000..6a4787b6b --- /dev/null +++ b/gen-windows-sys-binding/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "gen-windows-sys-binding" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +windows-bindgen = "0.49" + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] diff --git a/gen-windows-sys-binding/src/main.rs b/gen-windows-sys-binding/src/main.rs new file mode 100644 index 000000000..5dd96cdd6 --- /dev/null +++ b/gen-windows-sys-binding/src/main.rs @@ -0,0 +1,62 @@ +//! Adapted from +//! https://github.com/rust-lang/rust/blob/master/src/tools/generate-windows-sys/src/main.rs + +use std::{ + fs, + io::{self, 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 +// ``` +"#; + +const POSTLUDE: &str = r#" +/// Adapted from +/// [`core::ptr::invalid_mut()`](https://doc.rust-lang.org/src/core/ptr/mod.rs.html#600-607). +/// +/// This function should actually use `core::mem::transmute` but due to msrv +/// we use `as` casting instead. +/// +/// Once msrv is bumped to 1.56, replace this with `core::mem::transmute` since +/// it is const stablised in 1.56 +/// +/// NOTE that once supports `strict_provenance` we would also have to update +/// this. +const fn invalid_mut(addr: usize) -> *mut T { + addr as *mut T +} +"#; + +fn main() -> io::Result<()> { + // Load the list of APIs + let buffer = fs::read_to_string("windows_sys.list")?; + let names: Vec<&str> = buffer + .lines() + .filter_map(|line| { + let line = line.trim(); + if line.is_empty() || line.starts_with("//") { + None + } else { + Some(line) + } + }) + .collect(); + + // Write the bindings to windows-sys.rs + let bindings = + windows_bindgen::standalone_std(&names).replace("::core::ptr::invalid_mut", "invalid_mut"); + + let mut f = fs::File::create("../src/windows_sys.rs")?; + f.write_all(PRELUDE.as_bytes())?; + f.write_all(bindings.as_bytes())?; + f.write_all(POSTLUDE.as_bytes())?; + + Ok(()) +} diff --git a/gen-windows-sys-binding/windows_sys.list b/gen-windows-sys-binding/windows_sys.list new file mode 100644 index 000000000..f5ed55e16 --- /dev/null +++ b/gen-windows-sys-binding/windows_sys.list @@ -0,0 +1,40 @@ +Windows.Win32.Foundation.FILETIME +Windows.Win32.Foundation.INVALID_HANDLE_VALUE +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.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.Pipes.CreatePipe + +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.Threading.ReleaseSemaphore +Windows.Win32.System.Threading.WaitForSingleObject +Windows.Win32.System.Threading.SEMAPHORE_MODIFY_STATE +Windows.Win32.System.Threading.THREAD_SYNCHRONIZE + +Windows.Win32.System.WindowsProgramming.OpenSemaphoreA diff --git a/src/async_executor.rs b/src/async_executor.rs new file mode 100644 index 000000000..ad9e62a65 --- /dev/null +++ b/src/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(super) 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(super) fn block_on( + mut fut1: Fut1, + mut fut2: Fut2, + has_made_progress: &Cell, +) -> Result<(), Error> +where + Fut1: Future>, + Fut2: Future>, +{ + // 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 wth `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/bin/gcc-shim.rs b/src/bin/gcc-shim.rs index 1731df82e..e5b537258 100644 --- a/src/bin/gcc-shim.rs +++ b/src/bin/gcc-shim.rs @@ -2,7 +2,7 @@ use std::env; use std::fs::File; -use std::io::prelude::*; +use std::io::{self, prelude::*}; use std::path::PathBuf; fn main() { @@ -10,39 +10,61 @@ fn main() { let program = args.next().expect("Unexpected empty args"); let out_dir = PathBuf::from( - env::var_os("GCCTEST_OUT_DIR").expect(&format!("{}: GCCTEST_OUT_DIR not found", program)), + env::var_os("GCCTEST_OUT_DIR") + .unwrap_or_else(|| panic!("{}: GCCTEST_OUT_DIR not found", program)), ); // Find the first nonexistent candidate file to which the program's args can be written. - for i in 0.. { - let candidate = &out_dir.join(format!("out{}", i)); + let candidate = (0..).find_map(|i| { + let candidate = out_dir.join(format!("out{}", i)); - // If the file exists, commands have already run. Try again. if candidate.exists() { - continue; + // If the file exists, commands have already run. Try again. + None + } else { + Some(candidate) } + }).unwrap_or_else(|| panic!("Cannot find the first nonexistent candidate file to which the program's args can be written under out_dir '{}'", out_dir.display())); - // Create a file and record the args passed to the command. - let mut f = File::create(candidate).expect(&format!( - "{}: can't create candidate: {}", + // Create a file and record the args passed to the command. + let f = File::create(&candidate).unwrap_or_else(|e| { + panic!( + "{}: can't create candidate: {}, error: {}", program, - candidate.to_string_lossy() - )); + candidate.display(), + e + ) + }); + let mut f = io::BufWriter::new(f); + + (|| { for arg in args { - writeln!(f, "{}", arg).expect(&format!( - "{}: can't write to candidate: {}", - program, - candidate.to_string_lossy() - )); + writeln!(f, "{}", arg)?; } - break; - } + + f.flush()?; + + let mut f = f.into_inner()?; + f.flush()?; + f.sync_all() + })() + .unwrap_or_else(|e| { + panic!( + "{}: can't write to candidate: {}, error: {}", + program, + candidate.display(), + e + ) + }); // Create a file used by some tests. let path = &out_dir.join("libfoo.a"); - File::create(path).expect(&format!( - "{}: can't create libfoo.a: {}", - program, - path.to_string_lossy() - )); + File::create(path).unwrap_or_else(|e| { + panic!( + "{}: can't create libfoo.a: {}, error: {}", + program, + path.display(), + e + ) + }); } diff --git a/src/com.rs b/src/com.rs index a5f2afedf..cd2284427 100644 --- a/src/com.rs +++ b/src/com.rs @@ -1,33 +1,36 @@ // Copyright © 2017 winapi-rs developers // Licensed under the Apache License, Version 2.0 -// or the MIT license -// , at your option. +// or the MIT license +// , at your option. // All files in the project carrying such notice may not be copied, modified, or distributed // except according to those terms. #![allow(unused)] -use crate::winapi::CoInitializeEx; -use crate::winapi::IUnknown; -use crate::winapi::Interface; -use crate::winapi::BSTR; -use crate::winapi::COINIT_MULTITHREADED; -use crate::winapi::{SysFreeString, SysStringLen}; -use crate::winapi::{HRESULT, S_FALSE, S_OK}; -use std::ffi::{OsStr, OsString}; -use std::mem::forget; -use std::ops::Deref; -use std::os::windows::ffi::{OsStrExt, OsStringExt}; -use std::ptr::null_mut; -use std::slice::from_raw_parts; +use crate::{ + winapi::{IUnknown, Interface}, + windows_sys::{ + CoInitializeEx, SysFreeString, SysStringLen, BSTR, COINIT_MULTITHREADED, HRESULT, S_FALSE, + S_OK, + }, +}; +use std::{ + ffi::{OsStr, OsString}, + mem::ManuallyDrop, + ops::Deref, + os::windows::ffi::{OsStrExt, OsStringExt}, + ptr::{null, null_mut}, + slice::from_raw_parts, +}; pub fn initialize() -> Result<(), HRESULT> { - let err = unsafe { CoInitializeEx(null_mut(), COINIT_MULTITHREADED) }; + let err = unsafe { CoInitializeEx(null(), COINIT_MULTITHREADED) }; if err != S_OK && err != S_FALSE { // S_FALSE just means COM is already initialized - return Err(err); + Err(err) + } else { + Ok(()) } - Ok(()) } pub struct ComPtr(*mut T) @@ -55,9 +58,7 @@ where /// Extracts the raw pointer. /// You are now responsible for releasing it yourself. pub fn into_raw(self) -> *mut T { - let p = self.0; - forget(self); - p + ManuallyDrop::new(self).0 } /// For internal use only. fn as_unknown(&self) -> &IUnknown { diff --git a/src/job_token.rs b/src/job_token.rs new file mode 100644 index 000000000..4b4fa989e --- /dev/null +++ b/src/job_token.rs @@ -0,0 +1,171 @@ +use std::{mem::MaybeUninit, sync::Once}; + +use crate::Error; + +#[cfg(unix)] +#[path = "job_token/unix.rs"] +mod sys; + +#[cfg(windows)] +#[path = "job_token/windows.rs"] +mod sys; + +pub(super) 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(super) 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 = 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, 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 { + let var = var_os("CARGO_MAKEFLAGS") + .or_else(|| var_os("MAKEFLAGS")) + .or_else(|| var_os("MFLAGS"))?; + + let inner = sys::JobServerClient::open(var)?; + + Some(Self { + inner, + global_implicit_token: AtomicBool::new(true), + }) + } + + pub(super) fn try_acquire(&self) -> Result, 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 { + 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/job_token/unix.rs b/src/job_token/unix.rs new file mode 100644 index 000000000..af626c24d --- /dev/null +++ b/src/job_token/unix.rs @@ -0,0 +1,185 @@ +use std::{ + ffi::{OsStr, OsString}, + fs::{self, File}, + io::{self, Read, Write}, + mem::ManuallyDrop, + os::{ + raw::c_int, + unix::{ + ffi::{OsStrExt, OsStringExt}, + prelude::*, + }, + }, + path::Path, +}; + +pub(super) struct JobServerClient { + read: File, + write: Option, +} + +impl JobServerClient { + pub(super) unsafe fn open(var: OsString) -> Option { + let bytes = var.into_vec(); + + let s = bytes + .split(u8::is_ascii_whitespace) + .filter_map(|arg| { + arg.strip_prefix(b"--jobserver-fds=") + .or_else(|| arg.strip_prefix(b"--jobserver-auth=")) + }) + .find(|bytes| !bytes.is_empty())?; + + if let Some(fifo) = s.strip_prefix(b"fifo:") { + Self::from_fifo(Path::new(OsStr::from_bytes(fifo))) + } else { + Self::from_pipe(OsStr::from_bytes(s).to_str()?) + } + } + + /// `--jobserver-auth=fifo:PATH` + fn from_fifo(path: &Path) -> Option { + 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_nonblocking(&file)?; + + Some(Self { + read: file, + write: None, + }) + } else { + None + } + } + + /// `--jobserver-auth=fd-for-R,fd-for-W` + unsafe fn from_pipe(s: &str) -> Option { + 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_nonblocking(&read)?; + set_nonblocking(&write)?; + + Some(Self { + read, + write: Some(write), + }) + } + _ => None, + } + } + + pub(super) fn try_acquire(&self) -> io::Result> { + 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 set_nonblocking(file: &File) -> Option<()> { + // F_SETFL can only set the O_APPEND, O_ASYNC, O_DIRECT, O_NOATIME, and + // O_NONBLOCK flags. + // + // For pipe, only O_NONBLOCK is meaningful, so it is ok to + // not issue a F_GETFL fcntl syscall. + let ret = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_SETFL, libc::O_NONBLOCK) }; + + if ret == -1 { + None + } else { + Some(()) + } +} + +fn cvt(t: c_int) -> io::Result { + if t == -1 { + Err(io::Error::last_os_error()) + } else { + Ok(t) + } +} + +fn is_pipe(file: &File) -> Option { + Some(file.metadata().ok()?.file_type().is_fifo()) +} + +fn get_access_mode(file: &File) -> Option { + 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/job_token/windows.rs b/src/job_token/windows.rs new file mode 100644 index 000000000..03ec8869b --- /dev/null +++ b/src/job_token/windows.rs @@ -0,0 +1,76 @@ +use std::{ + ffi::{CString, OsString}, + io, ptr, +}; + +use crate::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: OsString) -> Option { + let var = var.to_str()?; + 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 s = var + .split_ascii_whitespace() + .filter_map(|arg| arg.strip_prefix("--jobserver-auth=")) + .find(|s| !s.is_empty())?; + + let name = CString::new(s).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> { + 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/lib.rs b/src/lib.rs index dac00eea6..d83eb0292 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,9 +34,6 @@ //! //! Cargo will also set this environment variable when executed with the `-jN` flag. //! -//! If `NUM_JOBS` is not set, the `RAYON_NUM_THREADS` environment variable can -//! also specify the build parallelism. -//! //! # Examples //! //! Use the `Build` struct to compile `src/foo.c`: @@ -56,17 +53,24 @@ #![allow(deprecated)] #![deny(missing_docs)] -use std::collections::HashMap; +use std::borrow::Cow; +use std::collections::{hash_map, HashMap}; use std::env; use std::ffi::{OsStr, OsString}; -use std::fmt::{self, Display}; -use std::fs; +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::path::{Component, Path, PathBuf}; use std::process::{Child, Command, Stdio}; use std::sync::{Arc, Mutex}; use std::thread::{self, JoinHandle}; +#[cfg(feature = "parallel")] +mod async_executor; +#[cfg(feature = "parallel")] +mod job_token; +mod os_pipe; // These modules are all glue to support reading the MSVC version from // the registry and from COM interfaces #[cfg(windows)] @@ -80,6 +84,8 @@ mod com; mod setup_config; #[cfg(windows)] mod vs_instances; +#[cfg(windows)] +mod windows_sys; pub mod windows_registry; @@ -90,30 +96,34 @@ pub mod windows_registry; /// documentation on each method itself. #[derive(Clone, Debug)] pub struct Build { - include_directories: Vec, - definitions: Vec<(String, Option)>, - objects: Vec, - flags: Vec, - flags_supported: Vec, + include_directories: Vec>, + definitions: Vec<(Arc, Option>)>, + objects: Vec>, + flags: Vec>, + flags_supported: Vec>, known_flag_support_status: Arc>>, - ar_flags: Vec, + ar_flags: Vec>, + asm_flags: Vec>, no_default_flags: bool, - files: Vec, + files: Vec>, cpp: bool, - cpp_link_stdlib: Option>, - cpp_set_stdlib: Option, + cpp_link_stdlib: Option>>, + cpp_set_stdlib: Option>, cuda: bool, - cudart: Option, - target: Option, - host: Option, - out_dir: Option, - opt_level: Option, + cudart: Option>, + std: Option>, + target: Option>, + host: Option>, + out_dir: Option>, + opt_level: Option>, debug: Option, force_frame_pointer: Option, - env: Vec<(OsString, OsString)>, - compiler: Option, - archiver: Option, + env: Vec<(Arc, Arc)>, + compiler: Option>, + archiver: Option>, + ranlib: Option>, cargo_metadata: bool, + link_lib_modifiers: Vec>, pic: Option, use_plt: Option, static_crt: Option, @@ -122,8 +132,9 @@ pub struct Build { warnings_into_errors: bool, warnings: Option, extra_warnings: Option, - env_cache: Arc>>>, + env_cache: Arc>>>>, apple_sdk_root_cache: Arc>>, + emit_rerun_if_env_changed: bool, } /// Represents the types of errors that may occur while using cc-rs. @@ -149,26 +160,26 @@ pub struct Error { /// Describes the kind of error that occurred. kind: ErrorKind, /// More explanation of error that occurred. - message: String, + message: Cow<'static, str>, } impl Error { - fn new(kind: ErrorKind, message: &str) -> Error { + fn new(kind: ErrorKind, message: impl Into>) -> Error { Error { - kind: kind, - message: message.to_owned(), + kind, + message: message.into(), } } } impl From for Error { fn from(e: io::Error) -> Error { - Error::new(ErrorKind::IOError, &format!("{}", e)) + Error::new(ErrorKind::IOError, format!("{}", e)) } } impl Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?}: {}", self.kind, self.message) } } @@ -192,6 +203,7 @@ pub struct Tool { family: ToolFamily, cuda: bool, removed_args: Vec, + has_internal_target_arg: bool, } /// Represents the family of tools this tool belongs to. @@ -212,13 +224,17 @@ enum ToolFamily { impl ToolFamily { /// What the flag to request debug info for this family of tools look like - fn add_debug_flags(&self, cmd: &mut Tool) { + fn add_debug_flags(&self, cmd: &mut Tool, dwarf_version: Option) { match *self { ToolFamily::Msvc { .. } => { cmd.push_cc_arg("-Z7".into()); } ToolFamily::Gnu | ToolFamily::Clang => { - cmd.push_cc_arg("-g".into()); + cmd.push_cc_arg( + dwarf_version + .map_or_else(|| "-g".into(), |v| format!("-gdwarf-{}", v)) + .into(), + ); } } } @@ -293,6 +309,7 @@ impl Build { flags_supported: Vec::new(), known_flag_support_status: Arc::new(Mutex::new(HashMap::new())), ar_flags: Vec::new(), + asm_flags: Vec::new(), no_default_flags: false, files: Vec::new(), shared_flag: None, @@ -302,6 +319,7 @@ impl Build { cpp_set_stdlib: None, cuda: false, cudart: None, + std: None, target: None, host: None, out_dir: None, @@ -311,7 +329,9 @@ impl Build { env: Vec::new(), compiler: None, archiver: None, + ranlib: None, cargo_metadata: true, + link_lib_modifiers: Vec::new(), pic: None, use_plt: None, static_crt: None, @@ -320,6 +340,7 @@ impl Build { warnings_into_errors: false, env_cache: Arc::new(Mutex::new(HashMap::new())), apple_sdk_root_cache: Arc::new(Mutex::new(HashMap::new())), + emit_rerun_if_env_changed: true, } } @@ -339,7 +360,7 @@ impl Build { /// .compile("foo"); /// ``` pub fn include>(&mut self, dir: P) -> &mut Build { - self.include_directories.push(dir.as_ref().to_path_buf()); + self.include_directories.push(dir.as_ref().into()); self } @@ -385,13 +406,13 @@ impl Build { /// ``` pub fn define<'a, V: Into>>(&mut self, var: &str, val: V) -> &mut Build { self.definitions - .push((var.to_string(), val.into().map(|s| s.to_string()))); + .push((var.into(), val.into().map(Into::into))); self } /// Add an arbitrary object file to link in pub fn object>(&mut self, obj: P) -> &mut Build { - self.objects.push(obj.as_ref().to_path_buf()); + self.objects.push(obj.as_ref().into()); self } @@ -406,7 +427,25 @@ impl Build { /// .compile("foo"); /// ``` pub fn flag(&mut self, flag: &str) -> &mut Build { - self.flags.push(flag.to_string()); + self.flags.push(flag.into()); + 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 } @@ -422,7 +461,26 @@ impl Build { /// .compile("foo"); /// ``` pub fn ar_flag(&mut self, flag: &str) -> &mut Build { - self.ar_flags.push(flag.to_string()); + self.ar_flags.push(flag.into()); + self + } + + /// Add a flag that will only be used with assembly files. + /// + /// The flag will be applied to input files with either a `.s` or + /// `.asm` extension (case insensitive). + /// + /// # Example + /// + /// ```no_run + /// cc::Build::new() + /// .asm_flag("-Wa,-defsym,abc=1") + /// .file("src/foo.S") // The asm flag will be applied here + /// .file("src/bar.c") // The asm flag will not be applied here + /// .compile("foo"); + /// ``` + pub fn asm_flag(&mut self, flag: &str) -> &mut Build { + self.asm_flags.push(flag.into()); self } @@ -469,12 +527,16 @@ impl Build { let host = self.get_host()?; let mut cfg = Build::new(); cfg.flag(flag) + .cargo_metadata(self.cargo_metadata) .target(&target) .opt_level(0) .host(&host) .debug(false) .cpp(self.cpp) .cuda(self.cuda); + if let Some(ref c) = self.compiler { + cfg.compiler(c.clone()); + } let mut compiler = cfg.try_get_compiler()?; // Clang uses stderr for verbose output, which yields a false positive @@ -482,30 +544,34 @@ 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"); let clang = compiler.family == ToolFamily::Clang; + let gnu = compiler.family == ToolFamily::Gnu; command_add_output_file( &mut cmd, &obj, self.cuda, target.contains("msvc"), clang, + gnu, false, 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); let output = cmd.output()?; - let is_supported = output.stderr.is_empty(); + let is_supported = output.status.success() && output.stderr.is_empty(); known_status.insert(flag.to_owned(), is_supported); Ok(is_supported) @@ -523,10 +589,39 @@ impl Build { /// .compile("foo"); /// ``` pub fn flag_if_supported(&mut self, flag: &str) -> &mut Build { - self.flags_supported.push(flag.to_string()); + self.flags_supported.push(flag.into()); self } + /// Add flags from the specified environment variable. + /// + /// Normally the `cc` crate will consult with the standard set of environment + /// variables (such as `CFLAGS` and `CXXFLAGS`) to construct the compiler invocation. Use of + /// this method provides additional levers for the end user to use when configuring the build + /// process. + /// + /// Just like the standard variables, this method will search for an environment variable with + /// appropriate target prefixes, when appropriate. + /// + /// # Examples + /// + /// This method is particularly beneficial in introducing the ability to specify crate-specific + /// flags. + /// + /// ```no_run + /// cc::Build::new() + /// .file("src/foo.c") + /// .try_flags_from_environment(concat!(env!("CARGO_PKG_NAME"), "_CFLAGS")) + /// .expect("the environment variable must be specified and UTF-8") + /// .compile("foo"); + /// ``` + /// + pub fn try_flags_from_environment(&mut self, environ_key: &str) -> Result<&mut Build, Error> { + let flags = self.envflags(environ_key)?; + self.flags.extend(flags.into_iter().map(Into::into)); + Ok(self) + } + /// Set the `-shared` flag. /// /// When enabled, the compiler will produce a shared object which can @@ -577,7 +672,7 @@ impl Build { /// Add a file which will be compiled pub fn file>(&mut self, p: P) -> &mut Build { - self.files.push(p.as_ref().to_path_buf()); + self.files.push(p.as_ref().into()); self } @@ -597,6 +692,12 @@ impl Build { /// /// The other `cpp_*` options will only become active if this is set to /// `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. + /// 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. pub fn cpp(&mut self, cpp: bool) -> &mut Build { self.cpp = cpp; self @@ -604,17 +705,19 @@ impl Build { /// Set CUDA C++ support. /// - /// Enabling CUDA will pass the detected C/C++ toolchain as an argument to - /// the CUDA compiler, NVCC. NVCC itself accepts some limited GNU-like args; - /// any other arguments for the C/C++ toolchain will be redirected using - /// "-Xcompiler" flags. + /// Enabling CUDA will invoke the CUDA compiler, NVCC. While NVCC accepts + /// 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/ + /// for more information. /// /// If enabled, this also implicitly enables C++ support. pub fn cuda(&mut self, cuda: bool) -> &mut Build { self.cuda = cuda; if cuda { self.cpp = true; - self.cudart = Some("static".to_string()); + self.cudart = Some("static".into()); } self } @@ -627,11 +730,42 @@ impl Build { /// at all, if the default is right for the project. pub fn cudart(&mut self, cudart: &str) -> &mut Build { if self.cuda { - self.cudart = Some(cudart.to_string()); + self.cudart = Some(cudart.into()); } 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. @@ -703,8 +837,6 @@ impl Build { /// Set the standard library to link against when compiling with C++ /// support. /// - /// See [`get_cpp_link_stdlib`](cc::Build::get_cpp_link_stdlib) documentation - /// for the default value. /// If the `CXXSTDLIB` environment variable is set, its value will /// override the default value, but not the value explicitly set by calling /// this function. @@ -793,7 +925,7 @@ impl Build { /// .compile("foo"); /// ``` pub fn target(&mut self, target: &str) -> &mut Build { - self.target = Some(target.to_string()); + self.target = Some(target.into()); self } @@ -811,7 +943,7 @@ impl Build { /// .compile("foo"); /// ``` pub fn host(&mut self, host: &str) -> &mut Build { - self.host = Some(host.to_string()); + self.host = Some(host.into()); self } @@ -820,7 +952,7 @@ impl Build { /// This option is automatically scraped from the `OPT_LEVEL` environment /// variable by build scripts, so it's not required to call this function. pub fn opt_level(&mut self, opt_level: u32) -> &mut Build { - self.opt_level = Some(opt_level.to_string()); + self.opt_level = Some(opt_level.to_string().into()); self } @@ -829,7 +961,7 @@ impl Build { /// This option is automatically scraped from the `OPT_LEVEL` environment /// variable by build scripts, so it's not required to call this function. pub fn opt_level_str(&mut self, opt_level: &str) -> &mut Build { - self.opt_level = Some(opt_level.to_string()); + self.opt_level = Some(opt_level.into()); self } @@ -860,7 +992,7 @@ impl Build { /// This option is automatically scraped from the `OUT_DIR` environment /// variable by build scripts, so it's not required to call this function. pub fn out_dir>(&mut self, out_dir: P) -> &mut Build { - self.out_dir = Some(out_dir.as_ref().to_owned()); + self.out_dir = Some(out_dir.as_ref().into()); self } @@ -870,7 +1002,7 @@ impl Build { /// number of environment variables, so it's not required to call this /// function. pub fn compiler>(&mut self, compiler: P) -> &mut Build { - self.compiler = Some(compiler.as_ref().to_owned()); + self.compiler = Some(compiler.as_ref().into()); self } @@ -880,9 +1012,20 @@ impl Build { /// number of environment variables, so it's not required to call this /// function. pub fn archiver>(&mut self, archiver: P) -> &mut Build { - self.archiver = Some(archiver.as_ref().to_owned()); + self.archiver = Some(archiver.as_ref().into()); + self + } + + /// Configures the tool used to index archives. + /// + /// This option is automatically determined from the target platform or a + /// number of environment variables, so it's not required to call this + /// function. + pub fn ranlib>(&mut self, ranlib: P) -> &mut Build { + self.ranlib = Some(ranlib.as_ref().into()); self } + /// Define whether metadata should be emitted for cargo allowing it to /// automatically link the binary. Defaults to `true`. /// @@ -892,12 +1035,23 @@ impl Build { /// - `rustc-link-search=native=`*target folder* /// - When target is MSVC, the ATL-MFC libs are added via `rustc-link-search=native=` /// - When C++ is enabled, the C++ stdlib is added via `rustc-link-lib` + /// - 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 } + /// 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 + /// 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()); + self + } + /// Configures whether the compiler will emit position independent code. /// /// This option defaults to `false` for `windows-gnu` and bare metal targets and @@ -922,6 +1076,17 @@ impl Build { self } + /// Define whether metadata should be emitted for cargo to detect environment + /// changes that should trigger a rebuild. + /// + /// This has no effect if the `cargo_metadata` option is `false`. + /// + /// This option defaults to `true`. + pub fn emit_rerun_if_env_changed(&mut self, emit_rerun_if_env_changed: bool) -> &mut Build { + self.emit_rerun_if_env_changed = emit_rerun_if_env_changed; + self + } + /// Configures whether the /MT flag or the /MD flag will be passed to msvc build tools. /// /// This option defaults to `false`, and affect only msvc targets. @@ -936,8 +1101,7 @@ impl Build { A: AsRef, B: AsRef, { - self.env - .push((a.as_ref().to_owned(), b.as_ref().to_owned())); + self.env.push((a.as_ref().into(), b.as_ref().into())); self } @@ -969,7 +1133,25 @@ impl Build { let mut objects = Vec::new(); for file in self.files.iter() { - let obj = dst.join(file).with_extension("o"); + 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.") @@ -990,8 +1172,11 @@ impl Build { objects.push(Object::new(file.to_path_buf(), obj)); } - self.compile_objects(&objects)?; - self.assemble(lib_name, &dst.join(gnu_lib_name), &objects)?; + + let print = PrintThread::new()?; + + self.compile_objects(&objects, &print)?; + self.assemble(lib_name, &dst.join(gnu_lib_name), &objects, &print)?; if self.get_target()?.contains("msvc") { let compiler = self.get_base_compiler()?; @@ -1007,29 +1192,40 @@ impl Build { }); if let Some(atlmfc_lib) = atlmfc_lib { - self.print(&format!( + self.print(&format_args!( "cargo:rustc-link-search=native={}", atlmfc_lib.display() )); } } - self.print(&format!("cargo:rustc-link-lib=static={}", lib_name)); - self.print(&format!("cargo:rustc-link-search=native={}", dst.display())); + if self.link_lib_modifiers.is_empty() { + self.print(&format_args!("cargo:rustc-link-lib=static={}", lib_name)); + } else { + let m = self.link_lib_modifiers.join(","); + self.print(&format_args!( + "cargo:rustc-link-lib=static:{}={}", + m, lib_name + )); + } + self.print(&format_args!( + "cargo:rustc-link-search=native={}", + dst.display() + )); // Add specific C++ libraries, if enabled. if self.cpp { if let Some(stdlib) = self.get_cpp_link_stdlib()? { - self.print(&format!("cargo:rustc-link-lib={}", stdlib)); + self.print(&format_args!("cargo:rustc-link-lib={}", stdlib)); } } let cudart = match &self.cudart { - Some(opt) => opt.as_str(), // {none|shared|static} + Some(opt) => &*opt, // {none|shared|static} None => "none", }; if cudart != "none" { - if let Some(nvcc) = which(&self.get_compiler().path) { + if let Some(nvcc) = which(&self.get_compiler().path, None) { // Try to figure out the -L search path. If it fails, // it's on user to specify one by passing it through // RUSTFLAGS environment variable. @@ -1121,17 +1317,22 @@ impl Build { } #[cfg(feature = "parallel")] - fn compile_objects<'me>(&'me self, objs: &[Object]) -> Result<(), Error> { - use std::sync::atomic::{AtomicBool, Ordering::SeqCst}; - use std::sync::Once; - - // 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(); - let reacquire = server.release_raw().is_ok(); + fn compile_objects(&self, objs: &[Object], print: &PrintThread) -> Result<(), Error> { + use std::cell::Cell; + + use 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)?; + } + + return Ok(()); + } + + // Limit our parallelism globally with a jobserver. + let tokens = crate::job_token::JobTokenServer::new(); // When compiling objects in parallel we do a few dirty tricks to speed // things up: @@ -1145,147 +1346,143 @@ impl Build { // Note that this jobserver is cached globally so we only used one per // process and only worry about creating it once. // - // * Next we use a raw `thread::spawn` per thread to actually compile - // objects in parallel. We only actually spawn a thread after we've - // acquired a token to perform some work - // - // * Finally though we want to keep the dependencies of this crate - // pretty light, so we avoid using a safe abstraction like `rayon` and - // instead rely on some bits of `unsafe` code. We know that this stack - // frame persists while everything is compiling so we use all the - // stack-allocated objects without cloning/reallocating. We use a - // transmute to `State` with a `'static` lifetime to persist - // everything we need across the boundary, and the join-on-drop - // semantics of `JoinOnDrop` should ensure that our stack frame is - // alive while threads are alive. + // * Next we use spawn the process to actually compile objects in + // parallel after we've acquired a token to perform some work // // With all that in mind we compile all objects in a loop here, after we // acquire the appropriate tokens, Once all objects have been compiled - // we join on all the threads and propagate the results of compilation. - // - // Note that as a slight optimization we try to break out as soon as - // possible as soon as any compilation fails to ensure that errors get - // out to the user as fast as possible. - let error = AtomicBool::new(false); - let mut threads = Vec::new(); - for obj in objs { - if error.load(SeqCst) { - break; - } - let token = server.acquire()?; - let state = State { - build: self, - obj, - error: &error, - }; - let state = unsafe { std::mem::transmute::>(state) }; - let thread = thread::spawn(|| { - let state: State<'me> = state; // erase the `'static` lifetime - let result = state.build.compile_object(state.obj); - if result.is_err() { - state.error.store(true, SeqCst); + // we wait on all the processes and propagate the results of compilation. + + let pendings = Cell::new(Vec::<( + Command, + String, + KillOnDrop, + crate::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. + retain_unordered_mut(&mut pendings, |(cmd, program, child, _token)| { + match try_wait_on_child(cmd, program, &mut child.0, &mut stdout) { + 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); + + let _ = writeln!(stdout, "cargo:warning={}", err); + error = Some(err); + + false + } + } + }); + pendings_is_empty = pendings.is_empty(); + pendings + }); + + if pendings_is_empty && is_disconnected.get() { + break if let Some(err) = error { + Err(err) + } else { + Ok(()) + }; } - drop(token); // make sure our jobserver token is released after the compile - return result; - }); - threads.push(JoinOnDrop(Some(thread))); - } - for mut thread in threads { - if let Some(thread) = thread.0.take() { - thread.join().expect("thread should not panic")?; + YieldOnce::default().await; } - } - - // Reacquire our process's token before we proceed, which we released - // before entering the loop above. - if reacquire { - server.acquire_raw()?; - } - - return Ok(()); - - /// Shared state from the parent thread to the child thread. This - /// package of pointers is temporarily transmuted to a `'static` - /// lifetime to cross the thread boundary and then once the thread is - /// running we erase the `'static` to go back to an anonymous lifetime. - struct State<'a> { - build: &'a Build, - obj: &'a Object, - error: &'a AtomicBool, - } - - /// 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 = None; - - fn _assert_sync() {} - _assert_sync::(); + }; + 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 child = spawn(&mut cmd, &program, print.pipe_writer_cloned()?.unwrap())?; - unsafe { - INIT.call_once(|| { - let server = default_jobserver(); - JOBSERVER = Some(server); + cell_update(&pendings, |mut pendings| { + pendings.push((cmd, program, KillOnDrop(child), 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 JoinOnDrop(Option>>); + struct KillOnDrop(Child); - impl Drop for JoinOnDrop { + impl Drop for KillOnDrop { fn drop(&mut self) { - if let Some(thread) = self.0.take() { - drop(thread.join()); - } + let child = &mut self.0; + + child.kill().ok(); } } + + fn cell_update(cell: &Cell, 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]) -> Result<(), Error> { + fn compile_objects(&self, objs: &[Object], print: &PrintThread) -> Result<(), Error> { for obj in objs { - self.compile_object(obj)?; + let (mut cmd, name) = self.create_compile_object_cmd(obj)?; + run(&mut cmd, &name, print)?; } + Ok(()) } - fn compile_object(&self, obj: &Object) -> Result<(), Error> { - let is_asm = obj.src.extension().and_then(|s| s.to_str()) == Some("asm"); + fn create_compile_object_cmd(&self, obj: &Object) -> Result<(Command, String), Error> { + let asm_ext = AsmFileExt::from_path(&obj.src); + let is_asm = asm_ext.is_some(); let target = self.get_target()?; let msvc = target.contains("msvc"); let compiler = self.try_get_compiler()?; let clang = compiler.family == ToolFamily::Clang; - let (mut cmd, name) = if msvc && is_asm { + let gnu = compiler.family == ToolFamily::Gnu; + + 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(); @@ -1303,15 +1500,20 @@ impl Build { ) }; let is_arm = target.contains("aarch64") || target.contains("arm"); - command_add_output_file(&mut cmd, &obj.dst, self.cuda, msvc, clang, is_asm, is_arm); + command_add_output_file( + &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.files.len() > 1 { + if self.cuda && self.cuda_file_count() > 1 { cmd.arg("--device-c"); } - if compiler.family == (ToolFamily::Msvc { clang_cl: true }) { + if is_asm { + cmd.args(self.asm_flags.iter().map(std::ops::Deref::deref)); + } + 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 @@ -1323,8 +1525,7 @@ impl Build { self.fix_env_for_apple_os(&mut cmd)?; } - run(&mut cmd, &name)?; - Ok(()) + Ok((cmd, name)) } /// This will return a result instead of panicing; see expand() for the complete description. @@ -1341,9 +1542,7 @@ impl Build { "Expand may only be called for a single file" ); - for file in self.files.iter() { - cmd.arg(file); - } + cmd.args(self.files.iter().map(std::ops::Deref::deref)); let name = compiler .path @@ -1407,7 +1606,6 @@ impl Build { let target = self.get_target()?; let mut cmd = self.get_base_compiler()?; - let envflags = self.envflags(if self.cpp { "CXXFLAGS" } else { "CFLAGS" }); // Disable default flag generation via `no_default_flags` or environment variable let no_defaults = self.no_default_flags || self.getenv("CRATE_CC_NO_DEFAULTS").is_some(); @@ -1418,13 +1616,23 @@ impl Build { println!("Info: default compiler flags are disabled"); } - for arg in envflags { - cmd.push_cc_arg(arg.into()); + 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()); + } } 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, @@ -1450,12 +1658,12 @@ impl Build { } for flag in self.flags.iter() { - cmd.args.push(flag.into()); + cmd.args.push((**flag).into()); } for flag in self.flags_supported.iter() { if self.is_flag_supported(flag).unwrap_or(false) { - cmd.push_cc_arg(flag.into()); + cmd.push_cc_arg((**flag).into()); } } @@ -1491,9 +1699,8 @@ impl Build { Some(true) => "-MT", Some(false) => "-MD", None => { - let features = self - .getenv("CARGO_CFG_TARGET_FEATURE") - .unwrap_or(String::new()); + let features = self.getenv("CARGO_CFG_TARGET_FEATURE"); + let features = features.as_deref().unwrap_or_default(); if features.contains("crt-static") { "-MT" } else { @@ -1520,6 +1727,13 @@ impl Build { cmd.push_opt_unless_duplicate(format!("-O{}", opt_level).into()); } + if cmd.family == ToolFamily::Clang && target.contains("windows") { + // Disambiguate mingw and msvc on Windows. Problem is that + // depending on the origin clang can default to a mismatchig + // run-time. + cmd.push_cc_arg(format!("--target={}", target).into()); + } + if cmd.family == ToolFamily::Clang && target.contains("android") { // For compatibility with code that doesn't use pre-defined `__ANDROID__` macro. // If compiler used via ndk-build or cmake (officially supported build methods) @@ -1529,7 +1743,10 @@ impl Build { cmd.push_opt_unless_duplicate("-DANDROID".into()); } - if !target.contains("apple-ios") { + 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()); } @@ -1555,7 +1772,7 @@ impl Build { cmd.args.push("-G".into()); } let family = cmd.family; - family.add_debug_flags(cmd); + family.add_debug_flags(cmd, self.get_dwarf_version()); } if self.get_force_frame_pointer() { @@ -1563,12 +1780,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) @@ -1581,14 +1806,14 @@ impl Build { map_darwin_target_from_rust_to_compiler_architecture(target) { cmd.args - .push(format!("--target={}-apple-ios13.0-macabi", arch).into()); + .push(format!("--target={}-apple-ios-macabi", arch).into()); } } else if target.contains("ios-sim") { 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 deployment_target = + self.apple_deployment_version(AppleOs::Ios, target, None); cmd.args.push( format!( "--target={}-apple-ios{}-simulator", @@ -1597,6 +1822,44 @@ impl Build { .into(), ); } + } else if target.contains("watchos-sim") { + if let Some(arch) = + map_darwin_target_from_rust_to_compiler_architecture(target) + { + let deployment_target = + self.apple_deployment_version(AppleOs::WatchOs, target, None); + cmd.args.push( + format!( + "--target={}-apple-watchos{}-simulator", + arch, deployment_target + ) + .into(), + ); + } + } else if target.contains("x86_64-apple-tvos") { + if let Some(arch) = + map_darwin_target_from_rust_to_compiler_architecture(target) + { + let deployment_target = + self.apple_deployment_version(AppleOs::TvOs, target, None); + 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 deployment_target = + self.apple_deployment_version(AppleOs::TvOs, target, None); + 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(), @@ -1613,8 +1876,32 @@ impl Build { } else if target.contains("aarch64") { cmd.args.push("--target=aarch64-unknown-windows-gnu".into()) } + } else if target.ends_with("-freebsd") { + // FreeBSD only supports C++11 and above when compiling against libc++ + // (available from FreeBSD 10 onwards). Under FreeBSD, clang uses libc++ by + // default on FreeBSD 10 and newer unless `--target` is manually passed to + // the compiler, in which case its default behavior differs: + // * If --target=xxx-unknown-freebsdX(.Y) is specified and X is greater than + // or equal to 10, clang++ uses libc++ + // * If --target=xxx-unknown-freebsd is specified (without a version), + // clang++ cannot assume libc++ is available and reverts to a default of + // libstdc++ (this behavior was changed in llvm 14). + // + // This breaks C++11 (or greater) builds if targeting FreeBSD with the + // generic xxx-unknown-freebsd triple on clang 13 or below *without* + // explicitly specifying that libc++ should be used. + // When cross-compiling, we can't infer from the rust/cargo target triple + // which major version of FreeBSD we are targeting, so we need to make sure + // that libc++ is used (unless the user has explicitly specified otherwise). + // There's no compelling reason to use a different approach when compiling + // natively. + if self.cpp && self.cpp_set_stdlib.is_none() { + cmd.push_cc_arg("-stdlib=libc++".into()); + } + + cmd.push_cc_arg(format!("--target={}", target).into()); } else { - cmd.args.push(format!("--target={}", target).into()); + cmd.push_cc_arg(format!("--target={}", target).into()); } } } @@ -1656,14 +1943,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) { @@ -1677,9 +1956,8 @@ impl Build { } if self.static_flag.is_none() { - let features = self - .getenv("CARGO_CFG_TARGET_FEATURE") - .unwrap_or(String::new()); + let features = self.getenv("CARGO_CFG_TARGET_FEATURE"); + let features = features.as_deref().unwrap_or_default(); if features.contains("crt-static") { cmd.args.push("-static".into()); } @@ -1839,6 +2117,9 @@ impl Build { } 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()); @@ -1858,8 +2139,8 @@ impl Build { } } - if target.contains("apple-ios") { - self.ios_flags(cmd)?; + if target.contains("-apple-") { + self.apple_flags(cmd)?; } if self.static_flag.unwrap_or(false) { @@ -1890,7 +2171,7 @@ impl Build { fn has_flags(&self) -> bool { let flags_env_var_name = if self.cpp { "CXXFLAGS" } else { "CFLAGS" }; - let flags_env_var_value = self.get_var(flags_env_var_name); + let flags_env_var_value = self.getenv_with_target_prefixes(flags_env_var_name); if let Ok(_) = flags_env_var_value { true } else { @@ -1912,11 +2193,32 @@ impl Build { let mut cmd = windows_registry::find(&target, tool).unwrap_or_else(|| self.cmd(tool)); cmd.arg("-nologo"); // undocumented, yet working with armasm[64] for directory in self.include_directories.iter() { - cmd.arg("-I").arg(directory); + cmd.arg("-I").arg(&**directory); } if target.contains("aarch64") || target.contains("arm") { - println!("cargo:warning=The MSVC ARM assemblers do not support -D flags"); + if self.get_debug() { + cmd.arg("-g"); + } + + for &(ref key, ref value) in self.definitions.iter() { + cmd.arg("-PreDefine"); + if let Some(ref value) = *value { + if let Ok(i) = value.parse::() { + 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() { if let Some(ref value) = *value { cmd.arg(&format!("-D{}={}", key, value)); @@ -1929,43 +2231,43 @@ 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]) -> Result<(), Error> { + fn assemble( + &self, + lib_name: &str, + dst: &Path, + objs: &[Object], + print: &PrintThread, + ) -> 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); + let _ = fs::remove_file(dst); // Add objects to the archive in limited-length batches. This helps keep // the length of the command line within a reasonable length to avoid // blowing system limits on limiting platforms like Windows. let objs: Vec<_> = objs .iter() - .map(|o| o.dst.clone()) - .chain(self.objects.clone()) + .map(|o| o.dst.as_path()) + .chain(self.objects.iter().map(std::ops::Deref::deref)) .collect(); for chunk in objs.chunks(100) { - self.assemble_progressive(dst, chunk)?; + self.assemble_progressive(dst, chunk, print)?; } - if self.cuda { + if self.cuda && self.cuda_file_count() > 0 { // Link the device-side code and add it to the target library, // so that non-CUDA linker can link the final binary. let out_dir = self.get_out_dir()?; 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.clone()) - .arg(dst); - run(&mut nvcc, "nvcc")?; - self.assemble_progressive(dst, &[dlink])?; + nvcc.arg("--device-link").arg("-o").arg(&dlink).arg(dst); + run(&mut nvcc, "nvcc", print)?; + self.assemble_progressive(dst, &[dlink.as_path()], print)?; } let target = self.get_target()?; @@ -1992,23 +2294,36 @@ impl Build { // Non-msvc targets (those using `ar`) need a separate step to add // the symbol table to archives since our construction command of // `cq` doesn't add it for us. - let (mut ar, cmd) = self.get_ar()?; - run(ar.arg("s").arg(dst), &cmd)?; + let (mut ar, cmd, _any_flags) = self.get_ar()?; + + // 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)?; } Ok(()) } - fn assemble_progressive(&self, dst: &Path, objs: &[PathBuf]) -> Result<(), Error> { + fn assemble_progressive( + &self, + dst: &Path, + objs: &[&Path], + print: &PrintThread, + ) -> Result<(), Error> { let target = self.get_target()?; if target.contains("msvc") { - let (mut cmd, program) = self.get_ar()?; + let (mut cmd, program, any_flags) = self.get_ar()?; + // NOTE: -out: here is an I/O flag, and so must be included even if $ARFLAGS/ar_flag is + // in use. -nologo on the other hand is just a regular flag, and one that we'll skip if + // the caller has explicitly dictated the flags they want. See + // https://github.com/rust-lang/cc-rs/pull/763 for further discussion. let mut out = OsString::from("-out:"); out.push(dst); - cmd.arg(out).arg("-nologo"); - for flag in self.ar_flags.iter() { - cmd.arg(flag); + cmd.arg(out); + if !any_flags { + cmd.arg("-nologo"); } // If the library file already exists, add the library name // as an argument to let lib.exe know we are appending the objs. @@ -2016,9 +2331,9 @@ impl Build { cmd.arg(dst); } cmd.args(objs); - run(&mut cmd, &program)?; + run(&mut cmd, &program, print)?; } else { - let (mut ar, cmd) = self.get_ar()?; + let (mut ar, cmd, _any_flags) = self.get_ar()?; // Set an environment variable to tell the OSX archiver to ensure // that all dates listed in the archive are zero, improving @@ -2043,16 +2358,17 @@ impl Build { // In any case if this doesn't end up getting read, it shouldn't // cause that many issues! ar.env("ZERO_AR_DATE", "1"); - for flag in self.ar_flags.iter() { - ar.arg(flag); - } - run(ar.arg("cq").arg(dst).args(objs), &cmd)?; + + // 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)?; } Ok(()) } - fn ios_flags(&self, cmd: &mut Tool) -> Result<(), Error> { + fn apple_flags(&self, cmd: &mut Tool) -> Result<(), Error> { enum ArchSpec { Device(&'static str), Simulator(&'static str), @@ -2060,10 +2376,24 @@ impl Build { } let target = self.get_target()?; - let arch = target.split('-').nth(0).ok_or_else(|| { + let os = if target.contains("-darwin") { + AppleOs::MacOs + } else if target.contains("-watchos") { + AppleOs::WatchOs + } else if target.contains("-tvos") { + AppleOs::TvOs + } else { + AppleOs::Ios + }; + let is_mac = match os { + AppleOs::MacOs => true, + _ => false, + }; + + let arch_str = target.split('-').nth(0).ok_or_else(|| { Error::new( ErrorKind::ArchitectureInvalid, - "Unknown architecture for iOS target.", + format!("Unknown architecture for {:?} target.", os), ) })?; @@ -2072,16 +2402,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 { + let arch = if is_mac { + match arch_str { + "i686" => ArchSpec::Device("-m32"), + "x86_64" | "x86_64h" | "aarch64" => ArchSpec::Device("-m64"), + _ => { + return Err(Error::new( + ErrorKind::ArchitectureInvalid, + "Unknown architecture for macOS target.", + )); + } + } + } else if is_catalyst { + match arch_str { "arm64e" => ArchSpec::Catalyst("arm64e"), "arm64" | "aarch64" => ArchSpec::Catalyst("arm64"), - "x86_64" => ArchSpec::Catalyst("-m64"), + "x86_64" | "x86_64h" => ArchSpec::Catalyst("-m64"), _ => { return Err(Error::new( ErrorKind::ArchitectureInvalid, @@ -2089,69 +2430,84 @@ impl Build { )); } } - } else if is_sim { - match arch { - "arm64" | "aarch64" => ArchSpec::Simulator("-arch arm64"), + } else if is_arm_sim { + match arch_str { + "arm64" | "aarch64" => ArchSpec::Simulator("arm64"), + "x86_64" | "x86_64h" => ArchSpec::Simulator("-m64"), _ => { return Err(Error::new( ErrorKind::ArchitectureInvalid, - "Unknown architecture for iOS simulator target.", + "Unknown architecture for simulator target.", )); } } } else { - match arch { + match arch_str { "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"), + "x86_64" | "x86_64h" => ArchSpec::Simulator("-m64"), _ => { return Err(Error::new( ErrorKind::ArchitectureInvalid, - "Unknown architecture for iOS target.", + format!("Unknown architecture for {:?} target.", os), )); } } }; - let min_version = - std::env::var("IPHONEOS_DEPLOYMENT_TARGET").unwrap_or_else(|_| "7.0".into()); + let min_version = self.apple_deployment_version(os, &target, Some(arch_str)); + 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 { + ArchSpec::Device(_) if is_mac => { + cmd.args + .push(format!("-mmacosx-version-min={}", min_version).into()); + "macosx".to_owned() + } ArchSpec::Device(arch) => { cmd.args.push("-arch".into()); cmd.args.push(arch.into()); cmd.args - .push(format!("-miphoneos-version-min={}", min_version).into()); - "iphoneos" + .push(format!("-m{}os-version-min={}", sdk_prefix, min_version).into()); + format!("{}os", sdk_prefix) } ArchSpec::Simulator(arch) => { - cmd.args.push(arch.into()); + if arch.starts_with('-') { + // -m32 or -m64 + cmd.args.push(arch.into()); + } else { + cmd.args.push("-arch".into()); + cmd.args.push(arch.into()); + } cmd.args - .push(format!("-mios-simulator-version-min={}", min_version).into()); - "iphonesimulator" + .push(format!("-m{}simulator-version-min={}", sim_prefix, min_version).into()); + format!("{}simulator", sdk_prefix) } - ArchSpec::Catalyst(_) => "macosx", + ArchSpec::Catalyst(_) => "macosx".to_owned(), }; - self.print(&format!("Detecting iOS SDK path for {}", sdk)); - let sdk_path = self.apple_sdk_root(sdk)?; - cmd.args.push("-isysroot".into()); - cmd.args.push(sdk_path); - cmd.args.push("-fembed-bitcode".into()); - /* - * TODO we probably ultimately want the -fembed-bitcode-marker flag - * but can't have it now because of an issue in LLVM: - * https://github.com/alexcrichton/cc-rs/issues/301 - * https://github.com/rust-lang/rust/pull/48896#comment-372192660 - */ - /* - if self.get_opt_level()? == "0" { - cmd.args.push("-fembed-bitcode-marker".into()); - } - */ + // AppleClang sometimes requires sysroot even for darwin + if cmd.is_xctoolchain_clang() || !target.ends_with("-darwin") { + 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())? + }; + + cmd.args.push("-isysroot".into()); + cmd.args.push(sdk_path); + } Ok(()) } @@ -2165,11 +2521,12 @@ impl Build { } fn get_base_compiler(&self) -> Result { - if let Some(ref c) = self.compiler { - return Ok(Tool::new(c.clone())); + if let Some(c) = &self.compiler { + return Ok(Tool::new((**c).to_owned())); } let host = self.get_host()?; let target = self.get_target()?; + let target = &*target; let (env, msvc, gnu, traditional, clang) = if self.cpp { ("CXX", "cl.exe", "g++", "c++", "clang++") } else { @@ -2201,7 +2558,7 @@ 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(PathBuf::from(tool.trim()), driver_mode); + let mut t = Tool::with_clang_driver(tool, driver_mode); if let Some(cc_wrapper) = wrapper { t.cc_wrapper_path = Some(PathBuf::from(cc_wrapper)); } @@ -2235,10 +2592,15 @@ impl Build { if target.contains("msvc") { msvc.to_string() } else { - format!("{}.exe", gnu) + 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") { + clang.to_string() + } else if target.contains("apple-tvos") { + clang.to_string() } else if target.contains("android") { autodetect_android_compiler(&target, &host, gnu, clang) } else if target.contains("cloudabi") { @@ -2258,10 +2620,13 @@ impl Build { format!("arm-kmc-eabi-{}", gnu) } else if target.starts_with("aarch64-kmc-solid_") { format!("aarch64-kmc-elf-{}", gnu) - } else if self.get_host()? != target { + } else if &*self.get_host()? != target { let prefix = self.prefix_for_target(&target); match prefix { - Some(prefix) => format!("{}-{}", prefix, gnu), + Some(prefix) => { + let cc = if target.contains("llvm") { clang } else { gnu }; + format!("{}-{}", prefix, cc) + } None => default.to_string(), } } else { @@ -2281,11 +2646,11 @@ impl Build { tool.args.is_empty(), "CUDA compilation currently assumes empty pre-existing args" ); - let nvcc = match self.get_var("NVCC") { - Err(_) => "nvcc".into(), - Ok(nvcc) => nvcc, + let nvcc = match self.getenv_with_target_prefixes("NVCC") { + Err(_) => PathBuf::from("nvcc"), + Ok(nvcc) => PathBuf::from(&*nvcc), }; - let mut nvcc_tool = Tool::with_features(PathBuf::from(nvcc), None, self.cuda); + let mut nvcc_tool = Tool::with_features(nvcc, None, self.cuda); nvcc_tool .args .push(format!("-ccbin={}", tool.path.display()).into()); @@ -2312,6 +2677,7 @@ impl Build { let file_name = path.to_str().unwrap().to_owned(); let (target, clang) = file_name.split_at(file_name.rfind("-").unwrap()); + 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()); @@ -2350,35 +2716,11 @@ impl Build { } } - Ok(tool) - } - - fn get_var(&self, var_base: &str) -> Result { - let target = self.get_target()?; - let host = self.get_host()?; - let kind = if host == target { "HOST" } else { "TARGET" }; - let target_u = target.replace("-", "_"); - let res = self - .getenv(&format!("{}_{}", var_base, target)) - .or_else(|| self.getenv(&format!("{}_{}", var_base, target_u))) - .or_else(|| self.getenv(&format!("{}_{}", kind, var_base))) - .or_else(|| self.getenv(var_base)); - - match res { - Some(res) => Ok(res), - None => Err(Error::new( - ErrorKind::EnvVarNotFound, - &format!("Could not find environment variable {}.", var_base), - )), + if target.contains("msvc") && tool.family == ToolFamily::Gnu { + println!("cargo:warning=GNU compiler is not supported for this target"); } - } - fn envflags(&self, name: &str) -> Vec { - self.get_var(name) - .unwrap_or(String::new()) - .split_ascii_whitespace() - .map(|slice| slice.to_string()) - .collect() + Ok(tool) } /// Returns a fallback `cc_compiler_wrapper` by introspecting `RUSTC_WRAPPER` @@ -2400,8 +2742,8 @@ impl Build { } /// Returns compiler path, optional modifier name from whitelist, and arguments vec - fn env_tool(&self, name: &str) -> Option<(String, Option, Vec)> { - let tool = match self.get_var(name) { + fn env_tool(&self, name: &str) -> Option<(PathBuf, Option, Vec)> { + let tool = match self.getenv_with_target_prefixes(name) { Ok(tool) => tool, Err(_) => return None, }; @@ -2409,8 +2751,8 @@ impl Build { // If this is an exact path on the filesystem we don't want to do any // 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((tool, None, Vec::new())); + if Path::new(&*tool).exists() { + return Some((PathBuf::from(&*tool), None, Vec::new())); } // Ok now we want to handle a couple of scenarios. We'll assume from @@ -2449,7 +2791,7 @@ impl Build { if known_wrappers.contains(&file_stem) { if let Some(compiler) = parts.next() { return Some(( - compiler.to_string(), + compiler.into(), Some(maybe_wrapper.to_string()), parts.map(|s| s.to_string()).collect(), )); @@ -2457,7 +2799,7 @@ impl Build { } Some(( - maybe_wrapper.to_string(), + maybe_wrapper.into(), Self::rustc_wrapper_fallback(), parts.map(|s| s.to_string()).collect(), )) @@ -2469,14 +2811,14 @@ impl Build { /// 3. Else the default is `libc++` for OS X and BSDs, `libc++_shared` for Android, /// `None` for MSVC and `libstdc++` for anything else. fn get_cpp_link_stdlib(&self) -> Result, Error> { - match self.cpp_link_stdlib.clone() { - Some(s) => Ok(s), + match &self.cpp_link_stdlib { + Some(s) => Ok(s.as_ref().map(|s| (*s).to_string())), None => { - if let Ok(stdlib) = self.get_var("CXXSTDLIB") { + if let Ok(stdlib) = self.getenv_with_target_prefixes("CXXSTDLIB") { if stdlib.is_empty() { Ok(None) } else { - Ok(Some(stdlib)) + Ok(Some(stdlib.to_string())) } } else { let target = self.get_target()?; @@ -2488,6 +2830,8 @@ impl Build { Ok(Some("c++".to_string())) } else if target.contains("openbsd") { Ok(Some("c++".to_string())) + } else if target.contains("aix") { + Ok(Some("c++".to_string())) } else if target.contains("android") { Ok(Some("c++_shared".to_string())) } else { @@ -2498,64 +2842,237 @@ impl Build { } } - fn get_ar(&self) -> Result<(Command, String), Error> { - if let Some(ref p) = self.archiver { - let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("ar"); - return Ok((self.cmd(p), name.to_string())); - } - if let Ok(p) = self.get_var("AR") { - return Ok((self.cmd(&p), p)); - } - let target = self.get_target()?; - let default_ar = "ar".to_string(); - let program = if target.contains("android") { - format!("{}-ar", target.replace("armv7", "arm")) - } else if target.contains("emscripten") { - // Windows use bat files so we have to be a bit more specific - if cfg!(windows) { - let mut cmd = self.cmd("cmd"); - cmd.arg("/c").arg("emar.bat"); - return Ok((cmd, "emar.bat".to_string())); - } + fn get_ar(&self) -> Result<(Command, String, bool), Error> { + self.try_get_archiver_and_flags() + } - "emar".to_string() - } else if target.contains("msvc") { - match windows_registry::find(&target, "lib.exe") { - Some(t) => return Ok((t, "lib.exe".to_string())), - None => "lib.exe".to_string(), - } - } else if target.contains("illumos") { - // The default 'ar' on illumos uses a non-standard flags, - // but the OS comes bundled with a GNU-compatible variant. - // - // Use the GNU-variant to match other Unix systems. - "gar".to_string() - } else if self.get_host()? != target { - match self.prefix_for_target(&target) { - Some(p) => { - let target_ar = format!("{}-ar", p); - if Command::new(&target_ar).output().is_ok() { - target_ar + /// Get the archiver (ar) that's in use for this configuration. + /// + /// You can use [`Command::get_program`] to get just the path to the command. + /// + /// This method will take into account all configuration such as debug + /// information, optimization level, include directories, defines, etc. + /// Additionally, the compiler binary in use follows the standard + /// conventions for this path, e.g. looking at the explicitly set compiler, + /// environment variables (a number of which are inspected here), and then + /// falling back to the default configuration. + /// + /// # Panics + /// + /// Panics if an error occurred while determining the architecture. + pub fn get_archiver(&self) -> Command { + match self.try_get_archiver() { + Ok(tool) => tool, + Err(e) => fail(&e.message), + } + } + + /// 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. + pub fn try_get_archiver(&self) -> Result { + Ok(self.try_get_archiver_and_flags()?.0) + } + + fn try_get_archiver_and_flags(&self) -> Result<(Command, String, bool), Error> { + let (mut cmd, name) = self.get_base_archiver()?; + let mut any_flags = false; + if let Ok(flags) = self.envflags("ARFLAGS") { + any_flags = any_flags | !flags.is_empty(); + cmd.args(flags); + } + for flag in &self.ar_flags { + any_flags = true; + cmd.arg(&**flag); + } + Ok((cmd, name, any_flags)) + } + + fn get_base_archiver(&self) -> Result<(Command, String), Error> { + if let Some(ref a) = self.archiver { + return Ok((self.cmd(&**a), a.to_string_lossy().into_owned())); + } + + self.get_base_archiver_variant("AR", "ar") + } + + /// Get the ranlib that's in use for this configuration. + /// + /// You can use [`Command::get_program`] to get just the path to the command. + /// + /// This method will take into account all configuration such as debug + /// information, optimization level, include directories, defines, etc. + /// Additionally, the compiler binary in use follows the standard + /// conventions for this path, e.g. looking at the explicitly set compiler, + /// environment variables (a number of which are inspected here), and then + /// falling back to the default configuration. + /// + /// # Panics + /// + /// Panics if an error occurred while determining the architecture. + pub fn get_ranlib(&self) -> Command { + match self.try_get_ranlib() { + Ok(tool) => tool, + Err(e) => fail(&e.message), + } + } + + /// 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. + pub fn try_get_ranlib(&self) -> Result { + let mut cmd = self.get_base_ranlib()?; + if let Ok(flags) = self.envflags("RANLIBFLAGS") { + cmd.args(flags); + } + Ok(cmd) + } + + fn get_base_ranlib(&self) -> Result { + if let Some(ref r) = self.ranlib { + return Ok(self.cmd(&**r)); + } + + Ok(self.get_base_archiver_variant("RANLIB", "ranlib")?.0) + } + + fn get_base_archiver_variant(&self, env: &str, tool: &str) -> Result<(Command, String), Error> { + let target = self.get_target()?; + let mut name = String::new(); + let tool_opt: Option = self + .env_tool(env) + .map(|(tool, _wrapper, args)| { + let mut cmd = self.cmd(tool); + cmd.args(args); + cmd + }) + .or_else(|| { + if target.contains("emscripten") { + // Windows use bat files so we have to be a bit more specific + if cfg!(windows) { + let mut cmd = self.cmd("cmd"); + name = format!("em{}.bat", tool); + cmd.arg("/c").arg(&name); + Some(cmd) + } else { + name = format!("em{}", tool); + Some(self.cmd(&name)) + } + } else if target.starts_with("wasm32") { + // 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 + // 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)) + } else { + None + } + } else { + None + } + }); + + let default = tool.to_string(); + let tool = match tool_opt { + Some(t) => t, + None => { + if target.contains("android") { + name = format!("{}-{}", target.replace("armv7", "arm"), tool); + self.cmd(&name) + } else if target.contains("msvc") { + // NOTE: There isn't really a ranlib on msvc, so arguably we should return + // `None` somehow here. But in general, callers will already have to be aware + // of not running ranlib on Windows anyway, so it feels okay to return lib.exe + // here. + + let compiler = self.get_base_compiler()?; + let mut lib = String::new(); + if compiler.family == (ToolFamily::Msvc { clang_cl: true }) { + // See if there is 'llvm-lib' next to 'clang-cl' + // Another possibility could be to see if there is 'clang' + // next to 'clang-cl' and use 'search_programs()' to locate + // 'llvm-lib'. This is because 'clang-cl' doesn't support + // the -print-search-dirs option. + if let Some(mut cmd) = which(&compiler.path, None) { + cmd.pop(); + cmd.push("llvm-lib.exe"); + if let Some(llvm_lib) = which(&cmd, None) { + lib = llvm_lib.to_str().unwrap().to_owned(); + } + } + } + + if lib.is_empty() { + name = String::from("lib.exe"); + match windows_registry::find(&target, "lib.exe") { + Some(t) => t, + None => self.cmd("lib.exe"), + } } else { - default_ar + name = lib; + self.cmd(&name) + } + } else if target.contains("illumos") { + // The default 'ar' on illumos uses a non-standard flags, + // but the OS comes bundled with a GNU-compatible variant. + // + // Use the GNU-variant to match other Unix systems. + name = format!("g{}", tool); + self.cmd(&name) + } else if self.get_host()? != target { + match self.prefix_for_target(&target) { + 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` + // toolchain where the archiver attempts to load the LTO plugin anyway but + // fails to find one). + // + // The same applies to ranlib. + let mut chosen = default; + for &infix in &["", "-gcc"] { + let target_p = format!("{}{}-{}", p, infix, tool); + if Command::new(&target_p).output().is_ok() { + chosen = target_p; + break; + } + } + name = chosen; + self.cmd(&name) + } + None => { + name = default; + self.cmd(&name) + } } + } else { + name = default; + self.cmd(&name) } - None => default_ar, } - } else { - default_ar }; - Ok((self.cmd(&program), program)) + + Ok((tool, name)) } fn prefix_for_target(&self, target: &str) -> Option { + // Put aside RUSTC_LINKER's prefix to be used as second choice, after CROSS_COMPILE + let linker_prefix = self + .getenv("RUSTC_LINKER") + .and_then(|var| var.strip_suffix("-gcc").map(str::to_string)); // CROSS_COMPILE is of the form: "arm-linux-gnueabi-" let cc_env = self.getenv("CROSS_COMPILE"); - let cross_compile = cc_env - .as_ref() - .map(|s| s.trim_right_matches('-').to_owned()); - cross_compile.or(match &target[..] { - "aarch64-pc-windows-gnu" => Some("aarch64-w64-mingw32"), + let cross_compile = cc_env.as_ref().map(|s| s.trim_end_matches('-').to_owned()); + cross_compile.or(linker_prefix).or(match &target[..] { + // Note: there is no `aarch64-pc-windows-gnu` target, only `-gnullvm` + "aarch64-pc-windows-gnullvm" => Some("aarch64-w64-mingw32"), "aarch64-uwp-windows-gnu" => Some("aarch64-w64-mingw32"), "aarch64-unknown-linux-gnu" => Some("aarch64-linux-gnu"), "aarch64-unknown-linux-musl" => Some("aarch64-linux-musl"), @@ -2590,6 +3107,7 @@ impl Build { ]), // explicit None if not found, so caller knows to fall back "i686-unknown-linux-musl" => Some("musl"), "i686-unknown-netbsd" => Some("i486--netbsdelf"), + "loongarch64-unknown-linux-gnu" => Some("loongarch64-linux-gnu"), "mips-unknown-linux-gnu" => Some("mips-linux-gnu"), "mips-unknown-linux-musl" => Some("mips-linux-musl"), "mipsel-unknown-linux-gnu" => Some("mipsel-linux-gnu"), @@ -2615,6 +3133,12 @@ impl Build { "riscv64-unknown-elf", "riscv-none-embed", ]), + "riscv32imac-unknown-xous-elf" => self.find_working_gnu_prefix(&[ + "riscv32-unknown-elf", + "riscv64-unknown-elf", + "riscv-none-embed", + ]), + "riscv32imc-esp-espidf" => Some("riscv32-esp-elf"), "riscv32imc-unknown-none-elf" => self.find_working_gnu_prefix(&[ "riscv32-unknown-elf", "riscv64-unknown-elf", @@ -2634,6 +3158,7 @@ impl Build { "riscv32gc-unknown-linux-gnu" => Some("riscv32-linux-gnu"), "riscv64gc-unknown-linux-musl" => Some("riscv64-linux-musl"), "riscv32gc-unknown-linux-musl" => Some("riscv32-linux-musl"), + "riscv64gc-unknown-netbsd" => Some("riscv64--netbsd"), "s390x-unknown-linux-gnu" => Some("s390x-linux-gnu"), "sparc-unknown-linux-gnu" => Some("sparc-linux-gnu"), "sparc64-unknown-linux-gnu" => Some("sparc64-linux-gnu"), @@ -2653,6 +3178,7 @@ impl Build { "thumbv8m.main-none-eabi" => Some("arm-none-eabi"), "thumbv8m.main-none-eabihf" => Some("arm-none-eabi"), "x86_64-pc-windows-gnu" => Some("x86_64-w64-mingw32"), + "x86_64-pc-windows-gnullvm" => Some("x86_64-w64-mingw32"), "x86_64-uwp-windows-gnu" => Some("x86_64-w64-mingw32"), "x86_64-rumprun-netbsd" => Some("x86_64-rumprun-netbsd"), "x86_64-unknown-linux-gnu" => self.find_working_gnu_prefix(&[ @@ -2697,72 +3223,139 @@ impl Build { prefixes.first().map(|prefix| *prefix)) } - fn get_target(&self) -> Result { - match self.target.clone() { - Some(t) => Ok(t), - None => Ok(self.getenv_unwrap("TARGET")?), + fn get_target(&self) -> Result, Error> { + match &self.target { + Some(t) => Ok(t.clone()), + None => self.getenv_unwrap("TARGET"), } } - fn get_host(&self) -> Result { - match self.host.clone() { - Some(h) => Ok(h), - None => Ok(self.getenv_unwrap("HOST")?), + fn get_host(&self) -> Result, Error> { + match &self.host { + Some(h) => Ok(h.clone()), + None => self.getenv_unwrap("HOST"), } } - fn get_opt_level(&self) -> Result { - match self.opt_level.as_ref().cloned() { - Some(ol) => Ok(ol), - None => Ok(self.getenv_unwrap("OPT_LEVEL")?), + fn get_opt_level(&self) -> Result, Error> { + match &self.opt_level { + Some(ol) => Ok(ol.clone()), + None => self.getenv_unwrap("OPT_LEVEL"), } } fn get_debug(&self) -> bool { self.debug.unwrap_or_else(|| match self.getenv("DEBUG") { - Some(s) => s != "false", + Some(s) => &*s != "false", None => false, }) } + fn get_dwarf_version(&self) -> Option { + // Tentatively matches the DWARF version defaults as of rustc 1.62. + let target = self.get_target().ok()?; + if target.contains("android") + || target.contains("apple") + || target.contains("dragonfly") + || target.contains("freebsd") + || target.contains("netbsd") + || target.contains("openbsd") + || target.contains("windows-gnu") + { + Some(2) + } else if target.contains("linux") { + Some(4) + } else { + None + } + } + fn get_force_frame_pointer(&self) -> bool { self.force_frame_pointer.unwrap_or_else(|| self.get_debug()) } - fn get_out_dir(&self) -> Result { - match self.out_dir.clone() { - Some(p) => Ok(p), - None => Ok(env::var_os("OUT_DIR").map(PathBuf::from).ok_or_else(|| { - Error::new( - ErrorKind::EnvVarNotFound, - "Environment variable OUT_DIR not defined.", - ) - })?), + fn get_out_dir(&self) -> Result, Error> { + match &self.out_dir { + Some(p) => Ok(Cow::Borrowed(&**p)), + None => env::var_os("OUT_DIR") + .map(PathBuf::from) + .map(Cow::Owned) + .ok_or_else(|| { + Error::new( + ErrorKind::EnvVarNotFound, + "Environment variable OUT_DIR not defined.", + ) + }), } } - fn getenv(&self, v: &str) -> Option { + fn getenv(&self, v: &str) -> Option> { + // Returns true for environment variables cargo sets for build scripts: + // https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts + // + // This handles more of the vars than we actually use (it tries to check + // complete-ish set), just to avoid needing maintenance if/when new + // calls to `getenv`/`getenv_unwrap` are added. + fn provided_by_cargo(envvar: &str) -> bool { + match envvar { + v if v.starts_with("CARGO") || v.starts_with("RUSTC") => true, + "HOST" | "TARGET" | "RUSTDOC" | "OUT_DIR" | "OPT_LEVEL" | "DEBUG" | "PROFILE" + | "NUM_JOBS" | "RUSTFLAGS" => true, + _ => false, + } + } let mut cache = self.env_cache.lock().unwrap(); if let Some(val) = cache.get(v) { return val.clone(); } - let r = env::var(v).ok(); - self.print(&format!("{} = {:?}", v, r)); + if self.emit_rerun_if_env_changed && !provided_by_cargo(v) { + self.print(&format_args!("cargo:rerun-if-env-changed={}", v)); + } + let r = env::var(v).ok().map(Arc::from); + self.print(&format_args!("{} = {:?}", v, r)); cache.insert(v.to_string(), r.clone()); r } - fn getenv_unwrap(&self, v: &str) -> Result { + fn getenv_unwrap(&self, v: &str) -> Result, Error> { match self.getenv(v) { Some(s) => Ok(s), None => Err(Error::new( ErrorKind::EnvVarNotFound, - &format!("Environment variable {} not defined.", v.to_string()), + format!("Environment variable {} not defined.", v), )), } } - fn print(&self, s: &str) { + fn getenv_with_target_prefixes(&self, var_base: &str) -> Result, Error> { + let target = self.get_target()?; + let host = self.get_host()?; + let kind = if host == target { "HOST" } else { "TARGET" }; + let target_u = target.replace("-", "_"); + let res = self + .getenv(&format!("{}_{}", var_base, target)) + .or_else(|| self.getenv(&format!("{}_{}", var_base, target_u))) + .or_else(|| self.getenv(&format!("{}_{}", kind, var_base))) + .or_else(|| self.getenv(var_base)); + + match res { + Some(res) => Ok(res), + None => Err(Error::new( + ErrorKind::EnvVarNotFound, + format!("Could not find environment variable {}.", var_base), + )), + } + } + + fn envflags(&self, name: &str) -> Result, Error> { + Ok(self + .getenv_with_target_prefixes(name)? + .split_ascii_whitespace() + .map(|slice| slice.to_string()) + .collect()) + } + + fn print(&self, s: &dyn Display) { if self.cargo_metadata { println!("{}", s); } @@ -2772,19 +3365,6 @@ impl Build { 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". @@ -2815,7 +3395,7 @@ impl Build { Err(_) => { return Err(Error::new( ErrorKind::IOError, - "Unable to determine iOS SDK path.", + "Unable to determine Apple SDK path.", )); } }; @@ -2823,6 +3403,132 @@ impl Build { cache.insert(sdk.into(), ret.clone()); Ok(ret) } + + fn apple_deployment_version( + &self, + os: AppleOs, + target: &str, + arch_str: Option<&str>, + ) -> String { + const OLD_IOS_MINIMUM_VERSION: &str = "7.0"; + + fn rustc_provided_target(rustc: Option<&str>, target: &str) -> Option { + let rustc = rustc?; + let output = Command::new(rustc) + .args(&["--target", target]) + .args(&["--print", "deployment-target"]) + .output() + .ok()?; + + if output.status.success() { + std::str::from_utf8(&output.stdout) + .unwrap() + .strip_prefix("deployment_target=") + .map(|v| v.trim()) + .map(ToString::to_string) + } else { + // rustc is < 1.71 + None + } + } + + 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 { + env::var(name).ok() + } + }; + + // 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| -> String { + if !self.cpp { + return deployment_target_ver; + } + + let mut deployment_target = deployment_target_ver + .split('.') + .map(|v| v.parse::().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 round up. + if major == 10 && minor < 9 { + println!( + "cargo-warning: macOS deployment target ({}) too low, it will be increased", + deployment_target_ver + ); + return String::from("10.9"); + } + } + AppleOs::Ios => { + let major = deployment_target.next().unwrap_or(0); + + if major < 7 { + println!( + "cargo-warning: iOS deployment target ({}) too low, it will be increased", + deployment_target_ver + ); + return String::from(OLD_IOS_MINIMUM_VERSION); + } + } + // watchOS, tvOS, and others are all new enough that libc++ is their baseline. + _ => {} + } + + // If the deployment target met or exceeded the C++ baseline + deployment_target_ver + }; + + let rustc = self.getenv("RUSTC"); + let rustc = rustc.as_deref(); + // note the hardcoded minimums here are subject to change in a future compiler release, + // so the ones rustc knows about are preferred. For any compiler version that has bumped them + // though, `--print deployment-target` will be present and the fallbacks won't be used. + // + // the ordering of env -> rustc -> old defaults is intentional for performance when using + // an explicit target + match os { + AppleOs::MacOs => deployment_from_env("MACOSX_DEPLOYMENT_TARGET") + .or_else(|| rustc_provided_target(rustc, target)) + .map(maybe_cpp_version_baseline) + .unwrap_or_else(|| { + if arch_str == Some("aarch64") { + "11.0".into() + } else { + maybe_cpp_version_baseline("10.7".into()) + } + }), + + AppleOs::Ios => deployment_from_env("IPHONEOS_DEPLOYMENT_TARGET") + .or_else(|| rustc_provided_target(rustc, target)) + .map(maybe_cpp_version_baseline) + .unwrap_or_else(|| OLD_IOS_MINIMUM_VERSION.into()), + + AppleOs::WatchOs => deployment_from_env("WATCHOS_DEPLOYMENT_TARGET") + .or_else(|| rustc_provided_target(rustc, target)) + .unwrap_or_else(|| "5.0".into()), + + AppleOs::TvOs => deployment_from_env("TVOS_DEPLOYMENT_TARGET") + .or_else(|| rustc_provided_target(rustc, target)) + .unwrap_or_else(|| "9.0".into()), + } + } + + fn cuda_file_count(&self) -> usize { + self.files + .iter() + .filter(|file| file.extension() == Some(OsStr::new("cu"))) + .count() + } } impl Default for Build { @@ -2852,10 +3558,40 @@ impl Tool { family: family, cuda: false, removed_args: Vec::new(), + has_internal_target_arg: false, } } fn with_features(path: PathBuf, clang_driver: Option<&str>, cuda: bool) -> Self { + fn detect_family(path: &Path) -> ToolFamily { + let mut cmd = Command::new(path); + cmd.arg("--version"); + + let stdout = match run_output(&mut cmd, &path.to_string_lossy()) + .ok() + .and_then(|o| String::from_utf8(o).ok()) + { + Some(s) => s, + None => { + // --version failed. fallback to gnu + println!("cargo-warning: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 + println!( + "cargo-warning:Compiler version doesn't include clang or GCC: {:?}", + cmd + ); + ToolFamily::Gnu + } + } + // 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") { @@ -2868,10 +3604,10 @@ impl Tool { _ => ToolFamily::Clang, } } else { - ToolFamily::Gnu + detect_family(&path) } } else { - ToolFamily::Gnu + detect_family(&path) }; Tool { @@ -2883,6 +3619,7 @@ impl Tool { family: family, cuda: cuda, removed_args: Vec::new(), + has_internal_target_arg: false, } } @@ -2891,11 +3628,14 @@ impl Tool { self.removed_args.push(flag); } - /// Add a flag, and optionally prepend the NVCC wrapper flag "-Xcompiler". + /// Push an "exotic" flag to the end of the compiler's arguments list. /// - /// Currently this is only used for compiling CUDA sources, since NVCC only - /// accepts a limited set of GNU-like flags, and the rest must be prefixed - /// with a "-Xcompiler" flag to get passed to the underlying C++ compiler. + /// 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()); @@ -2903,6 +3643,9 @@ impl Tool { 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(); @@ -2930,7 +3673,7 @@ impl Tool { return false; } - /// Don't push optimization arg if it conflicts with existing args + /// 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); @@ -3034,6 +3777,17 @@ impl Tool { self.family == ToolFamily::Clang } + /// Whether the tool is AppleClang under .xctoolchain + #[cfg(target_vendor = "apple")] + fn is_xctoolchain_clang(&self) -> bool { + let path = self.path.to_string_lossy(); + path.contains(".xctoolchain/") + } + #[cfg(not(target_vendor = "apple"))] + fn is_xctoolchain_clang(&self) -> bool { + false + } + /// Whether the tool is MSVC-like. pub fn is_like_msvc(&self) -> bool { match self.family { @@ -3043,21 +3797,19 @@ impl Tool { } } -fn run(cmd: &mut Command, program: &str) -> Result<(), Error> { - let (mut child, print) = spawn(cmd, program)?; +fn wait_on_child(cmd: &Command, program: &str, child: &mut Child) -> Result<(), Error> { let status = match child.wait() { Ok(s) => s, - Err(_) => { + Err(e) => { return Err(Error::new( ErrorKind::ToolExecError, - &format!( - "Failed to wait on spawned child process, command {:?} with args {:?}.", - cmd, program + format!( + "Failed to wait on spawned child process, command {:?} with args {:?}: {}.", + cmd, program, e ), )); } }; - print.join().unwrap(); println!("{}", status); if status.success() { @@ -3065,7 +3817,7 @@ fn run(cmd: &mut Command, program: &str) -> Result<(), Error> { } else { Err(Error::new( ErrorKind::ToolExecError, - &format!( + format!( "Command {:?} with args {:?} did not execute successfully (status code {}).", cmd, program, status ), @@ -3073,9 +3825,57 @@ fn run(cmd: &mut Command, program: &str) -> Result<(), Error> { } } +#[cfg(feature = "parallel")] +fn try_wait_on_child( + cmd: &Command, + program: &str, + child: &mut Child, + stdout: &mut dyn io::Write, +) -> Result, Error> { + match child.try_wait() { + Ok(Some(status)) => { + 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) => Err(Error::new( + ErrorKind::ToolExecError, + format!( + "Failed to wait on spawned child process, command {:?} with args {:?}: {}.", + cmd, program, e + ), + )), + } +} + +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 run_output(cmd: &mut Command, program: &str) -> Result, Error> { cmd.stdout(Stdio::piped()); - let (mut child, print) = spawn(cmd, program)?; + + let mut print = PrintThread::new()?; + let mut child = spawn(cmd, program, print.pipe_writer().take().unwrap())?; + let mut stdout = vec![]; child .stdout @@ -3083,70 +3883,46 @@ fn run_output(cmd: &mut Command, program: &str) -> Result, Error> { .unwrap() .read_to_end(&mut stdout) .unwrap(); - let status = match child.wait() { - Ok(s) => s, - Err(_) => { - return Err(Error::new( - ErrorKind::ToolExecError, - &format!( - "Failed to wait on spawned child process, command {:?} with args {:?}.", - cmd, program - ), - )); - } - }; - print.join().unwrap(); - println!("{}", status); - if status.success() { - Ok(stdout) - } else { - Err(Error::new( - ErrorKind::ToolExecError, - &format!( - "Command {:?} with args {:?} did not execute successfully (status code {}).", - cmd, program, status - ), - )) - } + wait_on_child(cmd, program, &mut child)?; + + Ok(stdout) } -fn spawn(cmd: &mut Command, program: &str) -> Result<(Child, JoinHandle<()>), Error> { - println!("running: {:?}", cmd); +fn spawn(cmd: &mut Command, program: &str, pipe_writer: File) -> Result { + struct ResetStderr<'cmd>(&'cmd mut Command); - // Capture the standard error coming from these programs, 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. - match cmd.stderr(Stdio::piped()).spawn() { - Ok(mut child) => { - let stderr = BufReader::new(child.stderr.take().unwrap()); - let print = thread::spawn(move || { - for line in stderr.split(b'\n').filter_map(|l| l.ok()) { - print!("cargo:warning="); - std::io::stdout().write_all(&line).unwrap(); - println!(""); - } - }); - Ok((child, print)) + 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()); } + } + + println!("running: {:?}", cmd); + + let cmd = ResetStderr(cmd); + + 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/alexcrichton/cc-rs#compile-time-requirements \ + " (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), + format!("Failed to find tool. Is `{}` installed?{}", program, extra), )) } - Err(ref e) => Err(Error::new( + Err(e) => Err(Error::new( ErrorKind::ToolExecError, - &format!( + format!( "Command {:?} with args {:?} failed to start: {:?}", - cmd, program, e + cmd.0, program, e ), )), } @@ -3163,10 +3939,11 @@ fn command_add_output_file( cuda: bool, msvc: bool, clang: bool, + gnu: bool, is_asm: bool, is_arm: bool, ) { - if msvc && !clang && !cuda && !(is_asm && is_arm) { + if msvc && !clang && !gnu && !cuda && !(is_asm && is_arm) { let mut s = OsString::from("-Fo"); s.push(&dst); cmd.arg(s); @@ -3175,6 +3952,24 @@ fn command_add_output_file( } } +#[derive(Clone, Copy)] +enum AppleOs { + MacOs, + Ios, + WatchOs, + TvOs, +} +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"), + } + } +} + // Use by default minimum available API level // See note about naming here // https://android.googlesource.com/platform/ndk/+/refs/heads/ndk-release-r21/docs/BuildSystemMaintainers.md#Clang @@ -3196,13 +3991,12 @@ 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() { - filename_str.contains("android") - } else { - false + if let Some(idx) = filename_str.rfind("-") { + return filename_str.split_at(idx).0.contains("android"); + } } - } else { - false } + false } #[test] @@ -3215,6 +4009,9 @@ fn test_android_clang_compiler_uses_target_arg_internally() { &PathBuf::from(format!("armv7a-linux-androideabi{}-clang++", version)) )); } + assert!(!android_clang_compiler_uses_target_arg_internally( + &PathBuf::from("clang-i686-linux-android") + )); assert!(!android_clang_compiler_uses_target_arg_internally( &PathBuf::from("clang") )); @@ -3272,7 +4069,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") @@ -3289,7 +4088,7 @@ fn map_darwin_target_from_rust_to_compiler_architecture(target: &str) -> Option< } } -fn which(tool: &Path) -> Option { +fn which(tool: &Path, path_entries: Option) -> Option { fn check_exe(exe: &mut PathBuf) -> bool { let exe_ext = std::env::consts::EXE_EXTENSION; exe.exists() || (!exe_ext.is_empty() && exe.set_extension(exe_ext) && exe.exists()) @@ -3302,9 +4101,127 @@ fn which(tool: &Path) -> Option { } // Loop through PATH entries searching for the |tool|. - let path_entries = env::var_os("PATH")?; + 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 }; }) } + +// search for |prog| on 'programs' path in '|cc| -print-search-dirs' output +fn search_programs(cc: &mut Command, prog: &str) -> Option { + let search_dirs = run_output(cc.arg("-print-search-dirs"), "cc").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()?; + for dirs in search_dirs.split(|c| c == '\r' || c == '\n') { + if let Some(path) = dirs.strip_prefix("programs: =") { + return which(Path::new(prog), Some(OsString::from(path))); + } + } + None +} + +#[derive(Clone, Copy, PartialEq)] +enum AsmFileExt { + /// `.asm` files. On MSVC targets, we assume these should be passed to MASM + /// (`ml{,64}.exe`). + DotAsm, + /// `.s` or `.S` files, which do not have the special handling on MSVC targets. + DotS, +} + +impl AsmFileExt { + fn from_path(file: &Path) -> Option { + if let Some(ext) = file.extension() { + if let Some(ext) = ext.to_str() { + let ext = ext.to_lowercase(); + match &*ext { + "asm" => return Some(AsmFileExt::DotAsm), + "s" => return Some(AsmFileExt::DotS), + _ => return None, + } + } + } + None + } +} + +struct PrintThread { + handle: Option>, + pipe_writer: Option, +} + +impl PrintThread { + fn new() -> Result { + 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 = Vec::with_capacity(20); + let stdout = io::stdout(); + + // read_until returns 0 on Eof + while stderr.read_until(b'\n', &mut line).unwrap() != 0 { + { + let mut stdout = stdout.lock(); + + stdout.write_all(b"cargo:warning=").unwrap(); + stdout.write_all(&line).unwrap(); + stdout.write_all(b"\n").unwrap(); + } + + // read_until does not clear the buffer + line.clear(); + } + }); + + Ok(Self { + handle: Some(print), + pipe_writer: Some(pipe_writer), + }) + } + + fn pipe_writer(&mut self) -> &mut Option { + &mut self.pipe_writer + } + + fn pipe_writer_cloned(&self) -> Result, 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(); + } +} + +/// Remove all element in `vec` which `f(element)` returns `false`. +/// +/// TODO: Remove this once the MSRV is bumped to v1.61 +#[cfg(feature = "parallel")] +fn retain_unordered_mut(vec: &mut Vec, 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/os_pipe.rs b/src/os_pipe.rs new file mode 100644 index 000000000..218c9bc9f --- /dev/null +++ b/src/os_pipe.rs @@ -0,0 +1,28 @@ +//! 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 new file mode 100644 index 000000000..ec4e547f0 --- /dev/null +++ b/src/os_pipe/unix.rs @@ -0,0 +1,121 @@ +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 { + 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 new file mode 100644 index 000000000..212632e43 --- /dev/null +++ b/src/os_pipe/windows.rs @@ -0,0 +1,24 @@ +use crate::windows_sys::{CreatePipe, INVALID_HANDLE_VALUE}; +use std::{fs::File, io, os::windows::prelude::*, ptr}; + +/// 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/registry.rs b/src/registry.rs index 2ac2fa63b..7c1c2dd79 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -3,71 +3,35 @@ // http://rust-lang.org/COPYRIGHT. // // Licensed under the Apache License, Version 2.0 or the MIT license -// , at your +// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// , at your // option. This file may not be copied, modified, or distributed // except according to those terms. -use std::ffi::{OsStr, OsString}; -use std::io; -use std::ops::RangeFrom; -use std::os::raw; -use std::os::windows::prelude::*; +use crate::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, + ops::RangeFrom, + os::windows::prelude::*, + ptr::null_mut, +}; + +/// Must never be `HKEY_PERFORMANCE_DATA`. +pub(crate) struct RegistryKey(Repr); -pub struct RegistryKey(Repr); - -type HKEY = *mut u8; type DWORD = u32; -type LPDWORD = *mut DWORD; -type LPCWSTR = *const u16; -type LPWSTR = *mut u16; -type LONG = raw::c_long; -type PHKEY = *mut HKEY; -type PFILETIME = *mut u8; -type LPBYTE = *mut u8; -type REGSAM = u32; - -const ERROR_SUCCESS: DWORD = 0; -const ERROR_NO_MORE_ITEMS: DWORD = 259; -const HKEY_LOCAL_MACHINE: HKEY = 0x80000002 as HKEY; -const REG_SZ: DWORD = 1; -const KEY_READ: DWORD = 0x20019; -const KEY_WOW64_32KEY: DWORD = 0x200; - -#[link(name = "advapi32")] -extern "system" { - fn RegOpenKeyExW( - key: HKEY, - lpSubKey: LPCWSTR, - ulOptions: DWORD, - samDesired: REGSAM, - phkResult: PHKEY, - ) -> LONG; - fn RegEnumKeyExW( - key: HKEY, - dwIndex: DWORD, - lpName: LPWSTR, - lpcName: LPDWORD, - lpReserved: LPDWORD, - lpClass: LPWSTR, - lpcClass: LPDWORD, - lpftLastWriteTime: PFILETIME, - ) -> LONG; - fn RegQueryValueExW( - hKey: HKEY, - lpValueName: LPCWSTR, - lpReserved: LPDWORD, - lpType: LPDWORD, - lpData: LPBYTE, - lpcbData: LPDWORD, - ) -> LONG; - fn RegCloseKey(hKey: HKEY) -> LONG; -} struct OwnedKey(HKEY); +/// Note: must not encode `HKEY_PERFORMANCE_DATA` or one of its subkeys. enum Repr { - Const(HKEY), + /// `HKEY_LOCAL_MACHINE`. + LocalMachine, + /// A subkey of `HKEY_LOCAL_MACHINE`. Owned(OwnedKey), } @@ -79,19 +43,20 @@ pub struct Iter<'a> { unsafe impl Sync for Repr {} unsafe impl Send for Repr {} -pub static LOCAL_MACHINE: RegistryKey = RegistryKey(Repr::Const(HKEY_LOCAL_MACHINE)); +pub(crate) const LOCAL_MACHINE: RegistryKey = RegistryKey(Repr::LocalMachine); impl RegistryKey { fn raw(&self) -> HKEY { match self.0 { - Repr::Const(val) => val, + Repr::LocalMachine => HKEY_LOCAL_MACHINE, Repr::Owned(ref val) => val.0, } } + /// Open a sub-key of `self`. pub fn open(&self, key: &OsStr) -> io::Result { let key = key.encode_wide().chain(Some(0)).collect::>(); - let mut ret = 0 as *mut _; + let mut ret = null_mut(); let err = unsafe { RegOpenKeyExW( self.raw(), @@ -101,7 +66,7 @@ impl RegistryKey { &mut ret, ) }; - if err == ERROR_SUCCESS as LONG { + if err == ERROR_SUCCESS { Ok(RegistryKey(Repr::Owned(OwnedKey(ret)))) } else { Err(io::Error::from_raw_os_error(err as i32)) @@ -124,12 +89,12 @@ impl RegistryKey { let err = RegQueryValueExW( self.raw(), name.as_ptr(), - 0 as *mut _, + null_mut(), &mut kind, - 0 as *mut _, + null_mut(), &mut len, ); - if err != ERROR_SUCCESS as LONG { + if err != ERROR_SUCCESS { return Err(io::Error::from_raw_os_error(err as i32)); } if kind != REG_SZ { @@ -140,28 +105,49 @@ impl RegistryKey { } // The length here is the length in bytes, but we're using wide - // characters so we need to be sure to halve it for the capacity + // characters so we need to be sure to halve it for the length // passed in. - let mut v = Vec::with_capacity(len as usize / 2); + assert!(len % 2 == 0, "impossible wide string size: {} bytes", len); + let vlen = len as usize / 2; + // Defensively initialized, see comment about + // `HKEY_PERFORMANCE_DATA` below. + let mut v = vec![0u16; vlen]; let err = RegQueryValueExW( self.raw(), name.as_ptr(), - 0 as *mut _, - 0 as *mut _, + null_mut(), + null_mut(), v.as_mut_ptr() as *mut _, &mut len, ); - if err != ERROR_SUCCESS as LONG { + // We don't check for `ERROR_MORE_DATA` (which would if the value + // grew between the first and second call to `RegQueryValueExW`), + // both because it's extremely unlikely, and this is a bit more + // defensive more defensive against weird types of registry keys. + if err != ERROR_SUCCESS { return Err(io::Error::from_raw_os_error(err as i32)); } - v.set_len(len as usize / 2); - + // The length is allowed to change, but should still be even, as + // well as smaller. + assert!(len % 2 == 0, "impossible wide string size: {} bytes", len); + // If the length grew but returned a success code, it *probably* + // indicates we're `HKEY_PERFORMANCE_DATA` or a subkey(?). We + // consider this UB, since those keys write "undefined" or + // "unpredictable" values to len, and need to use a completely + // different loop structure. This should be impossible (and enforce + // it in the API to the best of our ability), but to mitigate the + // damage we do some smoke-checks on the len, and ensure `v` has + // been fully initialized (rather than trusting the result of + // `RegQueryValueExW`). + let actual_len = len as usize / 2; + assert!(actual_len <= v.len()); + v.truncate(actual_len); // Some registry keys may have a terminating nul character, but // we're not interested in that, so chop it off if it's there. - if v[v.len() - 1] == 0 { + if !v.is_empty() && v[v.len() - 1] == 0 { v.pop(); } - Ok(OsString::from_wide(&v)) + return Ok(OsString::from_wide(&v)); } } } @@ -186,14 +172,14 @@ impl<'a> Iterator for Iter<'a> { i, v.as_mut_ptr(), &mut len, - 0 as *mut _, - 0 as *mut _, - 0 as *mut _, - 0 as *mut _, + null_mut(), + null_mut(), + null_mut(), + null_mut(), ); - if ret == ERROR_NO_MORE_ITEMS as LONG { + if ret == ERROR_NO_MORE_ITEMS { None - } else if ret != ERROR_SUCCESS as LONG { + } else if ret != ERROR_SUCCESS { Some(Err(io::Error::from_raw_os_error(ret as i32))) } else { v.set_len(len as usize); diff --git a/src/setup_config.rs b/src/setup_config.rs index bc2b1c2d3..fe2c03096 100644 --- a/src/setup_config.rs +++ b/src/setup_config.rs @@ -1,26 +1,26 @@ // Copyright © 2017 winapi-rs developers // Licensed under the Apache License, Version 2.0 -// or the MIT license -// , at your option. +// or the MIT license +// , at your option. // All files in the project carrying such notice may not be copied, modified, or distributed // except according to those terms. #![allow(bad_style)] #![allow(unused)] -use crate::winapi::Interface; -use crate::winapi::BSTR; -use crate::winapi::LPCOLESTR; -use crate::winapi::LPSAFEARRAY; -use crate::winapi::S_FALSE; -use crate::winapi::{CoCreateInstance, CLSCTX_ALL}; -use crate::winapi::{IUnknown, IUnknownVtbl}; -use crate::winapi::{HRESULT, LCID, LPCWSTR, PULONGLONG}; -use crate::winapi::{LPFILETIME, ULONG}; -use std::ffi::OsString; -use std::ptr::null_mut; +use crate::{ + com::{BStr, ComPtr}, + winapi::{ + IUnknown, IUnknownVtbl, Interface, LCID, LPCOLESTR, LPCWSTR, LPFILETIME, LPSAFEARRAY, + PULONGLONG, ULONG, + }, + windows_sys::{CoCreateInstance, BSTR, CLSCTX_ALL, HRESULT, S_FALSE}, +}; -use crate::com::{BStr, ComPtr}; +use std::{ + ffi::OsString, + ptr::{null, null_mut}, +}; // Bindings to the Setup.Configuration stuff pub type InstanceState = u32; @@ -212,7 +212,7 @@ impl SetupInstance { SetupInstance(ComPtr::from_raw(obj)) } pub fn instance_id(&self) -> Result { - let mut s = null_mut(); + let mut s = null(); let err = unsafe { self.0.GetInstanceId(&mut s) }; let bstr = unsafe { BStr::from_raw(s) }; if err < 0 { @@ -221,7 +221,7 @@ impl SetupInstance { Ok(bstr.to_osstring()) } pub fn installation_name(&self) -> Result { - let mut s = null_mut(); + let mut s = null(); let err = unsafe { self.0.GetInstallationName(&mut s) }; let bstr = unsafe { BStr::from_raw(s) }; if err < 0 { @@ -230,7 +230,7 @@ impl SetupInstance { Ok(bstr.to_osstring()) } pub fn installation_path(&self) -> Result { - let mut s = null_mut(); + let mut s = null(); let err = unsafe { self.0.GetInstallationPath(&mut s) }; let bstr = unsafe { BStr::from_raw(s) }; if err < 0 { @@ -239,7 +239,7 @@ impl SetupInstance { Ok(bstr.to_osstring()) } pub fn installation_version(&self) -> Result { - let mut s = null_mut(); + let mut s = null(); let err = unsafe { self.0.GetInstallationVersion(&mut s) }; let bstr = unsafe { BStr::from_raw(s) }; if err < 0 { @@ -248,7 +248,7 @@ impl SetupInstance { Ok(bstr.to_osstring()) } pub fn product_path(&self) -> Result { - let mut s = null_mut(); + let mut s = null(); let this = self.0.cast::()?; let err = unsafe { this.GetProductPath(&mut s) }; let bstr = unsafe { BStr::from_raw(s) }; diff --git a/src/winapi.rs b/src/winapi.rs index c416325b5..223599f62 100644 --- a/src/winapi.rs +++ b/src/winapi.rs @@ -1,7 +1,7 @@ // Copyright © 2015-2017 winapi-rs developers // Licensed under the Apache License, Version 2.0 -// or the MIT license -// , at your option. +// or the MIT license +// , at your option. // All files in the project carrying such notice may not be copied, modified, or distributed // except according to those terms. @@ -11,20 +11,13 @@ use std::os::raw; pub type wchar_t = u16; -pub type UINT = raw::c_uint; -pub type LPUNKNOWN = *mut IUnknown; +pub use crate::windows_sys::{FILETIME, GUID, HRESULT, SAFEARRAY, SAFEARRAYBOUND}; + pub type REFIID = *const IID; pub type IID = GUID; -pub type REFCLSID = *const IID; -pub type PVOID = *mut raw::c_void; -pub type USHORT = raw::c_ushort; pub type ULONG = raw::c_ulong; -pub type LONG = raw::c_long; pub type DWORD = u32; -pub type LPVOID = *mut raw::c_void; -pub type HRESULT = raw::c_long; pub type LPFILETIME = *mut FILETIME; -pub type BSTR = *mut OLECHAR; pub type OLECHAR = WCHAR; pub type WCHAR = wchar_t; pub type LPCOLESTR = *const OLECHAR; @@ -33,75 +26,10 @@ pub type LPCWSTR = *const WCHAR; pub type PULONGLONG = *mut ULONGLONG; pub type ULONGLONG = u64; -pub const S_OK: HRESULT = 0; -pub const S_FALSE: HRESULT = 1; -pub const COINIT_MULTITHREADED: u32 = 0x0; - -pub type CLSCTX = u32; - -pub const CLSCTX_INPROC_SERVER: CLSCTX = 0x1; -pub const CLSCTX_INPROC_HANDLER: CLSCTX = 0x2; -pub const CLSCTX_LOCAL_SERVER: CLSCTX = 0x4; -pub const CLSCTX_REMOTE_SERVER: CLSCTX = 0x10; - -pub const CLSCTX_ALL: CLSCTX = - CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER | CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER; - -#[repr(C)] -#[derive(Copy, Clone)] -pub struct GUID { - pub Data1: raw::c_ulong, - pub Data2: raw::c_ushort, - pub Data3: raw::c_ushort, - pub Data4: [raw::c_uchar; 8], -} - -#[repr(C)] -#[derive(Copy, Clone)] -pub struct FILETIME { - pub dwLowDateTime: DWORD, - pub dwHighDateTime: DWORD, -} - pub trait Interface { fn uuidof() -> GUID; } -#[link(name = "ole32")] -#[link(name = "oleaut32")] -extern "C" {} - -extern "system" { - pub fn CoInitializeEx(pvReserved: LPVOID, dwCoInit: DWORD) -> HRESULT; - pub fn CoCreateInstance( - rclsid: REFCLSID, - pUnkOuter: LPUNKNOWN, - dwClsContext: DWORD, - riid: REFIID, - ppv: *mut LPVOID, - ) -> HRESULT; - pub fn SysFreeString(bstrString: BSTR); - pub fn SysStringLen(pbstr: BSTR) -> UINT; -} - -#[repr(C)] -#[derive(Copy, Clone)] -pub struct SAFEARRAYBOUND { - pub cElements: ULONG, - pub lLbound: LONG, -} - -#[repr(C)] -#[derive(Copy, Clone)] -pub struct SAFEARRAY { - pub cDims: USHORT, - pub fFeatures: USHORT, - pub cbElements: ULONG, - pub cLocks: ULONG, - pub pvData: PVOID, - pub rgsabound: [SAFEARRAYBOUND; 1], -} - pub type LPSAFEARRAY = *mut SAFEARRAY; macro_rules! DEFINE_GUID { @@ -110,10 +38,10 @@ macro_rules! DEFINE_GUID { $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 { - Data1: $l, - Data2: $w1, - Data3: $w2, - Data4: [$b1, $b2, $b3, $b4, $b5, $b6, $b7, $b8], + data1: $l, + data2: $w1, + data3: $w2, + data4: [$b1, $b2, $b3, $b4, $b5, $b6, $b7, $b8], }; }; } @@ -197,10 +125,10 @@ macro_rules! RIDL { #[inline] fn uuidof() -> $crate::winapi::GUID { $crate::winapi::GUID { - Data1: $l, - Data2: $w1, - Data3: $w2, - Data4: [$b1, $b2, $b3, $b4, $b5, $b6, $b7, $b8], + data1: $l, + data2: $w1, + data3: $w2, + data4: [$b1, $b2, $b3, $b4, $b5, $b6, $b7, $b8], } } } diff --git a/src/windows_registry.rs b/src/windows_registry.rs index f68206273..c6f71effd 100644 --- a/src/windows_registry.rs +++ b/src/windows_registry.rs @@ -3,8 +3,8 @@ // http://rust-lang.org/COPYRIGHT. // // Licensed under the Apache License, Version 2.0 or the MIT license -// , at your +// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// , at your // option. This file may not be copied, modified, or distributed // except according to those terms. @@ -80,6 +80,7 @@ pub fn find_tool(target: &str, tool: &str) -> Option { /// A version of Visual Studio #[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[non_exhaustive] pub enum VsVers { /// Visual Studio 12 (2013) Vs12, @@ -91,13 +92,6 @@ pub enum VsVers { Vs16, /// Visual Studio 17 (2022) Vs17, - - /// Hidden variant that should not be matched on. Callers that want to - /// handle an enumeration of `VsVers` instances should always have a default - /// case meaning that it's a VS version they don't understand. - #[doc(hidden)] - #[allow(bad_style)] - __Nonexhaustive_do_not_match_this_or_your_code_will_break, } /// Find the most recent installed version of Visual Studio @@ -106,7 +100,7 @@ pub enum VsVers { /// generator. #[cfg(not(windows))] pub fn find_vs_version() -> Result { - Err(format!("not windows")) + Err("not windows".to_string()) } /// Documented above @@ -393,7 +387,7 @@ mod impl_ { // according to Microsoft. To help head off potential regressions though, // we keep the registry method as a fallback option. // - // [more reliable]: https://github.com/alexcrichton/cc-rs/pull/331 + // [more reliable]: https://github.com/rust-lang/cc-rs/pull/331 fn find_tool_in_vs15_path(tool: &str, target: &str) -> Option { let mut path = match vs15plus_instances(target) { Some(instances) => instances @@ -431,7 +425,7 @@ mod impl_ { target: &str, instance_path: &PathBuf, ) -> Option { - let (bin_path, host_dylib_path, lib_path, include_path) = + let (root_path, bin_path, host_dylib_path, lib_path, include_path) = vs15plus_vc_paths(target, instance_path)?; let tool_path = bin_path.join(tool); if !tool_path.exists() { @@ -444,7 +438,7 @@ mod impl_ { tool.libs.push(lib_path); tool.include.push(include_path); - if let Some((atl_lib_path, atl_include_path)) = atl_paths(target, &bin_path) { + if let Some((atl_lib_path, atl_include_path)) = atl_paths(target, &root_path) { tool.libs.push(atl_lib_path); tool.include.push(atl_include_path); } @@ -456,14 +450,10 @@ mod impl_ { fn vs15plus_vc_paths( target: &str, - instance_path: &PathBuf, - ) -> Option<(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(); + instance_path: &Path, + ) -> Option<(PathBuf, PathBuf, PathBuf, PathBuf, PathBuf)> { + let version = vs15plus_vc_read_version(instance_path)?; + let host = match host_arch() { X86 => "X86", X86_64 => "X64", @@ -489,7 +479,44 @@ mod impl_ { .join(&host.to_lowercase()); let lib_path = path.join("lib").join(&lib_subdir(target)?); let include_path = path.join("include"); - Some((bin_path, host_dylib_path, lib_path, include_path)) + Some((path, bin_path, host_dylib_path, lib_path, include_path)) + } + + fn vs15plus_vc_read_version(dir: &Path) -> Option { + // 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: &str, path: &Path) -> Option<(PathBuf, PathBuf)> { @@ -877,7 +904,9 @@ mod impl_ { // see http://stackoverflow.com/questions/328017/path-to-msbuild pub fn find_msbuild(target: &str) -> Option { // VS 15 (2017) changed how to locate msbuild - if let Some(r) = find_msbuild_vs16(target) { + if let Some(r) = find_msbuild_vs17(target) { + return Some(r); + } else if let Some(r) = find_msbuild_vs16(target) { return Some(r); } else if let Some(r) = find_msbuild_vs15(target) { return Some(r); diff --git a/src/windows_sys.rs b/src/windows_sys.rs new file mode 100644 index 000000000..20a256076 --- /dev/null +++ b/src/windows_sys.rs @@ -0,0 +1,224 @@ +// 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.49.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 CreatePipe( + hreadpipe: *mut HANDLE, + hwritepipe: *mut HANDLE, + lppipeattributes: *const SECURITY_ATTRIBUTES, + nsize: u32, + ) -> BOOL; +} +#[link(name = "kernel32")] +extern "system" { + pub fn OpenSemaphoreA(dwdesiredaccess: u32, binherithandle: BOOL, lpname: PCSTR) -> HANDLE; +} +#[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) -> WIN32_ERROR; +} +#[link(name = "ole32")] +extern "system" { + pub fn CoCreateInstance( + rclsid: *const GUID, + punkouter: IUnknown, + 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: COINIT) -> 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; +#[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 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(), + } + } +} +impl ::core::marker::Copy for GUID {} +impl ::core::clone::Clone for GUID { + fn clone(&self) -> Self { + *self + } +} +pub type HANDLE = *mut ::core::ffi::c_void; +pub type HKEY = *mut ::core::ffi::c_void; +pub const HKEY_LOCAL_MACHINE: HKEY = invalid_mut(-2147483646i32 as _); +pub type HRESULT = i32; +pub const INVALID_HANDLE_VALUE: HANDLE = invalid_mut(-1i32 as _); +pub type IUnknown = *mut ::core::ffi::c_void; +pub const KEY_READ: REG_SAM_FLAGS = 131097u32; +pub const KEY_WOW64_32KEY: REG_SAM_FLAGS = 512u32; +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 + } +} +#[repr(C)] +pub struct SECURITY_ATTRIBUTES { + pub nLength: u32, + pub lpSecurityDescriptor: *mut ::core::ffi::c_void, + pub bInheritHandle: BOOL, +} +impl ::core::marker::Copy for SECURITY_ATTRIBUTES {} +impl ::core::clone::Clone for SECURITY_ATTRIBUTES { + 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 = 1i32; +pub const S_OK: HRESULT = 0i32; +pub type THREAD_ACCESS_RIGHTS = u32; +pub const THREAD_SYNCHRONIZE: THREAD_ACCESS_RIGHTS = 1048576u32; +pub const WAIT_ABANDONED: WIN32_ERROR = 128u32; +pub const WAIT_FAILED: WIN32_ERROR = 4294967295u32; +pub const WAIT_OBJECT_0: WIN32_ERROR = 0u32; +pub const WAIT_TIMEOUT: WIN32_ERROR = 258u32; +pub type WIN32_ERROR = u32; + +/// Adapted from +/// [`core::ptr::invalid_mut()`](https://doc.rust-lang.org/src/core/ptr/mod.rs.html#600-607). +/// +/// This function should actually use `core::mem::transmute` but due to msrv +/// we use `as` casting instead. +/// +/// Once msrv is bumped to 1.56, replace this with `core::mem::transmute` since +/// it is const stablised in 1.56 +/// +/// NOTE that once supports `strict_provenance` we would also have to update +/// this. +const fn invalid_mut(addr: usize) -> *mut T { + addr as *mut T +} diff --git a/tests/support/mod.rs b/tests/support/mod.rs index f3c04405a..94b51f653 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -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 35ef87577..220d2f2f7 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -20,7 +20,7 @@ fn gnu_smoke() { test.cmd(0) .must_have("-O2") .must_have("foo.c") - .must_not_have("-g") + .must_not_have("-gdwarf-4") .must_have("-c") .must_have("-ffunction-sections") .must_have("-fdata-sections"); @@ -52,19 +52,46 @@ fn gnu_opt_level_s() { .must_not_have("-Oz"); } +#[test] +fn gnu_debug() { + let test = Test::gnu(); + 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(); + test.gcc() + .target("x86_64-apple-darwin") + .debug(true) + .file("foo.c") + .compile("foo"); + test.cmd(0).must_have("-gdwarf-2"); +} + #[test] fn gnu_debug_fp_auto() { let test = Test::gnu(); - test.gcc().debug(true).file("foo.c").compile("foo"); - test.cmd(0).must_have("-g"); + 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"); } #[test] fn gnu_debug_fp() { let test = Test::gnu(); - test.gcc().debug(true).file("foo.c").compile("foo"); - test.cmd(0).must_have("-g"); + 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"); } @@ -74,20 +101,22 @@ fn gnu_debug_nofp() { let test = Test::gnu(); test.gcc() + .target("x86_64-unknown-linux") .debug(true) .force_frame_pointer(false) .file("foo.c") .compile("foo"); - test.cmd(0).must_have("-g"); + test.cmd(0).must_have("-gdwarf-4"); test.cmd(0).must_not_have("-fno-omit-frame-pointer"); let test = Test::gnu(); test.gcc() + .target("x86_64-unknown-linux") .force_frame_pointer(false) .debug(true) .file("foo.c") .compile("foo"); - test.cmd(0).must_have("-g"); + test.cmd(0).must_have("-gdwarf-4"); test.cmd(0).must_not_have("-fno-omit-frame-pointer"); } @@ -314,11 +343,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) @@ -351,6 +378,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(); @@ -427,3 +462,117 @@ 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"))] +#[test] +fn asm_flags() { + let test = Test::gnu(); + test.gcc() + .file("foo.c") + .file("x86_64.asm") + .file("x86_64.S") + .asm_flag("--abc") + .compile("foo"); + test.cmd(0).must_not_have("--abc"); + 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 maintainence 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, expected) 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"); + + test.cmd(0) + .must_have(format!("-mmacosx-version-min={}", expected)); + } + + 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() + .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() + .target(&target) + .host(&target) + .file("foo.c") + .compile("foo"); + + test.cmd(0).must_have("-mappletvsimulator-version-min=9.0"); + } +}