1use crate::error::Error;
2use serde::Deserialize;
3use std::{
4 borrow::Cow,
5 collections::BTreeMap,
6 env::VarError,
7 fmt::{self, Display, Formatter},
8 io,
9 ops::Deref,
10 path::{Path, PathBuf},
11};
12
13#[derive(Debug)]
15pub enum EnvError {
16 Io(PathBuf, io::Error),
17 Var(VarError),
18}
19
20pub type Result<T, E = EnvError> = std::result::Result<T, E>;
21
22impl From<VarError> for EnvError {
23 fn from(var: VarError) -> Self {
24 Self::Var(var)
25 }
26}
27
28impl Display for EnvError {
29 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
30 match self {
31 Self::Io(path, error) => write!(f, "{}: {}", path.display(), error),
32 Self::Var(error) => error.fmt(f),
33 }
34 }
35}
36
37impl std::error::Error for EnvError {}
38
39#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
40#[serde(rename_all = "kebab-case")]
41pub struct Config {
42 pub build: Option<Build>,
43 pub env: Option<BTreeMap<String, EnvOption>>,
45}
46
47impl Config {
48 pub fn parse_from_toml(path: &Path) -> Result<Self, Error> {
49 let contents = std::fs::read_to_string(path).map_err(|e| Error::Io(path.to_owned(), e))?;
50 toml::from_str(&contents).map_err(|e| Error::Toml(path.to_owned(), e))
51 }
52}
53
54#[derive(Clone, Debug)]
55pub struct LocalizedConfig {
56 pub config: Config,
57 pub workspace: PathBuf,
59}
60
61impl Deref for LocalizedConfig {
62 type Target = Config;
63
64 fn deref(&self) -> &Self::Target {
65 &self.config
66 }
67}
68
69impl LocalizedConfig {
70 pub fn new(workspace: PathBuf) -> Result<Self, Error> {
71 Ok(Self {
72 config: Config::parse_from_toml(&workspace.join(".cargo/config.toml"))?,
73 workspace,
74 })
75 }
76
77 fn find_cargo_config_parent(workspace: impl AsRef<Path>) -> Result<Option<PathBuf>, Error> {
80 let workspace = workspace.as_ref();
81 let workspace =
82 dunce::canonicalize(workspace).map_err(|e| Error::Io(workspace.to_owned(), e))?;
83 Ok(workspace
84 .ancestors()
85 .find(|dir| dir.join(".cargo/config.toml").is_file())
86 .map(|p| p.to_path_buf()))
87 }
88
89 pub fn find_cargo_config_for_workspace(
91 workspace: impl AsRef<Path>,
92 ) -> Result<Option<Self>, Error> {
93 let config = Self::find_cargo_config_parent(workspace)?;
94 config.map(LocalizedConfig::new).transpose()
95 }
96
97 pub fn set_env_vars(&self) -> Result<()> {
102 if let Some(env) = &self.config.env {
103 for (key, env_option) in env {
104 if !matches!(env_option, EnvOption::Value { force: true, .. })
107 && std::env::var_os(key).is_some()
108 {
109 continue;
110 }
111
112 std::env::set_var(key, env_option.resolve_value(&self.workspace)?.as_ref())
113 }
114 }
115
116 Ok(())
117 }
118}
119
120#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
121#[serde(rename_all = "kebab-case")]
122pub struct Build {
123 pub target_dir: Option<String>,
124}
125
126#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
129#[serde(untagged, rename_all = "kebab-case")]
130pub enum EnvOption {
131 String(String),
132 Value {
133 value: String,
134 #[serde(default)]
135 force: bool,
136 #[serde(default)]
137 relative: bool,
138 },
139}
140
141impl EnvOption {
142 pub fn resolve_value(&self, config_parent: impl AsRef<Path>) -> Result<Cow<'_, str>> {
146 Ok(match self {
147 Self::Value {
148 value,
149 relative: true,
150 force: _,
151 } => {
152 let value = config_parent.as_ref().join(value);
153 let value = dunce::canonicalize(&value).map_err(|e| EnvError::Io(value, e))?;
154 value
155 .into_os_string()
156 .into_string()
157 .map_err(VarError::NotUnicode)?
158 .into()
159 }
160 Self::String(value) | Self::Value { value, .. } => value.into(),
161 })
162 }
163}
164
165#[test]
166fn test_env_parsing() {
167 let toml = r#"
168[env]
169# Set ENV_VAR_NAME=value for any process run by Cargo
170ENV_VAR_NAME = "value"
171# Set even if already present in environment
172ENV_VAR_NAME_2 = { value = "value", force = true }
173# Value is relative to .cargo directory containing `config.toml`, make absolute
174ENV_VAR_NAME_3 = { value = "relative/path", relative = true }"#;
175
176 let mut env = BTreeMap::new();
177 env.insert(
178 "ENV_VAR_NAME".to_string(),
179 EnvOption::String("value".into()),
180 );
181 env.insert(
182 "ENV_VAR_NAME_2".to_string(),
183 EnvOption::Value {
184 value: "value".into(),
185 force: true,
186 relative: false,
187 },
188 );
189 env.insert(
190 "ENV_VAR_NAME_3".to_string(),
191 EnvOption::Value {
192 value: "relative/path".into(),
193 force: false,
194 relative: true,
195 },
196 );
197
198 assert_eq!(
199 toml::from_str::<Config>(toml),
200 Ok(Config {
201 build: None,
202 env: Some(env)
203 })
204 );
205}
206
207#[test]
208fn test_env_precedence_rules() {
209 let toml = r#"
210[env]
211CARGO_SUBCOMMAND_TEST_ENV_NOT_FORCED = "not forced"
212CARGO_SUBCOMMAND_TEST_ENV_FORCED = { value = "forced", force = true }"#;
213
214 let config = LocalizedConfig {
215 config: toml::from_str::<Config>(toml).unwrap(),
216 workspace: PathBuf::new(),
217 };
218
219 config.set_env_vars().unwrap();
221
222 assert!(matches!(
223 std::env::var("CARGO_SUBCOMMAND_TEST_ENV_NOT_SET"),
224 Err(VarError::NotPresent)
225 ));
226 assert_eq!(
227 std::env::var("CARGO_SUBCOMMAND_TEST_ENV_NOT_FORCED").unwrap(),
228 "not forced"
229 );
230 assert_eq!(
231 std::env::var("CARGO_SUBCOMMAND_TEST_ENV_FORCED").unwrap(),
232 "forced"
233 );
234
235 std::env::set_var(
237 "CARGO_SUBCOMMAND_TEST_ENV_NOT_FORCED",
238 "not forced process environment value",
239 );
240 std::env::set_var(
241 "CARGO_SUBCOMMAND_TEST_ENV_FORCED",
242 "forced process environment value",
243 );
244
245 config.set_env_vars().unwrap();
246
247 assert_eq!(
248 std::env::var("CARGO_SUBCOMMAND_TEST_ENV_NOT_FORCED").unwrap(),
249 "not forced process environment value"
252 );
253 assert_eq!(
254 std::env::var("CARGO_SUBCOMMAND_TEST_ENV_FORCED").unwrap(),
255 "forced"
258 );
259}
260
261#[test]
262fn test_env_canonicalization() {
263 use std::ffi::OsStr;
264
265 let toml = r#"
266[env]
267CARGO_SUBCOMMAND_TEST_ENV_SRC_DIR = { value = "src", force = true, relative = true }
268"#;
269
270 let config = LocalizedConfig {
271 config: toml::from_str::<Config>(toml).unwrap(),
272 workspace: PathBuf::new(),
273 };
274
275 config.set_env_vars().unwrap();
276
277 let path = std::env::var("CARGO_SUBCOMMAND_TEST_ENV_SRC_DIR")
278 .expect("Canonicalization for a known-to-exist ./src folder should not fail");
279 let path = PathBuf::from(path);
280 assert!(path.is_absolute());
281 assert!(path.is_dir());
282 assert_eq!(path.file_name(), Some(OsStr::new("src")));
283
284 let toml = r#"
285[env]
286CARGO_SUBCOMMAND_TEST_ENV_INEXISTENT_DIR = { value = "blahblahthisfolderdoesntexist", force = true, relative = true }
287"#;
288
289 let config = LocalizedConfig {
290 config: toml::from_str::<Config>(toml).unwrap(),
291 workspace: PathBuf::new(),
292 };
293
294 assert!(matches!(config.set_env_vars(), Err(EnvError::Io(..))));
295}