Hey, thanks to <@U0BKAPC0G> my <https://github.com...
# multiplatform
s
Hey, thanks to @snrostov my https://github.com/sdeleuze/spring-kotlin-fullstack is now using the new MP Gradle plugin but I have to say I am puzzled by the dependency structure that seems rather not conventional and harder to understand that previous one. For the JS module, we have currently:
Copy code
kotlin {
    target {
        browser()

        sourceSets {
            main {
                dependencies {
                    implementation(kotlin("stdlib-js"))
                    implementation(project(":shared"))
                }
            }
        }
    }
}
Why not:
Copy code
kotlin {
    target {
        browser()
    }
}
dependencies {
        implementation(kotlin("stdlib-js"))
        implementation(project(":shared"))
    }
For the common module, we have this pretty confusing build:
Copy code
kotlin {
    jvm()
    js {
        browser()
        nodejs()
    }

    sourceSets {
        commonMain {
            dependencies {
                implementation(kotlin("stdlib-common"))
            }
        }
        commonTest {
            dependencies {
                implementation(kotlin("test-common"))
                implementation(kotlin("test-annotations-common"))
            }
        }
        getByName("jvmMain").apply {
            dependencies {
                implementation(kotlin("stdlib-jdk8"))
            }
        }
        getByName("jvmTest").apply {
            dependencies {
                implementation(kotlin("test"))
                implementation(kotlin("test-junit"))
            }
        }
        getByName("jsMain").apply {
            dependencies {
                implementation(kotlin("stdlib-js"))
            }
        }
        getByName("jsTest").apply {
            dependencies {
                implementation(kotlin("test-js"))
            }
        }
    }
}
Is it the expected final format we are going to use? cc @gaetan
s
For the JS module
Both ways supported, but there is no DSL for NPM dependencies in root
dependency
section. And it is not clear how to implement it, especially for build.gradle.kts.
r
I don’t see anything confusing here? Gradle build files are also pretty hard to understand for those not used to them, it’s much better to keep the same format everywhere IMHO
s
For the common module, we have this pretty confusing build
Could you please explain what exactly is confusing?
s
For the JS module, would it be possible to allow writing:
Copy code
kotlin {
    target {
        browser()
    }
}
dependencies {
    implementation(kotlin("stdlib-js"))
    implementation(project(":shared"))
    implementation(npm("html-minifier:4.0.0"))
}
r
What should that do? Add all root dependencies to all platforms?
This way of writing it looks more confusing to me than the other
s
@sdeleuze Yes, it is in our plan. I created issue to track: https://youtrack.jetbrains.com/issue/KT-32179
👍 2
r
So that would not build if you define 2+ targets?
s
@ribesg I guess the question is about single platform projects, i.e when
org.jetbraibs.kotlin.js
plugin applied.
s
Yes, see the sample project that is used as a basis for that discussion
r
Ok. But I could define multiple js targets, right? (sorry I don’t do js). Would that add the dependencies to all targets?
s
No, you can’t create multiple targets with
org.jetbraibs.kotlin.js
plugin. You can do it with
org.jetbraibs.kotlin.multiplatform
only.
👍 1
s
For the shared module I need to have a deeper look before proposing something
For the shared one, what about:
Copy code
kotlin {
	common {
		dependencies {
			implementation(kotlin("stdlib-common"))
			testImplementation(kotlin("test-common"))
			testImplementation(kotlin("test-annotations-common"))
		}
	}
    jvm {
		dependencies {
			implementation(kotlin("stdlib-jdk8"))
			testImplementation(kotlin("test"))
			testImplementation(kotlin("test-junit"))
		}
	}
    js {
        browser()
        nodejs()
		dependencies {
			implementation(kotlin("stdlib-js"))
			testImplementation(kotlin("test-js"))
		}
    }
}
IMO in term of design of the DSL it is important to leverage as much as we can existing syntax to not confusing totally users.
I understand the technical challenge
But Gradle + multiplatform is so powerful and flexible, users need a clear structure, statcially typed, reusing existing idioms.
Otherwise that will be a nightmare
r
So that would be adding another level of dependencies, right? Root dependencies, sourceSet dependencies, and now target dependencies
I mean, it does make sense
s
Maybe my example is not right, I know Gradle but I am still new to the new MP infra
r
But you understand that it’s different than defining dependencies for a sourceSet right?
s
Could you elaborate?
r
Well here you’re adding dependencies directly to a target, but targets and sourceSets are not the same thing. You can have a sourceSet without target for example
s
Ok but can't we have conventions to make things easier?
Each techno have its default source set rigth ?
r
What I mean is that it cannot replace what we have now, but it would sure make things more readable in most cases
Yes. Right now each target defines 2 sourceSets, main and test. But you could have a plugin adding a target defining dozens of sourceSets for whatever reason
s
I don't mind keeping existing syntax for advanced use case, I care about providing easy and discoverable defaults when we follow convention. The flexibility and powerfulness of Gradle needs to be balanced to be usable easily.
Currrent plugin seems to be unware of these conventions
g
Why not just expose
implementationNpm
configuration?
👍 1
r
There are also some cases not yet covered by the multiplatform plugin, for example on iOS I would like to have 3 targets with the same main sourceSet, and only one of those targets would have a test sourceSet
g
It will also work for Kotlin DSL
s
That's maybe another way to explore
g
Honestly I never liked defining dependencies in sourceSets and prefer use standard dependencies block. And it already working with MPP plugin: Instead of
kotlin.sourceSets.commonMain.dependencies.api("something")
use:
dependencies.commonMainApi(project("something"))
s
I think I would indeed favor this option as well
So that's already usable with 1.3.40 with that syntax @gildor?
g
It works with 1.3.30
and should also work with 1.3.40
s
Oh, I am going to try it on my app then
Thanks for the hint!
g
but not sure that npm dependencies are exposed using Gradle configuration, because it uses some custom npm builder function, but I’m pretty sure it’s possible to expose
s
So in https://youtrack.jetbrains.com/issue/KT-32179
implementation(npm("html-minifier:4.0.0"))
should be replaced by
npmImplementation("html-minifier:4.0.0")
?
g
Yes, something like that
s
Ok I will drop a comment
g
at least it’s standard way to define custom dependencies in Gradle
s
Works for me, and we avoid double parenthesis so +1 for readability
g
in your example
kotlin
function is just emit standard dependency id, so not really the same as npm
s
Yeah
I agree
g
and
project
is different kind of dependency, that may be harder to implement
Also, plugins like gogradle use custom Convention for dependencies and provide own syntax, it’s also an option (which also be supported by Kotlin DSL) https://github.com/gogradle/gogradle/blob/master/docs/dependency-management.md
Python plugin tho uses exactly the same approach as suggested above: https://github.com/linkedin/pygradle/blob/master/examples/example-project/build.gradle#L22
just use custom Gradle configuration which is more limited in terms of syntax, it’s always string, or some group, id, version, but more standard and widely used
👍 1
replaced by
npmImplementation("html-minifier:4.0.0")
Or, alternatevely:
implementation("npm:html-minifier:4.0.0")
because npm doesn’t have any
group
, only name of dependency and version, so in theory it’s possible to support this syntax for all dependencies, but problem that
npm
is also valid maven group id
s
Yeah maybe too hackish for my taste 😉
g
Yeah, probably
s
But the issue is open with a comment with your proposal so I guess JB will choose the best option
👍 1
g
Thanks for the hint!
@sdeleuze Just to make it clear, in my example I use Kotlin DSL and have static accessors for all configurations, but only because I extract config to buildSrc plugin, so it allows Gradle to generate static accessors for me, if you just declare MPP config in the same file, you have to use dynamic syntax for configurations (except of default ones, like commonMainApi etc, because they are registered by defult by MPP plugin):
jvmMainApi("something")
becomes
"jvmMainApi"("something")
without static accessors, but IMO it’s still better, and if you want to declare some common configuration, just use buildSrc, so it will make all the configs type safe
👍 1
But again, it’s not something custom, it works out of the box like that for all configurations in Kotlin DSL: static accessors are generating for them if they registered on configuration time
s
In your project, how do you extract config to buildSrc plugin exactly?
g
No, I just use custom precompiled plugin
s
Oh
seee
s
So maybe we could open an issue for that beiing available by default ?
Or at least with just a little of config
Discoverability is key and we can't expect people using
"jvmMainApi"("something")
g
See, problem that usually, if you just have 1 module and do not extract config to custom plugin, you define target platforms on build script runtime, so you don’t have those configurations in configuration time, and don’t have static accessors for them
Discoverability is key and we can’t expect people using
I agree
s
Yeah
I see the technical challenge
g
but ut’s just how this configuration approach works
s
Discoverability is key and we can’t expect people using
"jvmMainApi"("something")
Just an idea about build.gradle.kts: what do you think about
jvm.main.api("something")
?
g
so what is
jvm
in this case? Gradle Convention for dependencies block?
s
reference to kotlin target created from preset
g
As I understand
jvm
here is just default name of
jvm
configuration, so if you define it like:
jvm("java12") { // some MPP plugin }
Dependency should have configuration like
java12MainApi
reference to kotlin target created from preset
Shouldn’t it be
kotlin.jvm.main.api("something")
than?
I just not sure how to make it type safe in this case
s
Copy code
jvm("java12") { // some MPP plugin }
maybe we can replace it with
Copy code
val java12 by jvm {  }
g
yeah, it will work for sure
a bit non-conventional, but may solve problem of dynamic platform declearation
s
@gildor If I use that I have a
Configuration with name 'jvmMainImplementation' not found
error, what did I miss?
Copy code
plugins {
    kotlin("multiplatform")
}

repositories {
    mavenCentral()
}

dependencies {
	commonMainImplementation(kotlin("stdlib-common"))
	commonTestImplementation(kotlin("test-common"))
	commonTestImplementation(kotlin("test-annotations-common"))

	"jvmMainImplementation"(kotlin("stdlib-jdk8"))
	"jvmTestImplementation"(kotlin("test"))
	"jvmTestImplementation"(kotlin("test-junit"))

	"jsMainImplementation"(kotlin("stdlib-js"))
	"jsTestImplementation"(kotlin("test-js"))
}

kotlin {
	jvm()
    js {
        browser()
        nodejs()
    }
}
s
@gildor About npm dependencies declaration: added comment here https://youtrack.jetbrains.com/issue/KT-32179#focus=streamItem-27-3536861.0-0. Feel free to edit/comment.
g
@Alexandre Delattre Swap
dependencies
and
kotlin
blocks
Because again, it’s defined in runtime, so configuration just not available
s
I was hoping the version with quotes (no static accessor) to work, but it fails even on command line.
Can I fix that without buildSrc?
d
You can always do this in the root dependencies blpck:
"browserMainImplementation"("dependencyNotation")
g
To be sure to have a valid DSL, we should be sure to have all uses cases and see how the DSL would apply to these cases. You didn’t talk about one of them. Suppose you have an application with admin and users modules. Each of them depends on a core module. You have now also to manage the dependencies between modules: AdminCommon -> CoreCommon AdminJS -> CoreJs -> CoreCommon. How does that fit into the new MPP plugin? The current documentation is not clear at all about that. In our data2viz library, we have a lot of these dependencies.
s
You should have 2 mpp projects: admin and core. Within these projects, there should be, for example, JVM and JS target. To declare dependency from one project to another, you should just put
project(":core")
in
commonMainApi
configuration. It is enough to specify a single
project('...')
dependency in a source set’s dependencies, and the compilations that include the source set will receive a corresponding platform-specific artifact of that project. This is main reason for new mpp project layout: now platform-specific artifact selected automatically. https://kotlinlang.org/docs/reference/building-mpp-with-gradle.html#adding-dependencies
g
Ok. However, how it works with a project that has multiple JVM targets? For example, a web server and a JavaFx one. They must be split into different projects to be used as dependencies?
s
They should provide some attribute to select proper artifact variant: https://kotlinlang.org/docs/reference/building-mpp-with-gradle.html#setting-up-targets
Copy code
kotlin {
    /* ... */
    
    // Configure the attributes of the 'jvm6' target:
    jvm("jvm6").attributes { /* ... */ }
}
Corresponding dependency artifact will be selected by matching consumer and dependency attributes. More info about this Gradle feature can be found here: https://docs.gradle.org/current/userguide/dependency_management_attribute_based_matching.html
As alternative, you can specify artifact manually, for example:
"jvm6MainApi"("com.example:foo-jvm6:1.0")
@h0tk3y please correct me if I wrong.
g
Thanks for this information. Didn’t know about it. In fact, it’s in the documentation: https://kotlinlang.org/docs/reference/building-mpp-with-gradle.html#disambiguating-targets I think that one of the biggest problems now is the comprehension of how it’s working. The current “reference” documentation is mixing a few types of documentation: how-to, reference, and explanation. See https://www.divio.com/blog/documentation/ Documentation should clearly identify each part. The first chapter “project structure” can highlight what is specific to MPP and what is Gradle concept.
👍 1