Skip to content

Add android build script #222

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

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ check_include_file(immintrin.h HAVE_IMMINTRIN_H)

find_package(PkgConfig)

if(LIBSAMPLERATE_EXAMPLES OR BUILD_TESTING)
if((LIBSAMPLERATE_EXAMPLES OR BUILD_TESTING) AND (NOT ANDROID))
if((NOT VCPKG_TOOLCHAIN) AND PKG_CONFIG_FOUND AND (NOT CMAKE_VERSION VERSION_LESS 3.6))
pkg_check_modules(SndFile sndfile IMPORTED_TARGET)
if(SndFile_FOUND)
Expand Down
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

9 changes: 9 additions & 0 deletions android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
local.properties
gradle.properties
build/
.gradle/
.cxx/
.idea/
gradle/wrapper/
gradlew
gradlew.bat
Empty file added android/CMakeLists.txt
Empty file.
5 changes: 5 additions & 0 deletions android/aar-template/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.meganerd.samplerate" >
<uses-sdk android:minSdkVersion="21" />
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"abi": "arm64-v8a",
"api": 21,
"ndk": 27,
"stl": "none",
"static": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"abi": "armeabi-v7a",
"api": 21,
"ndk": 27,
"stl": "none",
"static": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"abi": "x86",
"api": 21,
"ndk": 27,
"stl": "none",
"static": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"abi": "x86_64",
"api": 21,
"ndk": 27,
"stl": "none",
"static": false
}
4 changes: 4 additions & 0 deletions android/aar-template/prefab/modules/samplerate/module.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"export_libraries": [],
"android": {}
}
6 changes: 6 additions & 0 deletions android/aar-template/prefab/prefab.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"schema_version": 2,
"name": "Samplerate",
"version": "0.2.2",
"dependencies": []
}
139 changes: 139 additions & 0 deletions android/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
@file:Suppress("UnstableApiUsage")

val requiredGradleVersion = "8.9"

require(gradle.gradleVersion == requiredGradleVersion) {
"Gradle version $requiredGradleVersion 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-r1"

val abis = listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")

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 += abis
}
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 {
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()
}
}
}

tasks.register<Zip>("prefabAar") {
archiveFileName = "${project.name}-release.aar"
destinationDirectory = file("build/outputs/prefab-aar")

from("aar-template")
from("${projectDir.parentFile}/include") {
include("**/*.h")
into("prefab/modules/${project.name}/include")
}
abis.forEach { abi ->
from("build/intermediates/cmake/release/obj/$abi") {
include("lib${project.name}.so")
into("prefab/modules/${project.name}/libs/android.$abi")
}
}
}

tasks.register<Exec>(getTestTaskName()) {
commandLine("./ndk-test.sh")
}

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

afterEvaluate {
tasks.named("preBuild") {
mustRunAfter("clean")
}

tasks.named("prefabAar") {
dependsOn("externalNativeBuildRelease")
}

tasks.named("generatePomFileFor${project.name.cap()}Publication") {
mustRunAfter("prefabAar")
}

tasks.named("publish") {
dependsOn("clean", "prefabAar")
}

tasks.named(getTestTaskName()) {
dependsOn("clean", "externalNativeBuildRelease")
}
}

publishing {
val githubPackagesUrl = "https://maven.pkg.github.com/jg-hot/libsamplerate-android"

repositories {
maven {
url = uri(githubPackagesUrl)
credentials {
username = properties["gpr.user"]?.toString()
password = properties["gpr.key"]?.toString()
}
}
}

publications {
create<MavenPublication>(project.name) {
artifact("build/outputs/prefab-aar/${project.name}-release.aar")
artifactId = "${project.name}-android"

pom {
distributionManagement {
downloadUrl = githubPackagesUrl
}
}
}
}
}

tasks.named<Wrapper>("wrapper") {
gradleVersion = requiredGradleVersion
}

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' -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.