use crate::errors::{CargoConfigSearchError, CargoConfigsConstructError, TargetTripleError};
use camino::{Utf8Path, Utf8PathBuf};
use once_cell::sync::OnceCell;
use serde::Deserialize;
use std::{collections::BTreeMap, fmt};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TargetTriple {
pub triple: String,
pub source: TargetTripleSource,
}
impl TargetTriple {
pub fn find(
cargo_configs: &CargoConfigs,
target_cli_option: Option<&str>,
) -> Result<Option<Self>, TargetTripleError> {
if let Some(triple) = target_cli_option {
return Ok(Some(TargetTriple {
triple: triple.to_owned(),
source: TargetTripleSource::CliOption,
}));
}
if let Some(triple) = Self::from_env()? {
return Ok(Some(triple));
}
Self::from_cargo_configs(cargo_configs)
}
pub const CARGO_BUILD_TARGET_ENV: &'static str = "CARGO_BUILD_TARGET";
fn from_env() -> Result<Option<Self>, TargetTripleError> {
if let Some(triple_val) = std::env::var_os(Self::CARGO_BUILD_TARGET_ENV) {
let triple = triple_val
.into_string()
.map_err(|_osstr| TargetTripleError::InvalidEnvironmentVar)?;
Ok(Some(Self {
triple,
source: TargetTripleSource::Env,
}))
} else {
Ok(None)
}
}
fn from_cargo_configs(cargo_configs: &CargoConfigs) -> Result<Option<Self>, TargetTripleError> {
for (source, config) in cargo_configs.discovered_configs()? {
if let Some(triple) = &config.build.target {
return Ok(Some(TargetTriple {
triple: triple.to_owned(),
source: TargetTripleSource::CargoConfig {
source: source.clone(),
},
}));
}
}
Ok(None)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TargetTripleSource {
CliOption,
Env,
CargoConfig {
source: CargoConfigSource,
},
}
impl fmt::Display for TargetTripleSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CliOption => {
write!(f, "--target option")
}
Self::Env => {
write!(f, "environment variable `CARGO_BUILD_TARGET`")
}
Self::CargoConfig {
source: CargoConfigSource::CliOption,
} => {
write!(f, "`build.target` specified by `--config`")
}
Self::CargoConfig {
source: CargoConfigSource::File(path),
} => {
write!(f, "`build.target` within `{path}`")
}
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CargoConfigSource {
CliOption,
File(Utf8PathBuf),
}
#[derive(Debug)]
pub struct CargoConfigs {
cli_configs: Vec<(CargoConfigSource, CargoConfig)>,
cwd: Utf8PathBuf,
terminate_search_at: Option<Utf8PathBuf>,
discovered: OnceCell<Vec<(CargoConfigSource, CargoConfig)>>,
}
impl CargoConfigs {
pub fn new(
cli_configs: impl IntoIterator<Item = impl AsRef<str>>,
) -> Result<Self, CargoConfigsConstructError> {
let cli_configs = parse_cli_configs(cli_configs.into_iter())?;
let cwd = std::env::current_dir()
.map_err(CargoConfigsConstructError::GetCurrentDir)
.and_then(|cwd| {
Utf8PathBuf::try_from(cwd)
.map_err(CargoConfigsConstructError::CurrentDirInvalidUtf8)
})?;
Ok(Self {
cli_configs,
cwd,
terminate_search_at: None,
discovered: OnceCell::new(),
})
}
#[doc(hidden)]
pub fn new_with_isolation(
cli_configs: impl IntoIterator<Item = impl AsRef<str>>,
cwd: &Utf8Path,
terminate_search_at: &Utf8Path,
) -> Result<Self, CargoConfigsConstructError> {
let cli_configs = parse_cli_configs(cli_configs.into_iter())?;
Ok(Self {
cli_configs,
cwd: cwd.to_owned(),
terminate_search_at: Some(terminate_search_at.to_owned()),
discovered: OnceCell::new(),
})
}
pub(crate) fn cwd(&self) -> &Utf8Path {
&self.cwd
}
pub(crate) fn discovered_configs(
&self,
) -> Result<
impl Iterator<Item = &(CargoConfigSource, CargoConfig)> + DoubleEndedIterator + '_,
CargoConfigSearchError,
> {
let cli_iter = self.cli_configs.iter();
let file_iter = self
.discovered
.get_or_try_init(|| discover_impl(&self.cwd, self.terminate_search_at.as_deref()))?
.iter();
Ok(cli_iter.chain(file_iter))
}
}
fn parse_cli_configs(
cli_configs: impl Iterator<Item = impl AsRef<str>>,
) -> Result<Vec<(CargoConfigSource, CargoConfig)>, CargoConfigsConstructError> {
cli_configs
.into_iter()
.map(|config_str| {
let config_str = config_str.as_ref();
let config = toml::from_str(config_str).map_err(|error| {
CargoConfigsConstructError::CliConfigParseError {
config_str: config_str.to_owned(),
error,
}
})?;
Ok((CargoConfigSource::CliOption, config))
})
.collect()
}
fn discover_impl(
start_search_at: &Utf8Path,
terminate_search_at: Option<&Utf8Path>,
) -> Result<Vec<(CargoConfigSource, CargoConfig)>, CargoConfigSearchError> {
fn read_config_dir(dir: &mut Utf8PathBuf) -> Option<Utf8PathBuf> {
dir.push("config");
if !dir.exists() {
dir.set_extension("toml");
}
let ret = if dir.exists() {
Some(dir.clone())
} else {
None
};
dir.pop();
ret
}
let mut dir = start_search_at.canonicalize_utf8().map_err(|error| {
CargoConfigSearchError::FailedPathCanonicalization {
path: start_search_at.to_owned(),
error,
}
})?;
let mut config_paths = Vec::new();
for _ in 0..dir.ancestors().count() {
dir.push(".cargo");
if !dir.exists() {
dir.pop();
dir.pop();
continue;
}
if let Some(path) = read_config_dir(&mut dir) {
config_paths.push(path);
}
dir.pop();
if Some(dir.as_path()) == terminate_search_at {
break;
}
dir.pop();
}
if terminate_search_at.is_none() {
let mut cargo_home_path = home::cargo_home_with_cwd(start_search_at.as_std_path())
.map_err(CargoConfigSearchError::GetCargoHome)
.and_then(|home| {
Utf8PathBuf::try_from(home).map_err(CargoConfigSearchError::NonUtf8Path)
})?;
if let Some(home_config) = read_config_dir(&mut cargo_home_path) {
if !config_paths.iter().any(|path| path == &home_config) {
config_paths.push(home_config);
}
}
}
let configs = config_paths
.into_iter()
.map(|path| {
let config_contents = std::fs::read_to_string(&path).map_err(|error| {
CargoConfigSearchError::ConfigReadError {
path: path.clone(),
error,
}
})?;
let config: CargoConfig = toml::from_str(&config_contents).map_err(|error| {
CargoConfigSearchError::ConfigParseError {
path: path.clone(),
error,
}
})?;
Ok((CargoConfigSource::File(path), config))
})
.collect::<Result<Vec<_>, CargoConfigSearchError>>()?;
Ok(configs)
}
#[derive(Deserialize, Debug)]
pub(crate) struct CargoConfig {
#[serde(default)]
pub(crate) build: CargoConfigBuild,
pub(crate) target: Option<BTreeMap<String, CargoConfigRunner>>,
}
#[derive(Deserialize, Default, Debug)]
pub(crate) struct CargoConfigBuild {
pub(crate) target: Option<String>,
}
#[derive(Deserialize, Debug)]
pub(crate) struct CargoConfigRunner {
#[serde(default)]
pub(crate) runner: Option<Runner>,
}
#[derive(Clone, Deserialize, Debug)]
#[serde(untagged)]
pub(crate) enum Runner {
Simple(String),
List(Vec<String>),
}
#[cfg(test)]
mod tests {
use super::*;
use camino::Utf8Path;
use color_eyre::eyre::{Context, Result};
use tempfile::TempDir;
#[test]
fn test_find_target_triple() {
let dir = setup_temp_dir().unwrap();
let dir_path = Utf8PathBuf::try_from(dir.path().canonicalize().unwrap()).unwrap();
let dir_foo_path = dir_path.join("foo");
let dir_foo_bar_path = dir_foo_path.join("bar");
assert_eq!(
find_target_triple(&[], &dir_foo_bar_path, &dir_path),
Some(TargetTriple {
triple: "x86_64-unknown-linux-gnu".into(),
source: TargetTripleSource::CargoConfig {
source: CargoConfigSource::File(dir_path.join("foo/bar/.cargo/config.toml")),
},
}),
);
assert_eq!(
find_target_triple(&[], &dir_foo_path, &dir_path),
Some(TargetTriple {
triple: "x86_64-pc-windows-msvc".into(),
source: TargetTripleSource::CargoConfig {
source: CargoConfigSource::File(dir_path.join("foo/.cargo/config")),
},
}),
);
assert_eq!(
find_target_triple(
&["build.target=\"aarch64-unknown-linux-gnu\""],
&dir_foo_bar_path,
&dir_path
),
Some(TargetTriple {
triple: "aarch64-unknown-linux-gnu".into(),
source: TargetTripleSource::CargoConfig {
source: CargoConfigSource::CliOption,
},
})
);
assert_eq!(
find_target_triple(
&[
"build.target=\"aarch64-unknown-linux-gnu\"",
"build.target=\"x86_64-unknown-linux-musl\""
],
&dir_foo_bar_path,
&dir_path
),
Some(TargetTriple {
triple: "aarch64-unknown-linux-gnu".into(),
source: TargetTripleSource::CargoConfig {
source: CargoConfigSource::CliOption,
},
})
);
assert_eq!(find_target_triple(&[], &dir_path, &dir_path), None);
}
fn setup_temp_dir() -> Result<TempDir> {
let dir = tempfile::Builder::new()
.tempdir()
.wrap_err("error creating tempdir")?;
std::fs::create_dir_all(dir.path().join("foo/.cargo"))
.wrap_err("error creating foo/.cargo subdir")?;
std::fs::create_dir_all(dir.path().join("foo/bar/.cargo"))
.wrap_err("error creating foo/bar/.cargo subdir")?;
std::fs::write(
dir.path().join("foo/.cargo/config"),
FOO_CARGO_CONFIG_CONTENTS,
)
.wrap_err("error writing foo/.cargo/config")?;
std::fs::write(
dir.path().join("foo/bar/.cargo/config.toml"),
FOO_BAR_CARGO_CONFIG_CONTENTS,
)
.wrap_err("error writing foo/bar/.cargo/config.toml")?;
Ok(dir)
}
fn find_target_triple(
cli_configs: &[&str],
start_search_at: &Utf8Path,
terminate_search_at: &Utf8Path,
) -> Option<TargetTriple> {
let configs =
CargoConfigs::new_with_isolation(cli_configs, start_search_at, terminate_search_at)
.unwrap();
TargetTriple::from_cargo_configs(&configs).unwrap()
}
static FOO_CARGO_CONFIG_CONTENTS: &str = r#"
[build]
target = "x86_64-pc-windows-msvc"
"#;
static FOO_BAR_CARGO_CONFIG_CONTENTS: &str = r#"
[build]
target = "x86_64-unknown-linux-gnu"
"#;
}