| import groovy.json.JsonOutput |
| import groovy.json.JsonSlurper |
| import org.apache.commons.codec.digest.DigestUtils |
| |
| /* |
| * 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([ |
| 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"), |
| ]) { |
| 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; example (sourceTask has Internal suffix stripped) |
| // |
| // ${sourceTask}ChecksumLoad |
| // ${sourceTask}ChecksumSave |
| // ${sourceTask}ChecksumCheck (fails if checksums are inconsistent) |
| // ${sourceTask} dependsOn [checksum-load, ${sourceTask}Internal, checksum-save] |
| // |
| // Checksums are persisted and computed from sourceTask's inputs/outputs. If the |
| // persisted checksums are identical to current checksums, source task |
| // is skipped (via sourceTaskInternal.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: other tasks that should be scheduled to run after source task and |
| // before checksum calculation. |
| 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")) |
| |
| // 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: ${sourceTask.name}):\n" + |
| "Current:\n ${current.entrySet().join('\n ')}\n\n" + |
| "Expected:\n ${expected.entrySet().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. |
| sourceTask.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 |
| } |
| } |
| } |