summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2024-12-10 14:34:42 +0100
committerDavid Lönnhager <david.l@mullvad.net>2024-12-10 14:34:42 +0100
commitc5089569e507c4485ef2916a40cb635d47abd1f3 (patch)
tree59181f75207d1c9fa8f92954226426fa7d238136
parent06b9a755512361ab16a9e8dcd6759108888a88e2 (diff)
parentbc3dc7b43385645e3f2e37eb3849bf36e928d53e (diff)
downloadmullvadvpn-c5089569e507c4485ef2916a40cb635d47abd1f3.tar.xz
mullvadvpn-c5089569e507c4485ef2916a40cb635d47abd1f3.zip
Merge branch 'win-universal-arch-installer'
-rw-r--r--Cargo.lock11
-rw-r--r--Cargo.toml40
-rwxr-xr-xbuild.sh33
-rwxr-xr-xci/buildserver-build.sh14
-rw-r--r--desktop/packages/mullvad-vpn/tasks/distribution.js554
-rw-r--r--desktop/scripts/pack-universal-win.sh66
-rw-r--r--windows-installer/Cargo.toml29
-rw-r--r--windows-installer/build.rs68
-rw-r--r--windows-installer/src/main.rs13
-rw-r--r--windows-installer/src/windows.rs145
-rw-r--r--windows-installer/windows-installer.manifest18
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`
diff --git a/build.sh b/build.sh
index 8509cdb1c7..7d2bce9d51 100755
--- a/build.sh
+++ b/build.sh
@@ -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>