//! 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::{Context, bail}; use std::{ ffi::OsStr, io::{self, Write}, num::NonZero, process::{Command, ExitStatus}, ptr::NonNull, }; use tempfile::TempPath; use windows_sys::{ Win32::System::LibraryLoader::{FindResourceW, LoadResource, LockResource, SizeofResource}, w, }; /// 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) -> io::Result { 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 { 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 { let Some(arch) = talpid_platform_metadata::get_native_arch().context("Failed to get native architecture")? else { bail!("Unable to detect native architecture (most likely unsupported)"); }; match arch { talpid_platform_metadata::Architecture::X86 => Ok(Architecture::X64), talpid_platform_metadata::Architecture::Arm64 => Ok(Architecture::Arm64), } }