Our main task is to make a GUI application on Windows (and preferably on Mac too) so it would be impossible/extremely difficult to find out the source code (Kotlin). Question is = how to do it?
Ideally, I would like to know:
.1. Tools to get the source code from our executable/files executable depends on.
.2. Tools for code obfuscation to the level where the tools of the previous point won't work.
.3. Next section is about what I did in Compose Desktop, but I do not limit myself to it. If you, for example, know how to make a working project with encrypted Kotlin code in JavaFx or something else, please let me know.
What has been done so far:
In Compose Desktop generated a .msi file, which on Windows creates files, and among them there are .jar files.
I opened one of the .jar in here and there was it = my beautiful source code, although in Java, but even the variables were not renamed!
Next I changed build.gradle.kts according to template
Important note = I do not understand this template. Perhaps I did something wrong.
I'll added the code, take a look.
But at least .msi got built and it installed the rest as previously. And again the same story = I open the .jar and see the source code in Java with variables not renamed!
But again = not even sure if this template is supposed to make the code in .jar unreadable.
Note = never done/learned custom obfuscation before. I work mainly with Kotlin and Android Studio. For Compose Desktop installed IntelliJ Idea.
Code:
build.gradle.kts
import org.jetbrains.compose.compose
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
buildscript {
dependencies {
classpath("com.guardsquare:proguard-gradle:7.2.1")
}
}
repositories {
google()
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
plugins {
kotlin("jvm") version "1.6.10"
id("org.jetbrains.compose") version "1.1.1"
}
dependencies {
implementation(compose.desktop.currentOs)
}
val obfuscate by tasks.registering(proguard.gradle.ProGuardTask::class)
fun mapObfuscatedJarFile(file: File) =
File("${project.buildDir}/tmp/obfuscated/${file.nameWithoutExtension}.min.jar")
compose.desktop {
application {
mainClass = "MainKt"
nativeDistributions {
targetFormats(TargetFormat.Exe, TargetFormat.Msi, TargetFormat.Deb)
packageName = "untitled_04"
packageVersion = "1.0.0"
}
}
}
obfuscate.configure {
dependsOn(tasks.jar.get())
val allJars = tasks.jar.get().outputs.files + sourceSets.main.get().runtimeClasspath.filter { it.path.endsWith(".jar") }
.filterNot { it.name.startsWith("skiko-awt-") && !it.name.startsWith("skiko-awt-runtime-") } // walkaround https://github.com/JetBrains/compose-jb/issues/1971
for (file in allJars) {
injars(file)
outjars(mapObfuscatedJarFile(file))
}
libraryjars("${compose.desktop.application.javaHome ?: System.getProperty("java.home")}/jmods")
configuration("proguard-rules.pro")
}
proguard-rules.pro
-keepclasseswithmembers public class MainKt {
public static void main(java.lang.String[]);
}
-dontwarn kotlinx.coroutines.debug.*
-keep class kotlin.** { *; }
-keep class kotlinx.coroutines.** { *; }
-keep class org.jetbrains.skia.** { *; }
-keep class org.jetbrains.skiko.** { *; }
-assumenosideeffects public class androidx.compose.runtime.ComposerKt {
void sourceInformation(androidx.compose.runtime.Composer,java.lang.String);
void sourceInformationMarkerStart(androidx.compose.runtime.Composer,int,java.lang.String);
void sourceInformationMarkerEnd(androidx.compose.runtime.Composer);
}
Related
I'm trying to build convention plugins for a common configuration, e.g. ktlint. They are stored under build-configuration/plugin/convention.
class KtlintConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
return with(target) {
pluginManager.apply(deps.plugins.ktlint.get().pluginId)
extensions.configure<KtlintExtension> {
// ...
}
}
}
}
build-configuration/plugin/convention/build.gradle.kts
plugins {
`kotlin-dsl`
}
group = "com.example"
dependencies {
compileOnly(deps.kotlin.jvm.gradle.plugin)
compileOnly(deps.ktor.gradle.plugin)
compileOnly(deps.ktlint.gradle.plugin)
compileOnly(files(deps.javaClass.superclass.protectionDomain.codeSource.location))
}
gradlePlugin {
plugins {
register("ktlint-plugin") {
id = "ktlint-plugin"
implementationClass = "KtlintConventionPlugin"
}
}
}
I also have an extension function to get dependencies defined in TOML file (stored in gradle/deps.versions.toml) in build-configuration/plugin/convention/src/main/kotlin/com/example/VersionCatalog.kt
import org.gradle.accessors.dm.LibrariesForDeps
import org.gradle.api.Project
import org.gradle.kotlin.dsl.the
internal val Project.deps: LibrariesForDeps
get() = the()
build-configuration/settings.gradle.kts
dependencyResolutionManagement {
repositories {
gradlePluginPortal()
mavenLocal()
mavenCentral()
}
versionCatalogs {
create("deps") {
from(files("../gradle/deps.versions.toml"))
}
}
}
rootProject.name = "build-configuration"
include(":plugin:convention")
When I include ktlint plugin in root build.gradle.kts, Gradle synchronization always fails.
An exception occurred applying plugin request [id: 'ktlint-plugin']
> Failed to apply plugin 'ktlint-plugin'.
> Type org.gradle.accessors.dm.LibrariesForDeps not present
I can see the class generated in .gradle/.../LibrariesForDeps, but under the project's root, not the plugin's one.
The whole build-configuration module is included in root settings.gradle.kts:
includeBuild("build-configuration")
Using the libs doesn't work in convention plugins, but there are some workarounds in the issue: https://github.com/gradle/gradle/issues/15383
But I don't think you need it. Instead the version can be defined in a single place: the included build's build.gradle.kts, which has access to the version catalog DSL.
define the Maven coordinates of the ktlint plugin in libs.versions.toml
[libraries]
gradlePlugin-ktlint = { module = "org.jlleitschuh.gradle:ktlint-gradle", version = "11.1.0" }
I prefer prefixing such dependencies with gradlePlugin to distinguish them from 'regular' project dependencies.
(The Maven coordinates are listed in the Gradle Plugin Portal, under the 'legacy' application as a classpath dependency)
Add a dependency on ktlint in the included build, build-configuration/build.gradle.kts
plugins {
`kotlin-dsl`
}
dependencies {
implementation(libs.deps.gradlePlugin.ktlint)
}
Now the ktlint plugin will be available on the classpath, and you can apply it as normal in a convention plugin, no version necessary.
In a precompiled script plugin:
// ./build-configuration/src/main/kotlin/my-ktlint-convention.gradle.kts
plugins {
id("org.jlleitschuh.gradle.ktlint") // version is provided by build.gradle.kts
}
Or in a traditional class plugin (which will require registering via gradlePlugins {}):
// ./build-configuration/src/main/kotlin/KtlintConventionPlugin.kt
class KtlintConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.apply(plugin = "org.jlleitschuh.gradle.ktlint")
target.extensions.configure<KtlintExtension> {
// ...
}
}
}
See also
For more information about buildSrc convention plugins, these answers are related:
https://stackoverflow.com/a/71562588/4161471
https://stackoverflow.com/a/71892685/4161471
I have created a new project and followed this approach. There is only one difference. I had to have the block below in both settings.gradle.kts files, root, and build-configuration:
dependencyResolutionManagement {
versionCatalogs {
create("deps") {
from(files("../gradle/deps.versions.toml"))
}
}
}
It just doesn't work for me without this duplication.
Also, I'm not using any module within the build-configuration plugin.
Here is my gradle.
plugins {
// ...
id "com.google.protobuf" version "0.8.12"
}
dependencies {
// DataStore
implementation "androidx.datastore:datastore-core:1.0.0"
// Architectural Components
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
// Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.4.0'
// Coroutine Lifecycle Scopes
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
// ...
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.10.0"
}
// Generates the java Protobuf-lite code for the Protobufs in this project. See
// https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
// for more information.
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}
I make the proto folder to under src/main.
And then, create proto file to src/main/proto,
It show the [Register New File Type Association] like this.
If there's anything I can add, please let me know.
It was my mistake. I just install proto plugin to android studio.
I am new to gradle. I am working on a kotlin library that requires JavaFX. I wanted to use gradle to manage my dependencies and make it easier to include my library in a new project.
My library project applies and configures the javafx plugin and its build file looks like this (kotlin dsl):
plugins {
`java-library`
id("org.openjfx.javafxplugin")
}
group = "me.shai"
version = "1.0"
javafx {
version = "15.0.1"
modules = listOf("javafx.controls", "javafx.fxml")
}
dependencies {
// Other dependencies
testImplementation(kotlin("test-junit5"))
testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.0")
}
tasks.test {
useJUnitPlatform()
}
kotlin {
explicitApi()
}
Apparently, a project that wants to use my library also has to apply and configure JavaFX. Here's an example for a build file of a demo project that consumes my library:
plugins {
id("org.openjfx.javafxplugin")
}
group = "me.shai"
version = "1.0"
javafx {
version = "15.0.1"
modules = listOf("javafx.controls", "javafx.fxml")
}
dependencies {
implementation(project(":MyLib")) // Pretend the library is published so we use a url instead of local project
}
This is slightly annoying, so I decided to do something similar to JavaFX: write a custom plugin to include my project. The plugin needs to:
Add my library as a dependency
Apply the JavaFX plugin
Configure the JavaFX plugin
I managed to achieve steps 1 and 2, but I am stuck at step 3. How can I configure the JavaFX plugin from a custom plugin kotlin class?
Here's what I have so far:
package mylib.gradle
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.DependencyResolutionListener
import org.gradle.api.artifacts.ResolvableDependencies
import org.gradle.kotlin.dsl.project
class MyLibPlugin : Plugin<Project> {
override fun apply(project: Project) {
// Apply the javafx plugin
project.plugins.apply("org.openjfx.javafxplugin")
// How to configure javafx here?
val implementationDeps = project.configurations.getByName("implementation").dependencies
project.gradle.addListener(object : DependencyResolutionListener {
override fun beforeResolve(dependencies: ResolvableDependencies) {
// Add dependency on my library project.
implementationDeps.add(project.dependencies.project(":MyLib"))
project.gradle.removeListener(this)
}
override fun afterResolve(dependencies: ResolvableDependencies) {}
})
}
}
I am basically trying to write the equivalent of the build script block:
javafx {
version = "15.0.1"
modules = listOf("javafx.controls", "javafx.fxml")
}
And I can't figure out how to achieve this. I'd appreciate any help on the subject.
You would configure the javafx extension just as you would in the build file:
// build.gradle[.kts]
dependencies {
implementation("org.openjfx:javafx-plugin:0.0.10")
}
And the plugin implementation:
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.JavaPlugin
import org.openjfx.gradle.JavaFXOptions
open class MyLibPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.pluginManager.apply("org.openjfx.javafxplugin")
val myLibDependency = project.dependencies.project(mapOf("path" to ":MyLib"))
project.dependencies.add(JavaPlugin.IMPLEMENTATION_CONFIGURATION_NAME, myLibDependency)
project.extensions.configure(JavaFXOptions::class.java) {
it.version = "15.0.1"
it.modules = listOf("javafx.controls", "javafx.fxml")
}
}
}
We have a fairly standard Kotlin DSL Gradle build. We've added an integrationTest sourceSet and task:
plugins {
kotlin("jvm") version "1.3.72"
application
}
sourceSets {
create("integrationTest") {
compileClasspath += sourceSets.main.get().output
runtimeClasspath += sourceSets.main.get().output
compileClasspath += sourceSets.test.get().output
compileClasspath += sourceSets.main.get().runtimeClasspath
runtimeClasspath += sourceSets.test.get().output
resources.srcDir(sourceSets.test.get().resources.srcDirs)
}
}
val integrationTest = task<Test>("integrationTest") {
description = "Runs the integration tests."
group = "verification"
testClassesDirs = sourceSets["integrationTest"].output.classesDirs
classpath = sourceSets["integrationTest"].runtimeClasspath
mustRunAfter(tasks.test)
useJUnitPlatform()
}
Classes in src/integrationTest/kotlin can use classes from src/test/kotlin just fine, but annotations defined in src/test/kotlin do not show up in reflection data for classes in src/integrationTest/kotlin. When used on classes in src/test/kotlin, the annotations are present in reflection data as expected.
The annotations are very simple:
#Target(FUNCTION, CLASS)
// NB: Default Retention is RUNTIME (Annotation is stored in binary output and visible for reflection)
annotation class SystemProperty(val key: String, val value: String)
// Kotlin does not yet support repeatable annotations https://youtrack.jetbrains.com/issue/KT-12794
#Target(FUNCTION, CLASS)
annotation class SystemProperties(vararg val systemProperties: SystemProperty)
This is how the annotations are used, in a JUnit 5 Extension:
class SystemPropertyExtension : BeforeAllCallback {
override fun beforeAll(extensionContext: ExtensionContext) {
val clazz = extensionContext.requiredTestClass
clazz.getAnnotation(SystemProperty::class.java)?.let {
System.setProperty(it.key, it.value)
}
clazz.getAnnotation(SystemProperties::class.java)?.let {
it.systemProperties.forEach { prop -> System.setProperty(prop.key, prop.value) }
}
}
}
And typical use on the test itself:
#SystemProperty(key = "aws.s3.endpoint", value = "http://localstack:4566")
#ExtendWith(SystemPropertyExtension::class)
class SomeIntegrationTest {
//
}
Setting breakpoints while running tests shows System.setProperty(it.key, it.value) getting called. However while debugging integration tests, the breakpoint is not hit.
Any ideas on what might be wrong/missing here?
We could add a "testing" module to the project and export the test-jar, but would like to avoid that if possible.
The annotations were simply missing #Inherited. They were found on classes, but without #Inherited, they weren't found via superclass.
I am trying to make a universal framework for iOS in KMP.
Here is my module build.gradle file
import org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask
buildscript {
ext.serialization_version = "0.20.0"
repositories {
mavenCentral()
jcenter()
maven { url "https://dl.bintray.com/jetbrains/kotlin-native-dependencies" }
}
}
repositories {
mavenCentral()
maven { url "https://kotlin.bintray.com/kotlinx" }
}
apply plugin: 'kotlin-multiplatform'
apply plugin: 'kotlinx-serialization'
def serialization_version = "0.20.0"
kotlin{
targets {
fromPreset(presets.jvm, 'android')
iosArm32("ios32")
iosArm64("ios64")
iosX64("simulator")
configure([ios32, ios64, simulator]) {
binaries.framework('Shared')
}
}
//we have 3 different sourceSets for common, android and iOS.
//each sourceSet can have their own set of dependencies and configurations
sourceSets {
commonMain.dependencies {
api 'org.jetbrains.kotlin:kotlin-stdlib-common'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version"
}
androidMain.dependencies {
api 'org.jetbrains.kotlin:kotlin-stdlib'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serialization_version"
}
iosMain {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:$serialization_version")
}
}
ios32.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:$serialization_version")
}
ios64.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:$serialization_version")
}
simulator.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:$serialization_version")
}
}
task fatFramework(type: FatFrameworkTask) {
// the fat framework must have the same base name as the initial frameworks
baseName = "Shared"
final File frameworkDir = new File(buildDir, "xcode-frameworks")
destinationDir = frameworkDir
// specify the frameworks to be merged
from(
targets.ios32.binaries.getFramework('Shared', 'RELEASE'),
targets.ios64.binaries.getFramework('Shared', 'RELEASE'),
targets.simulator.binaries.getFramework('Shared', 'RELEASE')
)
doLast {
new File(frameworkDir, 'gradlew').with {
text = "#!/bin/bash\nexport 'JAVA_HOME=${System.getProperty("java.home")}'\ncd '${rootProject.rootDir}'\n./gradlew \$#\n"
setExecutable(true)
}
}
}
}
configurations {
compileClasspath
}
tasks.build.dependsOn fatFramework
When I try to build my Module it gives me this error
Execution failed for task ':Shared:linkSharedReleaseFrameworkIos32'.
> Resolving configuration 'ios32Api' directly is not allowed
Am I missing something in my configuration?
I was unable to reproduce the error as the question features only part of the project. So, while modifying it to make things work as a separate project rather than a module, I accidentally lost the original problem. But here are my thoughts on the possible cause.
This snippet seems to be missing the difference between source sets and targets. I mean, as there are three named targets ios32, ios64, simulator, the kotlin-multiplatform plugin creates six default source sets: ios32Main, ios64Main, simulatorMain, ios32Test, ios64Test, simulatorTest. But in this code, I see new source sets named ios32 etc. being created instead of setting defaults' dependencies. This can be a problem, as there are no explicit connections between those source sets and declared targets.