mullvadvpn-app/build.sh
2025-01-30 16:48:39 +01:00

440 lines
15 KiB
Bash
Executable File

#!/usr/bin/env bash
# This script is used to build, and optionally sign the app.
# See `README.md` for further instructions.
set -eu
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$SCRIPT_DIR"
source scripts/utils/host
source scripts/utils/log
################################################################################
# Analyze environment and parse arguments
################################################################################
RUSTC_VERSION=$(rustc --version)
CARGO_TARGET_DIR=${CARGO_TARGET_DIR:-"target"}
echo "Computing build version..."
PRODUCT_VERSION=$(cargo run -q --bin mullvad-version)
log_header "Building Mullvad VPN $PRODUCT_VERSION"
# If compiler optimization and artifact compression should be turned on or not
OPTIMIZE="false"
# If the produced binaries should be signed (Windows + macOS only)
SIGN="false"
# If the produced app and pkg should be notarized by apple (macOS only)
NOTARIZE="false"
# If a macOS or Windows build should create an installer artifact working on both
# x86 and arm64
UNIVERSAL="false"
while [[ "$#" -gt 0 ]]; do
case $1 in
--optimize) OPTIMIZE="true";;
--sign) SIGN="true";;
--notarize) NOTARIZE="true";;
--universal)
if [[ "$(uname -s)" != "Darwin" && "$(uname -s)" != "MINGW"* ]]; then
log_error "--universal only works on macOS and Windows"
exit 1
fi
UNIVERSAL="true"
;;
*)
log_error "Unknown parameter: $1"
exit 1
;;
esac
shift
done
# Check if we are a building a release. Meaning we are configured to build with optimizations,
# sign the artifacts, AND we are currently building on a release git tag.
# Everything that is not a release build is called a "dev build" and has "-dev-{commit hash}"
# appended to the version name.
IS_RELEASE="false"
if [[ "$SIGN" == "true" && "$OPTIMIZE" == "true" && "$PRODUCT_VERSION" != *"-dev-"* ]]; then
IS_RELEASE="true"
fi
################################################################################
# Configure build
################################################################################
CARGO_ARGS=()
NPM_PACK_ARGS=()
if [[ -n ${TARGETS:-""} ]]; then
NPM_PACK_ARGS+=(--targets "${TARGETS[*]}")
fi
NPM_PACK_ARGS+=(--host-target-triple "$HOST")
if [[ "$UNIVERSAL" == "true" ]]; then
if [[ -n ${TARGETS:-""} ]]; then
log_error "'TARGETS' and '--universal' cannot be specified simultaneously."
exit 1
else
log_info "Building universal distribution"
fi
# 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.
# 'cargo run --bin mullvad --shell-completions'.
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)
fi
if [[ "$OPTIMIZE" == "true" ]]; then
CARGO_ARGS+=(--release)
RUST_BUILD_MODE="release"
NPM_PACK_ARGS+=(--release)
else
RUST_BUILD_MODE="debug"
NPM_PACK_ARGS+=(--no-compression)
fi
# The cargo builds that are part of the C++ builds only enforce `--locked` when built
# in release mode. And we must enforce `--locked` for all signed builds. So we enable
# release mode if either optimizations or signing is enabled.
if [[ "$OPTIMIZE" == "true" || "$SIGN" == "true" ]]; then
CPP_BUILD_MODE="Release"
else
CPP_BUILD_MODE="Debug"
fi
if [[ "$SIGN" == "true" ]]; then
if [[ $(git diff --shortstat 2> /dev/null | tail -n1) != "" ]]; then
log_error "Dirty working directory!"
log_error "Will only build a signed app in a clean working directory"
exit 1
fi
# Will not allow an outdated lockfile when building with signatures
# (The build servers should never build without --locked for
# reproducibility and supply chain security)
CARGO_ARGS+=(--locked)
if [[ "$(uname -s)" == "Darwin" ]]; then
log_info "Configuring environment for signing of binaries"
if [[ -z ${CSC_LINK-} ]]; then
log_error "The variable CSC_LINK is not set. It needs to point to a file containing the"
log_error "private key used for signing of binaries."
exit 1
fi
if [[ -z ${CSC_KEY_PASSWORD-} ]]; then
read -rsp "CSC_KEY_PASSWORD = " CSC_KEY_PASSWORD
echo ""
export CSC_KEY_PASSWORD
fi
# macOS: This needs to be set to 'true' to activate signing, even when CSC_LINK is set.
export CSC_IDENTITY_AUTO_DISCOVERY=true
elif [[ "$(uname -s)" == "MINGW"* ]]; then
if [[ -z ${CERT_HASH-} ]]; then
log_error "The variable CERT_HASH is not set. It needs to be set to the thumbprint of"
log_error "the signing certificate."
exit 1
fi
unset CSC_LINK CSC_KEY_PASSWORD
export CSC_IDENTITY_AUTO_DISCOVERY=false
else
unset CSC_LINK CSC_KEY_PASSWORD
export CSC_IDENTITY_AUTO_DISCOVERY=false
fi
else
log_info "!! Unsigned build. Not for general distribution !!"
unset CSC_LINK CSC_KEY_PASSWORD
export CSC_IDENTITY_AUTO_DISCOVERY=false
fi
if [[ "$NOTARIZE" == "true" ]]; then
NPM_PACK_ARGS+=(--notarize)
fi
if [[ "$IS_RELEASE" == "true" ]]; then
log_info "Removing old Rust build artifacts..."
cargo clean
else
# Allow dev builds to override which API server to use at runtime.
CARGO_ARGS+=(--features api-override)
fi
# Make Windows builds include a manifest in the daemon binary declaring it must
# be run as admin.
if [[ "$(uname -s)" == "MINGW"* ]]; then
export MULLVAD_ADD_MANIFEST="1"
fi
################################################################################
# Compile and build
################################################################################
# Sign all binaries passed as arguments to this function
function sign_win {
local NUM_RETRIES=3
for binary in "$@"; do
# Try multiple times in case the timestamp server cannot
# be contacted.
for i in $(seq 0 ${NUM_RETRIES}); do
log_info "Signing $binary..."
if signtool sign \
-tr http://timestamp.digicert.com -td sha256 \
-fd sha256 -d "Mullvad VPN" \
-du "https://github.com/mullvad/mullvadvpn-app#readme" \
-sha1 "$CERT_HASH" "$binary"
then
break
fi
if [ "$i" -eq "${NUM_RETRIES}" ]; then
return 1
fi
sleep 1
done
done
return 0
}
# Build the daemon and other Rust/C++ binaries, optionally
# sign them, and copy to `dist-assets/`.
function build {
local specified_target=${1:-""}
local current_target=${specified_target:-"$HOST"}
local for_target_string
if [[ -n $specified_target ]]; then
for_target_string=" for $current_target"
else
for_target_string=" for local target $HOST"
fi
################################################################################
# Compile and link all binaries.
################################################################################
log_header "Building Rust code in $RUST_BUILD_MODE mode using $RUSTC_VERSION$for_target_string"
local cargo_target_arg=()
if [[ -n $specified_target ]]; then
cargo_target_arg+=(--target="$specified_target")
fi
local cargo_crates_to_build=(
-p mullvad-daemon --bin mullvad-daemon
-p mullvad-cli --bin mullvad
-p mullvad-setup --bin mullvad-setup
-p mullvad-problem-report --bin mullvad-problem-report
-p talpid-openvpn-plugin --lib
)
if [[ ("$(uname -s)" == "Linux") ]]; then
cargo_crates_to_build+=(-p mullvad-exclude --bin mullvad-exclude)
fi
cargo build "${cargo_target_arg[@]}" "${CARGO_ARGS[@]}" "${cargo_crates_to_build[@]}"
################################################################################
# Move binaries to correct locations in dist-assets
################################################################################
# All the binaries produced by cargo that we want to include in the app
if [[ ("$(uname -s)" == "Darwin") ]]; then
BINARIES=(
mullvad-daemon
mullvad
mullvad-problem-report
libtalpid_openvpn_plugin.dylib
mullvad-setup
)
elif [[ ("$(uname -s)" == "Linux") ]]; then
BINARIES=(
mullvad-daemon
mullvad
mullvad-problem-report
libtalpid_openvpn_plugin.so
mullvad-setup
mullvad-exclude
)
elif [[ ("$(uname -s)" == "MINGW"*) ]]; then
BINARIES=(
mullvad-daemon.exe
mullvad.exe
mullvad-problem-report.exe
talpid_openvpn_plugin.dll
mullvad-setup.exe
libwg.dll
maybenot_ffi.dll
)
fi
if [[ -n $specified_target ]]; then
local cargo_output_dir="$CARGO_TARGET_DIR/$specified_target/$RUST_BUILD_MODE"
# To make it easier to package multiple targets, the binaries are
# located in a directory with the name of the target triple.
local destination_dir="dist-assets/$specified_target"
mkdir -p "$destination_dir"
else
local cargo_output_dir="$CARGO_TARGET_DIR/$RUST_BUILD_MODE"
local destination_dir="dist-assets"
fi
for binary in "${BINARIES[@]}"; do
local source="$cargo_output_dir/$binary"
local destination="$destination_dir/$binary"
log_info "Copying $source => $destination"
cp "$source" "$destination"
if [[ "$SIGN" == "true" && "$(uname -s)" == "MINGW"* ]]; then
sign_win "$destination"
fi
done
if [[ "$current_target" == "aarch64-pc-windows-msvc" ]]; then
# We ship x64 OpenVPN with ARM64, so we need an x64 talpid-openvpn-plugin
# to include in the package.
local source="$CARGO_TARGET_DIR/x86_64-pc-windows-msvc/$RUST_BUILD_MODE/talpid_openvpn_plugin.dll"
local destination
if [[ -n "$specified_target" ]]; then
destination="dist-assets/$specified_target/talpid_openvpn_plugin.dll"
else
destination="dist-assets/talpid_openvpn_plugin.dll"
fi
log_info "Workaround: building x64 talpid-openvpn-plugin"
cargo build --target x86_64-pc-windows-msvc "${CARGO_ARGS[@]}" -p talpid-openvpn-plugin --lib
cp "$source" "$destination"
if [[ "$SIGN" == "true" ]]; then
sign_win "$destination"
fi
fi
}
if [[ "$(uname -s)" == "MINGW"* ]]; then
if [[ "$IS_RELEASE" == "true" ]]; then
./build-windows-modules.sh clean
else
echo "Will NOT clean intermediate files in ./windows/**/bin/ in dev builds"
fi
for t in "${TARGETS[@]:-"$HOST"}"; do
case "${t:-"$HOST"}" in
x86_64-pc-windows-msvc) CPP_BUILD_TARGET=x64;;
aarch64-pc-windows-msvc) CPP_BUILD_TARGET=ARM64;;
*)
log_error "Unknown Windows target: $t"
exit 1
;;
esac
log_header "Building C++ code in $CPP_BUILD_MODE mode for $CPP_BUILD_TARGET"
CPP_BUILD_MODES=$CPP_BUILD_MODE CPP_BUILD_TARGETS=$CPP_BUILD_TARGET ./build-windows-modules.sh
if [[ "$SIGN" == "true" ]]; then
CPP_BINARIES=(
"windows/winfw/bin/$CPP_BUILD_TARGET-$CPP_BUILD_MODE/winfw.dll"
"windows/driverlogic/bin/$CPP_BUILD_TARGET-$CPP_BUILD_MODE/driverlogic.exe"
# The nsis plugin is always built in 32 bit release mode
windows/nsis-plugins/bin/Win32-Release/*.dll
)
sign_win "${CPP_BINARIES[@]}"
fi
done
fi
for t in "${TARGETS[@]:-""}"; do
source env.sh "$t"
build "$t"
done
################################################################################
# Package app.
################################################################################
log_header "Preparing for packaging Mullvad VPN $PRODUCT_VERSION"
if [[ "$(uname -s)" == "Darwin" || "$(uname -s)" == "Linux" ]]; then
mkdir -p "build/shell-completions"
for sh in bash zsh fish; do
log_info "Generating shell completion script for $sh..."
cargo run --bin mullvad "${CARGO_ARGS[@]}" -- shell-completions "$sh" \
"build/shell-completions/"
done
else
mkdir -p "build"
fi
log_info "Updating relays.json..."
cargo run -p mullvad-api --bin relay_list "${CARGO_ARGS[@]}" > build/relays.json
log_header "Installing JavaScript dependencies"
pushd desktop/packages/mullvad-vpn
npm ci --no-audit --no-fund
log_header "Packing Mullvad VPN $PRODUCT_VERSION artifact(s)"
case "$(uname -s)" in
Linux*) npm run pack:linux -- "${NPM_PACK_ARGS[@]}";;
Darwin*) npm run pack:mac -- "${NPM_PACK_ARGS[@]}";;
MINGW*) npm run pack:win -- "${NPM_PACK_ARGS[@]}";;
esac
popd
# sign installer on Windows
if [[ "$SIGN" == "true" && "$(uname -s)" == "MINGW"* ]]; then
for installer_path in dist/*"$PRODUCT_VERSION"*.exe; do
log_info "Signing $installer_path"
sign_win "$installer_path"
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"
xcrun notarytool submit dist/*"$PRODUCT_VERSION"*.pkg \
--keychain "$NOTARIZE_KEYCHAIN" \
--keychain-profile "$NOTARIZE_KEYCHAIN_PROFILE" \
--wait
log_info "Stapling pkg"
xcrun stapler staple dist/*"$PRODUCT_VERSION"*.pkg
fi
log_success "**********************************"
log_success ""
log_success " The build finished successfully! "
log_success " You have built:"
log_success ""
log_success " $PRODUCT_VERSION"
log_success ""
log_success "**********************************"