I have a Kotlin project (not Android) that uses the LWJGL library. Under macOS, I need to add the following options to build.gradle:
project.ext.lwjglNatives = "natives-macos"
applicationDefaultJvmArgs = ["-XstartOnFirstThread"]
dependencies {
implementation platform('org.jetbrains.kotlin:kotlin-bom')
implementation platform("org.lwjgl:lwjgl-bom:3.2.3")
implementation "org.lwjgl:lwjgl"
implementation "org.lwjgl:lwjgl-openal"
runtimeOnly "org.lwjgl:lwjgl::$lwjglNatives"
runtimeOnly "org.lwjgl:lwjgl-openal::$lwjglNatives"
}
On Windows, however, I need to drop applicationDefaultJvmArgs, and set lwjglNatives to:
project.ext.lwjglNatives = "natives-windows"
How can I tell gradle to do this? Basically I need some kind of target platform check.
Moreover, I need to know the target platform in Kotlin was well. How can I tell the build platform from Kotlin code?
I figured it out. The operating system can be tested like this:
import org.apache.tools.ant.taskdefs.condition.Os
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
project.ext.lwjglNatives = "natives-windows"
} else {
project.ext.lwjglNatives = "natives-macos"
applicationDefaultJvmArgs = ["-XstartOnFirstThread"]
}
To access this from Kotlin, a BuildConfig.java needs to be generated ad hoc:
tasks.register('generateSources') {
ext.outputDir = "$buildDir/generated/java"
outputs.dir outputDir
doFirst {
mkdir "$outputDir/ch/digorydoo/ksoundrender"
file("$outputDir/ch/digorydoo/ksoundrender/BuildConfig.java").text =
"""|package ch.digorydoo.ksoundrender;
|public class BuildConfig {
| public static Boolean isWindows() {
| return ${if (Os.isFamily(Os.FAMILY_WINDOWS)) "true" else "false"};
|}
|}""".stripMargin()
}
}
compileKotlin.dependsOn generateSources
sourceSets.main.java.srcDir generateSources.outputDir
Now I can just import BuildConfig from Kotlin.
I need to call the OpenJPA PCEnhancerTask class from Kotlin instead of Groovy. The following code works just fine (based on a previous solution documented here):
def openJPAClosure = {
def entityFiles = sourceSets.main.output.classesDirs.asFileTree.matching {
include 'com/company/persist/*Entity.class'
}
println "Enhancing with OpenJPA:"
entityFiles.getFiles().each {
println it
}
ant.taskdef(
name : 'openjpac',
classpath : sourceSets.main.runtimeClasspath.asPath,
classname : 'org.apache.openjpa.ant.PCEnhancerTask'
)
ant.openjpac(
classpath: sourceSets.main.runtimeClasspath.asPath,
addDefaultConstructor: false,
enforcePropertyRestrictions: true) {
entityFiles.addToAntBuilder(ant, 'fileset', FileCollection.AntType.FileSet)
}
}
I was looking at the documentation on how to call Ant tasks from Gradle but I could not translate all the necessary steps using the GroovyBuilder. So instead I tough of calling the PCEnhancer directly:
fun openJPAEnrich() {
val entityFiles = sourceSets.main.get().output.classesDirs.asFileTree.matching {
include("com/company/persist/*Entity.class")
}
println("Enhancing with OpenJPA, the following files...")
entityFiles.getFiles().forEach() {
println(it)
}
org.apache.openjpa.ant.PCEnhancerTask.main(asList(entityFiles))
}
But it complains about not being able to find org.apache.openjpa in the classpath (but is it listed as a compilation dependency)
My questions are:
What is the correct way to translate the original Groovy construct to Kotlin using groovyBuilder
If is not possible, how you can correctly call PCEnhancer from Kotlin in Gradle?
So I ended making it work with a custom JavaExec Gradle task:
tasks.create<JavaExec>("openJPAEnrich") {
val entityFiles = sourceSets.main.get().output.classesDirs.asFileTree.matching {
include("com/company/persist/*Entity.class")
}
println("Enhancing with OpenJPA, the following files...")
entityFiles.files.forEach() {
println(it)
}
classpath = sourceSets.main.get().runtimeClasspath
main = "org.apache.openjpa.enhance.PCEnhancer"
args(listOf("-enforcePropertyRestrictions", "true", "-addDefaultConstructor", "false"))
entityFiles.forEach { classFile -> args?.add(classFile.toString())}
}
I was tempted to build my own custom Gradle task but for this felt overkill.
Thanks.
--Jose
I have a package in my app and I'd like to go over all classes in that package to then generate some JSON schema automatically.
I'd like to create a gradle task and with some sort of build-time dependency would allow me to do:
tasks.register("my fancy task") {
doLast {
"my.package.name".readKotlinFiles().classes.forEach { klass ->
klass.properties["id"]... and do something here
}
}
}
How can you do such thing easily?
I was doing something similar, loading classes form the project, but not as a Gradle task or plugin but in Java application itself. To execute this logic as Gradle task / plugin I would do it in the following way:
First you generate jar package from your Kotlin project as a standard process of Kotlin-Gradle build.
As Gradle build is not a part of your project source code you have to load classes of the actual project with properly configured ClassLoader.
Then you can read those classes and generate the report.
Now that would translate into the pseudocode of a Gradle task:
tasks.register("processClasses") {
dependsOn tasks.named("assemble") // generate jar
doLast {
// Initiate classloader of assembled project
// Read target package from gradle properties or other kind of parameters
// Get list of all classes under the specific package
// Load the list of classes from classloader
// Execute custom logic upon the list of classes
// Generate report in specific format (JSON, YAML, HTML, ...)
}
}
I tried to validate the logic and created a POC as the following GitHub project. There are two flavors of build scripts:
Groovy build script is in master branch
Kotlin build script version is in kotlin branch. Note multiple
aspects should be improved and adapted to the specific goal you have
in mind (really curious actually). You are welcome to make any
suggestions, fork the code, do changes..
Excerpt of the code below:
// Pack all the dependencies into jar
tasks.jar {
from(configurations.runtimeClasspath.map { configuration ->
configuration.asFileTree.fold(files().asFileTree) { collection, file ->
if (file.isDirectory) collection else collection.plus(zipTree(file))
}
})
}
val packageToProcess: String by project
tasks.register("processClasses") {
group = "process"
description = "Process classes form a specific package"
dependsOn(tasks.named("assemble"))
doLast {
// Instantiate classloader from jar as you will need other dependencies for loading classes
val file: File = project.projectDir.toPath().resolve(tasks.jar.get().archiveFile.get().toString()).toFile()
println("Packaged Kotlin project jar file: $file")
val classloader: ClassLoader = URLClassLoader(arrayOf(file.toURI().toURL()))
// Iterate through all of the class files in the specified package
val packageToScan: String = packageToProcess.replace(".", File.separator)
val path: java.nio.file.Path = project.file("build/classes/kotlin/main/").toPath().resolve(packageToScan)
val classesFromSpecificPackage = Files.walk(path)
.filter {
Files.isRegularFile(it)
}
.map {
project.file("build/classes/kotlin/main/").toPath().relativize(it)
}
.map { it.toString().substring(0, it.toString().lastIndexOf(".")).replace(File.separator, ".") }
.map {
//println("Class = $it") // print class if necessary before loading it
classloader.loadClass(it)
}
// Do something with the classes form specific package
// Now just basic information is printed directly to the console
println("=======================================")
println("Classes from package: $packageToScan")
println("")
classesFromSpecificPackage.forEach {
println(" Class: ${it.canonicalName}")
val fields = it.declaredFields
for (element in fields) {
println(" - Declared field: $element")
}
println("")
}
println("=======================================")
}
}
I could use a helping hand converting victor trellos simple sourceSet extension to Kotlin DSL, I seem a bit lost.
Victor Trello has an extension on SourceSet named svg
project.extensions.create('victor', VictorPluginExtension)
// Add 'svg' as a source set extension
project.android.sourceSets.all { sourceSet ->
SourceDirectorySet sds = project.objects.sourceDirectorySet(sourceSet.name, "${sourceSet.name} svgs")
sourceSet.extensions.add('svg', sds)
}
This is how it is configured in Groovy:
// build.gradle:
android {
// Variant 1 with individual setup
sourceSets {
main {
svg.srcDir 'src/main/svg'
}
}
// Variant 2 handling all at once
sourceSets.all { sourceSet ->
svg.srcDir "src/${sourceSet.name}/svg"
}
}
// Plugin configuration of class com.trello.victor.VictorPluginExtension
victor {
svgDpi = 72
generateVectorDrawables = true
}
What would this look like in Kotlin DSL?
Here's the Victor Trello Gradle Plugin source
I notice that the Victor plugin extends SourceSet, and project.sourceSet has type SourceSet (from gradle) which contains an extensions, whereas AndroidSourceSet does not contain the .extensions. Also I can seem to get a gradle SourceSet from the AndroidSourceSet
This might be the right way to do the plugin configuration, though I cannot test it without the srcDir (I'll remove this from the question if its wrong, to avoid confusion for future readers).
// build.gradle.kts:
configure<com.trello.victor.VictorPluginExtension> {
svgDpi = 72
generateVectorDrawables = true
}
I found the solution with the help of several people (Google + Gradle forums). Hopefully this can help others with other plugins.
The solution had three parts to solve,
Configure a plugin with the configure()
Access Groovy extensions
Iterate sourceSets.all in kotlin (which is obvious if your not unlucky enough to start with sourceSets.all { sourceSet -> })
Here's the source
val Any.extensions get() = (this as org.gradle.api.plugins.ExtensionAware).extensions
android {
sourceSets {
// Variant 1 with individual setup
named("main") {
val a: com.android.build.api.dsl.AndroidSourceSet = this
java.srcDir("src/sharedTest/java")
}
}
// Variant 2 handling all at once
sourceSets.all {
val svgSourceSet = this.extensions["svg"] as SourceDirectorySet
svgSourceSet.srcDir("src/${name}/svg")
}
}
configure<com.trello.victor.VictorPluginExtension> {
// Any assets defined in relative terms needs a base DPI specified
svgDpi = 72
// Do not generate these densities for SVG assets
excludeDensities = listOf("ldpi", "xxxhdpi")
// Set this to "true" if you want to generate Android vectors instead of PNGs
generateVectorDrawables = false
}
i want to use aspectj aop in kotlin,here is my code:
my annotation in annotation.lazy_list:
Kotlin:
package anotation
#Retention(AnnotationRetention.RUNTIME)
#Target(AnnotationTarget.FUNCTION)
annotation class lazy_list
my aspectj aop class:
#Aspect
class ActiveListAop{
#Pointcut("execution(#annotation.lazy_list * *(..))")
fun profile() {
}
#Before("profile()")
fun testModeOnly(joinPoint: JoinPoint) {
println("123")
}
}
my usage:
#lazy_list
fun all():List<T>{
return lazy_obj?.all() as List<T>
}
when i call all() function , no error,but wont't print "123", why?
EDIT 9-2021 - there is a nice updated plugin for android that works well as an alternate to my original 2018 answer below: https://github.com/Ibotta/gradle-aspectj-pipeline-plugin
For what it's worth, we needed aspectJ weaving in our android project but really wanted to move to kotlin so we had to solve this problem. So the solutions in this thread using spring or maven didn't work for us. This is the solution for android gradle projects however, this WILL break incremental compilation and therefor slow down your build times and/or break something eventually. This gets us by until I can re-think our architecture and phase out aspectJ or (hopefully) android starts supporting it.
There is confusion in some of the answers and comments to the OP that kapt solves this, but kapt lets you do compile time annotation processing, not weaving. That is, annotation processors let you generate code based on annotations but do not let you inject logic into existing code.
This builds on top of this blog on adding aspectJ to android: https://fernandocejas.com/2014/08/03/aspect-oriented-programming-in-android
Your kotlin classes get compiled into byte code, just into a different directory. So this solution using the same process to weave the java classes but runs it again on the kotlin class files
at the top of your App/build.gradle add:
buildscript {
ext.aspectjVersion = '1.9.1'
dependencies {
classpath "org.aspectj:aspectjtools:$aspectjVersion"
}
}
At the bottom of your App/build.gradle add:
android.applicationVariants.all { variant ->
// add the versionName & versionCode to the apk file name
variant.outputs.all { output ->
def newPath = outputFileName.replace(".apk", "-${variant.versionName}.${variant.versionCode}.apk")
outputFileName = new File(outputFileName, newPath)
def fullName = ""
output.name.tokenize('-').eachWithIndex { token, index ->
fullName = fullName + (index == 0 ? token : token.capitalize())
}
JavaCompile javaCompile = variant.javaCompiler
MessageHandler handler = new MessageHandler(true)
javaCompile.doLast {
String[] javaArgs = ["-showWeaveInfo",
"-1.8",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(
File.pathSeparator)]
String[] kotlinArgs = ["-showWeaveInfo",
"-1.8",
"-inpath", project.buildDir.path + "/tmp/kotlin-classes/" + fullName,
"-aspectpath", javaCompile.classpath.asPath,
"-d", project.buildDir.path + "/tmp/kotlin-classes/" + fullName,
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(
File.pathSeparator)]
new Main().run(javaArgs, handler)
new Main().run(kotlinArgs, handler)
def log = project.logger
for (IMessage message : handler.getMessages(null, true)) {
switch (message.getKind()) {
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error message.message, message.thrown
break
case IMessage.WARNING:
case IMessage.INFO:
log.info message.message, message.thrown
break
case IMessage.DEBUG:
log.debug message.message, message.thrown
break
}
}
}
}
spring + kotlin + AOP work nice, just go to http://start.spring.io/ and generate a project with AOP support, you can see a piece of build.gradle here...
buildscript {
ext {
kotlinVersion = '1.2.30'
springBootVersion = '2.0.0.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}")
}
}
apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
apply plugin: 'org.springframework.boot'
...
dependencies {
compile('org.springframework.boot:spring-boot-starter-aop')
...
}
plugin kotlin-spring makes all classes open to allow AOP
Then, just declare your aspect as follows
#Aspect
#Component
class MyAspect {
...
Important: annotate your aspect class with #Aspect and #Component annotations
Piece of cake! :)
For annotation process in Kotlin, you must enable and use KAPT. Without this being added via Gradle or Maven plugin, nothing is going to work for annotation processing in Kotlin code.
The Kotlin plugin supports annotation processors like Dagger or DBFlow. In order for them to work with Kotlin classes, apply the kotlin-kapt plugin.
See also:
Pushing the limits of Kotlin annotation processing
kapt: Annotation Processing for Kotlin
Better Annotation Processing: Supporting Stubs in kapt
You can use freefair gradle plugin
buildscript {
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath "io.freefair.gradle:aspectj-plugin:5.2.1"
}
}
apply plugin: "io.freefair.aspectj.post-compile-weaving"
So I think I've got a good (but wordy) solution for Android. At the time of writing I'm using Gradle 6.7, Android plugin 4.1.0, and AspectJ tools 1.9.6.
The gist of the problem is that:
Java is compiled by task compileDebugJavaWithJavac
Kotlin is compiled by task compileDebugKotlin
Gradle can run either one of these tasks, or both of them, or none
compileDebugJavaWithJavac depends on compileDebugKotlin
weaving Kotlin usually requires Java classes.
If you look at these points closely, you'll see that you can't do weaving as part of compiling Kotlin, as Java classes can be missing at this point. If you do that, you'll get warnings such as:
WARN: incorrect classpath: C:\Users\user\StudioProjects\myapp\app\build\intermediates\javac\debug\classes
and errors such as
ERROR: can’t determine modifiers of missing type myapp.Foo.Bar
So the better approach would be to postpone weaving until Java classes are compiled. But as you would be modifying files not as a part of compilation task, you lose incremental builds... Besides, this postponed weaving is super hard to get right—remember, none of the compile tasks might be actually scheduled for running!
The real solution is to wrap weaving in a Transform, which will produce a Gradle task with its own inputs and outputs. This means that you will not be polluting the files of compile tasks, and those tasks, as well as this task, will be, so to say, UP-TO-DATE-able. This requires quite a bit of code, but it's rather sensible!
First, put this in your project build.gradle.kts:
buildscript {
dependencies {
classpath("org.aspectj:aspectjtools:1.9.6")
}
}
This is needed to run weaving from inside “inside” the buildscript. If you want to run weaving in a separate process, which is a good idea on Windows, you will need the path of this jar, which you can get by adding the following to your app build.gradle.kts:
val weaving: Configuration by configurations.creating
dependencies {
weaving("org.aspectj:aspectjtools:1.9.6")
}
Finally, put AspectJ runtime on the classpath (app build.gradle.kts, note that I only need weaving in debug builds):
dependencies {
debugImplementation("org.aspectj:aspectjrt:1.9.6")
}
Now, here's my setup. I have a local logging library, :cats, which containts aspects that I want to weave. Logging statements are only inside my project, and not anywhere else. Also, I only want to run these in debug builds. So here's the transformation that “weaves cats” into the app (app's build.gradle.kts):
class TransformCats : Transform() {
override fun getName(): String = TransformCats::class.simpleName!!
override fun getInputTypes() = setOf(QualifiedContent.DefaultContentType.CLASSES)
// only look for annotations in app classes
// transformation will consume these and put woven classes in the output dir
override fun getScopes() = mutableSetOf(QualifiedContent.Scope.PROJECT)
// ...but also have the rest on our class path
// these will not be touched by the transformation
override fun getReferencedScopes() = mutableSetOf(QualifiedContent.Scope.SUB_PROJECTS,
QualifiedContent.Scope.EXTERNAL_LIBRARIES)
override fun isIncremental() = false
// only run on debug builds
override fun applyToVariant(variant: VariantInfo) = variant.isDebuggable
override fun transform(invocation: TransformInvocation) {
if (!invocation.isIncremental) {
invocation.outputProvider.deleteAll()
}
val output = invocation.outputProvider.getContentLocation(name, outputTypes,
scopes, Format.DIRECTORY)
if (output.isDirectory) FileUtils.deleteDirectoryContents(output)
FileUtils.mkdirs(output)
val input = mutableListOf<File>()
val classPath = mutableListOf<File>()
val aspectPath = mutableListOf<File>()
invocation.inputs.forEach { source ->
source.directoryInputs.forEach { dir ->
input.add(dir.file)
classPath.add(dir.file)
}
source.jarInputs.forEach { jar ->
input.add(jar.file)
classPath.add(jar.file)
}
}
invocation.referencedInputs.forEach { source ->
source.directoryInputs.forEach { dir ->
classPath.add(dir.file)
}
source.jarInputs.forEach { jar ->
classPath.add(jar.file)
if (jar.name == ":cats") aspectPath.add(jar.file)
}
}
weave(classPath, aspectPath, input, output)
}
}
android.registerTransform(TransformCats())
And here's the weaving code mentioned above:
// ajc gets hold of some files such as R.jar, and on Windows it leads to errors such as:
// The process cannot access the file because it is being used by another process
// to avoid these, weave in a process, which `javaexec` will helpfully launch for us.
fun weave(classPath: Iterable<File>, aspectPath: Iterable<File>, input: Iterable<File>, output: File) {
val runInAProcess = OperatingSystem.current().isWindows
val bootClassPath = android.bootClasspath
println(if (runInAProcess) ":: weaving in a process..." else ":: weaving...")
println(":: boot class path: $bootClassPath")
println(":: class path: $classPath")
println(":: aspect path: $aspectPath")
println(":: input: $input")
println(":: output: $output")
val arguments = listOf("-showWeaveInfo",
"-1.8",
"-bootclasspath", bootClassPath.asArgument,
"-classpath", classPath.asArgument,
"-aspectpath", aspectPath.asArgument,
"-inpath", input.asArgument,
"-d", output.absolutePath)
if (runInAProcess) {
javaexec {
classpath = weaving
main = "org.aspectj.tools.ajc.Main"
args = arguments
}
} else {
val handler = MessageHandler(true)
Main().run(arguments.toTypedArray(), handler)
val log = project.logger
for (message in handler.getMessages(null, true)) {
when (message.kind) {
IMessage.DEBUG -> log.debug("DEBUG " + message.message, message.thrown)
IMessage.INFO -> log.info("INFO: " + message.message, message.thrown)
IMessage.WARNING -> log.warn("WARN: " + message.message, message.thrown)
IMessage.FAIL,
IMessage.ERROR,
IMessage.ABORT -> log.error("ERROR: " + message.message, message.thrown)
}
}
}
}
val Iterable<File>.asArgument get() = joinToString(File.pathSeparator)
(The Windows part is using weaving configuration; you may not want either part of the if)
This is it!
Edit: As of AGP 4.2.0, jar.name doesn't return anything useful. For the time being, I used this fragile workaround:
if (jar.file.directoriesInsideRootProject().contains("cats")) {
aspectPath.add(jar.file)
}
fun File.directoriesInsideRootProject() = sequence {
var file = this#directoriesInsideRootProject
while (true) {
yield(file.name)
file = file.parentFile ?: break
if (file == rootProject.projectDir) break
}
}