From 53cee6e243725a4d6efa3b910748c7035abb3e94 Mon Sep 17 00:00:00 2001
From: Mattias Reichel <mattias.reichel@gmail.com>
Date: Mon, 13 Jan 2025 15:40:27 +0100
Subject: [PATCH] build: streamline build and sort out dependencies

- This commit tries to sort out the dependency chains used, to enable further future decoupling. This will add a lot of exclusions to the POMs, but we can comment out the exclusions in the dependencies blocks if we think this is a problem.
- Streamline the build with composition.
---
 build.gradle                         |  56 ++++-----
 docs/build.gradle                    |  48 +++-----
 examples/sitemesh3/build.gradle      |  91 +++++---------
 gradle.properties                    |  10 +-
 gradle/aggregate-groovydoc.gradle    |  30 -----
 gradle/documentation-config.gradle   |  43 +++++++
 gradle/java-config.gradle            |  12 +-
 gradle/publish-config.gradle         |  80 ++++++++++++
 gradle/publish.gradle                |  93 --------------
 gradle/test-config.gradle            |  18 +++
 gradle/test.gradle                   |  40 ------
 grails-gsp/build.gradle              |  65 ++++++++--
 grails-plugin-gsp/build.gradle       | 175 +++++++++++++++++++++------
 grails-plugin-sitemesh3/build.gradle |  70 +++++++++--
 grails-taglib/build.gradle           |  57 +++++++--
 grails-web-gsp-taglib/build.gradle   |  34 +++++-
 grails-web-gsp/build.gradle          | 103 ++++++++++++++--
 grails-web-jsp/build.gradle          |  81 +++++++++++--
 grails-web-taglib/build.gradle       |  83 +++++++++++--
 19 files changed, 785 insertions(+), 404 deletions(-)
 delete mode 100644 gradle/aggregate-groovydoc.gradle
 create mode 100644 gradle/documentation-config.gradle
 create mode 100644 gradle/publish-config.gradle
 delete mode 100644 gradle/publish.gradle
 create mode 100644 gradle/test-config.gradle
 delete mode 100644 gradle/test.gradle

diff --git a/build.gradle b/build.gradle
index 3ec7268acb..0262b092d9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,77 +1,63 @@
+io.github.gradlenexus.publishplugin.InitializeNexusStagingRepository
+
 buildscript {
     repositories {
-        maven { url "https://repo.grails.org/grails/core" }
+        maven { url = 'https://repo.grails.org/grails/core' }
     }
     dependencies {
-        classpath platform("org.grails:grails-bom:$grailsVersion")
-        classpath "io.github.gradle-nexus:publish-plugin:2.0.0"
+        classpath 'io.github.gradle-nexus:publish-plugin:2.0.0'
     }
 }
 
 ext {
-    isCiBuild = (System.getenv().get("CI") as Boolean)
-    isSnapshot = project.projectVersion.endsWith('-SNAPSHOT')
+    isCiBuild = System.getenv('CI')
+    isSnapshot = projectVersion.endsWith('-SNAPSHOT')
     isReleaseVersion = !isSnapshot
 }
 
-group = "org.grails"
-version project.projectVersion
+group = 'org.grails'
+version = projectVersion
 
-apply plugin: 'java-library'
-apply plugin: 'idea'
-
-allprojects { prj ->
+allprojects {
     repositories {
         mavenCentral()
         maven { url = 'https://repo.grails.org/grails/core' }
         // mavenLocal() // Keep, this will be uncommented and used by CI (groovy-joint-workflow)
-        if (groovyVersion?.endsWith('-SNAPSHOT')) {
+        if (findProperty('groovyVersion')?.endsWith('-SNAPSHOT')) {
             maven {
                 name = 'ASF Snapshot repo'
                 url = 'https://repository.apache.org/content/repositories/snapshots'
             }
         }
-
-        if (System.getenv("GITHUB_MAVEN_PASSWORD") && !grailsVersion.endsWith('-SNAPSHOT')) {
-            System.out.println("Adding Grails Core Repo for project: ${prj.name}")
+        if (System.getenv('GITHUB_MAVEN_PASSWORD') && !grailsVersion.endsWith('-SNAPSHOT')) {
+            System.out.println("Adding Grails Core Staging Repo for project: $name")
             maven {
                 url = 'https://maven.pkg.github.com/grails/grails-core'
                 credentials {
                     username = 'DOES_NOT_MATTER'
-                    password = System.getenv("GITHUB_MAVEN_PASSWORD")
+                    password = System.getenv('GITHUB_MAVEN_PASSWORD')
                 }
             }
         }
     }
-
-    apply plugin: 'groovy'
 }
 
 if (isReleaseVersion) {
-    apply plugin: "io.github.gradle-nexus.publish-plugin"
-
+    apply plugin: 'io.github.gradle-nexus.publish-plugin'
     nexusPublishing {
         repositories {
             sonatype {
-                def ossUser = System.getenv('SONATYPE_USERNAME') ?: project.findProperty('sonatypeOssUsername') ?: ''
-                def ossPass = System.getenv('SONATYPE_PASSWORD') ?: project.findProperty('sonatypeOssPassword') ?: ''
-                def ossStagingProfileId = System.getenv('SONATYPE_STAGING_PROFILE_ID') ?: project.findProperty('sonatypeOssStagingProfileId') ?: ''
-                nexusUrl = uri("https://s01.oss.sonatype.org/service/local/")
-                username = ossUser
-                password = ossPass
-                stagingProfileId = ossStagingProfileId
+                nexusUrl = uri('https://s01.oss.sonatype.org/service/local')
+                username = System.getenv('SONATYPE_USERNAME') ?: project.findProperty('sonatypeOssUsername') ?: ''
+                password = System.getenv('SONATYPE_PASSWORD') ?: project.findProperty('sonatypeOssPassword') ?: ''
+                stagingProfileId = System.getenv('SONATYPE_STAGING_PROFILE_ID') ?: project.findProperty('sonatypeOssStagingProfileId') ?: ''
             }
         }
-        transitionCheckOptions {
-            maxRetries.set(60)
-            delayBetween.set(java.time.Duration.ofMillis(4000))
-        }
     }
-
     //do not generate extra load on Nexus with new staging repository if signing fails
-    tasks.withType(io.github.gradlenexus.publishplugin.InitializeNexusStagingRepository).configureEach {
-        shouldRunAfter(tasks.withType(Sign))
+    tasks.withType(InitializeNexusStagingRepository).configureEach {
+        shouldRunAfter = tasks.withType(Sign)
     }
 }
 
-apply from: rootProject.layout.projectDirectory.file('gradle/aggregate-groovydoc.gradle')
\ No newline at end of file
+apply from: layout.projectDirectory.file('gradle/documentation-config.gradle')
\ No newline at end of file
diff --git a/docs/build.gradle b/docs/build.gradle
index bd5034669d..b6c3b74897 100644
--- a/docs/build.gradle
+++ b/docs/build.gradle
@@ -2,34 +2,28 @@ import grails.doc.gradle.PublishGuide
 
 buildscript {
     repositories {
-        mavenLocal()
-        maven { url "https://repo.grails.org/grails/core" }
+        maven { url = 'https://repo.grails.org/grails/core' }
     }
     dependencies {
-        classpath platform("org.grails:grails-bom:$grailsVersion")
-        classpath "org.grails:grails-docs"
+        classpath "org.grails:grails-docs:$grailsVersion"
     }
 }
 
-version rootProject.version
-
 apply plugin: 'groovy'
 
-//TODO: PublishGuide should eventually ensure the build directory exists
+// TODO: PublishGuide should eventually ensure the build directory exists
 tasks.register('docsBuild') {
     doFirst {
         project.layout.buildDirectory.get().asFile.mkdirs()
     }
-
     // Do not cache this task since the directory must exist if publishGuide is going to run
     outputs.upToDateWhen { false }
 }
 
 tasks.register('publishGuide', PublishGuide) {
-    dependsOn 'docsBuild'
-
-    group = JavaBasePlugin.DOCUMENTATION_GROUP
+    group = 'documentation'
     description = 'Generate Guide'
+    dependsOn('docsBuild')
 
     targetDir = project.layout.buildDirectory.dir('docs').get().asFile
     outputs.dir(targetDir) // ensure gradle understands what this task generates
@@ -41,37 +35,35 @@ tasks.register('publishGuide', PublishGuide) {
     resourcesDir = project.file('src/main/docs/resources')
     properties = [
             'safe'          : 'UNSAFE', // Make sure any asciidoc security is disabled
-            'version'       : project.version,
+            'version'       : projectVersion,
             'title'         : 'Groovy Server Pages (GSP)',
             'subtitle'      : 'GSP (Groovy Server Pages) - A server-side view rendering technology based on Groovy',
-            // TODO: The javaee documentation has not been updated to jakarata
+            // TODO: The javaee documentation has not been updated to jakarta
             'javaee'        : 'https://docs.oracle.com/javaee/7/api/',
             'jakartaee'     : 'https://jakarta.ee/specifications/platform/10/apidocs/',
             'javase'        : 'https://docs.oracle.com/en/java/javase/17/docs/api/index.html',
-            'groovyapi'     : "https://docs.groovy-lang.org/$groovyVersion/html/gapi/",
-            'groovyjdk' : "https://docs.groovy-lang.org/$groovyVersion/html/groovy-jdk/",
+            'groovyapi'     : "https://docs.groovy-lang.org/latest/html/gapi/",
+            'groovyjdk'     : "https://docs.groovy-lang.org/latest/html/groovy-jdk/",
             'grailsapi'     : "https://docs.grails.org/$grailsVersion/api/",
-            'grailsdocs' : "https://docs.grails.org/$grailsVersion/",
-            'gormapi'       : 'http://gorm.grails.org/latest/api/',
+            'grailsdocs'    : "https://docs.grails.org/$grailsVersion/",
+            'gormapi'       : 'https://gorm.grails.org/latest/api/',
             'springapi'     : 'https://docs.spring.io/spring/docs/current/javadoc-api/',
             'commandLineRef': "https://docs.grails.org/$grailsVersion/ref/Command%20Line",
             'controllersRef': "https://docs.grails.org/$grailsVersion/ref/Controllers"
     ] as Properties
 
     doLast {
-        File destination = project.layout.buildDirectory.file("docs/guide/index.html").get().asFile
+        File destination = project.layout.buildDirectory.file('docs/guide/index.html').get().asFile
         destination.delete()
-
         project.layout.buildDirectory.file('docs/guide/single.html').get().asFile.renameTo(destination)
         project.layout.buildDirectory.file('docs/index.html').get().asFile.text = '''
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
-<html lang="en">
-<head>
-<meta http-equiv="refresh" content="0; url=guide/index.html" />
-</head>
-
-</body>
-</html>
-'''
+        <html lang="en">
+            <head>
+                <title>Redirecting...</title>
+                <meta http-equiv="refresh" content="0; url=guide/index.html" />
+            </head>
+            <body></body>
+        </html>
+        '''.stripIndent(8)
     }
 }
\ No newline at end of file
diff --git a/examples/sitemesh3/build.gradle b/examples/sitemesh3/build.gradle
index 997013c703..5aa34e8423 100644
--- a/examples/sitemesh3/build.gradle
+++ b/examples/sitemesh3/build.gradle
@@ -1,78 +1,43 @@
 buildscript {
     repositories {
-        maven { url "https://repo.grails.org/grails/core" }
+        maven { url = 'https://repo.grails.org/grails/core' }
     }
     dependencies {
         classpath platform("org.grails:grails-bom:$grailsVersion")
-        classpath "org.grails:grails-gradle-plugin"
+        classpath 'org.grails:grails-gradle-plugin'
     }
 }
 
-plugins {
-    id "war"
-    id "com.bertramlabs.asset-pipeline" version "5.0.5"
-}
-
-apply plugin:"org.grails.grails-web"
-apply plugin:"org.grails.grails-gsp"
-apply plugin:"org.grails.grails-plugin"
-
-version '0.0.1'
-group 'org.sitemesh.grails.plugins.sitemesh3'
+version = '0.0.1'
+group = 'org.sitemesh.grails.plugins.sitemesh3'
 
-apply plugin:"org.grails.grails-web"
-apply plugin:"org.grails.grails-gsp"
+apply plugin: 'org.grails.grails-web'
+apply plugin: 'org.grails.grails-gsp'
 
 dependencies {
-// for testing purposes
-//    implementation files('lib/sitemesh-3.1.0-SNAPSHOT.jar', 'lib/spring-boot-starter-sitemesh-3.1.0-SNAPSHOT-plain.jar')
-    console "org.grails:grails-console"
-
-    implementation "org.grails:grails-plugin-databinding"
-    implementation "org.grails:grails-plugin-i18n"
-    implementation "org.grails:grails-plugin-interceptors"
-    implementation "org.grails:grails-plugin-rest"
-    implementation "org.grails:grails-plugin-services"
-    implementation "org.grails:grails-plugin-url-mappings"
-    implementation "org.grails:grails-web-boot"
-    implementation project(':grails-plugin-gsp')
-    implementation project(':grails-plugin-sitemesh3')
-
-    runtimeOnly "com.bertramlabs.plugins:asset-pipeline-grails"
-    runtimeOnly "com.h2database:h2"
-    runtimeOnly "org.apache.tomcat:tomcat-jdbc"
-    runtimeOnly "org.fusesource.jansi:jansi"
-    runtimeOnly "org.grails.plugins:hibernate5"
-    runtimeOnly "org.grails.plugins:scaffolding"
-    runtimeOnly "org.grails:grails-core"
-    runtimeOnly "org.grails:grails-logging"
-    runtimeOnly "org.springframework.boot:spring-boot-autoconfigure"
-    runtimeOnly "org.springframework.boot:spring-boot-starter-actuator"
-    runtimeOnly "org.springframework.boot:spring-boot-starter-logging"
-    runtimeOnly "org.springframework.boot:spring-boot-starter-tomcat"
-    runtimeOnly "org.springframework.boot:spring-boot-starter-validation"
 
-    runtimeOnly "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:$jstlVersion"
-    runtimeOnly "org.glassfish.web:jakarta.servlet.jsp.jstl:$jstlVersion"
-    runtimeOnly 'org.apache.tomcat.embed:tomcat-embed-jasper:10.1.0' // jsp example
+    implementation platform("org.grails:grails-bom:$grailsVersion")
 
-    testImplementation 'org.grails:grails-gorm-testing-support'
-    testImplementation 'org.grails:grails-web-testing-support'
-    testImplementation 'org.spockframework:spock-core'
-    integrationTestImplementation testFixtures ("org.grails.plugins:geb")
-}
-
-tasks.withType(Test) {
-    useJUnitPlatform()
-}
-
-assets {
-    minifyJs = true
-    minifyCss = true
-    packagePlugin = true
+    implementation project(':grails-plugin-sitemesh3')
+    implementation 'org.grails:grails-core'
+    implementation 'org.grails:grails-plugin-controllers'
+
+    runtimeOnly project(':grails-plugin-gsp')
+    runtimeOnly 'com.bertramlabs.plugins:asset-pipeline-grails'
+    runtimeOnly 'org.grails:grails-plugin-url-mappings'
+    runtimeOnly 'org.grails:grails-plugin-i18n'
+    runtimeOnly 'org.springframework.boot:spring-boot-autoconfigure'
+    runtimeOnly 'org.springframework.boot:spring-boot-starter-logging'
+    runtimeOnly 'org.springframework.boot:spring-boot-starter-tomcat'
+
+    // JSP support
+    runtimeOnly 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api'
+    runtimeOnly 'org.apache.tomcat.embed:tomcat-embed-jasper'
+    runtimeOnly 'org.glassfish.web:jakarta.servlet.jsp.jstl'
+
+    integrationTestImplementation testFixtures('org.grails.plugins:geb')
+    integrationTestImplementation 'org.grails:grails-testing-support'
 }
 
-bootRun {
-    String springProfilesActive = 'spring.profiles.active'
-    systemProperty springProfilesActive, System.getProperty(springProfilesActive)
-}
\ No newline at end of file
+apply from: rootProject.layout.projectDirectory.file('gradle/java-config.gradle')
+apply from: rootProject.layout.projectDirectory.file('gradle/test-config.gradle')
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index b1a8704fcf..59c96bef5d 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,14 +3,16 @@ projectVersion=7.0.0-SNAPSHOT
 # for docs
 githubBranch = 7.0.x
 
+grailsVersion=7.0.0-SNAPSHOT
+javaVersion=17
+
 commonsTextVersion=1.13.0
 elApiVersion=6.0.1
-grailsVersion=7.0.0-SNAPSHOT
-groovyVersion=4.0.24
 jspApiVersion=4.0.0
-jstlVersion=3.0.1
-sitemeshLibraryVersion=3.2.2
+sitemeshVersion=3.2.2
 
+# This prevents the Grails Gradle Plugin from unnecessarily excluding slf4j-simple in the generated POMs
+# https://github.com/grails/grails-gradle-plugin/issues/222
 slf4jPreventExclusion=true
 
 org.gradle.caching=true
diff --git a/gradle/aggregate-groovydoc.gradle b/gradle/aggregate-groovydoc.gradle
deleted file mode 100644
index 837d446c91..0000000000
--- a/gradle/aggregate-groovydoc.gradle
+++ /dev/null
@@ -1,30 +0,0 @@
-
-tasks.register('cleanDocs', Delete) {
-    delete rootProject.layout.buildDirectory.dir('docs')
-}
-
-tasks.register('aggregateGroovydoc', Groovydoc) {
-    def groovyDocProjects = rootProject.subprojects.findAll { it.name != 'docs' && !it.name.startsWith('examples') }.findAll { it.file('src/main/groovy').exists() }
-    dependsOn = [tasks.named('cleanDocs')] + groovyDocProjects.collect { it.tasks.named('groovydoc') }
-
-    docTitle = "Groovy Server Pages (GSP) - ${project.name} - ${project.version}"
-    group = JavaBasePlugin.DOCUMENTATION_GROUP
-    description = 'Copies Groovy API Documentation for all supporting projects'
-    destinationDir = project.layout.buildDirectory.dir('docs/api').get().asFile
-    source = files(groovyDocProjects.collect { Project project -> project.files("src/main/groovy") })
-    classpath = files(groovyDocProjects.collect { Project project -> project.configurations.compileClasspath })
-}
-
-tasks.register('docs') {
-    group = JavaBasePlugin.DOCUMENTATION_GROUP
-    dependsOn = ['aggregateGroovydoc', 'docs:publishGuide']
-    finalizedBy 'copyGuide'
-}
-
-tasks.register('copyGuide', Copy) {
-    group = JavaBasePlugin.DOCUMENTATION_GROUP
-    from "${rootProject.allprojects.find { it.name == 'docs'}.projectDir}/build/docs"
-    includes = ['**']
-    into rootProject.layout.buildDirectory.dir('docs')
-    includeEmptyDirs = false
-}
diff --git a/gradle/documentation-config.gradle b/gradle/documentation-config.gradle
new file mode 100644
index 0000000000..085e08d70b
--- /dev/null
+++ b/gradle/documentation-config.gradle
@@ -0,0 +1,43 @@
+configurations.register('documentation')
+
+dependencies {
+    documentation "org.apache.groovy:groovy-groovydoc:4.0.24"
+    documentation "org.apache.groovy:groovy-ant:4.0.24"
+}
+
+tasks.register('cleanDocs', Delete) {
+    group = 'documentation'
+    delete(rootProject.layout.buildDirectory.dir('docs'))
+}
+
+tasks.register('groovydoc', Groovydoc) {
+    group = 'documentation'
+    description = 'Copies Groovy API Documentation for all supporting projects'
+    Set<Project> groovyDocProjects = rootProject.subprojects.findAll {
+        it.name != 'docs' && !it.name.startsWith('examples')
+    }
+    def groovydocClasspath = files(configurations.documentation + groovyDocProjects.configurations.compileClasspath)
+    classpath = groovydocClasspath
+    groovyClasspath = groovydocClasspath
+    docTitle = "Groovy Server Pages (GSP) - ${project.name} - ${project.version}"
+    access = GroovydocAccess.PRIVATE
+    includeAuthor = true
+    includeMainForScripts = false
+    processScripts = false
+    dependsOn = [tasks.named('cleanDocs')] + groovyDocProjects.collect { it.tasks.named('groovydoc') }
+    destinationDir = project.layout.buildDirectory.dir('docs/api').get().asFile
+    source = groovyDocProjects.sourceSets.main.groovy.srcDirs
+    doLast { delete(rootProject.layout.buildDirectory.dir('tmp')) }
+}
+
+tasks.register('docs') {
+    group = 'documentation'
+    dependsOn(':groovydoc', 'docs:publishGuide')
+    finalizedBy('copyGuide')
+}
+
+tasks.register('copyGuide', Copy) {
+    group = 'documentation'
+    from(project(':docs').tasks.named('publishGuide'))
+    into rootProject.layout.buildDirectory.dir('docs')
+}
diff --git a/gradle/java-config.gradle b/gradle/java-config.gradle
index ab5e0a963d..7c2c0520fc 100644
--- a/gradle/java-config.gradle
+++ b/gradle/java-config.gradle
@@ -1,16 +1,6 @@
-apply plugin: 'idea'
-apply plugin: 'java-library'
-
-compileJava.options.release = 17
+compileJava.options.release = javaVersion.toInteger()
 
 java {
     withSourcesJar()
     withJavadocJar()
-}
-
-dependencies {
-    implementation platform("org.grails:grails-bom:$grailsVersion")
-    api "org.apache.groovy:groovy:$groovyVersion"
-    compileOnly "jakarta.servlet:jakarta.servlet-api"
-    compileOnly "jakarta.persistence:jakarta.persistence-api"
 }
\ No newline at end of file
diff --git a/gradle/publish-config.gradle b/gradle/publish-config.gradle
new file mode 100644
index 0000000000..cba34af5b0
--- /dev/null
+++ b/gradle/publish-config.gradle
@@ -0,0 +1,80 @@
+apply plugin: 'maven-publish'
+apply plugin: 'signing'
+
+ext.set('isGrailsPlugin', project.group == 'org.grails.plugins')
+ext.set('signing.keyId', project.findProperty('signing.keyId') ?: System.getenv('SIGNING_KEY'))
+ext.set('signing.password', project.findProperty('signing.password') ?: System.getenv('SIGNING_PASSPHRASE'))
+ext.set('signing.secretKeyRingFile', project.findProperty('signing.secretKeyRingFile') ?: "${System.properties['user.home']}${File.separator}.gnupg${File.separator}secring.gpg")
+
+publishing {
+    if (isSnapshot) {
+        repositories {
+            maven {
+                credentials {
+                    username = System.getenv('ARTIFACTORY_USERNAME') ?: project.findProperty('artifactoryPublishUsername') ?: ''
+                    password = System.getenv('ARTIFACTORY_PASSWORD') ?: project.findProperty('artifactoryPublishPassword') ?: ''
+                }
+                url = isGrailsPlugin ?
+                        'https://repo.grails.org/grails/plugins3-snapshots-local' :
+                        'https://repo.grails.org/grails/libs-snapshots-local'
+            }
+        }
+    }
+
+    publications {
+        maven(MavenPublication) {
+
+            artifactId = project.name
+            groupId = project.group
+            version = project.version
+
+            from components.java
+
+            pom {
+                name = project.findProperty('pomTitle') ?: 'Groovy Server Pages (GSP)'
+                description = project.findProperty('pomDescription') ?: 'Groovy Server Pages (GSP) - A server-side view rendering technology based on Groovy'
+                url = project.findProperty('pomProjectUrl') ?: 'https://github.com/grails/grails-gsp'
+
+                licenses {
+                    license {
+                        name = 'The Apache Software License, Version 2.0'
+                        url = 'https://www.apache.org/licenses/LICENSE-2.0.txt'
+                        distribution = 'repo'
+                    }
+                }
+
+                developers {
+                    for (dev in project.findProperty('pomDevelopers') ?: [[id: 'graemerocher', name: 'Graeme Rocher']]) {
+                        developer {
+                            id = dev.id
+                            name = dev.name
+                        }
+                    }
+                }
+
+                scm {
+                    url = 'scm:git@github.com:grails/grails-gsp.git'
+                    connection = 'scm:git@github.com:grails/grails-gsp.git'
+                    developerConnection = 'scm:git@github.com:grails/grails-gsp.git'
+                }
+            }
+
+            // dependency management shouldn't be included
+            pom.withXml {
+                def pomNode = asNode()
+                try { pomNode.dependencyManagement.replaceNode({}) } catch (Throwable ignore) {}
+            }
+        }
+    }
+}
+
+afterEvaluate {
+    signing {
+        required = { isReleaseVersion && gradle.taskGraph.hasTask('publish') }
+        sign(publishing.publications.maven)
+    }
+}
+
+tasks.withType(Sign) {
+    onlyIf { isReleaseVersion }
+}
\ No newline at end of file
diff --git a/gradle/publish.gradle b/gradle/publish.gradle
deleted file mode 100644
index a783e55b51..0000000000
--- a/gradle/publish.gradle
+++ /dev/null
@@ -1,93 +0,0 @@
-apply plugin: 'maven-publish'
-apply plugin: 'signing'
-
-publishing {
-    if (isSnapshot) {
-        repositories {
-            maven {
-                credentials {
-                    def u = System.getenv('ARTIFACTORY_USERNAME') ?: project.findProperty('artifactoryPublishUsername') ?: ''
-                    def p = System.getenv('ARTIFACTORY_PASSWORD') ?: project.findProperty('artifactoryPublishPassword') ?: ''
-                    username = u
-                    password = p
-                }
-                if (project.group == 'org.grails.plugins') {
-                    url "https://repo.grails.org/grails/plugins3-snapshots-local"
-                } else {
-                    url "https://repo.grails.org/grails/libs-snapshots-local"
-                }
-            }
-        }
-    }
-
-    publications {
-        maven(MavenPublication) {
-            artifactId = project.name
-            groupId = project.group
-            version = project.version
-
-            from components.java
-
-            artifact sourcesJar
-            artifact javadocJar
-
-            if (project.group == 'org.grails.plugins') {
-                artifact source: project.layout.buildDirectory.dir("classes/groovy/main/META-INF/grails-plugin.xml"),
-                        classifier: "plugin",
-                        extension: 'xml'
-            }
-
-            pom.withXml {
-                def pomNode = asNode()
-
-                // dependency management shouldn't be included
-                try { pomNode.dependencyManagement.replaceNode({}) } catch (Throwable ignore) {}
-
-                name = project.findProperty('title') ?: 'Groovy Server Pages (GSP)'
-                description = project.findProperty('projectDesc') ?: 'Groovy Server Pages (GSP) - A server-side view rendering technology based on Groovy'
-                url = projectUrl
-
-                licenses {
-                    license {
-                        name = 'The Apache Software License, Version 2.0'
-                        url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
-                        distribution = 'repo'
-                    }
-                }
-
-                developers {
-                    for (dev in developers ?: [id: 'graemerocher', name: 'Graeme Rocher']) {
-                        developer {
-                            id dev.id
-                            name dev.name
-                        }
-                    }
-                }
-
-                scm {
-                    url "scm:git@github.com:${project.githubSlug}.git"
-                    connection "scm:git@github.com:${project.githubSlug}.git"
-                    developerConnection "scm:git@github.com:${project.githubSlug}.git"
-                }
-            }
-        }
-    }
-}
-
-
-afterEvaluate {
-    signing {
-        ext['signing.keyId'] = project.findProperty('signing.keyId') ?: System.getenv('SIGNING_KEY')
-        ext['signing.password'] = project.findProperty('signing.password') ?: System.getenv('SIGNING_PASSPHRASE')
-        ext['signing.secretKeyRingFile'] = project.findProperty('signing.secretKeyRingFile') ?: "${System.properties['user.home']}${File.separator}.gnupg${File.separator}secring.gpg"
-
-        required {
-            isReleaseVersion && gradle.taskGraph.hasTask('publish')
-        }
-        sign publishing.publications.maven
-    }
-}
-
-tasks.withType(Sign) {
-    onlyIf { isReleaseVersion }
-}
\ No newline at end of file
diff --git a/gradle/test-config.gradle b/gradle/test-config.gradle
new file mode 100644
index 0000000000..7f80348487
--- /dev/null
+++ b/gradle/test-config.gradle
@@ -0,0 +1,18 @@
+tasks.withType(Test).configureEach {
+    useJUnitPlatform()
+    testLogging {
+        events 'passed', 'skipped', 'failed', 'standardOut', 'standardError'
+        showStandardStreams = true
+        exceptionFormat = 'full'
+    }
+    if (isCiBuild) {
+        maxParallelForks = findProperty('testMaxParallelFork') ?: 2
+        forkEvery = findProperty('testForkEvery') ?: 0
+    } else {
+        maxParallelForks = findProperty('testMaxParallelFork') ?: 4
+        forkEvery = findProperty('testForkEvery') ?: 0
+    }
+    if (findProperty('testJvmArgs')) {
+        jvmArgs = findProperty('testJvmArgs')
+    }
+}
\ No newline at end of file
diff --git a/gradle/test.gradle b/gradle/test.gradle
deleted file mode 100644
index ef458f2a61..0000000000
--- a/gradle/test.gradle
+++ /dev/null
@@ -1,40 +0,0 @@
-dependencies {
-    testImplementation "jakarta.servlet:jakarta.servlet-api"
-    testImplementation "org.apache.groovy:groovy-test-junit5:$groovyVersion"
-    testImplementation "org.junit.jupiter:junit-jupiter-api"
-    testImplementation "org.junit.platform:junit-platform-runner"
-    testImplementation "org.spockframework:spock-core"
-    testImplementation "org.springframework:spring-test"
-    testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine"
-}
-
-test {
-    testLogging {
-        events "passed", "skipped", "failed", "standardOut", "standardError"
-    }
-}
-
-tasks.withType(Test) {
-    useJUnitPlatform()
-    testLogging {
-        showStandardStreams = true
-        exceptionFormat = 'full'
-    }
-
-    if (isCiBuild) {
-        maxParallelForks = project.findProperty('testMaxParallelFork') ?: 2
-        forkEvery = project.findProperty('testForkEvery') ?: 0
-    } else {
-        maxParallelForks = project.findProperty('testMaxParallelFork') ?: 4
-        forkEvery = project.findProperty('testForkEvery') ?: 0
-    }
-
-    if(project.findProperty('testJvmArgs')) {
-        jvmArgs = project.findProperty('testJvmArgs')
-    }
-
-    afterSuite {
-        System.out.print('.')
-        System.out.flush()
-    }
-}
\ No newline at end of file
diff --git a/grails-gsp/build.gradle b/grails-gsp/build.gradle
index 1f23069856..bb20341f6a 100644
--- a/grails-gsp/build.gradle
+++ b/grails-gsp/build.gradle
@@ -1,13 +1,62 @@
-version project.projectVersion
-group "org.grails"
+plugins {
+    id 'groovy'
+    id 'java-library'
+}
 
-apply from: rootProject.file('gradle/java-config.gradle')
+version = projectVersion
+group = 'org.grails'
 
 dependencies {
-    api "org.grails:grails-core"
-    api project(":grails-taglib")
-    api "org.apache.groovy:groovy-templates:$groovyVersion"
+
+    implementation platform("org.grails:grails-bom:$grailsVersion")
+
+    api 'org.grails:grails-bootstrap', { // ConfigMap
+        // API dependencies in grails-bootstrap
+        exclude group: 'org.yaml', module: 'snakeyaml'
+    }
+    api 'org.apache.groovy:groovy-templates' // Template, TemplateEngine
+
+    implementation project(':grails-taglib'), { // GrailsTagException, OutputEncodingSettings are used
+        // API dependencies in grails-taglib
+        exclude group: 'org.grails', module: 'grails-core'
+        exclude group: 'org.grails', module: 'grails-encoder'
+    }
+    implementation 'org.grails:grails-core', { // GrailsStringUtils
+        // API dependencies in grails-core
+        exclude group: 'org.apache.groovy', module: 'groovy'
+        exclude group: 'jakarta.inject', module: 'jakarta.inject-api'
+        exclude group: 'jakarta.persistence', module: 'jakarta.persistence-api'
+        exclude group: 'jakarta.annotation', module: 'jakarta.annotation-api'
+        exclude group: 'org.grails', module: 'grails-bootstrap'
+        //exclude group: 'org.grails', module: 'grails-datastore-core' // ClassPropertyFetcher
+        //exclude group: 'org.grails', module: 'grails-spring' // RuntimeSpringConfiguration (somehow needed for groovydoc)
+        exclude group: 'org.slf4j', module: 'jcl-over-slf4j'
+        exclude group: 'org.slf4j', module: 'slf4j-api'
+        exclude group: 'org.yaml', module: 'snakeyaml'
+        exclude group: 'org.springframework', module: 'spring-beans'
+        exclude group: 'org.springframework', module: 'spring-core'
+        exclude group: 'org.springframework', module: 'spring-context'
+        exclude group: 'org.springframework', module: 'spring-tx'
+        exclude group: 'org.springframework.boot', module: 'spring-boot'
+        exclude group: 'org.springframework.boot', module: 'spring-boot-autoconfigure'
+    }
+    implementation 'org.grails:grails-encoder', { // FastStringWriter, StreamByteBuffer, StreamCharBuffer are used
+        // API dependencies in grails-encoder
+        exclude group: 'org.apache.groovy', module: 'groovy'
+        exclude group: 'org.apache.groovy', module: 'groovy-json'
+        exclude group: 'org.grails', module: 'grails-core'
+        exclude group: 'org.slf4j', module: 'jcl-over-slf4j'
+        exclude group: 'org.slf4j', module: 'slf4j-api'
+        exclude group: 'org.springframework', module: 'spring-web'
+    }
+    implementation 'org.springframework:spring-context' // ApplicationContext
+
+    testImplementation 'org.junit.jupiter:junit-jupiter-api'
+    testImplementation 'org.spockframework:spock-core'
+
+    testRuntimeOnly 'org.slf4j:slf4j-nop' // Get rid of warning about missing slf4j implementation during test compilation
 }
 
-apply from: rootProject.file('gradle/test.gradle')
-apply from: rootProject.file('gradle/publish.gradle')
\ No newline at end of file
+apply from: rootProject.layout.projectDirectory.file('gradle/java-config.gradle')
+apply from: rootProject.layout.projectDirectory.file('gradle/test-config.gradle')
+apply from: rootProject.layout.projectDirectory.file('gradle/publish-config.gradle')
\ No newline at end of file
diff --git a/grails-plugin-gsp/build.gradle b/grails-plugin-gsp/build.gradle
index 849b0e0c4c..58c610bd56 100644
--- a/grails-plugin-gsp/build.gradle
+++ b/grails-plugin-gsp/build.gradle
@@ -1,19 +1,19 @@
 buildscript {
     repositories {
-        maven { url "https://repo.grails.org/grails/core" }
+        maven { url = 'https://repo.grails.org/grails/core' }
     }
     dependencies {
         classpath platform("org.grails:grails-bom:$grailsVersion")
-        classpath "org.grails:grails-gradle-plugin"
+        classpath 'org.grails:grails-gradle-plugin'
     }
 }
 
-version project.projectVersion
-group "org.grails.plugins"
+version = projectVersion
+group = 'org.grails.plugins'
 
-apply plugin: "org.grails.grails-plugin"
-
-apply from: rootProject.file('gradle/java-config.gradle')
+apply plugin: 'groovy'
+apply plugin: 'java-library'
+apply plugin: 'org.grails.grails-plugin'
 
 ext {
     testMaxParallelFork = isCiBuild ? 1 : 4
@@ -21,41 +21,142 @@ ext {
     testJvmArgs = ['-Xmx1536M']
 }
 
-// Fixes JSP tests
-configurations.all {
-    resolutionStrategy.dependencySubstitution.all { DependencySubstitution dependency ->
-        if (dependency.requested instanceof ModuleComponentSelector) {
-            if (group == 'org.grails' || group == 'org.grails.plugins') {
-                def targetProject = findProject(":${dependency.requested.module}")
-                if (targetProject != null) {
-                    dependency.useTarget targetProject
-                }
-            }
-        }
+dependencies {
+
+    implementation platform("org.grails:grails-bom:$grailsVersion")
+
+    api project(':grails-gsp'), { // GroovyPageResourceLoader, GroovyPagesTemplateEngine, CachingGroovyPageStaticResourceLocator
+        // API dependencies in grails-gsp
+        exclude group: 'org.grails', module: 'grails-bootstrap'
+        //exclude group: 'org.apache.groovy', module: 'groovy-templates' // TemplateEngine
     }
-}
+    api project(':grails-web-gsp'), { // PageRenderer
+        // API dependencies in grails-web-gsp
+        exclude group: 'org.grails', module: 'grails-gsp'
+        exclude group: 'org.grails', module: 'grails-web-common'
+        exclude group: 'org.grails', module: 'grails-web-taglib'
+    }
+    api project(':grails-web-taglib'), { // TagLibraryInvoker, TagLib, TagLibrary
+        // API dependencies in grails-web-taglib
+        exclude group: 'org.grails', module: 'grails-taglib'
+        exclude group: 'org.grails', module: 'grails-web-common'
+    }
+    api 'org.grails:grails-encoder', { // CodecLookup, Encoder
+        // API dependencies in grails-encoder
+        exclude group: 'org.apache.groovy', module: 'groovy'
+        exclude group: 'org.apache.groovy', module: 'groovy-json'
+        exclude group: 'org.grails', module: 'grails-core'
+        exclude group: 'org.slf4j', module: 'jcl-over-slf4j'
+        exclude group: 'org.slf4j', module: 'slf4j-api'
+        exclude group: 'org.springframework', module: 'spring-web'
+    }
+    api 'org.grails:grails-web-url-mappings', { // LinkGenerator
+        // API dependencies in grails-web-url-mappings
+        exclude group: 'org.grails', module: 'grails-web-common'
+        //exclude group: 'org.grails', module: 'grails-datastore-gorm-validation' // Constrained
+    }
+    api 'org.springframework:spring-context' // MessageSource, Errors, MessageSourceResolvable, DefaultMessageSourceResolvable, NoSuchMessageException
+    api 'org.springframework.boot:spring-boot' // ServletRegistrationBean
 
-dependencies {
-    api project(":grails-web-gsp-taglib")
-    api project(":grails-plugin-sitemesh3")
+    implementation project(':grails-taglib'), { // GroovyPageAttributes
+        // API dependencies in grails-taglib
+        exclude group: 'org.grails', module: 'grails-core'
+        exclude group: 'org.grails', module: 'grails-encoder'
+        exclude group: 'org.springframework', module: 'spring-core'
+    }
+    implementation project(':grails-web-gsp-taglib'), { // RenderTagLib
+        // API dependencies in grails-web-gsp-taglib
+        exclude group: 'org.grails', module: 'grails-taglib'
+        exclude group: 'org.grails', module: 'grails-web-gsp'
+    }
+    implementation "org.apache.commons:commons-text:$commonsTextVersion" // StringEscapeUtils
+    implementation 'org.apache.groovy:groovy-xml' // MarkupBuilder
+    implementation 'org.grails:grails-core', { // Config
+        // API dependencies in grails-core
+        exclude group: 'org.apache.groovy', module: 'groovy'
+        exclude group: 'jakarta.inject', module: 'jakarta.inject-api'
+        exclude group: 'jakarta.persistence', module: 'jakarta.persistence-api'
+        exclude group: 'jakarta.annotation', module: 'jakarta.annotation-api'
+        exclude group: 'org.grails', module: 'grails-bootstrap'
+        exclude group: 'org.grails', module: 'grails-datastore-core'
+        exclude group: 'org.grails', module: 'grails-spring'
+        exclude group: 'org.slf4j', module: 'jcl-over-slf4j'
+        exclude group: 'org.slf4j', module: 'slf4j-api'
+        exclude group: 'org.yaml', module: 'snakeyaml'
+        exclude group: 'org.springframework', module: 'spring-beans'
+        exclude group: 'org.springframework', module: 'spring-core'
+        exclude group: 'org.springframework', module: 'spring-context'
+        exclude group: 'org.springframework', module: 'spring-tx'
+        exclude group: 'org.springframework.boot', module: 'spring-boot'
+        exclude group: 'org.springframework.boot', module: 'spring-boot-autoconfigure'
+    }
+    implementation 'org.grails:grails-spring', { // RuntimeSpringConfiguration
+        // API dependencies in grails-spring
+        exclude group: 'org.springframework', module: 'spring-tx'
+        exclude group: 'org.springframework', module: 'spring-web'
+        exclude group: 'org.springframework', module: 'spring-context'
+        exclude group: 'org.grails', module: 'grails-bootstrap'
+        exclude group: 'org.apache.groovy', module: 'groovy-xml'
+    }
+    implementation 'org.grails:grails-web-common', {
+        // API dependencies in grails-web-common
+        exclude group: 'org.apache.groovy', module: 'groovy-templates'
+        exclude group: 'org.grails', module: 'grails-core'
+        exclude group: 'org.grails', module: 'grails-databinding'
+        exclude group: 'org.grails', module: 'grails-encoder'
+        exclude group: 'org.springframework', module: 'spring-webmvc'
+        exclude group: 'org.springframework', module: 'spring-context-support'
+    }
+    implementation 'org.grails:grails-web-mvc', { // SynchronizerTokensHolder
+        // API dependencies in grails-web-mvc
+        exclude group: 'org.grails', module: 'grails-web-common'
+        exclude group: 'org.grails', module: 'grails-web-url-mappings'
+    }
+    implementation 'org.springframework:spring-beans' // PropertyEditorRegistry
 
-    runtimeOnly(project(":grails-web-jsp"))
-    api "org.apache.commons:commons-text:$commonsTextVersion"
-    api "org.grails:grails-plugin-codecs"
-    astImplementation "org.grails:grails-web"
-    astImplementation "org.grails:grails-plugin-controllers"
+    astImplementation 'org.grails:grails-web', {
+        // API dependencies in grails-web
+        exclude group: 'org.grails', module: 'grails-web-common'
+        exclude group: 'org.grails', module: 'grails-web-databinding'
+        exclude group: 'org.grails', module: 'grails-web-gsp'
+        exclude group: 'org.grails', module: 'grails-web-mvc'
+        exclude group: 'org.grails', module: 'grails-web-url-mappings'
+    }
+    astImplementation 'org.grails:grails-plugin-controllers', {
+        // API dependencies in grails-plugin-controllers
+        //exclude group: 'org.grails', module: 'grails-core' // TraitInjector
+        exclude group: 'org.grails', module: 'grails-web'
+        exclude group: 'org.grails', module: 'grails-plugin-mimetypes'
+        exclude group: 'org.grails', module: 'grails-plugin-validation'
+        exclude group: 'org.grails', module: 'grails-plugin-domain-class'
+        exclude group: 'org.springframework.boot', module: 'spring-boot-autoconfigure'
+    }
+
+    compileOnly project(':grails-web-jsp'), { // Provided by Application for JSP support
+        // API dependencies in grails-web-jsp
+        exclude group: 'org.grails', module: 'grails-web-gsp'
+    }
 
-    testImplementation "jakarta.annotation:jakarta.annotation-api"
-    testImplementation "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:$jstlVersion"
+    testCompileOnly 'jakarta.annotation:jakarta.annotation-api'
+
+    testImplementation project(':grails-web-jsp'), { // TagLibraryResolverImpl
+        // API dependencies in grails-web-jsp
+        exclude group: 'org.grails', module: 'grails-web-gsp'
+    }
+    testImplementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api'
     testImplementation "jakarta.servlet.jsp:jakarta.servlet.jsp-api:$jspApiVersion"
-    testImplementation "org.grails:grails-gorm-testing-support"
-    testImplementation "org.grails:grails-testing-support"
-    testImplementation "org.grails:grails-web-testing-support"
+    testImplementation 'org.junit.jupiter:junit-jupiter-api'
+    testImplementation 'org.springframework:spring-test'
+    testImplementation 'org.spockframework:spock-core'
+    testImplementation 'org.grails:grails-gorm-testing-support'
+    testImplementation 'org.grails:grails-testing-support'
+    testImplementation 'org.grails:grails-web-testing-support'
 
-    testRuntimeOnly "org.glassfish.web:jakarta.servlet.jsp.jstl:$jstlVersion"
-    testRuntimeOnly "org.grails.plugins:async"
-    testRuntimeOnly "org.grails:grails-plugin-url-mappings"
+    testRuntimeOnly 'org.glassfish.web:jakarta.servlet.jsp.jstl'
+    testRuntimeOnly 'org.grails:grails-plugin-url-mappings'
+    testRuntimeOnly 'org.grails.plugins:async'
 }
 
-apply from: rootProject.file('gradle/test.gradle')
-apply from: rootProject.file('gradle/publish.gradle')
+apply from: rootProject.layout.projectDirectory.file('gradle/java-config.gradle')
+apply from: rootProject.layout.projectDirectory.file('gradle/test-config.gradle')
+apply from: rootProject.layout.projectDirectory.file('gradle/publish-config.gradle')
diff --git a/grails-plugin-sitemesh3/build.gradle b/grails-plugin-sitemesh3/build.gradle
index 0d9fb224b8..62b38882aa 100644
--- a/grails-plugin-sitemesh3/build.gradle
+++ b/grails-plugin-sitemesh3/build.gradle
@@ -1,20 +1,70 @@
-version project.projectVersion
-group "org.grails.plugins"
+buildscript {
+    repositories {
+        maven { url = 'https://repo.grails.org/grails/core' }
+    }
+    dependencies {
+        classpath platform("org.grails:grails-bom:$grailsVersion")
+        classpath 'org.grails:grails-gradle-plugin'
+    }
+}
+
+version = projectVersion
+group = 'org.grails.plugins'
 
-apply from: rootProject.file('gradle/java-config.gradle')
+apply plugin: 'groovy'
+apply plugin: 'java-library'
+apply plugin: 'org.grails.grails-plugin'
 
 ext {
-    title = 'SiteMesh 3 Grails Plugin'
-    description = 'SiteMesh is a web-page layout and decoration framework and web- application integration framework to aid in creating sites consisting of many pages for which a consistent look/feel, navigation and layout scheme is required.'
-    developers = [
+    pomTitle = 'SiteMesh 3 Grails Plugin'
+    pomDescription = 'SiteMesh is a web-page layout and decoration framework and web- application integration framework to aid in creating sites consisting of many pages for which a consistent look/feel, navigation and layout scheme is required.'
+    pomDevelopers = [
         [id: 'codeconsole', name: 'Scott Murphy Heiberg']
     ]
 }
 
 dependencies {
-    api "org.sitemesh:spring-boot-starter-sitemesh:$sitemeshLibraryVersion"
-    api project(':grails-web-gsp-taglib')
+
+    implementation platform("org.grails:grails-bom:$grailsVersion")
+
+    api project(':grails-web-gsp'), { // GrailsConventionGroovyPageLocator
+        // API dependencies in grails-web-gsp
+        //exclude group: 'org.grails', module: 'grails-gsp' // DefaultGroovyPageLocator
+        exclude group: 'org.grails', module: 'grails-web-common'
+        exclude group: 'org.grails', module: 'grails-web-taglib'
+    }
+    api project(':grails-web-gsp-taglib'), { // GrailsConventionGroovyPageLocator
+        // API dependencies in grails-web-gsp-taglib
+        exclude group: 'org.grails', module: 'grails-taglib'
+        //exclude group: 'org.grails', module: 'grails-web-gsp' // DefaultGroovyPageLocator
+    }
+    api "org.sitemesh:sitemesh:$sitemeshVersion" // SiteMeshFilter
+    api 'org.springframework:spring-webmvc' // AbstractHandlerAdapter, ParameterizableViewController, AbstractHandlerMapping
+    api 'org.springframework.boot:spring-boot' // FilterRegistrationBean
+
+    implementation project(':grails-web-taglib'), { // TagLib, TagLibrary
+        // API dependencies in grails-web-taglib
+        exclude group: 'org.grails', module: 'grails-taglib'
+        exclude group: 'org.grails', module: 'grails-web-common'
+    }
+    implementation 'org.grails:grails-web-common', { // WebUtils
+        // API dependencies in grails-web-common
+        exclude group: 'org.apache.groovy', module: 'groovy-templates'
+        exclude group: 'org.grails', module: 'grails-core'
+        exclude group: 'org.grails', module: 'grails-databinding'
+        exclude group: 'org.grails', module: 'grails-encoder'
+        exclude group: 'org.springframework', module: 'spring-webmvc'
+        exclude group: 'org.springframework', module: 'spring-context-support'
+    }
+    implementation 'org.springframework:spring-beans' // Autowired, Qualifier
+    implementation 'org.springframework:spring-web' // HttpMethod, ResponseStatusException, HttpStatus
+
+    runtimeOnly "org.sitemesh:spring-boot-starter-sitemesh:$sitemeshVersion"
+
+    compileOnly 'jakarta.servlet:jakarta.servlet-api' // Provided by servlet container
+    compileOnly 'org.apache.groovy:groovy' // Provided by Grails Application
+
 }
 
-apply from: rootProject.file('gradle/test.gradle')
-apply from: rootProject.file('gradle/publish.gradle')
+apply from: rootProject.layout.projectDirectory.file('gradle/java-config.gradle')
+apply from: rootProject.layout.projectDirectory.file('gradle/publish-config.gradle')
diff --git a/grails-taglib/build.gradle b/grails-taglib/build.gradle
index 90d5c2966c..dbc2ccba61 100644
--- a/grails-taglib/build.gradle
+++ b/grails-taglib/build.gradle
@@ -1,12 +1,55 @@
-version project.projectVersion
-group "org.grails"
+plugins {
+    id 'groovy'
+    id 'java-library'
+}
 
-apply from: rootProject.file('gradle/java-config.gradle')
+version = projectVersion
+group = 'org.grails'
 
 dependencies {
-    api "org.grails:grails-core"
-    api "org.grails:grails-encoder"
+
+    implementation platform("org.grails:grails-bom:$grailsVersion")
+
+    api 'org.grails:grails-core', { // InjectableGrailsClass, ArtefactHandlerAdapter, ArtefactInfo, GrailsClass, AbstractInjectableGrailsClass, GrailsApplication, EncodingStateRegistry, EncodingStateRegistryLookup
+        // API dependencies in grails-core
+        exclude group: 'org.apache.groovy', module: 'groovy'
+        exclude group: 'jakarta.inject', module: 'jakarta.inject-api'
+        exclude group: 'jakarta.persistence', module: 'jakarta.persistence-api'
+        exclude group: 'jakarta.annotation', module: 'jakarta.annotation-api'
+        //exclude group: 'org.grails', module: 'grails-bootstrap' // Resource
+        //exclude group: 'org.grails', module: 'grails-datastore-core' // MappingContext
+        exclude group: 'org.grails', module: 'grails-spring'
+        exclude group: 'org.slf4j', module: 'jcl-over-slf4j'
+        exclude group: 'org.slf4j', module: 'slf4j-api'
+        exclude group: 'org.yaml', module: 'snakeyaml'
+        //exclude group: 'org.springframework', module: 'spring-beans' // Aware
+        exclude group: 'org.springframework', module: 'spring-core'
+        //exclude group: 'org.springframework', module: 'spring-context' // ApplicationContext
+        exclude group: 'org.springframework', module: 'spring-tx'
+        exclude group: 'org.springframework.boot', module: 'spring-boot'
+        exclude group: 'org.springframework.boot', module: 'spring-boot-autoconfigure'
+    }
+    api 'org.grails:grails-encoder', { // EncodingStateRegistry
+        // API dependencies in grails-encoder
+        exclude group: 'org.apache.groovy', module: 'groovy'
+        exclude group: 'org.apache.groovy', module: 'groovy-json'
+        exclude group: 'org.grails', module: 'grails-core'
+        exclude group: 'org.slf4j', module: 'jcl-over-slf4j'
+        exclude group: 'org.slf4j', module: 'slf4j-api'
+        exclude group: 'org.springframework', module: 'spring-web'
+    }
+    api 'org.springframework:spring-core' // Ordered
+
+    implementation 'org.slf4j:jcl-over-slf4j' // Commons Logging is used
+
+    compileOnly 'org.apache.groovy:groovy' // Needed as there are Java files that reference Groovy classes
+
+    testImplementation 'org.junit.jupiter:junit-jupiter-api'
+    testImplementation 'org.spockframework:spock-core'
+
+    testRuntimeOnly 'org.slf4j:slf4j-nop' // Get rid of warning about missing slf4j implementation during tests
 }
 
-apply from: rootProject.file('gradle/test.gradle')
-apply from: rootProject.file('gradle/publish.gradle')
+apply from: rootProject.layout.projectDirectory.file('gradle/java-config.gradle')
+apply from: rootProject.layout.projectDirectory.file('gradle/test-config.gradle')
+apply from: rootProject.layout.projectDirectory.file('gradle/publish-config.gradle')
diff --git a/grails-web-gsp-taglib/build.gradle b/grails-web-gsp-taglib/build.gradle
index 370c3765c6..c6ee05d70f 100644
--- a/grails-web-gsp-taglib/build.gradle
+++ b/grails-web-gsp-taglib/build.gradle
@@ -1,11 +1,33 @@
-version project.projectVersion
-group "org.grails"
+version = project.projectVersion
+group = 'org.grails'
 
-apply from: rootProject.file('gradle/java-config.gradle')
+apply plugin: 'groovy'
+apply plugin: 'java-library'
 
 dependencies {
-    api project(':grails-web-jsp')
+
+    implementation platform("org.grails:grails-bom:$grailsVersion")
+
+    api project(':grails-taglib'), { // GrailsTagLibClass
+        // API dependencies in grails-taglib
+        exclude group: 'org.grails', module: 'grails-core'
+        exclude group: 'org.grails', module: 'grails-encoder'
+        exclude group: 'org.springframework', module: 'spring-core'
+    }
+    api project(':grails-web-gsp'), { // GroovyPagesTemplateRenderer
+        // API dependencies in grails-web-gsp
+        //exclude group: 'org.grails', module: 'grails-gsp' // GroovyPagesTemplateEngine
+        //exclude group: 'org.grails', module: 'grails-web-common' // GrailsApplicationAttributes
+        exclude group: 'org.grails', module: 'grails-web-taglib'
+    }
+
+    implementation project(':grails-web-taglib'), { // TagLib
+        // API dependencies in grails-web-taglib
+        exclude group: 'org.grails', module: 'grails-taglib'
+        //exclude group: 'org.grails', module: 'grails-web-common' // WebAttributes
+    }
 }
 
-apply from: rootProject.file('gradle/test.gradle')
-apply from: rootProject.file('gradle/publish.gradle')
+apply from: rootProject.layout.projectDirectory.file('gradle/java-config.gradle')
+apply from: rootProject.layout.projectDirectory.file('gradle/test-config.gradle')
+apply from: rootProject.layout.projectDirectory.file('gradle/publish-config.gradle')
diff --git a/grails-web-gsp/build.gradle b/grails-web-gsp/build.gradle
index 211d7f2c91..d29e8ac3d8 100644
--- a/grails-web-gsp/build.gradle
+++ b/grails-web-gsp/build.gradle
@@ -1,17 +1,98 @@
-version project.projectVersion
-group "org.grails"
+version = projectVersion
+group = 'org.grails'
 
-apply from: rootProject.file('gradle/java-config.gradle')
+apply plugin: 'groovy'
+apply plugin: 'java-library'
 
 dependencies {
-    compileOnly "org.apache.ant:ant"
-    api project(":grails-gsp")
-    api "org.grails:grails-web-common"
-    api project(":grails-web-taglib")
 
-    testImplementation "net.bytebuddy:byte-buddy"
-    testRuntimeOnly "org.grails:grails-spring"
+    implementation platform("org.grails:grails-bom:$grailsVersion")
+
+    api 'org.grails:grails-core', { // GrailsDomainClass, CacheEntry, Environment, GrailsStringUtils, GrailsApplication, GrailsControllerClass, GrailsApplicationAware, ControllerArtefactHandler, GrailsPluginManager, GrailsFactoriesLoader
+        // API dependencies in grails-core
+        exclude group: 'org.apache.groovy', module: 'groovy'
+        exclude group: 'jakarta.inject', module: 'jakarta.inject-api'
+        exclude group: 'jakarta.persistence', module: 'jakarta.persistence-api'
+        exclude group: 'jakarta.annotation', module: 'jakarta.annotation-api'
+        exclude group: 'org.grails', module: 'grails-bootstrap'
+        //exclude group: 'org.grails', module: 'grails-datastore-core' // MappingContext
+        //exclude group: 'org.grails', module: 'grails-spring' // RuntimeSpringConfiguration
+        exclude group: 'org.slf4j', module: 'jcl-over-slf4j'
+        exclude group: 'org.slf4j', module: 'slf4j-api'
+        exclude group: 'org.yaml', module: 'snakeyaml'
+        exclude group: 'org.springframework', module: 'spring-beans'
+        exclude group: 'org.springframework', module: 'spring-core'
+        exclude group: 'org.springframework', module: 'spring-context'
+        exclude group: 'org.springframework', module: 'spring-tx'
+        exclude group: 'org.springframework.boot', module: 'spring-boot'
+        exclude group: 'org.springframework.boot', module: 'spring-boot-autoconfigure'
+    }
+    api project(':grails-gsp'), { // GroovyPage, GroovyPageBinding, GroovyPageMetaInfo, GroovyPagesTemplateEngine, GroovyPageScriptSource, DefaultGroovyPageLocator, GroovyPageScriptSource, GroovyPageCompiledScriptSource, GroovyPageResourceScriptSource
+        // API dependencies in grails-gsp
+        exclude group: 'org.grails', module: 'grails-bootstrap'
+        exclude group: 'org.grails', module: 'grails-encoder'
+        exclude group: 'org.apache.groovy', module: 'groovy-templates'
+        // Implementation dependencies in grails-gsp
+        exclude group: 'org.grails', module: 'grails-taglib'
+
+    }
+    api project(':grails-taglib'), { // GrailsTagException, TemplateVariableBinding, OutputEncodingSettings, WithCodecHelper
+        // API dependencies in grails-taglib
+        exclude group: 'org.grails', module: 'grails-core'
+        exclude group: 'org.grails', module: 'grails-encoder'
+        exclude group: 'org.springframework', module: 'spring-core'
+    }
+    api 'org.grails:grails-web-common', { // GrailsWebRequest, GrailsApplicationAttributes, MimeType, MimeTypeResolver, GroovyPagesUriService, DefaultGroovyPagesUriService, AbstractGrailsView, GrailsViewResolver
+        // API dependencies in grails-web-common
+        exclude group: 'org.grails', module: 'grails-core'
+        exclude group: 'org.grails', module: 'grails-databinding'
+        exclude group: 'org.grails', module: 'grails-encoder'
+        //exclude group: 'org.apache.groovy', module: 'groovy-templates' // TemplateEngine needed downstream
+        exclude group: 'org.springframework', module: 'spring-webmvc'
+        exclude group: 'org.springframework', module: 'spring-context-support'
+    }
+    api 'org.springframework:spring-beans' // InitializingBean, Autowired
+    api 'org.springframework:spring-context' // ScriptSource
+    api 'org.springframework:spring-web' // WebApplicationContext, RequestAttributes, RequestContextHolder, ServletRequestAttributes, WebUtils
+    api 'org.springframework:spring-webmvc' // FrameworkServlet, View, InternalResourceViewResolver, AbstractUrlBasedView
+
+    implementation 'org.apache.groovy:groovy-templates' // Template
+    implementation 'org.grails:grails-bootstrap', { // GrailsNameUtils, GrailsResourceUtils
+        exclude group: 'org.yaml', module: 'snakeyaml'
+    }
+    implementation 'org.grails:grails-encoder', { // CodecPrintWriter, FastStringWriter, EncodedAppenderWriterFactory, Encoder, StreamingEncoder, StreamingEncoderWriter
+        // API dependencies in grails-encoder
+        exclude group: 'org.apache.groovy', module: 'groovy'
+        exclude group: 'org.apache.groovy', module: 'groovy-json'
+        exclude group: 'org.grails', module: 'grails-core'
+        exclude group: 'org.slf4j', module: 'jcl-over-slf4j'
+        exclude group: 'org.slf4j', module: 'slf4j-api'
+        exclude group: 'org.springframework', module: 'spring-web'
+    }
+    implementation 'org.springframework:spring-core' // ByteArrayResource, Assert, ReflectionUtils
+
+    compileOnly 'jakarta.servlet:jakarta.servlet-api'
+    compileOnly 'org.apache.ant:ant' // BuildException, DirectoryScanner, MatchingTask, Path, Reference
+    compileOnly 'org.apache.groovy:groovy'
+
+    testImplementation 'jakarta.servlet:jakarta.servlet-api'
+    testImplementation 'net.bytebuddy:byte-buddy'
+    testImplementation 'org.grails:grails-web-common', { // GrailsWebMockUtil
+        // API dependencies in grails-web-common
+        exclude group: 'org.grails', module: 'grails-core'
+        exclude group: 'org.grails', module: 'grails-databinding'
+        exclude group: 'org.grails', module: 'grails-encoder'
+        exclude group: 'org.apache.groovy', module: 'groovy-templates'
+        exclude group: 'org.springframework', module: 'spring-webmvc'
+        exclude group: 'org.springframework', module: 'spring-context-support'
+    }
+    testImplementation 'org.apache.groovy:groovy-xml'
+    testImplementation 'org.spockframework:spock-core'
+    testImplementation 'org.springframework:spring-test' // MockHttpServletRequest is transitively used by GrailsWebMockUtils in grails-web-common
+
+    testRuntimeOnly 'org.slf4j:slf4j-nop' // Get rid of warning about missing slf4j implementation during tests
 }
 
-apply from: rootProject.file('gradle/test.gradle')
-apply from: rootProject.file('gradle/publish.gradle')
+apply from: rootProject.file('gradle/java-config.gradle')
+apply from: rootProject.file('gradle/test-config.gradle')
+apply from: rootProject.file('gradle/publish-config.gradle')
diff --git a/grails-web-jsp/build.gradle b/grails-web-jsp/build.gradle
index e59dfca051..8b6fd73087 100644
--- a/grails-web-jsp/build.gradle
+++ b/grails-web-jsp/build.gradle
@@ -1,16 +1,77 @@
-version project.projectVersion
-group "org.grails"
+version = projectVersion
+group = 'org.grails'
 
-apply from: rootProject.file('gradle/java-config.gradle')
+apply plugin: 'groovy'
+apply plugin: 'java-library'
 
 dependencies {
-    api "org.grails:grails-web-common"
-    api project(":grails-web-gsp")
 
-    // Required for JSP support
-    compileOnly "jakarta.servlet.jsp:jakarta.servlet.jsp-api:$jspApiVersion"
-    compileOnly "jakarta.el:jakarta.el-api:$elApiVersion"
+    implementation platform("org.grails:grails-bom:$grailsVersion")
+
+    api project(':grails-web-gsp'), { // GroovyPagesServlet is used in public API (used in servlet context attribute value)
+        // API dependencies in grails-web-gsp
+        exclude group: 'org.grails', module: 'grails-gsp'
+        exclude group: 'org.grails', module: 'grails-web-common'
+        exclude group: 'org.grails', module: 'grails-web-taglib'
+    }
+
+    implementation project(':grails-gsp'), { // GroovyPage
+        // API dependencies in grails-gsp
+        //exclude group: 'org.grails', module: 'grails-bootstrap' // Resource is used
+        exclude group: 'org.grails', module: 'grails-encoder'
+        exclude group: 'org.apache.groovy', module: 'groovy-templates'
+        // Implementation dependencies in grails-gsp
+        exclude group: 'org.grails', module: 'grails-taglib'
+    }
+    implementation project(':grails-taglib'), { // GrailsTagLibClass, TagLibArtefactHandler, GrailsTagException
+        // API dependencies in grails-taglib
+        exclude group: 'org.grails', module: 'grails-core'
+        exclude group: 'org.grails', module: 'grails-encoder'
+        exclude group: 'org.springframework', module: 'spring-core'
+    }
+    implementation 'org.grails:grails-core', { // Holders
+        // API dependencies in grails-core
+        exclude group: 'org.apache.groovy', module: 'groovy'
+        exclude group: 'jakarta.inject', module: 'jakarta.inject-api'
+        exclude group: 'jakarta.persistence', module: 'jakarta.persistence-api'
+        exclude group: 'jakarta.annotation', module: 'jakarta.annotation-api'
+        //exclude group: 'org.grails', module: 'grails-bootstrap' // Resource is used
+        //exclude group: 'org.grails', module: 'grails-datastore-core' // MappingContext
+        exclude group: 'org.grails', module: 'grails-spring'
+        exclude group: 'org.slf4j', module: 'jcl-over-slf4j'
+        exclude group: 'org.slf4j', module: 'slf4j-api'
+        exclude group: 'org.yaml', module: 'snakeyaml'
+        exclude group: 'org.springframework', module: 'spring-beans'
+        exclude group: 'org.springframework', module: 'spring-core'
+        exclude group: 'org.springframework', module: 'spring-context'
+        exclude group: 'org.springframework', module: 'spring-tx'
+        exclude group: 'org.springframework.boot', module: 'spring-boot'
+        exclude group: 'org.springframework.boot', module: 'spring-boot-autoconfigure'
+    }
+    implementation 'org.grails:grails-encoder', { // StreamCharBuffer
+        // API dependencies in grails-encoder
+        exclude group: 'org.apache.groovy', module: 'groovy'
+        exclude group: 'org.apache.groovy', module: 'groovy-json'
+        exclude group: 'org.grails', module: 'grails-core'
+        exclude group: 'org.slf4j', module: 'jcl-over-slf4j'
+        exclude group: 'org.slf4j', module: 'slf4j-api'
+        exclude group: 'org.springframework', module: 'spring-web'
+    }
+    implementation 'org.grails:grails-web-common', { // GrailsWebRequest
+        // API dependencies in grails-web-common
+        exclude group: 'org.grails', module: 'grails-core'
+        exclude group: 'org.grails', module: 'grails-databinding'
+        exclude group: 'org.grails', module: 'grails-encoder'
+        exclude group: 'org.apache.groovy', module: 'groovy-templates'
+        //exclude group: 'org.springframework', module: 'spring-webmvc' // DispatcherServletWebRequest
+        exclude group: 'org.springframework', module: 'spring-context-support'
+    }
+
+    compileOnly 'jakarta.servlet:jakarta.servlet-api' // Provided by servlet container
+    compileOnly "jakarta.servlet.jsp:jakarta.servlet.jsp-api:$jspApiVersion" // Provided by servlet container
+    compileOnly "jakarta.el:jakarta.el-api:$elApiVersion" // Provided by servlet container
 }
 
-apply from: rootProject.file('gradle/test.gradle')
-apply from: rootProject.file('gradle/publish.gradle')
+apply from: rootProject.layout.projectDirectory.file('gradle/java-config.gradle')
+apply from: rootProject.layout.projectDirectory.file('gradle/test-config.gradle')
+apply from: rootProject.layout.projectDirectory.file('gradle/publish-config.gradle')
diff --git a/grails-web-taglib/build.gradle b/grails-web-taglib/build.gradle
index 6fcdc32d83..3028be8ded 100644
--- a/grails-web-taglib/build.gradle
+++ b/grails-web-taglib/build.gradle
@@ -1,16 +1,76 @@
-version project.projectVersion
-group "org.grails"
+plugins {
+    id 'java-library'
+    id 'groovy'
+}
 
-apply from: rootProject.file('gradle/java-config.gradle')
+version = projectVersion
+group = 'org.grails'
 
 dependencies {
-    compileOnlyApi "jakarta.servlet:jakarta.servlet-api" // api needed for TagLibrary trait
-    api "org.grails:grails-web-common"
-    api project(":grails-taglib")
-    compileOnly project(":grails-gsp")
-    testImplementation project(":grails-gsp")
 
-    testRuntimeOnly "org.grails:grails-spring"
+    implementation platform("org.grails:grails-bom:$grailsVersion")
+
+    compileOnlyApi 'jakarta.annotation:jakarta.annotation-api'
+    compileOnlyApi 'jakarta.servlet:jakarta.servlet-api' // Needed downstream to compile for TagLibrary trait
+
+    api 'org.grails:grails-core', { // ArtefactTypeAstTransformation
+        // API dependencies in grails-core
+        exclude group: 'org.apache.groovy', module: 'groovy'
+        exclude group: 'jakarta.inject', module: 'jakarta.inject-api'
+        exclude group: 'jakarta.persistence', module: 'jakarta.persistence-api'
+        exclude group: 'jakarta.annotation', module: 'jakarta.annotation-api'
+        //exclude group: 'org.grails', module: 'grails-bootstrap' // Resource
+        //exclude group: 'org.grails', module: 'grails-datastore-core' // MappingContext
+        exclude group: 'org.grails', module: 'grails-spring'
+        exclude group: 'org.slf4j', module: 'jcl-over-slf4j'
+        exclude group: 'org.slf4j', module: 'slf4j-api'
+        exclude group: 'org.yaml', module: 'snakeyaml'
+        exclude group: 'org.springframework', module: 'spring-beans'
+        exclude group: 'org.springframework', module: 'spring-core'
+        exclude group: 'org.springframework', module: 'spring-context'
+        exclude group: 'org.springframework', module: 'spring-tx'
+        exclude group: 'org.springframework.boot', module: 'spring-boot'
+        exclude group: 'org.springframework.boot', module: 'spring-boot-autoconfigure'
+    }
+    api project(':grails-taglib'), { // TagLibArtefactHandler, TagOutput, OutputContextLookupHelper, AbstractTemplateVariableBinding, TemplateVariableBinding, OutputContextLookup, OutputContext, OutputEncodingStack, AbstractTemplateVariableBinding
+        // API dependencies in grails-taglib
+        exclude group: 'org.grails', module: 'grails-core'
+        exclude group: 'org.grails', module: 'grails-encoder'
+        exclude group: 'org.springframework', module: 'spring-core'
+    }
+    api 'org.grails:grails-encoder', { // EncodingStateRegistry
+        // API dependencies in grails-encoder
+        exclude group: 'org.apache.groovy', module: 'groovy'
+        exclude group: 'org.apache.groovy', module: 'groovy-json'
+        exclude group: 'org.grails', module: 'grails-core'
+        exclude group: 'org.slf4j', module: 'jcl-over-slf4j'
+        exclude group: 'org.slf4j', module: 'slf4j-api'
+        exclude group: 'org.springframework', module: 'spring-web'
+    }
+    api 'org.grails:grails-web-common', { // WrappedResponseHolder, GrailsWebRequest, GrailsApplicationAttributes, WebUtils
+        // API dependencies in grails-web-common
+        exclude group: 'org.grails', module: 'grails-core'
+        exclude group: 'org.grails', module: 'grails-databinding'
+        exclude group: 'org.grails', module: 'grails-encoder'
+        exclude group: 'org.apache.groovy', module: 'groovy-templates'
+        //exclude group: 'org.springframework', module: 'spring-webmvc' // DispatcherServlet
+        exclude group: 'org.springframework', module: 'spring-context-support'
+    }
+
+    implementation 'org.springframework:spring-context' // ApplicationContext
+
+    compileOnly project(':grails-gsp'), { // ResourceAwareTemplateEngine
+        // API dependencies in grails-gsp
+        exclude group: 'org.grails', module: 'grails-bootstrap'
+        exclude group: 'org.apache.groovy', module: 'groovy-templates'
+    }
+
+    testImplementation project(':grails-gsp')
+    testImplementation 'org.spockframework:spock-core'
+    testImplementation 'org.springframework:spring-test'
+
+    testRuntimeOnly 'jakarta.servlet:jakarta.servlet-api'
+    testRuntimeOnly 'org.slf4j:slf4j-nop' // Get rid of warning about missing slf4j implementation during tests
 }
 
 // work around for issue #10118
@@ -20,5 +80,6 @@ compileGroovy.doLast {
     project.delete(compileGroovyTargetDir + "/META-INF/grails.factories")
 }
 
-apply from: rootProject.file('gradle/test.gradle')
-apply from: rootProject.file('gradle/publish.gradle')
\ No newline at end of file
+apply from: rootProject.layout.projectDirectory.file('gradle/java-config.gradle')
+apply from: rootProject.layout.projectDirectory.file('gradle/test-config.gradle')
+apply from: rootProject.layout.projectDirectory.file('gradle/publish-config.gradle')
\ No newline at end of file