mod before_hooks;
mod fingerprint;
#[cfg(test)]
mod hook_reuse_tests;
#[cfg(test)]
mod preparation_error_tests;
mod purge;
mod toolchain_cache;
use before_hooks::{HookOutcomes, run_before_once};
use fingerprint::{session_fingerprint, session_toolchain_key};
use purge::{cleanup_legacy_scratch_directories, purge_abandoned_scratch, purge_stale_session_scratch};
use toolchain_cache::{ToolchainCaches, purge_stale_toolchain_caches};
pub use toolchain_cache::DEFAULT_TOOLCHAIN_CACHE_GENERATIONS;
use crate::snippets::error::{Error, Result};
use crate::snippets::types::Language;
use rayon::prelude::*;
use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct SessionSpec {
pub language: Language,
pub working_directory: PathBuf,
pub manifest: Option<PathBuf>,
pub before: Vec<String>,
pub env: BTreeMap<String, String>,
pub include_paths: Vec<PathBuf>,
pub rust_features: Vec<String>,
pub rust_dependencies: BTreeMap<String, crate::core::config::output::DocsSnippetRustDependencyConfig>,
}
#[derive(Debug, Clone)]
pub struct ValidationSession {
pub language: Language,
pub working_directory: PathBuf,
pub manifest: Option<PathBuf>,
pub fingerprint: String,
pub env: BTreeMap<String, String>,
pub include_paths: Vec<PathBuf>,
pub rust_features: Vec<String>,
pub rust_dependencies: BTreeMap<String, crate::core::config::output::DocsSnippetRustDependencyConfig>,
}
pub(crate) struct SessionPreparation {
pub sessions: HashMap<String, ValidationSession>,
pub errors: HashMap<String, SessionPreparationError>,
}
#[derive(Debug, Clone)]
pub(crate) struct SessionPreparationError {
pub message: String,
pub ordering: bool,
}
pub(super) const SESSION_SCRATCH_ROOT: &str = ".alef/snippets/sessions";
pub(super) fn workspace_scratch_directory(working_directory: &Path, fingerprint: &str) -> PathBuf {
working_directory.join(SESSION_SCRATCH_ROOT).join(fingerprint)
}
pub(super) const fn keeps_scratch_outside_working_directory(language: Language) -> bool {
matches!(language, Language::Java)
}
impl ValidationSession {
pub fn workspace_directory(&self) -> Result<PathBuf> {
let directory = workspace_scratch_directory(&self.working_directory, &self.fingerprint);
crate::core::cache_dir::ensure_cache_dir(&directory)?;
Ok(directory)
}
pub fn external_workspace_directory(&self) -> Result<PathBuf> {
let directory = std::env::temp_dir()
.join("alef-snippets/sessions")
.join(&self.fingerprint);
std::fs::create_dir_all(&directory)?;
Ok(directory)
}
#[must_use]
pub fn scratch_root(&self) -> PathBuf {
crate::snippets::scratch::scratch_root(self.language, &self.working_directory, self.manifest.as_deref())
}
pub fn scratch_dir(&self) -> Result<crate::snippets::scratch::ScratchDir> {
crate::snippets::scratch::ScratchDir::for_session(self)
}
pub fn apply(&self, command: &mut std::process::Command) {
command.current_dir(&self.working_directory);
self.apply_environment(command);
}
pub fn apply_environment(&self, command: &mut std::process::Command) {
let caches = self.cache_directories();
command.env("GOCACHE", &caches.go_build);
command.env("ZIG_GLOBAL_CACHE_DIR", &caches.zig_global);
command.env("CARGO_TARGET_DIR", &caches.cargo_target);
for (name, value) in &self.env {
let path = std::path::Path::new(value);
let value = if TOOLCHAIN_CACHE_VARIABLES.contains(&name.as_str()) && path.is_relative() {
self.working_directory.join(path).into_os_string()
} else {
value.into()
};
command.env(name, value);
}
}
#[must_use]
pub fn cargo_target_directory(&self) -> PathBuf {
self.cache_directories().cargo_target
}
#[must_use]
pub fn toolchain_cache_key(&self) -> String {
session_toolchain_key(self)
}
fn cache_directories(&self) -> ToolchainCaches {
toolchain_cache::cache_directories(self)
}
}
const TOOLCHAIN_CACHE_VARIABLES: &[&str] = &["GOCACHE", "ZIG_GLOBAL_CACHE_DIR", "CARGO_TARGET_DIR"];
type ResolvedSession<'a> = (&'a String, &'a SessionSpec, ValidationSession);
#[cfg(test)]
pub(crate) fn prepare_sessions_isolated(specs: &HashMap<String, SessionSpec>, timeout_secs: u64) -> SessionPreparation {
prepare_sessions_isolated_with_activation_filter(
specs,
timeout_secs,
DEFAULT_TOOLCHAIN_CACHE_GENERATIONS,
&|_, _| true,
)
}
pub(crate) fn prepare_sessions_isolated_with_activation_filter(
specs: &HashMap<String, SessionSpec>,
timeout_secs: u64,
generations: usize,
needs_activation: &(dyn Fn(&str, &ValidationSession) -> bool + Sync),
) -> SessionPreparation {
let mut sessions = HashMap::new();
let mut errors = HashMap::new();
let mut resolved = Vec::new();
for (target, spec, outcome) in resolve_sessions(specs, timeout_secs) {
match outcome {
Ok(session) => resolved.push((target, spec, session)),
Err(error) => record_preparation_error(&mut errors, target, spec, &error),
}
}
purge_stale_session_scratch(&resolved);
purge_stale_toolchain_caches(&resolved, timeout_secs, generations);
let mut outcomes = activate_sessions(&resolved, timeout_secs, needs_activation);
outcomes.sort_by_key(|(index, _)| *index);
for ((target, spec, session), (_, outcome)) in resolved.into_iter().zip(outcomes) {
match outcome {
Ok(()) => {
sessions.insert(target.clone(), session);
}
Err(error) => record_preparation_error(&mut errors, target, spec, &error),
}
}
SessionPreparation { sessions, errors }
}
fn resolve_sessions(
specs: &HashMap<String, SessionSpec>,
timeout_secs: u64,
) -> Vec<(&String, &SessionSpec, Result<ValidationSession>)> {
let span = tracing::Span::current();
specs
.iter()
.collect::<Vec<_>>()
.into_par_iter()
.map(|(target, spec)| (target, spec, span.in_scope(|| resolve_session(spec, timeout_secs))))
.collect()
}
fn activate_sessions(
resolved: &[ResolvedSession<'_>],
timeout_secs: u64,
needs_activation: &(dyn Fn(&str, &ValidationSession) -> bool + Sync),
) -> Vec<(usize, Result<()>)> {
let span = tracing::Span::current();
activation_groups(resolved)
.into_par_iter()
.map(|indices| {
span.in_scope(|| {
let mut ran = HookOutcomes::default();
indices
.into_iter()
.map(|index| {
let (target, spec, session) = &resolved[index];
(
index,
activate_session(target, spec, session, timeout_secs, &mut ran, needs_activation),
)
})
.collect::<Vec<_>>()
})
})
.collect::<Vec<_>>()
.into_iter()
.flatten()
.collect()
}
fn activation_groups(resolved: &[ResolvedSession<'_>]) -> Vec<Vec<usize>> {
let mut groups: BTreeMap<&Path, Vec<usize>> = BTreeMap::new();
for (index, (_, spec, _)) in resolved.iter().enumerate() {
groups.entry(spec.working_directory.as_path()).or_default().push(index);
}
groups.into_values().collect()
}
fn record_preparation_error(
errors: &mut HashMap<String, SessionPreparationError>,
target: &str,
spec: &SessionSpec,
error: &Error,
) {
let ordering = matches!(error, Error::Timeout { .. });
let message = if ordering {
format!(
"preparing snippet validation target `{target}`: the {} session's `before` hook (the step that \
builds this language's artifacts) {error}. Run `alef build` before validating, or raise \
`docs.snippets.timeout_secs`; `alef all --clean` discards previously built artifacts, which makes \
this more likely.",
spec.language
)
} else {
format!("preparing snippet validation target `{target}`: {error}")
};
tracing::error!(
target = %target,
language = %spec.language,
error = %error,
ordering,
"snippet validation session preparation failed"
);
errors.insert(target.to_owned(), SessionPreparationError { message, ordering });
}
fn resolve_session(spec: &SessionSpec, timeout_secs: u64) -> Result<ValidationSession> {
let language = spec.language;
ensure_directory(&spec.working_directory, language)?;
cleanup_legacy_scratch_directories(&spec.working_directory, timeout_secs)?;
purge_abandoned_scratch(spec, timeout_secs);
if let Some(manifest) = &spec.manifest
&& !manifest.is_file()
{
return Err(Error::Other(format!(
"configured {language} snippet manifest does not exist: {}",
manifest.display()
)));
}
Ok(ValidationSession {
language,
working_directory: spec.working_directory.clone(),
manifest: spec.manifest.clone(),
fingerprint: session_fingerprint(spec)?,
env: spec.env.clone(),
include_paths: spec.include_paths.clone(),
rust_features: spec.rust_features.clone(),
rust_dependencies: spec.rust_dependencies.clone(),
})
}
fn activate_session(
target: &str,
spec: &SessionSpec,
session: &ValidationSession,
timeout_secs: u64,
ran: &mut HookOutcomes,
needs_activation: &(dyn Fn(&str, &ValidationSession) -> bool + Sync),
) -> Result<()> {
let language = spec.language;
if needs_activation(target, session) {
for command in &spec.before {
run_before_once(command, spec, timeout_secs, ran).map_err(|error| match error {
Error::Timeout { .. } => error,
other => Error::Other(format!("preparing {language} snippet validation session: {other}")),
})?;
}
} else {
tracing::debug!(
target = %target,
language = %language,
"skipping snippet validation session before hook: every claimed snippet is already cached"
);
}
let caches = session.cache_directories();
for directory in caches.directories() {
crate::core::cache_dir::ensure_cache_dir(directory).map_err(|error| {
Error::Other(format!(
"creating snippet toolchain cache {}: {error}",
directory.display()
))
})?;
}
caches.mark_used();
Ok(())
}
fn ensure_directory(path: &Path, language: Language) -> Result<()> {
if path.is_dir() {
Ok(())
} else {
Err(Error::Other(format!(
"configured {language} snippet working directory does not exist: {}",
path.display()
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prepares_before_command_once_per_language() {
let directory = tempfile::tempdir().expect("temp directory");
let marker = directory.path().join("prepared");
let mut specs = HashMap::new();
specs.insert(
"python".into(),
SessionSpec {
language: Language::Python,
working_directory: directory.path().to_path_buf(),
manifest: None,
before: vec![format!("test ! -e prepared && touch {}", marker.display())],
env: BTreeMap::new(),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: BTreeMap::new(),
},
);
let prepared = prepare_sessions_isolated(&specs, 5);
assert!(marker.exists());
assert!(prepared.errors.is_empty());
assert_eq!(prepared.sessions.len(), 1);
}
#[test]
fn scratch_cleanup_errors_name_the_working_directory() {
let directory = tempfile::tempdir().expect("temp directory");
let missing = directory.path().join("removed");
let error = cleanup_legacy_scratch_directories(&missing, 5).expect_err("missing root must fail");
let message = error.to_string();
assert!(message.contains("reading snippet working directory"));
assert!(message.contains(&missing.display().to_string()));
}
#[tracing_test::traced_test]
#[test]
fn rejects_missing_configured_manifest() {
let directory = tempfile::tempdir().expect("temp directory");
let mut specs = HashMap::new();
specs.insert(
"typescript".into(),
SessionSpec {
language: Language::TypeScript,
working_directory: directory.path().to_path_buf(),
manifest: Some(directory.path().join("missing.json")),
before: Vec::new(),
env: BTreeMap::new(),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: BTreeMap::new(),
},
);
let prepared = prepare_sessions_isolated(&specs, 5);
let error = prepared.errors.get("typescript").expect("missing manifest is rejected");
assert!(logs_contain("snippet validation session preparation failed"));
assert!(logs_contain("typescript"));
assert!(error.message.contains("manifest does not exist"));
assert!(
!error.ordering,
"a missing manifest is a configuration error, not an ordering problem: {}",
error.message
);
}
fn posix_hook(script: &str) -> String {
debug_assert!(
!script.contains(['&', '|', '<', '>', '(', ')', '^', '"']),
"a `cmd` metacharacter in {script:?} would be parsed before `sh` sees the hook"
);
if cfg!(windows) {
format!("sh -c '{script}'")
} else {
script.to_owned()
}
}
fn sh_path(path: &Path) -> String {
path.display().to_string().replace('\\', "/")
}
#[test]
fn stale_workspace_scratch_files_are_purged_before_before_hooks_run() {
let directory = tempfile::tempdir().expect("temp directory");
let spec = SessionSpec {
language: Language::TypeScript,
working_directory: directory.path().to_path_buf(),
manifest: None,
before: vec![posix_hook("find . -name snippet.ts -exec false {} +")],
env: BTreeMap::new(),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: BTreeMap::new(),
};
let fingerprint = session_fingerprint(&spec).expect("fingerprint");
let workspace = workspace_scratch_directory(directory.path(), &fingerprint);
std::fs::create_dir_all(&workspace).expect("workspace directory");
let stale_file = workspace.join("snippet.ts");
std::fs::write(&stale_file, "this does not compile: :::").expect("stale scratch file");
let cache_subdir = workspace.join("dist");
std::fs::create_dir_all(&cache_subdir).expect("cache subdirectory");
std::fs::write(cache_subdir.join("snippet.js"), b"cached").expect("cached artifact");
let mut specs = HashMap::new();
specs.insert("typescript".to_string(), spec);
let prepared = prepare_sessions_isolated(&specs, 5);
assert!(
prepared.errors.is_empty(),
"the `before` hook must run against an already-purged workspace: {:?}",
prepared.errors
);
assert!(!stale_file.exists(), "the stale scratch file must be purged");
assert!(
cache_subdir.join("snippet.js").exists(),
"cache subdirectories must survive the purge"
);
}
#[test]
fn a_stale_session_directory_from_a_previous_run_cannot_break_the_current_one() {
let directory = tempfile::tempdir().expect("temp directory");
let stale = workspace_scratch_directory(directory.path(), "fingerprint-from-a-previous-run");
std::fs::create_dir_all(&stale).expect("stale session directory");
std::fs::write(stale.join("Example.java"), "public class Example {}").expect("stale source");
std::fs::write(stale.join("Example.class"), b"stale").expect("stale class file");
let mut specs = HashMap::new();
specs.insert(
"java".to_string(),
SessionSpec {
language: Language::Java,
working_directory: directory.path().to_path_buf(),
manifest: None,
before: vec![posix_hook("find . -name Example.java -exec false {} +")],
env: BTreeMap::new(),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: BTreeMap::new(),
},
);
let prepared = prepare_sessions_isolated(&specs, 5);
assert!(
prepared.errors.is_empty(),
"a previous run's leftovers must not reach this run's `before` hook: {:?}",
prepared.errors
);
assert!(
!stale.exists(),
"a stale session directory must not survive into the next run"
);
}
#[test]
fn a_stale_fingerprint_is_removed_while_the_live_one_keeps_its_caches() {
let directory = tempfile::tempdir().expect("temp directory");
let spec = SessionSpec {
language: Language::TypeScript,
working_directory: directory.path().to_path_buf(),
manifest: None,
before: Vec::new(),
env: BTreeMap::new(),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: BTreeMap::new(),
};
let live = workspace_scratch_directory(directory.path(), &session_fingerprint(&spec).expect("fingerprint"));
std::fs::create_dir_all(live.join("dist")).expect("live cache directory");
std::fs::write(live.join("dist/cached.js"), b"cached").expect("cached artifact");
std::fs::write(live.join("snippet.ts"), "this does not compile: :::").expect("stale scratch file");
let stale = workspace_scratch_directory(directory.path(), "fingerprint-from-a-previous-run");
std::fs::create_dir_all(&stale).expect("stale session directory");
std::fs::write(stale.join("snippet.ts"), "this does not compile: :::").expect("stale scratch file");
let mut specs = HashMap::new();
specs.insert("typescript".to_string(), spec);
let prepared = prepare_sessions_isolated(&specs, 5);
assert!(prepared.errors.is_empty(), "{:?}", prepared.errors);
assert!(!stale.exists(), "a stale fingerprint's directory must be removed");
assert!(
live.join("dist/cached.js").exists(),
"the live fingerprint's caches must survive"
);
assert!(
!live.join("snippet.ts").exists(),
"the live fingerprint's stray scratch files must still be swept"
);
}
#[test]
fn sibling_sessions_sharing_a_working_directory_keep_each_others_scratch() {
let directory = tempfile::tempdir().expect("temp directory");
let base = SessionSpec {
language: Language::TypeScript,
working_directory: directory.path().to_path_buf(),
manifest: None,
before: Vec::new(),
env: BTreeMap::new(),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: BTreeMap::new(),
};
let mut node = base.clone();
node.env = BTreeMap::from([("ALEF_SESSION".into(), "node".into())]);
let mut wasm = base;
wasm.env = BTreeMap::from([("ALEF_SESSION".into(), "wasm".into())]);
let fingerprints = [
session_fingerprint(&node).expect("node fingerprint"),
session_fingerprint(&wasm).expect("wasm fingerprint"),
];
assert_ne!(fingerprints[0], fingerprints[1]);
for fingerprint in &fingerprints {
let workspace = workspace_scratch_directory(directory.path(), fingerprint);
std::fs::create_dir_all(workspace.join("dist")).expect("cache directory");
std::fs::write(workspace.join("dist/cached.js"), b"cached").expect("cached artifact");
}
let specs = HashMap::from([("node".to_string(), node), ("wasm".to_string(), wasm)]);
let prepared = prepare_sessions_isolated(&specs, 5);
assert!(prepared.errors.is_empty(), "{:?}", prepared.errors);
for fingerprint in &fingerprints {
let cached = workspace_scratch_directory(directory.path(), fingerprint).join("dist/cached.js");
assert!(
cached.exists(),
"a sibling session's live scratch must survive: {}",
cached.display()
);
}
}
fn waiting_spec(language: Language, working_directory: &Path, own: &Path, sibling: &Path) -> SessionSpec {
SessionSpec {
language,
working_directory: working_directory.to_path_buf(),
manifest: None,
before: vec![posix_hook(&format!(
"touch {own}; attempts=0; while [ $attempts -lt {ACTIVATION_PROBE_ATTEMPTS} ]; do \
if [ -e {sibling} ]; then exit 0; fi; sleep 0.01; attempts=`expr $attempts + 1`; done; exit 1",
own = sh_path(own),
sibling = sh_path(sibling),
))],
env: BTreeMap::new(),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: BTreeMap::new(),
}
}
const ACTIVATION_PROBE_ATTEMPTS: usize = 500;
#[test]
fn before_hooks_in_different_working_directories_run_concurrently() {
let first = tempfile::tempdir().expect("first directory");
let second = tempfile::tempdir().expect("second directory");
let first_marker = first.path().join("started");
let second_marker = second.path().join("started");
let specs = HashMap::from([
(
"typescript".to_string(),
waiting_spec(Language::TypeScript, first.path(), &first_marker, &second_marker),
),
(
"python".to_string(),
waiting_spec(Language::Python, second.path(), &second_marker, &first_marker),
),
]);
let prepared = prepare_sessions_isolated(&specs, 30);
assert!(
prepared.errors.is_empty(),
"both `before` hooks must be in flight at once: {:?}",
prepared.errors
);
assert_eq!(prepared.sessions.len(), 2);
}
#[test]
fn before_hooks_sharing_a_working_directory_do_not_overlap() {
let directory = tempfile::tempdir().expect("temp directory");
let claim = directory.path().join("activating");
let exclusive = format!(
"test ! -e {claim} && touch {claim} && sleep 0.3 && rm {claim}",
claim = claim.display()
);
let spec = SessionSpec {
language: Language::TypeScript,
working_directory: directory.path().to_path_buf(),
manifest: None,
before: vec![exclusive],
env: BTreeMap::new(),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: BTreeMap::new(),
};
let mut sibling = spec.clone();
sibling.env = BTreeMap::from([("ALEF_SESSION".into(), "sibling".into())]);
let specs = HashMap::from([("node".to_string(), spec), ("wasm".to_string(), sibling)]);
let prepared = prepare_sessions_isolated(&specs, 30);
assert!(
prepared.errors.is_empty(),
"sessions sharing a working directory must not build it concurrently: {:?}",
prepared.errors
);
assert_eq!(prepared.sessions.len(), 2);
}
#[test]
fn applies_environment_to_setup_and_validation_commands() {
let directory = tempfile::tempdir().expect("temp directory");
let mut specs = HashMap::new();
specs.insert(
"zig".into(),
SessionSpec {
language: Language::Zig,
working_directory: directory.path().to_path_buf(),
manifest: None,
before: vec!["test \"$ALEF_SESSION_CACHE\" = configured".into()],
env: BTreeMap::from([("ALEF_SESSION_CACHE".into(), "configured".into())]),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: BTreeMap::new(),
},
);
let prepared = prepare_sessions_isolated(&specs, 5);
assert!(prepared.errors.is_empty());
let session = prepared.sessions.get("zig").expect("zig session");
let mut command = std::process::Command::new("true");
session.apply(&mut command);
assert_eq!(
command.get_envs().next(),
Some(("ALEF_SESSION_CACHE".as_ref(), Some("configured".as_ref())))
);
}
#[test]
fn reuses_a_stable_workspace_for_a_prepared_session() {
let directory = tempfile::tempdir().expect("temp directory");
let session = ValidationSession {
language: Language::Python,
working_directory: directory.path().to_path_buf(),
manifest: None,
fingerprint: "neutral-fixture".into(),
env: BTreeMap::new(),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: BTreeMap::new(),
};
let first = session.workspace_directory().expect("first workspace");
std::fs::write(first.join("compiler-output"), "cached").expect("compiler output");
let second = session.workspace_directory().expect("second workspace");
assert_eq!(first, second);
assert_eq!(
std::fs::read_to_string(second.join("compiler-output")).unwrap(),
"cached"
);
}
#[test]
fn external_workspace_directory_stays_outside_the_working_directory_and_is_stable() {
let directory = tempfile::tempdir().expect("temp directory");
let fingerprint = format!(
"external-workspace-fixture-{}",
directory.path().to_string_lossy().replace(['/', '\\', ':'], "_")
);
let session = ValidationSession {
language: Language::Python,
working_directory: directory.path().to_path_buf(),
manifest: None,
fingerprint,
env: BTreeMap::new(),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: BTreeMap::new(),
};
let first = session
.external_workspace_directory()
.expect("first external workspace");
assert!(
!first.starts_with(directory.path()),
"external workspace must never be nested under working_directory: {}",
first.display()
);
std::fs::write(first.join("compiler-output"), "cached").expect("compiler output");
let second = session
.external_workspace_directory()
.expect("second external workspace");
assert_eq!(first, second);
assert_eq!(
std::fs::read_to_string(second.join("compiler-output")).unwrap(),
"cached"
);
let _ = std::fs::remove_dir_all(&first);
}
#[test]
fn provides_absolute_isolated_toolchain_directories() {
let directory = tempfile::tempdir().expect("temp directory");
let session = ValidationSession {
language: Language::Python,
working_directory: directory.path().to_path_buf(),
manifest: None,
fingerprint: "neutral-fixture".into(),
env: BTreeMap::new(),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: BTreeMap::new(),
};
let scratch = session.scratch_dir().expect("isolated scratch directory");
assert!(scratch.path().starts_with(directory.path().join(".alef/snippets/tmp")));
let mut command = std::process::Command::new("true");
session.apply_environment(&mut command);
let values = command
.get_envs()
.filter_map(|(name, value)| value.map(|value| (name.to_string_lossy().into_owned(), value.to_owned())))
.collect::<BTreeMap<_, _>>();
for name in TOOLCHAIN_CACHE_VARIABLES {
assert!(std::path::Path::new(&values[*name]).is_absolute(), "{name}");
}
assert_eq!(
std::path::Path::new(&values["CARGO_TARGET_DIR"]),
session.cargo_target_directory()
);
}
}