Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/pixi_build_backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ tokio = { workspace = true, features = ["macros"] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
url = { workspace = true }
which = { workspace = true }

pixi_build_types = { workspace = true }

Expand Down
45 changes: 45 additions & 0 deletions crates/pixi_build_backend/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,28 @@ pub fn sccache_envs(env: &HashMap<String, String>) -> Option<Vec<&str>> {
if res.is_empty() { None } else { Some(res) }
}

/// Ensure the binaries for a globally-configured compiler cache are available
/// on `PATH`.
///
/// A compiler cache that comes from the user's global pixi config is a
/// per-machine preference, so it is used as a compiler launcher only and is
/// deliberately *not* added to the build requirements (doing so would make the
/// lockfile depend on who runs the resolve). That means the tool has to be
/// installed on the machine already; if it is missing we fail with an
/// actionable hint instead of silently building without the cache.
pub fn ensure_compiler_cache_on_path(tools: &[SourcePackageName]) -> miette::Result<()> {
for tool in tools {
let name = tool.as_str();
if which::which(name).is_err() {
return Err(miette::miette!(
help = format!("install it with `pixi global install {name}`"),
"the global `compiler-cache` config requests `{name}`, but it was not found on PATH",
));
}
}
Ok(())
}

pub fn add_sccache<'a, P: ProjectModel>(
dependencies: &mut Dependencies<'a, <P::Targets as Targets>::Spec>,
sccache_tools: &'a [SourcePackageName],
Expand All @@ -33,3 +55,26 @@ pub fn add_sccache<'a, P: ProjectModel>(
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn ensure_compiler_cache_on_path_errors_when_missing() {
let tool = SourcePackageName::from(PackageName::new_unchecked(
"pixi-definitely-not-installed-cache",
));
let err = ensure_compiler_cache_on_path(std::slice::from_ref(&tool))
.expect_err("a non-existent tool must not be found on PATH");

// The hint should point users at `pixi global install`. Collapse
// whitespace first since miette wraps long lines in its rendering.
let rendered = format!("{err:?}");
let collapsed = rendered.split_whitespace().collect::<Vec<_>>().join(" ");
assert!(
collapsed.contains("pixi global install"),
"error should suggest `pixi global install`, got: {rendered}"
);
}
}
11 changes: 11 additions & 0 deletions crates/pixi_build_cmake/src/build_script.j2
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@
] -%}
{% endif -%}

{# Add sccache compiler launchers if available -#}
{%- if has_sccache -%}
{%- set cmake_args = cmake_args + [
"-DCMAKE_C_COMPILER_LAUNCHER=sccache",
"-DCMAKE_CXX_COMPILER_LAUNCHER=sccache",
] -%}
{% endif -%}

{#- Output version information -#}
ninja --version
cmake --version
Expand Down Expand Up @@ -56,3 +64,6 @@ fi

cmake --build . --target install
{% endif -%}
{%- if has_sccache %}
sccache --show-stats
{%- endif %}
23 changes: 23 additions & 0 deletions crates/pixi_build_cmake/src/build_script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pub struct BuildScriptContext {
/// This is used to determine if the build script
/// should include Python-related logic.
pub has_host_python: bool,
/// Whether sccache is available and should be used as a compiler launcher.
pub has_sccache: bool,
}

#[derive(Copy, Clone, Serialize)]
Expand Down Expand Up @@ -48,6 +50,7 @@ mod test {
source_dir: String::from("my-prefix-dir"),
extra_args: extra_args.clone(),
has_host_python,
has_sccache: false,
};
let script = context.render();

Expand All @@ -70,4 +73,24 @@ mod test {
insta::assert_snapshot!(script);
});
}

#[rstest]
fn test_build_script_sccache(
#[values(BuildPlatform::Windows, BuildPlatform::Unix)] build_platform: BuildPlatform,
) {
let context = BuildScriptContext {
build_platform,
source_dir: String::from("my-prefix-dir"),
extra_args: vec![],
has_host_python: false,
has_sccache: true,
};
let script = context.render();

let mut settings = insta::Settings::clone_current();
settings.set_snapshot_suffix(format!("{build_platform}"));
settings.bind(|| {
insta::assert_snapshot!(script);
});
}
}
113 changes: 112 additions & 1 deletion crates/pixi_build_cmake/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,64 @@ use indexmap::IndexMap;
use pixi_build_backend::generated_recipe::BackendConfig;
use serde::{Deserialize, Serialize};

/// The compiler cache to use during builds.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum CompilerCache {
/// Use sccache as the compiler cache.
Sccache,
}

/// A `compiler-cache` setting together with where it came from.
///
/// The two forms are deserialized from the same `compiler-cache` key but carry
/// different consequences, so the build can keep the lockfile deterministic:
///
/// - [`Self::Package`] — written by the package itself as a bare string
/// (`compiler-cache = "sccache"`). The cache tool is added to the build
/// requirements and therefore captured in the lockfile.
/// - [`Self::Default`] — injected by the command dispatcher as
/// `{ "default": "sccache" }` from the user's global/project pixi config. As
/// a per-machine preference it is used as a compiler launcher only and is
/// never added to the locked build requirements, so the tool must already be
/// on `PATH`.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum CompilerCacheConfig {
/// Set in the package manifest; locked as a build dependency.
Package(CompilerCache),
/// Injected default from pixi config; launcher only, not locked.
Default {
/// The cache requested by the global/project config.
default: CompilerCache,
},
}

impl CompilerCacheConfig {
/// The requested cache tool, regardless of where the setting came from.
pub fn cache(&self) -> &CompilerCache {
match self {
Self::Package(cache) | Self::Default { default: cache } => cache,
}
}

/// Whether the cache tool should be added to the locked build
/// requirements. Only a package-local setting is locked; an injected
/// per-machine default is used as a launcher only.
pub fn lock_as_dependency(&self) -> bool {
matches!(self, Self::Package(_))
}
}

#[derive(Debug, Default, Deserialize, Serialize, Clone)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct CMakeBackendConfig {
/// Extra args for CMake invocation
#[serde(default)]
pub extra_args: Vec<String>,
/// System environment variables (populated at runtime, not serialized)
#[serde(skip)]
pub system_env: IndexMap<String, String>,
/// Environment Variables
#[serde(default)]
pub env: IndexMap<String, String>,
Expand All @@ -23,6 +75,25 @@ pub struct CMakeBackendConfig {
/// List of compilers to use (e.g., ["c", "cxx", "cuda"])
/// If not specified, a default will be used
pub compilers: Option<Vec<String>>,
/// The compiler cache to use. A bare `compiler-cache = "sccache"` in the
/// package manifest is locked as a build dependency; a value injected from
/// the user's pixi config is used as a launcher only. See
/// [`CompilerCacheConfig`].
pub compiler_cache: Option<CompilerCacheConfig>,
}

fn collect_system_env() -> IndexMap<String, String> {
std::env::vars().collect()
}

impl CMakeBackendConfig {
/// Create a new `CMakeBackendConfig` with the current system environment.
pub fn new_with_system_environment() -> Self {
Self {
system_env: collect_system_env(),
..Self::default()
}
}
}

impl BackendConfig for CMakeBackendConfig {
Expand All @@ -43,6 +114,7 @@ impl BackendConfig for CMakeBackendConfig {
} else {
target_config.extra_args.clone()
},
system_env: collect_system_env(),
env: {
let mut merged_env = self.env.clone();
merged_env.extend(target_config.env.clone());
Expand All @@ -58,6 +130,10 @@ impl BackendConfig for CMakeBackendConfig {
.compilers
.clone()
.or_else(|| self.compilers.clone()),
compiler_cache: target_config
.compiler_cache
.clone()
.or_else(|| self.compiler_cache.clone()),
})
}
}
Expand All @@ -68,14 +144,46 @@ mod tests {
use serde_json::json;
use std::path::PathBuf;

use super::CMakeBackendConfig;
use super::{CMakeBackendConfig, CompilerCache, CompilerCacheConfig};

#[test]
fn test_ensure_deserialize_from_empty() {
let json_data = json!({});
serde_json::from_value::<CMakeBackendConfig>(json_data).unwrap();
}

#[test]
fn test_compiler_cache_distinguishes_package_from_injected_default() {
// A bare string is what a package writes in its manifest: locked.
let package = serde_json::from_value::<CMakeBackendConfig>(json!({
"compiler-cache": "sccache"
}))
.unwrap()
.compiler_cache
.unwrap();
assert_eq!(package, CompilerCacheConfig::Package(CompilerCache::Sccache));
assert!(package.lock_as_dependency());

// The tagged form is what the command dispatcher injects from global
// config: a launcher only, never locked.
let injected = serde_json::from_value::<CMakeBackendConfig>(json!({
"compiler-cache": { "default": "sccache" }
}))
.unwrap()
.compiler_cache
.unwrap();
assert_eq!(
injected,
CompilerCacheConfig::Default {
default: CompilerCache::Sccache
}
);
assert!(!injected.lock_as_dependency());

// Both still resolve to the same underlying cache tool.
assert_eq!(package.cache(), injected.cache());
}

#[test]
fn test_merge_with_target_config() {
let mut base_env = indexmap::IndexMap::new();
Expand All @@ -88,6 +196,7 @@ mod tests {
debug_dir: Some(PathBuf::from("/base/debug")),
extra_input_globs: vec!["*.base".to_string()],
compilers: Some(vec!["cxx".to_string()]),
..CMakeBackendConfig::default()
};

let mut target_env = indexmap::IndexMap::new();
Expand All @@ -100,6 +209,7 @@ mod tests {
debug_dir: None,
extra_input_globs: vec!["*.target".to_string()],
compilers: Some(vec!["c".to_string(), "cuda".to_string()]),
..CMakeBackendConfig::default()
};

let merged = base_config
Expand Down Expand Up @@ -144,6 +254,7 @@ mod tests {
debug_dir: Some(PathBuf::from("/base/debug")),
extra_input_globs: vec!["*.base".to_string()],
compilers: Some(vec!["cxx".to_string()]),
..CMakeBackendConfig::default()
};

let empty_target_config = CMakeBackendConfig::default();
Expand Down
Loading
Loading