-
-
Notifications
You must be signed in to change notification settings - Fork 42
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Propagate
.cargo/config.toml
[env]
settings to the process enviro…
…nment Environment variables can be set and optionally override the process environment through `.cargo/config.toml`'s `[env]` section: https://doc.rust-lang.org/cargo/reference/config.html#env These config variables have specific precedence rules with regards to overriding the environment set in the process, and can optionally represent paths relative to the parent of the containing `.cargo/` folder. Besides exposing variables to all other processes called by `xbuild`, this also allows `xbuild` itself to be driven by variables set in `.cargo/config.toml`, such as `$ANDROID_HOME` needed for #116. rust-mobile/cargo-subcommand#12 rust-mobile/cargo-subcommand#16
- Loading branch information
Showing
3 changed files
with
253 additions
and
16 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,33 +1,268 @@ | ||
use anyhow::Result; | ||
use anyhow::{Context, Result}; | ||
use serde::Deserialize; | ||
use std::path::Path; | ||
use std::{ | ||
borrow::Cow, | ||
collections::BTreeMap, | ||
env::VarError, | ||
ops::Deref, | ||
path::{Path, PathBuf}, | ||
}; | ||
|
||
#[derive(Debug, Deserialize)] | ||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] | ||
#[serde(rename_all = "kebab-case")] | ||
pub struct Config { | ||
pub build: Option<Build>, | ||
/// <https://doc.rust-lang.org/cargo/reference/config.html#env> | ||
pub env: Option<BTreeMap<String, EnvOption>>, | ||
} | ||
|
||
impl Config { | ||
pub fn parse_from_toml(path: impl AsRef<Path>) -> Result<Self> { | ||
let contents = std::fs::read_to_string(path)?; | ||
Ok(toml::from_str(&contents)?) | ||
} | ||
} | ||
|
||
#[derive(Debug)] | ||
pub struct LocalizedConfig { | ||
pub config: Config, | ||
/// The directory containing `./.cargo/config.toml` | ||
pub workspace: PathBuf, | ||
} | ||
|
||
impl Deref for LocalizedConfig { | ||
type Target = Config; | ||
|
||
fn deref(&self) -> &Self::Target { | ||
&self.config | ||
} | ||
} | ||
|
||
impl LocalizedConfig { | ||
pub fn new(workspace: PathBuf) -> Result<Self> { | ||
Ok(Self { | ||
config: Config::parse_from_toml(workspace.join(".cargo/config.toml"))?, | ||
workspace, | ||
}) | ||
} | ||
|
||
/// Search for and open `.cargo/config.toml` in any parent of the workspace root path. | ||
pub fn find_cargo_config_for_workspace(workspace: impl AsRef<Path>) -> Result<Option<Self>> { | ||
let workspace = workspace.as_ref(); | ||
let workspace = dunce::canonicalize(workspace)?; | ||
workspace | ||
.ancestors() | ||
.map(|dir| dir.join(".cargo/config.toml")) | ||
.find(|p| p.is_file()) | ||
.map(Config::parse_from_toml) | ||
.find(|dir| dir.join(".cargo/config.toml").is_file()) | ||
.map(|p| p.to_path_buf()) | ||
.map(Self::new) | ||
.transpose() | ||
} | ||
|
||
/// Propagate environment variables from this `.cargo/config.toml` to the process environment | ||
/// using [`std::env::set_var()`]. | ||
/// | ||
/// Note that this is automatically performed when calling [`Subcommand::new()`][super::Subcommand::new()]. | ||
pub fn set_env_vars(&self) -> Result<()> { | ||
if let Some(env) = &self.config.env { | ||
for (key, env_option) in env { | ||
// Existing environment variables always have precedence unless | ||
// the extended format is used to set `force = true`: | ||
if !matches!(env_option, EnvOption::Value { force: true, .. }) | ||
&& std::env::var_os(key).is_some() | ||
{ | ||
continue; | ||
} | ||
|
||
std::env::set_var(key, env_option.resolve_value(&self.workspace)?.as_ref()) | ||
} | ||
} | ||
|
||
Ok(()) | ||
} | ||
} | ||
|
||
#[derive(Debug, Deserialize)] | ||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] | ||
pub struct Build { | ||
#[serde(rename = "target-dir")] | ||
pub target_dir: Option<String>, | ||
} | ||
|
||
/// Serializable environment variable in cargo config, configurable as per | ||
/// <https://doc.rust-lang.org/cargo/reference/config.html#env>, | ||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] | ||
#[serde(untagged)] | ||
pub enum EnvOption { | ||
String(String), | ||
Value { | ||
value: String, | ||
#[serde(default)] | ||
force: bool, | ||
#[serde(default)] | ||
relative: bool, | ||
}, | ||
} | ||
|
||
impl EnvOption { | ||
/// Retrieve the value and canonicalize it relative to `config_parent` when [`EnvOption::Value::relative`] is set. | ||
/// | ||
/// `config_parent` is the directory containing `.cargo/config.toml` where this was parsed from. | ||
pub fn resolve_value(&self, config_parent: impl AsRef<Path>) -> Result<Cow<'_, str>> { | ||
Ok(match self { | ||
Self::Value { | ||
value, | ||
relative: true, | ||
force: _, | ||
} => { | ||
let value = config_parent.as_ref().join(value); | ||
let value = dunce::canonicalize(&value) | ||
.with_context(|| format!("Failed to canonicalize `{}`", value.display()))?; | ||
value | ||
.into_os_string() | ||
.into_string() | ||
.map_err(VarError::NotUnicode)? | ||
.into() | ||
} | ||
Self::String(value) | Self::Value { value, .. } => value.into(), | ||
}) | ||
} | ||
} | ||
|
||
#[test] | ||
fn test_env_parsing() { | ||
let toml = r#" | ||
[env] | ||
# Set ENV_VAR_NAME=value for any process run by Cargo | ||
ENV_VAR_NAME = "value" | ||
# Set even if already present in environment | ||
ENV_VAR_NAME_2 = { value = "value", force = true } | ||
# Value is relative to .cargo directory containing `config.toml`, make absolute | ||
ENV_VAR_NAME_3 = { value = "relative/path", relative = true }"#; | ||
|
||
let mut env = BTreeMap::new(); | ||
env.insert( | ||
"ENV_VAR_NAME".to_string(), | ||
EnvOption::String("value".into()), | ||
); | ||
env.insert( | ||
"ENV_VAR_NAME_2".to_string(), | ||
EnvOption::Value { | ||
value: "value".into(), | ||
force: true, | ||
relative: false, | ||
}, | ||
); | ||
env.insert( | ||
"ENV_VAR_NAME_3".to_string(), | ||
EnvOption::Value { | ||
value: "relative/path".into(), | ||
force: false, | ||
relative: true, | ||
}, | ||
); | ||
|
||
assert_eq!( | ||
toml::from_str::<Config>(toml), | ||
Ok(Config { | ||
build: None, | ||
env: Some(env) | ||
}) | ||
); | ||
} | ||
|
||
#[test] | ||
fn test_env_precedence_rules() { | ||
let toml = r#" | ||
[env] | ||
CARGO_SUBCOMMAND_TEST_ENV_NOT_FORCED = "not forced" | ||
CARGO_SUBCOMMAND_TEST_ENV_FORCED = { value = "forced", force = true }"#; | ||
|
||
let config = LocalizedConfig { | ||
config: toml::from_str::<Config>(toml).unwrap(), | ||
workspace: PathBuf::new(), | ||
}; | ||
|
||
// Check if all values are propagated to the environment | ||
config.set_env_vars().unwrap(); | ||
|
||
assert!(matches!( | ||
std::env::var("CARGO_SUBCOMMAND_TEST_ENV_NOT_SET"), | ||
Err(VarError::NotPresent) | ||
)); | ||
assert_eq!( | ||
std::env::var("CARGO_SUBCOMMAND_TEST_ENV_NOT_FORCED").unwrap(), | ||
"not forced" | ||
); | ||
assert_eq!( | ||
std::env::var("CARGO_SUBCOMMAND_TEST_ENV_FORCED").unwrap(), | ||
"forced" | ||
); | ||
|
||
// Set some environment values | ||
std::env::set_var( | ||
"CARGO_SUBCOMMAND_TEST_ENV_NOT_FORCED", | ||
"not forced process environment value", | ||
); | ||
std::env::set_var( | ||
"CARGO_SUBCOMMAND_TEST_ENV_FORCED", | ||
"forced process environment value", | ||
); | ||
|
||
config.set_env_vars().unwrap(); | ||
|
||
assert_eq!( | ||
std::env::var("CARGO_SUBCOMMAND_TEST_ENV_NOT_FORCED").unwrap(), | ||
// Value remains what is set in the process environment, | ||
// and is not overwritten by set_env_vars() | ||
"not forced process environment value" | ||
); | ||
assert_eq!( | ||
std::env::var("CARGO_SUBCOMMAND_TEST_ENV_FORCED").unwrap(), | ||
// Value is overwritten thanks to force=true, despite | ||
// also being set in the process environment | ||
"forced" | ||
); | ||
} | ||
|
||
#[test] | ||
fn test_env_canonicalization() { | ||
use std::ffi::OsStr; | ||
|
||
let toml = r#" | ||
[env] | ||
CARGO_SUBCOMMAND_TEST_ENV_SRC_DIR = { value = "src", force = true, relative = true } | ||
"#; | ||
|
||
let config = LocalizedConfig { | ||
config: toml::from_str::<Config>(toml).unwrap(), | ||
workspace: PathBuf::new(), | ||
}; | ||
|
||
config.set_env_vars().unwrap(); | ||
|
||
let path = std::env::var("CARGO_SUBCOMMAND_TEST_ENV_SRC_DIR") | ||
.expect("Canonicalization for a known-to-exist ./src folder should not fail"); | ||
let path = PathBuf::from(path); | ||
assert!(path.is_absolute()); | ||
assert!(path.is_dir()); | ||
assert_eq!(path.file_name(), Some(OsStr::new("src"))); | ||
|
||
let toml = r#" | ||
[env] | ||
CARGO_SUBCOMMAND_TEST_ENV_INEXISTENT_DIR = { value = "blahblahthisfolderdoesntexist", force = true, relative = true } | ||
"#; | ||
|
||
let config = LocalizedConfig { | ||
config: toml::from_str::<Config>(toml).unwrap(), | ||
workspace: PathBuf::new(), | ||
}; | ||
|
||
let e = config.set_env_vars().unwrap_err(); | ||
|
||
assert_eq!( | ||
e.to_string(), | ||
"Failed to canonicalize `blahblahthisfolderdoesntexist`" | ||
); | ||
assert_eq!( | ||
e.root_cause().to_string(), | ||
"No such file or directory (os error 2)" | ||
); | ||
} |
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