Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add android build script #222

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
12 changes: 12 additions & 0 deletions android/.gitattributes
Original file line number Diff line number Diff line change
@@ -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

7 changes: 7 additions & 0 deletions android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
local.properties
gradle.properties
build/
.gradle/
.cxx/
.idea/

Empty file added android/CMakeLists.txt
Empty file.
130 changes: 130 additions & 0 deletions android/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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<Exec>(getTestTaskName()) {
commandLine("./ndk-test.sh")
}

tasks.named<Delete>("clean") {
delete.add(".cxx")
}

publishing {
repositories {
mavenLocal()
}

publications {
create<MavenPublication>(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() }
10 changes: 10 additions & 0 deletions android/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -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" }
41 changes: 41 additions & 0 deletions android/ndk-test.sh
Original file line number Diff line number Diff line change
@@ -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"
25 changes: 25 additions & 0 deletions android/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -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()
}
}

85 changes: 85 additions & 0 deletions docs/android.md
Original file line number Diff line number Diff line change
@@ -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 <samplerate.h>` 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.