1use std::collections::BTreeMap;
16use std::fmt;
17use std::path::{Path, PathBuf};
18
19use serde::{Deserialize, Serialize};
20use thiserror::Error;
21
22use crate::condition::Condition;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
26#[serde(rename_all = "kebab-case")]
27pub enum ToolKind {
28 CCompiler,
30 CxxCompiler,
33 Archiver,
35}
36
37impl ToolKind {
38 pub fn as_key(self) -> &'static str {
41 match self {
42 ToolKind::CCompiler => "cc",
43 ToolKind::CxxCompiler => "cxx",
44 ToolKind::Archiver => "ar",
45 }
46 }
47
48 pub fn human_label(self) -> &'static str {
51 match self {
52 ToolKind::CCompiler => "C compiler",
53 ToolKind::CxxCompiler => "C++ compiler",
54 ToolKind::Archiver => "archiver",
55 }
56 }
57}
58
59impl fmt::Display for ToolKind {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 f.write_str(self.as_key())
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "kebab-case")]
70pub enum ToolSource {
71 Cli,
73 Env,
75 UserConfig,
77 WorkspaceConfig,
79 PackageConfig,
82 ExplicitConfig,
85 ManifestConditional,
88 Manifest,
90 Default,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100#[serde(untagged)]
101pub enum ToolSpec {
102 Path(PathBuf),
106 Name(String),
108}
109
110impl ToolSpec {
111 pub fn parse(raw: impl Into<String>) -> Self {
116 let raw = raw.into();
117 if looks_like_path(&raw) {
118 ToolSpec::Path(PathBuf::from(raw))
119 } else {
120 ToolSpec::Name(raw)
121 }
122 }
123
124 pub fn parse_non_empty(raw: &str) -> Option<ToolSpec> {
130 let trimmed = raw.trim();
131 if trimmed.is_empty() {
132 None
133 } else {
134 Some(ToolSpec::parse(trimmed.to_owned()))
135 }
136 }
137
138 pub fn display(&self) -> String {
140 match self {
141 ToolSpec::Path(p) => p.display().to_string(),
142 ToolSpec::Name(n) => n.clone(),
143 }
144 }
145
146 pub fn as_path(&self) -> &Path {
149 match self {
150 ToolSpec::Path(p) => p.as_path(),
151 ToolSpec::Name(n) => Path::new(n),
152 }
153 }
154}
155
156fn looks_like_path(raw: &str) -> bool {
157 if raw.contains('/') {
158 return true;
159 }
160 if cfg!(windows) && raw.contains('\\') {
161 return true;
162 }
163 false
164}
165
166#[derive(Debug, Clone, Default, PartialEq, Eq)]
172pub struct ToolSelection {
173 pub cli: Option<ToolSpec>,
176}
177
178#[derive(Debug, Clone, Default, PartialEq, Eq)]
180pub struct ToolchainSelection {
181 pub cc: ToolSelection,
182 pub cxx: ToolSelection,
183 pub ar: ToolSelection,
184}
185
186impl ToolchainSelection {
187 pub fn empty() -> Self {
189 Self::default()
190 }
191
192 pub fn with_cli(mut self, kind: ToolKind, spec: ToolSpec) -> Self {
194 let slot = match kind {
195 ToolKind::CCompiler => &mut self.cc,
196 ToolKind::CxxCompiler => &mut self.cxx,
197 ToolKind::Archiver => &mut self.ar,
198 };
199 slot.cli = Some(spec);
200 self
201 }
202}
203
204#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
211pub struct ToolchainDecl {
212 #[serde(default, skip_serializing_if = "Option::is_none")]
213 pub cc: Option<ToolSpec>,
214 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub cxx: Option<ToolSpec>,
216 #[serde(default, skip_serializing_if = "Option::is_none")]
217 pub ar: Option<ToolSpec>,
218}
219
220impl ToolchainDecl {
221 pub fn is_empty(&self) -> bool {
224 self.cc.is_none() && self.cxx.is_none() && self.ar.is_none()
225 }
226
227 pub fn get(&self, kind: ToolKind) -> Option<&ToolSpec> {
229 match kind {
230 ToolKind::CCompiler => self.cc.as_ref(),
231 ToolKind::CxxCompiler => self.cxx.as_ref(),
232 ToolKind::Archiver => self.ar.as_ref(),
233 }
234 }
235}
236
237#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
239pub struct ConditionalToolchainDecl {
240 pub condition: Condition,
241 #[serde(default, skip_serializing_if = "ToolchainDecl::is_empty", flatten)]
242 pub toolchain: ToolchainDecl,
243}
244
245#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
249pub struct ToolchainSettings {
250 #[serde(default, skip_serializing_if = "ToolchainDecl::is_empty")]
251 pub general: ToolchainDecl,
252 #[serde(default, skip_serializing_if = "Vec::is_empty")]
253 pub conditional: Vec<ConditionalToolchainDecl>,
254}
255
256impl ToolchainSettings {
257 pub fn is_empty(&self) -> bool {
259 self.general.is_empty() && self.conditional.is_empty()
260 }
261}
262
263#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
265pub struct ResolvedTool {
266 pub kind: ToolKind,
267 pub path: PathBuf,
271 pub spec: ToolSpec,
275 pub source: ToolSource,
277}
278
279impl ResolvedTool {
280 pub fn path(&self) -> &Path {
283 &self.path
284 }
285
286 pub fn as_json(&self) -> serde_json::Value {
290 serde_json::json!({
291 "kind": self.kind.as_key(),
292 "spec": self.spec.display(),
293 "source": tool_source_label(self.source),
294 })
295 }
296}
297
298pub(crate) fn tool_source_label(source: ToolSource) -> &'static str {
303 match source {
304 ToolSource::Cli => "cli",
305 ToolSource::Env => "env",
306 ToolSource::UserConfig => "user-config",
307 ToolSource::WorkspaceConfig => "workspace-config",
308 ToolSource::PackageConfig => "package-config",
309 ToolSource::ExplicitConfig => "explicit-config",
310 ToolSource::ManifestConditional => "manifest-conditional",
311 ToolSource::Manifest => "manifest",
312 ToolSource::Default => "default",
313 }
314}
315
316#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
323pub struct ResolvedToolchain {
324 pub cxx: ResolvedTool,
328 pub ar: ResolvedTool,
330 pub cc: Option<ResolvedTool>,
338}
339
340impl ResolvedToolchain {
341 pub fn iter(&self) -> impl Iterator<Item = &ResolvedTool> {
343 let mut entries: Vec<&ResolvedTool> = Vec::with_capacity(3);
344 if let Some(cc) = &self.cc {
345 entries.push(cc);
346 }
347 entries.push(&self.cxx);
348 entries.push(&self.ar);
349 entries.sort_by_key(|t| t.kind);
350 entries.into_iter()
351 }
352
353 pub fn as_json(&self) -> serde_json::Value {
356 let entries: BTreeMap<String, serde_json::Value> = self
357 .iter()
358 .map(|t| (t.kind.as_key().to_owned(), t.as_json()))
359 .collect();
360 serde_json::Value::Object(entries.into_iter().collect())
361 }
362}
363
364#[derive(Debug, Error, Clone, PartialEq, Eq)]
366pub enum ToolchainResolutionError {
367 #[error(
370 "{label} `{spec}` was requested by {source_label} but could not be found",
371 label = kind.human_label(),
372 source_label = source_label(*selected_from)
373 )]
374 ToolNotFound {
375 kind: ToolKind,
376 spec: String,
377 selected_from: ToolSource,
378 },
379 #[error("no usable {label} found on PATH; set {env_var} or add `{key} = ...` under [toolchain]",
382 label = kind.human_label(),
383 env_var = env_var_for(*kind),
384 key = kind.as_key()
385 )]
386 NoDefault { kind: ToolKind },
387 #[error(
390 "selected {label} `{spec}` is not supported by the current C++ backend; use a GCC- or Clang-like compiler driver",
391 label = kind.human_label()
392 )]
393 UnsupportedCompiler { kind: ToolKind, spec: String },
394}
395
396fn env_var_for(kind: ToolKind) -> &'static str {
397 match kind {
398 ToolKind::CCompiler => "CC",
399 ToolKind::CxxCompiler => "CXX",
400 ToolKind::Archiver => "AR",
401 }
402}
403
404fn source_label(source: ToolSource) -> &'static str {
405 match source {
406 ToolSource::Cli => "--cli",
407 ToolSource::Env => "an environment variable",
408 ToolSource::UserConfig => "the user `[toolchain]` config table",
409 ToolSource::WorkspaceConfig => "the workspace `[toolchain]` config table",
410 ToolSource::PackageConfig => "the package `[toolchain]` config table",
411 ToolSource::ExplicitConfig => "the `CABIN_CONFIG` `[toolchain]` table",
412 ToolSource::ManifestConditional => "[target.'cfg(...)'.toolchain]",
413 ToolSource::Manifest => "[toolchain]",
414 ToolSource::Default => "the built-in default list",
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 #[test]
423 fn tool_kind_keys_are_stable() {
424 assert_eq!(ToolKind::CCompiler.as_key(), "cc");
425 assert_eq!(ToolKind::CxxCompiler.as_key(), "cxx");
426 assert_eq!(ToolKind::Archiver.as_key(), "ar");
427 }
428
429 #[test]
430 fn tool_spec_parse_distinguishes_paths_and_names() {
431 match ToolSpec::parse("clang++") {
432 ToolSpec::Name(n) => assert_eq!(n, "clang++"),
433 ToolSpec::Path(p) => panic!("expected name, got {p:?}"),
434 }
435 match ToolSpec::parse("/usr/bin/clang++") {
436 ToolSpec::Path(p) => assert_eq!(p, PathBuf::from("/usr/bin/clang++")),
437 ToolSpec::Name(n) => panic!("expected path, got {n:?}"),
438 }
439 match ToolSpec::parse("./bin/clang++") {
440 ToolSpec::Path(p) => assert_eq!(p, PathBuf::from("./bin/clang++")),
441 ToolSpec::Name(n) => panic!("expected path, got {n:?}"),
442 }
443 }
444
445 #[test]
446 fn toolchain_decl_is_empty_when_unset() {
447 assert!(ToolchainDecl::default().is_empty());
448 let d = ToolchainDecl {
449 cxx: Some(ToolSpec::Name("clang++".into())),
450 ..Default::default()
451 };
452 assert!(!d.is_empty());
453 assert_eq!(
454 d.get(ToolKind::CxxCompiler).map(ToolSpec::display),
455 Some("clang++".to_owned())
456 );
457 assert!(d.get(ToolKind::CCompiler).is_none());
458 }
459
460 #[test]
461 fn resolved_toolchain_iter_is_sorted_and_skips_missing_cc() {
462 let cxx = ResolvedTool {
463 kind: ToolKind::CxxCompiler,
464 path: PathBuf::from("/usr/bin/c++"),
465 spec: ToolSpec::Name("c++".into()),
466 source: ToolSource::Default,
467 };
468 let ar = ResolvedTool {
469 kind: ToolKind::Archiver,
470 path: PathBuf::from("/usr/bin/ar"),
471 spec: ToolSpec::Name("ar".into()),
472 source: ToolSource::Default,
473 };
474 let resolved = ResolvedToolchain { cxx, ar, cc: None };
475 let kinds: Vec<ToolKind> = resolved.iter().map(|t| t.kind).collect();
476 assert_eq!(kinds, vec![ToolKind::CxxCompiler, ToolKind::Archiver]);
477 }
478}