For the longest time, Ive used a short gradle snip...
# gradle
z
For the longest time, Ive used a short gradle snippet that includes some util modules as dependencies for all my other modules. Its been great, but after introducing multiplatform modules Ive noticed that it no longer works - I believe the issue is that the
dependencies {}
block is now declared
sourceSets { commonMain { dependencies { ... } } }
instead. Is there a way for me to rewrite my snippet to work in both situations automagically, or do I need to have 2 sets - one for regular modules, and one for multiplatform ones? Ive also toyed with
allProjects
&
subProjects
, but I think the issue is really the same. Snippet in 🧵
Copy code
dependencies {
    apply(projects.util.logger, project)
    apply(projects.util.support, project)
}

private def apply(ProjectDependency dependency, Project project) {
    def projectPath = project.path
    def util = ":util"

    if (!projectPath.startsWith(util)) {
        project.dependencies {
            implementation(dependency)
        }
    }
}
e
this sort of cross-project configuration is discouraged anyway
c
It’s now considered an anti-pattern to use
allProjects
or
subProjects
, as they break Gradle’s ability to isolate projects and improve build times. Likewise, applying ad-hoc scripts (
apply from: "deps.gradle"
) is also discouraged. Fortunately, Gradle has solutions to replace those mechanisms: buildSrc script plugins (though I think this is now being deprecated in favor of included builds), and version catalogs
a
is your snippet equivalent to this?
Copy code
dependencies {
  if (!project.path.startsWith(":util")) {
    implementation(projects.util.logger)
    implementation(projects.util.support)
  }
}
e
but in principle you could react to the type of plugin applied, and add the dependency differently
z
@ephemient I know what youre referring to, but I dont know how to do it. Wanna elaborate or point me to an example? About it being an anti-pattern: I know, I know 😅 Im effectively applying this through a plugin (includeBuild); its just that I apply that plugin inside other plugins (which basically applies it to all modules). Not sure if thats any better, without going too much off-topic, if theres a better way to do it - please let me know! 😃
e
Copy code
pluginManager.withPlugin("org.jerbrains.kotlin.multiplatform") {
    // ...
}
a
I think it would be better to just be explicit about the dependencies, so even if it’s a little bit repetitive, it’s easy to check a subproject’s build config to see what its dependencies are Alternatively, maybe you want to expose util projects as transitive dependencies.
Copy code
kotlin {
  sourceSets {
    val commonMain by getting {
      dependencies {
        api(project(":util:logger"))
        api(project(":util:support"))
      }
    }
  }
}
You could even create an
all-utils
project that just exposes all util projects as an API dependency, then use that in all non-util projects.
but the core of your question is that
implementation
is the name of a Gradle Configuration - which have a confusing name and perhaps a better name is ‘DependencyCollection’. They’re a collection of files that might fetch their content from Maven, or a local directory, or wherever. (They can also be used to provide files - but that’s another story).
implementation
is the name of a Configuration provided by the Java plugin. There are a lot more of them, and they’re listed here https://docs.gradle.org/current/userguide/java_plugin.html#sec:java_plugin_and_dependency_management If you run
./gradlew resolvableConfigurations
then you’ll see all the Configurations. Obviously, because Kotlin Multiplatform is more than just Java, it needs to keep dependencies separate. To do that, the Kotlin Multiplatform plugin makes more Configurations - and they have different names. So if you want to add a dependency to the Kotlin/JS compile classpath, there’s a Configuration called
jsCompileClasspath
. And, under the hood, that’s what the Kotlin DSL does
Copy code
kotlin { 
  sourceSets {
    val jsMain by getting {
      dependencies {
        implementation("example:foo:1.2.3")
      }
    }
  }
}
That ^ adds
example:foo:1.2.3
into the
jsCompileClasspath
Configuration (and maybe also the
jsRuntimeClasspath
? I’m not sure). The Kotlin DSL is nice, but Gradle only really cares about the Configuration name. So you could also do it without the Kotlin DSL
Copy code
dependencies {
  "jsCompileClasspath"("example:foo:1.2.3")
}
but that’s a little more fragile, and not type-safe. So if you’ve moved from
kotlin("jvm")
to
kotlin("multiplatform")
, there’s no more Configuration named
implementation
. Instead, it’ll be called
jvmCompileClasspath
or something.
e
there even might be multiple, with any names, not necessarily "jvm"
Copy code
kotlin {
    jvm("one")
    jvm("two")
z
Being explicit about the dependencies: Overall I agree, but I think this would get too repetetive for my taste (the project is almost 300 modules big). I dont use the util modules everywhere, but having access to the code from them everywhere ensures that I can just type logcat { .. } any where in the codebase, which I love. Maybe its a bad habit, but I love it nevertheless! I had no idea about the dependency blocks - but now that youve mentioned it, it does make sense and explains the errors Im seeing when trying to just use the regular dependencies block. Thanks for enlightening me 😃 With all of this in mind, I dont see a "perfect" solution to my problem - but Ill check what plguinManager can do, and if I can tweak the dependency blocks according to each sourceSet. Please holla if anything more comes to mind that I can try!
a
What do you think about Gradle convention plugins? Here’s an example that adds a subproject dependency
Copy code
implementation(project(":kotest-runner:kotest-runner-junit5"))
to any subproject that applies the convention plugin https://github.com/kotest/kotest/blob/master/buildSrc/src/main/kotlin/kotest-jvm-conventions.gradle.kts
e
if you are explicitly writing
plugins { id("kotest-jvm-conventions") }
I think it's fair
but I didn't think Zoltan is doing that… otherwise they wouldn't need a
if (!projectPath.startsWith(util))
either that or you have one plugin that's trying to do too much… split it up into multiple plugins if that's the case
z
Both logger & support modules are added through a convention plugin actually! Its just that the plugin is inside another plugin, e.g. my convention plugins for android and kotlin modules both contain the util plugin as well.