Hi, I try to see if a more approachable `build.gr...
# gradle
s
Hi, I try to see if a more approachable
build.gradle.kts
syntax is possible. My biggest source of frustration https://github.com/gradle/gradle/issues/9268 seems to move forward, so that's good news. When using multiplatform projects, the other one I have hard time with is when there is this kind of code which is I think the idiomatic one documented:
Copy code
sourceSets {
    val commonMain by getting {
        dependencies {
            ...
        }
    }
}
Because: •
commonMain
is marked as never used in the IDE, how is it technically taken in account? • No discoverablity via autocomplete • I understand the technical reasons for
val commonMain by getting
but continues to looks to bit weird to me and not super approachable for beginners • It seems possible to write a much nicer version but I think it works only for
commonMain
not for platform specific variants:
Copy code
sourceSets {
    commonMain {
        dependencies {
            ...
        }
    }
}
• Using
val commonMain
when there is the
commonMain
getter mentioned above is confusing I can understand that Kotlin team don't want to pollute the DSL with all the crazy native platforms supported, but I am wondering if middle term it could be possible to just write:
Copy code
sourceSets {
    commonMain {
        dependencies {
            ...
        }
    }
    jvmMain {
        dependencies {
            ...
        }
    }
}
Not sure how, maybe by generating the right extensions based on the target configured on the project via a 2 passes approaches (not sure if that's possible)? Any thoughts? cc @eskatos
m
I just do
Copy code
getByName("commonMain"){
      // stuff 
    }
e
This would help for nested containers https://github.com/gradle/gradle/issues/9264
m
That comment on the Github issue above is on point. Accessors alone is half of the problem, how would you handle registration?
Hot take: I think this is a documentation issue. Just use
getByName("foo")
and
create("bar")
in Kotlin scripts and give up trying to make it look exactly like its Groovy couterpart
e
Or create extensions yourself 😅
s
I guess
val commonMain by getting
is promoted because it looks like a bit more statically typed and leverage some Kotlin magic to get the name from the variable.
getByName("foo")
or
create("bar")
avoid the "never used issue" and looks a bit more familiar syntax, but does not seems the end game solution to me. Passing a string is not really statically typed API with autocomplete which is the promise of Gradle Jotlin DSL.
There is already some 2 passes analysis to read the plugins I think, couldn't targets be parsed as well and related extensions created?
(like the
commonMain
one)
m
There is already some 2 passes analysis to read the plugins I think, couldn't targets be parsed as well and related extensions created?
plugins {}
works because it's a top level block and there are lots of constraints on it
Gradle actually parses the Kotlin files to extract it, I could find out the source if you're interested
s
Yep
Targets are pretty standardized, could not be the end of te world to parse ...
m
For the Kotlin block I guess they could do the same but that would put even more constraints
So people think they're writing a Kotlin file but in fact they're not
s
^^
m
Think of everyone doing something like this for an example
Copy code
afterEvaluate {
  kotlin {
    // bla
  }
}
(I know it's bad 😉 )
But people do it
s
Targets are standard, and if you don't generate the extensions, people can still use he regular ugly syntax.
m
The problem with source sets is that they are created after the Kotlin plugin has been applied:
Copy code
kotlin {
  jvmMain() // jvmMain is created here
}
So you'd need to extract that and find out what source set where created (by actually running this code)
But this code is turing complete:
Copy code
kotlin {
  if (someCondition) {
    jvmMain()
  }
}
s
You mean:
Copy code
kotlin {
  jvm()
}
?
m
Yup, edited
In all cases you cannot really run this code "before" the rest of the script, like the
plugins {}
block
Or if you want to do it, you need to add a lot of constraints
s
Yep, early approximative analysis via parsing is needed. here the goal is to create extensions for better syntax.
So if you create it while it is not necessary because you don't get all the logic in
kotlin { }
I think that's ok.
m
approximative analysis via parsing
FWIW (and AFAIK) this is not how it's done for plugins, it's actually calling
apply()
, running the code and reading the container values that have been contributed
For the record: • schema collector source (where containers are examined) • Lexer.kt (where the Kotlin file is parsed and plugins extracted)
a
it’s already possible to do
sourceSets.commonMain {}
in kts scripts
s
Yes that's mentioned in my original message
o
just in case, if you will create
jvm
target in some plugin (f.e. convention plugin) and apply it in your project, you will have autocomplete for
jvmMain
and
jvmTest
- the same will work for any other sourceSet created in this plugin (even custom, like
nativeMain
) f.e. jvm-convention.gradle.kts:
Copy code
plugins {
  kotlin("multiplatform")
}
kotlin {
  jvm()
}
and in some module: build.gradle.kts:
Copy code
plugins {
  id("jvm-convention")
}
kotlin {
  sourceSets {
    jvmMain { //will be generated type-safe accessor, even IDE will resolve it
    }
  }
}
a
Yes that’s mentioned in my original message
sorry I misunderstood!
s
Np
o
looks like the same machinery is working out of the box for
commonMain
as it’s created inside
kotlin-multiplatform
plugin
s
Interesting!
a
exactly what I was going to write Oleg :) There’s nothing special about
commonMain
vs
jvmMain
accessors.
kotlin.sourceSets
is a
NamedDomainObjectContainer
, and Gradle will generate Kotlin accessors for any named element so long as the elements are added before they are needed.
b
FYI instead of
getByName
and
create
you should use
named
and
register
respectively to avoid eager initialisation.
a
it’s also handy to register configurations in buildSrc convention plugins
Copy code
// buildSrc/src/main/kotlin/some-convention.gradle.kts
val myConfiguration by configurations.registering { ...}
because then Gradle will generate an accessor
Copy code
// build.gradle.kts
plugins {
  `some-convention`
}

dependencies {
b
As for accessors, commonMain works because it's created during plugin resolution time and jvmMain doesn't since it happens after that.
s
Maybe we just need specialization of:
Copy code
plugins {
  kotlin("multilatform") version "1.8.0"
}
Like there is already:
Copy code
ialization of:
plugins {
  kotlin("jvm") version "1.8.0"
}
For Wasm whihc was my use case, there could be maybe:
Copy code
ialization of:
plugins {
  kotlin("wasm") version "1.8.0"
}
And that would create the related accesssors automaticalyy.
v
https://github.com/gradle/gradle/issues/9268 seems to move forward
Yeah, they work on a compiler plugin that makes it possible, hopefully they can finish that any time. 🙂
commonMain
is marked as never used in the IDE, how is it technically taken in account?
The
by
is Kotlin property delegate mechanism. While calculating the actual delegate to use, the delegate mechanism can access the property name. So the name is acutally used. If IJ shows it as unused, that's imho a bug in IJ. I personally prefer those delegates over the string variant Martin suggested for refactoring and reuse, if just IJ would not show them as unused.
No discoverablity via autocomplete
That's the same for all of these named domain object containers Accessors that can then be autocompleted like the
commonMain
if you use the
sourceSets
within
kotlin { ... }
are generated from applying the plugins in the
plugins { ... }
block to a dummy project and investigating which things were added.
commonMain
is always added, so an accessor can be generated.
jvmMain
for example is only added if
kotlin { jvm() }
is done, so on that dummy project is not present and so no accessor can be generated. That is the price for a type-safe DSL.
Targets are pretty standardized, could not be the end of te world to parse ...
The point is, that this 2-phase thing is done by Gradle uniformly no matter which plugins are applied, not by the Kotlin plugin. So having special handling for Kotlin targets would introduce Kotlin plugin specifics into the Gradle core and that would not be too clean designwise unless some pluggable approach is invented. I think I made a feature request for that quite some time ago, but do not find it right now.
b
You could get jvmMain accessor by invoking jvm() target inside buildSrc precompuled script plugin instead on the project directly
v
And yes, if you would have specialized versions of the plugin for the different targets, that would allow accessors to be generated.
m
+1 to convention plugin all the things
s
Great, thanks for all your feedback, super interesting.
o
AFAIK https://github.com/gradle/gradle/issues/9268 PREVIEW is ‘planned’ for 8.1
s
jvmMain
for example is only added if
kotlin { jvm() }
is done, so on that dummy project is not present and so no accessor can be generated. That is the price for a type-safe DSL.
So maybe we need a 3 passes analysis 😅
a
something else to consider, to help avoiding tonnes of fragmented source sets, is to create a hierarchical structure to group related sourceSets
Copy code
sourceSets {
         val commonMain by getting {}
         val commonTest by getting {}

         val nativeMain by creating { dependsOn(commonMain) }
         val nativeTest by creating { dependsOn(commonTest) }

         // Linux
         val linuxX64Main by getting { dependsOn(nativeMain) }
         val linuxX64Test by getting { dependsOn(nativeTest) }

         // Windows - MinGW
         val mingwX64Main by getting { dependsOn(nativeMain) }
         val mingwX64Test by getting { dependsOn(nativeTest) }

   // and so on...
full code here:
build-logic/src/main/kotlin/ks3/conventions/lang/kotlin-multiplatform-native.gradle.kts
s
Hapefully Wasm will make things simpler there for me
o
regarding sourceSet/target hierarchies, soon (in 1.8.20) we will have experimental KGP API to define them in much more concise way: https://youtrack.jetbrains.com/issue/KT-53570/multiplatform-natural-hierarchy-prototype and it will work with convention plugins, so if you define hierarchy(default is single line, but custom hierarchies are possible) and targets(f.e. linux*, macos*, ios*, etc) in convention plugin - ALL sourceSet accessors like linuxMain/Test, macosMain/Test, appleMain/Test, nativeMain/Test will be generated and accessible from build script
v
So maybe we need a 3 passes analysis
You cannot make an automated 3 pass analysis. Gradle cannot know what exactly to copy over to the dummy project.
s
Yeah probably not a good idea.
v
Ah, that's the feature request I meant, but it was rejected: https://github.com/gradle/gradle/issues/16107