qualia_client_core/
prerequisites.rs1#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct PrerequisiteStatus {
13 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 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}