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:
Родитель
b8c4894e7e
Коммит
7bc69fe08a
|
@ -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()
|
||||
|
|
212
publish.gradle
212
publish.gradle
|
@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче