summaryrefslogtreecommitdiffhomepage
path: root/windows-installer/src/windows.rs
blob: ab7c8738b41a1d592b1671bf01ec265950e14597 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
//! 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<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 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),
    }
}