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 pub fn with_cli(mut self, kind: ToolKind, spec: ToolSpec) -> Self {
196 let slot = match kind {
197 ToolKind::CCompiler => &mut self.cc,
198 ToolKind::CxxCompiler => &mut self.cxx,
199 ToolKind::Archiver => &mut self.ar,
200 };
201 slot.cli = Some(spec);
202 self
203 }
204}
205
206#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
213pub struct ToolchainDecl {
214 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub cc: Option<ToolSpec>,
216 #[serde(default, skip_serializing_if = "Option::is_none")]
217 pub cxx: Option<ToolSpec>,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub ar: Option<ToolSpec>,
220}
221
222impl ToolchainDecl {
223 pub fn is_empty(&self) -> bool {
226 self.cc.is_none() && self.cxx.is_none() && self.ar.is_none()
227 }
228
229 pub fn get(&self, kind: ToolKind) -> Option<&ToolSpec> {
231 match kind {
232 ToolKind::CCompiler => self.cc.as_ref(),
233 ToolKind::CxxCompiler => self.cxx.as_ref(),
234 ToolKind::Archiver => self.ar.as_ref(),
235 }
236 }
237}
238
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
241pub struct ConditionalToolchainDecl {
242 pub condition: Condition,
243 #[serde(default, skip_serializing_if = "ToolchainDecl::is_empty", flatten)]
244 pub toolchain: ToolchainDecl,
245}
246
247#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
251pub struct ToolchainSettings {
252 #[serde(default, skip_serializing_if = "ToolchainDecl::is_empty")]
253 pub general: ToolchainDecl,
254 #[serde(default, skip_serializing_if = "Vec::is_empty")]
255 pub conditional: Vec<ConditionalToolchainDecl>,
256}
257
258impl ToolchainSettings {
259 pub fn is_empty(&self) -> bool {
261 self.general.is_empty() && self.conditional.is_empty()
262 }
263}
264
265#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
267pub struct ResolvedTool {
268 pub kind: ToolKind,
269 pub path: Utf8PathBuf,
273 pub spec: ToolSpec,
277 pub source: ToolSource,
279}
280
281impl ResolvedTool {
282 pub fn path(&self) -> &Utf8Path {
285 &self.path
286 }
287
288 pub fn as_json(&self) -> serde_json::Value {
292 serde_json::json!({
293 "kind": self.kind.as_key(),
294 "spec": self.spec.display(),
295 "source": tool_source_label(self.source),
296 })
297 }
298}
299
300pub(crate) fn tool_source_label(source: ToolSource) -> &'static str {
305 match source {
306 ToolSource::Cli => "cli",
307 ToolSource::Env => "env",
308 ToolSource::UserConfig => "user-config",
309 ToolSource::WorkspaceConfig => "workspace-config",
310 ToolSource::PackageConfig => "package-config",
311 ToolSource::ExplicitConfig => "explicit-config",
312 ToolSource::ManifestConditional => "manifest-conditional",
313 ToolSource::Manifest => "manifest",
314 ToolSource::Default => "default",
315 }
316}
317
318#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
325pub struct ResolvedToolchain {
326 pub cxx: ResolvedTool,
330 pub ar: ResolvedTool,
332 pub cc: Option<ResolvedTool>,
340}
341
342impl ResolvedToolchain {
343 pub fn iter(&self) -> impl Iterator<Item = &ResolvedTool> {
345 let mut entries: Vec<&ResolvedTool> = Vec::with_capacity(3);
346 if let Some(cc) = &self.cc {
347 entries.push(cc);
348 }
349 entries.push(&self.cxx);
350 entries.push(&self.ar);
351 entries.sort_by_key(|t| t.kind);
352 entries.into_iter()
353 }
354
355 pub fn as_json(&self) -> serde_json::Value {
358 let entries: BTreeMap<String, serde_json::Value> = self
359 .iter()
360 .map(|t| (t.kind.as_key().to_owned(), t.as_json()))
361 .collect();
362 serde_json::Value::Object(entries.into_iter().collect())
363 }
364}
365
366#[derive(Debug, Error, Clone, PartialEq, Eq)]
368pub enum ToolchainResolutionError {
369 #[error(
372 "{label} `{spec}` was requested by {source_label} but could not be found",
373 label = kind.human_label(),
374 source_label = source_label(*selected_from)
375 )]
376 ToolNotFound {
377 kind: ToolKind,
378 spec: String,
379 selected_from: ToolSource,
380 },
381 #[error("no usable {label} found on PATH; set {env_var} or add `{key} = ...` under [toolchain]",
384 label = kind.human_label(),
385 env_var = env_var_for(*kind),
386 key = kind.as_key()
387 )]
388 NoDefault { kind: ToolKind },
389 #[error(
392 "selected {label} `{spec}` is not supported by the current C++ backend; use a GCC- or Clang-like compiler driver",
393 label = kind.human_label()
394 )]
395 UnsupportedCompiler { kind: ToolKind, spec: String },
396 #[error(
401 "resolved {label} path `{path}` is not valid UTF-8",
402 label = kind.human_label(),
403 path = path.display(),
404 )]
405 NonUtf8Path { kind: ToolKind, path: PathBuf },
406}
407
408fn env_var_for(kind: ToolKind) -> &'static str {
409 match kind {
410 ToolKind::CCompiler => "CC",
411 ToolKind::CxxCompiler => "CXX",
412 ToolKind::Archiver => "AR",
413 }
414}
415
416fn source_label(source: ToolSource) -> &'static str {
417 match source {
418 ToolSource::Cli => "--cli",
419 ToolSource::Env => "an environment variable",
420 ToolSource::UserConfig => "the user `[toolchain]` config table",
421 ToolSource::WorkspaceConfig => "the workspace `[toolchain]` config table",
422 ToolSource::PackageConfig => "the package `[toolchain]` config table",
423 ToolSource::ExplicitConfig => "the `CABIN_CONFIG` `[toolchain]` table",
424 ToolSource::ManifestConditional => "[target.'cfg(...)'.toolchain]",
425 ToolSource::Manifest => "[toolchain]",
426 ToolSource::Default => "the built-in default list",
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433
434 #[test]
435 fn tool_kind_keys_are_stable() {
436 assert_eq!(ToolKind::CCompiler.as_key(), "cc");
437 assert_eq!(ToolKind::CxxCompiler.as_key(), "cxx");
438 assert_eq!(ToolKind::Archiver.as_key(), "ar");
439 }
440
441 #[test]
442 fn tool_spec_parse_distinguishes_paths_and_names() {
443 match ToolSpec::parse("clang++") {
444 ToolSpec::Name(n) => assert_eq!(n, "clang++"),
445 ToolSpec::Path(p) => panic!("expected name, got {p:?}"),
446 }
447 match ToolSpec::parse("/usr/bin/clang++") {
448 ToolSpec::Path(p) => assert_eq!(p, Utf8PathBuf::from("/usr/bin/clang++")),
449 ToolSpec::Name(n) => panic!("expected path, got {n:?}"),
450 }
451 match ToolSpec::parse("./bin/clang++") {
452 ToolSpec::Path(p) => assert_eq!(p, Utf8PathBuf::from("./bin/clang++")),
453 ToolSpec::Name(n) => panic!("expected path, got {n:?}"),
454 }
455 }
456
457 #[test]
458 fn toolchain_decl_is_empty_when_unset() {
459 assert!(ToolchainDecl::default().is_empty());
460 let d = ToolchainDecl {
461 cxx: Some(ToolSpec::Name("clang++".into())),
462 ..Default::default()
463 };
464 assert!(!d.is_empty());
465 assert_eq!(
466 d.get(ToolKind::CxxCompiler).map(ToolSpec::display),
467 Some("clang++".to_owned())
468 );
469 assert!(d.get(ToolKind::CCompiler).is_none());
470 }
471
472 #[test]
473 fn resolved_toolchain_iter_is_sorted_and_skips_missing_cc() {
474 let cxx = ResolvedTool {
475 kind: ToolKind::CxxCompiler,
476 path: Utf8PathBuf::from("/usr/bin/c++"),
477 spec: ToolSpec::Name("c++".into()),
478 source: ToolSource::Default,
479 };
480 let ar = ResolvedTool {
481 kind: ToolKind::Archiver,
482 path: Utf8PathBuf::from("/usr/bin/ar"),
483 spec: ToolSpec::Name("ar".into()),
484 source: ToolSource::Default,
485 };
486 let resolved = ResolvedToolchain { cxx, ar, cc: None };
487 let kinds: Vec<ToolKind> = resolved.iter().map(|t| t.kind).collect();
488 assert_eq!(kinds, vec![ToolKind::CxxCompiler, ToolKind::Archiver]);
489 }
490}