Skip to content

Commit adc955f

Browse files
committed
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 adc955f

File tree

4 files changed

+278
-23
lines changed

4 files changed

+278
-23
lines changed

xbuild/src/cargo/config.rs

+245-8
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,270 @@
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.
50+
// TODO: Find, parse, and merge _all_ config files following the hierarchical structure:
51+
// https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure
1752
pub fn find_cargo_config_for_workspace(workspace: impl AsRef<Path>) -> Result<Option<Self>> {
1853
let workspace = workspace.as_ref();
1954
let workspace = dunce::canonicalize(workspace)?;
2055
workspace
2156
.ancestors()
22-
.map(|dir| dir.join(".cargo/config.toml"))
23-
.find(|p| p.is_file())
24-
.map(Config::parse_from_toml)
57+
.find(|dir| dir.join(".cargo/config.toml").is_file())
58+
.map(|p| p.to_path_buf())
59+
.map(Self::new)
2560
.transpose()
2661
}
62+
63+
/// Propagate environment variables from this `.cargo/config.toml` to the process environment
64+
/// using [`std::env::set_var()`].
65+
///
66+
/// Note that this is automatically performed when calling [`Subcommand::new()`][super::Subcommand::new()].
67+
pub fn set_env_vars(&self) -> Result<()> {
68+
if let Some(env) = &self.config.env {
69+
for (key, env_option) in env {
70+
// Existing environment variables always have precedence unless
71+
// the extended format is used to set `force = true`:
72+
if !matches!(env_option, EnvOption::Value { force: true, .. })
73+
&& std::env::var_os(key).is_some()
74+
{
75+
continue;
76+
}
77+
78+
std::env::set_var(key, env_option.resolve_value(&self.workspace)?.as_ref())
79+
}
80+
}
81+
82+
Ok(())
83+
}
2784
}
2885

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

xbuild/src/cargo/mod.rs

+7-10
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ use std::path::{Path, PathBuf};
44
use std::process::Command;
55

66
mod artifact;
7-
mod config;
7+
pub mod config;
88
pub mod manifest;
99
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

@@ -72,13 +72,10 @@ impl Cargo {
7272

7373
let package_root = manifest_path.parent().unwrap();
7474

75-
// TODO: Find, parse, and merge _all_ config files following the hierarchical structure:
76-
// 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-
// }
75+
let config = LocalizedConfig::find_cargo_config_for_workspace(package_root)?;
76+
if let Some(config) = &config {
77+
config.set_env_vars()?;
78+
}
8279

8380
let target_dir = target_dir
8481
.or_else(|| {
@@ -101,7 +98,7 @@ impl Cargo {
10198
.unwrap_or_else(|| &manifest_path)
10299
.parent()
103100
.unwrap()
104-
.join(utils::get_target_dir_name(config.as_ref()).unwrap())
101+
.join(utils::get_target_dir_name(config.as_deref()).unwrap())
105102
});
106103

107104
Ok(Self {

xbuild/src/lib.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ macro_rules! exe {
1717
};
1818
}
1919

20-
mod cargo;
20+
pub mod cargo;
2121
pub mod command;
2222
mod config;
2323
mod devices;
@@ -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

xbuild/src/main.rs

+21-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use anyhow::Result;
22
use app_store_connect::certs_api::CertificateType;
33
use clap::{Parser, Subcommand};
44
use std::path::PathBuf;
5-
use xbuild::{command, BuildArgs, BuildEnv};
5+
use xbuild::{cargo::config::LocalizedConfig, command, BuildArgs, BuildEnv};
66

77
#[derive(Parser)]
88
#[clap(author, version, about, long_about = None)]
@@ -76,12 +76,30 @@ enum Commands {
7676
},
7777
}
7878

79+
/// Setup a partial build environment (e.g. read `[env]` from `.cargo/config.toml`) when there is
80+
/// no crate/manifest selected. Pretend `$PWD` is the workspace.
81+
///
82+
/// Only necessary for apps that don't call [`BuildEnv::new()`],
83+
fn partial_build_env() -> Result<()> {
84+
let config = LocalizedConfig::find_cargo_config_for_workspace(".")?;
85+
if let Some(config) = &config {
86+
config.set_env_vars()?;
87+
}
88+
Ok(())
89+
}
90+
7991
impl Commands {
8092
pub fn run(self) -> Result<()> {
8193
match self {
8294
Self::New { name } => command::new(&name)?,
83-
Self::Doctor => command::doctor(),
84-
Self::Devices => command::devices()?,
95+
Self::Doctor => {
96+
partial_build_env()?;
97+
command::doctor()
98+
}
99+
Self::Devices => {
100+
partial_build_env()?;
101+
command::devices()?
102+
}
85103
Self::Build { args } => {
86104
let env = BuildEnv::new(args)?;
87105
command::build(&env)?;

0 commit comments

Comments
 (0)