Skip to main content

resopt/
tools.rs

1//! Optional external tools. resopt never installs anything; it reports what is
2//! missing, what that tool would enable, and how to install it.
3use 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    /// What resopt uses the tool for.
15    pub purpose: &'static str,
16    /// Installation guidance for this platform, shown only when missing.
17    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
40/// Locate `aapt2` on PATH or in the newest installed Android SDK build-tools.
41pub(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}