Skip to content

Commit

Permalink
Fix windows-installer on non-Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
dlon committed Dec 9, 2024
1 parent a266318 commit 6615205
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 118 deletions.
4 changes: 2 additions & 2 deletions windows-installer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ windows-sys = { version = "0.52.0", features = ["Win32_System", "Win32_System_Li
tempfile = "3.10"
anyhow = "1.0"

[target.'cfg(all(target_os = "windows", target_arch = "x86_64"))'.build-dependencies]
[build-dependencies]
winres = "0.1"
anyhow = "1.0"
windows-sys = { version = "0.52.0" }
windows-sys = { version = "0.52.0", features = ["Win32_System", "Win32_System_LibraryLoader", "Win32_System_SystemServices"] }
mullvad-version = { path = "../mullvad-version" }

[package.metadata.winres]
Expand Down
242 changes: 126 additions & 116 deletions windows-installer/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,149 +3,159 @@
//! 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
#![windows_subsystem = "windows"]

use anyhow::{bail, Context};
use std::{
ffi::{c_ushort, OsStr},
io::{self, Write},
process::{Command, ExitStatus},
};
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,
},
};

mod resource {
include!(concat!(env!("OUT_DIR"), "/resource.rs"));
}
pub use implementation::*;

fn main() -> anyhow::Result<()> {
// TODO: set correct icon, etc.
#[cfg(not(all(target_os = "windows", target_arch = "x86_64")))]
pub mod implementation {
// nothing on non-Windows
pub fn main() {}
}

let resource_id = match get_native_arch()? {
Architecture::X64 => ResourceId::X86Bin,
Architecture::Arm64 => ResourceId::Arm64Bin,
Architecture::Unsupported(arch) => {
bail!("unsupported processor architecture {arch}");
}
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
pub mod implementation {
use anyhow::{bail, Context};
use std::{
ffi::{c_ushort, OsStr},
io::{self, Write},
process::{Command, ExitStatus},
};
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,
},
};

let exe_data = find_binary_data(resource_id)?;
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}");
mod resource {
include!(concat!(env!("OUT_DIR"), "/resource.rs"));
}

std::process::exit(status.code().unwrap());
}
pub fn main() -> anyhow::Result<()> {
// TODO: set correct icon, etc.

/// 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 resource_id = match get_native_arch()? {
Architecture::X64 => ResourceId::X86Bin,
Architecture::Arm64 => ResourceId::Arm64Bin,
Architecture::Unsupported(arch) => {
bail!("unsupported processor architecture {arch}");
}
};

let args = std::env::args().skip(1);
command.args(args).status()
}
let exe_data = find_binary_data(resource_id)?;
let path = write_file_to_temp(&exe_data)?;

/// 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())
}
let status = run_with_forwarded_args(&path).context("Failed to run unpacked installer")?;

#[repr(usize)]
enum ResourceId {
X86Bin = resource::IDB_X64EXE,
Arm64Bin = resource::IDB_ARM64EXE,
}
// 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}");
}

/// Return a slice of data for the given resource
fn find_binary_data(resource_id: ResourceId) -> anyhow::Result<&'static [u8]> {
// 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
let resource_info = unsafe { FindResourceW(0, resource_id as usize as _, w!("BINARY")) };
if resource_info == 0 {
bail!("Failed to find resource: {}", io::Error::last_os_error());
std::process::exit(status.code().unwrap());
}

// 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 resource = unsafe { LoadResource(0, resource_info) };
if resource.is_null() {
bail!("Failed to load resource: {}", io::Error::last_os_error());
/// 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()
}

// SAFETY: We have a valid resource info handle !TODO
let resource_size = unsafe { SizeofResource(0, resource_info) };
if resource_size == 0 {
bail!(
"Failed to get resource size: {}",
io::Error::last_os_error()
);
/// 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())
}

// SAFETY: We have a valid resource info handle !TODO
// 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 resource_data = unsafe { LockResource(resource as _) };
if resource_data.is_null() {
bail!(
"Failed to get resource data: {}",
io::Error::last_os_error()
);
#[repr(usize)]
enum ResourceId {
X86Bin = resource::IDB_X64EXE,
Arm64Bin = resource::IDB_ARM64EXE,
}

debug_assert!(resource_data.is_aligned());
/// Return a slice of data for the given resource
fn find_binary_data(resource_id: ResourceId) -> anyhow::Result<&'static [u8]> {
// 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
let resource_info = unsafe { FindResourceW(0, resource_id as usize as _, w!("BINARY")) };
if resource_info == 0 {
bail!("Failed to find resource: {}", io::Error::last_os_error());
}

// 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 *const u8,
usize::try_from(resource_size).unwrap(),
)
};
// 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 resource = unsafe { LoadResource(0, resource_info) };
if resource.is_null() {
bail!("Failed to load resource: {}", io::Error::last_os_error());
}

Ok(resource_slice)
}
// SAFETY: We have a valid resource info handle !TODO
let resource_size = unsafe { SizeofResource(0, resource_info) };
if resource_size == 0 {
bail!(
"Failed to get resource size: {}",
io::Error::last_os_error()
);
}

#[derive(Debug)]
enum Architecture {
X64,
Arm64,
Unsupported(u16),
}
// SAFETY: We have a valid resource info handle !TODO
// 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 resource_data = unsafe { LockResource(resource as _) };
if resource_data.is_null() {
bail!(
"Failed to get resource data: {}",
io::Error::last_os_error()
);
}

debug_assert!(resource_data.is_aligned());

/// Return native architecture (ignoring WOW64)
fn get_native_arch() -> io::Result<Architecture> {
let mut running_arch: c_ushort = 0;
let mut native_arch: c_ushort = 0;
// 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 *const u8,
usize::try_from(resource_size).unwrap(),
)
};

// 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 {
return Err(io::Error::last_os_error());
Ok(resource_slice)
}

match native_arch {
IMAGE_FILE_MACHINE_AMD64 => Ok(Architecture::X64),
IMAGE_FILE_MACHINE_ARM64 => Ok(Architecture::Arm64),
other => Ok(Architecture::Unsupported(other)),
#[derive(Debug)]
enum Architecture {
X64,
Arm64,
Unsupported(u16),
}

/// Return native architecture (ignoring WOW64)
fn get_native_arch() -> io::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 {
return Err(io::Error::last_os_error());
}

match native_arch {
IMAGE_FILE_MACHINE_AMD64 => Ok(Architecture::X64),
IMAGE_FILE_MACHINE_ARM64 => Ok(Architecture::Arm64),
other => Ok(Architecture::Unsupported(other)),
}
}
}

0 comments on commit 6615205

Please sign in to comment.