lucene/gradle/generation/regenerate.gradle

278 lines
11 KiB
Groovy

import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import org.apache.commons.codec.digest.DigestUtils
import java.util.function.Function
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Create common 'regenerate' task sub-tasks can hook into.
/**
* Compute all "checksummed" key-value pairs.
*/
def computeChecksummedEntries = { Task sourceTask ->
// An flat ordered map of key-value pairs.
Map<String, String> allEntries = new TreeMap<>()
// Make sure all input properties are either simple strings
// or closures returning simple strings.
//
// Don't overcomplicate things with other serializable types.
Map<String, Object> props = sourceTask.inputs.properties
props.forEach { key, val ->
// Handle closures and other lazy providers.
if (val instanceof Provider<?>) {
val = val.get()
}
if (val instanceof Closure<?>) {
val = val.call()
}
if (!(val instanceof String)) {
throw new GradleException("Input properties of wrapped tasks must all be " +
"strings: ${key} in ${sourceTask.name} is not.")
}
allEntries.put("property:" + key, (String) val)
}
// Collect all of task inputs/ output files and compute their checksums.
FileCollection allFiles = sourceTask.inputs.files + sourceTask.outputs.files
// Compute checksums for root-project relative paths
allFiles.files.forEach { file ->
allEntries.put(
sourceTask.project.rootDir.relativePath(file),
file.exists() ? new DigestUtils(DigestUtils.sha1Digest).digestAsHex(file).trim() : "--")
}
return allEntries
}
configure(rootProject) {
ext {
/**
* Utility function to read a file, apply changes to its content and write it back.
*/
modifyFile = { File path, Function<String, String> modify ->
Function<String, String> normalizeEols = { text -> text.replace("\r\n", "\n") }
String original = path.getText("UTF-8")
String modified = normalizeEols.apply(original)
modified = modify.apply(modified)
modified = normalizeEols.apply(modified)
if (!original.equals(modified)) {
path.write(modified, "UTF-8")
}
}
}
}
configure([
project(":lucene:analysis:common"),
project(":lucene:analysis:icu"),
project(":lucene:analysis:kuromoji"),
project(":lucene:analysis:nori"),
project(":lucene:backward-codecs"),
project(":lucene:core"),
project(":lucene:queryparser"),
project(":lucene:expressions"),
project(":lucene:test-framework"),
]) {
task regenerate() {
description "Rerun any code or static data generation tasks."
group "generation"
}
project.ext {
// This utility method implements the logic required for "persistent" incremental
// source-generating tasks. The idea is simple, the implementation quite complex.
//
// The idea is that, given source-generating task "sourceTaskInternal" (note the suffix),
// we create a bunch of other tasks that perform checksum generation, validation and
// source task skipping. For example, let's say we have a task 'genFooInternal";
// the following tasks would be created
//
// genFooChecksumLoad
// genFooChecksumSave
// genFooChecksumCheck (fails if checksums are inconsistent)
//
// and the following set of dependencies would be created (with additional
// constraints to run them in this particular order!).
//
// genFoo.dependsOn [genFooChecksumLoad, genFooInternal, genFooChecksumSave]
//
// Checksums are persisted and computed from sourceTask's inputs/outputs. If the
// persisted checksums are identical to the now-current checksums, the "internal" task
// is skipped (using onlyIf { false }).
//
// Implementation-wise things get complicated because gradle doesn't have the notion
// of "ordered" task execution with respect to task AND its dependencies (we can add
// constraints to each node in the execution graph but not node-and-dependencies).
//
// sourceTask - the task to wrap
// extraConfig - a map with extra (optional) configuration options.
// andThenTasks: any other tasks that should be scheduled to run after the internal task and
// before checksum calculation/ saving. spotless is a good example of this.
// ignoreWithSource: any other tasks that should be ignored if the internal task is ignored
// (checksums are identical)
// mustRunBefore: any tasks which should be scheduled to run after the internal task.
wrapWithPersistentChecksums = { Task sourceTask, Map<String, Object> extraConfig = [:] ->
if (!sourceTask.name.endsWith("Internal")) {
throw new GradleException("Wrapped task must follow the convention name of *Internal: ${sourceTask.name}")
}
String sourceTaskName = sourceTask.name.replaceAll('Internal$', '')
def toList = { value ->
if (value instanceof List) {
return value
} else if (value == null) {
return []
} else {
return [ value ]
}
}
List<Object> andThenTasks = toList(extraConfig.get("andThenTasks"))
List<Object> ignoreWithSource = toList(extraConfig.get("ignoreWithSource"))
// Schedule must-run-afters
List<Object> mustRunBefore = toList(extraConfig.get("mustRunBefore"))
// TODO: maybe ensure all task refs here are strings?
tasks.matching { it.name in mustRunBefore }.configureEach {
mustRunAfter sourceTask
}
// Create checksum-loader task.
Task checksumLoadTask = tasks.create("${sourceTaskName}ChecksumLoad", {
ext {
checksumMatch = true
}
doFirst {
// Current persisted task input/outputs (file checksums, properties)
ext.currentChecksums = computeChecksummedEntries(sourceTask)
// Load any previously written checksums
ext.savedChecksums = new TreeMap<>()
ext.checksumsFile = project.file("src/generated/checksums/${sourceTaskName}.json")
if (checksumsFile.exists()) {
savedChecksums.putAll(new JsonSlurper().parse(checksumsFile) as Map)
}
// Compare saved and current checksums for subsequent tasks.
ext.checksumMatch = (savedChecksums.equals(currentChecksums))
}
})
Task checksumCheckTask = tasks.create("${sourceTaskName}ChecksumCheck", {
dependsOn checksumLoadTask
doFirst {
if (!checksumLoadTask.checksumMatch) {
// This can be made prettier but leave it verbose for now:
Map<String, String> current = checksumLoadTask.currentChecksums
Map<String, String> expected = checksumLoadTask.savedChecksums
def same = current.intersect(expected)
current = current - same
expected = expected - same
throw new GradleException("Checksums mismatch for derived resources; you might have" +
" modified a generated resource (regenerate task: ${sourceTaskName}):\n" +
"Current:\n ${current.entrySet().join('\n ')}\n\n" +
"Expected:\n ${expected.entrySet().join('\n ')}\n\n" +
"Input files for this task are:\n " + sourceTask.inputs.files.join('\n ') + "\n\n" +
"Files generated by this task are:\n " + sourceTask.outputs.files.join('\n ')
)
}
}
})
check.dependsOn checksumCheckTask
Task checksumSaveTask = tasks.create("${sourceTaskName}ChecksumSave", {
dependsOn checksumLoadTask
doFirst {
File checksumsFile = checksumLoadTask.ext.checksumsFile
checksumsFile.parentFile.mkdirs()
// Recompute checksums after the task has completed and write them.
def updatedChecksums = computeChecksummedEntries(sourceTask)
checksumsFile.setText(
JsonOutput.prettyPrint(JsonOutput.toJson(new TreeMap<String, String>(updatedChecksums))), "UTF-8")
logger.warn("Updated generated file checksums for task ${sourceTask.path}.")
}
})
Task conditionalTask = tasks.create("${sourceTaskName}", {
def deps = [
checksumLoadTask,
sourceTask,
*andThenTasks,
checksumSaveTask
].flatten()
dependsOn deps
mustRunInOrder deps
doFirst {
if (checksumLoadTask.checksumMatch && !sourceTask.didWork) {
logger.lifecycle("Checksums consistent with sources, skipping task: ${sourceTask.path}")
}
}
})
// Load checksums before the source task executes, otherwise it's always ignored.
project.afterEvaluate {
resolveTaskRefs([sourceTask, *ignoreWithSource]).each { t ->
t.configure {
dependsOn checksumLoadTask
}
}
}
// Copy the description and group from the source task.
project.afterEvaluate {
conditionalTask.group sourceTask.group
conditionalTask.description sourceTask.description + " (if sources changed)"
// Hide low-level tasks from help.
sourceTask.group = null
sourceTask.description sourceTask.description + " (low-level)"
}
// Set conditional execution only if checksum mismatch occurred.
if (!gradle.startParameter.isRerunTasks()) {
project.afterEvaluate {
resolveTaskRefs([sourceTask, *ignoreWithSource, checksumSaveTask]).each { t ->
t.configure {
logger.info("Making " + t.name + " run only if " + checksumLoadTask.name + " indicates changes")
onlyIf { !checksumLoadTask.checksumMatch }
}
}
}
}
return conditionalTask
}
}
}