Skip to content

Commit

Permalink
Add various support for cargo configuration.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Alexhuszagh committed Jul 13, 2022
1 parent 6cd09b3 commit dd550c1
Show file tree
Hide file tree
Showing 15 changed files with 1,644 additions and 936 deletions.
16 changes: 12 additions & 4 deletions .changes/931.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
{
"description": "deny installation of debian packages that conflict with our cross-compiler toolchains.",
"type": "fixed"
}
[
{
"description": "add support for cargo aliases.",
"type": "added",
"issues": [562],
},
{
"description": "allow users to ignore config files in the package.",
"type": "added",
"issues": [621]
}
]
4 changes: 4 additions & 0 deletions .changes/933.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"description": "deny installation of debian packages that conflict with our cross-compiler toolchains.",
"type": "fixed"
}
2 changes: 2 additions & 0 deletions docs/cross_toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ For example:

```toml
[build.env]
cargo-config = "complete"
volumes = ["VOL1_ARG", "VOL2_ARG"]
passthrough = ["IMPORTANT_ENV_VARIABLES"]
```
Expand Down Expand Up @@ -63,6 +64,7 @@ This is similar to `build.env`, but allows you to be more specific per target.

```toml
[target.x86_64-unknown-linux-gnu.env]
cargo-config = "ignore"
volumes = ["VOL1_ARG", "VOL2_ARG"]
passthrough = ["IMPORTANT_ENV_VARIABLES"]
```
Expand Down
64 changes: 64 additions & 0 deletions src/cargo_config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use std::collections::HashMap;

use crate::cargo_toml::CargoToml;
use crate::config::{split_to_cloned_by_ws, Environment};
use crate::errors::*;

pub const CARGO_NO_PREFIX_ENVVARS: &[&str] = &[
"http_proxy",
"TERM",
"RUSTDOCFLAGS",
"RUSTFLAGS",
"BROWSER",
"HTTPS_PROXY",
"HTTP_TIMEOUT",
"https_proxy",
];

#[derive(Debug)]
struct CargoEnvironment(Environment);

impl CargoEnvironment {
fn new(map: Option<HashMap<&'static str, &'static str>>) -> Self {
CargoEnvironment(Environment::new("CARGO", map))
}

pub fn alias(&self, name: &str) -> Option<Vec<String>> {
let key = format!("ALIAS_{name}");
self.0
.get_var(&self.0.var_name(&key))
.map(|x| split_to_cloned_by_ws(&x))
}
}

#[derive(Debug)]
pub struct CargoConfig {
toml: Option<CargoToml>,
env: CargoEnvironment,
}

impl CargoConfig {
pub fn new(toml: Option<CargoToml>) -> Self {
CargoConfig {
toml,
env: CargoEnvironment::new(None),
}
}

pub fn alias(&self, name: &str) -> Result<Option<Vec<String>>> {
match self.env.alias(name) {
Some(alias) => Ok(Some(alias)),
None => match self.toml.as_ref() {
Some(t) => t.alias(name),
None => Ok(None),
},
}
}

pub fn to_toml(&self) -> Result<Option<String>> {
match self.toml.as_ref() {
Some(t) => Ok(Some(t.to_toml()?)),
None => Ok(None),
}
}
}
268 changes: 268 additions & 0 deletions src/cargo_toml.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
use std::collections::BTreeSet;
use std::env;
use std::path::Path;

use crate::config::split_to_cloned_by_ws;
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 CargoToml(Table);

impl CargoToml {
fn parse(path: &Path) -> Result<CargoToml> {
let contents = file::read(&path)
.wrap_err_with(|| format!("could not read cargo config file at `{path:?}`"))?;
Ok(CargoToml(toml::from_str(&contents)?))
}

pub fn to_toml(&self) -> Result<String> {
toml::to_string(&self.0).map_err(Into::into)
}

// 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() -> Result<Option<CargoToml>> {
// note: cargo supports both `config` and `config.toml`
// `config` exists for compatibility reasons, but if
// present, only it will be read.
let read = |dir: &Path| -> Result<Option<CargoToml>> {
let noext = dir.join("config");
let ext = dir.join("config.toml");
if noext.exists() {
Ok(Some(CargoToml::parse(&noext)?))
} else if ext.exists() {
Ok(Some(CargoToml::parse(&ext)?))
} else {
Ok(None)
}
};

let read_and_merge = |result: &mut Option<CargoToml>, dir: &Path| -> Result<()> {
let parent = read(dir)?;
// can't use a match, since there's a use-after-move issue
match (result.as_mut(), parent) {
(Some(r), Some(p)) => r.merge(&p)?,
(None, Some(p)) => *result = Some(p),
(Some(_), None) | (None, None) => (),
}

Ok(())
};

let mut result = None;
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)
}

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

pub fn alias(&self, name: &str) -> Result<Option<Vec<String>>> {
let parse_alias = |value: &Value| -> Result<Vec<String>> {
if let Some(s) = value.as_str() {
Ok(split_to_cloned_by_ws(s))
} else if let Some(a) = value.as_array() {
a.iter()
.map(|i| {
i.as_str()
.map(ToOwned::to_owned)
.ok_or_else(|| eyre::eyre!("invalid alias type, got {value}"))
})
.collect()
} else {
eyre::bail!("invalid alias type, got {}", value.type_str());
}
};

let alias = match self.0.get("alias") {
Some(a) => a,
None => return Ok(None),
};
let table = match alias.as_table() {
Some(t) => t,
None => eyre::bail!("cargo config aliases must be a table"),
};

match table.get(name) {
Some(v) => Ok(Some(parse_alias(v)?)),
None => Ok(None),
}
}
}

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

macro_rules! s {
($s:literal) => {
$s.to_owned()
};
}

#[test]
fn test_parse() -> Result<()> {
let config1 = CargoToml(toml::from_str(CARGO_TOML1)?);
let config2 = CargoToml(toml::from_str(CARGO_TOML2)?);
assert_eq!(config1.alias("foo")?, Some(vec![s!("build"), s!("foo")]));
assert_eq!(config1.alias("bar")?, Some(vec![s!("check"), s!("bar")]));
assert_eq!(config2.alias("baz")?, Some(vec![s!("test"), s!("baz")]));
assert_eq!(config2.alias("bar")?, Some(vec![s!("init"), s!("bar")]));
assert_eq!(config1.alias("far")?, None);
assert_eq!(config2.alias("far")?, None);

let mut merged = config1;
merged.merge(&config2)?;
assert_eq!(merged.alias("foo")?, Some(vec![s!("build"), s!("foo")]));
assert_eq!(merged.alias("baz")?, Some(vec![s!("test"), s!("baz")]));
assert_eq!(merged.alias("bar")?, Some(vec![s!("check"), s!("bar")]));

// check our merge went well, with arrays, etc.
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() -> Result<()> {
let config = CargoToml::read()?.expect("cross must have cargo config.");
assert_eq!(
config.alias("build-docker-image")?,
Some(vec![s!("xtask"), s!("build-docker-image")])
);
assert_eq!(
config.alias("xtask")?,
Some(vec![s!("run"), s!("-p"), s!("xtask"), s!("--")])
);

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

0 comments on commit dd550c1

Please sign in to comment.