Skip to content

Commit

Permalink
Support cargo aliases.
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexhuszagh committed Jul 10, 2022
1 parent 23f7b74 commit c5c647f
Show file tree
Hide file tree
Showing 5 changed files with 387 additions and 102 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased] - ReleaseDate

## Added

- #931 - add support for cargo aliases.

## Fixed

- #930 - fix any parsing of 1-character subcommands
Expand Down
246 changes: 246 additions & 0 deletions src/cargo_toml.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
use std::collections::{BTreeMap, 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 CargoConfig(Table);

impl CargoConfig {
fn merge(&mut self, parent: &CargoConfig) -> 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"))
}

// get all the aliases from the map
pub fn alias<'a>(&'a self) -> Option<BTreeMap<&'a str, Vec<&'a str>>> {
let parse_alias = |v: &'a Value| -> Option<Vec<&'a str>> {
if let Some(s) = v.as_str() {
Some(s.split_whitespace().collect())
} else if let Some(a) = v.as_array() {
a.iter().map(|i| i.as_str()).collect()
} else {
None
}
};

self.0
.get("alias")?
.as_table()?
.iter()
.map(|(k, v)| Some((k.as_str(), parse_alias(v)?)))
.collect()
}
}

fn parse_config_file(path: &Path) -> Result<CargoConfig> {
let contents = file::read(&path)
.wrap_err_with(|| format!("could not read cargo config file `{path:?}`"))?;
Ok(CargoConfig(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_config_files() -> Result<CargoConfig> {
// note: cargo supports both `config` and `config.toml`
// `config` exists for compatibility reasons, but if
// present, only it will be read.
let read_and_merge = |result: &mut CargoConfig, dir: &Path| -> Result<()> {
let noext = dir.join("config");
let ext = dir.join("config.toml");
if noext.exists() {
let parent = parse_config_file(&noext)?;
result.merge(&parent)?;
} else if ext.exists() {
let parent = parse_config_file(&ext)?;
result.merge(&parent)?;
}

Ok(())
};

let mut result = CargoConfig::default();
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)
}

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

#[test]
fn test_parse() -> Result<()> {
let config1 = CargoConfig(toml::from_str(CARGO_TOML1)?);
let config2 = CargoConfig(toml::from_str(CARGO_TOML2)?);
let alias1 = config1.alias().expect("unable to get aliases.");
let alias2 = config2.alias().expect("unable to get aliases.");
assert_eq!(
alias1,
BTreeMap::from([("foo", vec!["build", "foo"]), ("bar", vec!["check", "bar"]),])
);
assert_eq!(
alias2,
BTreeMap::from([("baz", vec!["test", "baz"]), ("bar", vec!["init", "bar"]),])
);
let mut merged = config1.clone();
merged.merge(&config2)?;
let alias_merge = merged.alias().expect("unable to get aliases.");
assert_eq!(
alias_merge,
BTreeMap::from([
("foo", vec!["build", "foo"]),
("baz", vec!["test", "baz"]),
("bar", vec!["check", "bar"]),
])
);

// check our merge went well
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_config() -> Result<()> {
// cross contains a few aliases, so test those
let config = read_config_files()?;
let aliases = config.alias().expect("must have aliases");
assert_eq!(
aliases.get("build-docker-image"),
Some(&vec!["xtask", "build-docker-image"]),
);
assert_eq!(
aliases.get("xtask"),
Some(&vec!["run", "-p", "xtask", "--"]),
);

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 }
"#;
}

0 comments on commit c5c647f

Please sign in to comment.