diff options
| author | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2025-01-08 09:33:44 +0100 |
|---|---|---|
| committer | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2025-01-08 10:48:48 +0100 |
| commit | 1c1ca559b69810904538c2e46c2326d2ebcaa014 (patch) | |
| tree | e42cb4535910bdeeb25018786035a21130e5ff31 /android | |
| parent | ac9ac4b824b62d60f64be46f69a4678152d64fb5 (diff) | |
| download | mullvadvpn-1c1ca559b69810904538c2e46c2326d2ebcaa014.tar.xz mullvadvpn-1c1ca559b69810904538c2e46c2326d2ebcaa014.zip | |
Add gradle rust plugin
Diffstat (limited to 'android')
| -rw-r--r-- | android/BuildInstructions.md | 10 | ||||
| -rw-r--r-- | android/app/build.gradle.kts | 110 | ||||
| -rw-r--r-- | android/build.gradle.kts | 3 | ||||
| -rwxr-xr-x | android/build.sh | 92 | ||||
| -rw-r--r-- | android/buildSrc/src/main/kotlin/Versions.kt | 1 | ||||
| -rw-r--r-- | android/docker/Dockerfile | 2 | ||||
| -rw-r--r-- | android/docs/BuildInstructions.macos.md | 24 | ||||
| -rw-r--r-- | android/docs/DebugInstructions.md | 16 | ||||
| -rw-r--r-- | android/gradle/libs.versions.toml | 6 | ||||
| -rwxr-xr-x | android/scripts/update-lockfile.sh | 4 |
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 |
