mozilla / rust-android-gradle

Apache License 2.0
1.03k stars 67 forks source link

Automatically bundle `libc++_shared` in apk? #106

Open Rodrigodd opened 2 years ago

Rodrigodd commented 2 years ago

I am currently migrating a project from using cargo-apk to a Gradle project, using rust-android-gradle.

The problem is that my project need to link to libc++_shared, but I am yet to find a compelling way to automatically bundle this library, that come with the NDK. I know that cargo-apk automatically bundles the library when linking to c++_shared, and, from searching, apparently gradle automatically also do this when using cmake or ndk-build.

Currently, I am simply copying the shared library to jniLibs, and searching how I could specify the original library in NDK to Gradle, but maybe rust-android-gradle should handle that?

Rodrigodd commented 2 years ago

I found out how to write a task to automatically copy the library to rustJniLibs. I take reference from rust-android-gradle itself, and from the NDK Build System Maintainers Guide.

But I have yet to write a task for each architecture.

tasks.whenTaskAdded { task ->
    if (task.name == 'mergeDebugJniLibFolders' || task.name == 'mergeReleaseJniLibFolders') {
        task.dependsOn 'cargoBuild'
    }
    if (task.name == 'cargoBuildArm64') {
        task.dependsOn 'copy_libc++_sharedArm64'
    }
}

// TODO: make a task for each architecture
tasks.register('copy_libc++_sharedArm64', Copy) {
    def ndkDir = android.ndkDirectory
    // hostTag and archTriple from: https://developer.android.com/ndk/guides/other_build_systems
    // TODO: detect the correct host tag
    def hostTag = 'windows-x86_64'
    def archTriple = 'aarch64-linux-android'
    from "$ndkDir/toolchains/llvm/prebuilt/$hostTag/sysroot/usr/lib/$archTriple/libc++_shared.so"
    into layout.buildDirectory.dir("rustJniLibs/android/arm64-v8a")
}
Rodrigodd commented 1 year ago

The necessity for fixing the TODOs has finally come:

def localProperties = new Properties()
localProperties.load(new FileInputStream(rootProject.file("local.properties")))

tasks.whenTaskAdded { task ->
    if (task.name == 'mergeDebugJniLibFolders' || task.name == 'mergeReleaseJniLibFolders') {
        task.dependsOn 'cargoBuild'
    }
    for (target in cargo.targets) {
        if (task.name == "cargoBuild${target.capitalize()}") {
            task.dependsOn "copy_libc++_shared${target.capitalize()}"
        }
    }
}

for (target in cargo.targets) {
    tasks.register("copy_libc++_shared${target.capitalize()}", Copy) {
        def ndkDir = android.ndkDirectory
        // hostTag and archTriple from: https://developer.android.com/ndk/guides/other_build_systems
        def hostTag = localProperties['hostTag']
        def archTriple = [
            arm: 'armv7a-linux-androideabi',
            arm64: 'aarch64-linux-android',
            x86: 'i686-linux-android',
            x86_64: 'x86_64-linux-android',
        ][target]
        from "$ndkDir/toolchains/llvm/prebuilt/$hostTag/sysroot/usr/lib/$archTriple/libc++_shared.so"
        into layout.buildDirectory.dir("rustJniLibs/android/arm64-v8a")
    }
}

As long as the code that generate the task name don't change, this probably will continue to work. I didn't find a way to get the current host tag, so I am manually writing it to local.properties.

ncalexan commented 1 year ago

Hi @Rodrigodd -- sorry for not responding to this when you filed it moons ago. I'd be happy to have this happen automatically, perhaps behind a flag, in the main plugin, since it's probably a common issue. Inside the plugin, we do some similar things with the prebuilt toolchains and must find the host tag somewhere.

Also, there's a small issue where you're copying into the arm64-v8a libs with regard to the target.

Rodrigodd commented 1 year ago

Hi @ncalexan , thanks for responding!

there's a small issue where you're copying into the arm64-v8a libs with regard to the target

Oh, true. I also noticed that the abi name for the arm target is wrong, because it is different for bintools, according to the android documentation. I also noticed that the task fails silently if the file does not exists.

we do some similar things with the prebuilt toolchains and must find the host tag somewhere

Ah yes, it happens here. I will copy it, if you don't mind.

Taking everything above in consideration, now I have the following code:

import org.apache.tools.ant.taskdefs.condition.Os

tasks.whenTaskAdded { task ->
    if (task.name == 'mergeDebugJniLibFolders' || task.name == 'mergeReleaseJniLibFolders') {
        task.dependsOn 'cargoBuild'
    }
    for (target in cargo.targets) {
        if (task.name == "cargoBuild${target.capitalize()}") {
            task.dependsOn "copy_libc++_shared${target.capitalize()}"
        }
    }
}

for (target in cargo.targets) {
      tasks.register("copy_libc++_shared${target.capitalize()}", Copy) {
        def ndkDir = android.ndkDirectory
        // hostTag, abi and archTriple from: https://developer.android.com/ndk/guides/other_build_systems

        def hostTag
        if (Os.isFamily(Os.FAMILY_WINDOWS)) {
            if (Os.isArch("x86_64") || Os.isArch("amd64")) {
                hostTag = "windows-x86_64"
            } else {
                hostTag = "windows"
            }
        } else if (Os.isFamily(Os.FAMILY_MAC)) {
            hostTag = "darwin-x86_64"
        } else {
            hostTag = "linux-x86_64"
        }

        def (abi, archTriple) = [
            arm: ['armeabi-v7a', 'arm-linux-androideabi'],
            arm64: ['arm64-v8a', 'aarch64-linux-android'],
            x86: ['x86', 'i686-linux-android'],
            x86_64: ['x86_64', 'x86_64-linux-android'],
        ][target]

        def from_path = "$ndkDir/toolchains/llvm/prebuilt/$hostTag/sysroot/usr/lib/$archTriple/libc++_shared.so"
        def into_path = layout.buildDirectory.dir("rustJniLibs/android/$abi")

        assert file(from_path).exists()

        from from_path
        into into_path
    }
}