Skip to content

Commit 203d111

Browse files
authored
Propagate .cargo/config.toml [env] settings to the process environment (#117)
* 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 * cargo/config: Don't canonicalize joined `[env]` `relative` paths Cargo doesn't do this either, and canonicalization requires the path to exist which it does not have to.
1 parent 165e3c0 commit 203d111

File tree

4 files changed

+254
-22
lines changed

4 files changed

+254
-22
lines changed

xbuild/src/cargo/config.rs

+221-7
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,247 @@
11
use anyhow::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 join it 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+
} => config_parent
117+
.as_ref()
118+
.join(value)
119+
.into_os_string()
120+
.into_string()
121+
.map_err(VarError::NotUnicode)?
122+
.into(),
123+
Self::String(value) | Self::Value { value, .. } => value.into(),
124+
})
125+
}
126+
}
127+
128+
#[test]
129+
fn test_env_parsing() {
130+
let toml = r#"
131+
[env]
132+
# Set ENV_VAR_NAME=value for any process run by Cargo
133+
ENV_VAR_NAME = "value"
134+
# Set even if already present in environment
135+
ENV_VAR_NAME_2 = { value = "value", force = true }
136+
# Value is relative to .cargo directory containing `config.toml`, make absolute
137+
ENV_VAR_NAME_3 = { value = "relative/path", relative = true }"#;
138+
139+
let mut env = BTreeMap::new();
140+
env.insert(
141+
"ENV_VAR_NAME".to_string(),
142+
EnvOption::String("value".into()),
143+
);
144+
env.insert(
145+
"ENV_VAR_NAME_2".to_string(),
146+
EnvOption::Value {
147+
value: "value".into(),
148+
force: true,
149+
relative: false,
150+
},
151+
);
152+
env.insert(
153+
"ENV_VAR_NAME_3".to_string(),
154+
EnvOption::Value {
155+
value: "relative/path".into(),
156+
force: false,
157+
relative: true,
158+
},
159+
);
160+
161+
assert_eq!(
162+
toml::from_str::<Config>(toml),
163+
Ok(Config {
164+
build: None,
165+
env: Some(env)
166+
})
167+
);
168+
}
169+
170+
#[test]
171+
fn test_env_precedence_rules() {
172+
let toml = r#"
173+
[env]
174+
CARGO_SUBCOMMAND_TEST_ENV_NOT_FORCED = "not forced"
175+
CARGO_SUBCOMMAND_TEST_ENV_FORCED = { value = "forced", force = true }"#;
176+
177+
let config = LocalizedConfig {
178+
config: toml::from_str::<Config>(toml).unwrap(),
179+
workspace: PathBuf::new(),
180+
};
181+
182+
// Check if all values are propagated to the environment
183+
config.set_env_vars().unwrap();
184+
185+
assert!(matches!(
186+
std::env::var("CARGO_SUBCOMMAND_TEST_ENV_NOT_SET"),
187+
Err(VarError::NotPresent)
188+
));
189+
assert_eq!(
190+
std::env::var("CARGO_SUBCOMMAND_TEST_ENV_NOT_FORCED").unwrap(),
191+
"not forced"
192+
);
193+
assert_eq!(
194+
std::env::var("CARGO_SUBCOMMAND_TEST_ENV_FORCED").unwrap(),
195+
"forced"
196+
);
197+
198+
// Set some environment values
199+
std::env::set_var(
200+
"CARGO_SUBCOMMAND_TEST_ENV_NOT_FORCED",
201+
"not forced process environment value",
202+
);
203+
std::env::set_var(
204+
"CARGO_SUBCOMMAND_TEST_ENV_FORCED",
205+
"forced process environment value",
206+
);
207+
208+
config.set_env_vars().unwrap();
209+
210+
assert_eq!(
211+
std::env::var("CARGO_SUBCOMMAND_TEST_ENV_NOT_FORCED").unwrap(),
212+
// Value remains what is set in the process environment,
213+
// and is not overwritten by set_env_vars()
214+
"not forced process environment value"
215+
);
216+
assert_eq!(
217+
std::env::var("CARGO_SUBCOMMAND_TEST_ENV_FORCED").unwrap(),
218+
// Value is overwritten thanks to force=true, despite
219+
// also being set in the process environment
220+
"forced"
221+
);
222+
}
223+
224+
#[test]
225+
fn test_env_relative() {
226+
let toml = r#"
227+
[env]
228+
CARGO_SUBCOMMAND_TEST_ENV_SRC_DIR = { value = "src", force = true, relative = true }
229+
"#;
230+
231+
let config = LocalizedConfig {
232+
config: toml::from_str::<Config>(toml).unwrap(),
233+
// Path does not have to exist
234+
workspace: PathBuf::from("my/work/space"),
235+
};
236+
237+
config.set_env_vars().unwrap();
238+
239+
let path = std::env::var("CARGO_SUBCOMMAND_TEST_ENV_SRC_DIR")
240+
.expect("Relative env var should always be set");
241+
let path = PathBuf::from(path);
242+
assert!(
243+
path.is_relative(),
244+
"Workspace was not absolute so this shouldn't either"
245+
);
246+
assert_eq!(path, Path::new("my/work/space/src"));
247+
}

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)