TurnKey Gradle Plugin
January 7, 2025 ยท View on GitHub
TurnKey Gradle Plugin
This is a helper plugin for transforming existing libraries into TurnKey bundles. For a detailed discussion of TurnKey bundles, see the TurnKey Support Library documentation. The plugin provides two core features: binary rewriting and source file rewriting.
Using the Plugin
The plugin is published on Maven Central, not the Gradle Plugin Portal. Add to your
settings.gradle.kts:
pluginManagement {
repositories {
gradlePluginPortal()
mavenLocal()
}
}
then import the plugin in the build.gradle.kts via
plugins {
id("tools.aqua.turnkey") version "<version>"
}
When applied, the plugin does nothing. You must add tasks from the plugin to your project to handle your specific build scenario.
Binary Rewriting
Binary rewriting operates on bundles of libraries for a specific platform. Each bundle is analyzed
as a whole and transformed into TurnKey-linked artifacts. The plugin automatically generates a
turnkey.xml descriptor file for the bundle. The task is applied like this:
val turnkeyLinuxAMD64 by
tasks.registering(ELFTurnKeyTask::class) {
libraries.from(myBinaries).filter { it.isFile }
rootLibraryNames.add("libexamplejava.so")
targetDirectory = layout.buildDirectory.dir("turnkey/linux-amd64")
targetSubPath = "com/acme/example/linux/amd64"
}
Note that the task class is specific to the binary format (in this example, ELF). TurnKey tasks offer the following properties:
libraries(required): must be set to the library files to be analyzed. Do not include duplicates, non-library files, or directories.rootLibraryNames(required): the libraries that are required by your Java code. This is often a specific JNI library. All dependencies of this library are automatically included by the plugin.targetDirectory(required): the base output directory for the transformed files and theturnkey.xml.targetSubPath(optional): when set, this suffix is added to the target directory for placement. This is a convenience that allows introducing a package structure while allowing a subsequent task to add thetargetDirectoryto its inputs while maintaining Gradle dependency tracking.
At the moment, tasks for COFF (Windows), ELF (Linux), and Mach-O (macOS) libraries exist. These
tasks require external, native programs to be installed. Native program lookup is designed to handle
most installation naming schemes by searching the following naming schemes for the program example
in each directory on the $PATH:
- try
example - for each defined file name extension
.e in$PATHEXT, tryexample.e (required for.exeon Windows) - try
example-n, where n is a numeric version (required for multiple LLVM versions on Debian); in case of conflicts, the highest version is chosen - try
llvm-example(required for LLVM reimplementations of tools) - try
llvm-example-n (required for multiple LLVM reimplementaion version on Debian); in case of conflicts, the highest version is chosen
COFF Support
COFF libraries can be handled by the COFFTurnKeyTask. This task requires a readobj binary to be
installed. Both the GNU project and the LLVM project provide implementations. Since on Windows, no
relative linkage can be specified, this task only analyzes the dependency graph and generates
explicit load instructions for all dependents. No rewriting is performed.
ELF Support
ELF libraries can be handled by the ELFTurnKeyTask. This task requires a patchelf binary to be
installed, which is maintained by the NixOS project. This sets each library's RPath to origin
linkage (i.e., same-directory lookup). Only the root libraries need to be loaded.
Mach-O Support
Mach-O libraries can be handled by the MachOTurnKeyTask. This task requires both a
install_name_tool (alternatively: install-name-tool) and a otool binary to be installed. Both
Apple and the LLVM project provide implementations. This sets each bundled dependency's linkage to
loader-relative paths (i.e., same-directory lookup). Only the root libraries need to be loaded.
Java Rewriting
Java rewriting operates on single files via JavaParser. It is designed to insert calls to the TurnKey support library into an existing native wrapper that does not use the TurnKey abstraction. A transformation can be defined like this:
val rewriteNativeJava by
tasks.registering(JavaRewriteTask::class) {
inputDirectory = myJava.flatMap { it.outputDir }
inputFile = "com/acme/example/Native.java"
rewrite { compilationUnit ->
val nativeClass = compilationUnit.types.single { it.name.id == "Native" }
val staticInitializer =
nativeClass.members
.filterIsInstance<InitializerDeclaration>()
.first(InitializerDeclaration::isStatic)
staticInitializer.body =
BlockStmt(
NodeList(
ExpressionStmt(
MethodCallExpr(
"tools.aqua.turnkey.support.TurnKey.load",
StringLiteralExpr("com/acme/example"),
MethodReferenceExpr(
ClassExpr(parseClassOrInterfaceType("com.acme.example.Native")),
NodeList(),
"getResourceAsStream")),
)))
}
outputDirectory = layout.buildDirectory.dir("generated/rewritten")
}
The task offers the following properties:
inputDirectory(required): must be set to a directory containing Java source files.inputFile(required): is resolved relative to theinputDirectoryto locate the file to rewrite. This is a convenience that allows depending on a previous task's outputs via theinputDirectorywhile maintaining Gradle dependency tracking.rewrite(required): a function that accepts a JavaParserCompilationUnitand applies the desired transformation. For convenience, this can be set via a function, as shown above. The example above replaces a static initializer with a call to the TurnKey support library. Important: the lambda assigned to this variable is subjected to Java serialization by Gradle for rebuild elision purposes. If the lambda captures any non-serializable object (e.g., the containing task object), runtime errors will result!outputDirectory(required): the base output directory for the transformed Java file.outputFile(optional): is resolved relative to theoutputDirectoryto locate the rewritten file (defaults to theinputFile). This is a convenience that allows maintaining a package structure while allowing a subsequent task to add theoutputDirectoryto its inputs while maintaining Gradle dependency tracking.
Java and Gradle
The plugin requires Java 17 (minimum for Gradle 9+) and is linked against the Gradle 8.10.1 stack.
License
The plugin and other non-runtime code are licensed under the Apache Licence, Version 2.0. Standalone documentation is licensed under the Creative Commons Attribution 4.0 International License. Dependencies use other open-source licenses.