Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 2f3aefb

Browse files
committedMay 15, 2023
Propagate .cargo/config.toml [env] settings to the process environment
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
1 parent 875f933 commit 2f3aefb

File tree

3 files changed

+253
-16
lines changed

3 files changed

+253
-16
lines changed
 

‎xbuild/src/cargo/config.rs

+243-8
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,268 @@
1-
use anyhow::Result;
1+
use anyhow::{Context, Result};
22
use serde::Deserialize;
3-
use std::path::Path;
3+
use std::{
4+
borrow::Cow,
5+
collections::BTreeMap,
6+
env::VarError,
7+
ops::Deref,
8+
path::{Path, PathBuf},
9+
};
410

5-
#[derive(Debug, Deserialize)]
11+
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
12+
#[serde(rename_all = "kebab-case")]
613
pub struct Config {
714
pub build: Option<Build>,
15+
/// <https://doc.rust-lang.org/cargo/reference/config.html#env>
16+
pub env: Option<BTreeMap<String, EnvOption>>,
817
}
918

1019
impl Config {
1120
pub fn parse_from_toml(path: impl AsRef<Path>) -> Result<Self> {
1221
let contents = std::fs::read_to_string(path)?;
1322
Ok(toml::from_str(&contents)?)
1423
}
24+
}
25+
26+
#[derive(Debug)]
27+
pub struct LocalizedConfig {
28+
pub config: Config,
29+
/// The directory containing `./.cargo/config.toml`
30+
pub workspace: PathBuf,
31+
}
32+
33+
impl Deref for LocalizedConfig {
34+
type Target = Config;
35+
36+
fn deref(&self) -> &Self::Target {
37+
&self.config
38+
}
39+
}
40+
41+
impl LocalizedConfig {
42+
pub fn new(workspace: PathBuf) -> Result<Self> {
43+
Ok(Self {
44+
config: Config::parse_from_toml(workspace.join(".cargo/config.toml"))?,
45+
workspace,
46+
})
47+
}
1548

1649
/// Search for and open `.cargo/config.toml` in any parent of the workspace root path.
1750
pub fn find_cargo_config_for_workspace(workspace: impl AsRef<Path>) -> Result<Option<Self>> {
1851
let workspace = workspace.as_ref();
1952
let workspace = dunce::canonicalize(workspace)?;
2053
workspace
2154
.ancestors()
22-
.map(|dir| dir.join(".cargo/config.toml"))
23-
.find(|p| p.is_file())
24-
.map(Config::parse_from_toml)
55+
.find(|dir| dir.join(".cargo/config.toml").is_file())
56+
.map(|p| p.to_path_buf())
57+
.map(Self::new)
2558
.transpose()
2659
}
60+
61+
/// Propagate environment variables from this `.cargo/config.toml` to the process environment
62+
/// using [`std::env::set_var()`].
63+
///
64+
/// Note that this is automatically performed when calling [`Subcommand::new()`][super::Subcommand::new()].
65+
pub fn set_env_vars(&self) -> Result<()> {
66+
if let Some(env) = &self.config.env {
67+
for (key, env_option) in env {
68+
// Existing environment variables always have precedence unless
69+
// the extended format is used to set `force = true`:
70+
if !matches!(env_option, EnvOption::Value { force: true, .. })
71+
&& std::env::var_os(key).is_some()
72+
{
73+
continue;
74+
}
75+
76+
std::env::set_var(key, env_option.resolve_value(&self.workspace)?.as_ref())
77+
}
78+
}
79+
80+
Ok(())
81+
}
2782
}
2883

29-
#[derive(Debug, Deserialize)]
84+
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
3085
pub struct Build {
31-
#[serde(rename = "target-dir")]
3286
pub target_dir: Option<String>,
3387
}
88+
89+
/// Serializable environment variable in cargo config, configurable as per
90+
/// <https://doc.rust-lang.org/cargo/reference/config.html#env>,
91+
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
92+
#[serde(untagged)]
93+
pub enum EnvOption {
94+
String(String),
95+
Value {
96+
value: String,
97+
#[serde(default)]
98+
force: bool,
99+
#[serde(default)]
100+
relative: bool,
101+
},
102+
}
103+
104+
impl EnvOption {
105+
/// Retrieve the value and canonicalize it relative to `config_parent` when [`EnvOption::Value::relative`] is set.
106+
///
107+
/// `config_parent` is the directory containing `.cargo/config.toml` where this was parsed from.
108+
pub fn resolve_value(&self, config_parent: impl AsRef<Path>) -> Result<Cow<'_, str>> {
109+
Ok(match self {
110+
Self::Value {
111+
value,
112+
relative: true,
113+
force: _,
114+
} => {
115+
let value = config_parent.as_ref().join(value);
116+
let value = dunce::canonicalize(&value)
117+
.with_context(|| format!("Failed to canonicalize `{}`", value.display()))?;
118+
value
119+
.into_os_string()
120+
.into_string()
121+
.map_err(VarError::NotUnicode)?
122+
.into()
123+
}
124+
Self::String(value) | Self::Value { value, .. } => value.into(),
125+
})
126+
}
127+
}
128+
129+
#[test]
130+
fn test_env_parsing() {
131+
let toml = r#"
132+
[env]
133+
# Set ENV_VAR_NAME=value for any process run by Cargo
134+
ENV_VAR_NAME = "value"
135+
# Set even if already present in environment
136+
ENV_VAR_NAME_2 = { value = "value", force = true }
137+
# Value is relative to .cargo directory containing `config.toml`, make absolute
138+
ENV_VAR_NAME_3 = { value = "relative/path", relative = true }"#;
139+
140+
let mut env = BTreeMap::new();
141+
env.insert(
142+
"ENV_VAR_NAME".to_string(),
143+
EnvOption::String("value".into()),
144+
);
145+
env.insert(
146+
"ENV_VAR_NAME_2".to_string(),
147+
EnvOption::Value {
148+
value: "value".into(),
149+
force: true,
150+
relative: false,
151+
},
152+
);
153+
env.insert(
154+
"ENV_VAR_NAME_3".to_string(),
155+
EnvOption::Value {
156+
value: "relative/path".into(),
157+
force: false,
158+
relative: true,
159+
},
160+
);
161+
162+
assert_eq!(
163+
toml::from_str::<Config>(toml),
164+
Ok(Config {
165+
build: None,
166+
env: Some(env)
167+
})
168+
);
169+
}
170+
171+
#[test]
172+
fn test_env_precedence_rules() {
173+
let toml = r#"
174+
[env]
175+
CARGO_SUBCOMMAND_TEST_ENV_NOT_FORCED = "not forced"
176+
CARGO_SUBCOMMAND_TEST_ENV_FORCED = { value = "forced", force = true }"#;
177+
178+
let config = LocalizedConfig {
179+
config: toml::from_str::<Config>(toml).unwrap(),
180+
workspace: PathBuf::new(),
181+
};
182+
183+
// Check if all values are propagated to the environment
184+
config.set_env_vars().unwrap();
185+
186+
assert!(matches!(
187+
std::env::var("CARGO_SUBCOMMAND_TEST_ENV_NOT_SET"),
188+
Err(VarError::NotPresent)
189+
));
190+
assert_eq!(
191+
std::env::var("CARGO_SUBCOMMAND_TEST_ENV_NOT_FORCED").unwrap(),
192+
"not forced"
193+
);
194+
assert_eq!(
195+
std::env::var("CARGO_SUBCOMMAND_TEST_ENV_FORCED").unwrap(),
196+
"forced"
197+
);
198+
199+
// Set some environment values
200+
std::env::set_var(
201+
"CARGO_SUBCOMMAND_TEST_ENV_NOT_FORCED",
202+
"not forced process environment value",
203+
);
204+
std::env::set_var(
205+
"CARGO_SUBCOMMAND_TEST_ENV_FORCED",
206+
"forced process environment value",
207+
);
208+
209+
config.set_env_vars().unwrap();
210+
211+
assert_eq!(
212+
std::env::var("CARGO_SUBCOMMAND_TEST_ENV_NOT_FORCED").unwrap(),
213+
// Value remains what is set in the process environment,
214+
// and is not overwritten by set_env_vars()
215+
"not forced process environment value"
216+
);
217+
assert_eq!(
218+
std::env::var("CARGO_SUBCOMMAND_TEST_ENV_FORCED").unwrap(),
219+
// Value is overwritten thanks to force=true, despite
220+
// also being set in the process environment
221+
"forced"
222+
);
223+
}
224+
225+
#[test]
226+
fn test_env_canonicalization() {
227+
use std::ffi::OsStr;
228+
229+
let toml = r#"
230+
[env]
231+
CARGO_SUBCOMMAND_TEST_ENV_SRC_DIR = { value = "src", force = true, relative = true }
232+
"#;
233+
234+
let config = LocalizedConfig {
235+
config: toml::from_str::<Config>(toml).unwrap(),
236+
workspace: PathBuf::new(),
237+
};
238+
239+
config.set_env_vars().unwrap();
240+
241+
let path = std::env::var("CARGO_SUBCOMMAND_TEST_ENV_SRC_DIR")
242+
.expect("Canonicalization for a known-to-exist ./src folder should not fail");
243+
let path = PathBuf::from(path);
244+
assert!(path.is_absolute());
245+
assert!(path.is_dir());
246+
assert_eq!(path.file_name(), Some(OsStr::new("src")));
247+
248+
let toml = r#"
249+
[env]
250+
CARGO_SUBCOMMAND_TEST_ENV_INEXISTENT_DIR = { value = "blahblahthisfolderdoesntexist", force = true, relative = true }
251+
"#;
252+
253+
let config = LocalizedConfig {
254+
config: toml::from_str::<Config>(toml).unwrap(),
255+
workspace: PathBuf::new(),
256+
};
257+
258+
let e = config.set_env_vars().unwrap_err();
259+
260+
assert_eq!(
261+
e.to_string(),
262+
"Failed to canonicalize `blahblahthisfolderdoesntexist`"
263+
);
264+
assert_eq!(
265+
e.root_cause().to_string(),
266+
"No such file or directory (os error 2)"
267+
);
268+
}

‎xbuild/src/cargo/mod.rs

+6-7
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ mod utils;
1010

1111
pub use artifact::{Artifact, CrateType};
1212

13-
use self::config::Config;
13+
use self::config::LocalizedConfig;
1414
use self::manifest::Manifest;
1515
use crate::{CompileTarget, Opt};
1616

@@ -74,11 +74,10 @@ impl Cargo {
7474

7575
// TODO: Find, parse, and merge _all_ config files following the hierarchical structure:
7676
// https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure
77-
let config = Config::find_cargo_config_for_workspace(package_root)?;
78-
// TODO: Import LocalizedConfig code from cargo-subcommand and propagate `[env]`
79-
// if let Some(config) = &config {
80-
// config.set_env_vars().unwrap();
81-
// }
77+
let config = LocalizedConfig::find_cargo_config_for_workspace(package_root)?;
78+
if let Some(config) = &config {
79+
config.set_env_vars().unwrap();
80+
}
8281

8382
let target_dir = target_dir
8483
.or_else(|| {
@@ -101,7 +100,7 @@ impl Cargo {
101100
.unwrap_or_else(|| &manifest_path)
102101
.parent()
103102
.unwrap()
104-
.join(utils::get_target_dir_name(config.as_ref()).unwrap())
103+
.join(utils::get_target_dir_name(config.as_deref()).unwrap())
105104
});
106105

107106
Ok(Self {

‎xbuild/src/lib.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ pub struct BuildTargetArgs {
387387
/// Build artifacts for target device. To find the device
388388
/// identifier of a connected device run `x devices`.
389389
#[clap(long, conflicts_with = "store")]
390-
device: Option<Device>,
390+
device: Option<String>,
391391
/// Build artifacts with format. Can be one of `aab`,
392392
/// `apk`, `appbundle`, `appdir`, `appimage`, `dmg`,
393393
/// `exe`, `ipa`, `msix`.
@@ -424,6 +424,9 @@ impl BuildTargetArgs {
424424
Some(Device::host())
425425
} else {
426426
self.device
427+
.as_ref()
428+
.map(|device| device.parse())
429+
.transpose()?
427430
};
428431
let platform = if let Some(platform) = self.platform {
429432
platform

0 commit comments

Comments
 (0)
Please sign in to comment.