1use anyhow::{Context as _, Result};
7
8use crate::config::PartialConfig;
9use crate::target;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum PartialTarget {
18 Exact(String),
20 OsArch { os: String, arch: Option<String> },
22}
23
24impl PartialTarget {
25 pub fn filter_targets(&self, targets: &[String]) -> Vec<String> {
27 match self {
28 PartialTarget::Exact(t) => targets.iter().filter(|tt| *tt == t).cloned().collect(),
29 PartialTarget::OsArch { os, arch } => targets
30 .iter()
31 .filter(|tt| {
32 let (t_os, t_arch) = target::map_target(tt);
33 t_os == *os && arch.as_ref().is_none_or(|a| t_arch == *a)
34 })
35 .cloned()
36 .collect(),
37 }
38 }
39
40 pub fn dist_subdir(&self) -> String {
45 match self {
46 PartialTarget::Exact(t) => t.clone(),
47 PartialTarget::OsArch { os, arch } => {
48 if let Some(a) = arch {
49 format!("{}_{}", os, a)
50 } else {
51 os.clone()
52 }
53 }
54 }
55 }
56}
57
58pub fn resolve_partial_target(config: &Option<PartialConfig>) -> Result<PartialTarget> {
70 if let Ok(t) = std::env::var("TARGET")
72 && !t.is_empty()
73 {
74 return Ok(PartialTarget::Exact(t));
75 }
76
77 let os = std::env::var("ANODIZER_OS")
80 .ok()
81 .filter(|s| !s.is_empty())
82 .or_else(|| std::env::var("GGOOS").ok().filter(|s| !s.is_empty()));
83 if let Some(os) = os {
84 let arch = std::env::var("ANODIZER_ARCH")
85 .ok()
86 .filter(|a| !a.is_empty())
87 .or_else(|| std::env::var("GGOARCH").ok().filter(|a| !a.is_empty()));
88 return Ok(PartialTarget::OsArch { os, arch });
89 }
90
91 let host = detect_host_target()?;
93 let by = config
94 .as_ref()
95 .and_then(|c| c.by.as_deref())
96 .unwrap_or("goos");
97
98 match by {
99 "goos" => {
100 let (os, _) = target::map_target(&host);
101 Ok(PartialTarget::OsArch { os, arch: None })
102 }
103 "target" => Ok(PartialTarget::Exact(host)),
104 other => anyhow::bail!(
105 "partial.by: unknown value '{}' (expected 'goos' or 'target')",
106 other
107 ),
108 }
109}
110
111pub fn detect_host_target() -> Result<String> {
113 let output = std::process::Command::new("rustc")
114 .args(["-vV"])
115 .output()
116 .context("failed to run `rustc -vV` for host target detection")?;
117
118 if !output.status.success() {
119 anyhow::bail!(
120 "rustc -vV failed: {}",
121 String::from_utf8_lossy(&output.stderr)
122 );
123 }
124
125 let stdout = String::from_utf8_lossy(&output.stdout);
126 for line in stdout.lines() {
127 if let Some(host) = line.strip_prefix("host: ") {
128 return Ok(host.trim().to_string());
129 }
130 }
131 anyhow::bail!("could not detect host target from `rustc -vV` output")
132}
133
134pub fn suggest_runner(os: &str) -> &'static str {
136 match os {
137 "linux" => "ubuntu-latest",
138 "darwin" => "macos-latest",
139 "windows" => "windows-latest",
140 _ => "ubuntu-latest", }
142}
143
144#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::config::PartialConfig;
152 use serial_test::serial;
153
154 #[test]
159 fn test_exact_filter_matches_one() {
160 let target = PartialTarget::Exact("x86_64-unknown-linux-gnu".to_string());
161 let targets = vec![
162 "x86_64-unknown-linux-gnu".to_string(),
163 "aarch64-unknown-linux-gnu".to_string(),
164 "x86_64-apple-darwin".to_string(),
165 ];
166 let filtered = target.filter_targets(&targets);
167 assert_eq!(filtered, vec!["x86_64-unknown-linux-gnu"]);
168 }
169
170 #[test]
171 fn test_exact_filter_no_match() {
172 let target = PartialTarget::Exact("riscv64gc-unknown-linux-gnu".to_string());
173 let targets = vec![
174 "x86_64-unknown-linux-gnu".to_string(),
175 "aarch64-apple-darwin".to_string(),
176 ];
177 let filtered = target.filter_targets(&targets);
178 assert!(filtered.is_empty());
179 }
180
181 #[test]
182 fn test_os_filter_matches_all_linux() {
183 let target = PartialTarget::OsArch {
184 os: "linux".to_string(),
185 arch: None,
186 };
187 let targets = vec![
188 "x86_64-unknown-linux-gnu".to_string(),
189 "aarch64-unknown-linux-gnu".to_string(),
190 "x86_64-apple-darwin".to_string(),
191 "x86_64-pc-windows-msvc".to_string(),
192 ];
193 let filtered = target.filter_targets(&targets);
194 assert_eq!(
195 filtered,
196 vec!["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu",]
197 );
198 }
199
200 #[test]
201 fn test_os_arch_filter() {
202 let target = PartialTarget::OsArch {
203 os: "linux".to_string(),
204 arch: Some("arm64".to_string()),
205 };
206 let targets = vec![
207 "x86_64-unknown-linux-gnu".to_string(),
208 "aarch64-unknown-linux-gnu".to_string(),
209 ];
210 let filtered = target.filter_targets(&targets);
211 assert_eq!(filtered, vec!["aarch64-unknown-linux-gnu"]);
212 }
213
214 #[test]
215 fn test_os_filter_darwin() {
216 let target = PartialTarget::OsArch {
217 os: "darwin".to_string(),
218 arch: None,
219 };
220 let targets = vec![
221 "x86_64-apple-darwin".to_string(),
222 "aarch64-apple-darwin".to_string(),
223 "x86_64-unknown-linux-gnu".to_string(),
224 ];
225 let filtered = target.filter_targets(&targets);
226 assert_eq!(
227 filtered,
228 vec!["x86_64-apple-darwin", "aarch64-apple-darwin"]
229 );
230 }
231
232 #[test]
233 fn test_os_filter_windows() {
234 let target = PartialTarget::OsArch {
235 os: "windows".to_string(),
236 arch: None,
237 };
238 let targets = vec![
239 "x86_64-pc-windows-msvc".to_string(),
240 "aarch64-pc-windows-msvc".to_string(),
241 "x86_64-unknown-linux-gnu".to_string(),
242 ];
243 let filtered = target.filter_targets(&targets);
244 assert_eq!(
245 filtered,
246 vec!["x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"]
247 );
248 }
249
250 #[test]
255 fn test_dist_subdir_exact() {
256 let target = PartialTarget::Exact("x86_64-unknown-linux-gnu".to_string());
257 assert_eq!(target.dist_subdir(), "x86_64-unknown-linux-gnu");
258 }
259
260 #[test]
261 fn test_dist_subdir_os_only() {
262 let target = PartialTarget::OsArch {
263 os: "linux".to_string(),
264 arch: None,
265 };
266 assert_eq!(target.dist_subdir(), "linux");
267 }
268
269 #[test]
270 fn test_dist_subdir_os_arch() {
271 let target = PartialTarget::OsArch {
272 os: "linux".to_string(),
273 arch: Some("amd64".to_string()),
274 };
275 assert_eq!(target.dist_subdir(), "linux_amd64");
276 }
277
278 #[test]
283 fn test_detect_host_target() {
284 let host = detect_host_target().unwrap();
287 assert!(!host.is_empty());
288 assert!(host.contains('-'), "host triple should contain '-': {host}");
290 }
291
292 #[test]
297 #[serial]
298 fn test_resolve_with_goos_default() {
299 unsafe {
302 std::env::remove_var("TARGET");
303 std::env::remove_var("ANODIZER_OS");
304 std::env::remove_var("ANODIZER_ARCH");
305 }
306
307 let config = None; let target = resolve_partial_target(&config).unwrap();
309
310 match target {
312 PartialTarget::OsArch { os, arch } => {
313 assert!(!os.is_empty());
314 assert!(arch.is_none()); }
316 other => panic!("expected OsArch, got: {other:?}"),
317 }
318 }
319
320 #[test]
321 #[serial]
322 fn test_resolve_with_by_target() {
323 unsafe {
325 std::env::remove_var("TARGET");
326 std::env::remove_var("ANODIZER_OS");
327 std::env::remove_var("ANODIZER_ARCH");
328 }
329
330 let config = Some(PartialConfig {
331 by: Some("target".to_string()),
332 });
333 let target = resolve_partial_target(&config).unwrap();
334
335 match target {
337 PartialTarget::Exact(t) => {
338 assert!(t.contains('-'), "should be full triple: {t}");
339 }
340 other => panic!("expected Exact, got: {other:?}"),
341 }
342 }
343
344 #[test]
345 #[serial]
346 fn test_resolve_invalid_by_value() {
347 unsafe {
349 std::env::remove_var("TARGET");
350 std::env::remove_var("ANODIZER_OS");
351 std::env::remove_var("ANODIZER_ARCH");
352 }
353
354 let config = Some(PartialConfig {
355 by: Some("invalid".to_string()),
356 });
357 let err = resolve_partial_target(&config).unwrap_err();
358 assert!(err.to_string().contains("unknown value"), "got: {}", err);
359 }
360
361 #[test]
366 fn test_suggest_runner() {
367 assert_eq!(suggest_runner("linux"), "ubuntu-latest");
368 assert_eq!(suggest_runner("darwin"), "macos-latest");
369 assert_eq!(suggest_runner("windows"), "windows-latest");
370 assert_eq!(suggest_runner("freebsd"), "ubuntu-latest");
371 }
372}