use std::fmt;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::compiler::CompilerVersion;
use crate::condition::Condition;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CompilerWrapperKind {
Ccache,
Sccache,
}
impl CompilerWrapperKind {
pub const fn as_key(self) -> &'static str {
match self {
CompilerWrapperKind::Ccache => "ccache",
CompilerWrapperKind::Sccache => "sccache",
}
}
pub const fn default_command(self) -> &'static str {
match self {
CompilerWrapperKind::Ccache => "ccache",
CompilerWrapperKind::Sccache => "sccache",
}
}
pub const fn all() -> &'static [CompilerWrapperKind] {
&[CompilerWrapperKind::Ccache, CompilerWrapperKind::Sccache]
}
}
impl fmt::Display for CompilerWrapperKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_key())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", tag = "kind")]
pub enum CompilerWrapperRequest {
Disabled,
Use { wrapper: CompilerWrapperKind },
}
impl CompilerWrapperRequest {
pub fn parse(raw: &str) -> Result<Self, CompilerWrapperParseError> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(CompilerWrapperParseError::Empty);
}
match trimmed.to_ascii_lowercase().as_str() {
"none" | "off" | "disabled" => Ok(Self::Disabled),
"ccache" => Ok(Self::Use {
wrapper: CompilerWrapperKind::Ccache,
}),
"sccache" => Ok(Self::Use {
wrapper: CompilerWrapperKind::Sccache,
}),
_ => Err(CompilerWrapperParseError::Unsupported {
raw: trimmed.to_owned(),
}),
}
}
pub const fn as_key(&self) -> &'static str {
match self {
CompilerWrapperRequest::Disabled => "none",
CompilerWrapperRequest::Use {
wrapper: CompilerWrapperKind::Ccache,
} => "ccache",
CompilerWrapperRequest::Use {
wrapper: CompilerWrapperKind::Sccache,
} => "sccache",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum CompilerWrapperParseError {
#[error("compiler-wrapper value must not be empty")]
Empty,
#[error(
"compiler-wrapper value `{raw}` is not supported; expected one of: none, ccache, sccache"
)]
Unsupported { raw: String },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConditionalCompilerWrapperDecl {
pub condition: Condition,
pub request: CompilerWrapperRequest,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilerWrapperManifestSettings {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub general: Option<CompilerWrapperRequest>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub conditional: Vec<ConditionalCompilerWrapperDecl>,
}
impl CompilerWrapperManifestSettings {
pub fn is_empty(&self) -> bool {
self.general.is_none() && self.conditional.is_empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CompilerWrapperSource {
Cli,
Env,
UserConfig,
WorkspaceConfig,
PackageConfig,
ExplicitConfig,
ManifestConditional,
Manifest,
}
impl CompilerWrapperSource {
pub const fn as_key(self) -> &'static str {
match self {
CompilerWrapperSource::Cli => "cli",
CompilerWrapperSource::Env => "env",
CompilerWrapperSource::UserConfig => "user-config",
CompilerWrapperSource::WorkspaceConfig => "workspace-config",
CompilerWrapperSource::PackageConfig => "package-config",
CompilerWrapperSource::ExplicitConfig => "explicit-config",
CompilerWrapperSource::ManifestConditional => "manifest-conditional",
CompilerWrapperSource::Manifest => "manifest",
}
}
}
impl fmt::Display for CompilerWrapperSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_key())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilerWrapperIdentity {
pub kind: CompilerWrapperKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<CompilerVersion>,
pub raw_version_line: String,
}
impl CompilerWrapperIdentity {
pub fn unknown_version(kind: CompilerWrapperKind, raw_version_line: impl Into<String>) -> Self {
Self {
kind,
version: None,
raw_version_line: raw_version_line.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolvedCompilerWrapper {
pub kind: CompilerWrapperKind,
pub path: PathBuf,
pub spec: String,
pub source: CompilerWrapperSource,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identity: Option<CompilerWrapperIdentity>,
}
impl ResolvedCompilerWrapper {
pub fn as_json(&self) -> serde_json::Value {
let version = self
.identity
.as_ref()
.and_then(|id| id.version.as_ref())
.map_or(serde_json::Value::Null, |v| {
serde_json::Value::String(v.to_display_string())
});
let raw = self
.identity
.as_ref()
.map_or(serde_json::Value::Null, |id| {
serde_json::Value::String(id.raw_version_line.clone())
});
serde_json::json!({
"kind": self.kind.as_key(),
"spec": self.spec,
"source": self.source.as_key(),
"version": version,
"raw_version_line": raw,
})
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilerWrapperSummary {
pub kind: String,
pub spec: String,
pub source: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}
impl CompilerWrapperSummary {
pub fn from_resolved(resolved: &ResolvedCompilerWrapper) -> Self {
Self {
kind: resolved.kind.as_key().to_owned(),
spec: resolved.spec.clone(),
source: resolved.source.as_key().to_owned(),
version: resolved
.identity
.as_ref()
.and_then(|id| id.version.as_ref())
.map(super::compiler::CompilerVersion::to_display_string),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_accepts_documented_values() {
assert_eq!(
CompilerWrapperRequest::parse("none").unwrap(),
CompilerWrapperRequest::Disabled
);
assert_eq!(
CompilerWrapperRequest::parse("None").unwrap(),
CompilerWrapperRequest::Disabled
);
assert_eq!(
CompilerWrapperRequest::parse("ccache").unwrap(),
CompilerWrapperRequest::Use {
wrapper: CompilerWrapperKind::Ccache,
}
);
assert_eq!(
CompilerWrapperRequest::parse("sccache").unwrap(),
CompilerWrapperRequest::Use {
wrapper: CompilerWrapperKind::Sccache,
}
);
}
#[test]
fn parse_rejects_unsupported_names_with_clear_error() {
let err = CompilerWrapperRequest::parse("fastcache").unwrap_err();
match err {
CompilerWrapperParseError::Unsupported { raw } => assert_eq!(raw, "fastcache"),
CompilerWrapperParseError::Empty => panic!("expected Unsupported, got Empty"),
}
}
#[test]
fn parse_rejects_paths_today() {
let err = CompilerWrapperRequest::parse("/usr/local/bin/ccache").unwrap_err();
assert!(matches!(err, CompilerWrapperParseError::Unsupported { .. }));
}
#[test]
fn parse_rejects_empty() {
assert_eq!(
CompilerWrapperRequest::parse("").unwrap_err(),
CompilerWrapperParseError::Empty
);
assert_eq!(
CompilerWrapperRequest::parse(" ").unwrap_err(),
CompilerWrapperParseError::Empty
);
}
#[test]
fn as_key_round_trips_through_parse() {
for value in ["none", "ccache", "sccache"] {
let parsed = CompilerWrapperRequest::parse(value).unwrap();
assert_eq!(parsed.as_key(), value);
}
}
#[test]
fn manifest_settings_is_empty_by_default() {
assert!(CompilerWrapperManifestSettings::default().is_empty());
}
#[test]
fn manifest_settings_reports_non_empty_when_general_set() {
let settings = CompilerWrapperManifestSettings {
general: Some(CompilerWrapperRequest::Use {
wrapper: CompilerWrapperKind::Ccache,
}),
..Default::default()
};
assert!(!settings.is_empty());
}
#[test]
fn source_keys_are_stable() {
for (source, key) in [
(CompilerWrapperSource::Cli, "cli"),
(CompilerWrapperSource::Env, "env"),
(
CompilerWrapperSource::ManifestConditional,
"manifest-conditional",
),
(CompilerWrapperSource::Manifest, "manifest"),
] {
assert_eq!(source.as_key(), key);
}
}
#[test]
fn resolved_as_json_includes_kind_spec_source_and_optional_version() {
let resolved = ResolvedCompilerWrapper {
kind: CompilerWrapperKind::Ccache,
path: PathBuf::from("/usr/local/bin/ccache"),
spec: "ccache".into(),
source: CompilerWrapperSource::Cli,
identity: Some(CompilerWrapperIdentity {
kind: CompilerWrapperKind::Ccache,
version: CompilerVersion::parse("4.10.2"),
raw_version_line: "ccache version 4.10.2".into(),
}),
};
let json = resolved.as_json();
assert_eq!(json["kind"], "ccache");
assert_eq!(json["spec"], "ccache");
assert_eq!(json["source"], "cli");
assert_eq!(json["version"], "4.10.2");
assert!(json["raw_version_line"].is_string());
}
#[test]
fn resolved_as_json_emits_null_version_when_missing() {
let resolved = ResolvedCompilerWrapper {
kind: CompilerWrapperKind::Sccache,
path: PathBuf::from("/usr/local/bin/sccache"),
spec: "sccache".into(),
source: CompilerWrapperSource::Manifest,
identity: None,
};
let json = resolved.as_json();
assert_eq!(json["version"], serde_json::Value::Null);
assert_eq!(json["raw_version_line"], serde_json::Value::Null);
}
#[test]
fn summary_from_resolved_keeps_display_version() {
let resolved = ResolvedCompilerWrapper {
kind: CompilerWrapperKind::Ccache,
path: PathBuf::from("/usr/local/bin/ccache"),
spec: "ccache".into(),
source: CompilerWrapperSource::Env,
identity: Some(CompilerWrapperIdentity {
kind: CompilerWrapperKind::Ccache,
version: CompilerVersion::parse("4.10.2"),
raw_version_line: "ccache version 4.10.2".into(),
}),
};
let summary = CompilerWrapperSummary::from_resolved(&resolved);
assert_eq!(summary.kind, "ccache");
assert_eq!(summary.source, "env");
assert_eq!(summary.version.as_deref(), Some("4.10.2"));
}
}