-
Notifications
You must be signed in to change notification settings - Fork 351
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
23f7b74
commit c5c647f
Showing
5 changed files
with
387 additions
and
102 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | ||
"#; | ||
} |
Oops, something went wrong.