use std::collections::BTreeMap;
use std::fmt;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::condition::Condition;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ToolKind {
CCompiler,
CxxCompiler,
Archiver,
}
impl ToolKind {
pub fn as_key(self) -> &'static str {
match self {
ToolKind::CCompiler => "cc",
ToolKind::CxxCompiler => "cxx",
ToolKind::Archiver => "ar",
}
}
pub fn human_label(self) -> &'static str {
match self {
ToolKind::CCompiler => "C compiler",
ToolKind::CxxCompiler => "C++ compiler",
ToolKind::Archiver => "archiver",
}
}
}
impl fmt::Display for ToolKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_key())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ToolSource {
Cli,
Env,
UserConfig,
WorkspaceConfig,
PackageConfig,
ExplicitConfig,
ManifestConditional,
Manifest,
Default,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ToolSpec {
Path(PathBuf),
Name(String),
}
impl ToolSpec {
pub fn parse(raw: impl Into<String>) -> Self {
let raw = raw.into();
if looks_like_path(&raw) {
ToolSpec::Path(PathBuf::from(raw))
} else {
ToolSpec::Name(raw)
}
}
pub fn parse_non_empty(raw: &str) -> Option<ToolSpec> {
let trimmed = raw.trim();
if trimmed.is_empty() {
None
} else {
Some(ToolSpec::parse(trimmed.to_owned()))
}
}
pub fn display(&self) -> String {
match self {
ToolSpec::Path(p) => p.display().to_string(),
ToolSpec::Name(n) => n.clone(),
}
}
pub fn as_path(&self) -> &Path {
match self {
ToolSpec::Path(p) => p.as_path(),
ToolSpec::Name(n) => Path::new(n),
}
}
}
fn looks_like_path(raw: &str) -> bool {
if raw.contains('/') {
return true;
}
if cfg!(windows) && raw.contains('\\') {
return true;
}
false
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ToolSelection {
pub cli: Option<ToolSpec>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ToolchainSelection {
pub cc: ToolSelection,
pub cxx: ToolSelection,
pub ar: ToolSelection,
}
impl ToolchainSelection {
pub fn empty() -> Self {
Self::default()
}
pub fn with_cli(mut self, kind: ToolKind, spec: ToolSpec) -> Self {
let slot = match kind {
ToolKind::CCompiler => &mut self.cc,
ToolKind::CxxCompiler => &mut self.cxx,
ToolKind::Archiver => &mut self.ar,
};
slot.cli = Some(spec);
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolchainDecl {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cc: Option<ToolSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cxx: Option<ToolSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ar: Option<ToolSpec>,
}
impl ToolchainDecl {
pub fn is_empty(&self) -> bool {
self.cc.is_none() && self.cxx.is_none() && self.ar.is_none()
}
pub fn get(&self, kind: ToolKind) -> Option<&ToolSpec> {
match kind {
ToolKind::CCompiler => self.cc.as_ref(),
ToolKind::CxxCompiler => self.cxx.as_ref(),
ToolKind::Archiver => self.ar.as_ref(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConditionalToolchainDecl {
pub condition: Condition,
#[serde(default, skip_serializing_if = "ToolchainDecl::is_empty", flatten)]
pub toolchain: ToolchainDecl,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolchainSettings {
#[serde(default, skip_serializing_if = "ToolchainDecl::is_empty")]
pub general: ToolchainDecl,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub conditional: Vec<ConditionalToolchainDecl>,
}
impl ToolchainSettings {
pub fn is_empty(&self) -> bool {
self.general.is_empty() && self.conditional.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolvedTool {
pub kind: ToolKind,
pub path: PathBuf,
pub spec: ToolSpec,
pub source: ToolSource,
}
impl ResolvedTool {
pub fn path(&self) -> &Path {
&self.path
}
pub fn as_json(&self) -> serde_json::Value {
serde_json::json!({
"kind": self.kind.as_key(),
"spec": self.spec.display(),
"source": tool_source_label(self.source),
})
}
}
pub(crate) fn tool_source_label(source: ToolSource) -> &'static str {
match source {
ToolSource::Cli => "cli",
ToolSource::Env => "env",
ToolSource::UserConfig => "user-config",
ToolSource::WorkspaceConfig => "workspace-config",
ToolSource::PackageConfig => "package-config",
ToolSource::ExplicitConfig => "explicit-config",
ToolSource::ManifestConditional => "manifest-conditional",
ToolSource::Manifest => "manifest",
ToolSource::Default => "default",
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolvedToolchain {
pub cxx: ResolvedTool,
pub ar: ResolvedTool,
pub cc: Option<ResolvedTool>,
}
impl ResolvedToolchain {
pub fn iter(&self) -> impl Iterator<Item = &ResolvedTool> {
let mut entries: Vec<&ResolvedTool> = Vec::with_capacity(3);
if let Some(cc) = &self.cc {
entries.push(cc);
}
entries.push(&self.cxx);
entries.push(&self.ar);
entries.sort_by_key(|t| t.kind);
entries.into_iter()
}
pub fn as_json(&self) -> serde_json::Value {
let entries: BTreeMap<String, serde_json::Value> = self
.iter()
.map(|t| (t.kind.as_key().to_owned(), t.as_json()))
.collect();
serde_json::Value::Object(entries.into_iter().collect())
}
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ToolchainResolutionError {
#[error(
"{label} `{spec}` was requested by {source_label} but could not be found",
label = kind.human_label(),
source_label = source_label(*selected_from)
)]
ToolNotFound {
kind: ToolKind,
spec: String,
selected_from: ToolSource,
},
#[error("no usable {label} found on PATH; set {env_var} or add `{key} = ...` under [toolchain]",
label = kind.human_label(),
env_var = env_var_for(*kind),
key = kind.as_key()
)]
NoDefault { kind: ToolKind },
#[error(
"selected {label} `{spec}` is not supported by the current C++ backend; use a GCC- or Clang-like compiler driver",
label = kind.human_label()
)]
UnsupportedCompiler { kind: ToolKind, spec: String },
}
fn env_var_for(kind: ToolKind) -> &'static str {
match kind {
ToolKind::CCompiler => "CC",
ToolKind::CxxCompiler => "CXX",
ToolKind::Archiver => "AR",
}
}
fn source_label(source: ToolSource) -> &'static str {
match source {
ToolSource::Cli => "--cli",
ToolSource::Env => "an environment variable",
ToolSource::UserConfig => "the user `[toolchain]` config table",
ToolSource::WorkspaceConfig => "the workspace `[toolchain]` config table",
ToolSource::PackageConfig => "the package `[toolchain]` config table",
ToolSource::ExplicitConfig => "the `CABIN_CONFIG` `[toolchain]` table",
ToolSource::ManifestConditional => "[target.'cfg(...)'.toolchain]",
ToolSource::Manifest => "[toolchain]",
ToolSource::Default => "the built-in default list",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tool_kind_keys_are_stable() {
assert_eq!(ToolKind::CCompiler.as_key(), "cc");
assert_eq!(ToolKind::CxxCompiler.as_key(), "cxx");
assert_eq!(ToolKind::Archiver.as_key(), "ar");
}
#[test]
fn tool_spec_parse_distinguishes_paths_and_names() {
match ToolSpec::parse("clang++") {
ToolSpec::Name(n) => assert_eq!(n, "clang++"),
ToolSpec::Path(p) => panic!("expected name, got {p:?}"),
}
match ToolSpec::parse("/usr/bin/clang++") {
ToolSpec::Path(p) => assert_eq!(p, PathBuf::from("/usr/bin/clang++")),
ToolSpec::Name(n) => panic!("expected path, got {n:?}"),
}
match ToolSpec::parse("./bin/clang++") {
ToolSpec::Path(p) => assert_eq!(p, PathBuf::from("./bin/clang++")),
ToolSpec::Name(n) => panic!("expected path, got {n:?}"),
}
}
#[test]
fn toolchain_decl_is_empty_when_unset() {
assert!(ToolchainDecl::default().is_empty());
let d = ToolchainDecl {
cxx: Some(ToolSpec::Name("clang++".into())),
..Default::default()
};
assert!(!d.is_empty());
assert_eq!(
d.get(ToolKind::CxxCompiler).map(ToolSpec::display),
Some("clang++".to_owned())
);
assert!(d.get(ToolKind::CCompiler).is_none());
}
#[test]
fn resolved_toolchain_iter_is_sorted_and_skips_missing_cc() {
let cxx = ResolvedTool {
kind: ToolKind::CxxCompiler,
path: PathBuf::from("/usr/bin/c++"),
spec: ToolSpec::Name("c++".into()),
source: ToolSource::Default,
};
let ar = ResolvedTool {
kind: ToolKind::Archiver,
path: PathBuf::from("/usr/bin/ar"),
spec: ToolSpec::Name("ar".into()),
source: ToolSource::Default,
};
let resolved = ResolvedToolchain { cxx, ar, cc: None };
let kinds: Vec<ToolKind> = resolved.iter().map(|t| t.kind).collect();
assert_eq!(kinds, vec![ToolKind::CxxCompiler, ToolKind::Archiver]);
}
}