Using Pre-built Binaries
You can open this sample inside an IDE using the IntelliJ native importer or Eclipse Buildship. |
This sample shows how to use pre-built binaries in a Java Native Interface (JNI) library. There are several possible use cases. This sample will cover the most common for Java developers. It uses a pre-built shared library, say from a continuous integration environment, as a replacement for building the native shared library from source.
In this sample, the JNI library’s implementation languages are Java and C++; however, this applies to other JVM and native languages as well.
The plugin sources the native libraries for the JNI JAR from the native runtime file collection. The sample configures the file collection to include only the pre-built shared library if they are present. If the pre-built binaries are absent, the plugin will build the shared library from the source. Given the following build script:
plugins {
id 'java'
id 'dev.nokee.jni-library'
id 'dev.nokee.cpp-language'
}
import java.util.concurrent.Callable
import dev.nokee.runtime.nativebase.OperatingSystemFamily
String getLibraryFileNameFor(OperatingSystemFamily osFamily) {
if (osFamily.windows) {
return "${project.name}.dll"
} else if (osFamily.linux) {
return "lib${project.name}.so"
} else if (osFamily.macOS) {
return "lib${project.name}.dylib"
}
throw new GradleException("Unknown operating system family '${osFamily}'.")
}
library {
variants.configureEach {
def prebuiltLibraryFile = file("pre-built-library/${getLibraryFileNameFor(targetMachine.operatingSystemFamily)}")
if (prebuiltLibraryFile.exists()) { (1)
nativeRuntimeFiles.setFrom(prebuiltLibraryFile)
nativeRuntimeFiles.from(new CallableLogger({project.logger.warn("Using the pre-build library.")})) (2)
} else {
nativeRuntimeFiles.from(new CallableLogger({project.logger.warn("Building from the source.")})) (2)
}
}
}
/**
* A callable to log a message on the console only on the first call.
*/
class CallableLogger implements Callable<List<File>> {
private final Runnable logger
private boolean messageAlreadyLogged = false
CallableLogger(Runnable logger) {
this.logger = logger
}
@Override
List<File> call() throws Exception {
if (!messageAlreadyLogged) { (3)
logger.run()
messageAlreadyLogged = true
}
return Collections.emptyList()
}
}
plugins {
id("java")
id("dev.nokee.jni-library")
id("dev.nokee.cpp-language")
}
import java.util.concurrent.Callable
fun getLibraryFileNameFor(osFamily: dev.nokee.runtime.nativebase.OperatingSystemFamily): String {
if (osFamily.isWindows) {
return "${project.name}.dll"
} else if (osFamily.isLinux) {
return "lib${project.name}.so"
} else if (osFamily.isMacOs) {
return "lib${project.name}.dylib"
}
throw GradleException("Unknown operating system family '${osFamily}'.")
}
library {
variants.configureEach {
val prebuiltLibraryFile = file("pre-built-library/${getLibraryFileNameFor(targetMachine.operatingSystemFamily)}")
if (prebuiltLibraryFile.exists()) { (1)
nativeRuntimeFiles.setFrom(prebuiltLibraryFile)
nativeRuntimeFiles.from(CallableLogger({project.logger.warn("Using the pre-build library.")})) (2)
} else {
nativeRuntimeFiles.from(CallableLogger({project.logger.warn("Building from the source.")})) (2)
}
}
}
/**
* A callable to log a message on the console only on the first call.
*/
class CallableLogger(logger:Runnable) : java.util.concurrent.Callable<List<File>> {
private val logger:Runnable
private var messageAlreadyLogged = false
init {
this.logger = logger
}
// @Throws(Exception::class)
override fun call(): List<File> {
if (!messageAlreadyLogged) { (3)
logger.run()
messageAlreadyLogged = true
}
return listOf()
}
}
1 | We can use sharedLibrary.buildable as an alternate condition. |
2 | We wrap the logging inside a callable to only log when we will build the binary. |
3 | The CallableLogger ensure the log isn’t displayed more than once. |
By default, the project will build the shared library from the source:
$ ./gradlew assemble Building from the source. BUILD SUCCESSFUL 4 actionable tasks: 4 executed
We can validate the shared library was built as expected:
$ tree ./build/libs ./build/libs ├── jni-library-with-pre-built-binaries.jar └── main └── libjni-library-with-pre-built-binaries.dylib 1 directory, 2 files
Now, we can trigger the pre-built scenario. First, we copy the library we just built to the expected pre-built location. Then, we clean the project to remove any intermediate files. Finally, we launch the second build:
$ mv build/libs/main/* pre-built-library/ $ ./gradlew clean BUILD SUCCESSFUL 1 actionable task: 1 executed $ ./gradlew assemble Using the pre-build library. BUILD SUCCESSFUL 2 actionable tasks: 2 executed $ tree ./build/libs ./build/libs └── jni-library-with-pre-built-binaries.jar 0 directories, 1 file
This second build used the pre-built shared library instead of building it from the source. As we reconfigured the native runtime file collection to include only the pre-built shared library, Gradle didn’t build the library from the source. It would also be possible to write a custom task that downloads the pre-built binary from a known remote location. The download task would be part of the task graph by adding the output of the custom task to the file collection. It removes any manual steps required to source the pre-built binary.
For more information, see JNI Library Plugin, C++ Language Plugin, and Building JNI Projects chapters.