summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-01-08 09:33:44 +0100
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-01-08 10:48:48 +0100
commit1c1ca559b69810904538c2e46c2326d2ebcaa014 (patch)
treee42cb4535910bdeeb25018786035a21130e5ff31 /android
parentac9ac4b824b62d60f64be46f69a4678152d64fb5 (diff)
downloadmullvadvpn-1c1ca559b69810904538c2e46c2326d2ebcaa014.tar.xz
mullvadvpn-1c1ca559b69810904538c2e46c2326d2ebcaa014.zip
Add gradle rust plugin
Diffstat (limited to 'android')
-rw-r--r--android/BuildInstructions.md10
-rw-r--r--android/app/build.gradle.kts110
-rw-r--r--android/build.gradle.kts3
-rwxr-xr-xandroid/build.sh92
-rw-r--r--android/buildSrc/src/main/kotlin/Versions.kt1
-rw-r--r--android/docker/Dockerfile2
-rw-r--r--android/docs/BuildInstructions.macos.md24
-rw-r--r--android/docs/DebugInstructions.md16
-rw-r--r--android/gradle/libs.versions.toml6
-rwxr-xr-xandroid/scripts/update-lockfile.sh4
10 files changed, 220 insertions, 48 deletions
diff --git a/android/BuildInstructions.md b/android/BuildInstructions.md
index c80214f013..e89d1b3102 100644
--- a/android/BuildInstructions.md
+++ b/android/BuildInstructions.md
@@ -118,10 +118,10 @@ Linux distro:
```bash
cd "$ANDROID_HOME" # Or some other directory to place the Android NDK
- wget https://dl.google.com/android/repository/android-ndk-r27b-linux.zip
- unzip android-ndk-r27b-linux.zip
+ wget https://dl.google.com/android/repository/android-ndk-r27c-linux.zip
+ unzip android-ndk-r27c-linux.zip
- cd android-ndk-r27b
+ cd android-ndk-r27c
export ANDROID_NDK_HOME="$PWD"
```
@@ -162,7 +162,7 @@ Run the following command to download wireguard-go-rs submodule: `git submodule
### Debug build
Run the following command to build a debug build:
```bash
-../build-apk.sh --dev-build
+../android/build.sh --dev-build
```
### Release build
@@ -170,7 +170,7 @@ Run the following command to build a debug build:
2. Move, copy or symlink the directory from step 1 to [./credentials/](./credentials/) (`<repository>/android/credentials/`).
3. Run the following command to build:
```bash
- ../build-apk.sh --app-bundle
+ ../android/build.sh --app-bundle
```
## Configure signing key
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 5fed7575d4..dd1444302c 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -1,7 +1,9 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
import com.android.build.gradle.internal.tasks.factory.dependsOn
import com.github.triplet.gradle.androidpublisher.ReleaseStatus
+import java.io.ByteArrayOutputStream
import java.io.FileInputStream
+import java.io.FileOutputStream
import java.util.Properties
import org.gradle.internal.extensions.stdlib.capitalized
@@ -13,6 +15,7 @@ plugins {
alias(libs.plugins.kotlin.ksp)
alias(libs.plugins.compose)
alias(libs.plugins.protobuf.core)
+ alias(libs.plugins.rust.android.gradle)
id(Dependencies.junit5AndroidPluginId) version Versions.junit5Plugin
}
@@ -21,7 +24,6 @@ val repoRootPath = rootProject.projectDir.absoluteFile.parentFile.absolutePath
val extraAssetsDirectory = layout.buildDirectory.dir("extraAssets").get()
val relayListPath = extraAssetsDirectory.file("relays.json").asFile
val defaultChangelogAssetsDirectory = "$repoRootPath/android/src/main/play/release-notes/"
-val extraJniDirectory = layout.buildDirectory.dir("extraJni").get()
val credentialsPath = "${rootProject.projectDir}/credentials"
val keystorePropertiesFile = file("$credentialsPath/keystore.properties")
@@ -35,6 +37,7 @@ android {
namespace = "net.mullvad.mullvadvpn"
compileSdk = Versions.compileSdkVersion
buildToolsVersion = Versions.buildToolsVersion
+ ndkVersion = Versions.ndkVersion
defaultConfig {
val localProperties = gradleLocalProperties(rootProject.projectDir, providers)
@@ -126,7 +129,6 @@ android {
.getOrDefault("OVERRIDE_CHANGELOG_DIR", defaultChangelogAssetsDirectory)
assets.srcDirs(extraAssetsDirectory, changelogDir)
- jniLibs.srcDirs(extraJniDirectory)
}
}
@@ -239,12 +241,14 @@ android {
createDistBundle.dependsOn("bundle$capitalizedVariantName")
- // Ensure all relevant assemble tasks depend on our ensure tasks.
- tasks["assemble$capitalizedVariantName"].apply {
- dependsOn(tasks["ensureRelayListExist"])
- dependsOn(tasks["ensureJniDirectoryExist"])
- dependsOn(tasks["ensureValidVersionCode"])
- }
+ // Ensure we have relay list ready before merging assets.
+ tasks["merge${capitalizedVariantName}Assets"].dependsOn(tasks["generateRelayList"])
+
+ // Ensure that we have all the JNI libs before merging them.
+ tasks["merge${capitalizedVariantName}JniLibFolders"].dependsOn("cargoBuild")
+
+ // Ensure all relevant assemble tasks depend on our ensure task.
+ tasks["assemble$capitalizedVariantName"].dependsOn(tasks["ensureValidVersionCode"])
}
}
@@ -255,6 +259,80 @@ junitPlatform {
}
}
+cargo {
+ val isReleaseBuild = isReleaseBuild()
+ val enableApiOverride = !isReleaseBuild || isAlphaOrDevBuild()
+ module = repoRootPath
+ libname = "mullvad-jni"
+ // All available targets:
+ // https://github.com/mozilla/rust-android-gradle/tree/master?tab=readme-ov-file#targets
+ targets =
+ gradleLocalProperties(rootProject.projectDir, providers)
+ .getProperty("CARGO_TARGETS")
+ ?.split(",") ?: listOf("arm", "arm64", "x86", "x86_64")
+ profile =
+ if (isReleaseBuild) {
+ "release"
+ } else {
+ "debug"
+ }
+ prebuiltToolchains = true
+ targetDirectory = "$repoRootPath/target"
+ features {
+ if (enableApiOverride) {
+ defaultAnd(arrayOf("api-override"))
+ }
+ }
+ targetIncludes = arrayOf("libmullvad_jni.so")
+ extraCargoBuildArguments = buildList {
+ add("--package=mullvad-jni")
+ if (isReleaseBuild) {
+ add("--locked")
+ }
+ }
+}
+
+tasks.register<Exec>("generateRelayList") {
+ workingDir = File(repoRootPath)
+ standardOutput = ByteArrayOutputStream()
+
+ onlyIf { isReleaseBuild() || !relayListPath.exists() }
+
+ commandLine("cargo", "run", "--bin", "relay_list")
+
+ doLast {
+ val output = standardOutput as ByteArrayOutputStream
+ // Create file if needed
+ relayListPath.parentFile.mkdirs()
+ relayListPath.createNewFile()
+ FileOutputStream(relayListPath).use { it.write(output.toByteArray()) }
+ }
+}
+
+tasks.register<Exec>("cargoClean") {
+ workingDir = File(repoRootPath)
+ commandLine("cargo", "clean")
+}
+
+if (
+ gradleLocalProperties(rootProject.projectDir, providers)
+ .getProperty("CLEAN_CARGO_BUILD")
+ ?.toBoolean() != false
+) {
+ tasks["clean"].dependsOn("cargoClean")
+}
+
+// This is a hack and will not work correctly under all scenarios.
+// See DROID-1696 for how we can improve this.
+fun isReleaseBuild() =
+ gradle.startParameter.getTaskNames().any { it.contains("release", ignoreCase = true) }
+
+fun isAlphaOrDevBuild(): Boolean {
+ val localProperties = gradleLocalProperties(rootProject.projectDir, providers)
+ val versionName = generateVersionName(localProperties)
+ return versionName.contains("dev") || versionName.contains("alpha")
+}
+
androidComponents {
beforeVariants { variantBuilder ->
variantBuilder.enable =
@@ -276,22 +354,6 @@ configure<org.owasp.dependencycheck.gradle.extension.DependencyCheckExtension> {
skipConfigurations = listOf("lintClassPath")
}
-tasks.register("ensureRelayListExist") {
- doLast {
- if (!relayListPath.exists()) {
- throw GradleException("Missing relay list: $relayListPath")
- }
- }
-}
-
-tasks.register("ensureJniDirectoryExist") {
- doLast {
- if (!extraJniDirectory.asFile.exists()) {
- throw GradleException("Missing JNI directory: $extraJniDirectory")
- }
- }
-}
-
// This is a safety net to avoid generating too big version codes, since that could potentially be
// hard and inconvenient to recover from.
tasks.register("ensureValidVersionCode") {
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
index 24bcb0e4d0..b43f4fec86 100644
--- a/android/build.gradle.kts
+++ b/android/build.gradle.kts
@@ -14,6 +14,7 @@ plugins {
alias(libs.plugins.kotlin.ksp) apply false
alias(libs.plugins.kotlin.parcelize) apply false
alias(libs.plugins.protobuf.core) apply false
+ alias(libs.plugins.rust.android.gradle) apply false
alias(libs.plugins.detekt) apply true
alias(libs.plugins.dependency.versions) apply true
@@ -68,6 +69,8 @@ buildscript {
classpath("$prebuilt:linux-x86_64@tar.gz")
classpath("$prebuilt:macos-aarch64@tar.gz")
classpath("$prebuilt:macos-x86_64@tar.gz")
+
+ classpath("org.mozilla.rust-android-gradle:plugin:${libs.versions.rust.android.gradle}")
}
}
diff --git a/android/build.sh b/android/build.sh
new file mode 100755
index 0000000000..43dc034ff8
--- /dev/null
+++ b/android/build.sh
@@ -0,0 +1,92 @@
+#!/usr/bin/env bash
+
+set -eu
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+cd "$SCRIPT_DIR"
+
+echo "Computing build version..."
+echo ""
+PRODUCT_VERSION=$(cargo run -q --bin mullvad-version versionName)
+echo "Building Mullvad VPN $PRODUCT_VERSION for Android"
+echo ""
+
+BUILD_TYPE="release"
+GRADLE_BUILD_TYPE="release"
+GRADLE_TASKS=(createOssProdReleaseDistApk createPlayProdReleaseDistApk)
+BUILD_BUNDLE="no"
+BUNDLE_TASKS=(createPlayProdReleaseDistBundle)
+RUN_PLAY_PUBLISH_TASKS="no"
+PLAY_PUBLISH_TASKS=()
+
+while [ -n "${1:-""}" ]; do
+ if [[ "${1:-""}" == "--dev-build" ]]; then
+ BUILD_TYPE="debug"
+ GRADLE_BUILD_TYPE="debug"
+ GRADLE_TASKS=(createOssProdDebugDistApk)
+ BUNDLE_TASKS=(createOssProdDebugDistBundle)
+ elif [[ "${1:-""}" == "--fdroid" ]]; then
+ GRADLE_BUILD_TYPE="fdroid"
+ GRADLE_TASKS=(createOssProdFdroidDistApk)
+ BUNDLE_TASKS=(createOssProdFdroidDistBundle)
+ elif [[ "${1:-""}" == "--app-bundle" ]]; then
+ BUILD_BUNDLE="yes"
+ elif [[ "${1:-""}" == "--enable-play-publishing" ]]; then
+ RUN_PLAY_PUBLISH_TASKS="yes"
+ fi
+
+ shift 1
+done
+
+if [[ "$GRADLE_BUILD_TYPE" == "release" ]]; then
+ if [ ! -f "$SCRIPT_DIR/credentials/keystore.properties" ]; then
+ echo "ERROR: No keystore.properties file found" >&2
+ echo " Please configure the signing keys as described in the README" >&2
+ exit 1
+ fi
+fi
+
+if [[ "$BUILD_TYPE" == "release" ]]; then
+ if [[ "$PRODUCT_VERSION" == *"-dev-"* ]]; then
+ GRADLE_TASKS+=(createPlayDevmoleReleaseDistApk createPlayStagemoleReleaseDistApk)
+ BUNDLE_TASKS+=(createPlayDevmoleReleaseDistBundle createPlayStagemoleReleaseDistBundle)
+ elif [[ "$PRODUCT_VERSION" == *"-alpha"* ]]; then
+ echo "Removing old Rust build artifacts"
+ GRADLE_TASKS+=(createPlayStagemoleReleaseDistApk)
+ BUNDLE_TASKS+=(createPlayStagemoleReleaseDistBundle)
+ PLAY_PUBLISH_TASKS=(publishPlayStagemoleReleaseBundle)
+ fi
+fi
+
+# Fallback to the system-wide gradle command if the gradlew script is removed.
+# It is removed by the F-Droid build process before the build starts.
+if [ -f "gradlew" ]; then
+ GRADLE_CMD="./gradlew"
+elif which gradle > /dev/null; then
+ GRADLE_CMD="gradle"
+else
+ echo "ERROR: No gradle command found" >&2
+ echo " Please either install gradle or restore the gradlew file" >&2
+ exit 2
+fi
+
+$GRADLE_CMD --console plain clean
+
+$GRADLE_CMD --console plain "${GRADLE_TASKS[@]}"
+
+if [[ "$BUILD_BUNDLE" == "yes" ]]; then
+ $GRADLE_CMD --console plain "${BUNDLE_TASKS[@]}"
+fi
+
+if [[ "$RUN_PLAY_PUBLISH_TASKS" == "yes" && "${#PLAY_PUBLISH_TASKS[@]}" -ne 0 ]]; then
+ $GRADLE_CMD --console plain "${PLAY_PUBLISH_TASKS[@]}"
+fi
+
+echo "**********************************"
+echo ""
+echo " The build finished successfully! "
+echo " You have built:"
+echo ""
+echo " $PRODUCT_VERSION"
+echo ""
+echo "**********************************"
diff --git a/android/buildSrc/src/main/kotlin/Versions.kt b/android/buildSrc/src/main/kotlin/Versions.kt
index 223331cfc5..d08012324b 100644
--- a/android/buildSrc/src/main/kotlin/Versions.kt
+++ b/android/buildSrc/src/main/kotlin/Versions.kt
@@ -4,6 +4,7 @@ object Versions {
const val buildToolsVersion = "35.0.0"
const val minSdkVersion = 26
const val targetSdkVersion = 35
+ const val ndkVersion = "27.2.12479018"
const val junitJupiter = "5.11.4"
const val junit5Android = "1.6.0"
diff --git a/android/docker/Dockerfile b/android/docker/Dockerfile
index 17a68510ed..32175853a6 100644
--- a/android/docker/Dockerfile
+++ b/android/docker/Dockerfile
@@ -8,7 +8,7 @@
# -v $GRADLE_CACHE_VOLUME_NAME:/root/.gradle:Z \
# -v $ANDROID_CREDENTIALS_DIR:/build/android/credentials:Z \
# -v /path/to/repository_root:/build:Z \
-# mullvadvpn-app-build-android ./build-apk.sh --dev-build
+# mullvadvpn-app-build-android ./android/build.sh --dev-build
#
# See the base image Dockerfile in the repository root (../../Dockerfile)
# for more information.
diff --git a/android/docs/BuildInstructions.macos.md b/android/docs/BuildInstructions.macos.md
index 78bf345d1d..0368c1e917 100644
--- a/android/docs/BuildInstructions.macos.md
+++ b/android/docs/BuildInstructions.macos.md
@@ -17,7 +17,7 @@ brew install --cask android-studio
Install the following packages:
```bash
-brew install protobuf gcc go openjdk@17 rustup-init
+brew install protobuf gcc go openjdk@17 rustup-init python3
```
> __*NOTE:*__ Ensure that you setup `openjdk@17` to be the active JDK, follow instructions in
@@ -38,7 +38,7 @@ Open Android Studio -> Tools -> SDK Manager, and install `Android SDK Command-li
Install the necessary Android SDK tools
```bash
-~/Library/Android/sdk/cmdline-tools/latest/bin/sdkmanager "platforms;android-35" "build-tools;35.0.0" "platform-tools" "ndk;27.1.12297006"
+~/Library/Android/sdk/cmdline-tools/latest/bin/sdkmanager "platforms;android-35" "build-tools;35.0.0" "platform-tools" "ndk;27.2.12479018"
```
Install Android targets
@@ -50,7 +50,7 @@ Export the following environmental variables, and possibly store them for exampl
`~/.zprofile` or `~/.zshrc` file:
```bash
export ANDROID_HOME="$HOME/Library/Android/sdk"
-export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/27.1.12297006"
+export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/27.2.12479018"
export NDK_TOOLCHAIN_DIR="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin"
export AR_aarch64_linux_android="$NDK_TOOLCHAIN_DIR/llvm-ar"
export AR_armv7_linux_androideabi="$NDK_TOOLCHAIN_DIR/llvm-ar"
@@ -74,10 +74,23 @@ git submodule update --init --recursive --depth=1 wireguard-go-rs
```
## 4. Debug build
+
+### Android Studio
+
+Create the file `android/local.properties` if it does not exist and add the following line:
+
+```bash
+rust.pythonCommand=/opt/homebrew/bin/python3
+```
+
+You should now be able to run the app directly from Android Studio.
+
+### `android/build.sh`
+
Run the build script in the root of the project to assemble all the native libraries and the app:
```bash
-./build-apk.sh --dev-build
+./android/build.sh --dev-build
```
Once the build is complete you should receive a message looking similar to this:
@@ -92,9 +105,6 @@ Once the build is complete you should receive a message looking similar to this:
**********************************
```
-Your native binaries have now been built, any subsequent builds that does not have changes to the
-native code can be done in Android Studio or using gradle.
-
# Build options and configuration
For configuring signing or options to your build continue with the general [build instructions](../BuildInstructions.md).
diff --git a/android/docs/DebugInstructions.md b/android/docs/DebugInstructions.md
index e4c9100e0e..9faa5237b2 100644
--- a/android/docs/DebugInstructions.md
+++ b/android/docs/DebugInstructions.md
@@ -1,15 +1,13 @@
## Debugging the native libraries in Android Studio with LLDB
-1. Make sure the native libraries have been built with debug symbols. If using the `build-apk.sh`
- script, run `SKIP_STRIPPING=yes ../build-apk.sh --dev-build`.
-2. In Android Studio, go to `Run -> Edit configurations...`
-3. Make sure the `app` configuration is selected.
-4. In the `Debugger` tab, select `Dual (Java + Native)`
-5. Start debugging the app as usual from Android Studio. The app should now stop on a SIGURG signal.
-6. Select the `LLDB` tab in the debugger. Now you can set breakpoints etc, e.g.
+1. In Android Studio, go to `Run -> Edit configurations...`
+2. Make sure the `app` configuration is selected.
+3. In the `Debugger` tab, select `Dual (Java + Native)`
+4. Start debugging the app as usual from Android Studio. The app should now stop on a SIGURG signal.
+5. Select the `LLDB` tab in the debugger. Now you can set breakpoints etc, e.g.
`breakpoint set -n open_tun`
-7. Before continuing run `pro hand -p true -s false SIGURG`
-8. Click `Resume Program` and the app will resume until the breakpoint is hit.
+6. Before continuing run `pro hand -p true -s false SIGURG`
+7. Click `Resume Program` and the app will resume until the breakpoint is hit.
NOTE: When running LLDB, Android Studio can sometimes get into a state where it will try to
connect to the debugger when running the app normally, which blocks the app from starting.
diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml
index 3428c8a8b9..bab6a4fd49 100644
--- a/android/gradle/libs.versions.toml
+++ b/android/gradle/libs.versions.toml
@@ -54,6 +54,9 @@ kotlinx-serialization = "2.1.0"
protobuf-gradle-plugin = "0.9.4"
protobuf = "4.29.2"
+# Rust Android Gradle
+rust-android-gradle = "0.9.5"
+
# Misc
commonsvalidator = "1.9.0"
dependency-check = "10.0.4"
@@ -186,6 +189,9 @@ protobuf-protoc = { id = "com.google.protobuf:protoc", version.ref = "protobuf"
grpc-protoc-gen-grpc-java = { id = "io.grpc:protoc-gen-grpc-java", version.ref = "grpc" }
grpc-protoc-gen-grpc-kotlin = { id = "io.grpc:protoc-gen-grpc-kotlin", version.ref = "grpc-kotlin-jar" }
+# Rust Android Gradle
+rust-android-gradle = { id = "org.mozilla.rust-android-gradle.rust-android", version.ref = "rust-android-gradle" }
+
# Misc
dependency-check = { id = "org.owasp.dependencycheck", version.ref = "dependency-check" }
dependency-versions = { id = "com.github.ben-manes.versions", version.ref = "dependency-versions" }
diff --git a/android/scripts/update-lockfile.sh b/android/scripts/update-lockfile.sh
index e400caed72..0c20ae31c3 100755
--- a/android/scripts/update-lockfile.sh
+++ b/android/scripts/update-lockfile.sh
@@ -20,8 +20,8 @@ GRADLE_TASKS=(
"lint"
)
EXCLUDED_GRADLE_TASKS=(
- "-xensureRelayListExist"
- "-xensureJniDirectoryExist"
+ "-xgenerateRelayList"
+ "-xcargoBuild"
)
export GRADLE_OPTS