diff --git a/Cargo.lock b/Cargo.lock index e1a4cacd9..26f288f5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -535,6 +535,70 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + [[package]] name = "encoding_rs" version = "0.8.31" @@ -2138,6 +2202,7 @@ dependencies = [ "crossbeam-utils", "daemonize", "directories", + "encoding", "env_logger", "filetime", "flate2", diff --git a/Cargo.toml b/Cargo.toml index 2b67346b0..e0d5c9478 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ opendal = { version= "0.27.1", optional=true } reqsign = {version="0.8.3", optional=true} clap = { version = "4.0.32", features = ["derive", "env", "wrap_help"] } directories = "4.0.1" +encoding = "0.2" env_logger = "0.10" filetime = "0.2" flate2 = { version = "1.0", optional = true, default-features = false, features = ["rust_backend"] } diff --git a/README.md b/README.md index d3a6bc345..ae384fd83 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ export RUSTC_WRAPPER=/path/to/sccache cargo build ``` -sccache supports gcc, clang, MSVC, rustc, NVCC, and [Wind River's diab compiler](https://www.windriver.com/products/development-tools/#diab_compiler). +sccache supports gcc, clang, MSVC, rustc, NVCC, and [Wind River's diab compiler](https://www.windriver.com/products/development-tools/#diab_compiler). Both gcc and msvc support Response Files, read more about their implementation [here](docs/ResponseFiles.md). If you don't [specify otherwise](#storage-options), sccache will use a local disk cache. diff --git a/docs/ResponseFiles.md b/docs/ResponseFiles.md new file mode 100644 index 000000000..9c0ccd027 --- /dev/null +++ b/docs/ResponseFiles.md @@ -0,0 +1,36 @@ +# Response Files + +Response files are a way for compilers to accept arguments that would otherwise overflow the character limit in the command line. [On Windows in particular](https://learn.microsoft.com/en-us/troubleshoot/windows-client/shell-experience/command-line-string-limitation), the character limit per command is 8191 characters. These files can contain additional options that the compiler will read and process as if they were provided in the original command. Each compiler that supports response files has different formats/expectations and implementations. Support for response files are also re-implemented per compiler by sccache so it can cache compilations accurately. There is currently support for response files on the gcc and msvc implementations in sccache. + +## GCC + +As defined by the [gcc docs](https://gcc.gnu.org/onlinedocs/gcc-4.6.3/gcc/Overall-Options.html#Overall-Options): + +1. Options in a response file are inserted in-place in the original command line. If the file does not exist or cannot be read, the option will be treated literally, and not removed. +2. Options in a response file are separated by whitespace. +3. Single or double quotes can be used to include whitespace in an option. +4. Any character (including a backslash) may be included by prefixing the character to be included with a backslash (e.g. `\\`, `\?`, `\@`, etc). +5. The response file may itself contain additional @file options; any such options will be processed recursively. + +Implementation details: +- The gcc implementation in sccache supports all of these **except** #3. If a response file contains **any** quotations (`"` or `'`), the @file arg is treated literally and not removed (and its content not processed). +- Additionally, sccache will not expand concatenated arguments such as `-include@foo` (see [#150](https://github.com/mozilla/sccache/issues/150#issuecomment-318586953) for more on this). +- Recursive files are processed depth-first; when an @file option is encountered, its contents are read and each option is evaluated in-place before continuing to options following the @file. + +## MSVC + +Per the [MSVC docs](https://learn.microsoft.com/en-us/cpp/build/reference/cl-command-files?view=msvc-170): + +1. The contents of a response file are inserted in-place in the original command. +2. Response files can contain multiple lines of options, but each option must begin and end on the same line. +3. Backslashes (`\`) cannot be used to combine options across multiple lines. +4. The `/link` directive has special treatment: + 1. Entering an @file: if the `/link` option is provided prior to an `@file` in the command line, the `/link` directive does not affect any options within the `@file`. + 2. Newlines: A `/link` directive provided in an `@file` on one line does not affect the next line. + 3. Exitting an @file: A `/link` directive on the final line of a response file does not affect options following the `@file` option in the command line. +5. A response file cannot contain additional `@file` options, they are not recursive. (found in a [separate doc](https://learn.microsoft.com/en-us/cpp/build/reference/at-specify-a-compiler-response-file?view=msvc-170)) +6. (implied) options can be wrapped in double-quotes (`"`), which allows whitespace to be preserved within the option + +The msvc implementaion in sccache supports all of these **except** #4, because sccache doesn't accept the `/link` directive. + +Additionally, because `msbuild` generates response files using an encoding other than `utf-8`, all text files under the [WHATWG encoding standard](https://encoding.spec.whatwg.org/) are supported. This includes both `utf-8` and `utf-16`. diff --git a/src/compiler/msvc.rs b/src/compiler/msvc.rs index c5b84f9db..4e9597c5f 100644 --- a/src/compiler/msvc.rs +++ b/src/compiler/msvc.rs @@ -20,14 +20,14 @@ use crate::compiler::{ clang, gcc, write_temp_file, Cacheable, ColorMode, CompileCommand, CompilerArguments, }; use crate::mock_command::{CommandCreatorSync, RunCommand}; -use crate::util::run_input_output; +use crate::util::{run_input_output, OsStrExt}; use crate::{counted_array, dist}; use fs::File; use fs_err as fs; use log::Level::Debug; use std::collections::{HashMap, HashSet}; use std::ffi::{OsStr, OsString}; -use std::io::{self, BufWriter, Write}; +use std::io::{self, BufWriter, Read, Write}; use std::path::{Path, PathBuf}; use std::process::{self, Stdio}; @@ -566,7 +566,11 @@ pub fn parse_arguments( let mut multiple_input = false; let mut multiple_input_files = Vec::new(); - for arg in ArgsIter::new(arguments.iter().cloned(), (&ARGS[..], &SLASH_ARGS[..])) { + // Custom iterator to expand `@` arguments which stand for reading a file + // and interpreting it as a list of more arguments. + let it = ExpandIncludeFile::new(cwd, arguments); + let it = ArgsIter::new(it, (&ARGS[..], &SLASH_ARGS[..])); + for arg in it { let arg = try_or_cannot_cache!(arg, "argument parse"); match arg.get_data() { Some(PassThrough) | Some(PassThroughWithPath(_)) | Some(PassThroughWithSuffix(_)) => {} @@ -1064,6 +1068,251 @@ fn generate_compile_commands( Ok((command, dist_command, cacheable)) } +/// Iterator that expands @response files in-place. +/// +/// According to MSDN [1], @file means: +/// +/// ```text +/// A text file containing compiler commands. +/// +/// A response file can contain any commands that you would specify on the +/// command line. This can be useful if your command-line arguments exceed +/// 127 characters. +/// +/// It is not possible to specify the @ option from within a response file. +/// That is, a response file cannot embed another response file. +/// +/// From the command line you can specify as many response file options (for +/// example, @respfile.1 @respfile.2) as you want. +/// ``` +/// +/// Per Microsoft [2], response files are used by MSBuild: +/// +/// ```text +/// Response (.rsp) files are text files that contain MSBuild.exe +/// command-line switches. Each switch can be on a separate line or all +/// switches can be on one line. Comment lines are prefaced with a # symbol. +/// The @ switch is used to pass another response file to MSBuild.exe. +/// +/// The autoresponse file is a special .rsp file that MSBuild.exe automatically +/// uses when building a project. This file, MSBuild.rsp, must be in the same +/// directory as MSBuild.exe, otherwise it will not be found. You can edit +/// this file to specify default command-line switches to MSBuild.exe. +/// For example, if you use the same logger every time you build a project, +/// you can add the -logger switch to MSBuild.rsp, and MSBuild.exe will +/// use the logger every time a project is built. +/// ``` +/// +/// Note that, in order to conform to the spec, response files are not +/// recursively expanded. +/// +/// [1]: https://docs.microsoft.com/en-us/cpp/build/reference/at-specify-a-compiler-response-file +/// [2]: https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-response-files?view=vs-2019 +struct ExpandIncludeFile<'a> { + cwd: &'a Path, + /// Arguments provided during initialization, which may include response-file directives (@). + /// Order is reversed from the iterator provided, + /// so they can be visited in front-to-back order by popping from the end. + args: Vec, + /// Arguments found in provided response-files. + /// These are also reversed compared to the order in the response file, + /// so they can be visited in front-to-back order by popping from the end. + stack: Vec, +} + +impl<'a> ExpandIncludeFile<'a> { + pub fn new(cwd: &'a Path, args: &[OsString]) -> Self { + ExpandIncludeFile { + // Reverse the provided iterator so we can pop from end to visit in the original order. + args: args.iter().rev().map(|a| a.to_owned()).collect(), + stack: Vec::new(), + cwd, + } + } +} + +impl<'a> Iterator for ExpandIncludeFile<'a> { + type Item = OsString; + + fn next(&mut self) -> Option { + loop { + // Visit all arguments found in the most recently read response file. + // Since response files are not recursive, we do not need to worry + // about these containing addditional @ directives. + if let Some(response_file_arg) = self.stack.pop() { + return Some(response_file_arg); + } + + // Visit the next argument provided by the original command iterator. + let arg = match self.args.pop() { + Some(arg) => arg, + None => return None, + }; + let file_arg = match arg.split_prefix("@") { + Some(file_arg) => file_arg, + None => return Some(arg), + }; + let file_path = self.cwd.join(file_arg); + // Read the contents of the response file, accounting for non-utf8 encodings. + let content = match File::open(&file_path).and_then(|mut file| read_text(&mut file)) { + Ok(content) => content, + Err(err) => { + debug!("failed to read @-file `{}`: {}", file_path.display(), err); + // If we failed to read the file content, return the orginal arg (including the `@` directive). + return Some(arg); + } + }; + + // Parse the response file contents, taking into account quote-wrapped strings and new-line separators. + // Special implementation to account for MSVC response file format. + let resp_file_args = SplitMsvcResponseFileArgs::from(&content).collect::>(); + // Pump arguments back to the stack, in reverse order so we can `Vec::pop` and visit in original front-to-back order. + let rev_args = resp_file_args.iter().rev().map(|s| s.into()); + self.stack.extend(rev_args); + } + } +} + +/// Reads the text stream as a unicode buffer, prioritizing UTF-8, UTF-16 (big and little endian), and falling back on ISO 8859-1. +fn read_text(reader: &mut R) -> io::Result +where + R: Read, +{ + let mut buf = Vec::new(); + reader.read_to_end(&mut buf)?; + + let (result, _) = encoding::decode( + &buf, + encoding::DecoderTrap::Strict, + encoding::all::ISO_8859_1, + ); + + result.map_err(|err| io::Error::new(io::ErrorKind::Other, err.into_owned())) +} + +/// An iterator over the arguments in a Windows command line. +/// +/// This produces results identical to `CommandLineToArgvW` except in the +/// following cases: +/// +/// 1. When passed an empty string, CommandLineToArgvW returns the path to the +/// current executable file. Here, the iterator will simply be empty. +/// 2. CommandLineToArgvW interprets the first argument differently than the +/// rest. Here, all arguments are treated in identical fashion. +/// +/// Parsing rules: +/// +/// - Arguments are delimited by whitespace (either a space or tab). +/// - A string surrounded by double quotes is interpreted as a single argument. +/// - Backslashes are interpreted literally unless followed by a double quote. +/// - 2n backslashes followed by a double quote reduce to n backslashes and we +/// enter the "in quote" state. +/// - 2n+1 backslashes followed by a double quote reduces to n backslashes, +/// we do *not* enter the "in quote" state, and the double quote is +/// interpreted literally. +/// +/// References: +/// - https://msdn.microsoft.com/en-us/library/windows/desktop/bb776391(v=vs.85).aspx +/// - https://msdn.microsoft.com/en-us/library/windows/desktop/17w5ykft(v=vs.85).aspx +#[derive(Clone, Debug)] +struct SplitMsvcResponseFileArgs<'a> { + /// String slice of the file content that is being parsed. + /// Slice is mutated as this iterator is executed. + file_content: &'a str, +} + +impl<'a, T> From<&'a T> for SplitMsvcResponseFileArgs<'a> +where + T: AsRef + 'static, +{ + fn from(file_content: &'a T) -> Self { + Self { + file_content: file_content.as_ref(), + } + } +} + +impl<'a> SplitMsvcResponseFileArgs<'a> { + /// Appends backslashes to `target` by decrementing `count`. + /// If `step` is >1, then `count` is decremented by `step`, resulting in 1 backslash appended for every `step`. + fn append_backslashes_to(target: &mut String, count: &mut usize, step: usize) { + while *count >= step { + target.push('\\'); + *count -= step; + } + } +} + +impl<'a> Iterator for SplitMsvcResponseFileArgs<'a> { + type Item = String; + + fn next(&mut self) -> Option { + let mut in_quotes = false; + let mut backslash_count: usize = 0; + + // Strip any leading whitespace before relevant characters + let is_whitespace = |c| matches!(c, ' ' | '\t' | '\n'); + self.file_content = self.file_content.trim_start_matches(is_whitespace); + + if self.file_content.is_empty() { + return None; + } + + // The argument string to return, built by analyzing the current slice in the iterator. + let mut arg = String::new(); + // All characters still in the string slice. Will be mutated by consuming + // values until the current arg is built. + let mut chars = self.file_content.chars(); + // Build the argument by evaluating each character in the string slice. + for c in &mut chars { + match c { + // In order to handle the escape character based on the char(s) which come after it, + // they are counted instead of appended literally, until a non-backslash character is encountered. + '\\' => backslash_count += 1, + // Either starting or ending a quoted argument, or appending a literal character (if the quote was escaped). + '"' => { + // Only append half the number of backslashes encountered, because this is an escaped string. + // This will reduce `backslash_count` to either 0 or 1. + Self::append_backslashes_to(&mut arg, &mut backslash_count, 2); + match backslash_count == 0 { + // If there are no remaining encountered backslashes, + // then we have found either the start or end of a quoted argument. + true => in_quotes = !in_quotes, + // The quote character is escaped, so it is treated as a literal and appended to the arg string. + false => { + backslash_count = 0; + arg.push('"'); + } + } + } + // If whitespace is encountered, only preserve it if we are currently in quotes. + // Otherwise it marks the end of the current argument. + ' ' | '\t' | '\n' => { + Self::append_backslashes_to(&mut arg, &mut backslash_count, 1); + // If not in a quoted string, then this is the end of the argument. + if !in_quotes { + break; + } + // Otherwise, the whitespace must be preserved in the argument. + arg.push(c); + } + // All other characters treated as is + _ => { + Self::append_backslashes_to(&mut arg, &mut backslash_count, 1); + arg.push(c); + } + } + } + + // Flush any backslashes at the end of the string. + Self::append_backslashes_to(&mut arg, &mut backslash_count, 1); + // Save the current remaining characters for the next step in the iterator. + self.file_content = chars.as_str(); + + Some(arg) + } +} + #[cfg(test)] mod test { use super::*; @@ -1584,13 +1833,231 @@ mod test { } #[test] - fn test_parse_arguments_response_file() { + fn test_responsefile_missing() { assert_eq!( CompilerArguments::CannotCache("@", None), parse_arguments(ovec!["-c", "foo.c", "@foo", "-Fofoo.obj"]) ); } + #[test] + fn test_responsefile_absolute_path() { + let td = tempfile::Builder::new() + .prefix("sccache") + .tempdir() + .unwrap(); + let cmd_file_path = td.path().join("foo"); + { + let mut file = File::create(&cmd_file_path).unwrap(); + let content = b"-c foo.c -o foo.o"; + file.write_all(content).unwrap(); + } + let arg = format!("@{}", cmd_file_path.display()); + let ParsedArguments { + input, + language, + outputs, + preprocessor_args, + msvc_show_includes, + common_args, + .. + } = match parse_arguments(ovec![arg]) { + CompilerArguments::Ok(args) => args, + o => panic!("Failed to parse @-file, err: {:?}", o), + }; + assert_eq!(Some("foo.c"), input.to_str()); + assert_eq!(Language::C, language); + assert_map_contains!( + outputs, + ( + "obj", + ArtifactDescriptor { + path: "foo.o".into(), + optional: false + } + ) + ); + assert!(preprocessor_args.is_empty()); + assert!(common_args.is_empty()); + assert!(!msvc_show_includes); + } + + #[test] + fn test_responsefile_relative_path() { + // Generate the tempdir in the currentdir so we can use a relative path in this test. + // MSVC allows relative paths to response files, so we must support that. + let td = tempfile::Builder::new() + .prefix("sccache") + .tempdir_in("./") + .unwrap(); + let relative_to_tmp = td + .path() + .strip_prefix(std::env::current_dir().unwrap()) + .unwrap(); + let cmd_file_path = relative_to_tmp.join("foo"); + { + let mut file = File::create(&cmd_file_path).unwrap(); + let content = b"-c foo.c -o foo.o"; + file.write_all(content).unwrap(); + } + let arg = format!("@{}", cmd_file_path.display()); + let ParsedArguments { + input, + language, + outputs, + preprocessor_args, + msvc_show_includes, + common_args, + .. + } = match parse_arguments(ovec![arg]) { + CompilerArguments::Ok(args) => args, + o => panic!("Failed to parse @-file, err: {:?}", o), + }; + assert_eq!(Some("foo.c"), input.to_str()); + assert_eq!(Language::C, language); + assert_map_contains!( + outputs, + ( + "obj", + ArtifactDescriptor { + path: "foo.o".into(), + optional: false + } + ) + ); + assert!(preprocessor_args.is_empty()); + assert!(common_args.is_empty()); + assert!(!msvc_show_includes); + } + + #[test] + fn test_responsefile_with_quotes() { + let td = tempfile::Builder::new() + .prefix("sccache") + .tempdir() + .unwrap(); + let cmd_file_path = td.path().join("foo"); + { + let mut file = File::create(&cmd_file_path).unwrap(); + let content = b"-c \"Foo Bar.c\" -o foo.o"; + file.write_all(content).unwrap(); + } + let arg = format!("@{}", cmd_file_path.display()); + let ParsedArguments { + input, + language, + outputs, + preprocessor_args, + msvc_show_includes, + common_args, + .. + } = match parse_arguments(ovec![arg]) { + CompilerArguments::Ok(args) => args, + o => panic!("Failed to parse @-file, err: {:?}", o), + }; + assert_eq!(Some("Foo Bar.c"), input.to_str()); + assert_eq!(Language::C, language); + assert_map_contains!( + outputs, + ( + "obj", + ArtifactDescriptor { + path: "foo.o".into(), + optional: false + } + ) + ); + assert!(preprocessor_args.is_empty()); + assert!(common_args.is_empty()); + assert!(!msvc_show_includes); + } + + #[test] + fn test_responsefile_multiline() { + let td = tempfile::Builder::new() + .prefix("sccache") + .tempdir() + .unwrap(); + let cmd_file_path = td.path().join("foo"); + { + let mut file = File::create(&cmd_file_path).unwrap(); + let content = b"\n-c foo.c\n-o foo.o"; + file.write_all(content).unwrap(); + } + let arg = format!("@{}", cmd_file_path.display()); + let ParsedArguments { + input, + language, + outputs, + preprocessor_args, + msvc_show_includes, + common_args, + .. + } = match parse_arguments(ovec![arg]) { + CompilerArguments::Ok(args) => args, + o => panic!("Failed to parse @-file, err: {:?}", o), + }; + assert_eq!(Some("foo.c"), input.to_str()); + assert_eq!(Language::C, language); + assert_map_contains!( + outputs, + ( + "obj", + ArtifactDescriptor { + path: "foo.o".into(), + optional: false + } + ) + ); + assert!(preprocessor_args.is_empty()); + assert!(common_args.is_empty()); + assert!(!msvc_show_includes); + } + + #[test] + fn test_responsefile_encoding_utf16le() { + let td = tempfile::Builder::new() + .prefix("sccache") + .tempdir() + .unwrap(); + let cmd_file_path = td.path().join("foo"); + { + use encoding::{all::UTF_16LE, EncoderTrap::Strict, Encoding}; + let mut file = File::create(&cmd_file_path).unwrap(); + let content = UTF_16LE.encode("-c foo€.c -o foo.o", Strict).unwrap(); + file.write_all(&[0xFF, 0xFE]).unwrap(); // little endian BOM + file.write_all(&content).unwrap(); + } + let arg = format!("@{}", cmd_file_path.display()); + let ParsedArguments { + input, + language, + outputs, + preprocessor_args, + msvc_show_includes, + common_args, + .. + } = match parse_arguments(ovec![arg]) { + CompilerArguments::Ok(args) => args, + o => panic!("Failed to parse @-file, err: {:?}", o), + }; + assert_eq!(Some("foo€.c"), input.to_str()); + assert_eq!(Language::C, language); + assert_map_contains!( + outputs, + ( + "obj", + ArtifactDescriptor { + path: "foo.o".into(), + optional: false + } + ) + ); + assert!(preprocessor_args.is_empty()); + assert!(common_args.is_empty()); + assert!(!msvc_show_includes); + } + #[test] fn test_parse_arguments_missing_pdb() { assert_eq!( diff --git a/tests/system.rs b/tests/system.rs index 977977c2f..8ddbb0063 100644 --- a/tests/system.rs +++ b/tests/system.rs @@ -209,6 +209,33 @@ fn test_msvc_deps(compiler: Compiler, tempdir: &Path) { assert_eq!(lines, expected_lines); } +fn test_msvc_responsefile(compiler: Compiler, tempdir: &Path) { + let Compiler { + name: _, + exe, + env_vars, + } = compiler; + + let out_file = tempdir.join(OUTPUT); + let cmd_file_name = "test_msvc.rsp"; + { + let mut file = File::create(&tempdir.join(cmd_file_name)).unwrap(); + let content = format!("-c {INPUT} -Fo{OUTPUT}"); + file.write_all(content.as_bytes()).unwrap(); + } + + let args = vec_from!(OsString, exe, &format!("@{cmd_file_name}")); + sccache_command() + .args(&args) + .current_dir(tempdir) + .envs(env_vars) + .assert() + .success(); + + assert!(fs::metadata(&out_file).map(|m| m.len() > 0).unwrap()); + fs::remove_file(&out_file).unwrap(); +} + fn test_gcc_mp_werror(compiler: Compiler, tempdir: &Path) { let Compiler { name, @@ -375,6 +402,7 @@ fn run_sccache_command_tests(compiler: Compiler, tempdir: &Path) { test_compile_with_define(compiler.clone(), tempdir); if compiler.name == "cl.exe" { test_msvc_deps(compiler.clone(), tempdir); + test_msvc_responsefile(compiler.clone(), tempdir); } if compiler.name == "gcc" { test_gcc_mp_werror(compiler.clone(), tempdir);