I have kotlin multiplatform project.
I know that I can read from infoDictionary (infoplist) data in runtime with:
val value = NSBundle.mainBundle.infoDictionary?.get("customValue")
My question, how can I add customValue to infoDictionary as part of build process, in build.gradle.kts of shared.
it.binaries.framework {
baseName = "shared"
embedBitcode("disable")
// here I want to infoDictionary.add("customValue", myValue)
}
Related
I created an IntelliJ plugin using the template https://github.com/JetBrains/intellij-platform-plugin-template. The template comes with a test that runs on an XML file. I want to create a similar test for a Kotlin file. Here's the template test file plus my added test (test2):
package org.jetbrains.plugins.template
import com.intellij.ide.highlighter.XmlFileType
import com.intellij.psi.xml.XmlFile
import com.intellij.testFramework.TestDataPath
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.intellij.util.PsiErrorElementUtil
#TestDataPath("\$CONTENT_ROOT/src/test/testData")
class MyPluginTest : BasePlatformTestCase() {
fun testXMLFile() {
val psiFile = myFixture.configureByText(XmlFileType.INSTANCE, "<foo>bar</foo>")
val xmlFile = assertInstanceOf(psiFile, XmlFile::class.java)
assertFalse(PsiErrorElementUtil.hasErrors(project, xmlFile.virtualFile))
assertNotNull(xmlFile.rootTag)
xmlFile.rootTag?.let {
assertEquals("foo", it.name)
assertEquals("bar", it.value.text)
}
}
override fun getTestDataPath() = "src/test/testData/rename"
fun testRename() {
myFixture.testRename("foo.xml", "foo_after.xml", "a2")
}
// Here's my test
fun test2() {
val fileText: String = """
package com.loganmay.test
data class MyClass(val myString: String)
""".trimIndent()
val psiFile = myFixture.configureByText("a.kt", fileText)
val xmlFile = assertInstanceOf(psiFile, XmlFile::class.java)
}
}
Without changing the build.gradle file, that test fails with:
Expected instance of: com.intellij.psi.xml.XmlFile actual: com.intellij.psi.impl.source.PsiPlainTextFileImpl
I want it to parse the text as a PsiFile that's also a KtFile. From various sources, I've been led to believe that the fixture is parsing it as a plain text file because the test project doesn't have access to the Kotlin compiler. So, I added:
dependencies {
testImplementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10")
}
to the build.gradle. Then, when I run the test, configureByText throws an exception with a big trace, the root exception of which is:
Caused by: java.lang.Throwable: 'filetype.archive.display.name' is not found in java.util.PropertyResourceBundle#4ecbb519(messages.CoreBundle)
... 53 more
org.jetbrains.plugins.template.MyPluginTest > test2 FAILED
com.intellij.diagnostic.PluginException at ComponentManagerImpl.kt:511
Caused by: java.util.MissingResourceException at Registry.java:164
Does anyone have any insight into what the issue is or know how to resolve it?
Notes:
I also tried importing the kotlin compiler and casting psiFile as KtFile, which produced the same error, an idea I got from here
This project has a test like this that may be working
This post and this post recommend adding the kotlin gradle plugin, which I did
This question seems similar
Yann Cebron replied on the jetbrains help forum with an answer for Java, which also worked for Kotlin.
The solution is to add a dependency to the IntelliJ gradle plugin. The template comes with these lines in the build.gradle:
intellij {
pluginName.set(properties("pluginName"))
version.set(properties("platformVersion"))
type.set(properties("platformType"))
// Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file.
plugins.set(properties("platformPlugins").split(',').map(String::trim).filter(String::isNotEmpty))
}
So, didn't need to do anything there. In my gradle.properties, I added
platformPlugins = com.intellij.java, org.jetbrains.kotlin
To my plugin.xml, I added:
<depends>com.intellij.modules.java</depends>
<depends>org.jetbrains.kotlin</depends>
I was able to remove
dependencies {
testImplementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10")
}
from the build.gradle which I mentioned above.
Now, the test works for Java and Kotlin files.
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
}
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
Is it possible to compile and instantiate Kotlin class at runtime? I'm talking about something like that but using Kotlin API: How do I programmatically compile and instantiate a Java class?
As example:
I'm getting full class definition as String:
val example = "package example\n" +
"\n" +
"fun main(args: Array<String>) {\n" +
" println(\"Hello World\")\n" +
"}\n"
And then inserting it into some class.kt and running it so I'm getting "Hello World" printed in console at runtime.
You might want to look at Kotlin Scripting, see https://github.com/andrewoma/kotlin-script
Alternatively, you'll need to write your own eval(kotlin-code-string-here) method which will dump the text inside blah.kt file for example, compile it using an external Kotlin compiler into blah.class then dynamically load those classes into the runtime using the Java Classloader doing something like this:
MainClass.class.classLoader.loadClass("com.mypackage.MyClass")
This might be very slow and unreliable.
Another no so great option is to make use of Rhino and run JavaScript inside your Kotlin code. So once again, you'll have an eval(kotlin-code-string-here) method which will dump the content to a blah.kt file, then you would use a Kotlin2JS compiler to compile it to JavaScript and directly execute the JavaScript inside Kotlin using Rhino which is not great either.
Another option is to make use of Kotlin Scripting or an external Kotlin compiler (in both cases, the Kotlin compiler will have to start up) and doing something like this will also allow you to execute dynamically, albeit, only on Unix systems.
Runtime.getRuntime().exec(""" "kotlin code here" > blah.kts | sh""")
I'm not aware of a clean solution for this, Kotlin was not designed to be run like like PHP / JavaScript / Python which just interprets text dynamically, it has to compile to bytecode first before it can do anything on the JVM; so in each scenario, you will need to compile that code first in one way or another, whether to bytecode or to javascript and in both cases load it into you application using the Java Classloader or Rhino.
Please check this solution for dependencies, jar resources, etc. Code below isn't enough for successful execution.
However, to compile dynamic class you can do the following:
val classLoader = Thread.currentThread().contextClassLoader
val engineManager = ScriptEngineManager(classLoader)
setIdeaIoUseFallback() // hack to have ability to do this from IntelliJ Idea context
val ktsEngine: ScriptEngine = engineManager.getEngineByExtension("kts")
ktsEngine.eval("object MyClass { val number = 123 } ")
println(ktsEngine.eval("MyClass.number"))
Please note: there is code injection possible here. Please be careful and use dedicated process or dedicated ClassLoader for this.
KotlinScript can be used to compile Kotlin source code (e.g. to generate a jar file that can then be loaded).
Here's a Java project which demonstrates this (code would be cleaner in Kotlin):
https://github.com/alexoooo/sample-kotlin-compile/blob/main/src/main/java/io/github/alexoooo/sample/compile/KotlinCompilerFacade.java
Note that the code you provide would be generated as a nested class (inside the script).
Here is a Kotlin version:
#KotlinScript
object KotlinDynamicCompiler {
//-----------------------------------------------------------------------------------------------------------------
const val scriptClassName = "__"
const val classNamePrefix = "${scriptClassName}$"
private val baseClassType: KotlinType = KotlinType(KotlinDynamicCompiler::class.java.kotlin)
private val contextClass: KClass<*> = ScriptCompilationConfiguration::class.java.kotlin
//-----------------------------------------------------------------------------------------------------------------
fun compile(
kotlinCode: String, outputJarFile: Path, classpathLocations: List<Path>, classLoader: ClassLoader
): String? {
Files.createDirectories(outputJarFile.parent)
val scriptCompilationConfiguration = createCompilationConfigurationFromTemplate(
baseClassType, defaultJvmScriptingHostConfiguration, contextClass
) {
jvm {
val classloaderClasspath: List<File> = classpathFromClassloader(classLoader, false)!!
val classpathFiles = classloaderClasspath + classpathLocations.map { it.toFile() }
updateClasspath(classpathFiles)
}
hostConfiguration(ScriptingHostConfiguration (defaultJvmScriptingHostConfiguration) {
jvm {
compilationCache(
CompiledScriptJarsCache { _, _ ->
outputJarFile.toFile()
}
)
}
})
}
val scriptCompilerProxy = ScriptJvmCompilerIsolated(defaultJvmScriptingHostConfiguration)
val result = scriptCompilerProxy.compile(
kotlinCode.toScriptSource(KotlinCode.scriptClassName), scriptCompilationConfiguration)
val errors = result.reports.filter { it.severity == ScriptDiagnostic.Severity.ERROR }
return when {
errors.isEmpty() -> null
else -> errors.joinToString(" | ")
}
}
}