Skip to content

Commit

Permalink
Support cargo aliases.
Browse files Browse the repository at this point in the history
Also adds support for parsing and reading configuration options/environment variables from cargo.
  • Loading branch information
Alexhuszagh committed Jul 12, 2022
1 parent 6cd09b3 commit 6e525e0
Show file tree
Hide file tree
Showing 14 changed files with 1,362 additions and 928 deletions.
5 changes: 3 additions & 2 deletions .changes/931.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"description": "deny installation of debian packages that conflict with our cross-compiler toolchains.",
"type": "fixed"
"description": "add support for cargo aliases.",
"type": "added",
"issues": [562],
}
4 changes: 4 additions & 0 deletions .changes/933.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"description": "deny installation of debian packages that conflict with our cross-compiler toolchains.",
"type": "fixed"
}
57 changes: 57 additions & 0 deletions src/cargo_config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use std::collections::HashMap;

use crate::cargo_toml::CargoToml;
use crate::config::Environment;
use crate::errors::*;

pub const CARGO_NO_PREFIX_ENVVARS: &[&str] = &[
"http_proxy",
"TERM",
"RUSTDOCFLAGS",
"RUSTFLAGS",
"BROWSER",
"HTTPS_PROXY",
"HTTP_TIMEOUT",
"https_proxy",
];

#[derive(Debug)]
struct CargoEnvironment(Environment);

impl CargoEnvironment {
fn new(map: Option<HashMap<&'static str, &'static str>>) -> Self {
CargoEnvironment(Environment::new("CARGO", map))
}

pub fn alias(&self, name: &str) -> Option<Vec<String>> {
let key = format!("ALIAS_{name}");
self.0
.get_var(&self.0.var_name(&key))
.map(|x| x.split_whitespace().map(ToOwned::to_owned).collect())
}
}

#[derive(Debug)]
pub struct CargoConfig {
toml: Option<CargoToml>,
env: CargoEnvironment,
}

impl CargoConfig {
pub fn new(toml: Option<CargoToml>) -> Self {
CargoConfig {
toml,
env: CargoEnvironment::new(None),
}
}

pub fn alias(&self, name: &str) -> Result<Option<Vec<String>>> {
match self.env.alias(name) {
Some(alias) => Ok(Some(alias)),
None => match self.toml.as_ref() {
Some(t) => t.alias(name),
None => Ok(None),
},
}
}
}
263 changes: 263 additions & 0 deletions src/cargo_toml.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
use std::collections::BTreeSet;
use std::env;
use std::path::Path;

use crate::errors::*;
use crate::file;

type Table = toml::value::Table;
type Value = toml::value::Value;

// the strategy is to merge, with arrays merging together
// and the deeper the config file is, the higher its priority.
// arrays merge, numbers/strings get replaced, objects merge in.
// we don't want to make any assumptions about the cargo
// config data, in case we need to use it later.
#[derive(Debug, Clone, Default)]
pub struct CargoToml(Table);

impl CargoToml {
fn parse(path: &Path) -> Result<CargoToml> {
let contents = file::read(&path)
.wrap_err_with(|| format!("could not read cargo config file at `{path:?}`"))?;
Ok(CargoToml(toml::from_str(&contents)?))
}

// finding cargo config files actually runs from the
// current working directory the command is invoked,
// not from the project root. same is true with work
// spaces: the project layout does not matter.
pub fn read() -> Result<Option<CargoToml>> {
// note: cargo supports both `config` and `config.toml`
// `config` exists for compatibility reasons, but if
// present, only it will be read.
let read = |dir: &Path| -> Result<Option<CargoToml>> {
let noext = dir.join("config");
let ext = dir.join("config.toml");
if noext.exists() {
Ok(Some(CargoToml::parse(&noext)?))
} else if ext.exists() {
Ok(Some(CargoToml::parse(&ext)?))
} else {
Ok(None)
}
};

let read_and_merge = |result: &mut Option<CargoToml>, dir: &Path| -> Result<()> {
let parent = read(dir)?;
// can't use a match, since there's a use-after-move issue
match (result.as_mut(), parent) {
(Some(r), Some(p)) => r.merge(&p)?,
(None, Some(p)) => *result = Some(p),
(Some(_), None) | (None, None) => (),
}

Ok(())
};

let mut result = None;
let cwd = env::current_dir()?;
let mut dir: &Path = &cwd;
loop {
read_and_merge(&mut result, &dir.join(".cargo"))?;
let parent_dir = dir.parent();
match parent_dir {
Some(path) => dir = path,
None => break,
}
}

read_and_merge(&mut result, &home::cargo_home()?)?;

Ok(result)
}

fn merge(&mut self, parent: &CargoToml) -> Result<()> {
// can error on mismatched-data

fn validate_types(x: &Value, y: &Value) -> Option<()> {
match x.same_type(y) {
true => Some(()),
false => None,
}
}

// merge 2 tables. x has precedence over y.
fn merge_tables(x: &mut Table, y: &Table) -> Option<()> {
// we need to iterate over both keys, so we need a full deduplication
let keys: BTreeSet<String> = x.keys().chain(y.keys()).cloned().collect();
for key in keys {
let in_x = x.contains_key(&key);
let in_y = y.contains_key(&key);
match (in_x, in_y) {
(true, true) => {
// need to do our merge strategy
let xk = x.get_mut(&key)?;
let yk = y.get(&key)?;
validate_types(xk, yk)?;

// now we've filtered out missing keys and optional values
// all key/value pairs should be same type.
if xk.is_table() {
merge_tables(xk.as_table_mut()?, yk.as_table()?)?;
} else if xk.is_array() {
xk.as_array_mut()?.extend_from_slice(yk.as_array()?);
}
}
(false, true) => {
// key in y is not in x: copy it over
let yk = y[&key].clone();
x.insert(key, yk);
}
// key isn't present in y: can ignore it
(_, false) => (),
}
}

Some(())
}

merge_tables(&mut self.0, &parent.0).ok_or_else(|| eyre::eyre!("could not merge"))
}

pub fn alias(&self, name: &str) -> Result<Option<Vec<String>>> {
let parse_alias = |value: &Value| -> Result<Vec<String>> {
if let Some(s) = value.as_str() {
Ok(s.split_whitespace().map(ToOwned::to_owned).collect())
} else if let Some(a) = value.as_array() {
a.iter()
.map(|i| {
i.as_str()
.map(ToOwned::to_owned)
.ok_or_else(|| eyre::eyre!("invalid alias type, got {value}"))
})
.collect()
} else {
eyre::bail!("invalid alias type, got {}", value.type_str());
}
};

let alias = match self.0.get("alias") {
Some(a) => a,
None => return Ok(None),
};
let table = match alias.as_table() {
Some(t) => t,
None => eyre::bail!("cargo config aliases must be a table"),
};

match table.get(name) {
Some(v) => Ok(Some(parse_alias(v)?)),
None => Ok(None),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

macro_rules! s {
($s:literal) => {
$s.to_owned()
};
}

#[test]
fn test_parse() -> Result<()> {
let config1 = CargoToml(toml::from_str(CARGO_TOML1)?);
let config2 = CargoToml(toml::from_str(CARGO_TOML2)?);
assert_eq!(config1.alias("foo")?, Some(vec![s!("build"), s!("foo")]));
assert_eq!(config1.alias("bar")?, Some(vec![s!("check"), s!("bar")]));
assert_eq!(config2.alias("baz")?, Some(vec![s!("test"), s!("baz")]));
assert_eq!(config2.alias("bar")?, Some(vec![s!("init"), s!("bar")]));
assert_eq!(config1.alias("far")?, None);
assert_eq!(config2.alias("far")?, None);

let mut merged = config1;
merged.merge(&config2)?;
assert_eq!(merged.alias("foo")?, Some(vec![s!("build"), s!("foo")]));
assert_eq!(merged.alias("baz")?, Some(vec![s!("test"), s!("baz")]));
assert_eq!(merged.alias("bar")?, Some(vec![s!("check"), s!("bar")]));

// check our merge went well, with arrays, etc.
assert_eq!(
merged
.0
.get("build")
.and_then(|x| x.get("jobs"))
.and_then(|x| x.as_integer()),
Some(2),
);
assert_eq!(
merged
.0
.get("build")
.and_then(|x| x.get("rustflags"))
.and_then(|x| x.as_array())
.and_then(|x| x.iter().map(|i| i.as_str()).collect()),
Some(vec!["-C lto", "-Zbuild-std", "-Zdoctest-xcompile"]),
);

Ok(())
}

#[test]
fn test_read() -> Result<()> {
let config = CargoToml::read()?.expect("cross must have cargo config.");
assert_eq!(
config.alias("build-docker-image")?,
Some(vec![s!("xtask"), s!("build-docker-image")])
);
assert_eq!(
config.alias("xtask")?,
Some(vec![s!("run"), s!("-p"), s!("xtask"), s!("--")])
);

Ok(())
}

const CARGO_TOML1: &str = r#"
[alias]
foo = "build foo"
bar = "check bar"
[build]
jobs = 2
rustc-wrapper = "sccache"
target = "x86_64-unknown-linux-gnu"
rustflags = ["-C lto", "-Zbuild-std"]
incremental = true
[doc]
browser = "firefox"
[env]
VAR1 = "VAL1"
VAR2 = { value = "VAL2", force = true }
VAR3 = { value = "relative/path", relative = true }
"#;

const CARGO_TOML2: &str = r#"
# want to check tables merge
# want to check arrays concat
# want to check rest override
[alias]
baz = "test baz"
bar = "init bar"
[build]
jobs = 4
rustc-wrapper = "sccache"
target = "x86_64-unknown-linux-gnu"
rustflags = ["-Zdoctest-xcompile"]
incremental = true
[doc]
browser = "chromium"
[env]
VAR1 = "NEW1"
VAR2 = { value = "VAL2", force = false }
"#;
}
Loading

0 comments on commit 6e525e0

Please sign in to comment.