Skip to content

Commit c5c647f

Browse files
committed
Support cargo aliases.
1 parent 23f7b74 commit c5c647f

File tree

5 files changed

+387
-102
lines changed

5 files changed

+387
-102
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
77

88
## [Unreleased] - ReleaseDate
99

10+
## Added
11+
12+
- #931 - add support for cargo aliases.
13+
1014
## Fixed
1115

1216
- #930 - fix any parsing of 1-character subcommands

src/cargo_toml.rs

+246
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
use std::collections::{BTreeMap, BTreeSet};
2+
use std::env;
3+
use std::path::Path;
4+
5+
use crate::errors::*;
6+
use crate::file;
7+
8+
type Table = toml::value::Table;
9+
type Value = toml::value::Value;
10+
11+
// the strategy is to merge, with arrays merging together
12+
// and the deeper the config file is, the higher its priority.
13+
// arrays merge, numbers/strings get replaced, objects merge in.
14+
// we don't want to make any assumptions about the cargo
15+
// config data, in case we need to use it later.
16+
#[derive(Debug, Clone, Default)]
17+
pub struct CargoConfig(Table);
18+
19+
impl CargoConfig {
20+
fn merge(&mut self, parent: &CargoConfig) -> Result<()> {
21+
// can error on mismatched-data
22+
23+
fn validate_types(x: &Value, y: &Value) -> Option<()> {
24+
match x.same_type(y) {
25+
true => Some(()),
26+
false => None,
27+
}
28+
}
29+
30+
// merge 2 tables. x has precedence over y.
31+
fn merge_tables(x: &mut Table, y: &Table) -> Option<()> {
32+
// we need to iterate over both keys, so we need a full deduplication
33+
let keys: BTreeSet<String> = x.keys().chain(y.keys()).cloned().collect();
34+
for key in keys {
35+
let in_x = x.contains_key(&key);
36+
let in_y = y.contains_key(&key);
37+
match (in_x, in_y) {
38+
(true, true) => {
39+
// need to do our merge strategy
40+
let xk = x.get_mut(&key)?;
41+
let yk = y.get(&key)?;
42+
validate_types(xk, yk)?;
43+
44+
// now we've filtered out missing keys and optional values
45+
// all key/value pairs should be same type.
46+
if xk.is_table() {
47+
merge_tables(xk.as_table_mut()?, yk.as_table()?)?;
48+
} else if xk.is_array() {
49+
xk.as_array_mut()?.extend_from_slice(yk.as_array()?);
50+
}
51+
}
52+
(false, true) => {
53+
// key in y is not in x: copy it over
54+
let yk = y[&key].clone();
55+
x.insert(key, yk);
56+
}
57+
// key isn't present in y: can ignore it
58+
(_, false) => (),
59+
}
60+
}
61+
62+
Some(())
63+
}
64+
65+
merge_tables(&mut self.0, &parent.0).ok_or_else(|| eyre::eyre!("could not merge"))
66+
}
67+
68+
// get all the aliases from the map
69+
pub fn alias<'a>(&'a self) -> Option<BTreeMap<&'a str, Vec<&'a str>>> {
70+
let parse_alias = |v: &'a Value| -> Option<Vec<&'a str>> {
71+
if let Some(s) = v.as_str() {
72+
Some(s.split_whitespace().collect())
73+
} else if let Some(a) = v.as_array() {
74+
a.iter().map(|i| i.as_str()).collect()
75+
} else {
76+
None
77+
}
78+
};
79+
80+
self.0
81+
.get("alias")?
82+
.as_table()?
83+
.iter()
84+
.map(|(k, v)| Some((k.as_str(), parse_alias(v)?)))
85+
.collect()
86+
}
87+
}
88+
89+
fn parse_config_file(path: &Path) -> Result<CargoConfig> {
90+
let contents = file::read(&path)
91+
.wrap_err_with(|| format!("could not read cargo config file `{path:?}`"))?;
92+
Ok(CargoConfig(toml::from_str(&contents)?))
93+
}
94+
95+
// finding cargo config files actually runs from the
96+
// current working directory the command is invoked,
97+
// not from the project root. same is true with work
98+
// spaces: the project layout does not matter.
99+
pub fn read_config_files() -> Result<CargoConfig> {
100+
// note: cargo supports both `config` and `config.toml`
101+
// `config` exists for compatibility reasons, but if
102+
// present, only it will be read.
103+
let read_and_merge = |result: &mut CargoConfig, dir: &Path| -> Result<()> {
104+
let noext = dir.join("config");
105+
let ext = dir.join("config.toml");
106+
if noext.exists() {
107+
let parent = parse_config_file(&noext)?;
108+
result.merge(&parent)?;
109+
} else if ext.exists() {
110+
let parent = parse_config_file(&ext)?;
111+
result.merge(&parent)?;
112+
}
113+
114+
Ok(())
115+
};
116+
117+
let mut result = CargoConfig::default();
118+
let cwd = env::current_dir()?;
119+
let mut dir: &Path = &cwd;
120+
loop {
121+
read_and_merge(&mut result, &dir.join(".cargo"))?;
122+
let parent_dir = dir.parent();
123+
match parent_dir {
124+
Some(path) => dir = path,
125+
None => break,
126+
}
127+
}
128+
129+
read_and_merge(&mut result, &home::cargo_home()?)?;
130+
131+
Ok(result)
132+
}
133+
134+
#[cfg(test)]
135+
mod tests {
136+
use super::*;
137+
138+
#[test]
139+
fn test_parse() -> Result<()> {
140+
let config1 = CargoConfig(toml::from_str(CARGO_TOML1)?);
141+
let config2 = CargoConfig(toml::from_str(CARGO_TOML2)?);
142+
let alias1 = config1.alias().expect("unable to get aliases.");
143+
let alias2 = config2.alias().expect("unable to get aliases.");
144+
assert_eq!(
145+
alias1,
146+
BTreeMap::from([("foo", vec!["build", "foo"]), ("bar", vec!["check", "bar"]),])
147+
);
148+
assert_eq!(
149+
alias2,
150+
BTreeMap::from([("baz", vec!["test", "baz"]), ("bar", vec!["init", "bar"]),])
151+
);
152+
let mut merged = config1.clone();
153+
merged.merge(&config2)?;
154+
let alias_merge = merged.alias().expect("unable to get aliases.");
155+
assert_eq!(
156+
alias_merge,
157+
BTreeMap::from([
158+
("foo", vec!["build", "foo"]),
159+
("baz", vec!["test", "baz"]),
160+
("bar", vec!["check", "bar"]),
161+
])
162+
);
163+
164+
// check our merge went well
165+
assert_eq!(
166+
merged
167+
.0
168+
.get("build")
169+
.and_then(|x| x.get("jobs"))
170+
.and_then(|x| x.as_integer()),
171+
Some(2),
172+
);
173+
assert_eq!(
174+
merged
175+
.0
176+
.get("build")
177+
.and_then(|x| x.get("rustflags"))
178+
.and_then(|x| x.as_array())
179+
.and_then(|x| x.iter().map(|i| i.as_str()).collect()),
180+
Some(vec!["-C lto", "-Zbuild-std", "-Zdoctest-xcompile"]),
181+
);
182+
183+
Ok(())
184+
}
185+
186+
#[test]
187+
fn test_read_config() -> Result<()> {
188+
// cross contains a few aliases, so test those
189+
let config = read_config_files()?;
190+
let aliases = config.alias().expect("must have aliases");
191+
assert_eq!(
192+
aliases.get("build-docker-image"),
193+
Some(&vec!["xtask", "build-docker-image"]),
194+
);
195+
assert_eq!(
196+
aliases.get("xtask"),
197+
Some(&vec!["run", "-p", "xtask", "--"]),
198+
);
199+
200+
Ok(())
201+
}
202+
203+
const CARGO_TOML1: &str = r#"
204+
[alias]
205+
foo = "build foo"
206+
bar = "check bar"
207+
208+
[build]
209+
jobs = 2
210+
rustc-wrapper = "sccache"
211+
target = "x86_64-unknown-linux-gnu"
212+
rustflags = ["-C lto", "-Zbuild-std"]
213+
incremental = true
214+
215+
[doc]
216+
browser = "firefox"
217+
218+
[env]
219+
VAR1 = "VAL1"
220+
VAR2 = { value = "VAL2", force = true }
221+
VAR3 = { value = "relative/path", relative = true }
222+
"#;
223+
224+
const CARGO_TOML2: &str = r#"
225+
# want to check tables merge
226+
# want to check arrays concat
227+
# want to check rest override
228+
[alias]
229+
baz = "test baz"
230+
bar = "init bar"
231+
232+
[build]
233+
jobs = 4
234+
rustc-wrapper = "sccache"
235+
target = "x86_64-unknown-linux-gnu"
236+
rustflags = ["-Zdoctest-xcompile"]
237+
incremental = true
238+
239+
[doc]
240+
browser = "chromium"
241+
242+
[env]
243+
VAR1 = "NEW1"
244+
VAR2 = { value = "VAL2", force = false }
245+
"#;
246+
}

0 commit comments

Comments
 (0)