diff --git a/README.md b/README.md index 0515418..0f3951c 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,10 @@ There are detailed instructions for building libsamplerate on Win32 in the file Building on macOS should be the same as building it on any other Unix platform. +## Android + +To build for Android using the Android NDK, see the instructions in the file [`docs/android.md`] + ## Other Platforms To compile libsamplerate on platforms which have a Bourne compatible shell, an ANSI C compiler and a make utility should require no more that the following three commands: diff --git a/android/.gitattributes b/android/.gitattributes new file mode 100644 index 0000000..f91f646 --- /dev/null +++ b/android/.gitattributes @@ -0,0 +1,12 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + +# Binary files should be left untouched +*.jar binary + diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..c38174b --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,7 @@ +local.properties +gradle.properties +build/ +.gradle/ +.cxx/ +.idea/ + diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt new file mode 100644 index 0000000..e69de29 diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..907b78e --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,130 @@ +@file:Suppress("UnstableApiUsage") + +require(gradle.gradleVersion == "8.9") { + "Gradle version 8.9 required (current version: ${gradle.gradleVersion})" +} + +plugins { + alias(libs.plugins.library) + id("maven-publish") +} + +// project.name ("samplerate") defined in settings.gradle.kts +project.group = "com.meganerd" +project.version = "0.2.2-android-rc2" + +android { + namespace = "${project.group}.${project.name}" + compileSdk = libs.versions.compilesdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.minsdk.get().toInt() + + buildToolsVersion = libs.versions.buildtools.get() + ndkVersion = libs.versions.ndk.get() + ndk { + abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") + } + externalNativeBuild { + // build static libs and testing binaries only when running :ndkTest + val buildSharedLibs = if (isTestBuild()) "OFF" else "ON" + val buildTesting = if (isTestBuild()) "ON" else "OFF" + + cmake { + cppFlags += "-std=c++17" + arguments += "-DANDROID_STL=c++_shared" + arguments += "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" + + arguments += "-DBUILD_SHARED_LIBS=$buildSharedLibs" + arguments += "-DBUILD_TESTING=$buildTesting" + arguments += "-DLIBSAMPLERATE_INSTALL=OFF" + arguments += "-DLIBSAMPLERATE_EXAMPLES=OFF" + } + } + } + + externalNativeBuild { + cmake { + path = file("${project.projectDir.parentFile}/CMakeLists.txt") + version = libs.versions.cmake.get() + } + } + + buildFeatures { + prefabPublishing = true + } + + prefab { + create(project.name) { + headers = "${project.projectDir.parentFile}/include" + } + } + + packaging { + // avoids duplicating libs in .aar due to using prefab + jniLibs { + excludes += "**/*" + } + } +} + +tasks.register(getTestTaskName()) { + commandLine("./ndk-test.sh") +} + +tasks.named("clean") { + delete.add(".cxx") +} + +publishing { + repositories { + mavenLocal() + } + + publications { + create(project.name) { + artifact("${project.projectDir}/build/outputs/aar/${project.name}-release.aar") + artifactId = "${project.name}-android" + } + } +} + +afterEvaluate { + tasks.named("preBuild") { + mustRunAfter("clean") + } + tasks.named(getTestTaskName()) { + dependsOn("clean", "assembleRelease") + } + + tasks.named("generatePomFileFor${project.name.cap()}Publication") { + mustRunAfter("assembleRelease") + } + tasks.named("publishToMavenLocal") { + dependsOn("clean", "assembleRelease") + } + + // suggests running ":ndkTest" task instead of default testing tasks + listOf( + "check", + "test", + "testDebugUnitTest", + "testReleaseUnitTest", + "connectedCheck", + "connectedAndroidTest", + "connectedDebugAndroidTest", + ).forEach { + tasks.named(it) { + doLast { + println(":$it task not supported; use :${getTestTaskName()} to run tests via adb") + } + } + } +} + +fun getTestTaskName(): String = "ndkTest" + +fun isTestBuild(): Boolean = gradle.startParameter.taskNames.contains(getTestTaskName()) + +// capitalize the first letter to make task names matched when written in camel case +fun String.cap(): String = this.replaceFirstChar { it.uppercase() } diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml new file mode 100644 index 0000000..6ba3bc6 --- /dev/null +++ b/android/gradle/libs.versions.toml @@ -0,0 +1,10 @@ +[versions] +agp = "8.7.1" +minsdk = "21" +compilesdk = "35" +buildtools = "35.0.0" +ndk = "27.2.12479018" +cmake = "3.30.5" + +[plugins] +library = { id = "com.android.library", version.ref = "agp" } diff --git a/android/ndk-test.sh b/android/ndk-test.sh new file mode 100755 index 0000000..084deb0 --- /dev/null +++ b/android/ndk-test.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +SCRIPT_DIR=$(dirname $0) + +LIB_NAME="samplerate" +TEST_DIR="/data/local/tmp/lib${LIB_NAME}/test" + +# remove existing test files +adb $@ shell "rm -r $TEST_DIR" > /dev/null +adb $@ shell "mkdir -p $TEST_DIR" > /dev/null + +ABIS=`adb $@ shell getprop ro.product.cpu.abilist` + +print_message() { + echo "[==========================================================]" + echo "| [lib${LIB_NAME}]: $1" + echo "[==========================================================]" +} + +for ABI in $(echo $ABIS | tr "," "\n"); do + if [ $ABI == "armeabi" ]; then + print_message "skipping deprecated ABI: [$ABI]"; echo + continue + fi + print_message "testing ABI [$ABI]" + + # create test abi directory + TEST_ABI_DIR="$TEST_DIR/$ABI" + adb $@ shell mkdir -p $TEST_ABI_DIR > /dev/null + + # push test files to device + pushd "$SCRIPT_DIR/build/intermediates/cmake/release/obj/$ABI" > /dev/null + adb $@ push * $TEST_ABI_DIR > /dev/null + popd > /dev/null + + # run tests + adb $@ shell -t "cd $TEST_ABI_DIR && export LD_LIBRARY_PATH=. && find . -type f -not -name '*.so' -executable -exec {} \;" + echo +done + +print_message "tests finished for ABIS: [$ABIS]"; echo +echo "NOTE: make sure to verify the test results manually. This task will not fail if tests fail" diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..b52cfa9 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,25 @@ +@file:Suppress("UnstableApiUsage") + +rootProject.name = "samplerate" + +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + diff --git a/docs/android.md b/docs/android.md new file mode 100644 index 0000000..0e06cee --- /dev/null +++ b/docs/android.md @@ -0,0 +1,85 @@ +layout: default +--- + +# Building for Android + +An Android `gradle` project is located in the `android/` directory. The project +uses the standard [NDK CMake](https://developer.android.com/ndk/guides/cmake) +build system to generate a [prefab](https://google.github.io/prefab/) NDK package. + +## Building the prefab package / .aar +The following commands will build `libsamplerate` as a prefab NDK package and place +it into an [.aar](https://developer.android.com/studio/projects/android-library) library. + +You will need `gradle` version 8.7+ installed in in your path. +``` +cd android/ +gradle assembleRelease +``` + +The resulting `.aar` will be located at: +`android/build/outputs/aar/samplerate-release.aar` + +If you need to specify additional arguments to the `cmake` build, change the +NDK version used for the build, etc, you can do so by editing the `gradle` build +script located at: + +`android/build.gradle.kts` + +## Using as a dependency +After building the `.aar`, do one of the following: +1. `gradle publishToMavenLocal` is already supported in the build script +2. `gradle publishToMavenRepository` is not setup, but you can edit `android/build.gradle.kts` + to add your own maven repository to publish to +3. Copy the `.aar` directly to the `libs/` directory of your project (not recommended) + +Then, add the library to your project's dependencies in your `build.gradle.kts`: +``` +dependencies { + implementation("com.meganerd:samplerate:0.2.2-android-rc1") +} +``` + +Enable `prefab` support in your `build.gradle.kts`: +``` +android { + buildFeatures { + prefab = true + } +} +``` + +Update your `CMakeLists.txt` to find and link the prefab package, which will be +extracted from the `aar` by the build system: + +``` +find_package(samplerate REQUIRED CONFIG) + +target_link_libraries(${CMAKE_PROJECT_NAME} samplerate::samplerate) +``` + +That's it! You can now `#include ` in your NDK source code. + +## Testing on a device +To run the tests, follow these steps: +1. Ensure `adb` is in your path. +2. Have a single device (or emulator) connected and in debug mode. The testing task +only supports a single device. If you have more than one connected (or none) it will +notify you with an error. +3. You will also need `bash` to run the test script + +Run the following commands: +``` +cd android/ +gradle ndkTest +``` + +The test task `:ndkTest` will run `gradle clean assembleRelease` with the following +options set for testing: +* `-DBUILD_SHARED_LIBS=OFF` +* `-DBUILD_TESTING=ON` + +Then it runs `android/ndk-test.sh`, which pushes the binaries located at +`android/build/intermediates/cmake/release/obj/$ABI` to `/data/local/tmp/libsamplerate/test` +on the device, and uses `adb` to execute them. The results will be printed to the console. +