Update our megazord/UniFFI gradle code

* Moved `dependsOnMegazord` from `publish.gradle` into
  `component-common.gradle`.  I don't think it makes sense to mix this
  with the publishing code.
* Use configurations to handle the native megazord dependency.
* Manually copy the native megazord into the resource dir when running
  unit tests.  The strategy we were using before doesn't work after
  Gradle 8.1.
* dependsOnMegazord no longer adds a native-support dependency.  This is
  only used by viaduct, so I moved the dependency into viaduct's
  build.gradle.
* Removed the jnaForTest code.  We no longer need this hack now that
  `implementation` and `testImplementation` can specify different types.
This commit is contained in:
Ben Dean-Kawamura 2024-12-10 16:50:40 -05:00 коммит произвёл bendk
Родитель b8c4894e7e
Коммит 7bc69fe08a
4 изменённых файлов: 136 добавлений и 244 удалений

Просмотреть файл

@ -50,3 +50,115 @@ dependencies {
androidTestImplementation libs.test.espresso.core
androidTestImplementation libs.test.runner
}
// Shared logic for projects that depend on libmegazord
//
// This ensures that libmegazord will be in the library path so that it can be loaded. It also adds
// the transitive JNA dependency.
ext.dependsOnTheMegazord = {
dependencies {
api project(":full-megazord")
// Add a JNA dependency, which is required by UniFFI.
//
// The tricky thing is getting the correct libjnidispatch library. Use the AAR version for
// normal builds, since that ensures the various Android libraries get packaged correctly when
// we build our own AAR. Use the JAR version for tests, since that ensures the tests can load
// the native version.
implementation(libs.jna) {
artifact {
type = "aar"
}
}
testImplementation(libs.jna) {
artifact {
type = "jar"
}
}
}
// Configurations are a somewhat mysterious Gradle concept. For our purposes, we can treat them
// sets of files produced by one component and consumed by another.
configurations {
megazordNative {
canBeConsumed = false
}
}
// Wire up the megazordNative configuration to the output produced from the `full-megazord` project.
dependencies {
megazordNative(project("path": ":full-megazord", "configuration": "megazordNative"))
}
afterEvaluate {
android.libraryVariants.all { variant ->
def variantName = variant.name.capitalize();
def testTask = tasks["test${variantName}UnitTest"]
def processTestResTask = tasks["process${variantName}UnitTestJavaRes"]
// Copy the native libmegazord to the resource dir so that it can be loaded by JNA.
// Note: we have to manually copy the library to the output dir ourselves. If we simply
// added the megazordNative directory to sourceSets.test.resources.srcDirs, then the
// android gradle plugin will refuse to copy it. For details see:
//
// * https://github.com/mozilla/application-services/pull/6476#issuecomment-2537227576
// * https://github.com/mozilla/glean/pull/2680#issuecomment-2056627683
def copyNativeMegazord = tasks.register("copy${variantName}NativeMegazord", Copy) {
from configurations.getByName("megazordNative")
into processTestResTask.destinationDir
// Make sure to run after the process java res task, otherwise that one may
// overwrite our work.
dependsOn(processTestResTask)
}
testTask.dependsOn(copyNativeMegazord)
}
}
}
// Shared logic for projects that use UniFFI-generated bindings
//
// Make sure to also call dependsOnTheMegazord()
ext.configureUniFFIBindgen = { crateName ->
// This will store the uniffi-bindgen generated files for our component
def uniffiOutDir = layout.buildDirectory.dir("generated/uniffi/")
android {
sourceSets.main.kotlin.srcDirs += uniffiOutDir
}
// Call `uniffi-bindgen` to generate the Kotlin bindings
def generateUniffiBindings = tasks.register("generateUniffiBindings") {
def megazordNative = configurations.getByName("megazordNative")
doFirst {
def libraryPath = megazordNative.asFileTree.matching {
include "**/libmegazord.*"
}.singleFile
if (libraryPath == null) {
throw new GradleException("libmegazord dynamic library path not found")
}
exec {
workingDir project.rootDir
commandLine '/usr/bin/env', 'cargo', 'uniffi-bindgen', 'generate', '--library', libraryPath, "--crate", crateName, '--language', 'kotlin', '--out-dir', uniffiOutDir.get()
}
}
outputs.dir uniffiOutDir
// Re-generate when the native megazord library is rebuilt
inputs.files megazordNative
// Re-generate if our uniffi-bindgen tooling changes.
inputs.dir "${project.rootDir}/tools/embedded-uniffi-bindgen/"
}
afterEvaluate {
def megazordNative = configurations.getByName("megazordNative")
android.libraryVariants.all { variant ->
def variantName = variant.name.capitalize();
def compileTask = tasks["compile${variantName}Kotlin"]
compileTask.dependsOn(generateUniffiBindings)
variant.registerJavaGeneratingTask(generateUniffiBindings, megazordNative.singleFile)
}
}
}

Просмотреть файл

@ -8,6 +8,7 @@ android {
dependencies {
api libs.mozilla.concept.fetch
implementation project(":native-support")
}
ext.dependsOnTheMegazord()

Просмотреть файл

@ -24,10 +24,6 @@ android {
}
}
sourceSets {
test.jniLibs.srcDirs += "$buildDir/rustJniLibs/desktop"
}
// Uncomment to include debug symbols in native library builds.
// packagingOptions { doNotStrip "**/*.so" }
}
@ -36,21 +32,28 @@ kotlin {
jvmToolchain(rootProject.ext.build.jvmTargetCompatibility)
}
// Configurations are a somewhat mysterious Gradle concept. For our purposes, we can treat them
// sets of files produced by one component and consumed by another.
configurations {
// There's an interaction between Gradle's resolution of dependencies with different types
// (@jar, @aar) for `implementation` and `testImplementation` and with Android Studio's built-in
// JUnit test runner. The runtime classpath in the built-in JUnit test runner gets the
// dependency from the `implementation`, which is type @aar, and therefore the JNA dependency
// doesn't provide the JNI dispatch libraries in the correct Java resource directories. I think
// what's happening is that @aar type in `implementation` resolves to the @jar type in
// `testImplementation`, and that it wins the dependency resolution battle.
//
// A workaround is to add a new configuration which depends on the @jar type and to reference
// the underlying JAR file directly in `testImplementation`. This JAR file doesn't resolve to
// the @aar type in `implementation`. This works when invoked via `gradle`, but also sets the
// correct runtime classpath when invoked with Android Studio's built-in JUnit test runner.
// Success!
jnaForTest
// Native megazord library, this is the one compatible with the user's local machine. We use it
// to run unit tests.
consumable("megazordNative")
}
// Wrap the cargoBuild task to copy the native library to an output dir
//
// This allows it to be piped in to a Gradle configuration.
def cargoBuildNativeArtifacts = tasks.register("copyNativeMegazord", Copy) {
from layout.buildDirectory.dir("rustJniLibs/desktop")
into layout.buildDirectory.dir("nativeMegazord")
def nativeTarget = rootProject.ext.nativeRustTarget
dependsOn tasks["cargoBuild${nativeTarget.capitalize()}"]
}
artifacts {
// Connect task output to configurations
megazordNative(cargoBuildNativeArtifacts)
}
cargo {
@ -82,15 +85,6 @@ cargo {
extraCargoBuildArguments = rootProject.ext.extraCargoBuildArguments
}
dependencies {
jnaForTest(libs.jna) {
artifact {
extension ="jar"
type = "jar"
}
}
}
afterEvaluate {
// The `cargoBuild` task isn't available until after evaluation.
android.libraryVariants.all { variant ->
@ -104,9 +98,4 @@ afterEvaluate {
}
apply from: "$rootDir/publish.gradle"
// If using jnaForTestConfiguration or variantWithoutLib,
// please also update the corresponding .buildconfig-android.yml
// `publications` property.
ext.configurePublish(
/* jnaForTestConfiguration= */ configurations.jnaForTest,
)
ext.configurePublish()

Просмотреть файл

@ -9,16 +9,7 @@ def libProjectName = properties.libProjectName
def libUrl = properties.libUrl
def libVcsUrl = properties.libVcsUrl
// `jnaForTestConfiguration` is a hacky way to say yes, I'm using JNA and want
// to pack the JNA dispatch libraries and my Rust libraries into a single JAR
// for use in unit tests that run on a development host (and not an Android
// target device). We extract the JNA libraries and our local Rust libraries
// and stick them into a JAR that consumers augment their test configuration
// with.
//
// It's only used for megazords, for which it's required. Passing it in for a
// non-megazord is allowed, but will trigger a warning.
ext.configurePublish = { jnaForTestConfiguration = null ->
ext.configurePublish = {
def theGroupId = rootProject.ext.library.groupId
def theArtifactId = project.ext.artifactId
def theDescription = project.ext.description
@ -27,49 +18,6 @@ ext.configurePublish = { jnaForTestConfiguration = null ->
// we are already doing it inside taskcluster.
def isMegazord = theArtifactId.endsWith("-megazord")
// Do some sanity checks. The following properties should either all be
// true, or none of them should be true:
// - We're a megazord
// - jnaForTestConfiguration was provided
// - we should have 2 publish artifacts, [project, project-forUnitTests]
if (isMegazord != (jnaForTestConfiguration != null)) {
throw new GradleException("ext.configurePublish needs a `jnaForTestConfiguration` iff the project is a megazord")
}
if (isMegazord) {
task extractJnaResources(type: Sync) {
dependsOn jnaForTestConfiguration
from {
// Defer the resolution of the configuration. This helps to
// avoid a nasty issue with the Android-Gradle plugin 3.2.1,
// like `Cannot change attributes of configuration
// ':PROJECT:kapt' after it has been resolved`.
zipTree(jnaForTestConfiguration.singleFile)
}
into "${buildDir}/jnaResources/"
eachFile { FileCopyDetails fcp ->
// The intention is to just keep the various `*jnidispatch.*` files.
if (fcp.relativePath.pathString.startsWith("META-INFO") || fcp.relativePath.pathString.endsWith(".class")) {
fcp.exclude()
}
}
includeEmptyDirs false
}
def forUnitTestsJarTask = task forUnitTestsJar(type: Jar) {
from extractJnaResources
from "$buildDir/rustJniLibs/desktop"
}
project.afterEvaluate {
forUnitTestsJarTask.dependsOn(tasks["cargoBuild"])
}
}
publishing {
publications {
aar(MavenPublication) {
@ -129,47 +77,6 @@ ext.configurePublish = { jnaForTestConfiguration = null ->
}
}
}
if (isMegazord) {
forUnitTestsJar(MavenPublication) {
artifact tasks['forUnitTestsJar']
artifact file("${projectDir}/../DEPENDENCIES.md"), {
extension "LICENSES.md"
}
pom {
groupId = theGroupId
artifactId = "${theArtifactId}-forUnitTests"
description = theDescription
// For mavenLocal publishing workflow, increment the version number every publish.
version = rootProject.ext.library.version + (rootProject.hasProperty('local') ? '-' + rootProject.property('local') : '')
packaging = "jar"
licenses {
license {
name = libLicense
url = libLicenseUrl
}
}
developers {
developer {
name = 'Sync Team'
email = 'sync-team@mozilla.com'
}
}
scm {
connection = libVcsUrl
developerConnection = libVcsUrl
url = libUrl
}
}
// This is never the publication we want to use when publishing a
// parent project with us as a child `project()` dependency.
alias = true
}
}
}
}
@ -182,120 +89,3 @@ ext.configurePublish = { jnaForTestConfiguration = null ->
checkMavenArtifacts.dependsOn(checkFileSizeTask)
}
}
// A convenience function for configuring a `uniffi-bindgen` task, with appropriate dependency info.
// This will call `uniffi-bindgen` with the provided crate in order to generate Kotlin language
// bindings and include them in the source set for the project.
ext.configureUniFFIBindgen = { crateName ->
android.libraryVariants.all { variant ->
def uniffiGeneratedPath = "generated/source/uniffi/${variant.name}/java"
def megazordPathCandidates = [
"${project.rootDir}/target/release/libmegazord.so",
"${project.rootDir}/target/release/libmegazord.dylib",
"${project.rootDir}/target/release/libmegazord.dll"
]
def taskName = "generate${variant.name.capitalize()}UniFFIBindings";
def t = tasks.register(taskName) {
doLast {
def megazordPath = null
for (path in megazordPathCandidates) {
def file = new File(path)
if (file.exists()) {
megazordPath = path
break
}
}
if (megazordPath == null) {
throw new GradleException("libmegazord dynamic library path not found")
}
exec {
workingDir project.rootDir
commandLine '/usr/bin/env', 'cargo', 'uniffi-bindgen', 'generate', '--library', megazordPath, "--crate", crateName, '--language', 'kotlin', '--out-dir', "${buildDir}/${uniffiGeneratedPath}"
}
}
outputs.dir "${buildDir}/${uniffiGeneratedPath}"
// Re-generate when the megazord is rebuilt, which should happen if any components
// change
it.inputs.files megazordPathCandidates
// Re-generate if our uniffi-bindgen tooling changes.
inputs.dir "${project.rootDir}/tools/embedded-uniffi-bindgen/"
// Re-generate if our uniffi-bindgen version changes.
inputs.file "${project.rootDir}/Cargo.lock"
}
variant.registerJavaGeneratingTask(t.get(), new File(buildDir, uniffiGeneratedPath))
tasks[taskName].dependsOn(":full-megazord:assembleRelease")
}
}
// A convenience function for configuring a project to depend on the megazord
// for native code. It sets up the correct set of dependencies for publishing
// a package whose Rust code is included in the megazord, and for running its
// tests using a local build.
ext.dependsOnTheMegazord = {
// There's an interaction between Gradle's resolution of dependencies with different types
// (@jar, @aar) for `implementation` and `testImplementation` and with Android Studio's built-in
// JUnit test runner. The runtime classpath in the built-in JUnit test runner gets the
// dependency from the `implementation`, which is type @aar, and therefore the JNA dependency
// doesn't provide the JNI dispatch libraries in the correct Java resource directories. I think
// what's happening is that @aar type in `implementation` resolves to the @jar type in
// `testImplementation`, and that it wins the dependency resolution battle.
//
// A workaround is to add a new configuration which depends on the @jar type and to reference
// the underlying JAR file directly in `testImplementation`. This JAR file doesn't resolve to
// the @aar type in `implementation`. This works when invoked via `gradle`, but also sets the
// correct runtime classpath when invoked with Android Studio's built-in JUnit test runner.
// Success!
configurations {
jnaForTest
}
// Add the full-megazord's build directory to our resource path so that
// we can actually find it during tests. (Unfortunately, each project
// has their own build dir)
android {
sourceSets {
test.resources.srcDirs += "${project(':full-megazord').buildDir}/rustJniLibs/desktop"
}
}
// Depend on the megazord and its support library, as well as the
// above-mentioned JNA stuff for testing.
dependencies {
api project(":full-megazord")
implementation project(":native-support")
jnaForTest(libs.jna) {
artifact {
extension ="jar"
type = "jar"
}
}
implementation(libs.jna) {
artifact {
extension ="aar"
type = "aar"
}
}
// For reasons unknown, resolving the jnaForTest configuration directly
// trips a nasty issue with the Android-Gradle plugin 3.2.1, like `Cannot
// change attributes of configuration ':PROJECT:kapt' after it has been
// resolved`. I think that the configuration is being made a
// super-configuration of the testImplementation and then the `.files` is
// causing it to be resolved. Cloning first dissociates the configuration,
// avoiding other configurations from being resolved. Tricky!
testImplementation files(configurations.jnaForTest.copyRecursive().files)
}
// For running local tests, depend on a local native `cargo build` of the megazord.
// Unfortunately the `cargoBuild` tasks aren't available until after evaluation.
evaluationDependsOn(":full-megazord")
afterEvaluate {
android.libraryVariants.all { variant ->
def productFlavor = ""
variant.productFlavors.each {
productFlavor += "${it.name.capitalize()}"
}
def buildType = "${variant.buildType.name.capitalize()}"
def nativeCargoBuildTask = "cargoBuild${rootProject.ext.nativeRustTarget.capitalize()}"
tasks["process${productFlavor}${buildType}UnitTestJavaRes"].dependsOn(project(':full-megazord').tasks[nativeCargoBuildTask])
}
}
}