Skip to content

Commit 81fe71f

Browse files
committed
Support cargo aliases.
Also adds support for parsing and reading configuration options/environment variables from cargo.
1 parent 6cd09b3 commit 81fe71f

14 files changed

+1362
-928
lines changed

.changes/931.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2-
"description": "deny installation of debian packages that conflict with our cross-compiler toolchains.",
3-
"type": "fixed"
2+
"description": "add support for cargo aliases.",
3+
"type": "added",
4+
"issues": [562],
45
}

.changes/933.json

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"description": "deny installation of debian packages that conflict with our cross-compiler toolchains.",
3+
"type": "fixed"
4+
}

src/cargo_config.rs

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use std::collections::HashMap;
2+
3+
use crate::cargo_toml::CargoToml;
4+
use crate::config::Environment;
5+
use crate::errors::*;
6+
7+
pub const CARGO_NO_PREFIX_ENVVARS: &[&str] = &[
8+
"http_proxy",
9+
"TERM",
10+
"RUSTDOCFLAGS",
11+
"RUSTFLAGS",
12+
"BROWSER",
13+
"HTTPS_PROXY",
14+
"HTTP_TIMEOUT",
15+
"https_proxy",
16+
];
17+
18+
#[derive(Debug)]
19+
struct CargoEnvironment(Environment);
20+
21+
impl CargoEnvironment {
22+
fn new(map: Option<HashMap<&'static str, &'static str>>) -> Self {
23+
CargoEnvironment(Environment::new("CARGO", map))
24+
}
25+
26+
pub fn alias(&self, name: &str) -> Option<Vec<String>> {
27+
let key = format!("ALIAS_{name}");
28+
self.0
29+
.get_var(&self.0.var_name(&key))
30+
.map(|x| x.split_whitespace().map(ToOwned::to_owned).collect())
31+
}
32+
}
33+
34+
#[derive(Debug)]
35+
pub struct CargoConfig {
36+
toml: Option<CargoToml>,
37+
env: CargoEnvironment,
38+
}
39+
40+
impl CargoConfig {
41+
pub fn new(toml: Option<CargoToml>) -> Self {
42+
CargoConfig {
43+
toml,
44+
env: CargoEnvironment::new(None),
45+
}
46+
}
47+
48+
pub fn alias(&self, name: &str) -> Result<Option<Vec<String>>> {
49+
match self.env.alias(name) {
50+
Some(alias) => Ok(Some(alias)),
51+
None => match self.toml.as_ref() {
52+
Some(t) => t.alias(name),
53+
None => Ok(None),
54+
},
55+
}
56+
}
57+
}

src/cargo_toml.rs

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

0 commit comments

Comments
 (0)