From 1ee3fbf62c1e4a39d3e746500c6ec75f55cfbef9 Mon Sep 17 00:00:00 2001 From: per Date: Sun, 2 Aug 2026 23:15:55 +0200 Subject: [PATCH] support Gradle configuration cache --- gmd-gradle-plugin/build.gradle | 9 +- gmd-gradle-plugin/release.md | 5 + .../alipsa/gmd/gradle/GmdGradlePlugin.groovy | 136 +++++------------- .../alipsa/gmd/gradle/ProcessGmdTask.groovy | 108 ++++++++++++++ .../gmd/gradle/GmdGradlePluginTest.groovy | 10 +- pom.xml | 2 +- 6 files changed, 162 insertions(+), 108 deletions(-) create mode 100644 gmd-gradle-plugin/src/main/groovy/se/alipsa/gmd/gradle/ProcessGmdTask.groovy diff --git a/gmd-gradle-plugin/build.gradle b/gmd-gradle-plugin/build.gradle index a044f45..56ebb86 100644 --- a/gmd-gradle-plugin/build.gradle +++ b/gmd-gradle-plugin/build.gradle @@ -24,6 +24,11 @@ gradlePlugin { displayName = "Gmd Plugin" description = project.description tags.set(["markdown", "Groovy", "pdf", "html", "gmd"]) + compatibility(it) { + features { + configurationCache = true + } + } } } } @@ -58,9 +63,9 @@ test { project.afterEvaluate { tasks.named('signPluginMavenPublication') { - enabled = project.properties['signing.keyId'] != null + enabled = project.findProperty('signing.keyId') != null } tasks.named("signSimplePluginPluginMarkerMavenPublication") { - enabled = project.properties['signing.keyId'] != null + enabled = project.findProperty('signing.keyId') != null } } diff --git a/gmd-gradle-plugin/release.md b/gmd-gradle-plugin/release.md index 8e45232..052c929 100644 --- a/gmd-gradle-plugin/release.md +++ b/gmd-gradle-plugin/release.md @@ -1,5 +1,10 @@ # GMD Gradle Plugin release history +## v3.1.1, in progress +- replace the deprecated `Project.getProperties()` calls used by signing configuration with `findProperty`, keeping the plugin compatible with Gradle 10 +- make `processGmd` compatible with the Gradle configuration cache and parallel execution +- declare configuration-cache support in the Plugin Portal metadata and disable build caching for potentially non-deterministic PDF output + ## v3.1.0, 2026-08-02 - resolve all output types without JavaFX dependencies - declare `processGmd` inputs and outputs and remove stale generated files diff --git a/gmd-gradle-plugin/src/main/groovy/se/alipsa/gmd/gradle/GmdGradlePlugin.groovy b/gmd-gradle-plugin/src/main/groovy/se/alipsa/gmd/gradle/GmdGradlePlugin.groovy index 3c53037..8f50b67 100644 --- a/gmd-gradle-plugin/src/main/groovy/se/alipsa/gmd/gradle/GmdGradlePlugin.groovy +++ b/gmd-gradle-plugin/src/main/groovy/se/alipsa/gmd/gradle/GmdGradlePlugin.groovy @@ -9,95 +9,47 @@ import org.gradle.api.artifacts.Dependency import org.gradle.api.artifacts.repositories.ArtifactRepository import org.gradle.api.artifacts.repositories.MavenArtifactRepository import org.gradle.api.tasks.TaskProvider -import org.gradle.process.ExecOperations - -import javax.inject.Inject @CompileStatic class GmdGradlePlugin implements Plugin { - ExecOperations execOperations - - @Inject - GmdGradlePlugin(ExecOperations execOperations) { - this.execOperations = execOperations - } - @Override void apply(Project project) { - def extension = project.extensions.create('gmdPlugin', GmdGradlePluginParams) + GmdGradlePluginParams extension = project.extensions.create('gmdPlugin', GmdGradlePluginParams) + extension.sourceDir.convention('src/main/gmd') + extension.targetDir.convention('build/gmd') + extension.outputType.convention('md') + extension.groovyVersion.convention('5.0.8') + extension.log4jVersion.convention('2.26.1') + extension.gmdVersion.convention('3.1.0') + extension.ivyVersion.convention('2.6.0') + extension.runTaskBefore.convention('test') - TaskProvider processGmdTask = project.tasks.register('processGmd') { - it.inputs.dir(project.provider { - project.file(extension.sourceDir.getOrElse('src/main/gmd')) - }) - it.outputs.dir(project.provider { - project.file(extension.targetDir.getOrElse('build/gmd')) - }) - it.inputs.property('outputType', project.provider { - extension.outputType.getOrElse('md') - }) - it.doLast { - File sourceDir= project.file(extension.sourceDir.getOrElse("src/main/gmd")) - File targetDir= project.file(extension.targetDir.getOrElse("build/gmd")) - String outputType= extension.outputType.getOrElse('md').trim().toLowerCase(Locale.ROOT) - String groovyVersion = extension.groovyVersion.getOrElse('5.0.8') - String log4jVersion = extension.log4jVersion.getOrElse('2.26.1') - String gmdVersion = extension.gmdVersion.getOrElse('3.1.0') - String ivyVersion = extension.ivyVersion.getOrElse('2.6.0') - if (!['md', 'html', 'pdf'].contains(outputType)) { - throw new IllegalArgumentException("Unknown output type ${outputType}, expected either md, html or pdf") - } + TaskProvider processGmdTask = project.tasks.register('processGmd', ProcessGmdTask) - if (!sourceDir.exists()) { - project.logger.warn("Source directory ${sourceDir.canonicalPath} does not exist, nothing to do") - return - } - if (!targetDir.exists()) { - if (!targetDir.mkdirs() && !targetDir.isDirectory()) { - throw new IllegalArgumentException("Could not create target directory ${targetDir.canonicalPath}") - } - } else if (!targetDir.isDirectory()) { - throw new IllegalArgumentException("Target path ${targetDir.canonicalPath} is a file, not a directory") - } - project.logger.info("Processing GMD in ${sourceDir} -> ${targetDir}, type: ${outputType}") - cleanStaleGeneratedFiles(project, sourceDir, targetDir, outputType) + project.afterEvaluate { + String sourceDir = extension.sourceDir.get() + String targetDir = extension.targetDir.get() + String outputType = extension.outputType.get() + Configuration configuration = addDependencies(project, + extension.groovyVersion.get(), + extension.log4jVersion.get(), + extension.gmdVersion.get(), + extension.ivyVersion.get() + ) - List addedRepositories = [] - Configuration configuration = addDependencies(project, addedRepositories, - groovyVersion, log4jVersion, gmdVersion, ivyVersion - ) - // a configuration is a FileCollection, no need to call resolve() - def result = execOperations.javaexec( a -> { - a.classpath = configuration - a.mainClass.set('se.alipsa.gmd.core.GmdProcessor') - a.args = [ - sourceDir.canonicalPath, - targetDir.canonicalPath, - outputType - ] - }) - // cleanup the added repositories - addedRepositories.each { repo -> - project.repositories.remove(repo) - } - result.assertNormalExitValue() - File[] sourceFiles = sourceDir.listFiles() - if (sourceFiles != null && sourceFiles.size() > 0) { - if (targetDir.exists()) { - project.logger.quiet("Gmd files processed and written to ${targetDir.canonicalPath}") - } else { - project.logger.warn("${targetDir.canonicalPath} should exists but does not, something is probably wrong") - } - } else { - project.logger.quiet("No gmd files found in ${sourceDir.canonicalPath}, nothing to do") - } + processGmdTask.configure { ProcessGmdTask task -> + // Resolve all project values during configuration. The task action only + // uses task properties and injected services, which enables the + // configuration cache and parallel task execution. + task.sourceDir.set(project.file(sourceDir)) + task.targetDir.set(project.file(targetDir)) + task.outputType.set(outputType) + task.classpath.from(configuration) } - } - project.afterEvaluate { + try { - def runTaskBefore = extension.runTaskBefore.getOrElse('test') - TaskProvider buildTask = it.tasks.named(runTaskBefore) + TaskProvider buildTask = project.tasks.named(extension.runTaskBefore.get()) buildTask.configure { Task task -> task.dependsOn(processGmdTask) } @@ -107,13 +59,12 @@ class GmdGradlePlugin implements Plugin { } } - static Configuration addDependencies(Project project, List addedRepositories, + static Configuration addDependencies(Project project, String groovyVersion, String log4jVersion, String gmdVersion, String ivyVersion) { - def mavenCentral = project.repositories.mavenCentral() + MavenArtifactRepository mavenCentral = project.repositories.mavenCentral() if (!hasRepository(project, mavenCentral)) { project.repositories.add(mavenCentral) - addedRepositories.add(mavenCentral) } List dependencies = [ @@ -128,29 +79,6 @@ class GmdGradlePlugin implements Plugin { return project.configurations.detachedConfiguration(dependencies.toArray(new Dependency[0])) } - private static void cleanStaleGeneratedFiles(Project project, File sourceDir, File targetDir, String outputType) { - Set expected = [] as Set - File[] sources = sourceDir.listFiles({ File file -> file.isFile() && file.name.endsWith('.gmd') } as FileFilter) - if (sources != null) { - sources.each { file -> - String base = file.name.substring(0, file.name.length() - 4) - expected.add("${base}.${outputType}".toString()) - } - } - File[] generated = targetDir.listFiles({ File file -> - file.isFile() && (file.name.endsWith('.md') || file.name.endsWith('.html') || file.name.endsWith('.pdf')) - } as FileFilter) - if (generated != null) { - generated.findAll { !expected.contains(it.name) }.each { File file -> - if (file.delete()) { - project.logger.lifecycle("Removed stale generated GMD output ${file.absolutePath}") - } else { - project.logger.warn("Could not remove stale generated GMD output ${file.absolutePath}") - } - } - } - } - static boolean hasRepository(Project project, MavenArtifactRepository repo) { return project.repositories.find { it instanceof MavenArtifactRepository && it.url == repo.url diff --git a/gmd-gradle-plugin/src/main/groovy/se/alipsa/gmd/gradle/ProcessGmdTask.groovy b/gmd-gradle-plugin/src/main/groovy/se/alipsa/gmd/gradle/ProcessGmdTask.groovy new file mode 100644 index 0000000..38e5f37 --- /dev/null +++ b/gmd-gradle-plugin/src/main/groovy/se/alipsa/gmd/gradle/ProcessGmdTask.groovy @@ -0,0 +1,108 @@ +package se.alipsa.gmd.gradle + +import groovy.transform.CompileStatic +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.process.ExecOperations +import org.gradle.process.JavaExecSpec +import org.gradle.work.DisableCachingByDefault + +import javax.inject.Inject + +@CompileStatic +@DisableCachingByDefault(because = 'GMD processing may generate non-deterministic PDF metadata') +abstract class ProcessGmdTask extends DefaultTask { + + private final ExecOperations execOperations + + @Inject + ProcessGmdTask(ExecOperations execOperations) { + this.execOperations = execOperations + } + + @InputDirectory + @Optional + @PathSensitive(PathSensitivity.RELATIVE) + abstract DirectoryProperty getSourceDir() + + @OutputDirectory + abstract DirectoryProperty getTargetDir() + + @Input + abstract org.gradle.api.provider.Property getOutputType() + + @org.gradle.api.tasks.Classpath + abstract ConfigurableFileCollection getClasspath() + + @TaskAction + void process() { + File source = getSourceDir().get().asFile + File target = getTargetDir().get().asFile + String output = getOutputType().get().trim().toLowerCase(Locale.ROOT) + if (!['md', 'html', 'pdf'].contains(output)) { + throw new IllegalArgumentException("Unknown output type ${output}, expected either md, html or pdf") + } + + if (!source.exists()) { + logger.warn("Source directory ${source.canonicalPath} does not exist, nothing to do") + return + } + if (!target.exists()) { + if (!target.mkdirs() && !target.isDirectory()) { + throw new IllegalArgumentException("Could not create target directory ${target.canonicalPath}") + } + } else if (!target.isDirectory()) { + throw new IllegalArgumentException("Target path ${target.canonicalPath} is a file, not a directory") + } + logger.info("Processing GMD in ${source} -> ${target}, type: ${output}") + cleanStaleGeneratedFiles(source, target, output) + + def result = execOperations.javaexec { JavaExecSpec spec -> + spec.classpath = getClasspath() + spec.mainClass.set('se.alipsa.gmd.core.GmdProcessor') + spec.args = [source.canonicalPath, target.canonicalPath, output] + } + result.assertNormalExitValue() + File[] sourceFiles = source.listFiles() + if (sourceFiles != null && sourceFiles.size() > 0) { + if (target.exists()) { + logger.quiet("Gmd files processed and written to ${target.canonicalPath}") + } else { + logger.warn("${target.canonicalPath} should exists but does not, something is probably wrong") + } + } else { + logger.quiet("No gmd files found in ${source.canonicalPath}, nothing to do") + } + } + + private void cleanStaleGeneratedFiles(File sourceDir, File targetDir, String outputType) { + Set expected = [] as Set + File[] sources = sourceDir.listFiles({ File file -> file.isFile() && file.name.endsWith('.gmd') } as FileFilter) + if (sources != null) { + sources.each { file -> + String base = file.name.substring(0, file.name.length() - 4) + expected.add("${base}.${outputType}".toString()) + } + } + File[] generated = targetDir.listFiles({ File file -> + file.isFile() && (file.name.endsWith('.md') || file.name.endsWith('.html') || file.name.endsWith('.pdf')) + } as FileFilter) + if (generated != null) { + generated.findAll { !expected.contains(it.name) }.each { File file -> + if (file.delete()) { + logger.lifecycle("Removed stale generated GMD output ${file.absolutePath}") + } else { + logger.warn("Could not remove stale generated GMD output ${file.absolutePath}") + } + } + } + } +} diff --git a/gmd-gradle-plugin/src/test/groovy/test/alipsa/gmd/gradle/GmdGradlePluginTest.groovy b/gmd-gradle-plugin/src/test/groovy/test/alipsa/gmd/gradle/GmdGradlePluginTest.groovy index bc66828..10df153 100644 --- a/gmd-gradle-plugin/src/test/groovy/test/alipsa/gmd/gradle/GmdGradlePluginTest.groovy +++ b/gmd-gradle-plugin/src/test/groovy/test/alipsa/gmd/gradle/GmdGradlePluginTest.groovy @@ -80,12 +80,20 @@ class GmdGradlePluginTest { def result = GradleRunner.create() .withProjectDir(testProjectDir) - .withArguments('processGmd') + .withArguments('processGmd', '--configuration-cache', '--parallel') .withPluginClasspath() .forwardOutput() .build() assert result.task(":processGmd").outcome == SUCCESS + def cachedResult = GradleRunner.create() + .withProjectDir(testProjectDir) + .withArguments('processGmd', '--configuration-cache', '--parallel') + .withPluginClasspath() + .forwardOutput() + .build() + assert cachedResult.task(":processGmd").outcome in [SUCCESS, org.gradle.testkit.runner.TaskOutcome.UP_TO_DATE] + // the directory differs on a mac even though they point to the same place so cannot include def expected = "Gmd files processed and written to $targetDir.canonicalPath".toString() Assertions.assertTrue(result.output.contains(expected), "expected \n$expected, but output was ${result.output}") diff --git a/pom.xml b/pom.xml index 2d0b57d..5c7aebe 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ - 3.1.0 + 3.1.1-SNAPSHOT UTF-8 21