1use std::fmt;
17use std::path::PathBuf;
18
19use serde::{Deserialize, Serialize};
20use thiserror::Error;
21
22use crate::compiler::CompilerVersion;
23use crate::condition::Condition;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
31#[serde(rename_all = "kebab-case")]
32pub enum CompilerWrapperKind {
33 Ccache,
35 Sccache,
37}
38
39impl CompilerWrapperKind {
40 pub const fn as_key(self) -> &'static str {
44 match self {
45 CompilerWrapperKind::Ccache => "ccache",
46 CompilerWrapperKind::Sccache => "sccache",
47 }
48 }
49
50 pub const fn default_command(self) -> &'static str {
56 match self {
57 CompilerWrapperKind::Ccache => "ccache",
58 CompilerWrapperKind::Sccache => "sccache",
59 }
60 }
61
62 pub const fn all() -> &'static [CompilerWrapperKind] {
66 &[CompilerWrapperKind::Ccache, CompilerWrapperKind::Sccache]
67 }
68}
69
70impl fmt::Display for CompilerWrapperKind {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 f.write_str(self.as_key())
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
84#[serde(rename_all = "kebab-case", tag = "kind")]
85pub enum CompilerWrapperRequest {
86 Disabled,
90 Use { wrapper: CompilerWrapperKind },
94}
95
96impl CompilerWrapperRequest {
97 pub fn parse(raw: &str) -> Result<Self, CompilerWrapperParseError> {
115 let trimmed = raw.trim();
116 if trimmed.is_empty() {
117 return Err(CompilerWrapperParseError::Empty);
118 }
119 match trimmed.to_ascii_lowercase().as_str() {
120 "none" | "off" | "disabled" => Ok(Self::Disabled),
121 "ccache" => Ok(Self::Use {
122 wrapper: CompilerWrapperKind::Ccache,
123 }),
124 "sccache" => Ok(Self::Use {
125 wrapper: CompilerWrapperKind::Sccache,
126 }),
127 _ => Err(CompilerWrapperParseError::Unsupported {
128 raw: trimmed.to_owned(),
129 }),
130 }
131 }
132
133 pub const fn as_key(&self) -> &'static str {
135 match self {
136 CompilerWrapperRequest::Disabled => "none",
137 CompilerWrapperRequest::Use {
138 wrapper: CompilerWrapperKind::Ccache,
139 } => "ccache",
140 CompilerWrapperRequest::Use {
141 wrapper: CompilerWrapperKind::Sccache,
142 } => "sccache",
143 }
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Error)]
149pub enum CompilerWrapperParseError {
150 #[error("compiler-wrapper value must not be empty")]
151 Empty,
152 #[error(
153 "compiler-wrapper value `{raw}` is not supported; expected one of: none, ccache, sccache"
154 )]
155 Unsupported { raw: String },
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
162pub struct ConditionalCompilerWrapperDecl {
163 pub condition: Condition,
164 pub request: CompilerWrapperRequest,
165}
166
167#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
176pub struct CompilerWrapperManifestSettings {
177 #[serde(default, skip_serializing_if = "Option::is_none")]
180 pub general: Option<CompilerWrapperRequest>,
181 #[serde(default, skip_serializing_if = "Vec::is_empty")]
184 pub conditional: Vec<ConditionalCompilerWrapperDecl>,
185}
186
187impl CompilerWrapperManifestSettings {
188 pub fn is_empty(&self) -> bool {
193 self.general.is_none() && self.conditional.is_empty()
194 }
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
201#[serde(rename_all = "kebab-case")]
202pub enum CompilerWrapperSource {
203 Cli,
206 Env,
208 UserConfig,
210 WorkspaceConfig,
212 PackageConfig,
215 ExplicitConfig,
218 ManifestConditional,
221 Manifest,
223}
224
225impl CompilerWrapperSource {
226 pub const fn as_key(self) -> &'static str {
229 match self {
230 CompilerWrapperSource::Cli => "cli",
231 CompilerWrapperSource::Env => "env",
232 CompilerWrapperSource::UserConfig => "user-config",
233 CompilerWrapperSource::WorkspaceConfig => "workspace-config",
234 CompilerWrapperSource::PackageConfig => "package-config",
235 CompilerWrapperSource::ExplicitConfig => "explicit-config",
236 CompilerWrapperSource::ManifestConditional => "manifest-conditional",
237 CompilerWrapperSource::Manifest => "manifest",
238 }
239 }
240}
241
242impl fmt::Display for CompilerWrapperSource {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 f.write_str(self.as_key())
245 }
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252pub struct CompilerWrapperIdentity {
253 pub kind: CompilerWrapperKind,
254 #[serde(default, skip_serializing_if = "Option::is_none")]
257 pub version: Option<CompilerVersion>,
258 pub raw_version_line: String,
262}
263
264impl CompilerWrapperIdentity {
265 pub fn unknown_version(kind: CompilerWrapperKind, raw_version_line: impl Into<String>) -> Self {
268 Self {
269 kind,
270 version: None,
271 raw_version_line: raw_version_line.into(),
272 }
273 }
274}
275
276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
284pub struct ResolvedCompilerWrapper {
285 pub kind: CompilerWrapperKind,
286 pub path: PathBuf,
287 pub spec: String,
290 pub source: CompilerWrapperSource,
291 #[serde(default, skip_serializing_if = "Option::is_none")]
295 pub identity: Option<CompilerWrapperIdentity>,
296}
297
298impl ResolvedCompilerWrapper {
299 pub fn as_json(&self) -> serde_json::Value {
303 let version = self
304 .identity
305 .as_ref()
306 .and_then(|id| id.version.as_ref())
307 .map_or(serde_json::Value::Null, |v| {
308 serde_json::Value::String(v.to_display_string())
309 });
310 let raw = self
311 .identity
312 .as_ref()
313 .map_or(serde_json::Value::Null, |id| {
314 serde_json::Value::String(id.raw_version_line.clone())
315 });
316 serde_json::json!({
317 "kind": self.kind.as_key(),
318 "spec": self.spec,
319 "source": self.source.as_key(),
320 "version": version,
321 "raw_version_line": raw,
322 })
323 }
324}
325
326#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
331pub struct CompilerWrapperSummary {
332 pub kind: String,
334 pub spec: String,
336 pub source: String,
338 #[serde(default, skip_serializing_if = "Option::is_none")]
342 pub version: Option<String>,
343}
344
345impl CompilerWrapperSummary {
346 pub fn from_resolved(resolved: &ResolvedCompilerWrapper) -> Self {
348 Self {
349 kind: resolved.kind.as_key().to_owned(),
350 spec: resolved.spec.clone(),
351 source: resolved.source.as_key().to_owned(),
352 version: resolved
353 .identity
354 .as_ref()
355 .and_then(|id| id.version.as_ref())
356 .map(super::compiler::CompilerVersion::to_display_string),
357 }
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 #[test]
366 fn parse_accepts_documented_values() {
367 assert_eq!(
368 CompilerWrapperRequest::parse("none").unwrap(),
369 CompilerWrapperRequest::Disabled
370 );
371 assert_eq!(
372 CompilerWrapperRequest::parse("None").unwrap(),
373 CompilerWrapperRequest::Disabled
374 );
375 assert_eq!(
376 CompilerWrapperRequest::parse("ccache").unwrap(),
377 CompilerWrapperRequest::Use {
378 wrapper: CompilerWrapperKind::Ccache,
379 }
380 );
381 assert_eq!(
382 CompilerWrapperRequest::parse("sccache").unwrap(),
383 CompilerWrapperRequest::Use {
384 wrapper: CompilerWrapperKind::Sccache,
385 }
386 );
387 }
388
389 #[test]
390 fn parse_rejects_unsupported_names_with_clear_error() {
391 let err = CompilerWrapperRequest::parse("fastcache").unwrap_err();
392 match err {
393 CompilerWrapperParseError::Unsupported { raw } => assert_eq!(raw, "fastcache"),
394 CompilerWrapperParseError::Empty => panic!("expected Unsupported, got Empty"),
395 }
396 }
397
398 #[test]
399 fn parse_rejects_paths_today() {
400 let err = CompilerWrapperRequest::parse("/usr/local/bin/ccache").unwrap_err();
404 assert!(matches!(err, CompilerWrapperParseError::Unsupported { .. }));
405 }
406
407 #[test]
408 fn parse_rejects_empty() {
409 assert_eq!(
410 CompilerWrapperRequest::parse("").unwrap_err(),
411 CompilerWrapperParseError::Empty
412 );
413 assert_eq!(
414 CompilerWrapperRequest::parse(" ").unwrap_err(),
415 CompilerWrapperParseError::Empty
416 );
417 }
418
419 #[test]
420 fn as_key_round_trips_through_parse() {
421 for value in ["none", "ccache", "sccache"] {
422 let parsed = CompilerWrapperRequest::parse(value).unwrap();
423 assert_eq!(parsed.as_key(), value);
424 }
425 }
426
427 #[test]
428 fn manifest_settings_is_empty_by_default() {
429 assert!(CompilerWrapperManifestSettings::default().is_empty());
430 }
431
432 #[test]
433 fn manifest_settings_reports_non_empty_when_general_set() {
434 let settings = CompilerWrapperManifestSettings {
435 general: Some(CompilerWrapperRequest::Use {
436 wrapper: CompilerWrapperKind::Ccache,
437 }),
438 ..Default::default()
439 };
440 assert!(!settings.is_empty());
441 }
442
443 #[test]
444 fn source_keys_are_stable() {
445 for (source, key) in [
446 (CompilerWrapperSource::Cli, "cli"),
447 (CompilerWrapperSource::Env, "env"),
448 (
449 CompilerWrapperSource::ManifestConditional,
450 "manifest-conditional",
451 ),
452 (CompilerWrapperSource::Manifest, "manifest"),
453 ] {
454 assert_eq!(source.as_key(), key);
455 }
456 }
457
458 #[test]
459 fn resolved_as_json_includes_kind_spec_source_and_optional_version() {
460 let resolved = ResolvedCompilerWrapper {
461 kind: CompilerWrapperKind::Ccache,
462 path: PathBuf::from("/usr/local/bin/ccache"),
463 spec: "ccache".into(),
464 source: CompilerWrapperSource::Cli,
465 identity: Some(CompilerWrapperIdentity {
466 kind: CompilerWrapperKind::Ccache,
467 version: CompilerVersion::parse("4.10.2"),
468 raw_version_line: "ccache version 4.10.2".into(),
469 }),
470 };
471 let json = resolved.as_json();
472 assert_eq!(json["kind"], "ccache");
473 assert_eq!(json["spec"], "ccache");
474 assert_eq!(json["source"], "cli");
475 assert_eq!(json["version"], "4.10.2");
476 assert!(json["raw_version_line"].is_string());
477 }
478
479 #[test]
480 fn resolved_as_json_emits_null_version_when_missing() {
481 let resolved = ResolvedCompilerWrapper {
482 kind: CompilerWrapperKind::Sccache,
483 path: PathBuf::from("/usr/local/bin/sccache"),
484 spec: "sccache".into(),
485 source: CompilerWrapperSource::Manifest,
486 identity: None,
487 };
488 let json = resolved.as_json();
489 assert_eq!(json["version"], serde_json::Value::Null);
490 assert_eq!(json["raw_version_line"], serde_json::Value::Null);
491 }
492
493 #[test]
494 fn summary_from_resolved_keeps_display_version() {
495 let resolved = ResolvedCompilerWrapper {
496 kind: CompilerWrapperKind::Ccache,
497 path: PathBuf::from("/usr/local/bin/ccache"),
498 spec: "ccache".into(),
499 source: CompilerWrapperSource::Env,
500 identity: Some(CompilerWrapperIdentity {
501 kind: CompilerWrapperKind::Ccache,
502 version: CompilerVersion::parse("4.10.2"),
503 raw_version_line: "ccache version 4.10.2".into(),
504 }),
505 };
506 let summary = CompilerWrapperSummary::from_resolved(&resolved);
507 assert_eq!(summary.kind, "ccache");
508 assert_eq!(summary.source, "env");
509 assert_eq!(summary.version.as_deref(), Some("4.10.2"));
510 }
511}