Skip to content

Solution to solve compatibility issue with Gradle 9 #657

Description

@jyjeanne

Description

@infotexture it describes a possible solution implemented to make the DITA-OT documentation build fully compatible with Gradle 9 and Configuration Cache. i joined a zip archive with modified files : gradle9-compatibility-solution.zip

Problems Solved

Problem 1: Deprecated exec Method

The getGitCommitHash() function used project.exec() which is deprecated and will be removed in Gradle 9.

Problem 2: Saxon-Gradle Plugin Incompatibility

The eerohele/saxon-gradle plugin (v0.9.0-beta4) is not compatible with Gradle's Configuration Cache, causing build failures when Configuration Cache is enabled.


Solution Summary

Problem Solution
Deprecated exec Replaced with providers.exec()
Saxon-gradle incompatibility Created custom XsltTransformTask in buildSrc

Implementation Details

Fix 1: Replace Deprecated exec

Location: build.gradle (lines 171-176)

Before:

def getGitCommitHash() {
    try {
        def result = new ByteArrayOutputStream()
        exec {
            workingDir = layout.projectDirectory.asFile
            commandLine 'git', 'rev-parse', 'HEAD'
            standardOutput = result
            ignoreExitValue = true
        }
        return result.toString().trim()
    } catch (Exception e) {
        logger.warn("Could not get git commit hash: ${e.message}")
        return 'unknown'
    }
}

def gitCommitHash = getGitCommitHash()

After:

// Get git commit hash using Gradle 9 compatible Provider API
def gitCommitHash = providers.exec {
    commandLine 'git', 'rev-parse', 'HEAD'
    ignoreExitValue = true
}.standardOutput.asText.map { it.trim() }.getOrElse('unknown')

Why this works:

  • providers.exec() is the Gradle 9 compatible replacement
  • Returns a Provider<ExecOutput> for lazy evaluation
  • Configuration Cache compatible
  • Cleaner error handling with .getOrElse('unknown')

Fix 2: Replace Saxon-Gradle with Custom Task

Problem: Saxon-gradle stores non-serializable objects (XmlSlurper) in task fields, breaking Configuration Cache.

Solution: Created a new XsltTransformTask in buildSrc/ that:

  • Uses abstract properties (Gradle best practice)
  • Is annotated with @CacheableTask
  • Uses proper @InputFile, @OutputFile annotations
  • Is fully Configuration Cache compatible

New Files Created

File: buildSrc/build.gradle

plugins {
    id 'groovy'
}

repositories {
    mavenCentral()
}

dependencies {
    // Saxon-HE for XSLT transformations
    implementation 'net.sf.saxon:Saxon-HE:10.6'

    // Gradle API
    implementation gradleApi()
}

File: buildSrc/src/main/groovy/XsltTransformTask.groovy

import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.MapProperty
import org.gradle.api.tasks.*

import net.sf.saxon.s9api.Processor
import net.sf.saxon.s9api.QName
import net.sf.saxon.s9api.XdmAtomicValue
import net.sf.saxon.s9api.Serializer

import javax.xml.transform.stream.StreamSource

/**
 * Gradle 9 compatible XSLT transformation task using Saxon-HE.
 * Replaces eerohele/saxon-gradle which is not Configuration Cache compatible.
 */
@CacheableTask
abstract class XsltTransformTask extends DefaultTask {

    @InputFile
    @PathSensitive(PathSensitivity.RELATIVE)
    abstract RegularFileProperty getInputFile()

    @InputFile
    @PathSensitive(PathSensitivity.RELATIVE)
    abstract RegularFileProperty getStylesheetFile()

    @OutputFile
    abstract RegularFileProperty getOutputFile()

    @Input
    @Optional
    abstract MapProperty<String, String> getParameters()

    @OutputDirectory
    @Optional
    abstract DirectoryProperty getAdditionalOutputDir()

    @TaskAction
    void transform() {
        def inputPath = inputFile.get().asFile
        def stylesheetPath = stylesheetFile.get().asFile
        def outputPath = outputFile.get().asFile

        logger.info("XSLT Transform: ${inputPath.name} -> ${outputPath.name}")

        // Ensure output directory exists
        outputPath.parentFile?.mkdirs()

        // Create Saxon processor and execute transformation
        def processor = new Processor(false)
        def compiler = processor.newXsltCompiler()
        def executable = compiler.compile(new StreamSource(stylesheetPath))
        def transformer = executable.load30()

        // Set parameters if provided
        if (parameters.isPresent() && !parameters.get().isEmpty()) {
            parameters.get().each { name, value ->
                transformer.setStylesheetParameters(
                    Collections.singletonMap(new QName(name), new XdmAtomicValue(value))
                )
            }
        }

        // Execute transformation
        transformer.transform(
            new StreamSource(inputPath),
            processor.newSerializer(outputPath)
        )

        logger.lifecycle("Generated: ${outputPath.name}")
    }

    // Backward-compatible DSL methods
    void input(Object path) { inputFile.set(project.file(path)) }
    void output(Object path) { outputFile.set(project.file(path)) }
    void stylesheet(Object path) { stylesheetFile.set(project.file(path)) }
    void parameters(Map<String, String> params) { parameters.putAll(params) }
}

Migration in build.gradle

Before:

plugins {
    id 'io.github.jyjeanne.dita-ot-gradle' version '2.3.2'
    id 'com.github.eerohele.saxon-gradle' version '0.9.0-beta4'
}

configurations {
    saxon
}

dependencies {
    saxon 'net.sf.saxon:Saxon-HE:10.6'
}

import com.github.eerohele.SaxonXsltTask

task messages(type: SaxonXsltTask) {
    input "${configDir}/messages.xml"
    output "${projectDirPath}/topics/error-messages.xml"
    stylesheet "${projectDirPath}/resources/messages.xsl"
}

task params(type: SaxonXsltTask) {
    input "${configDir}/plugins.xml"
    output "${projectDirPath}/parameters/all-parameters.dita"
    stylesheet "${projectDirPath}/resources/params.xsl"
    parameters('output-dir.url': toURI('parameters'))
    outputs.dir "${projectDirPath}/parameters"
}

After:

plugins {
    id 'io.github.jyjeanne.dita-ot-gradle' version '2.3.2'
    // Removed: saxon-gradle - not Configuration Cache compatible
}

// XsltTransformTask is provided by buildSrc

task messages(type: XsltTransformTask) {
    input "${configDir}/messages.xml"
    output "${projectDirPath}/topics/error-messages.xml"
    stylesheet "${projectDirPath}/resources/messages.xsl"
}

task params(type: XsltTransformTask) {
    input "${configDir}/plugins.xml"
    output "${projectDirPath}/parameters/all-parameters.dita"
    stylesheet "${projectDirPath}/resources/params.xsl"
    parameters(['output-dir.url': toURI('parameters')])
    outputs.dir "${projectDirPath}/parameters"
}

Note: The only syntax change is parameters('key': 'value') becomes parameters(['key': 'value']) (explicit Map).


Files Changed

File Type Description
build.gradle Modified Removed saxon-gradle, fixed exec, use XsltTransformTask
buildSrc/build.gradle New Build configuration for custom task
buildSrc/src/main/groovy/XsltTransformTask.groovy New Gradle 9 compatible XSLT task

Verification Results

No Deprecation Warnings

$ ./gradlew help --warning-mode all
BUILD SUCCESSFUL in 11s

Configuration Cache Works

# First run - stores cache
$ ./gradlew messages --configuration-cache
Configuration cache entry stored.
BUILD SUCCESSFUL

# Second run - reuses cache
$ ./gradlew messages --configuration-cache
Reusing configuration cache.
Configuration cache entry reused.
BUILD SUCCESSFUL

All XSLT Tasks Work

$ ./gradlew messages params extensionPoints generatePropertiesTemplate

> Task :messages
Generated: error-messages.xml

> Task :params
Generated: all-parameters.dita

> Task :extensionPoints
Generated: all-extension-points.dita

> Task :generatePropertiesTemplate
Generated: template.properties

BUILD SUCCESSFUL

Full Build Works

$ ./gradlew clean html --warning-mode all

> Task :html
DITA-OT Transformation Report
Status:           SUCCESS
Output size:      7.58 MB
Duration:         72.08s

BUILD SUCCESSFUL in 1m 24s

Benefits

Aspect Before After
Deprecation warnings 1 0
Configuration Cache Not compatible Fully compatible
External plugin dependencies saxon-gradle None
Task caching Limited Full @CacheableTask
Saxon version control Plugin-defined Project-controlled
Maintenance Depends on plugin updates Self-contained

Task Comparison

Feature SaxonXsltTask (old) XsltTransformTask (new)
Configuration Cache No Yes
@CacheableTask No Yes
Abstract properties No Yes
@InputFile / @OutputFile Partial Full
Serializable No (XmlSlurper) Yes
Saxon version Plugin bundled Project controlled

Usage Guide

Basic XSLT Transformation

task myTransform(type: XsltTransformTask) {
    input 'src/input.xml'
    output 'build/output.xml'
    stylesheet 'src/transform.xsl'
}

With Parameters

task myTransform(type: XsltTransformTask) {
    input 'src/input.xml'
    output 'build/output.xml'
    stylesheet 'src/transform.xsl'
    parameters([
        'param1': 'value1',
        'param2': 'value2'
    ])
}

With Additional Output Directory

For stylesheets that generate multiple files:

task myTransform(type: XsltTransformTask) {
    input 'src/input.xml'
    output 'build/main-output.xml'
    stylesheet 'src/multi-output.xsl'
    parameters(['output-dir.url': file('build/additional').toURI().toString()])
    outputs.dir 'build/additional'
}

Rollback Instructions

If needed, to revert to saxon-gradle:

  1. Delete buildSrc/ directory
  2. Restore build.gradle from git:
    git checkout HEAD -- build.gradle
  3. Note: Configuration Cache will not work with saxon-gradle

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions