Skip to content

Commit dd550c1

Browse files
committed
Add various support for cargo configuration.
Also adds support for parsing and reading configuration options/environment variables from cargo, and allows users to ignore config files in the package. This adds the `[build.env.cargo-config]` option, which can be set to `complete`, `ignore`, or `default`. If set to `complete`, cross will have access to every to every cargo config file on the host (if using remote cross, a config file is written to a temporary file which is mounted on the data volume at `/.cargo/config.toml`). If set to ignore, any `.cargo/config.toml` files outside of `CARGO_HOME` are ignored, by mounting anonymous data volumes to hide config files in any `.cargo` directories. The default behavior uses the backwards-compatible behavior, allowing cross to access any config files in the package and `CARGO_HOME` directories. If the build is called outside the workspace root or at the workspace root, then we only mount an anonymous volume at `$PWD/.cargo`. The alias support includes recursive subcommand detection, and errors before it invokes cargo. A sample error message is `[cross] error: alias y has unresolvable recursive definition: x -> y -> z -> a -> y`, and therefore can handle non-trivial recursive subcommands.
1 parent 6cd09b3 commit dd550c1

15 files changed

+1644
-936
lines changed

.changes/931.json

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
{
2-
"description": "deny installation of debian packages that conflict with our cross-compiler toolchains.",
3-
"type": "fixed"
4-
}
1+
[
2+
{
3+
"description": "add support for cargo aliases.",
4+
"type": "added",
5+
"issues": [562],
6+
},
7+
{
8+
"description": "allow users to ignore config files in the package.",
9+
"type": "added",
10+
"issues": [621]
11+
}
12+
]

.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+
}

docs/cross_toml.md

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ For example:
2121

2222
```toml
2323
[build.env]
24+
cargo-config = "complete"
2425
volumes = ["VOL1_ARG", "VOL2_ARG"]
2526
passthrough = ["IMPORTANT_ENV_VARIABLES"]
2627
```
@@ -63,6 +64,7 @@ This is similar to `build.env`, but allows you to be more specific per target.
6364

6465
```toml
6566
[target.x86_64-unknown-linux-gnu.env]
67+
cargo-config = "ignore"
6668
volumes = ["VOL1_ARG", "VOL2_ARG"]
6769
passthrough = ["IMPORTANT_ENV_VARIABLES"]
6870
```

src/cargo_config.rs

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use std::collections::HashMap;
2+
3+
use crate::cargo_toml::CargoToml;
4+
use crate::config::{split_to_cloned_by_ws, 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| split_to_cloned_by_ws(&x))
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+
58+
pub fn to_toml(&self) -> Result<Option<String>> {
59+
match self.toml.as_ref() {
60+
Some(t) => Ok(Some(t.to_toml()?)),
61+
None => Ok(None),
62+
}
63+
}
64+
}

src/cargo_toml.rs

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

0 commit comments

Comments
 (0)