1use crate::build::*;
2use crate::date_time::now_date_time;
3use crate::env::dep_source_replace::filter_cargo_tree;
4use crate::err::SdResult;
5use crate::{Format, Shadow};
6use is_debug::build_channel;
7use std::collections::BTreeMap;
8use std::env;
9use std::env as std_env;
10use std::process::Command;
11
12#[derive(Default, Debug)]
13pub struct SystemEnv {
14 map: BTreeMap<ShadowConst, ConstVal>,
15}
16
17pub(crate) fn get_std_env() -> BTreeMap<String, String> {
19 let mut env_map = BTreeMap::new();
20 for (k, v) in std_env::vars() {
21 env_map.insert(k, v);
22 }
23 env_map
24}
25
26const BUILD_OS_DOC: &str = r#"
27Operating system and architecture on which the project was build.
28The format of this variable is always `os-arch`,
29where `os` is the operating system name as returned by [`std::env::consts::OS`],
30and `arch` is the computer architecture as returned by [`std::env::consts::ARCH`]."#;
31pub const BUILD_OS: ShadowConst = "BUILD_OS";
32
33const RUST_VERSION_DOC: &str = r#"
34Rust version with which the project was built.
35The version always uses the canonical Rust version format,
36and is therefore identical to the output of the build toolchain's `rustc --version`."#;
37pub const RUST_VERSION: ShadowConst = "RUST_VERSION";
38
39const RUST_CHANNEL_DOC: &str = r#"
40The [Rustup toolchain](https://rust-lang.github.io/rustup/concepts/toolchains.html) with which the project was built.
41Note that as per Rustup toolchain format, this variable may or may not contain host and date information,
42but it will always contain [channel](https://rust-lang.github.io/rustup/concepts/channels.html) information (stable, beta or nightly)."#;
43pub const RUST_CHANNEL: ShadowConst = "RUST_CHANNEL";
44
45pub const CARGO_METADATA: ShadowConst = "CARGO_METADATA";
46const CARGO_METADATA_DOC: ShadowConst = r#"
47The information about the workspace members and resolved dependencies of the current package.
48See the [cargo_metadata](https://crates.io/crates/cargo_metadata) crate for a Rust API for reading the metadata."#;
49
50const CARGO_VERSION_DOC: &str = r#"
51The cargo version which which the project was built, as output by `cargo --version`."#;
52pub const CARGO_VERSION: ShadowConst = "CARGO_VERSION";
53
54const CARGO_TREE_DOC: &str = r#"
55The dependency tree of the project, as output by `cargo tree`.
56Note that this variable may contain local file system paths for path dependencies, and may therefore contain sensitive information and not be reproducible."#;
57pub const CARGO_TREE: ShadowConst = "CARGO_TREE";
58
59const BUILD_TARGET_DOC: &str = r#"
60The [target](https://doc.rust-lang.org/rustc/targets/index.html) for this build.
61This is possibly distinct from the host target during build, in which case this project build was created via cross-compilation."#;
62pub const BUILD_TARGET: ShadowConst = "BUILD_TARGET";
63
64const BUILD_TARGET_ARCH_DOC: &str = r#"
65The architecture of the target for this build. This is the "architecture" part of the [`BUILD_TARGET`] constant."#;
66pub const BUILD_TARGET_ARCH: ShadowConst = "BUILD_TARGET_ARCH";
67
68const CARGO_MANIFEST_DIR_DOC: &str = r#"
69The directory of the Cargo.toml manifest file of the project during build.
70Note that this variable will contain a full local file system path, and will therefore contain sensitive information and not be reproducible."#;
71pub const CARGO_MANIFEST_DIR: ShadowConst = "CARGO_MANIFEST_DIR";
72
73const PKG_VERSION_DOC: &str = r#"
74The project's full version string, as determined by the Cargo.toml manifest."#;
75pub const PKG_VERSION: ShadowConst = "PKG_VERSION";
76
77const PKG_DESCRIPTION_DOC: &str = r#"
78The project's description, as determined by the Cargo.toml manifest."#;
79pub const PKG_DESCRIPTION: ShadowConst = "PKG_DESCRIPTION";
80
81const PKG_VERSION_MAJOR_DOC: &str = r#"
82The project's semver major version, as determined by the Cargo.toml manifest."#;
83pub const PKG_VERSION_MAJOR: ShadowConst = "PKG_VERSION_MAJOR";
84
85const PKG_VERSION_MINOR_DOC: &str = r#"
86The project's semver minor version, as determined by the Cargo.toml manifest."#;
87pub const PKG_VERSION_MINOR: ShadowConst = "PKG_VERSION_MINOR";
88
89const PKG_VERSION_PATCH_DOC: &str = r#"
90The project's semver patch version, as determined by the Cargo.toml manifest."#;
91pub const PKG_VERSION_PATCH: ShadowConst = "PKG_VERSION_PATCH";
92
93const PKG_VERSION_PRE_DOC: &str = r#"
94The project's semver pre-release version, as determined by the Cargo.toml manifest."#;
95pub const PKG_VERSION_PRE: ShadowConst = "PKG_VERSION_PRE";
96
97impl SystemEnv {
98 fn init(&mut self, shadow: &Shadow) -> SdResult<()> {
99 let std_env = &shadow.std_env;
100 let mut update_val = |c: ShadowConst, v: String| {
101 if let Some(val) = self.map.get_mut(c) {
102 val.v = v;
103 }
104 };
105
106 if let Some(v) = std_env.get("RUSTUP_TOOLCHAIN") {
107 update_val(RUST_CHANNEL, v.to_string());
108 }
109
110 if let Ok(out) = Command::new("rustc").arg("-V").output() {
111 update_val(
112 RUST_VERSION,
113 String::from_utf8(out.stdout)?.trim().to_string(),
114 );
115 }
116
117 if let Ok(out) = Command::new("cargo").arg("-V").output() {
118 update_val(
119 CARGO_VERSION,
120 String::from_utf8(out.stdout)?.trim().to_string(),
121 );
122 }
123
124 if !shadow.deny_contains(CARGO_TREE) {
131 if let Ok(out) = Command::new("cargo").arg("tree").output() {
132 let input = String::from_utf8(out.stdout)?;
133 if let Some(index) = input.find('\n') {
134 let lines = filter_cargo_tree(
135 input.get(index..).unwrap_or_default().split('\n').collect(),
136 );
137 update_val(CARGO_TREE, lines);
138 }
139 }
140 }
141
142 if !shadow.deny_contains(CARGO_METADATA) {
149 if let Ok(output) = Command::new("cargo")
151 .args(["metadata", "--format-version", "1"])
152 .output()
153 {
154 update_val(
156 CARGO_METADATA,
157 String::from_utf8(output.stdout)?.trim().to_string(),
158 );
159 }
160 }
161
162 if let Some(v) = std_env.get("TARGET") {
163 update_val(BUILD_TARGET, v.to_string());
164 }
165
166 if let Some(v) = std_env.get("CARGO_CFG_TARGET_ARCH") {
167 update_val(BUILD_TARGET_ARCH, v.to_string());
168 }
169
170 if let Some(v) = std_env.get("CARGO_PKG_VERSION") {
171 update_val(PKG_VERSION, v.to_string());
172 }
173
174 if let Some(v) = std_env.get("CARGO_PKG_DESCRIPTION") {
175 update_val(PKG_DESCRIPTION, v.to_string());
176 }
177
178 if let Some(v) = std_env.get("CARGO_PKG_VERSION_MAJOR") {
179 update_val(PKG_VERSION_MAJOR, v.to_string());
180 }
181
182 if let Some(v) = std_env.get("CARGO_PKG_VERSION_MINOR") {
183 update_val(PKG_VERSION_MINOR, v.to_string());
184 }
185 if let Some(v) = std_env.get("CARGO_PKG_VERSION_PATCH") {
186 update_val(PKG_VERSION_PATCH, v.to_string());
187 }
188 if let Some(v) = std_env.get("CARGO_PKG_VERSION_PRE") {
189 update_val(PKG_VERSION_PRE, v.to_string());
190 }
191 if let Some(v) = std_env.get("CARGO_MANIFEST_DIR") {
192 update_val(CARGO_MANIFEST_DIR, v.to_string());
193 }
194
195 Ok(())
196 }
197}
198
199mod dep_source_replace {
200 use std::fs;
201
202 fn path_exists(path: &str) -> bool {
203 fs::metadata(path).is_ok()
204 }
205
206 const DEP_REPLACE_NONE: &str = "";
207 const DEP_REPLACE_PATH: &str = " (* path)";
208 const DEP_REPLACE_GIT: &str = " (* git)";
209 const DEP_REPLACE_REGISTRY: &str = " (* registry)";
210
211 pub fn filter_dep_source(input: &str) -> String {
241 let (val, index) = if let Some(index) = input.find(" (/") {
242 (DEP_REPLACE_PATH, index)
243 } else if let Some(index) = input.find(" (registry ") {
244 (DEP_REPLACE_REGISTRY, index)
245 } else if let Some(index) = input.find(" (http") {
246 (DEP_REPLACE_GIT, index)
247 } else if let Some(index) = input.find(" (https") {
248 (DEP_REPLACE_GIT, index)
249 } else if let Some(index) = input.find(" (ssh") {
250 (DEP_REPLACE_GIT, index)
251 } else if let (Some(start), Some(end)) = (input.find(" ("), input.find(')')) {
252 let path = input.get(start + 2..end).unwrap_or_default().trim();
253 if path_exists(path) {
254 (DEP_REPLACE_PATH, start)
255 } else {
256 (DEP_REPLACE_NONE, input.len())
257 }
258 } else {
259 (DEP_REPLACE_NONE, input.len())
260 };
261 format!("{}{}", &input.get(..index).unwrap_or_default(), val)
262 }
263
264 pub fn filter_cargo_tree(lines: Vec<&str>) -> String {
265 let mut tree = "\n".to_string();
266 for line in lines {
267 let val = filter_dep_source(line);
268 if tree.trim().is_empty() {
269 tree.push_str(&val);
270 } else {
271 tree = format!("{tree}\n{val}");
272 }
273 }
274 tree
275 }
276}
277
278pub(crate) fn new_system_env(shadow: &Shadow) -> BTreeMap<ShadowConst, ConstVal> {
281 let mut env = SystemEnv::default();
282 env.map.insert(
283 BUILD_OS,
284 ConstVal {
285 desc: BUILD_OS_DOC.to_string(),
286 v: format!("{}-{}", env::consts::OS, env::consts::ARCH),
287 t: ConstType::Str,
288 },
289 );
290
291 env.map
292 .insert(RUST_CHANNEL, ConstVal::new(RUST_CHANNEL_DOC));
293 env.map
294 .insert(CARGO_METADATA, ConstVal::new_slice(CARGO_METADATA_DOC));
295 env.map
296 .insert(RUST_VERSION, ConstVal::new(RUST_VERSION_DOC));
297 env.map
298 .insert(CARGO_VERSION, ConstVal::new(CARGO_VERSION_DOC));
299
300 env.map.insert(CARGO_TREE, ConstVal::new(CARGO_TREE_DOC));
301
302 env.map
303 .insert(BUILD_TARGET, ConstVal::new(BUILD_TARGET_DOC));
304
305 env.map
306 .insert(BUILD_TARGET_ARCH, ConstVal::new(BUILD_TARGET_ARCH_DOC));
307
308 env.map.insert(PKG_VERSION, ConstVal::new(PKG_VERSION_DOC));
309
310 env.map
311 .insert(PKG_DESCRIPTION, ConstVal::new(PKG_DESCRIPTION_DOC));
312
313 env.map
314 .insert(PKG_VERSION_MAJOR, ConstVal::new(PKG_VERSION_MAJOR_DOC));
315 env.map
316 .insert(PKG_VERSION_MINOR, ConstVal::new(PKG_VERSION_MINOR_DOC));
317 env.map
318 .insert(PKG_VERSION_PATCH, ConstVal::new(PKG_VERSION_PATCH_DOC));
319 env.map
320 .insert(PKG_VERSION_PRE, ConstVal::new(PKG_VERSION_PRE_DOC));
321 env.map
322 .insert(CARGO_MANIFEST_DIR, ConstVal::new(CARGO_MANIFEST_DIR_DOC));
323
324 if let Err(e) = env.init(shadow) {
325 println!("{e}");
326 }
327 env.map
328}
329
330#[derive(Default, Debug)]
331pub struct Project {
332 map: BTreeMap<ShadowConst, ConstVal>,
333}
334
335const PROJECT_NAME_DOC: &str = r#"
336The project name, as determined by the Cargo.toml manifest."#;
337const PROJECT_NAME: ShadowConst = "PROJECT_NAME";
338
339const BUILD_TIME_DOC: &str = r#"
340The project build time, formatted in modified ISO 8601 format (`YYYY-MM-DD HH-MM ±hh-mm` where hh-mm is the offset from UTC)."#;
341const BUILD_TIME: ShadowConst = "BUILD_TIME";
342
343const BUILD_TIME_2822_DOC: &str = r#"
344The project build time, formatted according to [RFC 2822](https://datatracker.ietf.org/doc/html/rfc2822#section-3.3) (e.g. HTTP Headers)."#;
345const BUILD_TIME_2822: ShadowConst = "BUILD_TIME_2822";
346
347const BUILD_TIME_3339_DOC: &str = r#"
348The project build time, formatted according to [RFC 3339 and ISO 8601](https://datatracker.ietf.org/doc/html/rfc3339#section-5.6)."#;
349const BUILD_TIME_3339: ShadowConst = "BUILD_TIME_3339";
350
351const BUILD_RUST_CHANNEL_DOC: &str = r#"
352The debug configuration with which the project was built.
353Note that this is not the Rust channel, but either `debug` or `release`, depending on whether debug assertions were enabled in the build or not. "#;
354const BUILD_RUST_CHANNEL: ShadowConst = "BUILD_RUST_CHANNEL";
355
356pub(crate) fn build_time(project: &mut Project) {
357 let time = now_date_time();
359 project.map.insert(
360 BUILD_TIME,
361 ConstVal {
362 desc: BUILD_TIME_DOC.to_string(),
363 v: time.human_format(),
364 t: ConstType::Str,
365 },
366 );
367 project.map.insert(
368 BUILD_TIME_2822,
369 ConstVal {
370 desc: BUILD_TIME_2822_DOC.to_string(),
371 v: time.to_rfc2822(),
372 t: ConstType::Str,
373 },
374 );
375
376 project.map.insert(
377 BUILD_TIME_3339,
378 ConstVal {
379 desc: BUILD_TIME_3339_DOC.to_string(),
380 v: time.to_rfc3339(),
381 t: ConstType::Str,
382 },
383 );
384}
385
386pub(crate) fn new_project(std_env: &BTreeMap<String, String>) -> BTreeMap<ShadowConst, ConstVal> {
387 let mut project = Project::default();
388 build_time(&mut project);
389 project.map.insert(
390 BUILD_RUST_CHANNEL,
391 ConstVal {
392 desc: BUILD_RUST_CHANNEL_DOC.to_string(),
393 v: build_channel().to_string(),
394 t: ConstType::Str,
395 },
396 );
397 project
398 .map
399 .insert(PROJECT_NAME, ConstVal::new(PROJECT_NAME_DOC));
400
401 if let (Some(v), Some(val)) = (
402 std_env.get("CARGO_PKG_NAME"),
403 project.map.get_mut(PROJECT_NAME),
404 ) {
405 val.t = ConstType::Str;
406 val.v = v.to_string();
407 }
408
409 project.map
410}
411
412#[cfg(test)]
413mod tests {
414 use crate::env::dep_source_replace::filter_dep_source;
415
416 #[test]
417 fn test_filter_dep_source_none() {
418 let input = "shadow-rs v0.5.23";
419 let ret = filter_dep_source(input);
420 assert_eq!(input, ret)
421 }
422
423 #[test]
424 fn test_filter_dep_source_multi() {
425 let input = "shadow-rs v0.5.23 (*)";
426 let ret = filter_dep_source(input);
427 assert_eq!(input, ret)
428 }
429
430 #[test]
431 fn test_filter_dep_source_path() {
432 let input = "shadow-rs v0.5.23 (/Users/baoyachi/shadow-rs)";
433 let ret = filter_dep_source(input);
434 assert_eq!("shadow-rs v0.5.23 (* path)", ret)
435 }
436
437 #[test]
438 fn test_filter_dep_source_registry() {
439 let input = "shadow-rs v0.5.23 (registry `ssh://git@github.com/baoyachi/shadow-rs.git`)";
440 let ret = filter_dep_source(input);
441 assert_eq!("shadow-rs v0.5.23 (* registry)", ret)
442 }
443
444 #[test]
445 fn test_filter_dep_source_git_https() {
446 let input = "shadow-rs v0.5.23 (https://github.com/baoyachi/shadow-rs#13572c90)";
447 let ret = filter_dep_source(input);
448 assert_eq!("shadow-rs v0.5.23 (* git)", ret)
449 }
450
451 #[test]
452 fn test_filter_dep_source_git_http() {
453 let input = "shadow-rs v0.5.23 (http://github.com/baoyachi/shadow-rs#13572c90)";
454 let ret = filter_dep_source(input);
455 assert_eq!("shadow-rs v0.5.23 (* git)", ret)
456 }
457
458 #[test]
459 fn test_filter_dep_source_git() {
460 let input = "shadow-rs v0.5.23 (ssh://git@github.com/baoyachi/shadow-rs)";
461 let ret = filter_dep_source(input);
462 assert_eq!("shadow-rs v0.5.23 (* git)", ret)
463 }
464
465 #[test]
466 fn test_filter_dep_windows_path() {
467 let input = r"shadow-rs v0.5.23 (FD:\a\shadow-rs\shadow-rs)";
468 let ret = filter_dep_source(input);
469 assert_eq!(input, ret)
470 }
471}