1use std::collections::BTreeMap;
16use std::fmt;
17use std::path::PathBuf;
18
19use camino::{Utf8Path, Utf8PathBuf};
20
21use serde::{Deserialize, Serialize};
22use thiserror::Error;
23
24use crate::condition::Condition;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
28#[serde(rename_all = "kebab-case")]
29pub enum ToolKind {
30 CCompiler,
32 CxxCompiler,
35 Archiver,
37}
38
39impl ToolKind {
40 pub fn as_key(self) -> &'static str {
43 match self {
44 ToolKind::CCompiler => "cc",
45 ToolKind::CxxCompiler => "cxx",
46 ToolKind::Archiver => "ar",
47 }
48 }
49
50 pub fn human_label(self) -> &'static str {
53 match self {
54 ToolKind::CCompiler => "C compiler",
55 ToolKind::CxxCompiler => "C++ compiler",
56 ToolKind::Archiver => "archiver",
57 }
58 }
59}
60
61impl fmt::Display for ToolKind {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 f.write_str(self.as_key())
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
71#[serde(rename_all = "kebab-case")]
72pub enum ToolSource {
73 Cli,
75 Env,
77 UserConfig,
79 WorkspaceConfig,
81 PackageConfig,
84 ExplicitConfig,
87 ManifestConditional,
90 Manifest,
92 Default,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102#[serde(untagged)]
103pub enum ToolSpec {
104 Path(Utf8PathBuf),
108 Name(String),
110}
111
112impl ToolSpec {
113 pub fn parse(raw: impl Into<String>) -> Self {
118 let raw = raw.into();
119 if looks_like_path(&raw) {
120 ToolSpec::Path(Utf8PathBuf::from(raw))
121 } else {
122 ToolSpec::Name(raw)
123 }
124 }
125
126 pub fn parse_non_empty(raw: &str) -> Option<ToolSpec> {
132 let trimmed = raw.trim();
133 if trimmed.is_empty() {
134 None
135 } else {
136 Some(ToolSpec::parse(trimmed.to_owned()))
137 }
138 }
139
140 pub fn display(&self) -> String {
142 match self {
143 ToolSpec::Path(p) => p.as_str().to_owned(),
144 ToolSpec::Name(n) => n.clone(),
145 }
146 }
147
148 pub fn as_path(&self) -> &Utf8Path {
151 match self {
152 ToolSpec::Path(p) => p.as_path(),
153 ToolSpec::Name(n) => Utf8Path::new(n),
154 }
155 }
156}
157
158fn looks_like_path(raw: &str) -> bool {
159 if raw.contains('/') {
160 return true;
161 }
162 if cfg!(windows) && raw.contains('\\') {
163 return true;
164 }
165 false
166}
167
168#[derive(Debug, Clone, Default, PartialEq, Eq)]
174pub struct ToolSelection {
175 pub cli: Option<ToolSpec>,
178}
179
180#[derive(Debug, Clone, Default, PartialEq, Eq)]
182pub struct ToolchainSelection {
183 pub cc: ToolSelection,
184 pub cxx: ToolSelection,
185 pub ar: ToolSelection,
186}
187
188impl ToolchainSelection {
189 pub fn empty() -> Self {
191 Self::default()
192 }
193
194 #[must_use]
196 pub fn with_cli(mut self, kind: ToolKind, spec: ToolSpec) -> Self {
197 let slot = match kind {
198 ToolKind::CCompiler => &mut self.cc,
199 ToolKind::CxxCompiler => &mut self.cxx,
200 ToolKind::Archiver => &mut self.ar,
201 };
202 slot.cli = Some(spec);
203 self
204 }
205}
206
207#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
214pub struct ToolchainDecl {
215 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub cc: Option<ToolSpec>,
217 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub cxx: Option<ToolSpec>,
219 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub ar: Option<ToolSpec>,
221}
222
223impl ToolchainDecl {
224 pub fn is_empty(&self) -> bool {
227 self.cc.is_none() && self.cxx.is_none() && self.ar.is_none()
228 }
229
230 pub fn get(&self, kind: ToolKind) -> Option<&ToolSpec> {
232 match kind {
233 ToolKind::CCompiler => self.cc.as_ref(),
234 ToolKind::CxxCompiler => self.cxx.as_ref(),
235 ToolKind::Archiver => self.ar.as_ref(),
236 }
237 }
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242pub struct ConditionalToolchainDecl {
243 pub condition: Condition,
244 #[serde(default, skip_serializing_if = "ToolchainDecl::is_empty", flatten)]
245 pub toolchain: ToolchainDecl,
246}
247
248#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
252pub struct ToolchainSettings {
253 #[serde(default, skip_serializing_if = "ToolchainDecl::is_empty")]
254 pub general: ToolchainDecl,
255 #[serde(default, skip_serializing_if = "Vec::is_empty")]
256 pub conditional: Vec<ConditionalToolchainDecl>,
257}
258
259impl ToolchainSettings {
260 pub fn is_empty(&self) -> bool {
262 self.general.is_empty() && self.conditional.is_empty()
263 }
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
268pub struct ResolvedTool {
269 pub kind: ToolKind,
270 pub path: Utf8PathBuf,
274 pub spec: ToolSpec,
278 pub source: ToolSource,
280}
281
282impl ResolvedTool {
283 pub fn path(&self) -> &Utf8Path {
286 &self.path
287 }
288
289 pub fn as_json(&self) -> serde_json::Value {
293 serde_json::json!({
294 "kind": self.kind.as_key(),
295 "spec": self.spec.display(),
296 "source": tool_source_label(self.source),
297 })
298 }
299}
300
301pub(crate) fn tool_source_label(source: ToolSource) -> &'static str {
306 match source {
307 ToolSource::Cli => "cli",
308 ToolSource::Env => "env",
309 ToolSource::UserConfig => "user-config",
310 ToolSource::WorkspaceConfig => "workspace-config",
311 ToolSource::PackageConfig => "package-config",
312 ToolSource::ExplicitConfig => "explicit-config",
313 ToolSource::ManifestConditional => "manifest-conditional",
314 ToolSource::Manifest => "manifest",
315 ToolSource::Default => "default",
316 }
317}
318
319#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
326pub struct ResolvedToolchain {
327 pub cxx: ResolvedTool,
331 pub ar: ResolvedTool,
333 pub cc: Option<ResolvedTool>,
341}
342
343impl ResolvedToolchain {
344 pub fn iter(&self) -> impl Iterator<Item = &ResolvedTool> {
346 let mut entries: Vec<&ResolvedTool> = Vec::with_capacity(3);
347 if let Some(cc) = &self.cc {
348 entries.push(cc);
349 }
350 entries.push(&self.cxx);
351 entries.push(&self.ar);
352 entries.sort_by_key(|t| t.kind);
353 entries.into_iter()
354 }
355
356 pub fn as_json(&self) -> serde_json::Value {
359 let entries: BTreeMap<String, serde_json::Value> = self
360 .iter()
361 .map(|t| (t.kind.as_key().to_owned(), t.as_json()))
362 .collect();
363 serde_json::Value::Object(entries.into_iter().collect())
364 }
365}
366
367#[derive(Debug, Error, Clone, PartialEq, Eq)]
369pub enum ToolchainResolutionError {
370 #[error(
373 "{label} `{spec}` was requested by {source_label} but could not be found",
374 label = kind.human_label(),
375 source_label = source_label(*selected_from)
376 )]
377 ToolNotFound {
378 kind: ToolKind,
379 spec: String,
380 selected_from: ToolSource,
381 },
382 #[error("no usable {label} found on PATH; set {env_var} or add `{key} = ...` under [toolchain]",
385 label = kind.human_label(),
386 env_var = env_var_for(*kind),
387 key = kind.as_key()
388 )]
389 NoDefault { kind: ToolKind },
390 #[error(
393 "selected {label} `{spec}` is not supported by the current C++ backend; use a GCC- or Clang-like compiler driver",
394 label = kind.human_label()
395 )]
396 UnsupportedCompiler { kind: ToolKind, spec: String },
397 #[error(
402 "resolved {label} path `{path}` is not valid UTF-8",
403 label = kind.human_label(),
404 path = path.display(),
405 )]
406 NonUtf8Path { kind: ToolKind, path: PathBuf },
407}
408
409fn env_var_for(kind: ToolKind) -> &'static str {
410 match kind {
411 ToolKind::CCompiler => "CC",
412 ToolKind::CxxCompiler => "CXX",
413 ToolKind::Archiver => "AR",
414 }
415}
416
417fn source_label(source: ToolSource) -> &'static str {
418 match source {
419 ToolSource::Cli => "--cli",
420 ToolSource::Env => "an environment variable",
421 ToolSource::UserConfig => "the user `[toolchain]` config table",
422 ToolSource::WorkspaceConfig => "the workspace `[toolchain]` config table",
423 ToolSource::PackageConfig => "the package `[toolchain]` config table",
424 ToolSource::ExplicitConfig => "the `CABIN_CONFIG` `[toolchain]` table",
425 ToolSource::ManifestConditional => "[target.'cfg(...)'.toolchain]",
426 ToolSource::Manifest => "[toolchain]",
427 ToolSource::Default => "the built-in default list",
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434
435 #[test]
436 fn tool_kind_keys_are_stable() {
437 assert_eq!(ToolKind::CCompiler.as_key(), "cc");
438 assert_eq!(ToolKind::CxxCompiler.as_key(), "cxx");
439 assert_eq!(ToolKind::Archiver.as_key(), "ar");
440 }
441
442 #[test]
443 fn tool_spec_parse_distinguishes_paths_and_names() {
444 match ToolSpec::parse("clang++") {
445 ToolSpec::Name(n) => assert_eq!(n, "clang++"),
446 ToolSpec::Path(p) => panic!("expected name, got {p:?}"),
447 }
448 match ToolSpec::parse("/usr/bin/clang++") {
449 ToolSpec::Path(p) => assert_eq!(p, Utf8PathBuf::from("/usr/bin/clang++")),
450 ToolSpec::Name(n) => panic!("expected path, got {n:?}"),
451 }
452 match ToolSpec::parse("./bin/clang++") {
453 ToolSpec::Path(p) => assert_eq!(p, Utf8PathBuf::from("./bin/clang++")),
454 ToolSpec::Name(n) => panic!("expected path, got {n:?}"),
455 }
456 }
457
458 #[test]
459 fn toolchain_decl_is_empty_when_unset() {
460 assert!(ToolchainDecl::default().is_empty());
461 let d = ToolchainDecl {
462 cxx: Some(ToolSpec::Name("clang++".into())),
463 ..Default::default()
464 };
465 assert!(!d.is_empty());
466 assert_eq!(
467 d.get(ToolKind::CxxCompiler).map(ToolSpec::display),
468 Some("clang++".to_owned())
469 );
470 assert!(d.get(ToolKind::CCompiler).is_none());
471 }
472
473 #[test]
474 fn resolved_toolchain_iter_is_sorted_and_skips_missing_cc() {
475 let cxx = ResolvedTool {
476 kind: ToolKind::CxxCompiler,
477 path: Utf8PathBuf::from("/usr/bin/c++"),
478 spec: ToolSpec::Name("c++".into()),
479 source: ToolSource::Default,
480 };
481 let ar = ResolvedTool {
482 kind: ToolKind::Archiver,
483 path: Utf8PathBuf::from("/usr/bin/ar"),
484 spec: ToolSpec::Name("ar".into()),
485 source: ToolSource::Default,
486 };
487 let resolved = ResolvedToolchain { cxx, ar, cc: None };
488 let kinds: Vec<ToolKind> = resolved.iter().map(|t| t.kind).collect();
489 assert_eq!(kinds, vec![ToolKind::CxxCompiler, ToolKind::Archiver]);
490 }
491}