diff --git a/src/cache/cache.rs b/src/cache/cache.rs index 3e883db19..7e958937e 100644 --- a/src/cache/cache.rs +++ b/src/cache/cache.rs @@ -50,6 +50,7 @@ use fs_err as fs; use serde::{Deserialize, Serialize}; use std::fmt; +use std::fs::File; use std::io::{self, Cursor, Read, Seek, Write}; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -210,6 +211,15 @@ impl CacheRead { optional, } in objects { + if path == Path::new("/dev/null") { + debug!("Skipping output to /dev/null"); + continue; + } + #[cfg(windows)] + if path == Path::new("NUL") { + debug!("Skipping output to NUL"); + continue; + } let dir = match path.parent() { Some(d) => d, None => bail!("Output file without a parent directory!"), @@ -217,15 +227,35 @@ impl CacheRead { // Write the cache entry to a tempfile and then atomically // move it to its final location so that other rustc invocations // happening in parallel don't see a partially-written file. - let mut tmp = NamedTempFile::new_in(dir)?; - match (self.get_object(&key, &mut tmp), optional) { - (Ok(mode), _) => { - tmp.persist(&path)?; + match (NamedTempFile::new_in(dir), optional) { + (Ok(mut tmp), _) => { + match (self.get_object(&key, &mut tmp), optional) { + (Ok(mode), _) => { + tmp.persist(&path)?; + if let Some(mode) = mode { + set_file_mode(&path, mode)?; + } + } + (Err(e), false) => return Err(e), + // skip if no object found and it's optional + (Err(_), true) => continue, + } + } + (Err(e), false) => { + // Fall back to writing directly to the final location + warn!("Failed to create temp file on the same file system: {e}"); + let mut f = File::create(&path)?; + // `optional` is false in this branch, so do not ignore errors + let mode = self.get_object(&key, &mut f)?; if let Some(mode) = mode { - set_file_mode(&path, mode)?; + if let Err(e) = set_file_mode(&path, mode) { + // Here we ignore errors from setting file mode because + // if we could not create a temp file in the same directory, + // we probably can't set the mode either (e.g. /dev/stuff) + warn!("Failed to reset file mode: {e}"); + } } } - (Err(e), false) => return Err(e), // skip if no object found and it's optional (Err(_), true) => continue, } @@ -838,4 +868,65 @@ mod test { }); } } + + #[test] + fn test_extract_object_to_devnull_works() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .worker_threads(1) + .build() + .unwrap(); + + let pool = runtime.handle(); + + let cache_data = CacheWrite::new(); + let cache_read = CacheRead::from(io::Cursor::new(cache_data.finish().unwrap())).unwrap(); + + let objects = vec![FileObjectSource { + key: "test_key".to_string(), + path: PathBuf::from("/dev/null"), + optional: false, + }]; + + let result = runtime.block_on(cache_read.extract_objects(objects, pool)); + assert!(result.is_ok(), "Extracting to /dev/null should succeed"); + } + + #[cfg(unix)] + #[test] + fn test_extract_object_to_dev_fd_something() { + // Open a pipe, write to `/dev/fd/{fd}` and check the other end that the correct data was written. + use std::os::fd::AsRawFd; + use tokio::io::AsyncReadExt; + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .worker_threads(1) + .build() + .unwrap(); + let pool = runtime.handle(); + let mut cache_data = CacheWrite::new(); + let data = b"test data"; + cache_data.put_bytes("test_key", data).unwrap(); + let cache_read = CacheRead::from(io::Cursor::new(cache_data.finish().unwrap())).unwrap(); + runtime.block_on(async { + let (sender, mut receiver) = tokio::net::unix::pipe::pipe().unwrap(); + let sender_fd = sender.into_blocking_fd().unwrap(); + let raw_fd = sender_fd.as_raw_fd(); + let objects = vec![FileObjectSource { + key: "test_key".to_string(), + path: PathBuf::from(format!("/dev/fd/{}", raw_fd)), + optional: false, + }]; + let result = cache_read.extract_objects(objects, pool).await; + assert!( + result.is_ok(), + "Extracting to /dev/fd/{} should succeed", + raw_fd + ); + let mut buf = vec![0; data.len()]; + let n = receiver.read_exact(&mut buf).await.unwrap(); + assert_eq!(n, data.len(), "Read the correct number of bytes"); + assert_eq!(buf, data, "Read the correct data from /dev/fd/{}", raw_fd); + }); + } } diff --git a/tests/system.rs b/tests/system.rs index a8bda4f21..9a864e10c 100644 --- a/tests/system.rs +++ b/tests/system.rs @@ -195,6 +195,8 @@ const INPUT_FOR_HIP_A: &str = "test_a.hip"; const INPUT_FOR_HIP_B: &str = "test_b.hip"; const INPUT_FOR_HIP_C: &str = "test_c.hip"; const OUTPUT: &str = "test.o"; +const DEV_NULL: &str = "/dev/null"; +const DEV_STDOUT: &str = "/dev/stdout"; // Copy the source files into the tempdir so we can compile with relative paths, since the commandline winds up in the hash key. fn copy_to_tempdir(inputs: &[&str], tempdir: &Path) { @@ -265,6 +267,110 @@ fn test_basic_compile(compiler: Compiler, tempdir: &Path) { }); } +fn test_basic_compile_into_dev_null(compiler: Compiler, tempdir: &Path) { + let Compiler { + name, + exe, + env_vars, + } = compiler; + println!("test_basic_compile_into_dev_null: {}", name); + zero_stats(); + // Compile a source file. + copy_to_tempdir(&[INPUT, INPUT_ERR], tempdir); + + trace!("compile"); + sccache_command() + .args(compile_cmdline(name, &exe, INPUT, DEV_NULL, Vec::new())) + .current_dir(tempdir) + .envs(env_vars.clone()) + .assert() + .success(); + trace!("request stats"); + get_stats(|info| { + assert_eq!(1, info.stats.compile_requests); + assert_eq!(1, info.stats.requests_executed); + assert_eq!(1, info.stats.cache_hits.all()); + assert_eq!(0, info.stats.cache_misses.all()); + assert!(info.stats.cache_misses.get("C/C++").is_none()); + let adv_key = adv_key_kind("c", compiler.name); + assert!(info.stats.cache_misses.get_adv(&adv_key).is_none()); + }); + trace!("compile"); + sccache_command() + .args(compile_cmdline(name, &exe, INPUT, DEV_NULL, Vec::new())) + .current_dir(tempdir) + .envs(env_vars) + .assert() + .success(); + trace!("request stats"); + get_stats(|info| { + assert_eq!(2, info.stats.compile_requests); + assert_eq!(2, info.stats.requests_executed); + assert_eq!(2, info.stats.cache_hits.all()); + assert_eq!(0, info.stats.cache_misses.all()); + assert_eq!(&2, info.stats.cache_hits.get("C/C++").unwrap()); + assert!(info.stats.cache_misses.get("C/C++").is_none()); + let adv_key = adv_key_kind("c", compiler.name); + assert_eq!(&2, info.stats.cache_hits.get_adv(&adv_key).unwrap()); + assert!(info.stats.cache_misses.get_adv(&adv_key).is_none()); + }); +} + +#[cfg(unix)] +fn test_basic_compile_into_dev_stdout(compiler: Compiler, tempdir: &Path) { + let Compiler { + name, + exe, + env_vars, + } = compiler; + println!("test_basic_compile_into_dev_stdout: {}", name); + zero_stats(); + // Compile a source file. + copy_to_tempdir(&[INPUT, INPUT_ERR], tempdir); + + trace!("compile"); + sccache_command() + .args(compile_cmdline(name, &exe, INPUT, DEV_STDOUT, Vec::new())) + .current_dir(tempdir) + .envs(env_vars.clone()) + .assert() + .success(); + trace!("request stats"); + get_stats(|info| { + assert_eq!(1, info.stats.compile_requests); + assert_eq!(1, info.stats.requests_executed); + assert_eq!(1, info.stats.cache_hits.all()); + assert_eq!(0, info.stats.cache_misses.all()); + assert!(info.stats.cache_misses.get("C/C++").is_none()); + let adv_key = adv_key_kind("c", compiler.name); + assert!(info.stats.cache_misses.get_adv(&adv_key).is_none()); + }); + trace!("compile"); + sccache_command() + .args(compile_cmdline(name, &exe, INPUT, DEV_STDOUT, Vec::new())) + .current_dir(tempdir) + .envs(env_vars) + .assert() + .success(); + trace!("request stats"); + get_stats(|info| { + assert_eq!(2, info.stats.compile_requests); + assert_eq!(2, info.stats.requests_executed); + assert_eq!(2, info.stats.cache_hits.all()); + assert_eq!(0, info.stats.cache_misses.all()); + assert_eq!(&2, info.stats.cache_hits.get("C/C++").unwrap()); + assert!(info.stats.cache_misses.get("C/C++").is_none()); + let adv_key = adv_key_kind("c", compiler.name); + assert_eq!(&2, info.stats.cache_hits.get_adv(&adv_key).unwrap()); + assert!(info.stats.cache_misses.get_adv(&adv_key).is_none()); + }); +} + +#[cfg(not(unix))] +fn test_basic_compile_into_dev_stdout(_: Compiler, _: &Path) { + warn!("Not unix, skipping tests with /dev/stdout"); +} + fn test_noncacheable_stats(compiler: Compiler, tempdir: &Path) { let Compiler { name, @@ -631,6 +737,8 @@ fn run_sccache_command_tests(compiler: Compiler, tempdir: &Path, preprocessor_ca test_basic_compile(compiler.clone(), tempdir); } test_compile_with_define(compiler.clone(), tempdir); + test_basic_compile_into_dev_null(compiler.clone(), tempdir); + test_basic_compile_into_dev_stdout(compiler.clone(), tempdir); if compiler.name == "cl.exe" { test_msvc_deps(compiler.clone(), tempdir); test_msvc_responsefile(compiler.clone(), tempdir);