diff options
| author | David Lönnhager <david.l@mullvad.net> | 2024-12-10 14:34:42 +0100 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2024-12-10 14:34:42 +0100 |
| commit | c5089569e507c4485ef2916a40cb635d47abd1f3 (patch) | |
| tree | 59181f75207d1c9fa8f92954226426fa7d238136 | |
| parent | 06b9a755512361ab16a9e8dcd6759108888a88e2 (diff) | |
| parent | bc3dc7b43385645e3f2e37eb3849bf36e928d53e (diff) | |
| download | mullvadvpn-c5089569e507c4485ef2916a40cb635d47abd1f3.tar.xz mullvadvpn-c5089569e507c4485ef2916a40cb635d47abd1f3.zip | |
Merge branch 'win-universal-arch-installer'
| -rw-r--r-- | Cargo.lock | 11 | ||||
| -rw-r--r-- | Cargo.toml | 40 | ||||
| -rwxr-xr-x | build.sh | 33 | ||||
| -rwxr-xr-x | ci/buildserver-build.sh | 14 | ||||
| -rw-r--r-- | desktop/packages/mullvad-vpn/tasks/distribution.js | 554 | ||||
| -rw-r--r-- | desktop/scripts/pack-universal-win.sh | 66 | ||||
| -rw-r--r-- | windows-installer/Cargo.toml | 29 | ||||
| -rw-r--r-- | windows-installer/build.rs | 68 | ||||
| -rw-r--r-- | windows-installer/src/main.rs | 13 | ||||
| -rw-r--r-- | windows-installer/src/windows.rs | 145 | ||||
| -rw-r--r-- | windows-installer/windows-installer.manifest | 18 |
11 files changed, 700 insertions, 291 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` @@ -28,8 +28,8 @@ OPTIMIZE="false" SIGN="false" # If the produced app and pkg should be notarized by apple (macOS only) NOTARIZE="false" -# If a macOS build should create an installer artifact working on both -# Intel and Apple Silicon Macs +# If a macOS or Windows build should create an installer artifact working on both +# x86 and arm64 UNIVERSAL="false" while [[ "$#" -gt 0 ]]; do @@ -38,8 +38,8 @@ while [[ "$#" -gt 0 ]]; do --sign) SIGN="true";; --notarize) NOTARIZE="true";; --universal) - if [[ "$(uname -s)" != "Darwin" ]]; then - log_error "--universal only works on macOS" + if [[ "$(uname -s)" != "Darwin" && "$(uname -s)" != "MINGW"* ]]; then + log_error "--universal only works on macOS and Windows" exit 1 fi UNIVERSAL="true" @@ -79,11 +79,11 @@ if [[ "$UNIVERSAL" == "true" ]]; then log_error "'TARGETS' and '--universal' cannot be specified simultaneously." exit 1 else - log_info "Building universal macOS distribution" + log_info "Building universal distribution" fi - # Universal macOS builds package targets for both aarch64-apple-darwin and x86_64-apple-darwin. - # We leave the target corresponding to the host machine empty to avoid rebuilding multiple times. + # Universal builds package targets for both aarch64 and x86_64. We leave the target + # corresponding to the host machine empty to avoid rebuilding multiple times. # When the --target flag is provided to cargo it always puts the build in the target/$ENV_TARGET # folder even when it matches you local machine, as opposed to just the target folder. # This causes the cached build not to get used when later running e.g. @@ -91,6 +91,8 @@ if [[ "$UNIVERSAL" == "true" ]]; then case $HOST in x86_64-apple-darwin) TARGETS=("" aarch64-apple-darwin);; aarch64-apple-darwin) TARGETS=("" x86_64-apple-darwin);; + x86_64-pc-windows-msvc) TARGETS=("" aarch64-pc-windows-msvc);; + aarch64-pc-windows-msvc) TARGETS=("" x86_64-pc-windows-msvc);; esac NPM_PACK_ARGS+=(--universal) @@ -311,7 +313,7 @@ function build { if [[ "$(uname -s)" == "MINGW"* ]]; then for t in "${TARGETS[@]:-"$HOST"}"; do - case $t in + case "${t:-"$HOST"}" in x86_64-pc-windows-msvc) CPP_BUILD_TARGET=x64;; aarch64-pc-windows-msvc) CPP_BUILD_TARGET=ARM64;; *) @@ -384,6 +386,21 @@ if [[ "$SIGN" == "true" && "$(uname -s)" == "MINGW"* ]]; then done fi +# pack universal installer on Windows +if [[ "$UNIVERSAL" == "true" && "$(uname -s)" == "MINGW"* ]]; then + WIN_PACK_ARGS=() + if [[ "$OPTIMIZE" == "true" ]]; then + WIN_PACK_ARGS+=(--optimize) + fi + ./desktop/scripts/pack-universal-win.sh \ + --x64-installer "$SCRIPT_DIR/dist/"*"$PRODUCT_VERSION"_x64.exe \ + --arm64-installer "$SCRIPT_DIR/dist/"*"$PRODUCT_VERSION"_arm64.exe \ + "${WIN_PACK_ARGS[@]}" + if [[ "$SIGN" == "true" ]]; then + sign_win "dist/MullvadVPN-${PRODUCT_VERSION}.exe" + fi +fi + # notarize installer on macOS if [[ "$NOTARIZE" == "true" && "$(uname -s)" == "Darwin" ]]; then log_info "Notarizing pkg" diff --git a/ci/buildserver-build.sh b/ci/buildserver-build.sh index 9e63db6a3b..b68bd79e77 100755 --- a/ci/buildserver-build.sh +++ b/ci/buildserver-build.sh @@ -196,6 +196,15 @@ function build_ref { if [[ "$(uname -s)" == "Darwin" ]]; then build_args+=(--universal --notarize) fi + if [[ "$(uname -s)" == "MINGW"* ]]; then + # Check if the windows-installer crate is present, and if so, build a universal installer. + # The check is needed for compatibility with older commits that don't have the crate/flag. + # It was added in December 2024. The condition can be removed when supporting commits + # older than that is no longer necessary. + if [[ -d "$BUILD_DIR/windows-installer" ]]; then + build_args+=(--universal) + fi + fi artifact_dir=$artifact_dir build "${build_args[@]}" || return 1 if [[ "$(uname -s)" == "Linux" ]]; then @@ -205,9 +214,6 @@ function build_ref { case "$(uname -s)" in MINGW*|MSYS_NT*) - echo "Building ARM64 installers" - target=aarch64-pc-windows-msvc artifact_dir=$artifact_dir build "${build_args[@]}" || return 1 - echo "Packaging all PDB files..." find ./windows/ \ ./target/release/mullvad-daemon.pdb \ @@ -226,7 +232,7 @@ function build_ref { # Pipes all matching names and their new name to mv pushd "$artifact_dir" for original_file in MullvadVPN-*-dev-*{.deb,.rpm,.exe,.pkg}; do - new_file=$(echo "$original_file" | perl -pe "s/^(MullvadVPN-.*?)(_arm64|_aarch64|_amd64|_x86_64)?(\.deb|\.rpm|\.exe|\.pkg)$/\1$version_suffix\2\3/p") + new_file=$(echo "$original_file" | perl -pe "s/^(MullvadVPN-.*?)(_x64|_arm64|_aarch64|_amd64|_x86_64)?(\.deb|\.rpm|\.exe|\.pkg)$/\1$version_suffix\2\3/p") mv "$original_file" "$new_file" done popd diff --git a/desktop/packages/mullvad-vpn/tasks/distribution.js b/desktop/packages/mullvad-vpn/tasks/distribution.js index fb6400180c..c8282434bd 100644 --- a/desktop/packages/mullvad-vpn/tasks/distribution.js +++ b/desktop/packages/mullvad-vpn/tasks/distribution.js @@ -20,265 +20,277 @@ function getOptionValue(option) { } } -const config = { - appId: 'net.mullvad.vpn', - copyright: 'Mullvad VPN AB', - productName: 'Mullvad VPN', - publish: null, - asar: true, - compression: noCompression ? 'store' : 'normal', - extraResources: [ - { from: distAssets('ca.crt'), to: '.' }, - { from: buildAssets('relays.json'), to: '.' }, - { from: root('CHANGELOG.md'), to: '.' }, - ], +function newConfig() { + return { + appId: 'net.mullvad.vpn', + copyright: 'Mullvad VPN AB', + productName: 'Mullvad VPN', + publish: null, + asar: true, + compression: noCompression ? 'store' : 'normal', + extraResources: [ + { from: distAssets('ca.crt'), to: '.' }, + { from: buildAssets('relays.json'), to: '.' }, + { from: root('CHANGELOG.md'), to: '.' }, + ], - directories: { - buildResources: root('dist-assets'), - output: root('dist'), - }, + directories: { + buildResources: root('dist-assets'), + output: root('dist'), + }, - extraMetadata: { - name: 'mullvad-vpn', - // We have to stick to semver on Windows for now due to: - // https://github.com/electron-userland/electron-builder/issues/7173 - version: productVersion(process.platform === 'win32' ? ['semver'] : []), - }, + extraMetadata: { + name: 'mullvad-vpn', + // We have to stick to semver on Windows for now due to: + // https://github.com/electron-userland/electron-builder/issues/7173 + version: productVersion(process.platform === 'win32' ? ['semver'] : []), + }, - files: [ - 'package.json', - 'changes.txt', - 'init.js', - 'build/', - '!build/src/renderer', - 'build/src/renderer/index.html', - 'build/src/renderer/bundle.js', - 'build/src/renderer/preloadBundle.js', - '!**/*.tsbuildinfo', - '!test/', - '!playwright.config.ts', - 'node_modules/', - '!node_modules/grpc-tools', - '!node_modules/@types', - '!node_modules/nseventforwarder/debug', - ], + files: [ + 'package.json', + 'changes.txt', + 'init.js', + 'build/', + '!build/src/renderer', + 'build/src/renderer/index.html', + 'build/src/renderer/bundle.js', + 'build/src/renderer/preloadBundle.js', + '!**/*.tsbuildinfo', + '!test/', + '!playwright.config.ts', + 'node_modules/', + '!node_modules/grpc-tools', + '!node_modules/@types', + '!node_modules/nseventforwarder/debug', + ], - // Make sure that all files declared in "extraResources" exists and abort if they don't. - afterPack: (context) => { - if (context.arch !== Arch.universal) { - const resources = context.packager.platformSpecificBuildOptions.extraResources; - for (const resource of resources) { - const filePath = resource.from.replace(/\$\{env\.(.*)\}/, function (match, captureGroup) { - return process.env[captureGroup]; - }); + // Make sure that all files declared in "extraResources" exists and abort if they don't. + afterPack: (context) => { + if (context.arch !== Arch.universal) { + const resources = context.packager.platformSpecificBuildOptions.extraResources; + for (const resource of resources) { + const filePath = resource.from.replaceAll( + /\$\{env\.(.*?)\}/g, + function (match, captureGroup) { + return process.env[captureGroup]; + }, + ); - if (!fs.existsSync(filePath)) { - throw new Error(`Can't find file: ${filePath}`); + if (!fs.existsSync(filePath)) { + throw new Error(`Can't find file: ${filePath}`); + } } } - } - }, - - mac: { - target: { - target: 'pkg', - arch: getMacArch(), - }, - singleArchFiles: 'node_modules/nseventforwarder/dist/**', - artifactName: 'MullvadVPN-${version}.${ext}', - category: 'public.app-category.tools', - icon: distAssets('icon-macos.icns'), - notarize: shouldNotarize, - extendInfo: { - LSUIElement: true, - NSUserNotificationAlertStyle: 'banner', }, - extraResources: [ - { from: distAssets(path.join('${env.BINARIES_PATH}', 'mullvad')), to: '.' }, - { from: distAssets(path.join('${env.BINARIES_PATH}', 'mullvad-problem-report')), to: '.' }, - { from: distAssets(path.join('${env.BINARIES_PATH}', 'mullvad-daemon')), to: '.' }, - { from: distAssets(path.join('${env.BINARIES_PATH}', 'mullvad-setup')), to: '.' }, - { - from: distAssets(path.join('${env.BINARIES_PATH}', 'libtalpid_openvpn_plugin.dylib')), - to: '.', + + mac: { + target: { + target: 'pkg', + arch: getMacArch(), }, - { from: distAssets(path.join('binaries', '${env.TARGET_TRIPLE}', 'openvpn')), to: '.' }, - { from: distAssets(path.join('binaries', '${env.TARGET_TRIPLE}', 'apisocks5')), to: '.' }, - { from: distAssets('uninstall_macos.sh'), to: './uninstall.sh' }, - { from: buildAssets('shell-completions/_mullvad'), to: '.' }, - { from: buildAssets('shell-completions/mullvad.fish'), to: '.' }, - { from: distAssets('maybenot_machines'), to: '.' }, - ], - }, + singleArchFiles: 'node_modules/nseventforwarder/dist/**', + artifactName: 'MullvadVPN-${version}.${ext}', + category: 'public.app-category.tools', + icon: distAssets('icon-macos.icns'), + notarize: shouldNotarize, + extendInfo: { + LSUIElement: true, + NSUserNotificationAlertStyle: 'banner', + }, + extraResources: [ + { from: distAssets(path.join('${env.BINARIES_PATH}', 'mullvad')), to: '.' }, + { from: distAssets(path.join('${env.BINARIES_PATH}', 'mullvad-problem-report')), to: '.' }, + { from: distAssets(path.join('${env.BINARIES_PATH}', 'mullvad-daemon')), to: '.' }, + { from: distAssets(path.join('${env.BINARIES_PATH}', 'mullvad-setup')), to: '.' }, + { + from: distAssets(path.join('${env.BINARIES_PATH}', 'libtalpid_openvpn_plugin.dylib')), + to: '.', + }, + { from: distAssets(path.join('binaries', '${env.TARGET_TRIPLE}', 'openvpn')), to: '.' }, + { from: distAssets(path.join('binaries', '${env.TARGET_TRIPLE}', 'apisocks5')), to: '.' }, + { from: distAssets('uninstall_macos.sh'), to: './uninstall.sh' }, + { from: buildAssets('shell-completions/_mullvad'), to: '.' }, + { from: buildAssets('shell-completions/mullvad.fish'), to: '.' }, + { from: distAssets('maybenot_machines'), to: '.' }, + ], + }, - pkg: { - allowAnywhere: false, - allowCurrentUserHome: false, - isRelocatable: false, - isVersionChecked: false, - }, + pkg: { + allowAnywhere: false, + allowCurrentUserHome: false, + isRelocatable: false, + isVersionChecked: false, + }, - nsis: { - guid: '2A356FD4-03B7-4F45-99B4-737BE580DC82', - oneClick: false, - perMachine: true, - allowElevation: true, - allowToChangeInstallationDirectory: false, - include: distAssets('windows/installer.nsh'), - installerSidebar: distAssets('windows/installersidebar.bmp'), - }, + nsis: { + guid: '2A356FD4-03B7-4F45-99B4-737BE580DC82', + oneClick: false, + perMachine: true, + allowElevation: true, + allowToChangeInstallationDirectory: false, + include: distAssets('windows/installer.nsh'), + installerSidebar: distAssets('windows/installersidebar.bmp'), + }, - win: { - target: [ - { - target: 'nsis', - arch: getWindowsTargetArch(), - }, - ], - artifactName: getWindowsArtifactName(), - publisherName: 'Mullvad VPN AB', - extraResources: [ - { from: distAssets(path.join(getWindowsDistSubdir(), 'mullvad.exe')), to: '.' }, - { - from: distAssets(path.join(getWindowsDistSubdir(), 'mullvad-problem-report.exe')), - to: '.', - }, - { from: distAssets(path.join(getWindowsDistSubdir(), 'mullvad-daemon.exe')), to: '.' }, - { from: distAssets(path.join(getWindowsDistSubdir(), 'talpid_openvpn_plugin.dll')), to: '.' }, - { - from: root( - path.join( - 'windows', - 'winfw', - 'bin', - getWindowsTargetArch() + '-${env.CPP_BUILD_MODE}', - 'winfw.dll', + win: { + target: [], + signAndEditExecutable: false, + artifactName: 'MullvadVPN-${version}_${arch}.${ext}', + publisherName: 'Mullvad VPN AB', + extraResources: [ + { from: distAssets(path.join('${env.DIST_SUBDIR}', 'mullvad.exe')), to: '.' }, + { + from: distAssets(path.join('${env.DIST_SUBDIR}', 'mullvad-problem-report.exe')), + to: '.', + }, + { from: distAssets(path.join('${env.DIST_SUBDIR}', 'mullvad-daemon.exe')), to: '.' }, + { from: distAssets(path.join('${env.DIST_SUBDIR}', 'talpid_openvpn_plugin.dll')), to: '.' }, + { + from: root( + path.join( + 'windows', + 'winfw', + 'bin', + '${env.TARGET_ARCHITECTURE}-${env.CPP_BUILD_MODE}', + 'winfw.dll', + ), ), - ), - to: '.', - }, - // TODO: OpenVPN does not have an ARM64 build yet. - { from: distAssets('binaries/x86_64-pc-windows-msvc/openvpn.exe'), to: '.' }, - { - from: distAssets(path.join('binaries', getWindowsTargetSubdir(), 'apisocks5.exe')), - to: '.', - }, - { - from: distAssets(path.join('binaries', getWindowsTargetSubdir(), 'wintun/wintun.dll')), - to: '.', - }, - { - from: distAssets( - path.join('binaries', getWindowsTargetSubdir(), 'split-tunnel/mullvad-split-tunnel.sys'), - ), - to: '.', - }, - { - from: distAssets( - path.join('binaries', getWindowsTargetSubdir(), 'wireguard-nt/mullvad-wireguard.dll'), - ), - to: '.', - }, - { from: distAssets('maybenot_machines'), to: '.' }, - ], - }, + to: '.', + }, + // TODO: OpenVPN does not have an ARM64 build yet. + { from: distAssets('binaries/x86_64-pc-windows-msvc/openvpn.exe'), to: '.' }, + { + from: distAssets(path.join('binaries', '${env.TARGET_SUBDIR}', 'apisocks5.exe')), + to: '.', + }, + { + from: distAssets(path.join('binaries', '${env.TARGET_SUBDIR}', 'wintun/wintun.dll')), + to: '.', + }, + { + from: distAssets( + path.join('binaries', '${env.TARGET_SUBDIR}', 'split-tunnel/mullvad-split-tunnel.sys'), + ), + to: '.', + }, + { + from: distAssets( + path.join('binaries', '${env.TARGET_SUBDIR}', 'wireguard-nt/mullvad-wireguard.dll'), + ), + to: '.', + }, + { from: distAssets('maybenot_machines'), to: '.' }, + ], + }, - linux: { - target: [ - { - target: 'deb', - arch: getLinuxTargetArch(), - }, - { - target: 'rpm', - arch: getLinuxTargetArch(), - }, - ], - artifactName: 'MullvadVPN-${version}_${arch}.${ext}', - category: 'Network', - icon: distAssets('icon.icns'), - extraFiles: [{ from: distAssets('linux/mullvad-gui-launcher.sh'), to: '.' }], - extraResources: [ - { from: distAssets(path.join(getLinuxTargetSubdir(), 'mullvad-problem-report')), to: '.' }, - { from: distAssets(path.join(getLinuxTargetSubdir(), 'mullvad-setup')), to: '.' }, - { - from: distAssets(path.join(getLinuxTargetSubdir(), 'libtalpid_openvpn_plugin.so')), - to: '.', - }, - { from: distAssets(path.join('linux', 'apparmor_mullvad')), to: '.' }, - { from: distAssets(path.join('binaries', '${env.TARGET_TRIPLE}', 'openvpn')), to: '.' }, - { from: distAssets(path.join('binaries', '${env.TARGET_TRIPLE}', 'apisocks5')), to: '.' }, - { from: distAssets('maybenot_machines'), to: '.' }, - ], - }, + linux: { + target: [ + { + target: 'deb', + arch: getLinuxTargetArch(), + }, + { + target: 'rpm', + arch: getLinuxTargetArch(), + }, + ], + artifactName: 'MullvadVPN-${version}_${arch}.${ext}', + category: 'Network', + icon: distAssets('icon.icns'), + extraFiles: [{ from: distAssets('linux/mullvad-gui-launcher.sh'), to: '.' }], + extraResources: [ + { from: distAssets(path.join(getLinuxTargetSubdir(), 'mullvad-problem-report')), to: '.' }, + { from: distAssets(path.join(getLinuxTargetSubdir(), 'mullvad-setup')), to: '.' }, + { + from: distAssets(path.join(getLinuxTargetSubdir(), 'libtalpid_openvpn_plugin.so')), + to: '.', + }, + { from: distAssets(path.join('linux', 'apparmor_mullvad')), to: '.' }, + { from: distAssets(path.join('binaries', '${env.TARGET_TRIPLE}', 'openvpn')), to: '.' }, + { from: distAssets(path.join('binaries', '${env.TARGET_TRIPLE}', 'apisocks5')), to: '.' }, + { from: distAssets('maybenot_machines'), to: '.' }, + ], + }, - deb: { - fpm: [ - '--no-depends', - '--version', - getLinuxVersion(), - '--before-install', - distAssets('linux/before-install.sh'), - '--before-remove', - distAssets('linux/before-remove.sh'), - distAssets('linux/mullvad-daemon.service') + - '=/usr/lib/systemd/system/mullvad-daemon.service', - distAssets('linux/mullvad-early-boot-blocking.service') + - '=/usr/lib/systemd/system/mullvad-early-boot-blocking.service', - distAssets(path.join(getLinuxTargetSubdir(), 'mullvad')) + '=/usr/bin/', - distAssets(path.join(getLinuxTargetSubdir(), 'mullvad-daemon')) + '=/usr/bin/', - distAssets(path.join(getLinuxTargetSubdir(), 'mullvad-exclude')) + '=/usr/bin/', - distAssets('linux/problem-report-link') + '=/usr/bin/mullvad-problem-report', - buildAssets('shell-completions/mullvad.bash') + - '=/usr/share/bash-completion/completions/mullvad', - buildAssets('shell-completions/_mullvad') + '=/usr/local/share/zsh/site-functions/_mullvad', - buildAssets('shell-completions/mullvad.fish') + - '=/usr/share/fish/vendor_completions.d/mullvad.fish', - ], - afterInstall: distAssets('linux/after-install.sh'), - afterRemove: distAssets('linux/after-remove.sh'), - }, + deb: { + fpm: [ + '--no-depends', + '--version', + getLinuxVersion(), + '--before-install', + distAssets('linux/before-install.sh'), + '--before-remove', + distAssets('linux/before-remove.sh'), + distAssets('linux/mullvad-daemon.service') + + '=/usr/lib/systemd/system/mullvad-daemon.service', + distAssets('linux/mullvad-early-boot-blocking.service') + + '=/usr/lib/systemd/system/mullvad-early-boot-blocking.service', + distAssets(path.join(getLinuxTargetSubdir(), 'mullvad')) + '=/usr/bin/', + distAssets(path.join(getLinuxTargetSubdir(), 'mullvad-daemon')) + '=/usr/bin/', + distAssets(path.join(getLinuxTargetSubdir(), 'mullvad-exclude')) + '=/usr/bin/', + distAssets('linux/problem-report-link') + '=/usr/bin/mullvad-problem-report', + buildAssets('shell-completions/mullvad.bash') + + '=/usr/share/bash-completion/completions/mullvad', + buildAssets('shell-completions/_mullvad') + '=/usr/local/share/zsh/site-functions/_mullvad', + buildAssets('shell-completions/mullvad.fish') + + '=/usr/share/fish/vendor_completions.d/mullvad.fish', + ], + afterInstall: distAssets('linux/after-install.sh'), + afterRemove: distAssets('linux/after-remove.sh'), + }, - rpm: { - fpm: [ - '--version', - getLinuxVersion(), - // Prevents RPM from packaging build-id metadata, some of which is the - // same across all electron-builder applications, which causes package - // conflicts - '--rpm-rpmbuild-define=_build_id_links none', - '--directories=/opt/Mullvad VPN/', - '--before-install', - distAssets('linux/before-install.sh'), - '--before-remove', - distAssets('linux/before-remove.sh'), - '--rpm-posttrans', - distAssets('linux/post-transaction.sh'), - distAssets('linux/mullvad-daemon.service') + - '=/usr/lib/systemd/system/mullvad-daemon.service', - distAssets('linux/mullvad-early-boot-blocking.service') + - '=/usr/lib/systemd/system/mullvad-early-boot-blocking.service', - distAssets(path.join(getLinuxTargetSubdir(), 'mullvad')) + '=/usr/bin/', - distAssets(path.join(getLinuxTargetSubdir(), 'mullvad-daemon')) + '=/usr/bin/', - distAssets(path.join(getLinuxTargetSubdir(), 'mullvad-exclude')) + '=/usr/bin/', - distAssets('linux/problem-report-link') + '=/usr/bin/mullvad-problem-report', - buildAssets('shell-completions/mullvad.bash') + - '=/usr/share/bash-completion/completions/mullvad', - buildAssets('shell-completions/_mullvad') + '=/usr/share/zsh/site-functions/_mullvad', - buildAssets('shell-completions/mullvad.fish') + - '=/usr/share/fish/vendor_completions.d/mullvad.fish', - ], - afterInstall: distAssets('linux/after-install.sh'), - afterRemove: distAssets('linux/after-remove.sh'), - depends: ['libXScrnSaver', 'libnotify', 'dbus-libs'], - }, -}; + rpm: { + fpm: [ + '--version', + getLinuxVersion(), + // Prevents RPM from packaging build-id metadata, some of which is the + // same across all electron-builder applications, which causes package + // conflicts + '--rpm-rpmbuild-define=_build_id_links none', + '--directories=/opt/Mullvad VPN/', + '--before-install', + distAssets('linux/before-install.sh'), + '--before-remove', + distAssets('linux/before-remove.sh'), + '--rpm-posttrans', + distAssets('linux/post-transaction.sh'), + distAssets('linux/mullvad-daemon.service') + + '=/usr/lib/systemd/system/mullvad-daemon.service', + distAssets('linux/mullvad-early-boot-blocking.service') + + '=/usr/lib/systemd/system/mullvad-early-boot-blocking.service', + distAssets(path.join(getLinuxTargetSubdir(), 'mullvad')) + '=/usr/bin/', + distAssets(path.join(getLinuxTargetSubdir(), 'mullvad-daemon')) + '=/usr/bin/', + distAssets(path.join(getLinuxTargetSubdir(), 'mullvad-exclude')) + '=/usr/bin/', + distAssets('linux/problem-report-link') + '=/usr/bin/mullvad-problem-report', + buildAssets('shell-completions/mullvad.bash') + + '=/usr/share/bash-completion/completions/mullvad', + buildAssets('shell-completions/_mullvad') + '=/usr/share/zsh/site-functions/_mullvad', + buildAssets('shell-completions/mullvad.fish') + + '=/usr/share/fish/vendor_completions.d/mullvad.fish', + ], + afterInstall: distAssets('linux/after-install.sh'), + afterRemove: distAssets('linux/after-remove.sh'), + depends: ['libXScrnSaver', 'libnotify', 'dbus-libs'], + }, + }; +} -function packWin() { - return builder.build({ - targets: builder.Platform.WINDOWS.createTarget(), - config: { +async function packWin() { + const DEFAULT_ARCH = targets === 'aarch64-pc-windows-msvc' ? 'arm64' : 'x64'; + + function prepareWinConfig(arch) { + const config = newConfig(); + return { ...config, + win: { + ...config.win, + target: [ + { + target: 'nsis', + arch: arch, + }, + ], + }, asarUnpack: ['build/assets/images/menubar-icons/win32/lock-*.ico'], beforeBuild: (options) => { process.env.CPP_BUILD_MODE = release ? 'Release' : 'Debug'; @@ -288,10 +300,14 @@ function packWin() { case 'x64': process.env.TARGET_TRIPLE = 'x86_64-pc-windows-msvc'; process.env.SETUP_SUBDIR = '.'; + process.env.TARGET_SUBDIR = 'x86_64-pc-windows-msvc'; + process.env.DIST_SUBDIR = ''; break; case 'arm64': process.env.TARGET_TRIPLE = 'aarch64-pc-windows-msvc'; process.env.SETUP_SUBDIR = 'aarch64-pc-windows-msvc'; + process.env.TARGET_SUBDIR = 'aarch64-pc-windows-msvc'; + process.env.DIST_SUBDIR = 'aarch64-pc-windows-msvc'; break; default: throw new Error('Invalid or unknown target (only one may be specified)'); @@ -317,12 +333,27 @@ function packWin() { fs.renameSync(artifactPath, targetArtifactPath); } }, - }, + }; + } + + if (universal) { + // For universal builds, we simply build for all targets. It is up to build.sh to pack the + // installers in the same binary. + await builder.build({ + targets: builder.Platform.WINDOWS.createTarget(), + config: prepareWinConfig(DEFAULT_ARCH === 'x64' ? 'arm64' : 'x64'), + }); + } + + return builder.build({ + targets: builder.Platform.WINDOWS.createTarget(), + config: prepareWinConfig(DEFAULT_ARCH), }); } function packMac() { const appOutDirs = []; + const config = newConfig(); return builder.build({ targets: builder.Platform.MAC.createTarget(), @@ -383,6 +414,8 @@ function packMac() { } function packLinux() { + const config = newConfig(); + if (noCompression) { config.rpm.fpm.unshift('--rpm-compression', 'none'); } @@ -438,43 +471,6 @@ function root(relativePath) { return path.join(path.resolve(__dirname, '../../../../'), relativePath); } -function getWindowsDistSubdir() { - if (targets === 'aarch64-pc-windows-msvc') { - return targets; - } else { - return ''; - } -} - -function getWindowsTargetArch() { - if (targets && process.platform === 'win32') { - if (targets === 'aarch64-pc-windows-msvc') { - return 'arm64'; - } - throw new Error('Invalid or unknown target (only one may be specified)'); - } - // Use host architecture (we assume this is x64 since building on Arm64 isn't supported). - return 'x64'; -} - -function getWindowsArtifactName() { - if (targets === 'aarch64-pc-windows-msvc') { - return 'MullvadVPN-${version}_${arch}.${ext}'; - } - return 'MullvadVPN-${version}.${ext}'; -} - -function getWindowsTargetSubdir() { - if (targets && process.platform === 'win32') { - if (targets === 'aarch64-pc-windows-msvc') { - return targets; - } - throw new Error('Invalid or unknown target (only one may be specified)'); - } - // Use host architecture (we assume this is x64 since building on Arm64 isn't supported). - return 'x86_64-pc-windows-msvc'; -} - function getLinuxTargetArch() { if (targets && process.platform === 'linux') { if (targets === 'aarch64-unknown-linux-gnu') { diff --git a/desktop/scripts/pack-universal-win.sh b/desktop/scripts/pack-universal-win.sh new file mode 100644 index 0000000000..b3144f27dd --- /dev/null +++ b/desktop/scripts/pack-universal-win.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# shellcheck shell=bash +# Build universal installer for both ARM and x64. + +set -eu + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$SCRIPT_DIR/../.." + +CARGO_TARGET_DIR=${CARGO_TARGET_DIR:-"target"} + +# If enabled, build in release mode with optimizations enabled +OPTIMIZE="false" + +source scripts/utils/log + +echo "Computing build version..." +PRODUCT_VERSION=$(cargo run -q --bin mullvad-version) +log_header "Building universal Windows installer for Mullvad VPN $PRODUCT_VERSION" + +while [[ "$#" -gt 0 ]]; do + case $1 in + --x64-installer) + export WIN_X64_INSTALLER="$2" + shift 2 + ;; + --arm64-installer) + export WIN_ARM64_INSTALLER="$2" + shift 2 + ;; + --optimize) + OPTIMIZE="true" + shift + ;; + *) + log_error "Unknown argument: $1" + exit 1 + ;; + esac +done + +CARGO_ARGS=() + +if [[ "$OPTIMIZE" == "true" ]]; then + CARGO_ARGS+=(--release) + RUST_BUILD_MODE="release" +else + RUST_BUILD_MODE="debug" +fi + +if [[ "$OPTIMIZE" == "true" && "$PRODUCT_VERSION" != *"-dev-"* ]]; then + CARGO_ARGS+=(--locked) +fi + +if [[ -z ${WIN_X64_INSTALLER-} ]] || [[ -z ${WIN_ARM64_INSTALLER-} ]]; then + log_error "Must provide --x64-installer and --arm64-installer" + exit 1 +fi + +cargo build "${CARGO_ARGS[@]}" -p windows-installer --target x86_64-pc-windows-msvc + +dest="dist/MullvadVPN-${PRODUCT_VERSION}.exe" + +cp "$CARGO_TARGET_DIR/x86_64-pc-windows-msvc/${RUST_BUILD_MODE}/windows-installer.exe" "$dest" + +log_success "Built universal installer: $dest" 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..64e70c282d --- /dev/null +++ b/windows-installer/src/main.rs @@ -0,0 +1,13 @@ +#![windows_subsystem = "windows"] +#![warn(clippy::undocumented_unsafe_blocks)] + +#[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> |
