diff options
| author | David Lönnhager <david.l@mullvad.net> | 2024-12-05 13:49:38 +0100 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2024-12-10 14:34:02 +0100 |
| commit | 930f24a7dc341027272eed73935cbbaa62041af6 (patch) | |
| tree | 81bf9d2141dcb5d3896f8a8cc966da45db7c2a63 | |
| parent | 06b9a755512361ab16a9e8dcd6759108888a88e2 (diff) | |
| download | mullvadvpn-930f24a7dc341027272eed73935cbbaa62041af6.tar.xz mullvadvpn-930f24a7dc341027272eed73935cbbaa62041af6.zip | |
Add windows-installer tool
| -rw-r--r-- | Cargo.lock | 11 | ||||
| -rw-r--r-- | Cargo.toml | 40 | ||||
| -rw-r--r-- | windows-installer/Cargo.toml | 29 | ||||
| -rw-r--r-- | windows-installer/build.rs | 68 | ||||
| -rw-r--r-- | windows-installer/src/main.rs | 12 | ||||
| -rw-r--r-- | windows-installer/src/windows.rs | 145 | ||||
| -rw-r--r-- | windows-installer/windows-installer.manifest | 18 |
7 files changed, 323 insertions, 0 deletions
diff --git a/Cargo.lock b/Cargo.lock index dcb142fbd9..ca1240e694 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5267,6 +5267,17 @@ dependencies = [ ] [[package]] +name = "windows-installer" +version = "0.0.0" +dependencies = [ + "anyhow", + "mullvad-version", + "tempfile", + "windows-sys 0.52.0", + "winres", +] + +[[package]] name = "windows-interface" version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml index 8c2d22a043..7e48d2ae28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,46 @@ members = [ "talpid-wireguard", "tunnel-obfuscation", "wireguard-go-rs", + "windows-installer", +] +# The default members may exclude packages that cannot be built for all targets, or that do not always +# need to be built +default-members = [ + "android/translations-converter", + "desktop/packages/nseventforwarder", + "mullvad-api", + "mullvad-cli", + "mullvad-daemon", + "mullvad-exclude", + "mullvad-fs", + "mullvad-ios", + "mullvad-jni", + "mullvad-management-interface", + "mullvad-nsis", + "mullvad-encrypted-dns-proxy", + "mullvad-paths", + "mullvad-problem-report", + "mullvad-relay-selector", + "mullvad-setup", + "mullvad-types", + "mullvad-types/intersection-derive", + "mullvad-version", + "talpid-core", + "talpid-dbus", + "talpid-future", + "talpid-macos", + "talpid-net", + "talpid-openvpn", + "talpid-openvpn-plugin", + "talpid-platform-metadata", + "talpid-routing", + "talpid-time", + "talpid-tunnel", + "talpid-tunnel-config-client", + "talpid-windows", + "talpid-wireguard", + "tunnel-obfuscation", + "wireguard-go-rs", ] # Keep all lints in sync with `test/Cargo.toml` diff --git a/windows-installer/Cargo.toml b/windows-installer/Cargo.toml new file mode 100644 index 0000000000..5af6b492f9 --- /dev/null +++ b/windows-installer/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "windows-installer" +description = "Pack the Mullvad VPN installer for several platforms into a one executable" +authors.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[target.'cfg(all(target_os = "windows", target_arch = "x86_64"))'.dependencies] +windows-sys = { version = "0.52.0", features = ["Win32_System", "Win32_System_LibraryLoader", "Win32_System_SystemInformation", "Win32_System_Threading"] } +tempfile = "3.10" +anyhow = "1.0" + +[build-dependencies] +winres = "0.1" +anyhow = "1.0" +windows-sys = { version = "0.52.0", features = ["Win32_System", "Win32_System_LibraryLoader", "Win32_System_SystemServices"] } +mullvad-version = { path = "../mullvad-version" } + +[package.metadata.winres] +ProductName = "Mullvad VPN" +CompanyName = "Mullvad VPN AB" +LegalCopyright = "(c) 2024 Mullvad VPN AB" +InternalName = "mullvad-installer" +OriginalFilename = "MullvadVPN.exe" diff --git a/windows-installer/build.rs b/windows-installer/build.rs new file mode 100644 index 0000000000..df3855e109 --- /dev/null +++ b/windows-installer/build.rs @@ -0,0 +1,68 @@ +use anyhow::Context; +use std::{io, path::Path}; + +const IDB_X64EXE: usize = 1; +const IDB_ARM64EXE: usize = 2; + +fn main() -> anyhow::Result<()> { + if !std::env::var("TARGET") + .context("missing TARGET")? + .as_str() + .starts_with("x86_64-pc-windows-") + { + // This crate only makes sense on x64 Windows + return Ok(()); + } + + build_resource_rust_header().context("failed to write resource.rs")?; + + let (Ok(x64_installer), Ok(arm64_installer)) = ( + std::env::var("WIN_X64_INSTALLER"), + std::env::var("WIN_ARM64_INSTALLER"), + ) else { + eprintln!("Not building resource.rc - WIN_X64_INSTALLER and WIN_ARM64_INSTALLER not set"); + // Linking must fail if the resource file isn't built + println!("cargo:rustc-link-lib=dylib=resource"); + return Ok(()); + }; + + let mut res = winres::WindowsResource::new(); + res.append_rc_content(&format!( + r#" +#define IDB_X64EXE {IDB_X64EXE} +#define IDB_ARM64EXE {IDB_ARM64EXE} + +IDB_X64EXE BINARY "{x64_installer}" +IDB_ARM64EXE BINARY "{arm64_installer}" +"# + )); + + res.set("ProductVersion", mullvad_version::VERSION); + res.set_icon("../dist-assets/icon.ico"); + res.set_language(make_lang_id( + windows_sys::Win32::System::SystemServices::LANG_ENGLISH as u16, + windows_sys::Win32::System::SystemServices::SUBLANG_ENGLISH_US as u16, + )); + + println!("cargo:rerun-if-changed=windows-installer.manifest"); + res.set_manifest_file("windows-installer.manifest"); + res.set("FileDescription", "Mullvad VPN installer"); + + res.compile().context("Failed to compile resources") +} + +fn build_resource_rust_header() -> io::Result<()> { + let resource_header = Path::new(&std::env::var("OUT_DIR").unwrap()).join("resource.rs"); + std::fs::write( + resource_header, + format!( + "pub const IDB_X64EXE: usize = {IDB_X64EXE};\n +pub const IDB_ARM64EXE: usize = {IDB_ARM64EXE};\n" + ), + ) +} + +// Sourced from winnt.h: https://learn.microsoft.com/en-us/windows/win32/api/winnt/nf-winnt-makelangid +fn make_lang_id(p: u16, s: u16) -> u16 { + (s << 10) | p +} diff --git a/windows-installer/src/main.rs b/windows-installer/src/main.rs new file mode 100644 index 0000000000..62f80ee024 --- /dev/null +++ b/windows-installer/src/main.rs @@ -0,0 +1,12 @@ +#![windows_subsystem = "windows"] + +#[cfg(target_os = "windows")] +#[path = "windows.rs"] +mod imp; + +#[cfg(not(target_os = "windows"))] +mod imp { + pub fn main() {} +} + +pub use imp::*; diff --git a/windows-installer/src/windows.rs b/windows-installer/src/windows.rs new file mode 100644 index 0000000000..1b74b074b3 --- /dev/null +++ b/windows-installer/src/windows.rs @@ -0,0 +1,145 @@ +//! Universal Windows installer which contains both an x86 installer package and an ARM package. +//! This can only be built for x86 Windows. This is because the installer must run on both x86 and +//! ARM64, and x86 binaries can run on ARM64, but not vice versa. +//! +//! Building this requires two inputs into build.rs: +//! * `WIN_X64_INSTALLER` - a path to the x64 Windows installer +//! * `WIN_ARM64_INSTALLER` - a path to the ARM64 Windows installer +use anyhow::{bail, Context}; +use std::{ + ffi::{c_ushort, OsStr}, + io::{self, Write}, + num::NonZero, + process::{Command, ExitStatus}, + ptr::NonNull, +}; +use tempfile::TempPath; +use windows_sys::{ + w, + Win32::System::{ + LibraryLoader::{FindResourceW, LoadResource, LockResource, SizeofResource}, + SystemInformation::{IMAGE_FILE_MACHINE_AMD64, IMAGE_FILE_MACHINE_ARM64}, + Threading::IsWow64Process2, + }, +}; + +/// Import resource constants from `resource.rs`. This is automatically generated by the build +/// script. See the [module-level documentation](crate). +mod resource { + include!(concat!(env!("OUT_DIR"), "/resource.rs")); +} + +pub fn main() -> anyhow::Result<()> { + let architecture = get_native_arch()?; + let exe_data = find_binary_data(architecture)?; + let path = write_file_to_temp(exe_data)?; + + let status = run_with_forwarded_args(&path).context("Failed to run unpacked installer")?; + + // We cannot rely on drop here since we need to `exit`, so remove explicitly + if let Err(error) = std::fs::remove_file(path) { + eprintln!("Failed to remove unpacked installer: {error}"); + } + + std::process::exit(status.code().unwrap()); +} + +/// Run path and pass all arguments from `argv[1..]` to it +fn run_with_forwarded_args(path: impl AsRef<OsStr>) -> io::Result<ExitStatus> { + let mut command = Command::new(path); + + let args = std::env::args().skip(1); + command.args(args).status() +} + +/// Write file to a temporary file and return its path +fn write_file_to_temp(data: &[u8]) -> anyhow::Result<TempPath> { + let mut file = tempfile::NamedTempFile::new().context("Failed to create tempfile")?; + file.write_all(data) + .context("Failed to extract temporary installer")?; + Ok(file.into_temp_path()) +} + +/// Return a slice of data for the given resource +fn find_binary_data(architecture: Architecture) -> anyhow::Result<&'static [u8]> { + let resource_id = match architecture { + Architecture::X64 => resource::IDB_X64EXE, + Architecture::Arm64 => resource::IDB_ARM64EXE, + }; + + let Some(resource_info) = + // SAFETY: Looks unsafe but is actually safe. The cast is equivalent to `MAKEINTRESOURCE`, + // which is not available in windows-sys, as it is a macro. + // `resource_id` is guaranteed by the build script to refer to an actual resource. + // See https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-findresourcew + NonZero::new(unsafe { FindResourceW(0, resource_id as _, w!("BINARY")) }) + else { + bail!("Failed to find resource: {}", io::Error::last_os_error()); + }; + + // SAFETY: We have a valid resource info handle + // NOTE: Resources loaded with LoadResource should not be freed. + // See https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadresource + let Some(resource) = NonNull::new(unsafe { LoadResource(0, resource_info.get()) }) else { + bail!("Failed to load resource: {}", io::Error::last_os_error()); + }; + + // SAFETY: We have a valid resource info handle + let Some(resource_size) = NonZero::new(unsafe { SizeofResource(0, resource_info.get()) }) + else { + bail!( + "Failed to get resource size: {}", + io::Error::last_os_error() + ); + }; + + // SAFETY: We have a valid resource info handle + // NOTE: We do not need to unload this handle, because it doesn't actually lock anything. + // See https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-lockresource + let Some(resource_data) = NonNull::new(unsafe { LockResource(resource.as_ptr()) }) else { + bail!( + "Failed to get resource data: {}", + io::Error::last_os_error() + ); + }; + + debug_assert!(resource_data.is_aligned()); + + // SAFETY: The pointer is non-null, valid and constant for the remainder of the process lifetime + let resource_slice = unsafe { + std::slice::from_raw_parts( + resource_data.as_ptr() as *const u8, + usize::try_from(resource_size.get()).unwrap(), + ) + }; + + Ok(resource_slice) +} + +#[derive(Debug)] +enum Architecture { + X64, + Arm64, +} + +/// Return native architecture (ignoring WOW64) +fn get_native_arch() -> anyhow::Result<Architecture> { + let mut running_arch: c_ushort = 0; + let mut native_arch: c_ushort = 0; + + // SAFETY: Trivially safe, since we provide the required arguments. `hprocess == 0` is + // undocumented but refers to the current process. + let result = unsafe { IsWow64Process2(0, &mut running_arch, &mut native_arch) }; + if result == 0 { + bail!( + "Failed to get native architecture: {}", + io::Error::last_os_error() + ); + } + + match native_arch { + IMAGE_FILE_MACHINE_AMD64 => Ok(Architecture::X64), + IMAGE_FILE_MACHINE_ARM64 => Ok(Architecture::Arm64), + other => bail!("unsupported architecture: {other}"), + } +} diff --git a/windows-installer/windows-installer.manifest b/windows-installer/windows-installer.manifest new file mode 100644 index 0000000000..f640b7c615 --- /dev/null +++ b/windows-installer/windows-installer.manifest @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"> +<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> + <security> + <requestedPrivileges> + <requestedExecutionLevel + level="requireAdministrator" + uiAccess="false"/> + </requestedPrivileges> + </security> +</trustInfo> +<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <!--The ID below indicates app support for Windows 10 and Windows 11 --> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> + </application> +</compatibility> +</assembly> |
