This document aims to explain how to build the Mullvad Android app. It's strongly recommended and primarily supported to build the app using the provided container, as it ensures the correct build environment.
The build process consist of two main steps. First building the native libraries (mullvad-daemon)
and then building the Android app/project which will bundle the previously built native libraries.
Building the native libraries requires some specific toolchains and packages to be installed, so
it's recommended to build using the provided build script and container image.
The native libraries doesn't have to be rebuilt very often, only when including daemon changes or after cleaning the project, so apart from that it's possible to build the Android app/project using the Gradle CLI or the Android Studio GUI.
NOTE: Build with provided container is only supported on Linux and may not work on other platforms.
Building both the native libraries and Android project can easily be achieved by running the
containerized-build.sh script, which helps using the correct
tag and mounting volumes. The script relies on podman
by default, however another container runner such as docker
can be used by setting the CONTAINER_RUNNER environment variable.
After the native libraries have been built, subsequent builds can that doesn't rely on changes to the native libraries can be ran using the Gradle CLI or the Android Studio GUI. This requires either:
- Rust to be installed, since a tooled called
mullvad-versionis used to resolved the version information for the Android app.
or
- Specifying custom version information by following these instructions.
-
Install podman and make sure it's configured to run in rootless mode.
-
OPTIONAL: Get the latest stable Rust toolchain via rustup.rs.
Run the following command to trigger a full debug build:
../building/containerized-build.sh android --dev-build- Configure a signing key by following these instructions.
- Run the following command after setting the
ANDROID_CREDENTIALS_DIRenvironment variable to the directory configured in step 1:
../building/containerized-build.sh android --app-bundleNOTE: This guide is only supported on Linux and may not work on other platforms, if you are using macOS please refer to macOS build instructions
Building without the provided container requires installing multiple Sdk:s and toolchains, and is therefore more complex.
These steps explain how to manually setup the build environment on a Linux system.
Install a protobuf compiler (version 3 and up), it can be installed on most major Linux distros via
the package name protobuf-compiler. An additional package might also be required depending on
Linux distro:
protobuf-develon Fedora.libprotobuf-devon Debian/Ubuntu.
-
Install the JDK
sudo apt install zip openjdk-17-jdk
-
Install the SDK and NDK
The SDK should be placed in a separate directory, like for example
~/androidor/opt/android. This directory should be exported as the$ANDROID_HOMEenvironment variable.Note: if
sdkmanagerfails to find the SDK root path, pass the option--sdk_root=$ANDROID_HOMEto the command above.cd /opt/android # Or some other directory to place the Android SDK export ANDROID_HOME=$PWD wget https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip mkdir -p cmdline-tools unzip commandlinetools-linux-13114758_latest.zip -d cmdline-tools-latest mv cmdline-tools-latest/cmdline-tools cmdline-tools/latest && rm -d cmdline-tools-latest ./cmdline-tools/latest/bin/sdkmanager "platforms;android-36" "build-tools;36.0.0" "platform-tools" "ndk;27.3.13750724"
-
Get the latest stable Rust toolchain via rustup.rs.
-
Configure Android cross-compilation targets and set up linker and archiver. This can be done by setting the following environment variables:
Add to
~/.bashrcor equivalent:export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/27.3.13750724" export NDK_TOOLCHAIN_DIR="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin" export AR_aarch64_linux_android="$NDK_TOOLCHAIN_DIR/llvm-ar" export AR_armv7_linux_androideabi="$NDK_TOOLCHAIN_DIR/llvm-ar" export AR_x86_64_linux_android="$NDK_TOOLCHAIN_DIR/llvm-ar" export AR_i686_linux_android="$NDK_TOOLCHAIN_DIR/llvm-ar" export CC_aarch64_linux_android="$NDK_TOOLCHAIN_DIR/aarch64-linux-android26-clang" export CC_armv7_linux_androideabi="$NDK_TOOLCHAIN_DIR/armv7a-linux-androideabi26-clang" export CC_x86_64_linux_android="$NDK_TOOLCHAIN_DIR/x86_64-linux-android26-clang" export CC_i686_linux_android="$NDK_TOOLCHAIN_DIR/i686-linux-android26-clang" export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$NDK_TOOLCHAIN_DIR/aarch64-linux-android26-clang" export CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="$NDK_TOOLCHAIN_DIR/armv7a-linux-androideabi26-clang" export CARGO_TARGET_I686_LINUX_ANDROID_LINKER="$NDK_TOOLCHAIN_DIR/i686-linux-android26-clang" export CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="$NDK_TOOLCHAIN_DIR/x86_64-linux-android26-clang" -
Install Android targets
./scripts/setup-rust android
-
(Optional) Run the following to install a git
post-checkouthook that will automatically run thesetup-rustscript when the Rust version specified in therust-toolchain.tomlfile changes:.scripts/setup-rust install-hook
git submodule update --init android/rust-android-gradle-pluginRun the following command to build a debug build:
../android/build.sh --dev-build- Configure a signing key by following these instructions.
- Move, copy or symlink the directory from step 1 to ./credentials/ (
<repository>/android/credentials/). - Run the following command to build:
../android/build.sh --app-bundle
This is supported on Linux (x86_64) as well as macOS (x86_64 and aarch64).
- Install the nix package manager by following the official instructions.
- Enable the experimental
nix-commandandflakefeatures by following these instructions. - Launch a devshell (in
<repository>/android) by running:nix develop .#android - Build the app as usual by running for example:
or
build
or./build.sh --dev-build
./gradlew assembleOssProdDebug
-
Create a directory to store the signing key, keystore and its configuration:
export ANDROID_CREDENTIALS_DIR=/tmp/credentials mkdir -p $ANDROID_CREDENTIALS_DIR -
Generate a key/keystore named
app-keys.jksinANDROID_CREDENTIALS_DIRand make sure to write down the used passwords:keytool -genkey -v -keystore $ANDROID_CREDENTIALS_DIR/app-keys.jks -alias release -keyalg RSA -keysize 4096 -validity 10000 -
Create a file named
keystore.propertiesinANDROID_CREDENTIALS_DIR. Enter the following, but replacekey-passwordandkeystore-passwordwith the values from step 2:keyAlias = release keyPassword = key-password storePassword = keystore-password
Run the prepare-release.sh script with the desired version you wish to release as an argument. The prepare script will download the latest relay list and update the version files, and add as commits.
# Replace `202X.X-alphaX` with the alpha version you intend to create.
./scripts/prepare-release.sh 202X.X-alphaXContinue by following the instructions provided by the script.
This lockfile helps ensuring the integrity of the gradle dependencies in the project.
When adding or updating dependencies, it's necessary to also update the lockfile. This can be done in the following way:
-
Run update script:
./scripts/lockfile -u
If you're on macOS make sure GNU sed is installed. Install with
brew install gnu-sedand add it to yourPATHso that it is used instead of thesedmacOS ships withPATH="$HOMEBREW_PREFIX/opt/gnu-sed/libexec/gnubin:$PATH" -
Check diff before committing.
This is easiest done by temporarily removing the lockfile:
rm ./gradle/verification-metadata.xmlSome gradle properties can be set to simplify development, for the full list see android/gradle.properties.
In order to override them, add the properties in <USER_GRADLE_HOME>/gradle.properties. See the
gradle documentation
for more info of the prioritization of properties.
To avoid or override the rust based version generation, the mullvad.app.config.override.versionCode and
mullvad.app.config.override.versionName properties can be set:
mullvad.app.config.override.versionCode=123
mullvad.app.config.override.versionName=1.2.3
To disable in-app notifications related to the app version during development or testing,
the mullvad.app.config.inAppVersionNotifications.enable property can be set:
mullvad.app.config.inAppVersionNotifications.enable=false
To avoid being rate limited we avoid running tests sending requests that are highly rate limited
too often. If you want to run these tests you can override the
mullvad.test.e2e.config.runHighlyRateLimitedTests gradle properties. The default value is false.
Reproducible builds are a way to verify that the app was built from the exact source code it claims to be built from. When a build is reproducible, compiling the same source code with the same tools will always produce bit-for-bit identical output.
The Mullvad Android app is by default reproducible when built using our build container, as the container ensures a consistent build environment with fixed versions of all tools and dependencies.
When building without the container on Linux systems, reproducibility depends on having the exact same versions of system tools (compilers, build tools, etc) installed. Small differences in tool versions or configurations can lead to different build outputs even when using the same source code.
Make sure that any
gradle.propertieshas not changed or been overridden it will affect the reproducibility of the build such as changingmullvad.app.build.cargo.targetsandmullvad.app.config.inAppVersionNotifications.enable.
To maximize reproducibility when building without the container:
- Build the app on a Linux system or virtual machine.
- Use the exact same versions of all build dependencies as specified in the root Dockerfile and Android Dockerfile. This includes for example Android SDK and NDK versions.
A simple way to check that a build is reproducible across environments is to build the fdroid version of the app with and without the container and comparing the checksums of the produced APKs.
- Build the app with the container:
../building/containerized-build.sh android --fdroid - Copy the resulting APK to a different folder as it will be overwritten in the following step:
app/build/outputs/apk/ossProd/fdroid/app-oss-prod-fdroid-unsigned.apk fdroid-container.apk - Build the app locally without the container:
./build.sh --fdroid - Compare the checksums of the two APKs:
sha256sum fdroid-container.apk app/build/outputs/apk/ossProd/fdroid/app-oss-prod-fdroid-unsigned.apk
- Obtain the release APK (
2025.2-beta1or newer) from GitHub releases - Checkout the release tag:
git checkout android/<version> - Build a release build using our build instructions
- Delete the signatures from the two APKs by running
zip -d app-oss-prod-release.apk "META-INF/*"andzip -d MullvadVPN-<version>.apk "META-INF/*" - Compare the checksums of the two APKs:
sha256sum app-oss-prod-release.apk MullvadVPN-<version>.apk. If the checksums are equal the build is reproducible.
If two APKs built from the same commit have different checksums the build is not reproducible. This could be because of either:
- A build dependency on the local system has the wrong version.
- There is a bug that breaks the build reproducibility.
- The APK built is a version prior to
2025.2-beta1, which is the first version that supports reproducible builds.
If you suspect that a bug is causing the build to not be reproducible, please open a Github issue.