windows_reactor_setup/
lib.rs1#![doc = include_str!("../readme.md")]
2
3use std::env;
4use std::ffi::OsStr;
5use std::fs;
6use std::path::{Path, PathBuf};
7use std::process::Command;
8
9const RUNTIME_PKG: &str = "Microsoft.WindowsAppSDK.Runtime";
10const RUNTIME_VER: &str = "2.4.0";
11const RUNTIME_FILES: &str = include_str!("../assets/runtime.txt");
12const APP_MANIFEST: &str = include_str!("../assets/app.manifest");
13const NUGET_URL: &str = "https://www.nuget.org/api/v2/package/{name}/{version}";
14const WEBVIEW2_PKG: &str = "Microsoft.Web.WebView2";
15const WEBVIEW2_VER: &str = "1.0.4078.44";
16const WEBVIEW2_CORE_DLL: &str = "Microsoft.Web.WebView2.Core.dll";
17const SELF_CONTAINED_MARKER: &str = "windows-reactor-self-contained";
18
19fn assert_windows() {
20 match env::var("CARGO_CFG_TARGET_OS").as_deref() {
21 Ok("windows") => {}
22 Ok(os) => panic!("unsupported target OS: {os}"),
23 Err(_) => panic!("CARGO_CFG_TARGET_OS not set"),
24 }
25}
26
27pub fn as_self_contained() {
29 assert_windows();
30
31 let out_dir = out_dir();
32 let temp_dir = temp_dir();
33 let runtime = stage_pkg(RUNTIME_PKG, RUNTIME_VER, &temp_dir);
34 let extract = ensure_msix_extracted(&runtime);
35 let dest = target_dir_from_out(&out_dir);
36 copy_runtime_to(&extract, &dest);
37 deploy_webview2(&temp_dir, &dest);
38
39 let manifest_path = out_dir.join("app.manifest");
40 let mut manifest = APP_MANIFEST.to_string();
41 let assembly = manifest.find("<assembly").unwrap();
42 let opening = assembly + manifest[assembly..].find('>').unwrap() + 1;
43 manifest.insert_str(
44 opening,
45 &format!("<description>{SELF_CONTAINED_MARKER}</description>"),
46 );
47 fs::write(&manifest_path, manifest).unwrap_or_else(|e| {
48 panic!(
49 "failed to write manifest to {}: {e}",
50 manifest_path.display()
51 )
52 });
53 let target_env = env::var("CARGO_CFG_TARGET_ENV").expect("CARGO_CFG_TARGET_ENV not set");
54 let target_abi = env::var("CARGO_CFG_TARGET_ABI").unwrap_or_default();
55 match (target_env.as_str(), target_abi.as_str()) {
56 ("msvc", _) => {
57 println!("cargo:rustc-link-arg-bins=/MANIFEST:EMBED");
58 println!(
59 "cargo:rustc-link-arg-bins=/MANIFESTINPUT:{}",
60 manifest_path.display()
61 );
62 }
63 ("gnu", "llvm") => {
64 println!("cargo:rustc-link-arg-bins=-Wl,/MANIFEST:EMBED");
65 println!(
66 "cargo:rustc-link-arg-bins=-Wl,/MANIFESTINPUT:{}",
67 manifest_path.display()
68 );
69 }
70 _ => panic!("unsupported target environment: {target_env}{target_abi}"),
71 }
72}
73
74fn deploy_webview2(temp: &Path, dest: &Path) {
82 let pkg = stage_pkg(WEBVIEW2_PKG, WEBVIEW2_VER, temp);
83 let src = pkg
84 .join(format!("win-{}", target_arch()))
85 .join("native_uap")
86 .join(WEBVIEW2_CORE_DLL);
87 copy_file(&src, dest, WEBVIEW2_CORE_DLL);
88}
89
90fn out_dir() -> PathBuf {
91 PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"))
92}
93
94fn temp_dir() -> PathBuf {
95 let base = if let Some(p) = env::var_os("LOCALAPPDATA") {
96 PathBuf::from(p)
97 } else if let Some(p) = env::var_os("XDG_CACHE_HOME") {
98 PathBuf::from(p)
99 } else if let Some(p) = env::var_os("HOME") {
100 PathBuf::from(p).join(".cache")
101 } else {
102 panic!(
103 "could not determine cache directory: LOCALAPPDATA, XDG_CACHE_HOME, and HOME are all unset"
104 );
105 };
106 let temp = base.join("windows-reactor-setup").join("temp");
107 let _ = fs::create_dir_all(&temp);
108 temp
109}
110
111fn ensure_msix_extracted(runtime: &Path) -> PathBuf {
112 let arch = format!("win10-{}", target_arch());
113 let msix = runtime
114 .join("MSIX")
115 .join(&arch)
116 .join("Microsoft.WindowsAppRuntime.2.msix");
117 let extract = runtime.join(".msix_extract");
118 if !extract.is_dir() {
119 let _ = fs::create_dir_all(&extract);
120 if !msix.is_file() {
121 println!("MSIX not found at {}", msix.display());
122 } else {
123 extract_tar(&msix, &extract, &[]);
124 }
125 }
126 extract
127}
128
129fn copy_runtime_to(src: &Path, dest: &Path) {
130 let Ok(entries) = fs::read_dir(src) else {
131 return;
132 };
133 for entry in entries.flatten() {
134 let path = entry.path();
135 let Some(name) = entry.file_name().into_string().ok() else {
136 continue;
137 };
138 if !RUNTIME_FILES
139 .lines()
140 .any(|l| l.trim().eq_ignore_ascii_case(&name))
141 {
142 continue;
143 }
144 if path.is_file() {
145 copy_file(&path, dest, &name);
146 } else if path.is_dir() {
147 let sub = dest.join(&name);
148 let _ = fs::create_dir_all(&sub);
149 copy_dir_contents(&path, &sub);
150 }
151 }
152}
153
154fn copy_dir_contents(src: &Path, dest: &Path) {
155 let Ok(entries) = fs::read_dir(src) else {
156 return;
157 };
158 for entry in entries.flatten() {
159 let path = entry.path();
160 let Some(name) = entry.file_name().into_string().ok() else {
161 continue;
162 };
163 if path.is_file() {
164 copy_file(&path, dest, &name);
165 } else if path.is_dir() {
166 let sub = dest.join(&name);
167 let _ = fs::create_dir_all(&sub);
168 copy_dir_contents(&path, &sub);
169 }
170 }
171}
172
173fn copy_file(src: &Path, base: &Path, name: &str) {
174 if !src.is_file() {
175 println!("{name} not found at {}", src.display());
176 return;
177 }
178 let _ = fs::create_dir_all(base);
179 let _ = fs::copy(src, base.join(name));
180}
181
182fn stage_pkg(name: &str, ver: &str, temp: &Path) -> PathBuf {
183 let nupkg = temp.join(format!("{name}.{ver}.nupkg"));
184 let extract = temp.join(format!("{name}-{ver}"));
185 if !nupkg.is_file() {
186 dl_nupkg(name, ver, &nupkg);
187 }
188 if !extract.is_dir() {
189 let _ = fs::create_dir_all(&extract);
190 extract_tar(&nupkg, &extract, &["--strip-components=1"]);
191 }
192 extract
193}
194
195fn dl_nupkg(name: &str, ver: &str, dest: &Path) {
196 let url = NUGET_URL.replace("{name}", name).replace("{version}", ver);
197 println!("Downloading {name} {ver}");
198 let curl = env::var_os("SystemRoot")
199 .map(|r| PathBuf::from(r).join("System32\\curl.exe"))
200 .filter(|p| p.is_file());
201 match curl.and_then(|c| {
202 Command::new(&c)
203 .args(["-s", "-L", "-o", dest.to_str().unwrap(), &url])
204 .output()
205 .ok()
206 }) {
207 Some(out) if out.status.success() => {
208 println!("Downloaded {name} {ver}");
209 }
210 _ => {
211 println!("Download failed for {name} {ver}");
212 }
213 }
214}
215
216fn extract_tar(src: &Path, dst: &Path, extra: &[&str]) {
217 println!("Extracting {} to {}", src.display(), dst.display());
218 let tar = env::var_os("SystemRoot")
219 .map(|r| PathBuf::from(r).join("System32\\tar.exe"))
220 .filter(|p| p.is_file());
221 if let Some(t) = tar {
222 let _ = Command::new(&t)
223 .args(["-xf", src.to_str().unwrap(), "-C", dst.to_str().unwrap()])
224 .args(extra)
225 .output();
226 }
227}
228
229fn target_dir_from_out(out: &Path) -> PathBuf {
230 env::var_os("PROFILE")
231 .and_then(|profile| target_dir_for_profile(out, &profile))
232 .unwrap_or_else(|| out.ancestors().nth(3).unwrap_or(out).to_path_buf())
233}
234
235fn target_dir_for_profile(out: &Path, profile: &OsStr) -> Option<PathBuf> {
236 out.ancestors()
237 .find(|path| path.file_name() == Some(profile))
238 .map(Path::to_path_buf)
239}
240
241fn target_arch() -> &'static str {
242 match env::var("CARGO_CFG_TARGET_ARCH").as_deref() {
243 Ok("aarch64") => "arm64",
244 Ok("x86") => "x86",
245 _ => "x64",
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn finds_profile_in_standard_cargo_out_dir() {
255 let out = Path::new(r"C:\repo\target\debug\build\package-hash\out");
256 assert_eq!(
257 target_dir_for_profile(out, OsStr::new("debug")),
258 Some(PathBuf::from(r"C:\repo\target\debug"))
259 );
260 }
261
262 #[test]
263 fn finds_profile_in_split_package_cargo_out_dir() {
264 let out = Path::new(r"C:\repo\target\debug\build\package\hash\out");
265 assert_eq!(
266 target_dir_for_profile(out, OsStr::new("debug")),
267 Some(PathBuf::from(r"C:\repo\target\debug"))
268 );
269 }
270}