1use serde::Serialize;
4use std::{
5 path::PathBuf,
6 process::{Command, Stdio},
7};
8
9#[derive(Debug, Clone, Serialize)]
10pub struct Tool {
11 pub name: &'static str,
12 pub available: bool,
13 pub path: Option<PathBuf>,
14 pub purpose: &'static str,
16 pub install: &'static str,
18}
19
20fn runs(program: &str, argument: &str) -> bool {
21 Command::new(program)
22 .arg(argument)
23 .stdin(Stdio::null())
24 .stdout(Stdio::null())
25 .stderr(Stdio::null())
26 .status()
27 .is_ok_and(|status| status.success())
28}
29
30fn install_hint(macos: &'static str, linux: &'static str, windows: &'static str) -> &'static str {
31 if cfg!(target_os = "macos") {
32 macos
33 } else if cfg!(windows) {
34 windows
35 } else {
36 linux
37 }
38}
39
40pub(crate) fn find_aapt2() -> Option<PathBuf> {
42 let executable = if cfg!(windows) { "aapt2.exe" } else { "aapt2" };
43 if runs(executable, "version") {
44 return Some(PathBuf::from(executable));
45 }
46 let mut roots: Vec<PathBuf> = ["ANDROID_HOME", "ANDROID_SDK_ROOT"]
47 .iter()
48 .filter_map(|name| std::env::var_os(name).filter(|v| !v.is_empty()))
49 .map(PathBuf::from)
50 .collect();
51 if let Some(home) = std::env::var_os("HOME").filter(|v| !v.is_empty()) {
52 roots.push(PathBuf::from(&home).join("Library/Android/sdk"));
53 roots.push(PathBuf::from(&home).join("Android/Sdk"));
54 }
55 if let Some(local) = std::env::var_os("LOCALAPPDATA").filter(|v| !v.is_empty()) {
56 roots.push(PathBuf::from(local).join("Android").join("Sdk"));
57 }
58 for root in roots {
59 let Ok(entries) = std::fs::read_dir(root.join("build-tools")) else {
60 continue;
61 };
62 let mut versions: Vec<PathBuf> = entries.filter_map(|e| Some(e.ok()?.path())).collect();
63 versions.sort();
64 for version in versions.into_iter().rev() {
65 let candidate = version.join(executable);
66 if candidate.is_file() && runs(&candidate.to_string_lossy(), "version") {
67 return Some(candidate);
68 }
69 }
70 }
71 None
72}
73
74pub fn detect() -> Vec<Tool> {
75 let aapt2 = find_aapt2();
76 let simple = |name: &'static str, flag: &str, purpose, install| {
77 let available = runs(name, flag);
78 Tool {
79 name,
80 available,
81 path: available.then(|| PathBuf::from(name)),
82 purpose,
83 install,
84 }
85 };
86 vec![
87 simple(
88 "ffprobe",
89 "-version",
90 "Reports codec, duration and bitrate for audio and video resources (inspection only; resopt does not transcode media).",
91 install_hint(
92 "brew install ffmpeg",
93 "Install ffmpeg with your package manager, e.g. `sudo apt install ffmpeg`.",
94 "winget install Gyan.FFmpeg",
95 ),
96 ),
97 Tool {
98 name: "aapt2",
99 available: aapt2.is_some(),
100 path: aapt2,
101 purpose: "Compiles migrated Android resources to confirm they are accepted by the build tools.",
102 install: "Install Android SDK Build-Tools (Android Studio → SDK Manager) and set ANDROID_HOME.",
103 },
104 ]
105}