cargo_build_rx/
context.rs1use anyhow::{Context, Result};
9use cargo_metadata::{Metadata, MetadataCommand};
10use std::path::Path;
11use std::process::Command;
12
13pub struct ProjectContext {
15 pub metadata: Metadata,
17 pub cargo_config: Option<toml::Table>,
19 pub cargo_toml: toml::Table,
21 pub rustc_version: Option<RustcVersion>,
23 pub host_triple: Option<String>,
26 pub installed_linkers: InstalledLinkers,
28 pub env_vars: EnvVars,
30 pub os: Os,
32}
33
34#[derive(Debug)]
36pub struct RustcVersion {
37 pub major: u32,
39 pub minor: u32,
41 pub patch: u32,
43 pub raw: String,
45}
46
47#[derive(Debug, Default)]
49pub struct InstalledLinkers {
50 pub mold: bool,
52 pub lld: bool,
54}
55
56#[derive(Debug, Default)]
58pub struct EnvVars {
59 pub cargo_incremental: Option<String>,
61 pub rustflags: Option<String>,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum Os {
68 Linux,
70 Macos,
72 Windows,
74 Other,
76}
77
78impl ProjectContext {
79 pub fn gather(manifest_path: Option<&Path>) -> Result<Self> {
83 let mut cmd = MetadataCommand::new();
84 if let Some(path) = manifest_path {
85 cmd.manifest_path(path);
86 }
87 let metadata = cmd.exec().context("Failed to run `cargo metadata`")?;
88
89 let project_root = metadata.workspace_root.as_std_path().to_path_buf();
90
91 let cargo_toml = read_toml(&project_root.join("Cargo.toml"))?;
92 let cargo_config = read_cargo_config(&project_root);
93 let RustcInfo {
94 version: rustc_version,
95 host_triple,
96 } = parse_rustc_info();
97 let installed_linkers = detect_linkers();
98 let env_vars = gather_env_vars();
99 let os = detect_os();
100
101 Ok(Self {
102 metadata,
103 cargo_config,
104 cargo_toml,
105 rustc_version,
106 host_triple,
107 installed_linkers,
108 env_vars,
109 os,
110 })
111 }
112
113 pub fn project_name(&self) -> &str {
115 self.metadata
116 .root_package()
117 .map_or("project", |p| p.name.as_str())
118 }
119
120 pub fn is_workspace(&self) -> bool {
122 self.metadata.workspace_members.len() > 1
123 }
124
125 pub fn declared_msrv(&self) -> Option<(u32, u32)> {
130 let from_package = self
131 .cargo_toml
132 .get("package")
133 .and_then(|p| p.as_table())
134 .and_then(|p| p.get("rust-version"))
135 .and_then(toml::Value::as_str);
136
137 let from_workspace = self
138 .cargo_toml
139 .get("workspace")
140 .and_then(|w| w.as_table())
141 .and_then(|w| w.get("package"))
142 .and_then(|p| p.as_table())
143 .and_then(|p| p.get("rust-version"))
144 .and_then(toml::Value::as_str);
145
146 let raw = from_package.or(from_workspace)?;
147 parse_major_minor(raw)
148 }
149}
150
151fn parse_major_minor(raw: &str) -> Option<(u32, u32)> {
153 let mut parts = raw.trim().split('.');
154 let major = parts.next()?.parse().ok()?;
155 let minor = parts.next()?.parse().ok()?;
156 Some((major, minor))
157}
158
159fn read_toml(path: &Path) -> Result<toml::Table> {
160 let content = std::fs::read_to_string(path)
161 .with_context(|| format!("Failed to read {}", path.display()))?;
162 content
163 .parse::<toml::Table>()
164 .with_context(|| format!("Failed to parse {}", path.display()))
165}
166
167fn read_cargo_config(root: &Path) -> Option<toml::Table> {
168 let candidates = [root.join(".cargo/config.toml"), root.join(".cargo/config")];
169 for path in &candidates {
170 if let Ok(content) = std::fs::read_to_string(path) {
171 if let Ok(table) = content.parse::<toml::Table>() {
172 return Some(table);
173 }
174 }
175 }
176 None
177}
178
179struct RustcInfo {
181 version: Option<RustcVersion>,
182 host_triple: Option<String>,
183}
184
185fn parse_rustc_info() -> RustcInfo {
186 let Some(output) = Command::new("rustc").arg("-vV").output().ok() else {
187 return RustcInfo {
188 version: None,
189 host_triple: None,
190 };
191 };
192 let text = String::from_utf8_lossy(&output.stdout);
193
194 let version = text.lines().next().and_then(parse_version_line);
196
197 let host_triple = text
199 .lines()
200 .find_map(|line| line.strip_prefix("host:"))
201 .map(|t| t.trim().to_string());
202
203 RustcInfo {
204 version,
205 host_triple,
206 }
207}
208
209fn parse_version_line(line: &str) -> Option<RustcVersion> {
210 let raw = line.trim().to_string();
211 let version_part = raw.strip_prefix("rustc ")?.split_whitespace().next()?;
212 let mut parts = version_part.split('.');
213 let major = parts.next()?.parse().ok()?;
214 let minor = parts.next()?.parse().ok()?;
215 let patch = parts.next()?.parse().ok()?;
216 Some(RustcVersion {
217 major,
218 minor,
219 patch,
220 raw,
221 })
222}
223
224fn detect_linkers() -> InstalledLinkers {
225 InstalledLinkers {
226 mold: on_path("mold"),
227 lld: on_path("lld") || on_path("ld.lld"),
228 }
229}
230
231fn on_path(name: &str) -> bool {
236 let Some(path) = std::env::var_os("PATH") else {
237 return false;
238 };
239 std::env::split_paths(&path).any(|dir| {
240 let candidate = dir.join(name);
241 candidate.is_file() || (cfg!(windows) && candidate.with_extension("exe").is_file())
242 })
243}
244
245fn gather_env_vars() -> EnvVars {
246 EnvVars {
247 cargo_incremental: std::env::var("CARGO_INCREMENTAL").ok(),
248 rustflags: std::env::var("RUSTFLAGS").ok(),
249 }
250}
251
252fn detect_os() -> Os {
253 if cfg!(target_os = "linux") {
254 Os::Linux
255 } else if cfg!(target_os = "macos") {
256 Os::Macos
257 } else if cfg!(target_os = "windows") {
258 Os::Windows
259 } else {
260 Os::Other
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn parses_full_version_line() {
270 let v = parse_version_line("rustc 1.96.0 (ac68faa20 2026-05-25)").unwrap();
271 assert_eq!((v.major, v.minor, v.patch), (1, 96, 0));
272 }
273
274 #[test]
275 fn rejects_non_version_line() {
276 assert!(parse_version_line("host: aarch64-apple-darwin").is_none());
277 }
278
279 #[test]
280 fn parses_msrv_two_and_three_component() {
281 assert_eq!(parse_major_minor("1.74"), Some((1, 74)));
282 assert_eq!(parse_major_minor("1.74.1"), Some((1, 74)));
283 assert_eq!(parse_major_minor("nightly"), None);
284 }
285}