1
1
use crate :: error:: Error ;
2
2
use serde:: Deserialize ;
3
- use std:: path:: Path ;
3
+ use 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
+ } ;
4
12
5
- #[ derive( Debug , Deserialize ) ]
13
+ /// Specific errors that can be raised during environment parsing
14
+ #[ derive( Debug ) ]
15
+ pub enum EnvError {
16
+ Io ( PathBuf , io:: Error ) ,
17
+ Var ( VarError ) ,
18
+ }
19
+
20
+ impl From < VarError > for EnvError {
21
+ fn from ( var : VarError ) -> Self {
22
+ Self :: Var ( var)
23
+ }
24
+ }
25
+
26
+ impl Display for EnvError {
27
+ fn fmt ( & self , f : & mut Formatter ) -> fmt:: Result {
28
+ match self {
29
+ Self :: Io ( path, error) => write ! ( f, "{}: {}" , path. display( ) , error) ,
30
+ Self :: Var ( error) => error. fmt ( f) ,
31
+ }
32
+ }
33
+ }
34
+
35
+ impl std:: error:: Error for EnvError { }
36
+
37
+ type Result < T , E = EnvError > = std:: result:: Result < T , E > ;
38
+
39
+ #[ derive( Clone , Debug , Deserialize , PartialEq ) ]
40
+ #[ serde( rename_all = "kebab-case" ) ]
6
41
pub struct Config {
7
42
pub build : Option < Build > ,
43
+ /// <https://doc.rust-lang.org/cargo/reference/config.html#env>
44
+ pub env : Option < BTreeMap < String , EnvOption > > ,
8
45
}
9
46
10
47
impl Config {
@@ -14,8 +51,254 @@ impl Config {
14
51
}
15
52
}
16
53
17
- #[ derive( Debug , Deserialize ) ]
54
+ #[ derive( Clone , Debug ) ]
55
+ pub struct LocalizedConfig {
56
+ pub config : Config ,
57
+ /// The directory containing `./.cargo/config.toml`
58
+ pub workspace : PathBuf ,
59
+ }
60
+
61
+ impl Deref for LocalizedConfig {
62
+ type Target = Config ;
63
+
64
+ fn deref ( & self ) -> & Self :: Target {
65
+ & self . config
66
+ }
67
+ }
68
+
69
+ impl 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
+ /// Search for `.cargo/config.toml` in any parent of the workspace root path.
78
+ /// Returns the directory which contains this path, not the path to the config file.
79
+ 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
+ /// Search for and open `.cargo/config.toml` in any parent of the workspace root path.
90
+ 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
+ /// Read an environment variable from the `[env]` section in this `.cargo/config.toml`.
98
+ ///
99
+ /// It is interpreted as path and canonicalized relative to [`Self::workspace`] if
100
+ /// [`EnvOption::Value::relative`] is set.
101
+ ///
102
+ /// Process environment variables (from [`std::env::var()`]) have [precedence]
103
+ /// unless [`EnvOption::Value::force`] is set. This value is also returned if
104
+ /// the given key was not set under `[env]`.
105
+ ///
106
+ /// [precedence]: https://doc.rust-lang.org/cargo/reference/config.html#env
107
+ pub fn resolve_env ( & self , key : & str ) -> Result < Cow < ' _ , str > > {
108
+ let config_var = self . config . env . as_ref ( ) . and_then ( |env| env. get ( key) ) ;
109
+
110
+ // Environment variables always have precedence unless
111
+ // the extended format is used to set `force = true`:
112
+ if let Some ( env_option @ EnvOption :: Value { force : true , .. } ) = config_var {
113
+ // Errors iresolving (canonicalizing, really) the config variable take precedence, too:
114
+ return env_option. resolve_value ( & self . workspace ) ;
115
+ }
116
+
117
+ let process_var = std:: env:: var ( key) ;
118
+ if process_var != Err ( VarError :: NotPresent ) {
119
+ // Errors from env::var() also have precedence here:
120
+ return Ok ( process_var?. into ( ) ) ;
121
+ }
122
+
123
+ // Finally, the value in `[env]` (if it exists) is taken into account
124
+ config_var
125
+ . ok_or ( VarError :: NotPresent ) ?
126
+ . resolve_value ( & self . workspace )
127
+ }
128
+ }
129
+
130
+ #[ derive( Clone , Debug , Deserialize , PartialEq ) ]
131
+ #[ serde( rename_all = "kebab-case" ) ]
18
132
pub struct Build {
19
- #[ serde( rename = "target-dir" ) ]
20
133
pub target_dir : Option < String > ,
21
134
}
135
+
136
+ /// Serializable environment variable in cargo config, configurable as per
137
+ /// <https://doc.rust-lang.org/cargo/reference/config.html#env>,
138
+ #[ derive( Clone , Debug , Deserialize , PartialEq ) ]
139
+ #[ serde( untagged, rename_all = "kebab-case" ) ]
140
+ pub enum EnvOption {
141
+ String ( String ) ,
142
+ Value {
143
+ value : String ,
144
+ #[ serde( default ) ]
145
+ force : bool ,
146
+ #[ serde( default ) ]
147
+ relative : bool ,
148
+ } ,
149
+ }
150
+
151
+ impl EnvOption {
152
+ /// Retrieve the value and canonicalize it relative to `config_parent` when [`EnvOption::Value::relative`] is set.
153
+ ///
154
+ /// `config_parent` is the directory containing `.cargo/config.toml` where this was parsed from.
155
+ pub fn resolve_value ( & self , config_parent : impl AsRef < Path > ) -> Result < Cow < ' _ , str > > {
156
+ Ok ( match self {
157
+ Self :: Value {
158
+ value,
159
+ relative : true ,
160
+ force : _,
161
+ } => {
162
+ let value = config_parent. as_ref ( ) . join ( value) ;
163
+ let value = dunce:: canonicalize ( & value) . map_err ( |e| EnvError :: Io ( value, e) ) ?;
164
+ value
165
+ . into_os_string ( )
166
+ . into_string ( )
167
+ . map_err ( VarError :: NotUnicode ) ?
168
+ . into ( )
169
+ }
170
+ Self :: String ( value) | Self :: Value { value, .. } => value. into ( ) ,
171
+ } )
172
+ }
173
+ }
174
+
175
+ #[ test]
176
+ fn test_env_parsing ( ) {
177
+ let toml = r#"
178
+ [env]
179
+ # Set ENV_VAR_NAME=value for any process run by Cargo
180
+ ENV_VAR_NAME = "value"
181
+ # Set even if already present in environment
182
+ ENV_VAR_NAME_2 = { value = "value", force = true }
183
+ # Value is relative to .cargo directory containing `config.toml`, make absolute
184
+ ENV_VAR_NAME_3 = { value = "relative/path", relative = true }"# ;
185
+
186
+ let mut env = BTreeMap :: new ( ) ;
187
+ env. insert (
188
+ "ENV_VAR_NAME" . to_string ( ) ,
189
+ EnvOption :: String ( "value" . into ( ) ) ,
190
+ ) ;
191
+ env. insert (
192
+ "ENV_VAR_NAME_2" . to_string ( ) ,
193
+ EnvOption :: Value {
194
+ value : "value" . into ( ) ,
195
+ force : true ,
196
+ relative : false ,
197
+ } ,
198
+ ) ;
199
+ env. insert (
200
+ "ENV_VAR_NAME_3" . to_string ( ) ,
201
+ EnvOption :: Value {
202
+ value : "relative/path" . into ( ) ,
203
+ force : false ,
204
+ relative : true ,
205
+ } ,
206
+ ) ;
207
+
208
+ assert_eq ! (
209
+ toml:: from_str:: <Config >( toml) ,
210
+ Ok ( Config {
211
+ build: None ,
212
+ env: Some ( env)
213
+ } )
214
+ ) ;
215
+ }
216
+
217
+ #[ test]
218
+ fn test_env_precedence_rules ( ) {
219
+ let toml = r#"
220
+ [env]
221
+ CARGO_SUBCOMMAND_TEST_ENV_NOT_FORCED = "not forced"
222
+ CARGO_SUBCOMMAND_TEST_ENV_FORCED = { value = "forced", force = true }"# ;
223
+
224
+ let config = LocalizedConfig {
225
+ config : toml:: from_str :: < Config > ( toml) . unwrap ( ) ,
226
+ workspace : PathBuf :: new ( ) ,
227
+ } ;
228
+
229
+ assert ! ( matches!(
230
+ config. resolve_env( "CARGO_SUBCOMMAND_TEST_ENV_NOT_SET" ) ,
231
+ Err ( EnvError :: Var ( VarError :: NotPresent ) )
232
+ ) ) ;
233
+ assert_eq ! (
234
+ config
235
+ . resolve_env( "CARGO_SUBCOMMAND_TEST_ENV_NOT_FORCED" )
236
+ . unwrap( ) ,
237
+ Cow :: from( "not forced" )
238
+ ) ;
239
+ assert_eq ! (
240
+ config
241
+ . resolve_env( "CARGO_SUBCOMMAND_TEST_ENV_FORCED" )
242
+ . unwrap( ) ,
243
+ Cow :: from( "forced" )
244
+ ) ;
245
+
246
+ std:: env:: set_var ( "CARGO_SUBCOMMAND_TEST_ENV_NOT_SET" , "set in env" ) ;
247
+ std:: env:: set_var (
248
+ "CARGO_SUBCOMMAND_TEST_ENV_NOT_FORCED" ,
249
+ "not forced overridden" ,
250
+ ) ;
251
+ std:: env:: set_var ( "CARGO_SUBCOMMAND_TEST_ENV_FORCED" , "forced overridden" ) ;
252
+
253
+ assert_eq ! (
254
+ config
255
+ . resolve_env( "CARGO_SUBCOMMAND_TEST_ENV_NOT_SET" )
256
+ . unwrap( ) ,
257
+ // Even if the value isn't present in [env] it should still resolve to the
258
+ // value in the process environment
259
+ Cow :: from( "set in env" )
260
+ ) ;
261
+ assert_eq ! (
262
+ config
263
+ . resolve_env( "CARGO_SUBCOMMAND_TEST_ENV_NOT_FORCED" )
264
+ . unwrap( ) ,
265
+ // Value changed now that it is set in the environment
266
+ Cow :: from( "not forced overridden" )
267
+ ) ;
268
+ assert_eq ! (
269
+ config
270
+ . resolve_env( "CARGO_SUBCOMMAND_TEST_ENV_FORCED" )
271
+ . unwrap( ) ,
272
+ // Value stays at how it was configured in [env] with force=true, despite
273
+ // also being set in the process environment
274
+ Cow :: from( "forced" )
275
+ ) ;
276
+ }
277
+
278
+ #[ test]
279
+ fn test_env_canonicalization ( ) {
280
+ use std:: ffi:: OsStr ;
281
+
282
+ let toml = r#"
283
+ [env]
284
+ CARGO_SUBCOMMAND_TEST_ENV_SRC_DIR = { value = "src", force = true, relative = true }
285
+ CARGO_SUBCOMMAND_TEST_ENV_INEXISTENT_DIR = { value = "blahblahthisfolderdoesntexist", force = true, relative = true }
286
+ "# ;
287
+
288
+ let config = LocalizedConfig {
289
+ config : toml:: from_str :: < Config > ( toml) . unwrap ( ) ,
290
+ workspace : PathBuf :: new ( ) ,
291
+ } ;
292
+
293
+ let path = config
294
+ . resolve_env ( "CARGO_SUBCOMMAND_TEST_ENV_SRC_DIR" )
295
+ . expect ( "Canonicalization for a known-to-exist ./src folder should not fail" ) ;
296
+ let path = Path :: new ( path. as_ref ( ) ) ;
297
+ assert ! ( path. is_absolute( ) ) ;
298
+ assert ! ( path. is_dir( ) ) ;
299
+ assert_eq ! ( path. file_name( ) , Some ( OsStr :: new( "src" ) ) ) ;
300
+
301
+ assert ! ( config
302
+ . resolve_env( "CARGO_SUBCOMMAND_TEST_ENV_INEXISTENT_DIR" )
303
+ . is_err( ) ) ;
304
+ }
0 commit comments