Skip to content
Open
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ This is most useful when using sccache for Rust compilation, as rustc supports u

---

Normalizing Paths with `SCCACHE_BASEDIRS`
Normalizing paths with `SCCACHE_BASEDIRS`
-----------------------------------------

By default, sccache requires absolute paths to match for cache hits. To enable cache sharing across different build directories, you can set `SCCACHE_BASEDIRS` to strip a base directory from paths before hashing:
Expand All @@ -344,13 +344,26 @@ export SCCACHE_BASEDIRS="/home/user/project:/home/user/workspace"

Path matching is **case-insensitive** on Windows and **case-sensitive** on other operating systems.

For Rust compilations, sccache normalizes matching absolute source arguments,
the source side of `--remap-path-prefix`, Cargo path variables, tracked
environment dependency values that are absolute paths, and the current working
directory before computing the cache key.

This is similar to ccache's `CCACHE_BASEDIR` and helps when:
* Building the same project from different directories
* Sharing cache between CI jobs with different checkout paths
* Multiple developers working with different username paths
* Working with multiple project checkouts simultaneously

**Note:** Only absolute paths are supported. Relative paths will prevent server from starting.
**Note:** Only absolute paths are supported. A relative request value fails that
compiler invocation. A relative config-file value prevents the server from
starting.

**Rust note:** This setting normalizes cache-key inputs; it does not rewrite
paths embedded in compiled artifacts. If a crate deliberately embeds an
absolute path, for example with `env!("CARGO_MANIFEST_DIR")`, a cache hit from
another checkout can contain the path from the compilation that populated the
cache. Use this opt-in setting only when that behavior is acceptable.

You can also configure this in the sccache config file:

Expand All @@ -362,6 +375,17 @@ basedirs = ["/home/user/project"]
basedirs = ["/home/user/project", "/home/user/workspace"]
```

The environment variable applies to each compiler invocation. Its value
overrides the config-file value for that request, so a persistent sccache
daemon can serve builds from different checkout roots without a restart. Set
`SCCACHE_BASEDIRS=""` on an invocation to disable the config-file fallback for
that request.

Only the config file defines the daemon's fallback. A `SCCACHE_BASEDIRS` value
in the environment that starts the daemon does not become global daemon state.
Statistics report the config-file fallback and do not accumulate paths from
individual requests.

---

Known Caveats
Expand Down
2 changes: 1 addition & 1 deletion docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ Note that some env variables may need sccache server restart to take effect.

* `SCCACHE_ALLOW_CORE_DUMPS` to enable core dumps by the server
* `SCCACHE_CONF` configuration file path
* `SCCACHE_BASEDIRS` base directory (or directories) to strip from paths for cache key computation. This is similar to ccache's `CCACHE_BASEDIR` and enables cache hits across different absolute paths when compiling the same source code. Multiple directories can be separated by `;` on Windows hosts and by `:` on any other operating system. When multiple directories are specified, the longest matching prefix is used. Path matching is **case-insensitive** on Windows and **case-sensitive** on other operating systems. Environment variable takes precedence over file configuration. Only absolute paths are supported; relative paths will cause an error and prevent the server from start.
* `SCCACHE_BASEDIRS` base directory (or directories) to strip from paths for cache key computation. This is similar to ccache's `CCACHE_BASEDIR` and enables cache hits across different absolute paths when compiling the same source code. Multiple directories can be separated by `;` on Windows hosts and by `:` on any other operating system. When multiple directories are specified, the longest matching prefix is used. Path matching is **case-insensitive** on Windows and **case-sensitive** on other operating systems. For Rust, sccache normalizes matching absolute source arguments, the source side of `--remap-path-prefix`, Cargo path variables, tracked environment dependency values that are absolute paths, and the current working directory. The environment variable applies to each compiler invocation and overrides the config-file fallback for that request. An explicitly empty value disables the fallback for that request. Only the config file defines the daemon fallback; the daemon ignores `SCCACHE_BASEDIRS` in its startup environment. Statistics report only the configured fallback. Only absolute paths are supported; a relative path causes an error for the affected invocation or during config-file loading. This setting changes cache keys but does not rewrite paths embedded in artifacts; a Rust artifact can retain an absolute path from the compilation that populated the cache.
* `SCCACHE_CACHED_CONF`
* `SCCACHE_IDLE_TIMEOUT` how long the local daemon process waits for more client requests before exiting, in seconds. Set to `0` to run sccache permanently
* `SCCACHE_STARTUP_NOTIFY` specify a path to a socket which will be used for server completion notification
Expand Down
1 change: 1 addition & 0 deletions docs/Rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ sccache includes support for caching Rust compilation. This includes many caveat
* Procedural macros that read files from the filesystem may not be cached properly.
* `rustc`'s incremental compilation needs to be disabled. See [The Cargo Book](https://doc.rust-lang.org/cargo/reference/profiles.html#incremental)
* Crates that invoke the system linker cannot be cached. Examples are `bin`, `dylib`, `cdylib`, and `proc-macro` crates.
* `SCCACHE_BASEDIRS` normalizes matching paths in cache-key inputs, but it does not rewrite paths embedded in artifacts. For example, a crate that uses `env!("CARGO_MANIFEST_DIR")` can retain the path from the compilation that populated a shared cache entry.

If you are using Rust 1.18 or later, you can ask cargo to wrap all compilation with sccache by setting `RUSTC_WRAPPER=sccache` in your build environment.
176 changes: 176 additions & 0 deletions src/cache/basedirs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Copyright 2026 Mozilla Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::sync::Arc;
use std::time::Duration;

use async_trait::async_trait;
use bytes::Bytes;

use crate::cache::multilevel::MultiLevelStats;
use crate::cache::{Cache, CacheMode, CacheWrite, GetPathResult, Storage};
use crate::compiler::PreprocessorCacheEntry;
use crate::config::PreprocessorCacheModeConfig;
use crate::errors::*;

struct BasedirsStorage {
inner: Arc<dyn Storage>,
basedirs: Vec<Vec<u8>>,
}

pub(crate) fn with_basedirs(storage: Arc<dyn Storage>, basedirs: Vec<Vec<u8>>) -> Arc<dyn Storage> {
if storage.basedirs() == basedirs {
storage
} else {
Arc::new(BasedirsStorage {
inner: storage,
basedirs,
})
}
}

#[async_trait]
impl Storage for BasedirsStorage {
async fn get(&self, key: &str) -> Result<Cache> {
self.inner.get(key).await
}

async fn put(&self, key: &str, entry: CacheWrite) -> Result<Duration> {
self.inner.put(key, entry).await
}

async fn get_raw(&self, key: &str) -> Result<Option<Bytes>> {
self.inner.get_raw(key).await
}

async fn put_raw(&self, key: &str, data: Bytes) -> Result<Duration> {
self.inner.put_raw(key, data).await
}

async fn check(&self) -> Result<CacheMode> {
self.inner.check().await
}

fn location(&self) -> String {
self.inner.location()
}

fn cache_type_name(&self) -> &'static str {
self.inner.cache_type_name()
}

async fn current_size(&self) -> Result<Option<u64>> {
self.inner.current_size().await
}

async fn max_size(&self) -> Result<Option<u64>> {
self.inner.max_size().await
}

fn multilevel_stats(&self) -> Option<MultiLevelStats> {
self.inner.multilevel_stats()
}

fn preprocessor_cache_mode_config(&self) -> PreprocessorCacheModeConfig {
self.inner.preprocessor_cache_mode_config()
}

fn basedirs(&self) -> &[Vec<u8>] {
&self.basedirs
}

async fn get_path(&self, key: &str) -> GetPathResult {
self.inner.get_path(key).await
}

async fn get_preprocessor_cache_entry(
&self,
key: &str,
) -> Result<Option<Box<dyn crate::lru_disk_cache::ReadSeek>>> {
self.inner.get_preprocessor_cache_entry(key).await
}

async fn put_preprocessor_cache_entry(
&self,
key: &str,
entry: PreprocessorCacheEntry,
) -> Result<()> {
self.inner.put_preprocessor_cache_entry(key, entry).await
}
}

#[cfg(test)]
mod test {
use super::*;
use crate::cache::disk::DiskCache;
use crate::cache::readonly::ReadOnlyStorage;

#[tokio::test]
async fn request_view_preserves_storage_capabilities() -> Result<()> {
let tempdir = tempfile::tempdir()?;

let fallback = vec![b"/configured/".to_vec()];
let storage: Arc<dyn Storage> = Arc::new(DiskCache::new(
tempdir.path(),
1024 * 1024,
&tokio::runtime::Handle::current(),
PreprocessorCacheModeConfig::default(),
CacheMode::ReadWrite,
fallback.clone(),
));

let unchanged = with_basedirs(Arc::clone(&storage), fallback.clone());
assert!(Arc::ptr_eq(&unchanged, &storage));

let request_basedirs = vec![b"/request/".to_vec()];
let view = with_basedirs(Arc::clone(&storage), request_basedirs.clone());
assert!(!Arc::ptr_eq(&view, &storage));
assert_eq!(view.basedirs(), request_basedirs);
assert_eq!(storage.basedirs(), fallback);
assert_eq!(view.location(), storage.location());
assert_eq!(view.cache_type_name(), storage.cache_type_name());

let key = "0123456789abcdef";
let raw: Bytes = CacheWrite::default().finish()?.into();

view.put_raw(key, raw.clone()).await?;
assert_eq!(view.get_raw(key).await?.as_deref(), Some(raw.as_ref()));
assert!(matches!(view.get_path(key).await, GetPathResult::Found(_)));

let entry_key = "fedcba9876543210";
view.put(entry_key, CacheWrite::default()).await?;
assert!(matches!(view.get(entry_key).await?, Cache::Hit(_)));

let preprocessor_key = "preprocessor";
view.put_preprocessor_cache_entry(preprocessor_key, PreprocessorCacheEntry::default())
.await?;
assert!(
view.get_preprocessor_cache_entry(preprocessor_key)
.await?
.is_some()
);
assert_eq!(
view.preprocessor_cache_mode_config(),
storage.preprocessor_cache_mode_config()
);
assert_eq!(view.current_size().await?, storage.current_size().await?);
assert_eq!(view.max_size().await?, storage.max_size().await?);

let read_only: Arc<dyn Storage> = Arc::new(ReadOnlyStorage(Arc::clone(&storage)));
let read_only_view = with_basedirs(read_only, vec![b"/read-only-request/".to_vec()]);
assert_eq!(read_only_view.check().await?, CacheMode::ReadOnly);

Ok(())
}
}
2 changes: 2 additions & 0 deletions src/cache/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

#[cfg(feature = "azure")]
pub mod azure;
pub(crate) mod basedirs;
#[allow(clippy::module_inception)]
pub mod cache;
pub mod cache_io;
Expand Down Expand Up @@ -52,6 +53,7 @@ pub mod webdav;
))]
pub(crate) mod http_client;

pub(crate) use crate::cache::basedirs::with_basedirs;
pub use crate::cache::cache::*;
pub use crate::cache::cache_io::*;
pub use crate::cache::ipc_storage::IpcStorage;
Expand Down
10 changes: 9 additions & 1 deletion src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,15 @@ where
pub fn run_command(cmd: Command) -> Result<i32> {
// Config isn't required for all commands, but if it's broken then we should flag
// it early and loudly.
let config = &Config::load()?;
let config = if matches!(
&cmd,
Command::StartServer | Command::InternalStartServer | Command::ShowStats(_, _)
) {
Config::load_with_file_basedirs()?
} else {
Config::load()?
};
let config = &config;
let startup_timeout = config.server_startup_timeout;

match cmd {
Expand Down
13 changes: 8 additions & 5 deletions src/compiler/nvcc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1314,16 +1314,19 @@ where
},
)),
CompilerArguments::Ok(hasher) => {
let env_vars = env_vars
.iter()
.chain([("SCCACHE_DIRECT".into(), "false".into())].iter())
.cloned()
.collect::<Vec<_>>();
let storage = srvc.storage_for_request(&env_vars)?;
srvc.start_compile_task(
compiler,
hasher,
args,
cwd.to_owned(),
env_vars
.iter()
.chain([("SCCACHE_DIRECT".into(), "false".into())].iter())
.cloned()
.collect::<Vec<_>>(),
env_vars,
storage,
)
.await
}
Expand Down
Loading
Loading