Skip to main content

qualia_client_core/
prerequisites.rs

1//! Windows desktop runtime prerequisites (WebView2 + VC++ redistributable).
2//!
3//! WebView2: prefer a **Fixed Version** runtime shipped next to the executable
4//! (`WebView2Runtime/` or `WebView2/`). If absent, fall back to the system
5//! Evergreen runtime (registry). Sets `WEBVIEW2_BROWSER_EXECUTABLE_FOLDER` when
6//! a bundled runtime is found.
7//!
8//! VC++ 2015–2022 x64: not redistributable inside QualiaDB — user installs
9//! Microsoft's installer; we detect via registry and re-check after launch.
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct PrerequisiteStatus {
13    /// True on Windows when the prerequisite gate should run.
14    pub platform_requires_check: bool,
15    pub webview2_ready: bool,
16    pub webview2_bundled: bool,
17    pub webview2_evergreen: bool,
18    pub vc_redist_ready: bool,
19    pub all_ready: bool,
20    /// Folder containing `msedgewebview2.exe` when bundled; empty if none.
21    pub bundled_webview2_dir: String,
22}
23
24#[cfg(windows)]
25const VC_REDIST_URL: &str = "https://aka.ms/vs/17/release/vc_redist.x64.exe";
26#[cfg(windows)]
27const WEBVIEW2_BOOTSTRAPPER_URL: &str = "https://go.microsoft.com/fwlink/p/?LinkId=2124703";
28
29#[cfg(windows)]
30mod win {
31    use super::{PrerequisiteStatus, VC_REDIST_URL, WEBVIEW2_BOOTSTRAPPER_URL};
32    use std::path::{Path, PathBuf};
33    use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
34    use winreg::RegKey;
35
36    const WEBVIEW2_CLIENT_GUID: &str = r"{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}";
37
38    fn exe_dir() -> Option<PathBuf> {
39        std::env::current_exe()
40            .ok()
41            .and_then(|p| p.parent().map(|d| d.to_path_buf()))
42    }
43
44    fn folder_with_msedge(base: &Path) -> Option<PathBuf> {
45        let direct = base.join("msedgewebview2.exe");
46        if direct.is_file() {
47            return Some(base.to_path_buf());
48        }
49        let nested = base.join("x64").join("msedgewebview2.exe");
50        if nested.is_file() {
51            return nested.parent().map(|p| p.to_path_buf());
52        }
53        None
54    }
55
56    fn find_bundled_webview2() -> Option<PathBuf> {
57        let root = exe_dir()?;
58        for name in [
59            "WebView2Runtime",
60            "WebView2",
61            "Microsoft.WebView2.FixedVersionRuntime",
62            "webview2",
63        ] {
64            let candidate = root.join(name);
65            if let Some(found) = folder_with_msedge(&candidate) {
66                return Some(found);
67            }
68        }
69        None
70    }
71
72    fn evergreen_webview2_installed() -> bool {
73        let hives = [HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER];
74        let bases = [
75            r"SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients",
76            r"SOFTWARE\Microsoft\EdgeUpdate\Clients",
77        ];
78        for hive in hives {
79            for base in bases {
80                let path = format!("{base}\\{WEBVIEW2_CLIENT_GUID}");
81                if let Ok(key) = RegKey::predef(hive).open_subkey(path) {
82                    if key.get_value::<String, _>("pv").is_ok() {
83                        return true;
84                    }
85                }
86            }
87        }
88        false
89    }
90
91    fn vc_redist_x64_installed() -> bool {
92        let paths = [
93            r"SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64",
94            r"SOFTWARE\WOW6432Node\Microsoft\VisualStudio\14.0\VC\Runtimes\x64",
95        ];
96        for path in paths {
97            if let Ok(key) = RegKey::predef(HKEY_LOCAL_MACHINE).open_subkey(path) {
98                if key.get_value::<u32, _>("Installed").ok() == Some(1) {
99                    return true;
100                }
101            }
102        }
103        false
104    }
105
106    pub fn check() -> PrerequisiteStatus {
107        let bundled = find_bundled_webview2();
108        let webview2_bundled = bundled.is_some();
109        let webview2_evergreen = evergreen_webview2_installed();
110        let webview2_ready = webview2_bundled || webview2_evergreen;
111        let vc_redist_ready = vc_redist_x64_installed();
112        PrerequisiteStatus {
113            platform_requires_check: true,
114            webview2_ready,
115            webview2_bundled,
116            webview2_evergreen,
117            vc_redist_ready,
118            all_ready: webview2_ready && vc_redist_ready,
119            bundled_webview2_dir: bundled
120                .map(|p| p.to_string_lossy().into_owned())
121                .unwrap_or_default(),
122        }
123    }
124
125    pub fn configure_webview2_runtime() -> bool {
126        let Some(dir) = find_bundled_webview2() else {
127            return evergreen_webview2_installed();
128        };
129        std::env::set_var(
130            "WEBVIEW2_BROWSER_EXECUTABLE_FOLDER",
131            dir.to_string_lossy().as_ref(),
132        );
133        true
134    }
135
136    fn launch_downloaded(path: &Path) -> Result<(), String> {
137        std::process::Command::new("cmd")
138            .args(["/C", "start", "", &path.to_string_lossy()])
139            .spawn()
140            .map_err(|e| format!("Launch installer: {e}"))?;
141        Ok(())
142    }
143
144    async fn download_to_temp(url: &str, file_name: &str) -> Result<PathBuf, String> {
145        let response = reqwest::get(url)
146            .await
147            .map_err(|e| format!("Download failed: {e}"))?;
148        if !response.status().is_success() {
149            return Err(format!("Download HTTP {}", response.status()));
150        }
151        let bytes = response
152            .bytes()
153            .await
154            .map_err(|e| format!("Read body: {e}"))?;
155        let path = std::env::temp_dir().join(file_name);
156        std::fs::write(&path, &bytes).map_err(|e| format!("Write installer: {e}"))?;
157        Ok(path)
158    }
159
160    pub async fn install_prerequisite(kind: &str) -> Result<(), String> {
161        let (url, file_name) = match kind {
162            "vc_redist" => (VC_REDIST_URL, "vc_redist.x64.exe"),
163            "webview2" => (WEBVIEW2_BOOTSTRAPPER_URL, "MicrosoftEdgeWebview2Setup.exe"),
164            _ => return Err(format!("Unknown prerequisite kind: {kind}")),
165        };
166        let path = download_to_temp(url, file_name).await?;
167        launch_downloaded(&path)
168    }
169}
170
171#[cfg(not(windows))]
172mod win {
173    use super::PrerequisiteStatus;
174
175    pub fn check() -> PrerequisiteStatus {
176        PrerequisiteStatus {
177            platform_requires_check: false,
178            webview2_ready: true,
179            webview2_bundled: false,
180            webview2_evergreen: false,
181            vc_redist_ready: true,
182            all_ready: true,
183            bundled_webview2_dir: String::new(),
184        }
185    }
186
187    pub fn configure_webview2_runtime() -> bool {
188        true
189    }
190
191    pub async fn install_prerequisite(_kind: &str) -> Result<(), String> {
192        Ok(())
193    }
194}
195
196pub fn check_prerequisites() -> PrerequisiteStatus {
197    win::check()
198}
199
200pub fn configure_webview2_runtime() -> bool {
201    win::configure_webview2_runtime()
202}
203
204pub async fn install_prerequisite(kind: String) -> Result<(), String> {
205    win::install_prerequisite(kind.as_str()).await
206}
207
208#[cfg(test)]
209mod tests {
210    #[allow(unused_imports)]
211    use super::*;
212
213    #[test]
214    fn non_windows_all_ready() {
215        #[cfg(not(windows))]
216        {
217            let s = check_prerequisites();
218            assert!(s.all_ready);
219            assert!(!s.platform_requires_check);
220        }
221    }
222}