Move version number to a variable in Gradle Kotlin DSL - kotlin

My build.gradle.kts contains my dependencies like this:
dependencies {
implementation("io.insert-koin:koin-core:3.1.6")
implementation("io.insert-koin:koin-test:3.1.6")
testImplementation(kotlin("test"))
}
How can I move the 3.1.6 to a local variable (?) so I can avoid duplicating it in several places.

If you just want it local, you can add a value to your dependencies block:
dependencies {
val koinVersion = "3.1.6"
implementation("io.insert-koin:koin-core:$koinVersion")
implementation("io.insert-koin:koin-test:$koinVersion")
testImplementation(kotlin("test"))
}
If you want to use it multiple spots, you can add an extra value in your project's build.gradle.kts file:
val koinVersion by extra { "3.1.6" }
then in the app's build.gradle.kts file, you import it before using it:
val koinVersion: String by rootProject.extra
dependencies {
implementation("io.insert-koin:koin-core:$koinVersion")
implementation("io.insert-koin:koin-test:$koinVersion")
testImplementation(kotlin("test"))
}

Related

gradle kotlin dsl: copy project dependencies

I can copy all dependencies of a module in a multimodule gradle project with a task like
tasks.register<Sync>("copyResources") {
from(configurations.runtimeClasspath)
into(layout.buildDirectory.dir("extraResources"))
}
But actually, I only need to copy the project dependencies by applying a filter with project group id.
Something like
tasks.register<Sync>("copyResources") {
from(configurations.runtimeClasspath) {
include {
group "this.project.group" // NOT WORKING.
}
}
into(layout.buildDirectory.dir("extraResources"))
}
What is the right way to do this in Gradle with Kotlin DSL?
The following works.
But I feel there must be a simpler solution.
This solution uses regular expressions to filter out dependencies. The regex match is true if the absolute path of the dependency file contains the multimodule project root in it.
val copyImplemtations by configurations.creating {
extendsFrom(configurations.implementation.get())
}
tasks.register<Sync>("copyResources") {
val regex = Regex(".*[\\\\/]kotlin-spring-demo[\\\\/].*")
from(copyImplemtations.filter { regex.matches(it.absolutePath) })
into(layout.buildDirectory.dir("extraResources"))
}

How to process Kotlin code in Gradle KTS?

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("=======================================")
}
}

Using a Custom SourceSet in Gradle

According to this from the official docs and this SO answer, defining a new source set should be as simple as (in kotlin):
sourceSets {
create("demo") {
compileClasspath += sourceSets.main.get().output
}
}
The second reference also explicitly claims this can now be used in building a jar. However, while the above does not throw an error, actually trying to use the new source set anywhere does. Minimal example (generated with gradle init -> "kotlin application" and project name "foobar"):
plugins {
id("org.jetbrains.kotlin.jvm").version("1.3.21")
application
}
repositories {
jcenter()
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
}
application {
mainClassName = "foobar.AppKt"
}
sourceSets {
create("demo") {
compileClasspath += sourceSets.main.get().output
}
}
I've removed configuration of the test source set since it isn't relevant here. The src directory looks like this (which is a bit non standard, but I think fine for the discussion):
src
├── demo
│   └── Demo.kt
└── main
└── foobar
└── App.kt
Ideally, I'd like to have completely separate source trees (but this question is not specifically about that). What this builds makes sense in relation to the configuration above, but it is not really the goal. To do that, I want to add a custom jar or two.1
Unfortunately though, the demo source set can't be referenced in the gradle script.
val jar by tasks.getting(Jar::class) {
manifest { attributes["Main-Class"] = "DemoKt" }
// HERE IS THE PROBLEM:
from(sourceSets["demo"].get().output)
dependsOn(configurations.runtimeClasspath)
from({
configurations.runtimeClasspath.get().filter { it.name.endsWith("jar") }.map { zipTree(it) }
})
}
This fat jar task definition is taken from the gradle docs and I've used much before.
As is, gradle chokes:
Script compilation errors:
Line 28: from(sourceSets["demo"].get().output)
^ Unresolved reference.
If I change that line to:
from(sourceSets.demo.get().output)
Which is the syntax used with sourceSets.main, then the error becomes:
Line 28: from(sourceSets.demo.get().output)
^ Unresolved reference: demo
How am I supposed to work with the custom source set?
I am aware of the gradle subproject pattern and have used it to do much the same thing, but I'd prefer the simple separate source sets solution, which does I think meet all three criteria for such mentioned in that doc.
The sourceSets property (an extension property for Project) returns a SourceSetContainer which extends NamedDomainObjectContainer<SourceSet>. The latter interface defines get(String) (an operator extension function) which is what allows you to use the ["..."] syntax. That method returns T directly, meaning you should just be using:
from(sourceSets["demo"].output)
The reason you have to use get() when using sourceSets.main is because you're getting a NamedDomainObjectProvider<SourceSet> which wraps the source set.
You can also get the custom source set via delegation
When creating it:
val demo by sourceSets.creating { /* configure */ }
Or some time after creating it:
val demo by sourceSets

Adds an implementation dependency only to the "free" product flavor in Kotlin DSL

I am in process of migrating our Groovy based scripts to Kotlin. I have been able to get most of done except not sure how to add a dependency for a particular flavour.
This is how looks like in Kotlin DSL so far but not sure why the freeImplementation("bar:2.2.8")
productFlavors {
create("free") {
...
...
}
create("paid") {
...
...
}
}
dependencies {
implementation("foo:1.2.0")
// This is not working when migrated to Kotlin DSL
freeImplementation("bar:2.2.8")
//Below was the code in Groovy which was working fine earlier
//freeImplementation "bar:2.2.8"
}
Below is the solution for it.
val freeImplementation by configurations
dependencies {
freeImplementation("bar:2.2.8")
}
Alternatively, a string literal can be used to denote a dynamic configuration:
dependencies {
"freeImplementation"("bar:2.2.8")
}

Aspectj doesn't work with kotlin

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
}
}