1use std::env;
14use std::fs;
15use std::path::{Path, PathBuf};
16use std::process::Command;
17
18pub fn emit(lockfile: &str, crates: &[&str]) {
21 println!(
22 "cargo:rustc-env=IRBT_GIT={}",
23 git().as_deref().unwrap_or("unknown")
24 );
25 println!("cargo:rustc-env=IRBT_RUSTC={}", rustc());
26 println!(
27 "cargo:rustc-env=IRBT_DEPS={}",
28 deps(Path::new(lockfile), crates)
29 );
30
31 let mut watched = vec![
35 PathBuf::from("src"),
36 PathBuf::from("Cargo.toml"),
37 PathBuf::from(lockfile),
38 ];
39 watched.extend(git_paths());
40 for path in watched.iter().filter(|p| p.exists()) {
41 println!("cargo:rerun-if-changed={}", path.display());
42 }
43}
44
45fn run(program: &str, args: &[&str]) -> Option<String> {
46 let output = Command::new(program).args(args).output().ok()?;
47 let stdout = String::from_utf8(output.stdout).ok()?;
48 output.status.success().then(|| stdout.trim().to_owned())
49}
50
51fn git() -> Option<String> {
52 if let Ok(info) = fs::read_to_string(".cargo_vcs_info.json") {
55 return vcs_sha1(&info).map(|sha| sha.chars().take(7).collect());
56 }
57 let describe = run(
58 "git",
59 &["describe", "--tags", "--always", "--dirty", "--abbrev=7"],
60 )?;
61 let date = run("git", &["log", "-1", "--format=%cs"])?;
62 Some(format!("{describe} {date}"))
63}
64
65fn git_paths() -> Vec<PathBuf> {
71 let mut names = vec![
72 "HEAD".to_owned(),
73 "index".to_owned(),
74 "packed-refs".to_owned(),
75 "refs/tags".to_owned(),
76 ];
77 if let Some(head_ref) = run("git", &["symbolic-ref", "-q", "HEAD"]) {
78 names.push(head_ref);
79 }
80 names
81 .iter()
82 .filter_map(|name| run("git", &["rev-parse", "--git-path", name]))
83 .map(PathBuf::from)
84 .collect()
85}
86
87fn vcs_sha1(info: &str) -> Option<&str> {
90 let rest = &info[info.find("\"sha1\"")? + "\"sha1\"".len()..];
91 let rest = rest.trim_start().strip_prefix(':')?.trim_start();
92 let sha = rest.strip_prefix('"')?.split('"').next()?;
93 (!sha.is_empty() && sha.bytes().all(|b| b.is_ascii_hexdigit())).then_some(sha)
94}
95
96fn rustc() -> String {
97 let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".to_owned());
98 run(&rustc, &["-V"]).unwrap_or_else(|| "rustc unknown".to_owned())
99}
100
101fn deps(lockfile: &Path, crates: &[&str]) -> String {
102 let lock = fs::read_to_string(lockfile).unwrap_or_default();
103 let packages = parse_lock(&lock);
104 crates
105 .iter()
106 .filter_map(|&name| {
107 let versions: Vec<&str> = packages
108 .iter()
109 .filter(|(n, _)| *n == name)
110 .map(|&(_, v)| v)
111 .collect();
112 (!versions.is_empty()).then(|| format!("{name}={}", versions.join(", ")))
113 })
114 .collect::<Vec<_>>()
115 .join(";")
116}
117
118fn parse_lock(lock: &str) -> Vec<(&str, &str)> {
121 let mut packages = Vec::new();
122 let mut name = None;
123 for line in lock.lines() {
124 if let Some(n) = value(line, "name") {
125 name = Some(n);
126 } else if let (Some(n), Some(v)) = (name, value(line, "version")) {
127 packages.push((n, v));
128 name = None;
129 }
130 }
131 packages
132}
133
134fn value<'a>(line: &'a str, key: &str) -> Option<&'a str> {
136 line.strip_prefix(key)?
137 .trim_start()
138 .strip_prefix('=')?
139 .trim()
140 .strip_prefix('"')?
141 .strip_suffix('"')
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn reads_vcs_info_sha1() {
150 let info = r#"{
151 "git": {
152 "sha1": "16e4ae9c0ffee0123456789abcdef0123456789a",
153 "dirty": true
154 },
155 "path_in_vcs": "host/irbt-cli"
156}"#;
157 assert_eq!(
158 vcs_sha1(info),
159 Some("16e4ae9c0ffee0123456789abcdef0123456789a")
160 );
161 assert_eq!(vcs_sha1(r#"{"path_in_vcs": ""}"#), None);
162 }
163
164 #[test]
165 fn parses_lockfile_packages() {
166 let lock = r#"
167version = 4
168
169[[package]]
170name = "a"
171version = "1.0.0"
172source = "registry+https://github.com/rust-lang/crates.io-index"
173
174[[package]]
175name = "b"
176version = "0.2.0"
177dependencies = [
178 "a",
179]
180
181[[package]]
182name = "a"
183version = "2.0.0"
184"#;
185 assert_eq!(
186 parse_lock(lock),
187 [("a", "1.0.0"), ("b", "0.2.0"), ("a", "2.0.0")]
188 );
189 }
190}