Repository: rust-lang/cc-rs
Pull request #918 (2024-01-24 10:00:30)

Use RUSTC_WRAPPER if no other wrapper is provided
Previously, the `rustc_wrapper_fallback` was only called if the tool
 was **not** a full path.
With this patch, the `RUSTC_WRAPPER` is always used, even if the tool provided is an exact path.
Providing a wrapper manually is still possible and overrides the `RUSTC_WRAPPER`.

If the path to the tool includes spaces it is otherwise impossible to provide a compiler cache like sccache through the CXX or CC environment variables, as the arguments will be split up incorrectly.

See also: https://github.com/corrosion-rs/corrosion/pull/474


Patch:
diff --git a/src/lib.rs b/src/lib.rs
index 0a5b2ae..a555a64 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -122,7 +122,7 @@ pub struct Build {
     compiler: Option<Arc<Path>>,
     archiver: Option<Arc<Path>>,
     ranlib: Option<Arc<Path>>,
-    cargo_metadata: bool,
+    cargo_output: CargoOutput,
     link_lib_modifiers: Vec<Arc<str>>,
     pic: Option<bool>,
     use_plt: Option<bool>,
@@ -330,7 +330,7 @@ impl Build {
             compiler: None,
             archiver: None,
             ranlib: None,
-            cargo_metadata: true,
+            cargo_output: CargoOutput::new(),
             link_lib_modifiers: Vec::new(),
             pic: None,
             use_plt: None,
@@ -527,7 +527,7 @@ impl Build {
         let host = self.get_host()?;
         let mut cfg = Build::new();
         cfg.flag(flag)
-            .cargo_metadata(self.cargo_metadata)
+            .cargo_metadata(self.cargo_output.metadata)
             .target(&target)
             .opt_level(0)
             .host(&host)
@@ -1038,7 +1038,17 @@ impl Build {
     ///  - If `emit_rerun_if_env_changed` is not `false`, `rerun-if-env-changed=`*env*
     ///
     pub fn cargo_metadata(&mut self, cargo_metadata: bool) -> &mut Build {
-        self.cargo_metadata = cargo_metadata;
+        self.cargo_output.metadata = cargo_metadata;
+        self
+    }
+
+    /// Define whether compile warnings should be emitted for cargo. Defaults to
+    /// `true`.
+    ///
+    /// If disabled, compiler messages will not be printed.
+    /// Issues unrelated to the compilation will always produce cargo warnings regardless of this setting.
+    pub fn cargo_warnings(&mut self, cargo_warnings: bool) -> &mut Build {
+        self.cargo_output.warnings = cargo_warnings;
         self
     }
 
@@ -1131,52 +1141,11 @@ impl Build {
         };
         let dst = self.get_out_dir()?;
 
-        let mut objects = Vec::new();
-        for file in self.files.iter() {
-            let obj = if file.has_root() || file.components().any(|x| x == Component::ParentDir) {
-                // If `file` is an absolute path or might not be usable directly as a suffix due to
-                // using "..", use the `basename` prefixed with the `dirname`'s hash to ensure name
-                // uniqueness.
-                let basename = file
-                    .file_name()
-                    .ok_or_else(|| Error::new(ErrorKind::InvalidArgument, "file_name() failure"))?
-                    .to_string_lossy();
-                let dirname = file
-                    .parent()
-                    .ok_or_else(|| Error::new(ErrorKind::InvalidArgument, "parent() failure"))?
-                    .to_string_lossy();
-                let mut hasher = hash_map::DefaultHasher::new();
-                hasher.write(dirname.to_string().as_bytes());
-                dst.join(format!("{:016x}-{}", hasher.finish(), basename))
-                    .with_extension("o")
-            } else {
-                dst.join(file).with_extension("o")
-            };
-            let obj = if !obj.starts_with(&dst) {
-                dst.join(obj.file_name().ok_or_else(|| {
-                    Error::new(ErrorKind::IOError, "Getting object file details failed.")
-                })?)
-            } else {
-                obj
-            };
+        let objects = objects_from_files(&self.files, &dst)?;
+        let print = self.cargo_output.print_thread()?;
 
-            match obj.parent() {
-                Some(s) => fs::create_dir_all(s)?,
-                None => {
-                    return Err(Error::new(
-                        ErrorKind::IOError,
-                        "Getting object file details failed.",
-                    ));
-                }
-            };
-
-            objects.push(Object::new(file.to_path_buf(), obj));
-        }
-
-        let print = PrintThread::new()?;
-
-        self.compile_objects(&objects, &print)?;
-        self.assemble(lib_name, &dst.join(gnu_lib_name), &objects, &print)?;
+        self.compile_objects(&objects, print.as_ref())?;
+        self.assemble(lib_name, &dst.join(gnu_lib_name), &objects, print.as_ref())?;
 
         if self.get_target()?.contains("msvc") {
             let compiler = self.get_base_compiler()?;
@@ -1192,7 +1161,7 @@ impl Build {
                 });
 
             if let Some(atlmfc_lib) = atlmfc_lib {
-                self.print(&format_args!(
+                self.cargo_output.print_metadata(&format_args!(
                     "cargo:rustc-link-search=native={}",
                     atlmfc_lib.display()
                 ));
@@ -1200,15 +1169,16 @@ impl Build {
         }
 
         if self.link_lib_modifiers.is_empty() {
-            self.print(&format_args!("cargo:rustc-link-lib=static={}", lib_name));
+            self.cargo_output
+                .print_metadata(&format_args!("cargo:rustc-link-lib=static={}", lib_name));
         } else {
             let m = self.link_lib_modifiers.join(",");
-            self.print(&format_args!(
+            self.cargo_output.print_metadata(&format_args!(
                 "cargo:rustc-link-lib=static:{}={}",
                 m, lib_name
             ));
         }
-        self.print(&format_args!(
+        self.cargo_output.print_metadata(&format_args!(
             "cargo:rustc-link-search=native={}",
             dst.display()
         ));
@@ -1216,7 +1186,8 @@ impl Build {
         // Add specific C++ libraries, if enabled.
         if self.cpp {
             if let Some(stdlib) = self.get_cpp_link_stdlib()? {
-                self.print(&format_args!("cargo:rustc-link-lib={}", stdlib));
+                self.cargo_output
+                    .print_metadata(&format_args!("cargo:rustc-link-lib={}", stdlib));
             }
         }
 
@@ -1254,10 +1225,10 @@ impl Build {
                     }
                 }
                 if libtst && libdir.is_dir() {
-                    println!(
+                    self.cargo_output.print_metadata(&format_args!(
                         "cargo:rustc-link-search=native={}",
                         libdir.to_str().unwrap()
-                    );
+                    ));
                 }
 
                 // And now the -l flag.
@@ -1266,7 +1237,8 @@ impl Build {
                     "static" => "cudart_static",
                     bad => panic!("unsupported cudart option: {}", bad),
                 };
-                println!("cargo:rustc-link-lib={}", lib);
+                self.cargo_output
+                    .print_metadata(&format_args!("cargo:rustc-link-lib={}", lib));
             }
         }
 
@@ -1316,8 +1288,34 @@ impl Build {
         }
     }
 
+    /// Run the compiler, generating intermediate files, but without linking
+    /// them into an archive file.
+    ///
+    /// This will return a list of compiled object files, in the same order
+    /// as they were passed in as `file`/`files` methods.
+    pub fn compile_intermediates(&self) -> Vec<PathBuf> {
+        match self.try_compile_intermediates() {
+            Ok(v) => v,
+            Err(e) => fail(&e.message),
+        }
+    }
+
+    /// Run the compiler, generating intermediate files, but without linking
+    /// them into an archive file.
+    ///
+    /// This will return a result instead of panicing; see `compile_intermediates()` for the complete description.
+    pub fn try_compile_intermediates(&self) -> Result<Vec<PathBuf>, Error> {
+        let dst = self.get_out_dir()?;
+        let objects = objects_from_files(&self.files, &dst)?;
+        let print = self.cargo_output.print_thread()?;
+
+        self.compile_objects(&objects, print.as_ref())?;
+
+        Ok(objects.into_iter().map(|v| v.dst).collect())
+    }
+
     #[cfg(feature = "parallel")]
-    fn compile_objects(&self, objs: &[Object], print: &PrintThread) -> Result<(), Error> {
+    fn compile_objects(&self, objs: &[Object], print: Option<&PrintThread>) -> Result<(), Error> {
         use std::cell::Cell;
 
         use async_executor::{block_on, YieldOnce};
@@ -1393,7 +1391,9 @@ impl Build {
                                 // sure users always see all the compilation failures.
                                 has_made_progress.set(true);
 
-                                let _ = writeln!(stdout, "cargo:warning={}", err);
+                                if self.cargo_output.warnings {
+                                    let _ = writeln!(stdout, "cargo:warning={}", err);
+                                }
                                 error = Some(err);
 
                                 false
@@ -1425,7 +1425,8 @@ impl Build {
                         YieldOnce::default().await
                     }
                 };
-                let child = spawn(&mut cmd, &program, print.pipe_writer_cloned()?.unwrap())?;
+                let pipe_writer = print.map(PrintThread::clone_pipe_writer).transpose()?;
+                let child = spawn(&mut cmd, &program, pipe_writer)?;
 
                 cell_update(&pendings, |mut pendings| {
                     pendings.push((cmd, program, KillOnDrop(child), token));
@@ -1463,7 +1464,7 @@ impl Build {
     }
 
     #[cfg(not(feature = "parallel"))]
-    fn compile_objects(&self, objs: &[Object], print: &PrintThread) -> Result<(), Error> {
+    fn compile_objects(&self, objs: &[Object], print: Option<&PrintThread>) -> Result<(), Error> {
         for obj in objs {
             let (mut cmd, name) = self.create_compile_object_cmd(obj)?;
             run(&mut cmd, &name, print)?;
@@ -1566,7 +1567,7 @@ impl Build {
             .to_string_lossy()
             .into_owned();
 
-        Ok(run_output(&mut cmd, &name)?)
+        Ok(run_output(&mut cmd, &name, &self.cargo_output)?)
     }
 
     /// Run the compiler, returning the macro-expanded version of the input files.
@@ -2167,11 +2168,7 @@ impl Build {
                     cmd.push_cc_arg(format!("-stdlib=lib{}", stdlib).into());
                 }
                 _ => {
-                    println!(
-                        "cargo:warning=cpp_set_stdlib is specified, but the {:?} compiler \
-                         does not support this option, ignored",
-                        cmd.family
-                    );
+                    self.cargo_output.print_warning(&format_args!("cpp_set_stdlib is specified, but the {:?} compiler does not support this option, ignored", cmd.family));
                 }
             }
         }
@@ -2250,7 +2247,7 @@ impl Build {
         lib_name: &str,
         dst: &Path,
         objs: &[Object],
-        print: &PrintThread,
+        print: Option<&PrintThread>,
     ) -> Result<(), Error> {
         // Delete the destination if it exists as we want to
         // create on the first iteration instead of appending.
@@ -2319,7 +2316,7 @@ impl Build {
         &self,
         dst: &Path,
         objs: &[&Path],
-        print: &PrintThread,
+        print: Option<&PrintThread>,
     ) -> Result<(), Error> {
         let target = self.get_target()?;
 
@@ -2379,9 +2376,11 @@ impl Build {
     }
 
     fn apple_flags(&self, cmd: &mut Tool) -> Result<(), Error> {
+        #[allow(dead_code)]
         enum ArchSpec {
             Device(&'static str),
             Simulator(&'static str),
+            #[allow(dead_code)]
             Catalyst(&'static str),
         }
 
@@ -2508,7 +2507,8 @@ impl Build {
 
         // 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));
+            self.cargo_output
+                .print_metadata(&format_args!("Detecting {:?} SDK path for {}", os, sdk));
             let sdk_path = if let Some(sdkroot) = env::var_os("SDKROOT") {
                 sdkroot
             } else {
@@ -2532,7 +2532,7 @@ impl Build {
 
     fn get_base_compiler(&self) -> Result<Tool, Error> {
         if let Some(c) = &self.compiler {
-            return Ok(Tool::new((**c).to_owned()));
+            return Ok(Tool::new((**c).to_owned(), &self.cargo_output));
         }
         let host = self.get_host()?;
         let target = self.get_target()?;
@@ -2568,7 +2568,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(tool, driver_mode);
+                let mut t = Tool::with_clang_driver(tool, driver_mode, &self.cargo_output);
                 if let Some(cc_wrapper) = wrapper {
                     t.cc_wrapper_path = Some(PathBuf::from(cc_wrapper));
                 }
@@ -2582,12 +2582,12 @@ impl Build {
                     let tool = if self.cpp { "em++" } else { "emcc" };
                     // Windows uses bat file so we have to be a bit more specific
                     if cfg!(windows) {
-                        let mut t = Tool::new(PathBuf::from("cmd"));
+                        let mut t = Tool::new(PathBuf::from("cmd"), &self.cargo_output);
                         t.args.push("/c".into());
                         t.args.push(format!("{}.bat", tool).into());
                         Some(t)
                     } else {
-                        Some(Tool::new(PathBuf::from(tool)))
+                        Some(Tool::new(PathBuf::from(tool), &self.cargo_output))
                     }
                 } else {
                     None
@@ -2642,7 +2642,7 @@ impl Build {
                     default.to_string()
                 };
 
-                let mut t = Tool::new(PathBuf::from(compiler));
+                let mut t = Tool::new(PathBuf::from(compiler), &self.cargo_output);
                 if let Some(cc_wrapper) = Self::rustc_wrapper_fallback() {
                     t.cc_wrapper_path = Some(PathBuf::from(cc_wrapper));
                 }
@@ -2659,7 +2659,7 @@ impl Build {
                 Err(_) => PathBuf::from("nvcc"),
                 Ok(nvcc) => PathBuf::from(&*nvcc),
             };
-            let mut nvcc_tool = Tool::with_features(nvcc, None, self.cuda);
+            let mut nvcc_tool = Tool::with_features(nvcc, None, self.cuda, &self.cargo_output);
             nvcc_tool
                 .args
                 .push(format!("-ccbin={}", tool.path.display()).into());
@@ -2726,7 +2726,8 @@ impl Build {
         }
 
         if target.contains("msvc") && tool.family == ToolFamily::Gnu {
-            println!("cargo:warning=GNU compiler is not supported for this target");
+            self.cargo_output
+                .print_warning(&"GNU compiler is not supported for this target");
         }
 
         Ok(tool)
@@ -2761,7 +2762,11 @@ impl Build {
         // interpretation at all, just pass it on through. This'll hopefully get
         // us to support spaces-in-paths.
         if Path::new(&*tool).exists() {
-            return Some((PathBuf::from(&*tool), None, Vec::new()));
+            return Some((
+                PathBuf::from(&*tool),
+                Self::rustc_wrapper_fallback(),
+                Vec::new(),
+            ));
         }
 
         // Ok now we want to handle a couple of scenarios. We'll assume from
@@ -2977,7 +2982,7 @@ impl Build {
                     let compiler = self.get_base_compiler().ok()?;
                     if compiler.family == ToolFamily::Clang {
                         name = format!("llvm-{}", tool);
-                        search_programs(&mut self.cmd(&compiler.path), &name)
+                        search_programs(&mut self.cmd(&compiler.path), &name, &self.cargo_output)
                             .map(|name| self.cmd(name))
                     } else {
                         None
@@ -3321,10 +3326,12 @@ impl Build {
             return val.clone();
         }
         if self.emit_rerun_if_env_changed && !provided_by_cargo(v) {
-            self.print(&format_args!("cargo:rerun-if-env-changed={}", v));
+            self.cargo_output
+                .print_metadata(&format_args!("cargo:rerun-if-env-changed={}", v));
         }
         let r = env::var(v).ok().map(Arc::from);
-        self.print(&format_args!("{} = {:?}", v, r));
+        self.cargo_output
+            .print_metadata(&format_args!("{} = {:?}", v, r));
         cache.insert(v.to_string(), r.clone());
         r
     }
@@ -3367,12 +3374,6 @@ impl Build {
             .collect())
     }
 
-    fn print(&self, s: &dyn Display) {
-        if self.cargo_metadata {
-            println!("{}", s);
-        }
-    }
-
     fn fix_env_for_apple_os(&self, cmd: &mut Command) -> Result<(), Error> {
         let target = self.get_target()?;
         let host = self.get_host()?;
@@ -3400,6 +3401,7 @@ impl Build {
                 .arg("--sdk")
                 .arg(sdk),
             "xcrun",
+            &self.cargo_output,
         )?;
 
         let sdk_path = match String::from_utf8(sdk_path) {
@@ -3474,10 +3476,10 @@ impl Build {
 
                     // If below 10.9, we round up.
                     if major == 10 && minor < 9 {
-                        println!(
-                            "cargo-warning: macOS deployment target ({}) too low, it will be increased",
+                        self.cargo_output.print_warning(&format_args!(
+                            "macOS deployment target ({}) too low, it will be increased",
                             deployment_target_ver
-                        );
+                        ));
                         return String::from("10.9");
                     }
                 }
@@ -3485,10 +3487,10 @@ impl Build {
                     let major = deployment_target.next().unwrap_or(0);
 
                     if major < 7 {
-                        println!(
-                            "cargo-warning: iOS deployment target ({}) too low, it will be increased",
+                        self.cargo_output.print_warning(&format_args!(
+                            "iOS deployment target ({}) too low, it will be increased",
                             deployment_target_ver
-                        );
+                        ));
                         return String::from(OLD_IOS_MINIMUM_VERSION);
                     }
                 }
@@ -3550,12 +3552,16 @@ impl Default for Build {
 }
 
 impl Tool {
-    fn new(path: PathBuf) -> Self {
-        Tool::with_features(path, None, false)
+    fn new(path: PathBuf, cargo_output: &CargoOutput) -> Self {
+        Tool::with_features(path, None, false, cargo_output)
     }
 
-    fn with_clang_driver(path: PathBuf, clang_driver: Option<&str>) -> Self {
-        Self::with_features(path, clang_driver, false)
+    fn with_clang_driver(
+        path: PathBuf,
+        clang_driver: Option<&str>,
+        cargo_output: &CargoOutput,
+    ) -> Self {
+        Self::with_features(path, clang_driver, false, cargo_output)
     }
 
     #[cfg(windows)]
@@ -3574,19 +3580,29 @@ impl Tool {
         }
     }
 
-    fn with_features(path: PathBuf, clang_driver: Option<&str>, cuda: bool) -> Self {
-        fn detect_family(path: &Path) -> ToolFamily {
+    fn with_features(
+        path: PathBuf,
+        clang_driver: Option<&str>,
+        cuda: bool,
+        cargo_output: &CargoOutput,
+    ) -> Self {
+        fn detect_family(path: &Path, cargo_output: &CargoOutput) -> ToolFamily {
             let mut cmd = Command::new(path);
             cmd.arg("--version");
 
-            let stdout = match run_output(&mut cmd, &path.to_string_lossy())
-                .ok()
-                .and_then(|o| String::from_utf8(o).ok())
+            let stdout = match run_output(
+                &mut cmd,
+                &path.to_string_lossy(),
+                // tool detection issues should always be shown as warnings
+                cargo_output,
+            )
+            .ok()
+            .and_then(|o| String::from_utf8(o).ok())
             {
                 Some(s) => s,
                 None => {
                     // --version failed. fallback to gnu
-                    println!("cargo-warning:Failed to run: {:?}", cmd);
+                    cargo_output.print_warning(&format_args!("Failed to run: {:?}", cmd));
                     return ToolFamily::Gnu;
                 }
             };
@@ -3596,10 +3612,10 @@ impl Tool {
                 ToolFamily::Gnu
             } else {
                 // --version doesn't include clang for GCC
-                println!(
-                    "cargo-warning:Compiler version doesn't include clang or GCC: {:?}",
+                cargo_output.print_warning(&format_args!(
+                    "Compiler version doesn't include clang or GCC: {:?}",
                     cmd
-                );
+                ));
                 ToolFamily::Gnu
             }
         }
@@ -3616,10 +3632,10 @@ impl Tool {
                     _ => ToolFamily::Clang,
                 }
             } else {
-                detect_family(&path)
+                detect_family(&path, cargo_output)
             }
         } else {
-            detect_family(&path)
+            detect_family(&path, cargo_output)
         };
 
         Tool {
@@ -3837,6 +3853,54 @@ fn wait_on_child(cmd: &Command, program: &str, child: &mut Child) -> Result<(),
     }
 }
 
+/// Find the destination object path for each file in the input source files,
+/// and store them in the output Object.
+fn objects_from_files(files: &[Arc<Path>], dst: &Path) -> Result<Vec<Object>, Error> {
+    let mut objects = Vec::with_capacity(files.len());
+    for file in files {
+        let basename = file
+            .file_name()
+            .ok_or_else(|| {
+                Error::new(
+                    ErrorKind::InvalidArgument,
+                    "No file_name for object file path!",
+                )
+            })?
+            .to_string_lossy();
+        let dirname = file
+            .parent()
+            .ok_or_else(|| {
+                Error::new(
+                    ErrorKind::InvalidArgument,
+                    "No parent for object file path!",
+                )
+            })?
+            .to_string_lossy();
+
+        // Hash the dirname. This should prevent conflicts if we have multiple
+        // object files with the same filename in different subfolders.
+        let mut hasher = hash_map::DefaultHasher::new();
+        hasher.write(dirname.to_string().as_bytes());
+        let obj = dst
+            .join(format!("{:016x}-{}", hasher.finish(), basename))
+            .with_extension("o");
+
+        match obj.parent() {
+            Some(s) => fs::create_dir_all(s)?,
+            None => {
+                return Err(Error::new(
+                    ErrorKind::InvalidArgument,
+                    "dst is an invalid path with no parent",
+                ));
+            }
+        };
+
+        objects.push(Object::new(file.to_path_buf(), obj));
+    }
+
+    Ok(objects)
+}
+
 #[cfg(feature = "parallel")]
 fn try_wait_on_child(
     cmd: &Command,
@@ -3871,22 +3935,31 @@ fn try_wait_on_child(
     }
 }
 
-fn run_inner(cmd: &mut Command, program: &str, pipe_writer: File) -> Result<(), Error> {
+fn run_inner(cmd: &mut Command, program: &str, pipe_writer: Option<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())?;
+fn run(cmd: &mut Command, program: &str, print: Option<&PrintThread>) -> Result<(), Error> {
+    let pipe_writer = print.map(PrintThread::clone_pipe_writer).transpose()?;
+    run_inner(cmd, program, pipe_writer)?;
 
     Ok(())
 }
 
-fn run_output(cmd: &mut Command, program: &str) -> Result<Vec<u8>, Error> {
+fn run_output(
+    cmd: &mut Command,
+    program: &str,
+    cargo_output: &CargoOutput,
+) -> Result<Vec<u8>, Error> {
     cmd.stdout(Stdio::piped());
 
-    let mut print = PrintThread::new()?;
-    let mut child = spawn(cmd, program, print.pipe_writer().take().unwrap())?;
+    let mut print = cargo_output.print_thread()?;
+    let mut child = spawn(
+        cmd,
+        program,
+        print.as_mut().map(PrintThread::take_pipe_writer),
+    )?;
 
     let mut stdout = vec![];
     child
@@ -3901,7 +3974,7 @@ fn run_output(cmd: &mut Command, program: &str) -> Result<Vec<u8>, Error> {
     Ok(stdout)
 }
 
-fn spawn(cmd: &mut Command, program: &str, pipe_writer: File) -> Result<Child, Error> {
+fn spawn(cmd: &mut Command, program: &str, pipe_writer: Option<File>) -> Result<Child, Error> {
     struct ResetStderr<'cmd>(&'cmd mut Command);
 
     impl Drop for ResetStderr<'_> {
@@ -3915,8 +3988,11 @@ fn spawn(cmd: &mut Command, program: &str, pipe_writer: File) -> Result<Child, E
     println!("running: {:?}", cmd);
 
     let cmd = ResetStderr(cmd);
-
-    match cmd.0.stderr(pipe_writer).spawn() {
+    let child = cmd
+        .0
+        .stderr(pipe_writer.map_or_else(Stdio::null, Stdio::from))
+        .spawn();
+    match child {
         Ok(child) => Ok(child),
         Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
             let extra = if cfg!(windows) {
@@ -4125,8 +4201,14 @@ fn which(tool: &Path, path_entries: Option<OsString>) -> Option<PathBuf> {
 }
 
 // search for |prog| on 'programs' path in '|cc| -print-search-dirs' output
-fn search_programs(cc: &mut Command, prog: &str) -> Option<PathBuf> {
-    let search_dirs = run_output(cc.arg("-print-search-dirs"), "cc").ok()?;
+fn search_programs(cc: &mut Command, prog: &str, cargo_output: &CargoOutput) -> Option<PathBuf> {
+    let search_dirs = run_output(
+        cc.arg("-print-search-dirs"),
+        "cc",
+        // this doesn't concern the compilation so we always want to show warnings.
+        cargo_output,
+    )
+    .ok()?;
     // clang driver appears to be forcing UTF-8 output even on Windows,
     // hence from_utf8 is assumed to be usable in all cases.
     let search_dirs = std::str::from_utf8(&search_dirs).ok()?;
@@ -4163,6 +4245,37 @@ impl AsmFileExt {
     }
 }
 
+#[derive(Clone, Debug)]
+struct CargoOutput {
+    metadata: bool,
+    warnings: bool,
+}
+
+impl CargoOutput {
+    const fn new() -> Self {
+        Self {
+            metadata: true,
+            warnings: true,
+        }
+    }
+
+    fn print_metadata(&self, s: &dyn Display) {
+        if self.metadata {
+            println!("{}", s);
+        }
+    }
+
+    fn print_warning(&self, arg: &dyn Display) {
+        if self.warnings {
+            println!("cargo:warning={}", arg);
+        }
+    }
+
+    fn print_thread(&self) -> Result<Option<PrintThread>, Error> {
+        self.warnings.then(PrintThread::new).transpose()
+    }
+}
+
 struct PrintThread {
     handle: Option<JoinHandle<()>>,
     pipe_writer: Option<File>,
@@ -4202,11 +4315,21 @@ impl PrintThread {
         })
     }
 
-    fn pipe_writer(&mut self) -> &mut Option<File> {
-        &mut self.pipe_writer
+    /// # Panics
+    ///
+    /// Will panic if the pipe writer has already been taken.
+    fn take_pipe_writer(&mut self) -> File {
+        self.pipe_writer.take().unwrap()
+    }
+
+    /// # Panics
+    ///
+    /// Will panic if the pipe writer has already been taken.
+    fn clone_pipe_writer(&self) -> Result<File, Error> {
+        self.try_clone_pipe_writer().map(Option::unwrap)
     }
 
-    fn pipe_writer_cloned(&self) -> Result<Option<File>, Error> {
+    fn try_clone_pipe_writer(&self) -> Result<Option<File>, Error> {
         self.pipe_writer
             .as_ref()
             .map(File::try_clone)


Test patch:
diff --git a/cc-test/build.rs b/cc-test/build.rs
index 4097fbb..8968eb5 100644
--- a/cc-test/build.rs
+++ b/cc-test/build.rs
@@ -1,12 +1,35 @@
 use std::env;
 use std::fs;
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
+use std::process::Command;
 
 fn main() {
+    // if we are being executed from a `fork_run_action` call (i.e. this is a
+    // "fork"), perform the requested action and then return.
+    if run_action_if_forked() {
+        return;
+    }
+
     let out = PathBuf::from(env::var_os("OUT_DIR").unwrap());
     fs::remove_dir_all(&out).unwrap();
     fs::create_dir(&out).unwrap();
 
+    // The following are builds where we want to capture the output (i.e. stdout and
+    // stderr). We do that by re-running _this_ executable and passing in the
+    // action as the first argument.
+    run_forked_capture_output(&out, "metadata-on");
+    run_forked_capture_output(&out, "metadata-off");
+
+    run_forked_capture_output(&out, "warnings-off");
+    if cc::Build::new().get_compiler().is_like_msvc() {
+        // MSVC doesn't output warnings to stderr, so we can't capture them.
+        // the test will use this env var to know whether to run the test.
+        println!("cargo:rustc-env=TEST_WARNINGS_ON=0");
+    } else {
+        println!("cargo:rustc-env=TEST_WARNINGS_ON=1");
+        run_forked_capture_output(&out, "warnings-on");
+    }
+
     cc::Build::new()
         .file("src/foo.c")
         .flag_if_supported("-Wall")
@@ -104,3 +127,48 @@ fn main() {
     let out = String::from_utf8(out).unwrap();
     assert!(out.contains("hello world"));
 }
+
+#[track_caller]
+fn run_forked_capture_output(out: &Path, action: &str) {
+    let program = env::current_exe().unwrap();
+    let output = Command::new(&program).arg(action).output().unwrap();
+    assert!(output.status.success(), "output: {:#?}", output);
+    // we've captured the output and now we write it to a dedicated directory in the
+    // build output so our tests can access the output.
+    let action_dir = out.join(action);
+    fs::create_dir_all(&action_dir).unwrap();
+    fs::write(action_dir.join("stdout"), output.stdout).unwrap();
+    fs::write(action_dir.join("stderr"), output.stderr).unwrap();
+}
+
+fn run_action_if_forked() -> bool {
+    let mut args = env::args();
+    let _program = args.next().unwrap();
+    let action = args.next();
+    match action.as_deref() {
+        Some("metadata-on") => build_cargo_metadata(true),
+        Some("metadata-off") => build_cargo_metadata(false),
+        Some("warnings-on") => build_cargo_warnings(true),
+        Some("warnings-off") => build_cargo_warnings(false),
+        // No action requested, we're being called from cargo. Proceed with build.
+        _ => return false,
+    }
+    true
+}
+
+fn build_cargo_warnings(warnings: bool) {
+    cc::Build::new()
+        .cargo_metadata(false)
+        .cargo_warnings(warnings)
+        .file("src/compile_error.c")
+        .try_compile("compile_error")
+        .unwrap_err();
+}
+
+fn build_cargo_metadata(metadata: bool) {
+    cc::Build::new()
+        .cargo_metadata(metadata)
+        .file("src/dummy.c")
+        .try_compile("dummy")
+        .unwrap();
+}
diff --git a/cc-test/src/compile_error.c b/cc-test/src/compile_error.c
new file mode 100644
index 0000000..535ae34
--- /dev/null
+++ b/cc-test/src/compile_error.c
@@ -0,0 +1,1 @@
+#error "if you see this, cargo_warnings(false) didn't do its job"
diff --git a/cc-test/src/dummy.c b/cc-test/src/dummy.c
new file mode 100644
index 0000000..0edb14b
--- /dev/null
+++ b/cc-test/src/dummy.c
@@ -0,0 +1,3 @@
+/* just an empty file */
+
+void dummy(void) {}
diff --git a/cc-test/tests/output.rs b/cc-test/tests/output.rs
new file mode 100644
index 0000000..9bfe61d
--- /dev/null
+++ b/cc-test/tests/output.rs
@@ -0,0 +1,68 @@
+use std::fs;
+use std::path::PathBuf;
+
+#[test]
+fn cargo_warnings_on() {
+    if env!("TEST_WARNINGS_ON") == "0" {
+        // in some cases we don't catch compiler warnings and turn them into cargo
+        // instructions.
+        return;
+    }
+    let (stdout, stderr) = load_output("warnings-on");
+    assert!(stderr.is_empty());
+    assert!(stdout.contains("cargo:warning="));
+}
+
+#[test]
+fn cargo_warnings_off() {
+    let (stdout, stderr) = load_output("warnings-off");
+    assert!(stderr.is_empty());
+    assert!(!stdout.contains("cargo:warning="));
+}
+
+#[test]
+fn cargo_metadata_on() {
+    let (stdout, stderr) = load_output("metadata-on");
+    assert!(stderr.is_empty());
+    assert!(stdout.contains("cargo:rustc-link-lib="));
+    assert!(stdout.contains("cargo:rustc-link-search="));
+}
+
+#[test]
+fn cargo_metadata_off() {
+    let (stdout, stderr) = load_output("metadata-off");
+    assert!(stderr.is_empty());
+
+    // most of the instructions aren't currently used
+    const INSTRUCTIONS: &[&str] = &[
+        "cargo:rerun-if-changed=",
+        "cargo:rerun-if-env-changed=",
+        "cargo:rustc-cdylib-link-arg=",
+        "cargo:rustc-cfg=",
+        "cargo:rustc-env=",
+        "cargo:rustc-flags=",
+        "cargo:rustc-link-arg-benches=",
+        "cargo:rustc-link-arg-bin=",
+        "cargo:rustc-link-arg-bins=",
+        "cargo:rustc-link-arg-examples=",
+        "cargo:rustc-link-arg-tests=",
+        "cargo:rustc-link-arg=",
+        "cargo:rustc-link-lib=",
+        "cargo:rustc-link-search=",
+    ];
+    for instr in INSTRUCTIONS {
+        assert!(!stdout.contains(instr), "instruction present: {}", instr);
+    }
+}
+
+#[track_caller]
+fn load_output(action: &str) -> (String, String) {
+    // these files are written by the `run_forked_capture_output` function in the
+    // build script.
+    let action_dir = PathBuf::from(env!("OUT_DIR")).join(action);
+    let stdout = fs::read_to_string(action_dir.join("stdout")).unwrap();
+    let stderr = fs::read_to_string(action_dir.join("stderr")).unwrap();
+    println!("compile stdout: {:?}", stdout);
+    println!("compile stderr: {:?}", stderr);
+    (stdout, stderr)
+}
diff --git a/tests/test.rs b/tests/test.rs
index 36cdd84..1983fd3 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -24,7 +24,8 @@ fn gnu_smoke() {
         .must_have("-c")
         .must_have("-ffunction-sections")
         .must_have("-fdata-sections");
-    test.cmd(1).must_have(test.td.path().join("foo.o"));
+    test.cmd(1)
+        .must_have(test.td.path().join("d1fba762150c532c-foo.o"));
 }
 
 #[test]
@@ -399,7 +400,8 @@ fn msvc_smoke() {
         .must_not_have("-Z7")
         .must_have("-c")
         .must_have("-MD");
-    test.cmd(1).must_have(test.td.path().join("foo.o"));
+    test.cmd(1)
+        .must_have(test.td.path().join("d1fba762150c532c-foo.o"));
 }
 
 #[test]
@@ -576,3 +578,21 @@ fn clang_apple_tvsimulator() {
         test.cmd(0).must_have("-mappletvsimulator-version-min=9.0");
     }
 }
+
+#[test]
+fn compile_intermediates() {
+    let test = Test::gnu();
+    let intermediates = test
+        .gcc()
+        .file("foo.c")
+        .file("x86_64.asm")
+        .file("x86_64.S")
+        .asm_flag("--abc")
+        .compile_intermediates();
+
+    assert_eq!(intermediates.len(), 3);
+
+    assert!(intermediates[0].display().to_string().contains("foo"));
+    assert!(intermediates[1].display().to_string().contains("x86_64"));
+    assert!(intermediates[2].display().to_string().contains("x86_64"));
+}