use std::fmt;
use camino::Utf8PathBuf;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::compiler::CompilerVersion;
use crate::toolchain::ToolSpec;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct CompilerWrapperKind(String);
impl CompilerWrapperKind {
pub fn from_spec(spec: &ToolSpec) -> Self {
let display = spec.display();
let basename = camino::Utf8Path::new(&display)
.file_name()
.unwrap_or(&display);
let kind = basename.strip_suffix(".exe").unwrap_or(basename);
Self(kind.to_owned())
}
pub fn as_key(&self) -> &str {
&self.0
}
}
impl fmt::Display for CompilerWrapperKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_key())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", tag = "kind")]
pub enum CompilerWrapperRequest {
Disabled,
Use { wrapper: ToolSpec },
}
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),
_ => Ok(Self::Use {
wrapper: ToolSpec::parse(trimmed.to_owned()),
}),
}
}
pub fn as_key(&self) -> String {
match self {
CompilerWrapperRequest::Disabled => "none".to_owned(),
CompilerWrapperRequest::Use { wrapper } => wrapper.display(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum CompilerWrapperParseError {
#[error("compiler-wrapper value must not be empty")]
Empty,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CompilerWrapperSource {
Cli,
Env,
UserConfig,
WorkspaceConfig,
PackageConfig,
ExplicitConfig,
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::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: Utf8PathBuf,
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, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilerWrapperSummary {
pub kind: CompilerWrapperKind,
pub spec: String,
pub source: CompilerWrapperSource,
#[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.clone(),
spec: resolved.spec.clone(),
source: resolved.source,
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::*;
fn wrapper_kind(name: &str) -> CompilerWrapperKind {
CompilerWrapperKind::from_spec(&ToolSpec::parse(name))
}
#[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: ToolSpec::Name("ccache".into()),
}
);
assert_eq!(
CompilerWrapperRequest::parse("sccache").unwrap(),
CompilerWrapperRequest::Use {
wrapper: ToolSpec::Name("sccache".into()),
}
);
}
#[test]
fn parse_accepts_any_executable_name() {
let request = CompilerWrapperRequest::parse("icecc");
assert!(
request.is_ok(),
"expected arbitrary executable name: {request:?}"
);
}
#[test]
fn parse_does_not_shell_split_executable_value() {
assert_eq!(
CompilerWrapperRequest::parse("wrapper with spaces").unwrap(),
CompilerWrapperRequest::Use {
wrapper: ToolSpec::Name("wrapper with spaces".into()),
}
);
}
#[test]
fn parse_accepts_executable_paths() {
let request = CompilerWrapperRequest::parse("/usr/local/bin/icecc");
assert!(request.is_ok(), "expected executable path: {request:?}");
}
#[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 kind_is_derived_from_executable_basename() {
assert_eq!(wrapper_kind("/opt/bin/icecc").as_key(), "icecc");
assert_eq!(wrapper_kind("sccache.exe").as_key(), "sccache");
}
#[test]
fn source_keys_are_stable() {
for (source, key) in [
(CompilerWrapperSource::Cli, "cli"),
(CompilerWrapperSource::Env, "env"),
(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: wrapper_kind("ccache"),
path: Utf8PathBuf::from("/usr/local/bin/ccache"),
spec: "ccache".into(),
source: CompilerWrapperSource::Cli,
identity: Some(CompilerWrapperIdentity {
kind: wrapper_kind("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: wrapper_kind("sccache"),
path: Utf8PathBuf::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: wrapper_kind("ccache"),
path: Utf8PathBuf::from("/usr/local/bin/ccache"),
spec: "ccache".into(),
source: CompilerWrapperSource::Env,
identity: Some(CompilerWrapperIdentity {
kind: wrapper_kind("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.as_key(), "ccache");
assert_eq!(summary.source, CompilerWrapperSource::Env);
assert_eq!(summary.version.as_deref(), Some("4.10.2"));
}
}