This is an automated email from the ASF dual-hosted git repository.

benw pushed a commit to branch gradle-improvements-javax
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git

commit 12ae9918421cb480e9ff3d4b848e8bdebf42e8c8
Author: Ben Weidig <[email protected]>
AuthorDate: Sat Sep 13 13:07:01 2025 +0200

    TAP5-2809: basic gradle setup
    
    An attempt to replicate the basic gradle setup from (future) master.
    The project doesn't build, but Gradle itself is running clean without
    instantly failing.
---
 beanmodel/build.gradle                             |  60 +-
 build.gradle                                       | 806 ++++++++-------------
 buildSrc/build.gradle                              |  20 +
 .../main/groovy/t5build/GenerateChecksums.groovy   |  61 ++
 buildSrc/src/main/groovy/t5build/SSshExec.groovy   |  21 +
 buildSrc/src/main/groovy/t5build/Scp.groovy        |  36 +
 buildSrc/src/main/groovy/t5build/SshTask.groovy    |  94 +++
 .../main/groovy/t5build/TapestryBuildLogic.groovy  |  22 +
 .../main/groovy/tapestry.java-convention.gradle    |  66 ++
 .../tapestry.junit4-legacy-convention.gradle       |  12 +
 .../main/groovy/tapestry.junit5-convention.gradle  |  13 +
 .../groovy/tapestry.junit5-spock-convention.gradle |   9 +
 .../src/main/groovy/tapestry.ssh-convention.gradle |  15 +
 .../groovy/tapestry.testing-base-convention.gradle |  64 ++
 .../main/groovy/tapestry.testng-convention.gradle  |  27 +
 commons/build.gradle                               |  26 +-
 genericsresolver-guava/build.gradle                |  16 +-
 gradle/libs.versions.toml                          | 168 +++++
 gradle/wrapper/gradle-wrapper.jar                  | Bin 59821 -> 43462 bytes
 gradle/wrapper/gradle-wrapper.properties           |   4 +-
 gradlew                                            |  41 +-
 gradlew.bat                                        |  15 +-
 plastic/build.gradle                               |  23 +-
 quickstart/build.gradle                            |  24 +-
 settings.gradle                                    |  87 ++-
 sha256.gradle                                      |  25 -
 ssh.gradle                                         | 124 ----
 tapestry-beanvalidator/build.gradle                |  44 +-
 tapestry-cdi/build.gradle                          |  91 ++-
 tapestry-clojure/build.gradle                      |  29 +-
 tapestry-core/build.gradle                         | 222 +++---
 tapestry-func/build.gradle                         |  12 +-
 tapestry-hibernate-core/build.gradle               |  21 +-
 tapestry-hibernate/build.gradle                    |  39 +-
 tapestry-http/build.gradle                         |  34 +-
 tapestry-internal-test/build.gradle                |   6 +-
 tapestry-ioc-jcache/build.gradle                   |  54 +-
 tapestry-ioc-junit/build.gradle                    |  13 +-
 tapestry-ioc/build.gradle                          |  35 +-
 tapestry-javadoc/build.gradle                      |  51 +-
 tapestry-jmx/build.gradle                          |  17 +-
 tapestry-jpa/build.gradle                          |  53 +-
 tapestry-json/build.gradle                         |  18 +-
 tapestry-kaptcha/build.gradle                      |  18 +-
 tapestry-latest-java-tests/build.gradle            |  26 +-
 tapestry-mongodb/build.gradle                      |  27 +-
 tapestry-openapi-viewer/build.gradle               |   7 +-
 tapestry-rest-jackson/build.gradle                 |  16 +-
 tapestry-runner/build.gradle                       |  27 +-
 tapestry-spock/build.gradle                        |  22 +-
 tapestry-spring/build.gradle                       |  44 +-
 tapestry-test-data/build.gradle                    |   4 +-
 tapestry-test/build.gradle                         |  33 +-
 tapestry-upload/build.gradle                       |  25 +-
 tapestry-version-migrator/build.gradle             |  18 +-
 tapestry-webresources/build.gradle                 |  78 +-
 tapestry5-annotations/build.gradle                 |   2 +-
 57 files changed, 1710 insertions(+), 1255 deletions(-)

diff --git a/beanmodel/build.gradle b/beanmodel/build.gradle
index 5adf920bd..68391ae01 100644
--- a/beanmodel/build.gradle
+++ b/beanmodel/build.gradle
@@ -1,39 +1,47 @@
-import org.gradle.plugins.ide.idea.model.*
-import t5build.*
-
-description = "Fast class property discovery, reading and writing library 
based on bytecode generation. Extracted from Apache Tapestry, but not dependent 
on the Web framework (tapestry-core) nor the IoC one (tapestry-ioc)."
-
-//apply plugin: JavaPlugin
-apply plugin: 'antlr'
+plugins {
+    id 'tapestry.junit5-convention'
+    id 'antlr'
+}
 
-buildDir = 'target/gradle-build'
+description = 'Fast class property discovery, reading and writing library 
based on bytecode generation. Extracted from Apache Tapestry, but not dependent 
on the Web framework (tapestry-core) nor the IoC one (tapestry-ioc).'
 
 dependencies {
-    api project(":plastic")
-    api project(":tapestry5-annotations")
-    api project(":commons")
-    implementation "org.slf4j:slf4j-api:${versions.slf4j}"
+    implementation project(':plastic')
+    implementation project(':tapestry5-annotations')
+    implementation project(':commons')
 
-    // ANTLR tool path used with the generateGrammarSource task
-    antlr "org.antlr:antlr:3.5.2"
+    implementation libs.slf4j.api
+
+    antlr libs.antlr
 
     // Transitive will bring in the unwanted string template library as well
-    implementation "org.antlr:antlr-runtime:3.5.2", {
-        exclude group: "org.antlr", module: "stringtemplate"
+    implementation(libs.antlr.runtime) {
+        exclude group: 'org.antlr', module: 'stringtemplate'
     }
 
-    testImplementation "org.testng:testng:${versions.testng}", { transitive = 
false }
-    testImplementation "org.easymock:easymock:${versions.easymock}"
-    testImplementation 
"org.junit.jupiter:junit-jupiter:${versions.junitJupiter}"
-    testImplementation "org.spockframework:spock-core:${versions.spock}"
+    testImplementation platform(libs.spock.bom)
+    testImplementation libs.spock.core
 }
 
-clean.delete generateGrammarSource.outputDirectory
+tasks.named('clean') {
+    delete generateGrammarSource.outputDirectory
+}
 
-compileJava {
-    options.fork(memoryMaximumSize: '512m')
+tasks.named('compileJava', JavaCompile) {
+    dependsOn generateGrammarSource
+    options.fork = true
+    options.forkOptions.memoryMaximumSize = '512M'
 }
 
-test {
-    useJUnit()
-}
\ No newline at end of file
+tasks.named('sourcesJar') {
+    dependsOn 'generateGrammarSource'
+    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
+}
+
+sourceSets {
+    main {
+        java {
+            srcDir generateGrammarSource.outputDirectory
+        }
+    }
+}
diff --git a/build.gradle b/build.gradle
index 184c8da88..300c62ef3 100755
--- a/build.gradle
+++ b/build.gradle
@@ -1,291 +1,125 @@
-description = "Apache Tapestry 5 Project"
-
+import t5build.GenerateChecksums
+import t5build.TapestryBuildLogic
 import org.apache.tools.ant.filters.ReplaceTokens
 
-apply plugin: "base"
-apply plugin: "maven-publish"
-
-apply from: "ssh.gradle"
-apply from: "md5.gradle"
-apply from: "sha256.gradle"
-
-project.ext.versions = [
-    jetty: "8.1.19.v20160209",
-    tomcat: "7.0.70",
-    testng: "7.5.1",
-    easymock: "5.4.0",
-    servletapi: "3.0.1",
-    spock: "2.3-groovy-3.0",
-    hibernate: "5.4.32.Final",
-    slf4j: "1.7.25",
-    geb: "2.0",
-    selenium: "4.5.0",
-    seleniumServer: "4.12.1",
-    jackson: "2.13.1",
-    jsonschemaGenerator: "4.20.0",
-    junitJupiter: "5.10.2",
-    commonsLang: "3.17.0",
-    commonsIo: "2.17.0",
-    webdriverManager: "5.3.1",
-    antlrRuntime: "3.5.3",
-    hsqldb: "2.7.3:jdk8",
-    snakeyaml: "2.3"
-]
-
-def artifactSuffix = ""
-
-// Artifacts that have both an unsuffixed artifact from the javax branch
-// and a suffixed one from the master branch
-def suffixedArtifactNames = ["tapestry-core", "tapestry-http", "tapestry-test",
-        "tapestry-runner", "tapestry-spring", "tapestry-kaptcha",
-        "tapestry-openapi-viewer", "tapestry-upload", "tapestry-jmx", 
-        "tapestry-jpa", "tapestry-kaptcha", "tapestry-openapi-viewer",
-        "tapestry-rest-jackson", "tapestry-webresources", "tapestry-cdi", 
-        "tapestry-ioc", "tapestry-ioc-jcache", "tapestry-jmx", 
"tapestry-spock",
-        "tapestry-clojure", "tapestry-hibernate", "tapestry-hibernate-core",
-        "tapestry-ioc-junit", "tapestry-latest-java-tests", "tapestry-mongodb",
-        "tapestry-spock", "tapestry-beanvalidator"]
-
-ext.continuousIntegrationBuild = Boolean.getBoolean("ci")
-
-// Provided so that the CI server can override the normal version number for 
nightly builds.
-project.version = tapestryVersion()
-
-// Remember that when generating a release, this should be incremented. Also 
don"t forget to
-// tag the release in Git.
-// Version number is always "5.x(.y)?-SNAPSHOT" and only gets fixed, e.g. to 
5.4-alpha-1
-// during a release
-
-def tapestryVersion() {
-
-    def major = "5.10.0"
-    def minor = ""
-
-    // When building on the CI server, make sure -SNAPSHOT is appended, as it 
is a nightly build.
-    // When building normally, or for a release, no suffix is desired.
-    continuousIntegrationBuild ? major + "-SNAPSHOT" : major + minor
+plugins {
+    id 'base'
+    id 'maven-publish'
 }
 
-// Let analysis.apache.org get in touch with our builds
-
-project.ext {
-
-    stagingUrl = 
"https://repository.apache.org/service/local/staging/deploy/maven2/";
-    snapshotUrl = 
"https://repository.apache.org/content/repositories/snapshots";
-
-    doSign = !project.hasProperty("noSign") && 
project.hasProperty("signing.keyId")
+description = 'Apache Tapestry 5 Project'
+
+ext {
+    tapestryMajorVersion = '5.10.0'
+    tapestryMinorVersion = '' // Use for release suffixes like '-alpha-1'
+
+    artifactSuffix = '-jakarta'
+
+    // Artifacts that have both an unsuffixed artifact from the javax branch
+    // and a suffixed one from the master branch
+    suffixedArtifactNames = [
+        'tapestry-beanvalidator',
+        'tapestry-cdi',
+        'tapestry-clojure',
+        'tapestry-hibernate-core',
+        'tapestry-hibernate',
+        'tapestry-http',
+        'tapestry-ioc-jcache',
+        'tapestry-ioc-junit',
+        'tapestry-ioc',
+        'tapestry-jmx',
+        'tapestry-jpa',
+        'tapestry-kaptcha',
+        'tapestry-latest-java-tests',
+        'tapestry-mongodb',
+        'tapestry-openapi-viewer',
+        'tapestry-rest-jackson',
+        'tapestry-runner',
+        'tapestry-spock',
+        'tapestry-spring',
+        'tapestry-test',
+        'tapestry-upload',
+        'tapestry-webresources',
+        'tapestry-core',
+    ]
+
+    continuousIntegrationBuild = Boolean.getBoolean('ci')
+    stagingUrl = 
'https://repository.apache.org/service/local/staging/deploy/maven2/'
+    snapshotUrl = 
'https://repository.apache.org/content/repositories/snapshots'
+    doSign = !project.hasProperty('noSign') && 
project.hasProperty('signing.keyId')
 
     // apacheDeployUserName and apacheDeployPassword should be specified in 
~/.gradle/gradle.properties
 
-    deployUsernameProperty = isSnapshot() ? "snapshotDeployUserName" : 
"apacheDeployUserName"
-    deployPasswordProperty = isSnapshot() ? "snapshotDeployPassword" : 
"apacheDeployPassword"
-
-    canDeploy = [deployUsernameProperty, deployPasswordProperty, 
"apacheArchivesFolder"].every { project.hasProperty(it) }
-
-    // These are all deferred inside closures, to allow people without the 
necessary values in their
-    // gradle.properties to build locally, just not deploy. getProperty() 
throws an exception if the property
-    // is not present.
-    deployUsername = { getProperty(deployUsernameProperty) }
-    deployPassword = { getProperty(deployPasswordProperty) }
-
-    archiveDeployFolder = { getProperty("apacheArchivesFolder") }
-}
-
-println "JDK: " + System.getProperty("java.version")
-
-//println "Can deploy? $canDeploy"
-//println "Is snapshot? isSnapshot"
-//println "deployUsernameProperty $deployUsernameProperty"
-//println "deployPasswordProperty $deployPasswordProperty"
-//println "continuousIntegrationBuild? $continuousIntegrationBuild"
-
-allprojects {
-
-    apply plugin: "eclipse"
-    apply plugin: "idea"
+    def deployFlavor = TapestryBuildLogic.isSnapshot(project) ? 'snapshot' : 
'apache'
 
-    repositories {
-        mavenLocal()
-        mavenCentral()
-
-        // All things JBoss/Hibernate
-        maven {
-            name "JBoss"
-            url 
"https://repository.jboss.org/nexus/content/repositories/releases/";
-        }
-    }
-
-    configurations {
-        // Non-code artifacts, such as sources JARs and zipped JavaDocs
-        meta
-    }
+    deployUsername = providers.gradleProperty("${deployFlavor}DeployUserName")
+    deployPassword = providers.gradleProperty("${deployFlavor}DeployPassword")
+    archiveDeployFolder = providers.gradleProperty('apacheArchivesFolder')
 
+    canDeploy = [
+        deployUsername,
+        deployPassword,
+        archiveDeployFolder
+    ].every { it.present }
 }
 
-idea {
-    project {
-        languageLevel = "1.8"
-    }
-}
+// Provided so that the CI server can override the normal version number for 
nightly builds.
+version = TapestryBuildLogic.tapestryVersion(project)
 
 // Specific to top-level build, not set for subprojects:
-
 configurations {
     javadoc
     published.extendsFrom archives, meta
+
     if (doSign) {
         published.extendsFrom signatures
     }
+
     binaries // additional dependencies included in the binary archive
 }
 
 dependencies {
-    if (JavaVersion.current() != JavaVersion.VERSION_1_8) {
-        javadoc project(":tapestry-javadoc")
+    if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_11)) {
+        javadoc project(':tapestry-javadoc')
+        meta providers.provider {
+            tasks.named('aggregateJavadoc').get().outputs.files
+        }
     }
 
     // From tapestry-ioc:
-    binaries "javax.inject:javax.inject:1"
-    binaries "org.slf4j:slf4j-api:${versions.slf4j}"
-    binaries "commons-codec:commons-codec:1.17.1"
-    binaries "org.antlr:antlr-runtime:${versions.antlrRuntime}", { transitive 
= false }
+    binaries libs.javax.inject
+    binaries libs.slf4j.api
+    binaries libs.commons.codec
+    binaries libs.antlr.runtime, {
+        transitive = false
+    }
 }
 
-String jdkVersion = System.properties['java.version']
-def jdkMajorVersion = jdkVersion.substring(0, jdkVersion.indexOf(".")) // 1, 
9, 10...
-
 subprojects {
-    
-    def specifyMaxPermSize = jdkVersion ==~ /1\.[67].+/
-
-    apply plugin: "maven-publish"  // for deployment
+    plugins.apply 'tapestry.java-convention'
+    plugins.apply 'maven-publish'
+    plugins.apply 'project-report'
+    plugins.apply 'jacoco'
+    plugins.apply 'groovy'
 
-    configurations {
-        provided
-    }
-
-    apply plugin: "java"
-    apply plugin: "java-library"
-    apply plugin: "groovy" // mostly for testing
-
-    apply plugin: "project-report"
-    apply plugin: "jacoco"
-    apply plugin: "base"
-    
     jacoco {
-        toolVersion = "0.8.7"
-    }
-    
-    sourceCompatibility = "1.8"
-    targetCompatibility = "1.8"
-
-    // See http://jira.codehaus.org/browse/GRADLE-784
-
-    sourceSets {
-        main {
-            compileClasspath += configurations.provided
-        }
-        test {
-            compileClasspath += configurations.provided
-            runtimeClasspath += configurations.provided
-        }
+        toolVersion = '0.8.7'
     }
 
-    idea.module {
-        scopes.PROVIDED.plus += [configurations.provided]
-    }
-
-    eclipse.classpath.plusConfigurations += [configurations.provided]
-
     dependencies {
-        
-        // 
https://docs.gradle.org/7.3.3/userguide/upgrading_version_6.html#potential_breaking_changes,
 
-        // Table 1. Common configuration upgrades
-        testImplementation "org.spockframework:spock-core:${versions.spock}"
-         
-        testRuntimeOnly "org.slf4j:slf4j-log4j12:${versions.slf4j}"
-    }
-
-    compileTestGroovy {
-        configure(groovyOptions.forkOptions) {
-            memoryMaximumSize = '1g'
-            jvmArgs = ['-Xms512m', '-Xmx1g']
-            if (specifyMaxPermSize){
-              jvmArgs << '-XX:MaxPermSize=512m'
-            }
-        }
+        implementation libs.slf4j.api
     }
 
-    tasks.withType(Test) {
-        useTestNG()
-
-        options.suites("src/test/conf/testng.xml")
-        if (specifyMaxPermSize){
-          maxHeapSize "400M"
-          jvmArgs("-XX:MaxPermSize=200m")
-        }else{
-          maxHeapSize "600M"
-        }
-    
-        // Needed to have XMLTokenStreamTests.testStreamEncoding() passing on 
Java 9+
-        if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_1_9)) {
-            jvmArgs("--add-opens=java.base/java.nio.charset=ALL-UNNAMED");
-        }
-    
-        // Turn off live service reloading
-
-        systemProperties["tapestry.service-reloading-enabled"] = "false"
-        systemProperties["java.io.tmpdir"] = temporaryDir.absolutePath
-
-        jvmArgs("-Dfile.encoding=UTF-8")
-
-        environment.LANG = 'en_US.UTF-8'
-        
-        if (continuousIntegrationBuild) {
-          // Travis runs our builds with TERM=dumb and kills it if we don't 
produce any
-          // output for 10 minutes, so we log some task execution progress 
-
-          testLogging {
-            exceptionFormat "full"
-          }
-
-          def numberOfTestsExecuted = 0
-          afterTest { descriptor, result->
-            numberOfTestsExecuted++
-            if (numberOfTestsExecuted % 25 == 0){
-              logger.lifecycle "$numberOfTestsExecuted tests executed"
-            }
-          }
+    tasks.withType(GroovyCompile).configureEach {
+        if (name == 'compileTestGroovy') {
+            options.fork = true
+            options.forkOptions.memoryMaximumSize = '1g'
+            options.forkOptions.jvmArgs = ['-Xms512m', '-Xmx1g']
         }
     }
 
-    jar {
-       // println "JAR projectDir: " + projectDir.getName().replaceAll("5", 
"").replaceAll("tapestry-", "").replaceAll("-", "");
-        from(projectDir) {
-            include "*.txt"
-            into "META-INF"
-        }
-        manifest {
-               attributes("Automatic-Module-Name": "org.apache.tapestry." + 
projectDir.getName()
-                       .replaceAll("tapestry5", "tapestry")
-                       .replaceAll("tapestry-", "")
-                       .replaceAll("-", ""))                   
-               if (projectDir.getName().equals("tapestry-version-migrator")) {
-                       attributes("Main-Class": 
"org.apache.tapestry5.versionmigrator.Main")
-               }
-        }
-    }
-    
-    assemble.dependsOn(processResources, compileJava, jar)
-
-    task sourcesJar(type: Jar) {
-        dependsOn classes
-        archiveClassifier = "sources"
-        from sourceSets.main.allSource
-        from(projectDir) {
-            include "*.txt"
-            into "META-INF"
-        }
+    tasks.named('assemble') {
+        dependsOn processResources, compileJava, jar
     }
 
     artifacts {
@@ -301,42 +135,41 @@ subprojects {
             published.extendsFrom signatures
         }
     }
-    
+
     publishing {
         publications {
             mavenJava(MavenPublication) {
                 version = parent.version
-                groupId = "org.apache.tapestry"
+                groupId = 'org.apache.tapestry'
                 if (suffixedArtifactNames.contains(project.name)) {
                     artifactId = project.name + artifactSuffix
                 }
                 from components.java
                 artifact sourcesJar
-                
-                
+
                 pom {
                     name = project.name
                     // TODO: find some way to get the subproject description 
here.
-                    // description = 
-                    url = "https://tapestry.apache.org/";
+                    // description =
+                    url = 'https://tapestry.apache.org/'
                     licenses {
                         license {
                             name = 'The Apache License, Version 2.0'
-                                    url = 
'http://www.apache.org/licenses/LICENSE-2.0.txt'
+                            url = 
'http://www.apache.org/licenses/LICENSE-2.0.txt'
                         }
                     }
                     scm {
                         connection = 
'scm:git:https://gitbox.apache.org/repos/asf/tapestry-5.git'
-                                developerConnection = 
'scm:git://gitbox.apache.org/repos/asf/tapestry-5.git'
-                                url = 
'https://git-wip-us.apache.org/repos/asf?p=tapestry-5.git;a=summary'
+                        developerConnection = 
'scm:git://gitbox.apache.org/repos/asf/tapestry-5.git'
+                        url = 
'https://git-wip-us.apache.org/repos/asf?p=tapestry-5.git;a=summary'
                     }
                     // Changes the generated pom.xml so its dependencies on 
suffixed artifacts
                     // get properly updated with suffixed artifact ids
                     withXml {
                         def artifactIdQName = new groovy.namespace.QName(
-                                "http://maven.apache.org/POM/4.0.0";, 
"artifactId")
-                        def node = asNode();
-                        def dependencies = node.get("dependencies")[0]
+                                'http://maven.apache.org/POM/4.0.0', 
'artifactId')
+                        def node = asNode()
+                        def dependencies = node.get('dependencies')[0]
                         if (dependencies != null) {
                             dependencies.'*'.forEach {
                                 def artifactIdNode = 
it.getAt(artifactIdQName)[0]
@@ -347,103 +180,87 @@ subprojects {
                         }
                     }
                 }
-                matching {
-                    it.name.endsWith(".jar") || it.name.endsWith(".pom")
-                }
             }
         }
         if (canDeploy) {
             repositories {
                 mavenLocal()
-                if (continuousIntegrationBuild) {
-                    maven {
-                        name = "apacheSnapshots"
-                        url = snapshotUrl
-                        credentials {
-                            username = deployUsername()
-                            password = deployPassword()
-                        }
-                    }
-                }
-                else {
-                    maven {
-                        name = "apacheStaging"
-                        url = stagingUrl
-                        credentials {
-                            username = deployUsername()
-                            password = deployPassword()
-                        }
+
+                def repoUrl = continuousIntegrationBuild ? snapshotUrl : 
stagingUrl
+                def repoName = continuousIntegrationBuild ? 'apacheSnapshots' 
: 'apacheStaging'
+
+                maven {
+                    name = repoName
+                    url = repoUrl
+                    credentials {
+                        username = deployUsername.get()
+                        password = deployPassword.get()
                     }
                 }
-                
             }
         }
     }
 
     if (doSign) {
-        apply plugin: "signing"
+        apply plugin: 'signing'
         signing {
             sign publishing.publications.mavenJava
         }
     }
-    
+
     def actuallyPublish = !artifactSuffix.isEmpty() || 
suffixedArtifactNames.contains(project.name)
-    // println "XXXXXX Actually publish? " + actuallyPublish + " project " + 
project.name + " " + "!artifactSuffix.isEmpty() " + !artifactSuffix.isEmpty()
+    // println 'XXXXXX Actually publish? ' + actuallyPublish + ' project ' + 
project.name + ' ' + '!artifactSuffix.isEmpty() ' + !artifactSuffix.isEmpty()
     if (!actuallyPublish) {
-        tasks.withType(PublishToMavenRepository).configureEach { it.enabled = 
false }
-        tasks.withType(PublishToMavenLocal).configureEach { it.enabled = false 
}
+        tasks.withType(PublishToMavenRepository).configureEach {
+            it.enabled = false
+        }
+        tasks.withType(PublishToMavenLocal).configureEach {
+            it.enabled = false
+        }
     }
 
-    task uploadPublished {
-
+    tasks.register('uploadPublished') {
         doFirst {
             if (!canDeploy) {
-                throw new InvalidUserDataException("Missing upload 
credentials. Set '$deployUsernameProperty' and '$deployPasswordProperty' root 
project properties.")
+                throw new InvalidUserDataException("Missing upload 
credentials. Set '${deployUsernameProperty}' and '${deployPasswordProperty}' 
root project properties.")
             }
         }
-
     }
 }
 
-subprojects.each { project.evaluationDependsOn(it.name) }
-
-subprojects {
-
-    configurations.all {
-
-        resolutionStrategy.force "antlr:antlr:2.7.7",
-            "cglib:cglib-nodep:2.2",
-            "commons-codec:commons-codec:1.10",
-            "commons-io:commons-io:${versions.commonsIo}",
-            "commons-logging:commons-logging:1.1.3",
-            "org.hsqldb:hsqldb:${versions.hsqldb}",
-            "org.antlr:antlr-runtime:${versions.antlrRuntime}",
-            "org.apache.tomcat:dbcp:6.0.32",
-            "org.hamcrest:hamcrest-core:1.3",
-            "org.json:json:20140107",
-            "org.yaml:snakeyaml:${versions.snakeyaml}",
-            "xml-apis:xml-apis:1.4.01"
-
-    }
-}
 
 // Cribbed from 
https://github.com/hibernate/hibernate-core/blob/master/release/release.gradle#L19
 
-task aggregateJavadoc(type: Javadoc) {
+tasks.register('aggregateJavadoc', Javadoc) {
+    group = 'Documentation'
+    description = 'Build the aggregated JavaDocs for all modules'
+
     dependsOn configurations.javadoc
-    group "Documentation"
 
-    description "Build the aggregated JavaDocs for all modules"
-    maxMemory "512m"
-    destinationDir file("$buildDir/documentation/javadocs")
+    maxMemory = '512m'
+    destinationDir = 
layout.buildDirectory.dir('documentation/javadocs').get().asFile
 
-    def tapestryStylesheet = file("src/javadoc/stylesheet7.css")
+    def tapestryStylesheet = file('src/javadoc/stylesheet7.css')
     int thisYear = java.time.Year.now().getValue()
 
     def allMainSourceSets = subprojects*.sourceSets*.main.flatten()
     def allMainJavaFiles = allMainSourceSets*.java
     def allMainJavaSrcDirs = allMainJavaFiles*.srcDirs
 
+    if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)) {
+        exclude 'org/apache/tapestry5/spring/**'
+    }
+
+    exclude 'org/apache/tapestry5/internal/plastic/asm/**'
+    exclude 'org/apache/tapestry5/internal/webresources/**'
+    exclude 'org/apache/tapestry5/webresources/modules/**'
+
+    source allMainJavaFiles
+
+    classpath += files(allMainSourceSets*.compileClasspath)
+
+    inputs.files allMainJavaSrcDirs
+
     options {
         splitIndex true
         linkSource true
@@ -452,28 +269,24 @@ task aggregateJavadoc(type: Javadoc) {
         header "Tapestry API - ${project.version}"
         docTitle "Tapestry API - ($project.version)"
         bottom "${project.version} - Copyright &copy; 2003-${thisYear} <a 
href=\"http://tapestry.apache.org/\";>The Apache Software Foundation</a>."
-        use = true // 'use' seems to be a reserved word for the DSL
-        links "https://docs.oracle.com/javase/8/docs/api/";
-        links "https://docs.oracle.com/javaee/7/api/";
-        if (JavaVersion.current() != JavaVersion.VERSION_1_8) {
+        setUse(use)
+        links 'https://docs.oracle.com/javase/8/docs/api/'
+
+        if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_11)) {
             tagletPath Collections.unmodifiableList(new ArrayList<>((Set) 
configurations.javadoc.files))
         }
         //  Uncomment jFlags to debug `./gradlew aggregateJavadoc`
-//        jFlags '-Xdebug', 
'-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'
+        //        jFlags '-Xdebug', 
'-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'
         addStringOption '-source-path', 
files(allMainJavaSrcDirs.flatten()).asPath
         addStringOption 'source', '8'
-        taglets "org.apache.tapestry5.javadoc.TapestryDocTaglet"
-    }
-
-    exclude "org/apache/tapestry5/internal/plastic/asm/**"
-    exclude "org/apache/tapestry5/internal/webresources/**"
-    exclude "org/apache/tapestry5/webresources/modules/**"
-    
-    source allMainJavaFiles
-
-    classpath += files(allMainSourceSets*.compileClasspath)
+        taglets 'org.apache.tapestry5.javadoc.TapestryDocTaglet'
 
-    inputs.files allMainJavaSrcDirs
+        // Javadoc became stricer, so lets do this to actually build for now
+        if (JavaVersion.current().isJava9Compatible()) {
+            addBooleanOption('html5', true)
+            addStringOption('Xdoclint:none', '-quiet')
+        }
+    }
 
     // As part of generating the documentation, ALSO copy any related files:
     // Any extra images (Tapestry logo)
@@ -483,138 +296,159 @@ task aggregateJavadoc(type: Javadoc) {
         copy {
             from allMainJavaSrcDirs
             into aggregateJavadoc.destinationDir
-            exclude "**/*.java"
-            exclude "**/*.xdoc"
-            exclude "**/package.html"
+            exclude '**/*.java'
+            exclude '**/*.xdoc'
+            exclude '**/package.html'
         }
 
         copy {
-            from file("src/javadoc/images")
+            from file('src/javadoc/images')
             into aggregateJavadoc.destinationDir
         }
     }
 }
 
-task typeScriptDocs() {
-    group "Documentation"
-    description "Builds typedoc documentation for all TypeScript sources"
-    dependsOn project(":tapestry-core").tasks.generateTypeScriptDocs
+tasks.register('typeScriptDocs') {
+    group = 'Documentation'
+    description = 'Builds typedoc documentation for all TypeScript sources'
+
+    dependsOn(project(':tapestry-core').tasks.named('generateTypeScriptDocs'))
 }
 
-dependencies {
-    if (JavaVersion.current() != JavaVersion.VERSION_1_8) {
-        meta aggregateJavadoc.outputs.files
+tasks.register('combinedJacocoReport', JacocoReport) {
+    group = 'Verification'
+    description = 'Generates combined JaCoCo coverage report for all 
subprojects'
+    
+    def excludedProjects = [
+        'quickstart',
+        'beanmodel',
+        'commons',
+        'genericsresolver-guava',
+        'tapestry5-annotations',
+        'tapestry-internal-test',
+        'tapestry-runner',
+        'tapestry-test-constants',
+        'tapestry-test-data',
+        'tapestry-ioc-jcache'
+    ]
+
+    def subprojectsToConsider = subprojects.findAll {
+        !excludedProjects.contains(it.name)
+    }
+
+    dependsOn = subprojectsToConsider.test
+
+    
additionalSourceDirs.from(files(subprojectsToConsider.sourceSets.main.allSource.srcDirs))
+    
sourceDirectories.from(files(subprojectsToConsider.sourceSets.main.allSource.srcDirs))
+    classDirectories.from(files(subprojectsToConsider.sourceSets.main.output))
+    
executionData.from(files(subprojectsToConsider.jacocoTestReport.executionData).filter
 {
+        it.exists()
+    })
+    jacocoClasspath = 
files(subprojectsToConsider.jacocoTestReport.jacocoClasspath)
+
+    reports {
+        html {
+            required = true
+            outputLocation = layout.buildDirectory.dir('reports/jacoco')
+        }
+        xml.required = false
+        csv.required = false
     }
 }
 
-task combinedJacocoReport(type:JacocoReport){
-  def subprojectsToConsider = subprojects.findAll {it.name != 'quickstart' && 
it.name != 'beanmodel' && it.name != 'commons' && it.name != 
'genericsresolver-guava' && it.name != 'tapestry5-annotations' && it.name != 
'tapestry-internal-test' && it.name != 'tapestry-runner' && it.name != 
'tapestry-test-constants' && it.name != 'tapestry-test-data' && it.name != 
'tapestry-ioc-jcache'}
-  dependsOn = subprojectsToConsider.test
-  
additionalSourceDirs.from(files(subprojectsToConsider.sourceSets.main.allSource.srcDirs))
-  
sourceDirectories.from(files(subprojectsToConsider.sourceSets.main.allSource.srcDirs))
-  classDirectories.from(files(subprojectsToConsider.sourceSets.main.output))
-  
executionData.from(files(subprojectsToConsider.jacocoTestReport.executionData).filter
 { it.exists() })
-  jacocoClasspath = 
files(subprojectsToConsider.jacocoTestReport.jacocoClasspath)
-  reports {
-      html {
-        required = true
-        destination = file("$buildDir/reports/jacoco")
-      }
-      xml {
-        required = false
-      }
-      csv {
-        required = false
-      }
-  }
-  onlyIf = {
-      true
-  }
-}
+tasks.register('continuousIntegration') {
+    group = 'Verification'
+    description = 'Runs a full CI build: assembles all artifacts, runs all 
checks, and generates aggregate reports.'
 
-task continuousIntegration {
     def dependants = [
-            'tapestry-core:testWithPrototypeAndRequireJsDisabled',
-            'tapestry-core:testWithJqueryAndRequireJsDisabled',
-            subprojects.build, // jQuery and Require.js enabled
-            'tapestry-core:testWithPrototypeAndRequireJsEnabled', 
-            combinedJacocoReport]
+        'tapestry-core:testWithPrototypeAndRequireJsDisabled',
+        'tapestry-core:testWithJqueryAndRequireJsDisabled',
+        'tapestry-core:testWithPrototypeAndRequireJsEnabled', 
+        subprojects.check, // jQuery and Require.js enabled
+        combinedJacocoReport
+    ]
+
     // tapestry-javadoc doesn't work with Java 8 anymore. That's why it's only 
added if != 8.
     if (JavaVersion.current() != JavaVersion.VERSION_1_8) {
         dependants << aggregateJavadoc
     }
-    dependsOn dependants
-    description "Task executed on Jenkins CI server after Git commits"
+
+    dependsOn(dependants)
 }
 
-task zippedSources(type: Zip) {
-    description "Creates a combined Zip file of all sub-project's sources"
-    group "Release artifact"
+
+tasks.register('zippedSources', Zip) {
+    group = 'Release artifact'
+    description = "Creates a combined Zip file of all sub-project's sources"
     
-    dependsOn("tapestry-core:compileTypeScript")
+    dependsOn 'tapestry-core:compileTypeScript'
 
-    destinationDirectory = buildDir
-    archiveBaseName = "apache-tapestry"
+    destinationDirectory = layout.buildDirectory
+    archiveBaseName = 'apache-tapestry'
     version project.version
-    archiveClassifier = "sources"
+    archiveClassifier = 'sources'
 
     from project.projectDir
-    exclude "out/**"
-    exclude "**/*.iml"
-    exclude "**/*.ipr"
-    exclude "**/*.iws"
-    exclude "**/.*/**"
-    exclude "**/bin/**"
-    exclude "**/target/**"
-    exclude "**/build/**"
-    exclude "**/test-output/**"  // Left around by TestNG sometimes
-    exclude "**/modules/***.js"
-    exclude "**/es-modules/***.js"
+    exclude 'out/**'
+    exclude '**/*.iml'
+    exclude '**/*.ipr'
+    exclude '**/*.iws'
+    exclude '**/.*/**'
+    exclude '**/bin/**'
+    exclude '**/target/**'
+    exclude '**/build/**'
+    exclude '**/test-output/**'  // Left around by TestNG sometime
+    exclude '**/modules/***.js'
+    exclude '**/es-modules/***.js'
 }
 
-task zippedApidoc(type: Zip) {
+tasks.register('zippedApidoc', Zip) {
+    group = 'Release artifact'
+    description = "Zip archive of the project's aggregate JavaDoc and 
TypeScript documentation"
+
     dependsOn typeScriptDocs
     dependsOn aggregateJavadoc
-    description "Zip archive of the project's aggregate JavaDoc and TypeScript 
documentation"
-    group "Release artifact"
 
-    destinationDirectory = buildDir
-    archiveBaseName = "apache-tapestry"
+    destinationDirectory = layout.buildDirectory
+    archiveBaseName = 'apache-tapestry'
     version project.version
-    archiveClassifier = "apidocs"
+    archiveClassifier = 'apidocs'
 
-    from file("src/docroot-template"), {
+    from(file('src/docroot-template')) {
         filter ReplaceTokens, tokens: [version: project.version]
-        include "*.html"
+        include '*.html'
     }
 
-    from file("src/docroot-template"), {
-        exclude "*.html"
+    from(file('src/docroot-template')) {
+        exclude '*.html'
     }
 
-    into "apidocs", { from aggregateJavadoc.outputs.files }
-
-
-    into "typescript", { from typeScriptDocs.outputs.files }
+    into('apidocs') {
+        from aggregateJavadoc.outputs.files
+    }
 
+    into('typescript') {
+        from typeScriptDocs.outputs.files
+    }
 }
 
-task zippedBinaries(type: Zip) {
-    description "Zip archive of binaries of each sub-project"
+tasks.register('zippedBinaries', Zip) {
+    group 'Release artifact'
+    description 'Zip archive of binaries of each sub-project'
+
     // TODO: Plus dependencies?
-    group "Release artifact"
     // This may create a few unwanted dependencies, but does
     // seem to ensure that the subprojects are created
     inputs.files subprojects*.configurations*.archives.artifacts.files
 
-    destinationDirectory = buildDir
-    archiveBaseName = "apache-tapestry"
+    destinationDirectory = layout.buildDirectory
+    archiveBaseName = 'apache-tapestry'
     version project.version
-    archiveClassifier = "bin"
+    archiveClassifier = 'bin'
 
     // This is via some experimentation
     from subprojects*.configurations*.archives.artifacts*.file*.findAll {
-        !(it.name.endsWith(".asc") || it.name.startsWith("quickstart"))
+        !(it.name.endsWith('.asc') || it.name.startsWith('quickstart'))
     }
 
     from configurations.binaries
@@ -622,12 +456,12 @@ task zippedBinaries(type: Zip) {
     // Pick up various licenses and notices
 
     from(projectDir) {
-        include "*.txt"
+        include '*.txt'
     }
 
     subprojects.each { sub ->
         from(sub.projectDir) {
-            include "*.txt"
+            include '*.txt'
             into sub.name
         }
     }
@@ -649,25 +483,19 @@ if (canDeploy) {
         upload.extendsFrom archives, signatures
     }
 
-    task generateMD5Checksums(type: GenMD5) {
-        group "Release artifact"
-        description "Creates MD5 checksums for archives of source and JavaDoc"
-        source tasks.withType(Zip)
-        outputDir "$buildDir/md5"
-    }
+    tasks.register('generateChecksums', GenerateChecksums) {
+        group 'Release artifact'
+        description 'Creates MD5/SHA256 checksums for archives of source and 
JavaDoc'
 
-    task generateSHA256Checksums(type: GenSHA256) {
-        group "Release artifact"
-        description "Creates SHA-256 checksums for archives of source and 
JavaDoc"
         source tasks.withType(Zip)
-        outputDir "$buildDir/sha256"
+        outputDir = layout.buildDirectory.dir('checksums')
     }
 
     // This requires that you have the apacheArchivesFolder property 
configured in your
     // ~/.gradle/gradle.properties. The folder should be a Subversion 
workspace for
     // https://dist.apache.org/repos/dist/dev/tapestry
-    // after the build, you must manually add the new files to the workspace 
(using "svn add")
-    // then commit ("svn commit").
+    // after the build, you must manually add the new files to the workspace 
(using 'svn add')
+    // then commit ('svn commit').
 
     // The files will be visible in 
https://dist.apache.org/repos/dist/dev/tapestry/, allowing
     // committers to download and verify them.
@@ -677,75 +505,71 @@ if (canDeploy) {
     // there will publish them to http://www.apache.org/dist/tapestry ... and 
from there
     // to all Apache mirrors (after about a 24 hour delay).
 
-    task copyArchives(type: Copy) {
-        group "Release artifact"
-        description "Copies build archives (source, bin, docs) to a configured 
deployment folder, along with MD5 and SHA-256 checksums and PGP signatures (if 
signing is enabled)"
+    tasks.register('copyArchives', Copy) {
+        group 'Release artifact'
+        description 'Copies build archives (source, bin, docs) to a configured 
deployment folder, along with MD5 and SHA-256 checksums and PGP signatures (if 
signing is enabled)'
 
-        destinationDir file(archiveDeployFolder())
+        destinationDirectory = file(archiveDeployFolder.get())
 
-        from generateMD5Checksums
-        from generateSHA256Checksums
+        from tasks.generateChecksums
         from configurations.uploads.allArtifacts.files
     }
 
-    task generateRelease {
+    tasks.register('generateRelease') {
+        group 'Release artifact'
+        description 'Generates and uploads a final release to Apache Nexus and 
copies archives for deployment'
+
         dependsOn subprojects.assemble, subprojects.uploadPublished, 
subprojects.publish, copyArchives
-        group "Release artifact"
-        description "Generates and uploads a final release to Apache Nexus and 
copies archives for deployment"
     }
 }
 
-boolean isSnapshot() {
-    project.version.contains("SNAPSHOT")
-}
+tasks.register('updateBootstrap') {
+    group = 'Maintenance'
+    description = 'Updates the included Bootstrap dependencies from GitHub'
 
-boolean isWindows() {
-    System.properties['os.name'].toLowerCase().contains('windows')
-}
+    doLast {
+        def bootstrapVersion = '3.3.7'
+        def target = new File(temporaryDir, 'bootstrap.zip')
+        ant.get(src: 
"https://github.com/twbs/bootstrap/archive/v${bootstrapVersion}.zip";, dest: 
target)
+
+        def adjustDirectory = {
+            def relativePath = it.relativePath
+            if (relativePath.pathString.contains('/dist/')){
+                relativePath = new RelativePath(!it.file.isDirectory(), 
relativePath.segments[2..-1] as String[])
+            } else {
+                relativePath = new RelativePath(!it.file.isDirectory(), 
relativePath.segments[1..-1] as String[])
+            }
+            println "copying ${it.relativePath} to ${relativePath}"
+            it.relativePath = relativePath
+        }
+
+        copy {
+            from(zipTree(target)){
+                include('*/js/*.js')
+                include('*/dist/fonts/*')
+                eachFile adjustDirectory
+            }
+            from(zipTree(target)){
+                include('*/dist/css/bootstrap.css')
+                include('*/dist/css/bootstrap-theme.css')
+                eachFile adjustDirectory
+                // TAP5-2351: remove source map reference from css files
+                filter({
+                    (it ==~ /\/\*\s*# sourceMappingURL=[\S]+\s*\*\//) ? '' : it
+                })
+            }
+            
into('tapestry-core/src/main/resources/META-INF/assets/tapestry5/bootstrap/')
+        }
 
-task updateBootstrap {
-  doLast {
-         def bootstrapVersion = '3.3.7'
-         def target = new File(temporaryDir, 'bootstrap.zip')
-         ant.get(src: 
"https://github.com/twbs/bootstrap/archive/v${bootstrapVersion}.zip";, dest: 
target)
-       
-         def adjustDirectory = {
-             def relativePath = it.relativePath
-             if (relativePath.pathString.contains('/dist/')){
-                 relativePath = new RelativePath(!it.file.isDirectory(), 
relativePath.segments[2..-1] as String[])
-             } else {
-                 relativePath = new RelativePath(!it.file.isDirectory(), 
relativePath.segments[1..-1] as String[])
-             }
-             println "copying $it.relativePath to $relativePath"
-             it.relativePath = relativePath
-       
-         }
-       
-         copy {
-           from(zipTree(target)){
-               include('*/js/*.js')
-               include('*/dist/fonts/*')
-               eachFile adjustDirectory
-           }
-           from(zipTree(target)){
-               include('*/dist/css/bootstrap.css')
-               include('*/dist/css/bootstrap-theme.css')
-               eachFile adjustDirectory
-               // TAP5-2351: remove source map reference from css files
-               filter({ (it ==~ /\/\*\s*# sourceMappingURL=[\S]+\s*\*\//) ? "" 
: it })
-           }
-           
into('tapestry-core/src/main/resources/META-INF/assets/tapestry5/bootstrap/')
-         }
-       
-         copy {
-           from(zipTree(target)){
-               include('*/js/*.js')
-               include('*/dist/fonts/*')
-               include('*/less/**/*.less')
-       
-               eachFile adjustDirectory
-           }
-           into('tapestry-webresources/src/test/webapp/bootstrap/')
-         }
-  }
+        copy {
+            from(zipTree(target)){
+                include('*/js/*.js')
+                include('*/dist/fonts/*')
+                include('*/less/**/*.less')
+
+                eachFile adjustDirectory
+            }
+            into('tapestry-webresources/src/test/webapp/bootstrap/')
+        }
+    }
 }
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
new file mode 100644
index 000000000..a4c2a3726
--- /dev/null
+++ b/buildSrc/build.gradle
@@ -0,0 +1,20 @@
+plugins {
+    id 'groovy-gradle-plugin'
+}
+
+repositories {
+    gradlePluginPortal()
+}
+
+dependencies {
+    implementation('ro.isdc.wro4j:wro4j-extensions:1.8.0') {
+        exclude group: 'org.jruby'
+        exclude module: 'spring-web'
+        exclude module: 'closure-compiler'
+        exclude module: 'gmaven-runtime-1.7'
+        exclude module: 'less4j'
+    }
+    implementation 'org.apache.ant:ant-jsch:1.8.2'
+
+    gradleApi()
+}
diff --git a/buildSrc/src/main/groovy/t5build/GenerateChecksums.groovy 
b/buildSrc/src/main/groovy/t5build/GenerateChecksums.groovy
new file mode 100644
index 000000000..dd9f9997f
--- /dev/null
+++ b/buildSrc/src/main/groovy/t5build/GenerateChecksums.groovy
@@ -0,0 +1,61 @@
+package t5build
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.TaskAction
+import org.gradle.api.tasks.SourceTask
+import org.gradle.api.file.ConfigurableFileCollection
+
+import java.security.MessageDigest
+
+class GenerateChecksums extends SourceTask {
+
+    enum Algorithm {
+        MD5('MD5', 32, 'md5'),
+        SHA256('SHA-256', 64, 'sha256')
+
+        final String name
+        final int padding
+        final String extension
+
+        Algorithm(String name, int padding, String extension) {
+            this.name = name
+            this.padding = padding
+            this.extension = extension
+        }
+    }
+
+    @OutputDirectory
+    File outputDir
+
+
+    @TaskAction
+    void generate() {
+        source.each { file ->
+            // Create a map of MessageDigest instances, one for each algorithm,
+            // so it's easier to update them and use later
+            def digests = Algorithm.values().collectEntries { alg ->
+                [(alg): MessageDigest.getInstance(alg.name)]
+            }
+
+            // use inputstream so to avoid loading whole file into memory
+            file.withInputStream { is ->
+                byte[] buffer = new byte[8192]
+                int bytesRead
+                while ((bytesRead = is.read(buffer)) != -1) {
+                    digests.values().each { digest ->
+                        digest.update(buffer, 0, bytesRead)
+                    }
+                }
+            }
+
+            // Write checksum files
+            digests.each { algo, digest ->
+                def checksum = new BigInteger(1, 
digest.digest()).toString(16).padLeft(algo.padding, '0')
+                new File(outputDir, "${file.name}.${algo.extension}").text = 
checksum
+            }
+        }
+    }
+}
diff --git a/buildSrc/src/main/groovy/t5build/SSshExec.groovy 
b/buildSrc/src/main/groovy/t5build/SSshExec.groovy
new file mode 100644
index 000000000..e985b4591
--- /dev/null
+++ b/buildSrc/src/main/groovy/t5build/SSshExec.groovy
@@ -0,0 +1,21 @@
+package t5build
+
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.TaskAction
+
+class SshExec extends SshTask {
+
+    @Input
+    List<String[]> commandLines = []
+
+    void commandLine(String... commandLine) {
+        commandLines << commandLine
+    }
+
+    @TaskAction
+    void doActions() {
+        commandLines.each { commandLine ->
+            ssh(*commandLine)
+        }
+    }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/t5build/Scp.groovy 
b/buildSrc/src/main/groovy/t5build/Scp.groovy
new file mode 100644
index 000000000..3280cfb9d
--- /dev/null
+++ b/buildSrc/src/main/groovy/t5build/Scp.groovy
@@ -0,0 +1,36 @@
+package t5build
+
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.SkipWhenEmpty
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+
+class Scp extends SshTask {
+
+    @InputFiles @SkipWhenEmpty
+    def source
+
+    @Input
+    String destination
+
+    @Input
+    boolean isDir = false
+
+    @TaskAction
+    void doActions() {
+        if (isDir) {
+            scpDir(source, destination)
+            return
+        }
+        project.files(source).each { doFile(it) }
+    }
+
+    private void doFile(File file) {
+        if (file.isDirectory()) {
+            file.eachFile { doFile(it) }
+        } else {
+            scpFile(file, destination)
+        }
+    }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/t5build/SshTask.groovy 
b/buildSrc/src/main/groovy/t5build/SshTask.groovy
new file mode 100644
index 000000000..769b32adf
--- /dev/null
+++ b/buildSrc/src/main/groovy/t5build/SshTask.groovy
@@ -0,0 +1,94 @@
+package t5build
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.FileCollection
+import org.gradle.api.logging.LogLevel
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Internal
+
+abstract class SshTask extends DefaultTask {
+
+    @InputFiles
+    FileCollection sshAntClasspath
+
+    @Input
+    String host
+
+    @Input
+    String userName
+
+    // TODO: Passwords should not be plain @Input.
+    @Input
+    String password
+
+    @Input
+    boolean verbose = false
+
+    private boolean antInited = false
+
+    protected void initAnt() {
+        if (antInited) {
+            return
+        }
+        ant.taskdef(name: 'scp',
+                    classname: 
'org.apache.tools.ant.taskdefs.optional.ssh.Scp',
+                    classpath: sshAntClasspath.asPath,
+                    loaderref: 'ssh')
+
+        ant.taskdef(name: 'sshexec',
+                    classname: 
'org.apache.tools.ant.taskdefs.optional.ssh.SSHExec',
+                    classpath: sshAntClasspath.asPath,
+                    loaderref: 'ssh')
+        antInited = true
+    }
+
+    protected void withInfoLogging(Closure action) {
+        def oldLogLevel = getLogging().getLevel()
+        getLogging().setLevel([LogLevel.INFO, oldLogLevel].min())
+        try {
+            action()
+        } finally {
+            if (oldLogLevel != null) {
+                getLogging().setLevel(oldLogLevel)
+            }
+        }
+    }
+
+    protected void scpFile(Object source, String destination) {
+        initAnt()
+        withInfoLogging {
+            // TODO: This keyfile is hardcoded and uses an old algorithm (dsa)
+            ant.scp(localFile: project.files(source).singleFile,
+                    remoteToFile: "${userName}@${host}:${destination}",
+                    keyfile: "${System.properties['user.home']}/.ssh/id_dsa",
+                    verbose: verbose)
+        }
+    }
+
+    protected void scpDir(Object source, String destination) {
+        initAnt()
+        withInfoLogging {
+            ant.sshexec(host: host,
+                        username: userName,
+                        password: password,
+                        command: "mkdir -p ${destination}")
+
+            ant.scp(remoteTodir: "${userName}@${host}:${destination}",
+                    keyfile: "${System.properties['user.home']}/.ssh/id_dsa",
+                    verbose: verbose) {
+                project.files(source).addToAntBuilder(ant, 'fileSet', 
FileCollection.AntType.FileSet)
+            }
+        }
+    }
+
+    protected void ssh(Object... commandLine) {
+        initAnt()
+        withInfoLogging {
+            ant.sshexec(host: host,
+                        username: userName,
+                        password: password,
+                        command: commandLine.join(' '))
+        }
+    }
+}
diff --git a/buildSrc/src/main/groovy/t5build/TapestryBuildLogic.groovy 
b/buildSrc/src/main/groovy/t5build/TapestryBuildLogic.groovy
new file mode 100644
index 000000000..13f9b935a
--- /dev/null
+++ b/buildSrc/src/main/groovy/t5build/TapestryBuildLogic.groovy
@@ -0,0 +1,22 @@
+package t5build
+
+import org.gradle.api.Project
+
+class TapestryBuildLogic {
+
+    static boolean isSnapshot(Project project) {
+        return tapestryVersion(project).endsWith('SNAPSHOT')
+    }
+
+    static boolean isWindows() {
+        return System.properties['os.name'].toLowerCase().contains('windows')
+    }
+
+    static String tapestryVersion(Project project) {
+        String major = project.rootProject.ext.tapestryMajorVersion
+        String minor = project.rootProject.ext.tapestryMinorVersion
+      
+        boolean isCiBuild = 
project.rootProject.hasProperty('continuousIntegrationBuild') && 
project.rootProject.continuousIntegrationBuild
+        return isCiBuild ? major + '-SNAPSHOT' : major + minor
+    }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/tapestry.java-convention.gradle 
b/buildSrc/src/main/groovy/tapestry.java-convention.gradle
new file mode 100644
index 000000000..b94a82142
--- /dev/null
+++ b/buildSrc/src/main/groovy/tapestry.java-convention.gradle
@@ -0,0 +1,66 @@
+plugins {
+    id 'java-library'
+    id 'eclipse'
+    id 'idea'
+    id 'groovy'
+}
+
+java {
+    sourceCompatibility = JavaVersion.VERSION_1_8
+    targetCompatibility = JavaVersion.VERSION_1_8
+
+    withSourcesJar()
+}
+
+configurations {
+    provided
+    meta
+}
+
+// Ensure the 'provided' dependencies are available on the compile classpaths.
+sourceSets {
+    main.compileClasspath += configurations.provided
+    test.compileClasspath += configurations.provided
+    test.runtimeClasspath += configurations.provided
+}
+
+// Ensure IDEs understand the 'provided' configuration and more IDE setup
+idea {
+    module {
+        scopes.PROVIDED.plus += [configurations.provided]
+    }
+}
+eclipse.classpath.plusConfigurations += [configurations.provided]
+
+
+// Enforce consistent dependency versions across all modules using constraints.
+// This prevents multiple versions of the same library from appearing in the 
classpath.
+// The versions themselves are defined in `gradle/libs.versions.toml`.
+dependencies {
+    constraints {
+        implementation libs.cglib.nodep
+        implementation libs.commons.codec
+        implementation libs.commons.io
+        implementation libs.commons.logging
+        
implementation("${libs.hsqldb.get().module.group}:${libs.hsqldb.get().module.name}:${libs.hsqldb.get().version}:jdk8")
+        implementation libs.hamcrest.core
+        implementation libs.json
+        implementation libs.snakeyaml
+        implementation libs.xml.apis
+    }
+}
+
+tasks.withType(Jar).configureEach {
+    // Include license/notice files in the final JAR.
+    from(project.projectDir) {
+        include '*.txt'
+        into 'META-INF'
+    }
+
+    // JPMS compatibility
+    manifest {
+        attributes("Automatic-Module-Name": 
"org.apache.tapestry.${project.name}"
+        .replace('tapestry-', '')
+        .replace('-', '.'))
+    }
+}
diff --git a/buildSrc/src/main/groovy/tapestry.junit4-legacy-convention.gradle 
b/buildSrc/src/main/groovy/tapestry.junit4-legacy-convention.gradle
new file mode 100644
index 000000000..436efdc53
--- /dev/null
+++ b/buildSrc/src/main/groovy/tapestry.junit4-legacy-convention.gradle
@@ -0,0 +1,12 @@
+plugins {
+    id 'tapestry.testing-base-convention'
+}
+
+dependencies {
+    testImplementation libs.junit4
+    testImplementation libs.hamcrest.core
+}
+
+tasks.withType(Test).configureEach {
+    useJUnit()
+}
diff --git a/buildSrc/src/main/groovy/tapestry.junit5-convention.gradle 
b/buildSrc/src/main/groovy/tapestry.junit5-convention.gradle
new file mode 100644
index 000000000..0d2bf88de
--- /dev/null
+++ b/buildSrc/src/main/groovy/tapestry.junit5-convention.gradle
@@ -0,0 +1,13 @@
+plugins {
+    id 'tapestry.testing-base-convention'
+}
+
+dependencies {
+    testImplementation platform(libs.junit.bom)
+    testImplementation libs.bundles.junit.jupiter.essentials
+    testRuntimeOnly libs.junit.jupiter.engine
+}
+
+tasks.withType(Test).configureEach {
+    useJUnitPlatform()
+}
diff --git a/buildSrc/src/main/groovy/tapestry.junit5-spock-convention.gradle 
b/buildSrc/src/main/groovy/tapestry.junit5-spock-convention.gradle
new file mode 100644
index 000000000..3f58f33c0
--- /dev/null
+++ b/buildSrc/src/main/groovy/tapestry.junit5-spock-convention.gradle
@@ -0,0 +1,9 @@
+plugins {
+    id 'tapestry.junit5-convention'
+    id 'groovy'
+}
+
+dependencies {
+    testImplementation platform(libs.spock.bom)
+    testImplementation libs.spock.core
+}
diff --git a/buildSrc/src/main/groovy/tapestry.ssh-convention.gradle 
b/buildSrc/src/main/groovy/tapestry.ssh-convention.gradle
new file mode 100644
index 000000000..2f6aa7c8a
--- /dev/null
+++ b/buildSrc/src/main/groovy/tapestry.ssh-convention.gradle
@@ -0,0 +1,15 @@
+import t5build.SshTask
+
+// This configuration will hold the ant-jsch.jar at runtime
+configurations {
+    sshAntTask
+}
+
+dependencies {
+    sshAntTask libs.ant.jsch
+}
+
+// Configure all SshTask instances to use the files from the configuration
+tasks.withType(SshTask).configureEach {
+    it.sshAntClasspath = configurations.sshAntTask
+}
diff --git a/buildSrc/src/main/groovy/tapestry.testing-base-convention.gradle 
b/buildSrc/src/main/groovy/tapestry.testing-base-convention.gradle
new file mode 100644
index 000000000..7a3efcd79
--- /dev/null
+++ b/buildSrc/src/main/groovy/tapestry.testing-base-convention.gradle
@@ -0,0 +1,64 @@
+plugins {
+    id 'java-library'
+}
+
+dependencies {
+    testRuntimeOnly libs.slf4j.simple
+}
+
+tasks.withType(Test).configureEach { testTask ->
+    maxHeapSize = '600M'
+
+    systemProperties['tapestry.service-reloading-enabled'] = 'false'
+    systemProperties['java.io.tmpdir'] = temporaryDir.absolutePath
+
+    jvmArgs '-Dfile.encoding=UTF-8'
+    environment.LANG = 'en_US.UTF-8'
+
+    // TAP5-2722
+    systemProperty 'user.language', 'en'
+
+    testLogging {
+        exceptionFormat 'full'
+        events 'passed', 'skipped', 'failed'
+        showStandardStreams = true
+    }
+
+    def total = 0, passed = 0, failed = 0, skipped = 0
+
+    afterTest { descriptor, result ->
+        switch (result.resultType) {
+            case org.gradle.api.tasks.testing.TestResult.ResultType.SUCCESS:
+                passed++
+                break
+            case org.gradle.api.tasks.testing.TestResult.ResultType.FAILURE:
+                failed++
+                break
+            case org.gradle.api.tasks.testing.TestResult.ResultType.SKIPPED:
+                skipped++
+                break
+        }
+        total++
+        if (total % 25 == 0) {
+            logger.lifecycle "Project ${project.name}: Tests run: ${total}, 
Passed: ${passed}, Failed: ${failed}, Skipped: ${skipped}"
+        }
+    }
+
+    afterSuite { descriptor, result ->
+        // The root suite has a null parent. We only want to log the final 
summary.
+        if (descriptor.parent == null) {
+            total = passed + failed + skipped
+            // Using project.path gives a clear identifier like 
":tapestry-core"
+            def projectName = project.path
+
+            // Don't log if no tests were run
+            if (total > 0) {
+                logger.lifecycle 
"------------------------------------------------------------------------"
+                logger.lifecycle "Test Results for ${projectName}"
+                logger.lifecycle "  Tests run: ${total}, Passed: ${passed}, 
Failed: ${failed}, Skipped: ${skipped}"
+                logger.lifecycle 
"------------------------------------------------------------------------"
+            }
+        }
+    }
+}
+
diff --git a/buildSrc/src/main/groovy/tapestry.testng-convention.gradle 
b/buildSrc/src/main/groovy/tapestry.testng-convention.gradle
new file mode 100644
index 000000000..90dac3640
--- /dev/null
+++ b/buildSrc/src/main/groovy/tapestry.testng-convention.gradle
@@ -0,0 +1,27 @@
+plugins {
+    id 'tapestry.testing-base-convention'
+}
+
+dependencies {
+    testImplementation libs.testng
+    testImplementation libs.easymock
+    testImplementation libs.hamcrest.core
+}
+
+tasks.withType(Test).configureEach { testTask ->
+
+    def suiteFile = [
+        'src/test/resources/testng.xml',
+        'src/test/conf/testng.xml'
+    ].find { path ->
+        project.file(path).exists()
+    }
+
+    if (suiteFile) {
+        testTask.useTestNG {
+            suites suiteFile
+        }
+    } else {
+        testTask.useTestNG()
+    }
+}
diff --git a/commons/build.gradle b/commons/build.gradle
index 99cb1d4d9..ecc9bf194 100644
--- a/commons/build.gradle
+++ b/commons/build.gradle
@@ -1,24 +1,12 @@
-import org.gradle.plugins.ide.idea.model.*
-import t5build.*
-
-description = "Project including common classes for tapestry-core, 
tapestry-ioc and beanmodel."
+plugins {
+    id 'tapestry.junit5-spock-convention'
+}
 
-//apply plugin: JavaPlugin
+description = 'Project including common classes for tapestry-core, 
tapestry-ioc and beanmodel.'
 
-buildDir = 'target/gradle-build'
-       
 dependencies {
-       api project(":plastic")
-       api project(":tapestry5-annotations")
-       implementation project(":tapestry-func")
-    testImplementation 
"org.junit.jupiter:junit-jupiter:${versions.junitJupiter}"
-}
+    api project(':plastic')
+    api project(':tapestry5-annotations')
 
-jar {  
-       manifest {      
-       }
+    implementation project(':tapestry-func')
 }
-
-test {
-    useJUnit()
-}
\ No newline at end of file
diff --git a/genericsresolver-guava/build.gradle 
b/genericsresolver-guava/build.gradle
index a20be0d48..90892270a 100644
--- a/genericsresolver-guava/build.gradle
+++ b/genericsresolver-guava/build.gradle
@@ -1,9 +1,13 @@
+plugins {
+    id 'tapestry.testng-convention'
+}
 description = "Replaces the Tapestry Commons's own Java Generics resolution 
code with the one from Google Guava's one"
 
 dependencies {
-  implementation project(':commons')
-  testImplementation project(':tapestry-core')
-  testImplementation project(':tapestry-test')
-  provided implementation ('com.google.guava:guava:27.0.1-jre')
-  testImplementation "org.junit.jupiter:junit-jupiter:${versions.junitJupiter}"
-}
\ No newline at end of file
+    implementation project(':commons')
+
+    provided libs.guava
+
+    testImplementation project(':tapestry-core')
+    testImplementation project(':tapestry-test')
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 000000000..b0055439f
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,168 @@
+[versions]
+
+# LOGGING
+
+slf4j = "1.7.25"
+
+# JAVAX/JAKARTA
+
+javax-inject         = "1"
+javax-servlet-api    = "3.0.1"
+javax-validation-api = "1.0.0.GA"
+
+jakarta-annotation-api  = "1.3.4"
+jakarta-persistence-api = "3.0.0"
+jakarta-xml-bind-api    = "2.3.2"
+
+# APACHE COMMONS
+
+commons-cli        = "1.2"
+commons-codec      = "1.18.0"
+commons-httpclient = "4.1"
+commons-io         = "2.19.0"
+commons-lang3      = "3.17.0"
+commons-logging    = "1.3.5"
+commons-text       = "1.13.1"
+
+# HIBERNATE
+
+hibernate           = "5.4.32.Final"
+hibernate-validator = "4.3.2.Final"
+
+hsqldb = "2.7.3"
+
+# ANTLR
+
+antlr = "3.5.3"
+
+# MISC
+
+snakeyaml = "2.3"
+cglib = "2.2"
+hamcrest-core = "1.3"
+json = "20140107"
+xml-apis = "1.4.01"
+guice = "3.0"
+ant-jsch = "1.8.2"
+guava = "27.0.1-jre"
+jackson = "2.13.1"
+clojure = "1.6.0"
+httpcomponents-httpclient = "4.5.14"
+
+# TESTING
+
+slf4j-simple = "2.0.17"
+junit5 = "5.10.2"
+junit4 = "4.13.2"
+testng = "7.5.1"
+easymock = "5.4.0"
+spock = "2.3-groovy-3.0"
+geb = "2.0"
+webdrivermanager = "5.3.1"
+selenium = "4.5.0"
+selenium-java = "4.12.1"
+selenium-server = "4.12.1"
+
+# QUICKSTART
+
+quickstart-spring-boot    = "2.5.4"
+quickstart-json           = "1.1.4"
+quickstart-log4j          = "2.17.2"
+quickstart-yasson         = "2.0.4"
+quickstart-maven-compiler = "3.8.1"
+quickstart-maven-surefire = "3.1"
+quickstart-maven-war      = "3.3.1"
+quickstart-maven-jetty    = "10.0.6"
+quickstart-gretty         = "3.0.6"
+
+[libraries]
+
+# LOGGING
+
+slf4j-api     = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
+slf4j-log4j12 = { module = "org.slf4j:slf4j-log4j12", version.ref = "slf4j" }
+
+# JAVAX/JAKARTA
+
+javax-inject         = { module = "javax.inject:javax.inject", version.ref = 
"javax-inject" }
+javax-servlet-api    = { module = "javax.servlet:javax.servlet-api", 
version.ref = "javax-servlet-api" }
+javax-validation-api = { module = "javax.validation:validation-api", 
version.ref = "javax-validation-api" }
+
+jakarta-annotation-api  = { module = 
"jakarta.annotation:jakarta.annotation-api", version.ref = 
"jakarta-annotation-api" }
+jakarta-persistence-api = { module = 
"jakarta.persistence:jakarta.persistence-api", version.ref = 
"jakarta-persistence-api" }
+jakarta-xml-bind-api    = { module = "jakarta.xml.bind:jakarta.xml.bind-api", 
version.ref = "jakarta-xml-bind-api" }
+
+# APACHE COMMONS
+
+commons-cli        = { module = "commons-cli:commons-cli", version.ref = 
"commons-cli" }
+commons-codec      = { module = "commons-codec:commons-codec", version.ref = 
"commons-codec" }
+commons-httpclient = { module = "commons-httpclient:commons-httpclient", 
version.ref = "commons-httpclient" }
+commons-io         = { module = "commons-io:commons-io", version.ref = 
"commons-io" }
+commons-lang3      = { module = "org.apache.commons:commons-lang3", 
version.ref = "commons-lang3" }
+commons-logging    = { module = "commons-logging:commons-logging", version.ref 
= "commons-logging" }
+commons-text       = { module = "org.apache.commons:commons-text", version.ref 
= "commons-text" }
+
+# HIBERNATE
+
+hibernate-core      = { module = "org.hibernate:hibernate-core", version.ref = 
"hibernate" }
+hibernate-validator = { module = "org.hibernate:hibernate-validator", 
version.ref = "hibernate-validator" }
+
+hsqldb = { module = "org.hsqldb:hsqldb", version.ref = "hsqldb" }
+
+# ANTLR
+
+antlr = { module = "org.antlr:antlr", version.ref = "antlr" }
+antlr-runtime = { module = "org.antlr:antlr-runtime", version.ref = "antlr" }
+
+# MISC
+
+snakeyaml = { module = "org.yaml:snakeyaml", version.ref = "snakeyaml" }
+cglib-nodep = { module = "cglib:cglib-nodep", version.ref = "cglib" }
+hamcrest-core = { module = "org.hamcrest:hamcrest-core", version.ref = 
"hamcrest-core" }
+json = { module = "org.json:json", version.ref = "json" }
+xml-apis = { module = "xml-apis:xml-apis", version.ref = "xml-apis" }
+guice = { module = "com.google.inject:guice", version.ref = "guice" }
+ant-jsch = { module = "org.apache.ant:ant-jsch", version.ref = "ant-jsch" }
+guava = { module = "com.google.guava:guava", version.ref = "guava" }
+clojure = { module = "org.clojure:clojure", version.ref = "clojure" }
+httpcomponents-httpclient = { module = "org.apache.httpcomponents:httpclient", 
version.ref = "httpcomponents-httpclient" }
+
+# JACKSON
+
+jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", 
version.ref = "jackson" }
+
+# TESTING
+
+slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = 
"slf4j-simple" }
+
+junit-bom            = { module = "org.junit:junit-bom", version.ref = 
"junit5" }
+junit-jupiter        = { module = "org.junit.jupiter:junit-jupiter"}
+junit-jupiter-api    = { module = "org.junit.jupiter:junit-jupiter-api" }
+junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params" }
+junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine" }
+
+junit4 = { module = "junit:junit", version.ref = "junit4" }
+
+testng = { module = "org.testng:testng", version.ref = "testng" }
+
+easymock = { module = "org.easymock:easymock", version.ref = "easymock" }
+
+spock-bom  = { module = "org.spockframework:spock-bom", version.ref = "spock" }
+spock-core = { module = "org.spockframework:spock-core" }
+
+geb-spock = { module = "org.gebish:geb-spock", version.ref = "geb" }
+
+selenium-legrc  = { module = "org.seleniumhq.selenium:selenium-leg-rc", 
version.ref = "selenium" }
+selenium-api    = { module = "org.seleniumhq.selenium:selenium-api", 
version.ref = "selenium" }
+selenium-java   = { module = "org.seleniumhq.selenium:selenium-java", 
version.ref = "selenium-java" }
+selenium-server = { module = "org.seleniumhq.selenium:selenium-server", 
version.ref = "selenium-server" }
+
+webdrivermanager = { module = "io.github.bonigarcia:webdrivermanager", 
version.ref = "webdrivermanager" }
+
+
+[bundles]
+
+junit-jupiter-essentials = [
+    "junit-jupiter-api",
+    "junit-jupiter-params"
+]
diff --git a/gradle/wrapper/gradle-wrapper.jar 
b/gradle/wrapper/gradle-wrapper.jar
index 41d9927a4..d64cd4917 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and 
b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties 
b/gradle/wrapper/gradle-wrapper.properties
index a59520664..ff23a68d7 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,7 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 1b6c78733..1aa94a426 100755
--- a/gradlew
+++ b/gradlew
@@ -55,7 +55,7 @@
 #       Darwin, MinGW, and NonStop.
 #
 #   (3) This script is generated from the Groovy template
-#       
https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       
https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
 #       within the Gradle project.
 #
 #       You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,13 +80,11 @@ do
     esac
 done
 
-APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
-
-APP_NAME="Gradle"
+# This is normally unused
+# shellcheck disable=SC2034
 APP_BASE_NAME=${0##*/}
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to 
pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# Discard cd standard output in case $CDPATH is set 
(https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
 
 # Use the maximum available, or set MAX_FD != -1 to use that value.
 MAX_FD=maximum
@@ -133,22 +131,29 @@ location of your Java installation."
     fi
 else
     JAVACMD=java
-    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 
'java' command could be found in your PATH.
+    if ! command -v java >/dev/null 2>&1
+    then
+        die "ERROR: JAVA_HOME is not set and no 'java' command could be found 
in your PATH.
 
 Please set the JAVA_HOME variable in your environment to match the
 location of your Java installation."
+    fi
 fi
 
 # Increase the maximum file descriptors if we can.
 if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
     case $MAX_FD in #(
       max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is 
checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
         MAX_FD=$( ulimit -H -n ) ||
             warn "Could not query maximum file descriptor limit"
     esac
     case $MAX_FD in  #(
       '' | soft) :;; #(
       *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is 
checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
         ulimit -n "$MAX_FD" ||
             warn "Could not set maximum file descriptor limit to $MAX_FD"
     esac
@@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then
     done
 fi
 
-# Collect all arguments for the java command;
-#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
-#     shell script including quotes and variable substitutions, so put them in
-#     double quotes to make sure that they get re-expanded; and
-#   * put everything else in single quotes, so that it's not re-expanded.
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to 
pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not 
allowed to contain shell fragments,
+#     and any embedded shellness will be escaped.
+#   * For example: A user cannot expect ${Hostname} to be expanded, as it is 
an environment variable and will be
+#     treated as '${Hostname}' itself on the command line.
 
 set -- \
         "-Dorg.gradle.appname=$APP_BASE_NAME" \
@@ -205,6 +214,12 @@ set -- \
         org.gradle.wrapper.GradleWrapperMain \
         "$@"
 
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
+fi
+
 # Use "xargs" to parse quoted args.
 #
 # With -n1 it outputs one arg per line, with the quotes and backslashes 
removed.
diff --git a/gradlew.bat b/gradlew.bat
index ac1b06f93..6689b85be 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -14,7 +14,7 @@
 @rem limitations under the License.
 @rem
 
-@if "%DEBUG%" == "" @echo off
+@if "%DEBUG%"=="" @echo off
 @rem ##########################################################################
 @rem
 @rem  Gradle startup script for Windows
@@ -25,7 +25,8 @@
 if "%OS%"=="Windows_NT" setlocal
 
 set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
 set APP_BASE_NAME=%~n0
 set APP_HOME=%DIRNAME%
 
@@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
 
 set JAVA_EXE=java.exe
 %JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto execute
+if %ERRORLEVEL% equ 0 goto execute
 
 echo.
 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your 
PATH.
@@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
 
 :end
 @rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
+if %ERRORLEVEL% equ 0 goto mainEnd
 
 :fail
 rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code 
instead of
 rem the _cmd.exe /c_ return code!
-if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
 
 :mainEnd
 if "%OS%"=="Windows_NT" endlocal
diff --git a/plastic/build.gradle b/plastic/build.gradle
index f5bdee139..35f5d8103 100644
--- a/plastic/build.gradle
+++ b/plastic/build.gradle
@@ -1,15 +1,18 @@
-description = "High-level runtime transformations of Java classes"
+plugins {
+    id 'tapestry.junit5-spock-convention'
+}
+
+description = 'High-level runtime transformations of Java classes'
 
 dependencies {
-    implementation "org.slf4j:slf4j-api:${versions.slf4j}"
-    testImplementation 
"org.junit.jupiter:junit-jupiter:${versions.junitJupiter}"
+    implementation libs.slf4j.api
 }
 
-test { 
-  useJUnit()
+// add vendored ASM
+sourceSets {
+    main {
+        java {
+            srcDir 'src/external/java'
+        }
+    }
 }
-
-// Add the source directory for the imported/repackaged ASM 7.0.1 code
-
-sourceSets.main.java.srcDir "src/external/java"
-
diff --git a/quickstart/build.gradle b/quickstart/build.gradle
index 98225bf97..a88fe8054 100644
--- a/quickstart/build.gradle
+++ b/quickstart/build.gradle
@@ -50,18 +50,18 @@ task processFiltered(type: Copy) {
     filter(ReplaceTokens, tokens: [
         quickstartVersion: project.parent.version,
         tapestryVersion: project.parent.version,
-        springBootVersion: '2.5.4',
-        junitVersion: '5.8.2',
-        jacksonVersion: '2.13.1',
-        jsonVersion: '1.1.4',
-        log4jVersion: '2.17.2',
-        yassonVersion: '2.0.4',
-        servletVersion: '3.1.0',
-        mavenCompilerVersion: '3.8.1',
-        mavenSurefireVersion: '3.0.0-M5',
-        mavenWarVersion: '3.3.1',
-        mavenJettyVersion: '10.0.6',
-        grettyVersion: '3.0.6'
+        springBootVersion: libs.versions.quickstart.spring.boot.get(),
+        junitVersion: libs.versions.junit5.get(),
+        jacksonVersion: libs.versions.jackson.get(),
+        jsonVersion: libs.versions.quickstart.json.get(),
+        log4jVersion: libs.versions.quickstart.log4j.get(),
+        yassonVersion: libs.versions.quickstart.yasson.get(),
+        servletVersion: libs.versions.javax.servlet.api.get(),
+        mavenCompilerVersion: libs.versions.quickstart.maven.compiler.get(),
+        mavenSurefireVersion: libs.versions.quickstart.maven.surefire.get(),
+        mavenWarVersion: libs.versions.quickstart.maven.war.get(),
+        mavenJettyVersion: libs.versions.quickstart.maven.jetty.get(),
+        grettyVersion: libs.versions.quickstart.gretty.get()
     ])
 }
 
diff --git a/settings.gradle b/settings.gradle
index 58d041ac8..5f4f20356 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -3,20 +3,83 @@ plugins {
     id 'com.gradle.common-custom-user-data-gradle-plugin' version '2.0.2'
 }
 
+rootProject.name = 'tapestry-5'
+
+dependencyResolutionManagement {
+    repositories {
+        mavenLocal()
+        mavenCentral()
+        maven {
+            name = 'JBoss'
+            url = 
'https://repository.jboss.org/nexus/content/repositories/releases/'
+        }
+        maven {
+            // tapestry-jpa
+            name = 'EclipseLink'
+            url = 'https://download.eclipse.org/rt/eclipselink/maven.repo/'
+        }
+    }
+}
+
+include('plastic',
+        'tapestry5-annotations',
+        'tapestry-test',
+        'tapestry-func',
+        'tapestry-ioc',
+        'tapestry-json',
+        'tapestry-http',
+        'tapestry-core',
+        'tapestry-hibernate-core',
+        'tapestry-hibernate',
+        'tapestry-jmx',
+        'tapestry-upload',
+        'tapestry-beanvalidator',
+        'tapestry-jpa',
+        'tapestry-kaptcha',
+        'quickstart',
+        'tapestry-clojure',
+        'tapestry-mongodb',
+        'tapestry-test-data',
+        'tapestry-internal-test',
+        'tapestry-ioc-junit',
+        'tapestry-webresources',
+        'tapestry-runner',
+        'tapestry-test-constants',
+        'tapestry-ioc-jcache',
+        'beanmodel',
+        'commons',
+        'genericsresolver-guava',
+        'tapestry-version-migrator',
+        'tapestry-spock',
+        'tapestry-openapi-viewer',
+        'tapestry-rest-jackson')
+
+if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_11)) {
+    include('tapestry-javadoc')
+}
+
+if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)) {
+    include('tapestry-latest-java-tests', 'tapestry-spring')
+}
+
 def isTravisCI = System.getenv('TRAVIS_JOB_ID') != null
 def isJenkins = System.getenv('JENKINS_URL') != null
 def isCI = isTravisCI || isJenkins
 
 develocity {
-    server = "https://develocity.apache.org";
-    projectId = "tapestry"
+    server = 'https://develocity.apache.org'
+    projectId = 'tapestry'
     buildScan {
         uploadInBackground = !isCI
         publishing.onlyIf { it.authenticated }
         obfuscation {
             // This obfuscates the IP addresses of the build machine in the 
build scan.
             // Alternatively, the build scan will provide the hostname for 
troubleshooting host-specific issues.
-            ipAddresses { addresses -> addresses.collect { address -> 
"0.0.0.0"} }
+            ipAddresses { addresses ->
+                addresses.collect { address ->
+                    '0.0.0.0'
+                }
+            }
         }
     }
 }
@@ -30,21 +93,3 @@ buildCache {
         enabled = false
     }
 }
-
-rootProject.name = "tapestry-5"
-
-include "plastic", "tapestry5-annotations", "tapestry-test", "tapestry-func", 
"tapestry-ioc", "tapestry-json", "tapestry-http", "tapestry-core"
-include "tapestry-hibernate-core", "tapestry-hibernate", "tapestry-jmx", 
"tapestry-upload", "tapestry-spring"
-include "tapestry-beanvalidator", "tapestry-jpa", "tapestry-kaptcha"
-if (JavaVersion.current() != JavaVersion.VERSION_1_8) {
-    include "tapestry-javadoc"
-}
-if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)) {
-    include "tapestry-latest-java-tests"
-}
-include "quickstart", "tapestry-clojure", "tapestry-mongodb"
-include "tapestry-test-data", 'tapestry-internal-test', "tapestry-ioc-junit"
-include "tapestry-webresources", "tapestry-runner", "tapestry-test-constants"
-include "tapestry-ioc-jcache", "beanmodel", "commons", 
"genericsresolver-guava", "tapestry-version-migrator"
-// include "tapestry-cdi"
-include "tapestry-spock", "tapestry-openapi-viewer", "tapestry-rest-jackson"
diff --git a/sha256.gradle b/sha256.gradle
deleted file mode 100644
index 8abaec5ff..000000000
--- a/sha256.gradle
+++ /dev/null
@@ -1,25 +0,0 @@
-import java.security.MessageDigest
-
-class GenSHA256 extends SourceTask {
-
-  def outputDir
-
-  @OutputDirectory
-  File getOutputDir() { project.file(outputDir) }
-
-  @TaskAction
-  void writeSHA256s() {
-
-    source.each { file ->
-      MessageDigest digest = MessageDigest.getInstance("SHA-256")
-
-      digest.update(file.bytes)
-
-      def checksum = new BigInteger(1, 
digest.digest()).toString(16).padLeft(32, "0")
-
-      new File(outputDir, file.name + ".sha256").text = checksum
-    }
-  }
-}
-
-project.ext.GenSHA256 = GenSHA256.class
\ No newline at end of file
diff --git a/ssh.gradle b/ssh.gradle
deleted file mode 100644
index e367719ce..000000000
--- a/ssh.gradle
+++ /dev/null
@@ -1,124 +0,0 @@
-project.ext.Scp = Scp.class
-project.ext.SshExec = SshExec.class
-project.ext.SshTask = SshTask.class
-
-configurations {
-  sshAntTask
-}
-
-dependencies {
-  sshAntTask "org.apache.ant:ant-jsch:1.8.2"
-}
-
-tasks.withType(SshTask) {
-  sshAntClasspath = configurations.sshAntTask
-}
-
-class SshTask extends DefaultTask {
-  @InputFiles
-  FileCollection sshAntClasspath
-
-  @Input
-  String host
-
-  @Input
-  String userName
-
-  @Input
-  String password
-
-  boolean verbose = false
-
-  private boolean antInited = false
-
-  protected initAnt() {
-    if (!antInited) {
-      ant.taskdef(name: 'scp',
-        classname: 'org.apache.tools.ant.taskdefs.optional.ssh.Scp',
-        classpath: sshAntClasspath.asPath,
-        loaderref: 'ssh')
-      ant.taskdef(name: 'sshexec',
-        classname: 'org.apache.tools.ant.taskdefs.optional.ssh.SSHExec',
-        classpath: sshAntClasspath.asPath,
-        loaderref: 'ssh')
-      antInited = true
-    }
-  }
-
-  protected withInfoLogging(Closure action) {
-    def oldLogLevel = logging.level
-    logging.level = [LogLevel.INFO, oldLogLevel].min()
-    try {
-      action()
-    } finally {
-      if (oldLogLevel) {
-        logging.level = oldLogLevel
-      }
-    }
-  }
-
-  def scpFile(source, destination) {
-    initAnt()
-    withInfoLogging {
-      ant.scp(localFile: project.files(source).singleFile, remoteToFile: 
"${userName}@${host}:${destination}", keyfile : 
"${System.properties['user.home']}/.ssh/id_dsa", verbose: verbose)
-    }
-  }
-
-  def scpDir(source, destination) {
-    initAnt()
-    withInfoLogging {
-      ant.sshexec(host: host, username: userName, password: password, command: 
"mkdir -p ${destination}")
-      ant.scp(remoteTodir: "${userName}@${host}:${destination}", keyfile : 
"${System.properties['user.home']}/.ssh/id_dsa", verbose: verbose) {
-        project.files(source).addToAntBuilder(ant, "fileSet", 
FileCollection.AntType.FileSet)
-      }
-    }
-  }
-
-  def ssh(Object... commandLine) {
-    initAnt()
-    withInfoLogging {
-      ant.sshexec(host: host, username: userName, password: password, command: 
commandLine.join(' '))
-    }
-  }
-}
-
-class Scp extends SshTask {
-  @InputFiles @SkipWhenEmpty source
-  @Input destination
-  boolean isDir = false
-
-  @TaskAction
-  void doActions() {
-    if (isDir) {
-      scpDir(source, destination)
-      return
-    }
-
-    project.files(source).each { doFile(it) }
-  }
-
-  void doFile(File file) {
-
-    if (file.directory) {
-      file.eachFile { doFile(it) }
-    } else {
-      scpFile(file, destination)
-    }
-  }
-}
-
-class SshExec extends SshTask {
-  @Input
-  List<String[]> commandLines = []
-
-  void commandLine(String... commandLine) {
-    commandLines << commandLine
-  }
-
-  @TaskAction
-  void doActions() {
-    commandLines.each { commandLine ->
-      ssh(* commandLine)
-    }
-  }
-}
\ No newline at end of file
diff --git a/tapestry-beanvalidator/build.gradle 
b/tapestry-beanvalidator/build.gradle
index 82ba5f0b9..b11ffabcd 100644
--- a/tapestry-beanvalidator/build.gradle
+++ b/tapestry-beanvalidator/build.gradle
@@ -1,28 +1,34 @@
+plugins {
+    id 'tapestry.testng-convention'
+}
+
 import t5build.*
 
-description = "Support for JSR-303 Bean Validation via the Hibernate validator 
implementation"
+description = 'Support for JSR-303 Bean Validation via the Hibernate validator 
implementation'
 
 dependencies {
-  implementation project(':tapestry-core')
-
-  implementation "javax.validation:validation-api:1.0.0.GA"
-  implementation "org.hibernate:hibernate-validator:4.3.2.Final"
-
-  testImplementation project(':tapestry-test')
-  implementation 
"org.seleniumhq.selenium:selenium-leg-rc:${versions.selenium}", {
-      exclude group: "org.seleniumhq.selenium", module: "jetty-repacked"
-      exclude group: "org.testng", module: "testng"
-      exclude group: "javax.servlet", module: "javax.servlet-api"
-  }
-  testImplementation "org.junit.jupiter:junit-jupiter:${versions.junitJupiter}"
-  
+    implementation project(':tapestry-core')
+
+    implementation libs.javax.validation.api
+    implementation libs.hibernate.validator
+
+    implementation(libs.selenium.legrc) {
+        exclude group: 'org.seleniumhq.selenium', module: 'jetty-repacked'
+        exclude group: 'org.testng', module: 'testng'
+    }
+
+    testImplementation project(':tapestry-test')
 }
 
 // Start up the test app, useful when debugging failing integration tests
-task runTestApp303(type:JavaExec) {
-  main = 'org.apache.tapestry5.test.JettyRunner'
-  args "-d", "src/test/webapp", "-p", "8080"
-  classpath += project.sourceSets.test.runtimeClasspath
+tasks.register('runTestApp303', JavaExec) {
+    mainClass = 'org.apache.tapestry5.test.JettyRunner'
+    args '-d', 'src/test/webapp', '-p', '8080'
+    classpath += project.sourceSets.test.runtimeClasspath
 }
 
-jar.manifest.attributes 'Tapestry-Module-Classes': 
'org.apache.tapestry5.beanvalidator.modules.BeanValidatorModule'
+tasks.named('jar', Jar) {
+    manifest {
+        attributes 'Tapestry-Module-Classes': 
'org.apache.tapestry5.beanvalidator.modules.BeanValidatorModule'
+    }
+}
diff --git a/tapestry-cdi/build.gradle b/tapestry-cdi/build.gradle
index 2e51bd96c..d40e22e16 100644
--- a/tapestry-cdi/build.gradle
+++ b/tapestry-cdi/build.gradle
@@ -8,22 +8,20 @@ apply plugin: JavaPlugin
 buildDir = 'target/gradle-build'
        
 project.ext.libraryVersions = [
-    javaeeapi: '6.0-4', 
-    tomee: '1.6.0', 
-    ziplock: '1.5.1',
-    junit: '4.8.1', 
-    commonsHttpclient: '3.1',
-    arquillian: '1.1.1.Final', 
-    jbossJavaeeSpec: '1.0.0.Final',
-    arquillianGlassfish: '1.0.0.CR4',
-    glassfishDistrib: '3.1.1-b04', 
-    jbossDistrib: '7.1.1.Final', 
-    cdi: '1.0-SP4',
-    jbossAS7: '7.1.1.Final', 
-    shrinkwrapDesc: '2.0.0-alpha-3'
+    tomee: 'org.apache.openejb:arquillian-tomee-embedded:1.6.0',
+    ziplock: 'org.apache.openejb:ziplock:1.5.1',
+    commonsHttpclient: 'commons-httpclient:commons-httpclient:3.1',
+    arquillianJunitContainer: 
'org.jboss.arquillian.junit:arquillian-junit-container:1.1.1.Final',
+    arquillianGlassfish: 
'org.jboss.arquillian.container:arquillian-glassfish-managed-3.1:1.0.0.CR4',
+    glassfishDistrib: 'org.glassfish.distributions:glassfish:3.1.1-b04@zip',
+    jbossAS7: 'org.jboss.as:jboss-as-arquillian-container-managed:7.1.1.Final',
+    jbossDistrib: 'org.jboss.as:jboss-as-dist:7.1.1.Final@zip',
+    jbossJavaeeSpec: 'org.jboss.spec:jboss-javaee-6.0:1.0.0.Final',
+    cdiApi: 'jakarta.enterprise:cdi-api:1.0-SP4',
+    shrinkwrapDescriptorsApi: 
'org.jboss.shrinkwrap.descriptors:shrinkwrap-descriptors-api-javaee:2.0.0-alpha-3',
+    shrinkwrapDescriptorsImpl: 
'org.jboss.shrinkwrap.descriptors:shrinkwrap-descriptors-impl-javaee:2.0.0-alpha-3',
 ]
 
-
 configurations {
     compileOnly 
     jboss
@@ -36,30 +34,28 @@ configurations {
 dependencies {
     compile project(':tapestry-core')
     compile project(':tapestry-ioc')
-    testCompile (project(':tapestry-test')){
-         transitive = false
-     } 
-      
-    // replace javax.enterprise:cdi-api with group: 'org.jboss.spec', name: 
'jboss-javaee-6.0', version: libraryVersions.jbossJavaeeSpec to compile against 
full Java EE API
-    compileOnly group: 'javax.enterprise', name: 'cdi-api', version: 
libraryVersions.cdi
-    
-    
-    testCompile group: 'org.apache.openejb', name: 'ziplock', version: 
libraryVersions.ziplock
-    testCompile group: 'junit', name: 'junit', version: libraryVersions.junit
-    testCompile group: 'commons-httpclient', name: 'commons-httpclient', 
version: libraryVersions.commonsHttpclient
+    testCompile (project(':tapestry-test')) {
+        transitive = false
+     }
+
+    // replace jakarta.enterprise:cdi-api with group: 'org.jboss.spec', name: 
'jboss-javaee-6.0', version: libraryVersions.jbossJavaeeSpec to compile against 
full Java EE API
+    compileOnly moduleLibs.cdiApi
+
+    testCompile moduleLibs.ziplock
+    testCompile moduleLibs.commonsHttpclient
     
-    testCompile group: 'org.jboss.arquillian.junit', name: 
'arquillian-junit-container', version: libraryVersions.arquillian
-    testCompile group: 'org.jboss.shrinkwrap.descriptors', name: 
'shrinkwrap-descriptors-api-javaee', version: libraryVersions.shrinkwrapDesc
-    testRuntime group: 'org.jboss.shrinkwrap.descriptors', name: 
'shrinkwrap-descriptors-impl-javaee', version: libraryVersions.shrinkwrapDesc
+    testCompile moduleLibs.arquillianJunitContainer
+    testCompile moduleLibs.shrinkwrapDescriptorsApi
+    testRuntime moduleLibs.shrinkwrapDescriptorsImpl
     
-    tomeeEmbeddedTestRuntime group: 'org.apache.openejb', name: 
'arquillian-tomee-embedded', version: libraryVersions.tomee
+    tomeeEmbeddedTestRuntime mopduleLibs.tomee
     
-    jbossAS7ManagedTestRuntime group: 'org.jboss.as', name: 
'jboss-as-arquillian-container-managed', version: libraryVersions.jbossAS7
-    jbossAS7ManagedTestRuntime group: 'org.jboss.spec', name: 
'jboss-javaee-6.0', version: libraryVersions.jbossJavaeeSpec
-    jboss "org.jboss.as:jboss-as-dist:$libraryVersions.jbossDistrib@zip"
+    jbossAS7ManagedTestRuntime moduleLibs.jbossAS7
+    jbossAS7ManagedTestRuntime moduleLibs.jbossJavaeeSpec
+    jboss moduleLibs.jbossDistrib
  
-    glassfishManagedTestRuntime group: 'org.jboss.arquillian.container', name: 
'arquillian-glassfish-managed-3.1', version: libraryVersions.arquillianGlassfish
-    glassfish 
"org.glassfish.distributions:glassfish:$libraryVersions.glassfishDistrib@zip"   
  
+    glassfishManagedTestRuntime moduleLibs.arquillianGlassfish
+    glassfish moduleLibs.glassfishDistrib
 }
 
 sourceSets {
@@ -72,10 +68,10 @@ sourceSets {
 }
 
 idea {
-      module {
-        scopes.PROVIDED.plus += configurations.compileOnly
-      }
+    module {
+       scopes.PROVIDED.plus += configurations.compileOnly
     }
+}
 
 eclipse {
     classpath {
@@ -83,8 +79,8 @@ eclipse {
     }
 }
 
-task resolveJBoss(type: Copy) {  
-    destinationDir = file('target') 
+task resolveJBoss(type: Copy) {
+    destinationDir = file('target')
     from { zipTree(configurations.jboss.singleFile) }
 }
 
@@ -95,16 +91,16 @@ task resolveGlassfish(type: Copy) {
 
 
 task tomeeEmbeddedTest(type: Test) {
-  systemProperty 'arquillian.launch', "tomee_embedded"
+  systemProperty 'arquillian.launch', 'tomee_embedded'
 }
 
 task jbossAS7ManagedTest(type: Test) {
-  systemProperty 'arquillian.launch', "jbossas_managed"
+  systemProperty 'arquillian.launch', 'jbossas_managed'
    dependsOn { resolveJBoss }
 }
 
 task glassfishManagedTest(type: Test){
-  systemProperty 'arquillian.launch', "glassfish_managed"
+  systemProperty 'arquillian.launch', 'glassfish_managed'
    dependsOn { resolveGlassfish }
 }
 
@@ -118,9 +114,8 @@ test {
   setEnabled(false)
 }
 
-jar {  
-
-       manifest {      
-               attributes 'Tapestry-Module-Classes': 
'org.apache.tapestry5.cdi.CDIInjectModule'
-               }
- }
+jar {
+    manifest {
+        attributes 'Tapestry-Module-Classes': 
'org.apache.tapestry5.cdi.CDIInjectModule'
+    }
+}
diff --git a/tapestry-clojure/build.gradle b/tapestry-clojure/build.gradle
index d1f044446..0d7c7c95f 100644
--- a/tapestry-clojure/build.gradle
+++ b/tapestry-clojure/build.gradle
@@ -1,21 +1,22 @@
-description = "Allows Clojure functions to be injected into Tapestry services 
and components"
+plugins {
+    id 'tapestry.junit5-spock-convention'
+}
+
+description = 'Allows Clojure functions to be injected into Tapestry services 
and components'
 
 dependencies {
-  implementation project(':tapestry-ioc')
-  implementation "org.clojure:clojure:1.6.0"
+    implementation project(':tapestry-ioc')
 
-  // Added just to prove that it works (TAP5-1945)
-  testImplementation project(':tapestry-core')
-  testRuntimeOnly "javax.servlet:javax.servlet-api:${versions.servletapi}"
-  testImplementation "org.junit.jupiter:junit-jupiter:${versions.junitJupiter}"
-}
+    implementation libs.clojure
 
-test {
-  useJUnit()
+    // Added just to prove that it works (TAP5-1945)
+    testImplementation project(':tapestry-core')
+
+    testRuntimeOnly libs.javax.servlet.api
 }
 
 jar {
-  manifest {
-    attributes 'Tapestry-Module-Classes': 
'org.apache.tapestry5.clojure.modules.ClojureModule'
-  }
-}
\ No newline at end of file
+    manifest {
+        attributes 'Tapestry-Module-Classes': 
'org.apache.tapestry5.clojure.modules.ClojureModule'
+    }
+}
diff --git a/tapestry-core/build.gradle b/tapestry-core/build.gradle
index e985abf06..c5ed7c95e 100644
--- a/tapestry-core/build.gradle
+++ b/tapestry-core/build.gradle
@@ -1,15 +1,15 @@
-import org.gradle.plugins.ide.idea.model.*
-import org.apache.tools.ant.filters.ReplaceTokens
-//import t5build.*
+plugins {
+    id 'tapestry.junit5-convention'
+    id 'tapestry.testng-convention'
+}
 
-description = "Central module for Tapestry, containing all core services and 
components"
+import t5build.TapestryBuildLogic
+import org.apache.tools.ant.filters.ReplaceTokens
 
-project.ext {
-    mainGeneratedDir = "src/main/generated"
-    testGeneratedDir = "src/test/generated"
-}
+description = 'Central module for Tapestry, containing all core services and 
components'
 
-clean.delete mainGeneratedDir, testGeneratedDir
+def npmWorkingDir = 'src/main/typescript/'
+def npmExec = TapestryBuildLogic.isWindows() ? 'npm.cmd' : 'npm'
 
 dependencies {
     api project(':tapestry-ioc')
@@ -17,148 +17,152 @@ dependencies {
     api project(':beanmodel')
     api project(':tapestry-http')
 
-    api "org.apache.commons:commons-lang3:${versions.commonsLang}"
+    implementation libs.jakarta.annotation.api
+    implementation libs.jakarta.xml.bind.api
 
-    
-    implementation 'jakarta.annotation:jakarta.annotation-api:1.3.4'
-    implementation 'jakarta.xml.bind:jakarta.xml.bind-api:2.3.2'
-    implementation 'org.glassfish.jaxb:jaxb-runtime:2.3.2'
-    implementation 'com.sun.xml.ws:jaxws-rt:2.3.2'
-    implementation 'javax.xml.ws:jaxws-api:2.3.1'
+    implementation libs.commons.codec
+    implementation libs.commons.lang3
 
-    provided project(":tapestry-test")
-    provided project(":tapestry-test-constants")
+    provided project(':tapestry-test')
+    provided project(':tapestry-test-constants')
 
-    provided "javax.servlet:javax.servlet-api:${versions.servletapi}"
+    provided libs.javax.servlet.api
 
-    implementation "commons-codec:commons-codec:1.10"
-    
-    testImplementation 
"org.junit.jupiter:junit-jupiter:${versions.junitJupiter}"
-    testImplementation "org.apache.httpcomponents:httpclient:4.5.14"
-    testRuntimeOnly project(':tapestry-spock')
+    testImplementation libs.httpcomponents.httpclient
+    testImplementation project(":tapestry-spock")
 
-    testRuntimeOnly "org.hsqldb:hsqldb:${versions.hsqldb}"
-    testRuntimeOnly 'com.google.inject:guice:3.0'
+    
testRuntimeOnly("${libs.hsqldb.get().module.group}:${libs.hsqldb.get().module.name}:${libs.hsqldb.get().version}:jdk8")
+    testRuntimeOnly libs.guice
 }
 
-def npmWorkingDir = "src/main/typescript/"
-
-task npmInstall(type: Exec) {
-    group "TypeScript"
-    description "Runs npm install"
+tasks.register('npmInstall', Exec) {
+    group = 'TypeScript'
+    description = 'Runs npm install'
     
     workingDir = npmWorkingDir
-    commandLine isWindows() ? "npm.cmd" : "npm", 'install'
+    commandLine npmExec, 'install'
 }
 
-task compileTypeScriptToAmd(type: Exec) {
+tasks.register('compileTypeScriptToAmd', Exec) {
+    group = 'TypeScript'
+    description = 'Compiles TypeScript to AMD modules'
+
     dependsOn npmInstall
 
     workingDir = npmWorkingDir
-    commandLine isWindows() ? "npm.cmd" : "npm", 'run', 'build-amd'
+    commandLine npmExec, 'run', 'build-amd'
 }
 
-task compileTypeScriptToEsModule(type: Exec) {
+tasks.register('compileTypeScriptToEsModule', Exec) {
+    group = 'TypeScript'
+    description = 'Compiles TypeScript to ES modules'
+
     dependsOn npmInstall
     
     workingDir = npmWorkingDir
-    commandLine isWindows() ? "npm.cmd" : "npm", 'run', 'build-es-module'
+    commandLine npmExec, 'run', 'build-es-module'
 }
 
-task compileTypeScript() {
-    dependsOn compileTypeScriptToAmd
-    dependsOn compileTypeScriptToEsModule
+tasks.register('compileTypeScript', Delete) {
+    group = 'TypeScript'
+    description = 'Compiles all TypeScript variants'
+
+    dependsOn compileTypeScriptToAmd, compileTypeScriptToEsModule
 }
 
-task generateTypeScriptDocs(type: Exec) {
+tasks.named('sourcesJar') {
+    dependsOn compileTypeScript
+}
+
+tasks.register('generateTypeScriptDocs', Exec) {
+    group = 'TypeScript'
+    description = 'Generates TypeScript documentation'
+
     dependsOn npmInstall
-    
+
     workingDir = npmWorkingDir
-    commandLine isWindows() ? "npm.cmd" : "npm", 'run', 'docs'
+    commandLine npmExec, 'run', 'docs'
 }
 
-task cleanTypeScriptFiles(type: Delete) {
-    delete fileTree("src/main/resources/META-INF/assets/es-modules/t5/core") {
-        include '**.js'
+tasks.register('cleanTypeScriptFiles', Delete) {
+    group = 'TypeScript'
+    description = 'Cleans generated TypeScript files'
+
+    delete fileTree('src/main/resources/META-INF/assets/es-modules/t5/core') {
+        include '**/*.js'
     }
-    delete fileTree("src/main/resources/META-INF/modules/t5/core") {
-        include '**.js'
+    delete fileTree('src/main/resources/META-INF/modules/t5/core') {
+        include '**/*.js'
     }
-    delete "src/main/typescript/docs"
+    delete 'src/main/typescript/docs'
+}
+
+tasks.named('processResources') {
+    dependsOn compileTypeScript
+}
+
+tasks.named('clean') {
+    dependsOn cleanTypeScriptFiles
 }
 
-processResources.dependsOn compileTypeScript
-clean.dependsOn cleanTypeScriptFiles
 
 // Not sure why this is necessary:
-compileTestGroovy.dependsOn compileTestJava
+tasks.named('compileTestGroovy') {
+    dependsOn compileTestJava
+}
 
 test {
-       // Needed to have XMLTokenStreamTests.testStreamEncoding() passing on 
Java 9+
+    // Needed to have XMLTokenStreamTests.testStreamEncoding() passing on Java 
9+
     if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_1_9)) {
-        jvmArgs("--add-opens=java.base/java.nio.charset=ALL-UNNAMED");
+        jvmArgs('--add-opens=java.base/java.nio.charset=ALL-UNNAMED')
+    }
+
+    // TAP5-2722
+    systemProperty 'user.language', 'en'
+}
+
+[
+    'app1',
+    'app2',
+    'app3',
+    'app4',
+    'app5',
+    'app7',
+    'appfolder'
+].each { appName ->
+    tasks.register("runTest${appName.capitalize()}", JavaExec) {
+        group = 'Application'
+        description = "Starts the ${appName} integration test app, useful when 
debugging."
+
+        mainClass = 'org.apache.tapestry5.test.JettyRunner'
+        args '-d', "src/test/${appName}", '-p', '8080'
+        classpath = sourceSets.test.runtimeClasspath
     }
-       // TAP5-2722
-       systemProperty 'user.language', 'en'
-}
-
-task runTestApp1(type:JavaExec) {
-  description 'Start app1 integration test app, useful when debugging failing 
integration tests'
-  main = 'org.apache.tapestry5.test.JettyRunner'
-  args "-d", "src/test/app1", "-p", "8080"
-  classpath += project.sourceSets.test.runtimeClasspath
-}
-task runTestApp2(type:JavaExec) {
-  description 'Start app2 integration test app, useful when debugging failing 
integration tests'
-  main = 'org.apache.tapestry5.test.JettyRunner'
-  args "-d", "src/test/app2", "-p", "8080"
-  classpath += project.sourceSets.test.runtimeClasspath
-}
-task runTestApp3(type:JavaExec) {
-  description 'Start app3 integration test app, useful when debugging failing 
integration tests'
-  main = 'org.apache.tapestry5.test.JettyRunner'
-  args "-d", "src/test/app3", "-p", "8080"
-  classpath += project.sourceSets.test.runtimeClasspath
-}
-task runTestApp4(type:JavaExec) {
-  description 'Start app4 integration test app, useful when debugging failing 
integration tests'
-  main = 'org.apache.tapestry5.test.JettyRunner'
-  args "-d", "src/test/app4", "-p", "8080"
-  classpath += project.sourceSets.test.runtimeClasspath
-}
-task runTestApp5(type:JavaExec) {
-  description 'Start app5 integration test app, useful when debugging failing 
integration tests'
-  main = 'org.apache.tapestry5.test.JettyRunner'
-  args "-d", "src/test/app5", "-p", "8080"
-  classpath += project.sourceSets.test.runtimeClasspath
-}
-task runTestApp7(type:JavaExec) {
-  description 'Start app7 integration test app, useful when debugging failing 
integration tests'
-  main = 'org.apache.tapestry5.test.JettyRunner'
-  args "-d", "src/test/app7", "-p", "8080"
-  classpath += project.sourceSets.test.runtimeClasspath
-}
-task runTestAppfolder(type:JavaExec) {
-  description 'Start appFolder integration test app, useful when debugging 
failing integration tests'
-  main = 'org.apache.tapestry5.test.JettyRunner'
-  args "-d", "src/test/appfolder", "-p", "8080"
-  classpath += project.sourceSets.test.runtimeClasspath
 }
 
 // Default is jQuery and Require.js enabled, so here are the tests for the
 // other combinations
 
-task testWithJqueryAndRequireJsDisabled(type:Test) {
-  systemProperties."tapestry.javascript-infrastructure-provider" = "jquery"
-  systemProperties."tapestry.require-js-enabled" = "false"
+tasks.register('testWithJqueryAndRequireJsDisabled', Test) {
+    group = 'Verification'
+    description = 'Runs tests with jQuery and Require.js disabled'
+
+    systemProperty 'tapestry.javascript-infrastructure-provider', 'jquery'
+    systemProperty 'tapestry.require-js-enabled', 'false'
 }
 
-task testWithPrototypeAndRequireJsEnabled(type:Test) {
-  systemProperties."tapestry.javascript-infrastructure-provider" = "prototype"
-  systemProperties."tapestry.require-js-enabled" = "true"
-}  
+tasks.register('testWithPrototypeAndRequireJsEnabled', Test) {
+    group = 'Verification'  
+    description = 'Runs tests with Prototype and Require.js enabled'
 
-task testWithPrototypeAndRequireJsDisabled(type:Test) {
-  systemProperties."tapestry.javascript-infrastructure-provider" = "prototype"
-  systemProperties."tapestry.require-js-enabled" = "false"
-}
\ No newline at end of file
+    systemProperty 'tapestry.javascript-infrastructure-provider', 'prototype'
+    systemProperty 'tapestry.require-js-enabled', 'true'
+}
+
+tasks.register('testWithPrototypeAndRequireJsDisabled', Test) {
+    group = 'Verification'
+    description = 'Runs tests with Prototype and Require.js disabled'
+
+    systemProperty 'tapestry.javascript-infrastructure-provider', 'prototype'
+    systemProperty 'tapestry.require-js-enabled', 'false'
+}
diff --git a/tapestry-func/build.gradle b/tapestry-func/build.gradle
index fa7b059ea..d52fbe364 100644
--- a/tapestry-func/build.gradle
+++ b/tapestry-func/build.gradle
@@ -1,7 +1,9 @@
-description = "Light-weight functional programming for Flows of values"
+plugins {
+    id 'tapestry.junit5-convention'
+}
+
+description = 'Light-weight functional programming for Flows of values'
 
 dependencies {
-  testImplementation   "org.testng:testng:${versions.testng}"
-  testImplementation   "commons-lang:commons-lang:2.6"
-  testImplementation "org.junit.jupiter:junit-jupiter:${versions.junitJupiter}"
-}
\ No newline at end of file
+    testImplementation libs.commons.lang3
+}
diff --git a/tapestry-hibernate-core/build.gradle 
b/tapestry-hibernate-core/build.gradle
index 3e541327a..f89144f41 100644
--- a/tapestry-hibernate-core/build.gradle
+++ b/tapestry-hibernate-core/build.gradle
@@ -1,21 +1,26 @@
-description = "Basic Hibernate services for Tapestry, useable outside of a 
Tapestry web application"
+plugins {
+    id 'tapestry.testng-convention'
+}
+
+description = 'Basic Hibernate services for Tapestry, useable outside of a 
Tapestry web application'
 
+def moduleLibs = [
+    jaxbRuntime: 'org.glassfish.jaxb:jaxb-runtime:2.3.2',
+]
 
 dependencies {
     implementation project(':tapestry-ioc')
 
-    api group: 'org.hibernate', name: 'hibernate-core', version: 
versions.hibernate
-    
-    implementation 'org.glassfish.jaxb:jaxb-runtime:2.3.2'
+    api libs.hibernate.core
+
+    implementation moduleLibs.jaxbRuntime
 
     testImplementation project(':tapestry-test')
-    testImplementation "org.easymock:easymock:${versions.easymock}"
-    testImplementation "org.testng:testng:${versions.testng}", { transitive = 
false }
-    testRuntimeOnly "org.hsqldb:hsqldb:${versions.hsqldb}"
+    
testRuntimeOnly("${libs.hsqldb.get().module.group}:${libs.hsqldb.get().module.name}:${libs.hsqldb.get().version}:jdk8")
 }
 
 jar {
     manifest {
         attributes 'Tapestry-Module-Classes': 
'org.apache.tapestry5.hibernate.modules.HibernateCoreModule'
     }
-}
\ No newline at end of file
+}
diff --git a/tapestry-hibernate/build.gradle b/tapestry-hibernate/build.gradle
index f13cdcde9..de74ab1cd 100644
--- a/tapestry-hibernate/build.gradle
+++ b/tapestry-hibernate/build.gradle
@@ -1,24 +1,33 @@
-description = "Provides support for simple CRUD applications built on top of 
Tapestry and Hibernate"
+plugins {
+    id 'tapestry.testng-convention'
+}
+
+description = 'Provides support for simple CRUD applications built on top of 
Tapestry and Hibernate'
+
+def moduleLibs = [
+    jbossLogging: 'org.jboss.logging:jboss-logging:3.3.0.Final',
+]
 
 dependencies {
-  api project(':tapestry-core')
-  api project(':tapestry-hibernate-core')
-  implementation "org.jboss.logging:jboss-logging:3.3.0.Final"  
+    api project(':tapestry-core')
+    api project(':tapestry-hibernate-core')
 
-  testImplementation project(':tapestry-test')
-  
-  testRuntimeOnly "org.hsqldb:hsqldb:${versions.hsqldb}"
+    implementation moduleLibs.jbossLogging
+
+    testImplementation project(':tapestry-test')
+    
testRuntimeOnly("${libs.hsqldb.get().module.group}:${libs.hsqldb.get().module.name}:${libs.hsqldb.get().version}:jdk8")
 }
 
-jar {
+tasks.register('runTestApp0', JavaExec) {
+    description = 'Start tapestry-hibernate integration test app, useful when 
debugging failing integration tests'
+
+    mainClass = 'org.apache.tapestry5.test.JettyRunner'
+    args '-d', 'src/test/webapp', '-p', '8080'
+    classpath += project.sourceSets.test.runtimeClasspath
+}
+
+tasks.named('jar', Jar) {
     manifest {
         attributes 'Tapestry-Module-Classes': 
'org.apache.tapestry5.hibernate.web.modules.HibernateModule'
     }
 }
-
-task runTestApp0(type:JavaExec) {
-  description 'Start tapestry-hibernate integration test app, useful when 
debugging failing integration tests'
-  main = 'org.apache.tapestry5.test.JettyRunner'
-  args "-d", "src/test/webapp", "-p", "8080"
-  classpath += project.sourceSets.test.runtimeClasspath
-}
diff --git a/tapestry-http/build.gradle b/tapestry-http/build.gradle
index be6b4cede..f6cbd76da 100644
--- a/tapestry-http/build.gradle
+++ b/tapestry-http/build.gradle
@@ -1,32 +1,36 @@
-import org.gradle.plugins.ide.idea.model.*
+plugins {
+    id 'tapestry.testng-convention'
+}
+
 import org.apache.tools.ant.filters.ReplaceTokens
-import t5build.*
 
-description = "Tapestry classes that handle HTTP requests"
+description = 'Tapestry classes that handle HTTP requests'
 
 dependencies {
     implementation project(':tapestry-ioc')
     implementation project(':tapestry-json')
     implementation project(':beanmodel')
 
-    provided project(":tapestry-test")
-    provided project(":tapestry-test-constants")
-
-    provided "javax.servlet:javax.servlet-api:${versions.servletapi}"
+    provided libs.javax.servlet.api
 
-    implementation "commons-codec:commons-codec:1.10"
-    implementation "commons-io:commons-io:${versions.commonsIo}"
+    implementation libs.commons.codec
+    implementation libs.commons.io
 
-    testRuntimeOnly "org.hsqldb:hsqldb:${versions.hsqldb}"
+    testImplementation project(':tapestry-test')
+    testImplementation project(':tapestry-test-constants')
 
-    testRuntimeOnly 'com.google.inject:guice:3.0'
+    
testRuntimeOnly("${libs.hsqldb.get().module.group}:${libs.hsqldb.get().module.name}:${libs.hsqldb.get().version}:jdk8")
+    testRuntimeOnly libs.guice
+    testRuntimeOnly libs.slf4j.simple
 }
 
 // Not sure why this is necessary:
-compileTestGroovy.dependsOn compileTestJava
+tasks.named('compileTestGroovy') {
+    dependsOn compileTestJava
+}
 
-jar {
-    from("src/main/filtered-resources") {
+tasks.named('jar', Jar) {
+    from('src/main/filtered-resources') {
         filter(ReplaceTokens, tokens: [version: project.parent.version])
     }
-}
\ No newline at end of file
+}
diff --git a/tapestry-internal-test/build.gradle 
b/tapestry-internal-test/build.gradle
index 9caa72c9b..4213b6d2d 100644
--- a/tapestry-internal-test/build.gradle
+++ b/tapestry-internal-test/build.gradle
@@ -1,5 +1,5 @@
-description = "Internal utilties used to assist with testing; not intended for 
outside use"
+description = 'Internal utilties used to assist with testing; not intended for 
outside use'
 
 dependencies {
-    implementation project(":tapestry-core")
-}
\ No newline at end of file
+    implementation project(':tapestry-core')
+}
diff --git a/tapestry-ioc-jcache/build.gradle b/tapestry-ioc-jcache/build.gradle
index 878cbab07..6d10137f7 100644
--- a/tapestry-ioc-jcache/build.gradle
+++ b/tapestry-ioc-jcache/build.gradle
@@ -1,35 +1,41 @@
-import org.gradle.plugins.ide.idea.model.*
-import t5build.*
-
-description = "JCache (JSR 107) integration for Tapestry-IoC"
+plugins {
+    id 'tapestry.junit4-legacy-convention'
+}
 
-//apply plugin: JavaPlugin
+description = 'JCache (JSR 107) integration for Tapestry-IoC'
 
-project.ext.libraryVersions = [
-    jcache: '1.0.0',
+def moduleLibs = [
+    javaxCacheApi: 'javax.cache:cache-api:1.0.0',
+    javaxCacheTests: 'javax.cache:cache-tests:1.0.0',
+    javaxCacheDomain: 'javax.cache:test-domain:1.0.0',
+    cacheAnnotations: 'org.jsr107.ri:cache-annotations-ri-common:1.0.0',
+    infinispanJcache: 'org.infinispan:infinispan-jcache:7.0.0.Alpha4',
 ]
 
 dependencies {
-    /*compile "org.apache.tapestry:tapestry-ioc:${tapestryVersion()}"*/
-       provided project(":tapestry-ioc")
-       implementation "javax.cache:cache-api:${libraryVersions.jcache}"
-       implementation 
"org.jsr107.ri:cache-annotations-ri-common:${libraryVersions.jcache}"
-    testImplementation "javax.cache:cache-tests:${libraryVersions.jcache}"
-    testImplementation "javax.cache:test-domain:${libraryVersions.jcache}"
-    testRuntimeOnly "org.infinispan:infinispan-jcache:7.0.0.Alpha4" /* Just to 
be able to run the tests */
+    provided project(":tapestry-ioc")
+
+    implementation moduleLibs.javaxCacheApi
+    implementation moduleLibs.cacheAnnotations
+
+    testImplementation(moduleLibs.javaxCacheTests)
+    testImplementation moduleLibs.javaxCacheDomain
+    testRuntimeOnly moduleLibs.infinispanJcache /* Just to be able to run the 
tests */
 }
 
-jar {
-       manifest {
-               attributes 'Tapestry-Module-Classes': 
'org.apache.tapestry5.jcache.module.JCacheModule'
-       }
+tasks.named('jar', Jar) {
+    manifest {
+        attributes 'Tapestry-Module-Classes': 
'org.apache.tapestry5.jcache.module.JCacheModule'
+    }
 }
 
 test {
-  useJUnit()
-
-  // those tests do not work with JDK 6 
(java.lang.UnsupportedClassVersionError)
-  exclude '**/InterceptionUsingDefaultCacheNameTest.class',
-   '**/InterceptionCacheNameOnEachMethodTest.class',
-   '**/InterceptionUsingCacheConfigTest.class'
+    if (JavaVersion.current().isJava9Compatible()) {
+        jvmArgs '--add-opens', 'java.base/java.util=ALL-UNNAMED'
+        jvmArgs '--add-opens', 'java.base/java.util.concurrent=ALL-UNNAMED'
+        jvmArgs '--add-opens', 'java.base/java.io=ALL-UNNAMED'
+        jvmArgs '--add-opens', 'java.base/java.lang=ALL-UNNAMED'
+        jvmArgs '--add-opens', 'java.base/java.lang.invoke=ALL-UNNAMED'
+        jvmArgs '--add-opens', 'java.base/java.lang.reflect=ALL-UNNAMED'
+    }
 }
diff --git a/tapestry-ioc-junit/build.gradle b/tapestry-ioc-junit/build.gradle
index 39a81a2ca..117e20ba3 100644
--- a/tapestry-ioc-junit/build.gradle
+++ b/tapestry-ioc-junit/build.gradle
@@ -1,10 +1,13 @@
-description = "Utilities for junit testing a tapestry-ioc application"
+plugins {
+    id 'tapestry.junit5-convention'
+}
+
+description = 'Utilities for junit testing a tapestry-ioc application'
 
 dependencies {
     implementation project(':tapestry-ioc')
-       implementation 'junit:junit:4.11'
-}
 
-test {
-    useJUnit()
+    // We also need these in implementation
+    implementation platform(libs.junit.bom)
+    implementation libs.junit.jupiter.api
 }
diff --git a/tapestry-ioc/build.gradle b/tapestry-ioc/build.gradle
index df3f1d9ac..75879fb7b 100644
--- a/tapestry-ioc/build.gradle
+++ b/tapestry-ioc/build.gradle
@@ -1,35 +1,28 @@
-description = "A code-centric, high-performance, simple Inversion of Control 
container"
+plugins {
+    id 'tapestry.junit5-spock-convention'
+}
+description = 'A code-centric, high-performance, simple Inversion of Control 
container'
 
 dependencies {
     api project(':tapestry-func')
-    api project(':tapestry5-annotations')
-    api project(":plastic")
-    api project(":beanmodel")
+    api project(':commons')
+    implementation project(":beanmodel")
 
     provided project(':tapestry-test')
 
     // For now, keep these compile dependencies synchronized with the binaries 
dependencies
     // of the top-level build:
 
-    api "javax.inject:javax.inject:1"
-    api "javax.annotation:javax.annotation-api:1.3.2"
+    api libs.javax.inject
+    api libs.jakarta.annotation.api
 
-    api "org.slf4j:slf4j-api:${versions.slf4j}"
+    testImplementation libs.commons.lang3
+    testImplementation libs.hibernate.core
+    
testRuntimeOnly("${libs.hsqldb.get().module.group}:${libs.hsqldb.get().module.name}:${libs.hsqldb.get().version}:jdk8")
 
-    testImplementation "commons-lang:commons-lang:2.6"
-    testImplementation "org.apache.commons:commons-lang3:3.4"
-    
-    testImplementation "org.apache.commons:commons-lang3:3.4"
-    testImplementation "org.hibernate:hibernate-core:5.2.10.Final"
-    testRuntimeOnly "org.hsqldb:hsqldb:${versions.hsqldb}"
-    
-    testImplementation 
"org.junit.jupiter:junit-jupiter:${versions.junitJupiter}"
-    
-    provided "org.testng:testng:${versions.testng}", { transitive = false }
 }
 
-test {
-       useJUnitPlatform()
+tasks.named('test', Test) {
     // Override the master build.gradle
-    systemProperties.remove("tapestry.service-reloading-enabled")
-}
\ No newline at end of file
+    systemProperties.remove('tapestry.service-reloading-enabled')
+}
diff --git a/tapestry-javadoc/build.gradle b/tapestry-javadoc/build.gradle
index d72b91a73..59bd8a2b7 100644
--- a/tapestry-javadoc/build.gradle
+++ b/tapestry-javadoc/build.gradle
@@ -1,32 +1,35 @@
-description = "JavaDoc Plugin for Tapestry that generates component reference 
documentation for component classes"
+plugins {
+    id 'tapestry.junit5-convention'
+}
+
+description = 'JavaDoc Plugin for Tapestry that generates component reference 
documentation for component classes'
 
 dependencies {
-  implementation project(':tapestry-core')
-  implementation "commons-lang:commons-lang:2.6"
-  
-  if (JavaVersion.current() <= JavaVersion.VERSION_1_8) {
-    implementation files(getTools())
-  }
-  
-}
+    implementation project(':tapestry-core')
 
-/** Returns the tools.jar/classes.jar of the Java runtime. */
-File getTools() {
+    implementation libs.commons.lang3
+    implementation libs.commons.text
 
-  def dir = isMacOSX_1_6() ? "classes" : "lib"
-       
-  def jar = isMacOSX_1_6() ? "classes" : "tools.jar"
-  
-  def jreRelpath = "../${dir}/${jar}"
-  def jdkRelpath = "$dir/${jar}"
-  
-  def javaHome = System.properties['java.home']
+    testImplementation platform(libs.spock.bom)
+    testImplementation libs.spock.core
+}
 
-  return new File(javaHome, jdkRelpath).exists() ? new File(javaHome, 
jdkRelpath) : new File(javaHome, jreRelpath)
+java {
+    toolchain {
+        languageVersion = JavaLanguageVersion.of(11)
+    }
 }
 
-boolean isMacOSX_1_6() {
-  System.properties['os.name'].toLowerCase().contains('mac os') &&
-      System.properties['java.version'].startsWith("1.6.")
+tasks.withType(JavaCompile).configureEach {
+    // Override Java 8 compatibility inherited from root
+    sourceCompatibility = '11'
+    targetCompatibility = '11'
+
+    // Only apply --add-modules if target is not 1.8
+    if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_11)) {
+        options.compilerArgs += [
+            '--add-modules',
+            'jdk.compiler'
+        ]
+    }
 }
-  
\ No newline at end of file
diff --git a/tapestry-jmx/build.gradle b/tapestry-jmx/build.gradle
index ef371e800..00694090c 100644
--- a/tapestry-jmx/build.gradle
+++ b/tapestry-jmx/build.gradle
@@ -1,18 +1,17 @@
-description = "Allows easy exposure of Tapestry Services as JMX MBeans"
+plugins {
+    id 'tapestry.testng-convention'
+}
 
-dependencies {
-  implementation project(':tapestry-core')
+description = 'Allows easy exposure of Tapestry Services as JMX MBeans'
 
-  testImplementation project(':tapestry-test')
-  }
+dependencies {
+    implementation project(':tapestry-core')
 
-test {
-    systemProperties "tapestry.service-reloading-enabled": "false"
+    testImplementation project(':tapestry-test')
 }
 
-
 jar {
     manifest {
         attributes 'Tapestry-Module-Classes': 
'org.apache.tapestry5.jmx.modules.JmxModule'
     }
-}
\ No newline at end of file
+}
diff --git a/tapestry-jpa/build.gradle b/tapestry-jpa/build.gradle
index e72805b6a..58c723e7b 100644
--- a/tapestry-jpa/build.gradle
+++ b/tapestry-jpa/build.gradle
@@ -1,40 +1,41 @@
-description = "Provides support for simple CRUD applications built on top of 
Tapestry and JPA"
+plugins {
+    id 'tapestry.testng-convention'
+}
+
+description = 'Provides support for simple CRUD applications built on top of 
Tapestry and JPA'
+
+def moduleLibs = [
+    jpaSpecs: 'org.apache.geronimo.specs:geronimo-jpa_2.0_spec:1.1',
+    javaxCdiApi: 'javax.enterprise:cdi-api:1.2',
+    eclipselink: 'org.eclipse.persistence:eclipselink:2.7.7',
+    dbcp: 'org.apache.tomcat:dbcp:6.0.32',
+    h2: 'com.h2database:h2:1.3.175'
+]
 
 dependencies {
-  implementation project(':tapestry-core')
-  implementation "org.apache.geronimo.specs:geronimo-jpa_2.0_spec:1.1"
-  implementation 'javax.enterprise:cdi-api:1.2'
+    implementation project(':tapestry-core')
 
+    implementation moduleLibs.jpaSpecs
+    implementation moduleLibs.javaxCdiApi
 
-  testImplementation project(':tapestry-test')
-  testImplementation 'org.eclipse.persistence:eclipselink:2.7.7'
+    testImplementation project(':tapestry-test')
+    testImplementation moduleLibs.eclipselink
 
-  testRuntimeOnly "com.h2database:h2:1.2.145"
-  testRuntimeOnly "org.apache.tomcat:dbcp:6.0.32"
-  testRuntimeOnly 'com.h2database:h2:1.3.175'
+    testRuntimeOnly moduleLibs.dbcp
+    testRuntimeOnly moduleLibs.h2
 }
 
-repositories {
-    maven {
-        name "EclipseLink"
-        url "https://download.eclipse.org/rt/eclipselink/maven.repo/";
+(1..6).each {
+    tasks.register("runTestApp${it}", JavaExec) {
+        description = "Start app${it} integration test app, useful when 
debugging failing integration tests"
+        mainClass = 'org.apache.tapestry5.test.JettyRunner'
+        args '-d', "src/test/app${it}", '-p', '8080'
+        classpath = project.sourceSets.test.runtimeClasspath
     }
 }
 
-test {
-    systemProperties "tapestry.service-reloading-enabled": "false"
-}
-
-
-jar {
+tasks.named('jar', Jar) {
     manifest {
         attributes 'Tapestry-Module-Classes': 
'org.apache.tapestry5.jpa.modules.JpaModule'
     }
 }
-
-task runTestApp6(type:JavaExec) {
-    description 'Start app6 integration test app, useful when debugging 
failing integration tests'
-    main = 'org.apache.tapestry5.test.JettyRunner'
-    args "-d", "src/test/app6", "-p", "8080"
-    classpath += project.sourceSets.test.runtimeClasspath
-}
diff --git a/tapestry-json/build.gradle b/tapestry-json/build.gradle
index 055ed9a73..9d71e30d7 100644
--- a/tapestry-json/build.gradle
+++ b/tapestry-json/build.gradle
@@ -1,16 +1,18 @@
-description = "Repackaged, improved (and tested) version of code originally 
from https://github.com/tdunning/open-json";
+plugins {
+    id 'tapestry.junit5-convention'
+}
+
+description = 'Repackaged, improved (and tested) version of code originally 
from https://github.com/tdunning/open-json'
 
 dependencies {
-    provided project(':tapestry-ioc')
-    testImplementation 
"org.junit.jupiter:junit-jupiter:${versions.junitJupiter}"
-}
+    implementation project(':tapestry5-annotations')
+    implementation project(':commons')
 
-test {
-    useJUnit()
+    testImplementation project(":tapestry-func")
 }
 
-jar {
+tasks.named('jar', Jar) {
     manifest {
         attributes 'Tapestry-Module-Classes': 
'org.apache.tapestry5.json.modules.JSONModule'
     }
-}
\ No newline at end of file
+}
diff --git a/tapestry-kaptcha/build.gradle b/tapestry-kaptcha/build.gradle
index 4b79ed964..8ccbd8a4a 100644
--- a/tapestry-kaptcha/build.gradle
+++ b/tapestry-kaptcha/build.gradle
@@ -1,17 +1,25 @@
-description = "Kaptcha user verification support"
+plugins {
+    id 'tapestry.testng-convention'
+}
+
+description = 'Kaptcha user verification support'
+
+def moduleLibs= [
+    kaptcha: 'com.github.axet:kaptcha:0.0.8',
+]
 
 dependencies {
     implementation project(':tapestry-core')
 
-    implementation "com.github.axet:kaptcha:0.0.8"
+    implementation moduleLibs.kaptcha
 
-    provided "javax.servlet:javax.servlet-api:${versions.servletapi}"
+    provided libs.javax.servlet.api
 
     testImplementation project(':tapestry-test')
 }
 
-jar {
+tasks.named('jar', Jar) {
     manifest {
         attributes 'Tapestry-Module-Classes': 
'org.apache.tapestry5.kaptcha.modules.KaptchaModule'
     }
-}
\ No newline at end of file
+}
diff --git a/tapestry-latest-java-tests/build.gradle 
b/tapestry-latest-java-tests/build.gradle
index 4371b82ff..6954db351 100644
--- a/tapestry-latest-java-tests/build.gradle
+++ b/tapestry-latest-java-tests/build.gradle
@@ -1,26 +1,20 @@
-description = "Test suite for making sure Tapestry runs on latest Java"
+plugins {
+    id 'tapestry.junit5-spock-convention'
+}
 
-sourceCompatibility = "17"
-targetCompatibility = "17"
+description = 'Test suite for making sure Tapestry runs on latest Java'
 
-tasks.withType(JavaCompile).each { 
-    it.options.compilerArgs.add("--enable-preview")
-}
+sourceCompatibility = '17'
+targetCompatibility = '17'
 
-test {
-    dependsOn 'testNG'
-    useJUnitPlatform()
-    jvmArgs(["--enable-preview"])
+tasks.withType(JavaCompile).configureEach {
+    options.compilerArgs += '--enable-preview'
 }
 
-tasks.register('testNG', Test) {
-    useTestNG()
-    jvmArgs(["--enable-preview"])
+tasks.withType(Test).configureEach {
+    jvmArgs += '--enable-preview'
 }
 
 dependencies {
     testImplementation project(':tapestry-ioc')
-    testImplementation "org.slf4j:slf4j-api:${versions.slf4j}"
-    testImplementation "org.testng:testng:${versions.testng}"
-    testImplementation 
"org.junit.jupiter:junit-jupiter:${versions.junitJupiter}"
 }
diff --git a/tapestry-mongodb/build.gradle b/tapestry-mongodb/build.gradle
index 6428ece0d..bd6e72126 100644
--- a/tapestry-mongodb/build.gradle
+++ b/tapestry-mongodb/build.gradle
@@ -1,18 +1,25 @@
-description = "Basic MongoDB services for Tapestry, useable outside of a 
Tapestry web application"
+plugins {
+    id 'tapestry.junit5-spock-convention'
+}
+
+import groovy.transform.AutoExternalize
+
+description = 'Basic MongoDB services for Tapestry, useable outside of a 
Tapestry web application'
+
+def moduleLibs = [
+    mongoJavaDriver: 'org.mongodb:mongo-java-driver:2.10.1',
+    jongo: 'org.jongo:jongo:0.3',
+    embedMongo: 'de.flapdoodle.embed:de.flapdoodle.embed.mongo:1.28',
+]
 
 dependencies {
     implementation project(':tapestry-ioc')
-    testImplementation project(':tapestry-test')
-
-    implementation group: 'org.mongodb', name: 'mongo-java-driver', version: 
'2.10.1'
 
-    testImplementation group: 'org.jongo', name: 'jongo', version: '0.3'
-    testImplementation 
"org.junit.jupiter:junit-jupiter:${versions.junitJupiter}"
-    testImplementation "de.flapdoodle.embed:de.flapdoodle.embed.mongo:1.28"
-}
+    implementation moduleLibs.mongoJavaDriver
 
-test {
-    useJUnit()
+    testImplementation project(':tapestry-test')
+    testImplementation moduleLibs.jongo
+    testImplementation moduleLibs.embedMongo
 }
 
 jar {
diff --git a/tapestry-openapi-viewer/build.gradle 
b/tapestry-openapi-viewer/build.gradle
index b6a62e6ff..5662fb5ba 100644
--- a/tapestry-openapi-viewer/build.gradle
+++ b/tapestry-openapi-viewer/build.gradle
@@ -1,11 +1,12 @@
-description = "Embedded OpenAPI (Swagger) viewer for Tapestry, based on 
Swagger UI"
+description = 'Embedded OpenAPI (Swagger) viewer for Tapestry, based on 
Swagger UI'
 
 dependencies {
     implementation project(':tapestry-core')
-    testImplementation "javax.servlet:javax.servlet-api:${versions.servletapi}"
+
+    testImplementation libs.javax.servlet.api
 }
 
-jar {
+tasks.named('jar', Jar) {
     manifest {
         attributes 'Tapestry-Module-Classes': 
'org.apache.tapestry5.openapiviewer.modules.TapestryOpenApiViewerModule'
     }
diff --git a/tapestry-rest-jackson/build.gradle 
b/tapestry-rest-jackson/build.gradle
index aa806f3a4..85e76208f 100644
--- a/tapestry-rest-jackson/build.gradle
+++ b/tapestry-rest-jackson/build.gradle
@@ -1,13 +1,19 @@
-description = "Support for using Jackson Databind with the Tapestry REST 
support"
+def moduleLibs = [
+    jsonschemaGenerator: 'com.github.victools:jsonschema-generator:4.20.0',
+]
+
+description = 'Support for using Jackson Databind with the Tapestry REST 
support'
 
 dependencies {
     implementation project(':tapestry-core')
-    implementation 
"com.fasterxml.jackson.core:jackson-databind:${versions.jackson}"
-    implementation 
"com.github.victools:jsonschema-generator:${versions.jsonschemaGenerator}"
-    provided "javax.servlet:javax.servlet-api:${versions.servletapi}"
+
+    implementation libs.jackson.databind
+    implementation moduleLibs.jsonschemaGenerator
+
+    provided libs.javax.servlet.api
 }
 
-jar {
+tasks.named('jar', Jar) {
     manifest {
         attributes 'Tapestry-Module-Classes': 
'org.apache.tapestry5.rest.jackson.modules.RestJacksonModule'
     }
diff --git a/tapestry-runner/build.gradle b/tapestry-runner/build.gradle
index 88acdb405..1391e5b0c 100644
--- a/tapestry-runner/build.gradle
+++ b/tapestry-runner/build.gradle
@@ -1,17 +1,22 @@
-description = "Utilities for running a Tapestry application in Jetty or Tomcat 
as part of test suite."
+description = 'Utilities for running a Tapestry application in Jetty or Tomcat 
as part of test suite.'
+
+def moduleVersions = [
+    jetty: '8.1.19.v20160209',
+    tomcat: '7.0.70',
+]
 
 dependencies {
+    api project(':tapestry-test-constants')
 
-    api project(":tapestry-test-constants")
-    implementation "org.eclipse.jetty:jetty-server:${versions.jetty}"
-    implementation "org.eclipse.jetty:jetty-jndi:${versions.jetty}"
-    implementation "org.eclipse.jetty:jetty-plus:${versions.jetty}"
-    implementation "org.eclipse.jetty:jetty-webapp:${versions.jetty}"
+    implementation "org.eclipse.jetty:jetty-server:${moduleVersions.jetty}"
+    implementation "org.eclipse.jetty:jetty-jndi:${moduleVersions.jetty}"
+    implementation "org.eclipse.jetty:jetty-plus:${moduleVersions.jetty}"
+    implementation "org.eclipse.jetty:jetty-webapp:${moduleVersions.jetty}"
 
-    implementation "org.apache.tomcat:tomcat-catalina:${versions.tomcat}"
-    implementation "org.apache.tomcat:tomcat-coyote:${versions.tomcat}"
-    implementation "org.apache.tomcat:tomcat-jasper:${versions.tomcat}"
+    implementation "org.apache.tomcat:tomcat-catalina:${moduleVersions.tomcat}"
+    implementation "org.apache.tomcat:tomcat-coyote:${moduleVersions.tomcat}"
+    implementation "org.apache.tomcat:tomcat-jasper:${moduleVersions.tomcat}"
 
-    implementation "org.apache.tomcat:tomcat-dbcp:${versions.tomcat}"
-    implementation "commons-cli:commons-cli:1.2"
+    implementation "org.apache.tomcat:tomcat-dbcp:${moduleVersions.tomcat}"
+    implementation libs.commons.cli
 }
diff --git a/tapestry-spock/build.gradle b/tapestry-spock/build.gradle
index 894614390..50a9a60c8 100644
--- a/tapestry-spock/build.gradle
+++ b/tapestry-spock/build.gradle
@@ -1,14 +1,16 @@
-description = "Provides support Tapestry injections in Spock specifications"
+plugins {
+    id 'tapestry.junit5-convention'
+    id 'groovy'
+}
+
+description = 'Provides support Tapestry injections in Spock specifications'
 
 dependencies {
-       implementation project(':commons')
-       implementation project(':tapestry-ioc')
-       implementation "org.spockframework:spock-core:${versions.spock}"
-       
-       testImplementation "javax.inject:javax.inject:1"
-    testImplementation 
"org.junit.jupiter:junit-jupiter:${versions.junitJupiter}"
-}
+    provided project(':commons')
+    provided project(':tapestry-ioc')
+
+    api platform(libs.spock.bom)
+    api libs.spock.core
 
-test {
-       useJUnitPlatform()
+    testImplementation libs.javax.inject
 }
diff --git a/tapestry-spring/build.gradle b/tapestry-spring/build.gradle
index 1b4bb0c34..fd0346f6a 100644
--- a/tapestry-spring/build.gradle
+++ b/tapestry-spring/build.gradle
@@ -1,30 +1,34 @@
-description = "Integration of Tapestry with the Spring Inversion Of Control 
Container"
+plugins {
+    id 'tapestry.testng-convention'
+}
+
+description = 'Integration of Tapestry with the Spring Inversion Of Control 
Container'
 
+def moduleLibs = [
+    springWeb: 'org.springframework:spring-web:3.2.9.RELEASE',
+]
 dependencies {
-  implementation project(':tapestry-core')
-  implementation "org.springframework:spring-web:3.2.9.RELEASE"
+    implementation project(':tapestry-core')
+
+    implementation moduleLibs.springWeb
 
-  provided "javax.servlet:javax.servlet-api:${versions.servletapi}"
+    provided libs.javax.servlet.api
 
-  testImplementation project(':tapestry-test')
+    testImplementation project(':tapestry-test')
 }
 
-jar {
-    manifest {
-        attributes 'Tapestry-Module-Classes': 
'org.apache.tapestry5.spring.modules.SpringModule'
+['', '1'].each { suffix ->
+    tasks.register("runTestApp${suffix}", JavaExec) {
+        group = 'Application'
+        description = "Start tapestry-spring integration test app${suffix}, 
useful when debugging failing integration tests"
+        mainClass = 'org.apache.tapestry5.test.JettyRunner'
+        args '-d', "src/test/webapp${suffix}", '-p', '8080'
+        classpath += project.sourceSets.test.runtimeClasspath
     }
 }
 
-task runTestApp(type:JavaExec) {
-    description 'Start tapestry-spring integration test app, useful when 
debugging failing integration tests'
-    main = 'org.apache.tapestry5.test.JettyRunner'
-    args "-d", "src/test/webapp", "-p", "8080"
-    classpath += project.sourceSets.test.runtimeClasspath
+tasks.named('jar', Jar) {
+    manifest {
+        attributes 'Tapestry-Module-Classes': 
'org.apache.tapestry5.spring.modules.SpringModule'
+    }
 }
-
-task runTestApp1(type:JavaExec) {
-    description 'Start tapestry-spring integration test app 1, useful when 
debugging failing integration tests'
-    main = 'org.apache.tapestry5.test.JettyRunner'
-    args "-d", "src/test/webapp1", "-p", "8080"
-    classpath += project.sourceSets.test.runtimeClasspath
-}
\ No newline at end of file
diff --git a/tapestry-test-data/build.gradle b/tapestry-test-data/build.gradle
index 63229bc4f..ea9530ee9 100644
--- a/tapestry-test-data/build.gradle
+++ b/tapestry-test-data/build.gradle
@@ -1,5 +1,5 @@
-description = "Utilities for generating random data used when populating 
entities"
+description = 'Utilities for generating random data used when populating 
entities'
 
 dependencies {
-    implementation "org.slf4j:slf4j-api:${versions.slf4j}"
+    implementation libs.slf4j.api
 }
diff --git a/tapestry-test/build.gradle b/tapestry-test/build.gradle
index faf10b63e..70b34c377 100644
--- a/tapestry-test/build.gradle
+++ b/tapestry-test/build.gradle
@@ -1,20 +1,27 @@
-description = "[Deprecated] Utilities for integration testing of Tapestry 
applications using Selenium"
+plugins {
+    id 'tapestry.testng-convention'
+}
+
+description = '[Deprecated] Utilities for integration testing of Tapestry 
applications using Selenium'
 
 dependencies {
 
-  api project(":tapestry-test-data")
-  api project(":tapestry-runner")
+    api project(':tapestry-test-data')
+    api project(':tapestry-runner')
+
+    implementation libs.webdrivermanager
 
-  implementation 
("io.github.bonigarcia:webdrivermanager:${versions.webdriverManager}")
+    api libs.selenium.java
 
-  api "org.seleniumhq.selenium:selenium-leg-rc:${versions.selenium}", {
-      exclude group: "org.seleniumhq.selenium", module: "jetty-repacked"
-      exclude group: "org.testng", module: "testng"
-      exclude group: "javax.servlet", module: "javax.servlet-api"
-      exclude group: "org.seleniumhq.selenium", module: 
"selenium-firefox-driver"
-  }
-  api "org.seleniumhq.selenium:selenium-java:${versions.seleniumServer}"
+    api(libs.selenium.legrc) {
+        exclude group: 'org.seleniumhq.selenium', module: 'jetty-repacked'
+        exclude group: 'org.testng', module: 'testng'
+        exclude group: 'junit', module: 'junit'
+        exclude group: 'javax.servlet', module: 'javax.servlet-api'
+        exclude group: 'org.seleniumhq.selenium', module: 
'selenium-firefox-driver'
+    }
 
-  api "org.testng:testng:${versions.testng}"
-  api "org.easymock:easymock:${versions.easymock}"
+    // we need to add them to api
+    api libs.testng
+    api libs.easymock
 }
diff --git a/tapestry-upload/build.gradle b/tapestry-upload/build.gradle
index 99d15479c..048f8e0a2 100644
--- a/tapestry-upload/build.gradle
+++ b/tapestry-upload/build.gradle
@@ -1,15 +1,26 @@
-description = "File Upload component, with supporting services"
+plugins {
+    id 'tapestry.testng-convention'
+}
+
+description = 'File Upload component, with supporting services'
+
+def moduleLibs = [
+    commonsFileupload: 'commons-fileupload:commons-fileupload:1.5',
+]
 
 dependencies {
-  implementation project(':tapestry-core')
-  api "commons-fileupload:commons-fileupload:1.5"
-  implementation "commons-io:commons-io:${versions.commonsIo}"
-  provided "javax.servlet:javax.servlet-api:${versions.servletapi}"
+    implementation project(':tapestry-core')
+
+    implementation libs.commons.io
+
+    api moduleLibs.commonsFileupload
+
+    provided libs.javax.servlet.api
 
-  testImplementation project(':tapestry-test')
+    testImplementation project(':tapestry-test')
 }
 
-jar {
+tasks.named('jar', Jar) {
     manifest {
         attributes 'Tapestry-Module-Classes': 
'org.apache.tapestry5.upload.modules.UploadModule'
     }
diff --git a/tapestry-version-migrator/build.gradle 
b/tapestry-version-migrator/build.gradle
index 04a897d10..4a19631fa 100644
--- a/tapestry-version-migrator/build.gradle
+++ b/tapestry-version-migrator/build.gradle
@@ -1,16 +1,18 @@
-description = "Tool to help migrate source code using Tapestry from one 
version to another. Initially built for 5.7.0"
-
-dependencies {
-    testImplementation group: 'org.testng', name: 'testng', version: '7.3.0'
+plugins {
+    id 'tapestry.testng-convention'
 }
 
-test {
-    useTestNG() {
-        useDefaultListeners = true // Tells TestNG to execute its default 
reporting structure
+description = 'Tool to help migrate source code using Tapestry from one 
version to another. Initially built for 5.7.0'
+
+tasks.withType(Test).configureEach {
+    if (options instanceof TestNGOptions) {
+        options {
+            useDefaultListeners = true
+        }
     }
 }
 
-jar {
+tasks.named('jar', Jar) {
     manifest {
         attributes 'Main-Class': 'org.apache.tapestry5.versionmigrator.Main'
     }
diff --git a/tapestry-webresources/build.gradle 
b/tapestry-webresources/build.gradle
index 73a69bdf2..943d36fdb 100644
--- a/tapestry-webresources/build.gradle
+++ b/tapestry-webresources/build.gradle
@@ -1,48 +1,52 @@
-description = "Integration with WRO4J to perform runtime CoffeeScript 
compilation, JavaScript minimization, and more."
+plugins {
+    id 'tapestry.junit5-convention'
+    id 'tapestry.testng-convention'
+}
+
+description = 'Integration with WRO4J to perform runtime CoffeeScript 
compilation, JavaScript minimization, and more.'
 
-//configurations {
-//     all {
-//             exclude group: "org.codehaus.groovy", module: "groovy-all" // 
avoid multiple Groovy compilers on classpath
-//     }
-//}
+def moduleLibs = [
+    closureCompiler: 
'com.google.javascript:closure-compiler-unshaded:v20220502',
+    less4j: 'com.github.sommeri:less4j:1.12.0',
+    autoValueAnnotations: 'com.google.auto.value:auto-value-annotations:1.9',
+    rhino: 'org.mozilla:rhino:1.7.7.2',
+]
 
 dependencies {
-    api            project(":tapestry-core")
-    testImplementation project(":tapestry-test")
-    api            "com.google.javascript:closure-compiler-unshaded:v20220502"
-    implementation "com.github.sommeri:less4j:1.12.0"
-    
-    compileOnly    "com.google.auto.value:auto-value-annotations:1.9"
-    
-    implementation "org.mozilla:rhino:1.7.7.2"
-
-    testImplementation project(":tapestry-runner")
-    testImplementation "org.gebish:geb-spock:${versions.geb}", {
-               exclude group: "org.codehaus.groovy", module: "groovy-all" // 
avoid multiple Groovy compilers on classpath
-       }
-    testImplementation "org.spockframework:spock-tapestry:${versions.spock}", {
-       exclude group: "org.apache.tapestry"
-    }
-    testImplementation 
"org.junit.jupiter:junit-jupiter:${versions.junitJupiter}"
-    testImplementation 
"org.seleniumhq.selenium:selenium-leg-rc:${versions.selenium}", {
-        exclude group: "org.seleniumhq.selenium", module: "jetty-repacked"
-        exclude group: "org.testng", module: "testng"
-        exclude group: "javax.servlet", module: "javax.servlet-api"
-        exclude group: "org.seleniumhq.selenium", module: 
"selenium-firefox-driver"
+    api project(':tapestry-core')
+
+    api moduleLibs.closureCompiler
+
+    implementation moduleLibs.less4j
+    implementation moduleLibs.rhino
+
+    compileOnly moduleLibs.autoValueAnnotations
+
+    testImplementation project(':tapestry-test')
+    testImplementation project(':tapestry-runner')
+
+    testImplementation(libs.geb.spock) {
+        exclude group: 'org.codehaus.groovy', module: 'groovy-all' // avoid 
multiple Groovy compilers on classpath
     }
-    testImplementation 
"org.seleniumhq.selenium:selenium-java:${versions.seleniumServer}"
-    testImplementation 
("io.github.bonigarcia:webdrivermanager:${versions.webdriverManager}")
-}
 
-jar.manifest {
-    attributes 'Tapestry-Module-Classes': 
'org.apache.tapestry5.webresources.modules.WebResourcesModule'
+    testImplementation project(":tapestry-spock")
+    testImplementation libs.selenium.java
+    testImplementation libs.webdrivermanager
 }
 
 
 test {
-    useJUnitPlatform()
+    systemProperties(
+        'geb.build.reportsDir': "$reporting.baseDir/geb",
+        'tapestry.compiled-asset-cache-dir': "$buildDir/compiled-asset-cache",
+        'tapestry.production-mode': 'false',
+        'tapestry.compress-whitespace': 'false',
+        'tapestry.combine-scripts': 'false'
+    )
+}
 
-    systemProperties("geb.build.reportsDir": "$reporting.baseDir/geb",
-        "tapestry.compiled-asset-cache-dir": "$buildDir/compiled-asset-cache",
-        "tapestry.production-mode": "false")
+tasks.named('jar', Jar) {
+    manifest {
+        attributes 'Tapestry-Module-Classes': 
'org.apache.tapestry5.webresources.modules.WebResourcesModule'
+    }
 }
diff --git a/tapestry5-annotations/build.gradle 
b/tapestry5-annotations/build.gradle
index 4f411a174..bc3d98c9e 100644
--- a/tapestry5-annotations/build.gradle
+++ b/tapestry5-annotations/build.gradle
@@ -1 +1 @@
-description = "Annotations used with Tapestry, Tapestry-IoC and BeanModel 
applications"
\ No newline at end of file
+description = 'Annotations used with Tapestry, Tapestry-IoC and BeanModel 
applications'


Reply via email to