diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
index 0000000..ee27032
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,7 @@
+# Link the C runtime statically. Without the Visual C++ Redistributable the loader
+# would stop the tool before its Windows App Runtime check can run.
+[target.x86_64-pc-windows-msvc]
+rustflags = ["-C", "target-feature=+crt-static"]
+
+[target.aarch64-pc-windows-msvc]
+rustflags = ["-C", "target-feature=+crt-static"]
diff --git a/Cargo.lock b/Cargo.lock
index 228bb51..7a6e440 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -201,6 +201,7 @@ version = "0.1.2"
dependencies = [
"embed-resource",
"ico",
+ "nyanpasu-windows-dependencies",
"rayon",
"resvg",
"rust-i18n",
@@ -475,12 +476,39 @@ dependencies = [
"autocfg",
]
+[[package]]
+name = "nyanpasu-windows-dependencies"
+version = "0.1.0"
+source = "git+https://github.com/libnyanpasu/nyanpasu-windows-dependencies.git?rev=df7fa8f4adbfe60f0d574560a228ccae3dd7b548#df7fa8f4adbfe60f0d574560a228ccae3dd7b548"
+dependencies = [
+ "snafu",
+ "strum",
+ "tracing",
+ "windows-collections",
+ "windows-core",
+ "windows-future",
+ "windows-link 0.100.0",
+ "windows-registry",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
[[package]]
name = "pico-args"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
[[package]]
name = "png"
version = "0.17.16"
@@ -827,6 +855,27 @@ dependencies = [
"float-cmp",
]
+[[package]]
+name = "strum"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
[[package]]
name = "svgtypes"
version = "0.15.3"
@@ -974,6 +1023,37 @@ version = "1.1.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770"
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
[[package]]
name = "triomphe"
version = "0.1.17"
@@ -1153,6 +1233,15 @@ dependencies = [
"windows-time",
]
+[[package]]
+name = "windows-registry"
+version = "0.100.0"
+dependencies = [
+ "windows-link 0.100.0",
+ "windows-result",
+ "windows-strings",
+]
+
[[package]]
name = "windows-result"
version = "0.100.0"
diff --git a/Cargo.toml b/Cargo.toml
index 18f66cf..a7d9acb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,3 +18,14 @@ codegen-units = 1
# Native GUI callbacks cannot unwind across the Windows ABI boundary.
panic = "abort"
strip = "symbols"
+
+# nyanpasu-windows-dependencies uses the published 0.100 support crates; build them
+# from the vendored fork instead, so the executable carries one copy.
+[patch.crates-io]
+windows-collections = { path = "vendor/windows-rs/crates/libs/collections" }
+windows-core = { path = "vendor/windows-rs/crates/libs/core" }
+windows-future = { path = "vendor/windows-rs/crates/libs/future" }
+windows-link = { path = "vendor/windows-rs/crates/libs/link" }
+windows-registry = { path = "vendor/windows-rs/crates/libs/registry" }
+windows-result = { path = "vendor/windows-rs/crates/libs/result" }
+windows-strings = { path = "vendor/windows-rs/crates/libs/strings" }
diff --git a/README.md b/README.md
index 94a3fea..0561528 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,8 @@ App Runtime **2.5.1.0 or later**, matching the process architecture:
Download the official **2.5.1** installer for [x64](https://aka.ms/windowsappsdk/2.5/2.5.1/windowsappruntimeinstall-x64.exe)
or [arm64](https://aka.ms/windowsappsdk/2.5/2.5.1/windowsappruntimeinstall-arm64.exe).
See also the [Windows App SDK downloads](https://learn.microsoft.com/windows/apps/windows-app-sdk/downloads).
+When the runtime is missing, the GUI offers to install it from the Microsoft Store
+(where the Store is available) or opens these downloads.
Build and run in Git Bash:
diff --git a/crates/enable-loopback/Cargo.toml b/crates/enable-loopback/Cargo.toml
index a506622..14c983e 100644
--- a/crates/enable-loopback/Cargo.toml
+++ b/crates/enable-loopback/Cargo.toml
@@ -13,10 +13,11 @@ test = false
[dependencies]
rust-i18n = "4"
+nyanpasu-windows-dependencies = { git = "https://github.com/libnyanpasu/nyanpasu-windows-dependencies.git", rev = "df7fa8f4adbfe60f0d574560a228ccae3dd7b548" }
snafu = "0.8"
sys-locale = "0.3"
windows-reactor = { path = "../../vendor/windows-rs/crates/libs/reactor" }
-windows = { path = "../../vendor/windows-rs/crates/libs/windows", features = ["appmodel", "libloaderapi", "debugapi", "winuser", "shellapi", "commctrl", "winerror", "winnt", "netfw", "sddl", "secext", "securitybaseapi", "shlwapi", "winbase", "heapapi", "winreg", "errhandlingapi", "processenv", "synchapi", "memoryapi", "handleapi", "processthreadsapi", "datetimeapi", "sysinfoapi"] }
+windows = { path = "../../vendor/windows-rs/crates/libs/windows", features = ["libloaderapi", "debugapi", "winuser", "shellapi", "commctrl", "winerror", "winnt", "netfw", "sddl", "secext", "securitybaseapi", "shlwapi", "winbase", "heapapi", "winreg", "errhandlingapi", "processenv", "synchapi", "memoryapi", "handleapi", "processthreadsapi", "datetimeapi", "sysinfoapi"] }
[build-dependencies]
resvg = { version = "0.45", default-features = false }
diff --git a/crates/enable-loopback/build.rs b/crates/enable-loopback/build.rs
index 8a7dff1..e078257 100644
--- a/crates/enable-loopback/build.rs
+++ b/crates/enable-loopback/build.rs
@@ -48,6 +48,7 @@ fn main() {
PerMonitorV2
UTF-8
+ SegmentHeap
diff --git a/crates/enable-loopback/locales/en.json b/crates/enable-loopback/locales/en.json
index 277d8e1..8196f89 100644
--- a/crates/enable-loopback/locales/en.json
+++ b/crates/enable-loopback/locales/en.json
@@ -38,9 +38,14 @@
"runtime": {
"title": "Windows App Runtime required",
"missing": "This tool requires Windows App Runtime %{version} or later for %{architecture}.",
+ "install_from_store": "Install from Microsoft Store",
"download_installer": "Download installer",
"open_download_page": "Open download page",
- "cancel": "Cancel"
+ "cancel": "Cancel",
+ "installing": "Installing Windows App Runtime from Microsoft Store. If you close this window, the installation continues in the background.",
+ "close": "Close",
+ "store_failed": "Microsoft Store could not install Windows App Runtime: %{detail}",
+ "store_incomplete": "Microsoft Store finished, but Windows App Runtime %{version} or later is still not installed."
},
"cli": {
"syntax": "Syntax:\nenableLoopback [--lang CODE] [-all | -none | -allow NAME... | -deny NAME... | ?]\nenableLoopback [manual] [-registry] [--lang CODE]\n\n-all: Exempt all AppContainers.\n-none: Clear all exemptions.\n-allow NAME: Add AppContainers whose names contain NAME.\n-deny NAME: Remove matching exemptions; * removes all.\nCommands ignore case; the leading - or / is optional.\nNAME may contain spaces. Use * to match all.\nLanguages: en, zh-cn, zh-tw, ja, ko, ru.\nmanual: Skip automatic refresh. -registry: Read the registry directly.",
diff --git a/crates/enable-loopback/locales/ja.json b/crates/enable-loopback/locales/ja.json
index a4cc2f4..83873ec 100644
--- a/crates/enable-loopback/locales/ja.json
+++ b/crates/enable-loopback/locales/ja.json
@@ -38,9 +38,14 @@
"runtime": {
"title": "Windows App Runtime が必要です",
"missing": "このツールには %{architecture} 用の Windows App Runtime %{version} 以降が必要です。",
+ "install_from_store": "Microsoft Store からインストール",
"download_installer": "インストーラーをダウンロード",
"open_download_page": "ダウンロードページを開く",
- "cancel": "キャンセル"
+ "cancel": "キャンセル",
+ "installing": "Microsoft Store から Windows App Runtime をインストールしています。このウィンドウを閉じても、インストールはバックグラウンドで続行されます。",
+ "close": "閉じる",
+ "store_failed": "Microsoft Store で Windows App Runtime をインストールできませんでした: %{detail}",
+ "store_incomplete": "Microsoft Store の処理は完了しましたが、Windows App Runtime %{version} 以降はまだインストールされていません。"
},
"cli": {
"syntax": "構文:\nenableLoopback [--lang CODE] [-all | -none | -allow NAME... | -deny NAME... | ?]\nenableLoopback [manual] [-registry] [--lang CODE]\n\n-all:すべての AppContainer を除外。\n-none:除外リストを空にする。\n-allow NAME:名前に NAME を含む AppContainer を追加。\n-deny NAME:一致する除外を解除。* ですべて解除。\n大文字と小文字は区別しません。先頭の - または / は省略可能です。\nNAME には空白を含められます。* はすべてに一致します。\n言語:en、zh-cn、zh-tw、ja、ko、ru。\nmanual:自動更新しない。-registry:レジストリから直接読み取る。",
diff --git a/crates/enable-loopback/locales/ko.json b/crates/enable-loopback/locales/ko.json
index b426789..73e2ea4 100644
--- a/crates/enable-loopback/locales/ko.json
+++ b/crates/enable-loopback/locales/ko.json
@@ -38,9 +38,14 @@
"runtime": {
"title": "Windows App Runtime 필요",
"missing": "이 도구에는 %{architecture}용 Windows App Runtime %{version} 이상이 필요합니다.",
+ "install_from_store": "Microsoft Store에서 설치",
"download_installer": "설치 프로그램 다운로드",
"open_download_page": "다운로드 페이지 열기",
- "cancel": "취소"
+ "cancel": "취소",
+ "installing": "Microsoft Store에서 Windows App Runtime을 설치하는 중입니다. 이 창을 닫아도 설치는 백그라운드에서 계속됩니다.",
+ "close": "닫기",
+ "store_failed": "Microsoft Store에서 Windows App Runtime을 설치하지 못했습니다: %{detail}",
+ "store_incomplete": "Microsoft Store 작업은 끝났지만 Windows App Runtime %{version} 이상이 아직 설치되지 않았습니다."
},
"cli": {
"syntax": "사용법:\nenableLoopback [--lang CODE] [-all | -none | -allow NAME... | -deny NAME... | ?]\nenableLoopback [manual] [-registry] [--lang CODE]\n\n-all: 모든 AppContainer의 예외를 허용합니다.\n-none: 예외 목록을 비웁니다.\n-allow NAME: 이름에 NAME이 포함된 AppContainer를 추가합니다.\n-deny NAME: 일치하는 예외를 해제합니다. *는 모두 해제합니다.\n명령은 대소문자를 구분하지 않으며 앞의 - 또는 /는 생략할 수 있습니다.\nNAME에 공백을 사용할 수 있습니다. *는 모두 일치합니다.\n언어: en, zh-cn, zh-tw, ja, ko, ru.\nmanual: 자동 새로 고침을 건너뜁니다. -registry: 레지스트리에서 직접 읽습니다.",
diff --git a/crates/enable-loopback/locales/ru.json b/crates/enable-loopback/locales/ru.json
index 321ff19..901b374 100644
--- a/crates/enable-loopback/locales/ru.json
+++ b/crates/enable-loopback/locales/ru.json
@@ -38,9 +38,14 @@
"runtime": {
"title": "Требуется Windows App Runtime",
"missing": "Для этой утилиты требуется Windows App Runtime %{version} или новее для %{architecture}.",
+ "install_from_store": "Установить из Microsoft Store",
"download_installer": "Скачать установщик",
"open_download_page": "Открыть страницу загрузки",
- "cancel": "Отмена"
+ "cancel": "Отмена",
+ "installing": "Windows App Runtime устанавливается из Microsoft Store. Если закрыть это окно, установка продолжится в фоновом режиме.",
+ "close": "Закрыть",
+ "store_failed": "Microsoft Store не удалось установить Windows App Runtime: %{detail}",
+ "store_incomplete": "Microsoft Store завершил работу, но Windows App Runtime %{version} или новее по-прежнему не установлен."
},
"cli": {
"syntax": "Синтаксис:\nenableLoopback [--lang CODE] [-all | -none | -allow NAME... | -deny NAME... | ?]\nenableLoopback [manual] [-registry] [--lang CODE]\n\n-all: Разрешить замыкание на себя всем AppContainer.\n-none: Очистить список исключений.\n-allow NAME: Добавить AppContainer, имена которых содержат NAME.\n-deny NAME: Удалить совпадающие исключения; * удаляет все.\nРегистр не учитывается. Начальный - или / необязателен.\nNAME может содержать пробелы. * соответствует всем именам.\nЯзыки: en, zh-cn, zh-tw, ja, ko, ru.\nmanual: Не обновлять автоматически. -registry: Читать прямо из реестра.",
diff --git a/crates/enable-loopback/locales/zh-cn.json b/crates/enable-loopback/locales/zh-cn.json
index 9996be7..7707462 100644
--- a/crates/enable-loopback/locales/zh-cn.json
+++ b/crates/enable-loopback/locales/zh-cn.json
@@ -38,9 +38,14 @@
"runtime": {
"title": "需要 Windows App Runtime",
"missing": "本工具需要适用于 %{architecture} 的 Windows App Runtime %{version} 或更高版本。",
+ "install_from_store": "从 Microsoft Store 安装",
"download_installer": "下载安装程序",
"open_download_page": "打开下载页",
- "cancel": "取消"
+ "cancel": "取消",
+ "installing": "正在从 Microsoft Store 安装 Windows App Runtime。关闭此窗口后,安装会在后台继续。",
+ "close": "关闭",
+ "store_failed": "Microsoft Store 无法安装 Windows App Runtime:%{detail}",
+ "store_incomplete": "Microsoft Store 已完成,但仍未安装 Windows App Runtime %{version} 或更高版本。"
},
"cli": {
"syntax": "语法:\nenableLoopback [--lang CODE] [-all | -none | -allow NAME... | -deny NAME... | ?]\nenableLoopback [manual] [-registry] [--lang CODE]\n\n-all:豁免所有 AppContainer。\n-none:清空豁免名单。\n-allow NAME:添加名称包含 NAME 的 AppContainer。\n-deny NAME:移除名称匹配的豁免;* 表示全部移除。\n命令不区分大小写,前导 - 或 / 可省略。\nNAME 可包含空格,用 * 匹配全部。\n语言:en、zh-cn、zh-tw、ja、ko、ru。\nmanual:不自动刷新。-registry:直接读取注册表。",
diff --git a/crates/enable-loopback/locales/zh-tw.json b/crates/enable-loopback/locales/zh-tw.json
index 75b96fb..b8ff48b 100644
--- a/crates/enable-loopback/locales/zh-tw.json
+++ b/crates/enable-loopback/locales/zh-tw.json
@@ -38,9 +38,14 @@
"runtime": {
"title": "需要 Windows App Runtime",
"missing": "本工具需要適用於 %{architecture} 的 Windows App Runtime %{version} 或更新版本。",
+ "install_from_store": "從 Microsoft Store 安裝",
"download_installer": "下載安裝程式",
"open_download_page": "開啟下載頁面",
- "cancel": "取消"
+ "cancel": "取消",
+ "installing": "正在從 Microsoft Store 安裝 Windows App Runtime。關閉此視窗後,安裝會在背景繼續。",
+ "close": "關閉",
+ "store_failed": "Microsoft Store 無法安裝 Windows App Runtime:%{detail}",
+ "store_incomplete": "Microsoft Store 已完成,但仍未安裝 Windows App Runtime %{version} 或更新版本。"
},
"cli": {
"syntax": "語法:\nenableLoopback [--lang CODE] [-all | -none | -allow NAME... | -deny NAME... | ?]\nenableLoopback [manual] [-registry] [--lang CODE]\n\n-all:豁免所有 AppContainer。\n-none:清空豁免清單。\n-allow NAME:新增名稱包含 NAME 的 AppContainer。\n-deny NAME:移除名稱相符的豁免;* 表示全部移除。\n命令不分大小寫,開頭的 - 或 / 可省略。\nNAME 可包含空格,以 * 比對全部。\n語言:en、zh-cn、zh-tw、ja、ko、ru。\nmanual:不自動重新整理。-registry:直接讀取登錄。",
diff --git a/crates/enable-loopback/src/runtime_check.rs b/crates/enable-loopback/src/runtime_check.rs
index 0c5521f..21a8e8e 100644
--- a/crates/enable-loopback/src/runtime_check.rs
+++ b/crates/enable-loopback/src/runtime_check.rs
@@ -1,23 +1,37 @@
-//! Check the framework runtime before reactor bootstrap; offer a localized download prompt (§8.2).
+//! Check the framework runtime before reactor bootstrap. When it is missing, offer to
+//! install it through the Microsoft Store, or the official download (§8.2).
+use std::{
+ sync::{
+ Arc, Mutex,
+ atomic::{AtomicBool, AtomicU32, Ordering},
+ },
+ thread,
+};
+
+use nyanpasu_windows_dependencies::{
+ FrameworkDependency, Version,
+ store::{InstallState, StoreInstaller},
+};
use snafu::Snafu;
use windows::{
Win32::{
- appmodel::*,
+ HWND, LPARAM, WPARAM,
commctrl::*,
shellapi::ShellExecuteW,
- winerror::ERROR_INSUFFICIENT_BUFFER,
winnt::{PROCESSOR_ARCHITECTURE_AMD64, PROCESSOR_ARCHITECTURE_ARM64},
winuser::*,
},
- core::{HSTRING, PCWSTR, PWSTR, w},
+ core::{HRESULT, HSTRING, PCWSTR, w},
};
pub const DOWNLOAD_URL: &str = "https://learn.microsoft.com/windows/apps/windows-app-sdk/downloads";
-const FAMILY: PCWSTR = w!("Microsoft.WindowsAppRuntime.2_8wekyb3d8bbwe");
+const RUNTIME: FrameworkDependency = FrameworkDependency::WindowsAppRuntime2;
// windows-rs a29e064, crates/libs/reactor/src/native/bindings.rs:32212–32216,
// WINDOWSAPPSDK_RUNTIME_VERSION_{MAJOR,MINOR,BUILD,REVISION,UINT64}.
-const REQUIRED_VERSION: [u16; 4] = [2, 5, 1, 0];
+const REQUIRED_VERSION: Version = Version::new(2, 5, 1, 0);
+/// Names this tool in the Microsoft Store's install log.
+const STORE_CLIENT_ID: &str = "enable-loopback";
#[cfg(target_arch = "x86_64")]
const PROCESS_ARCHITECTURE: u32 = PROCESSOR_ARCHITECTURE_AMD64 as u32;
#[cfg(target_arch = "aarch64")]
@@ -27,105 +41,14 @@ compile_error!("enable-loopback supports Windows x64 and arm64 only");
#[derive(Debug, Snafu)]
pub enum Error {
- #[snafu(display("GetPackagesByPackageFamily failed with status {status:#x}"))]
- EnumeratePackages { status: i32 },
- #[snafu(display(
- "PackageIdFromFullName failed for {package_full_name} with status {status:#x}"
- ))]
- ReadPackageId {
- package_full_name: String,
- status: i32,
- },
#[snafu(display("ShellExecuteW failed for {url} with status {status}"))]
OpenDownload { url: String, status: isize },
- #[snafu(display("Displaying the runtime download dialog failed: {source}"))]
+ #[snafu(display("Displaying the runtime dialog failed: {source}"))]
ShowDialog { source: windows::core::Error },
}
-fn required_version_u64() -> u64 {
- REQUIRED_VERSION
- .into_iter()
- .fold(0, |version, part| (version << 16) | u64::from(part))
-}
-
-fn compatible(version: u64, architecture: u32) -> bool {
- version >= required_version_u64() && architecture == PROCESS_ARCHITECTURE
-}
-
-/// Query packages registered for the current user without COM or WinAppSDK initialization.
-/// Retry if registration changes between the size query and the buffered query.
-pub fn is_installed() -> Result {
- for _ in 0..3 {
- let (mut count, mut length) = (0, 0);
- // SAFETY: output counters are valid; null buffers request required sizes.
- let status =
- unsafe { GetPackagesByPackageFamily(FAMILY, &mut count, None, &mut length, None) };
- if status == 0 && count == 0 {
- return Ok(false);
- }
- if status != ERROR_INSUFFICIENT_BUFFER {
- return Err(Error::EnumeratePackages { status });
- }
- let mut names = vec![PWSTR::null(); count as usize];
- let mut buffer = vec![0u16; length as usize];
- // SAFETY: both buffers have the exact capacities returned by the first query.
- let status = unsafe {
- GetPackagesByPackageFamily(
- FAMILY,
- &mut count,
- Some(names.as_mut_ptr()),
- &mut length,
- Some(buffer.as_mut_ptr()),
- )
- };
- if status == ERROR_INSUFFICIENT_BUFFER {
- continue;
- }
- if status != 0 {
- return Err(Error::EnumeratePackages { status });
- }
- for name in names.into_iter().take(count as usize) {
- // SAFETY: name is a terminated string pointing into the still-live buffer.
- if unsafe { package_is_compatible(PCWSTR(name.0)) }? {
- return Ok(true);
- }
- }
- return Ok(false);
- }
- Err(Error::EnumeratePackages {
- status: ERROR_INSUFFICIENT_BUFFER,
- })
-}
-
-/// `name` must point to a live, NUL-terminated package full name.
-unsafe fn package_is_compatible(name: PCWSTR) -> Result {
- let mut length = 0;
- // SAFETY: caller guarantees name validity; null buffer requests its size.
- let status = unsafe { PackageIdFromFullName(name, 0, &mut length, None) };
- if status != ERROR_INSUFFICIENT_BUFFER {
- return Err(Error::ReadPackageId {
- package_full_name: unsafe { name.to_string() }.unwrap_or_default(),
- status,
- });
- }
- // u64 storage supplies alignment as well as the requested byte count.
- let mut buffer = vec![0u64; (length as usize).div_ceil(size_of::())];
- // SAFETY: buffer is aligned and has at least length bytes; name is still live.
- let status =
- unsafe { PackageIdFromFullName(name, 0, &mut length, Some(buffer.as_mut_ptr().cast())) };
- if status != 0 {
- return Err(Error::ReadPackageId {
- package_full_name: unsafe { name.to_string() }.unwrap_or_default(),
- status,
- });
- }
- // SAFETY: successful API call populated PACKAGE_ID and its trailing strings.
- let id = unsafe { buffer.as_ptr().cast::().read_unaligned() };
- // SAFETY: Version is the packed integer representation of the version union.
- Ok(compatible(
- unsafe { id.version.Anonymous.Version },
- id.processorArchitecture,
- ))
+fn satisfied(installed: Option) -> bool {
+ installed.is_some_and(|version| version >= REQUIRED_VERSION)
}
/// Build the official installer URL for a supported process architecture.
@@ -135,51 +58,100 @@ fn installer_url(architecture: u32) -> String {
PROCESSOR_ARCHITECTURE_ARM64 => "arm64",
_ => unreachable!("only x64 and arm64 processes are supported"),
};
- let [major, minor, build, _] = REQUIRED_VERSION;
+ let Version {
+ major,
+ minor,
+ build,
+ ..
+ } = REQUIRED_VERSION;
format!(
"https://aka.ms/windowsappsdk/{major}.{minor}/{major}.{minor}.{build}/windowsappruntimeinstall-{architecture}.exe"
)
}
-/// Returns false after a missing-runtime prompt. An inconclusive check lets reactor bootstrap.
+/// Returns false when the runtime is still missing after the prompt. An
+/// inconclusive check lets reactor bootstrap.
pub fn ensure_available() -> Result {
- match is_installed() {
- Ok(true) => return Ok(true),
- Ok(false) => {}
+ match RUNTIME.diagnose() {
+ Ok(installed) if satisfied(installed) => return Ok(true),
+ Ok(_) => {}
Err(error) => {
- crate::debug(&error.to_string());
+ crate::debug(&describe(&error));
return Ok(true);
}
}
- const DOWNLOAD_INSTALLER: i32 = 100;
- const OPEN_DOWNLOAD_PAGE: i32 = 101;
- let version = REQUIRED_VERSION.map(|part| part.to_string()).join(".");
+ let mut store = store_available();
+ loop {
+ let url = match prompt(store)? {
+ Choice::Store => match install_from_store()? {
+ StoreOutcome::Installed => return Ok(true),
+ StoreOutcome::StoppedWaiting => return Ok(false),
+ StoreOutcome::Failed(detail) => {
+ crate::show_message(
+ &rust_i18n::t!("runtime.store_failed", detail = detail),
+ MB_ICONERROR as u32,
+ );
+ store = false;
+ continue;
+ }
+ },
+ Choice::DownloadInstaller => installer_url(PROCESS_ARCHITECTURE),
+ Choice::OpenDownloadPage => DOWNLOAD_URL.to_owned(),
+ Choice::Cancel => return Ok(false),
+ };
+ open(url)?;
+ return Ok(false);
+ }
+}
+
+/// Windows without the Store app, such as LTSC, cannot install from the Store.
+fn store_available() -> bool {
+ match StoreInstaller::new(STORE_CLIENT_ID).and_then(|store| store.is_available()) {
+ Ok(available) => available,
+ Err(error) => {
+ crate::debug(&describe(&error));
+ false
+ }
+ }
+}
+
+enum Choice {
+ Store,
+ DownloadInstaller,
+ OpenDownloadPage,
+ Cancel,
+}
+
+fn prompt(store: bool) -> Result {
+ const INSTALL_FROM_STORE: i32 = 100;
+ const DOWNLOAD_INSTALLER: i32 = 101;
+ const OPEN_DOWNLOAD_PAGE: i32 = 102;
let text = HSTRING::from(
rust_i18n::t!(
"runtime.missing",
- version = version,
+ version = REQUIRED_VERSION.to_string(),
architecture = std::env::consts::ARCH
)
.as_ref(),
);
let title = HSTRING::from(rust_i18n::t!("runtime.title").as_ref());
+ let from_store = HSTRING::from(rust_i18n::t!("runtime.install_from_store").as_ref());
let installer = HSTRING::from(rust_i18n::t!("runtime.download_installer").as_ref());
let page = HSTRING::from(rust_i18n::t!("runtime.open_download_page").as_ref());
let cancel = HSTRING::from(rust_i18n::t!("runtime.cancel").as_ref());
- let buttons = [
- TASKDIALOG_BUTTON {
- nButtonID: DOWNLOAD_INSTALLER,
- pszButtonText: PCWSTR(installer.as_ptr()),
- },
- TASKDIALOG_BUTTON {
- nButtonID: OPEN_DOWNLOAD_PAGE,
- pszButtonText: PCWSTR(page.as_ptr()),
- },
- TASKDIALOG_BUTTON {
- nButtonID: IDCANCEL,
- pszButtonText: PCWSTR(cancel.as_ptr()),
- },
- ];
+ let button = |id, text: &HSTRING| TASKDIALOG_BUTTON {
+ nButtonID: id,
+ pszButtonText: PCWSTR(text.as_ptr()),
+ };
+ let mut buttons = Vec::with_capacity(4);
+ if store {
+ buttons.push(button(INSTALL_FROM_STORE, &from_store));
+ }
+ buttons.extend([
+ button(DOWNLOAD_INSTALLER, &installer),
+ button(OPEN_DOWNLOAD_PAGE, &page),
+ button(IDCANCEL, &cancel),
+ ]);
let config = TASKDIALOGCONFIG {
cbSize: size_of::() as u32,
dwFlags: TDF_ALLOW_DIALOG_CANCELLATION | TDF_SIZE_TO_CONTENT,
@@ -187,7 +159,7 @@ pub fn ensure_available() -> Result {
pszContent: PCWSTR(text.as_ptr()),
cButtons: buttons.len() as u32,
pButtons: buttons.as_ptr(),
- nDefaultButton: IDCANCEL,
+ nDefaultButton: if store { INSTALL_FROM_STORE } else { IDCANCEL },
..Default::default()
};
let mut answer = IDCANCEL;
@@ -195,11 +167,139 @@ pub fn ensure_available() -> Result {
unsafe { TaskDialogIndirect(&config, Some(&mut answer), None, None) }
.ok()
.map_err(|source| Error::ShowDialog { source })?;
- let url = match answer {
- DOWNLOAD_INSTALLER => installer_url(PROCESS_ARCHITECTURE),
- OPEN_DOWNLOAD_PAGE => DOWNLOAD_URL.to_owned(),
- _ => return Ok(false),
+ Ok(match answer {
+ INSTALL_FROM_STORE => Choice::Store,
+ DOWNLOAD_INSTALLER => Choice::DownloadInstaller,
+ OPEN_DOWNLOAD_PAGE => Choice::OpenDownloadPage,
+ _ => Choice::Cancel,
+ })
+}
+
+enum StoreOutcome {
+ Installed,
+ /// The user closed the progress dialog; the Store keeps installing.
+ StoppedWaiting,
+ Failed(String),
+}
+
+/// Shared between the installing thread and the progress dialog's timer.
+#[derive(Default)]
+struct StoreProgress {
+ percent: AtomicU32,
+ /// The bar starts as a marquee until the Store reports a percentage.
+ determinate: AtomicBool,
+ outcome: Mutex