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