Skip to content
Open
103 changes: 97 additions & 6 deletions src/cache/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,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;
Expand Down Expand Up @@ -207,22 +208,51 @@ 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!"),
};
// 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,
}
Expand Down Expand Up @@ -823,4 +853,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);
});
}
}
108 changes: 108 additions & 0 deletions tests/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
Loading