Build: Add AntTask to simplify controlling logging when running ant from gradle

This new task allows setting code, similar to a doLast or doFirst,
except it is specifically geared at running ant (and thus called doAnt).
It adjusts the ant logging while running the ant so that the log
level/behavior can be tweaked, and automatically buffers based on gradle
logging level, and dumps the ant output upon failure.
This commit is contained in:
Ryan Ernst 2015-12-18 10:59:07 -08:00
parent 4ec605eab3
commit 9f1dfdbaea
5 changed files with 166 additions and 61 deletions

View File

@ -123,17 +123,6 @@ subprojects {
} }
} }
} }
// For reasons we don't fully understand yet, external dependencies are not picked up by Ant's optional tasks.
// But you can easily do it in another way.
// Only if your buildscript and Ant's optional task need the same library would you have to define it twice.
// https://docs.gradle.org/current/userguide/organizing_build_logic.html
configurations {
buildTools
}
dependencies {
buildTools 'de.thetaphi:forbiddenapis:2.0'
buildTools 'org.apache.rat:apache-rat:0.11'
}
} }
// Ensure similar tasks in dependent projects run first. The projectsEvaluated here is // Ensure similar tasks in dependent projects run first. The projectsEvaluated here is

View File

@ -63,6 +63,7 @@ dependencies {
compile 'com.perforce:p4java:2012.3.551082' // THIS IS SUPPOSED TO BE OPTIONAL IN THE FUTURE.... compile 'com.perforce:p4java:2012.3.551082' // THIS IS SUPPOSED TO BE OPTIONAL IN THE FUTURE....
compile 'de.thetaphi:forbiddenapis:2.0' compile 'de.thetaphi:forbiddenapis:2.0'
compile 'com.bmuschko:gradle-nexus-plugin:2.3.1' compile 'com.bmuschko:gradle-nexus-plugin:2.3.1'
compile 'org.apache.rat:apache-rat:0.11'
} }
processResources { processResources {

View File

@ -0,0 +1,111 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch 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.
*/
package org.elasticsearch.gradle
import org.apache.tools.ant.BuildException
import org.apache.tools.ant.BuildListener
import org.apache.tools.ant.BuildLogger
import org.apache.tools.ant.DefaultLogger
import org.apache.tools.ant.Project
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.TaskAction
import java.nio.charset.Charset
/**
* A task which will run ant commands.
*
* Logging for the task is customizable for subclasses by overriding makeLogger.
*/
public class AntTask extends DefaultTask {
/**
* A buffer that will contain the output of the ant code run,
* if the output was not already written directly to stdout.
*/
public final ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream()
@TaskAction
final void executeTask() {
// capture the current loggers
List<BuildLogger> savedLoggers = new ArrayList<>();
for (BuildListener l : project.ant.project.getBuildListeners()) {
if (l instanceof BuildLogger) {
savedLoggers.add(l);
}
}
// remove them
for (BuildLogger l : savedLoggers) {
project.ant.project.removeBuildListener(l)
}
final int outputLevel = logger.isDebugEnabled() ? Project.MSG_DEBUG : Project.MSG_INFO
final PrintStream stream = useStdout() ? System.out : new PrintStream(outputBuffer, true, Charset.defaultCharset().name())
BuildLogger antLogger = makeLogger(stream, outputLevel)
// now run the command with just our logger
project.ant.project.addBuildListener(antLogger)
try {
runAnt(project.ant)
} catch (BuildException e) {
// ant failed, so see if we have buffered output to emit, then rethrow the failure
String buffer = outputBuffer.toString()
if (buffer.isEmpty() == false) {
logger.error("=== Ant output ===\n${buffer}")
}
throw e
} finally {
project.ant.project.removeBuildListener(antLogger)
// add back the old loggers before returning
for (BuildLogger l : savedLoggers) {
project.ant.project.addBuildListener(l)
}
}
}
/** Runs the doAnt closure. This can be overridden by subclasses instead of having to set a closure. */
protected void runAnt(AntBuilder ant) {
if (doAnt == null) {
throw new GradleException("Missing doAnt for ${name}")
}
doAnt(ant)
}
/** Create the logger the ant runner will use, with the given stream for error/output. */
protected BuildLogger makeLogger(PrintStream stream, int outputLevel) {
return new DefaultLogger(
errorPrintStream: stream,
outputPrintStream: stream,
messageOutputLevel: outputLevel)
}
/**
* Returns true if the ant logger should write to stdout, or false if to the buffer.
* The default implementation writes to the buffer when gradle info logging is disabled.
*/
protected boolean useStdout() {
return logger.isInfoEnabled()
}
}

View File

@ -18,34 +18,33 @@
*/ */
package org.elasticsearch.gradle.precommit package org.elasticsearch.gradle.precommit
import java.nio.file.Files import org.apache.rat.anttasks.Report
import org.apache.rat.anttasks.SubstringLicenseMatcher
import org.gradle.api.DefaultTask import org.apache.rat.license.SimpleLicenseFamily
import org.elasticsearch.gradle.AntTask
import org.gradle.api.tasks.SourceSet import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.TaskAction
import groovy.xml.NamespaceBuilder import java.nio.file.Files
import groovy.xml.NamespaceBuilderSupport
/** /**
* Checks files for license headers. * Checks files for license headers.
* <p> * <p>
* This is a port of the apache lucene check * This is a port of the apache lucene check
*/ */
public class LicenseHeadersTask extends DefaultTask { public class LicenseHeadersTask extends AntTask {
LicenseHeadersTask() { LicenseHeadersTask() {
description = "Checks sources for missing, incorrect, or unacceptable license headers" description = "Checks sources for missing, incorrect, or unacceptable license headers"
if (ant.project.taskDefinitions.contains('ratReport') == false) {
ant.project.addTaskDefinition('ratReport', Report)
ant.project.addDataTypeDefinition('substringMatcher', SubstringLicenseMatcher)
ant.project.addDataTypeDefinition('approvedLicense', SimpleLicenseFamily)
}
} }
@TaskAction @Override
public void check() { protected void runAnt(AntBuilder ant) {
// load rat tasks
AntBuilder ant = new AntBuilder()
ant.typedef(resource: "org/apache/rat/anttasks/antlib.xml",
uri: "antlib:org.apache.rat.anttasks",
classpath: project.configurations.buildTools.asPath)
NamespaceBuilderSupport rat = NamespaceBuilder.newInstance(ant, "antlib:org.apache.rat.anttasks")
// create a file for the log to go to under reports/ // create a file for the log to go to under reports/
File reportDir = new File(project.buildDir, "reports/licenseHeaders") File reportDir = new File(project.buildDir, "reports/licenseHeaders")
@ -54,7 +53,7 @@ public class LicenseHeadersTask extends DefaultTask {
Files.deleteIfExists(reportFile.toPath()) Files.deleteIfExists(reportFile.toPath())
// run rat, going to the file // run rat, going to the file
rat.report(reportFile: reportFile.absolutePath, addDefaultLicenseMatchers: true) { ant.ratReport(reportFile: reportFile.absolutePath, addDefaultLicenseMatchers: true) {
// checks all the java sources (allJava) // checks all the java sources (allJava)
for (SourceSet set : project.sourceSets) { for (SourceSet set : project.sourceSets) {
for (File dir : set.allJava.srcDirs) { for (File dir : set.allJava.srcDirs) {

View File

@ -18,6 +18,10 @@
*/ */
package org.elasticsearch.gradle.precommit package org.elasticsearch.gradle.precommit
import org.apache.tools.ant.DefaultLogger
import org.elasticsearch.gradle.AntTask
import org.gradle.api.artifacts.Configuration
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.FileVisitResult import java.nio.file.FileVisitResult
import java.nio.file.Path import java.nio.file.Path
@ -35,7 +39,7 @@ import org.apache.tools.ant.Project
/** /**
* Basic static checking to keep tabs on third party JARs * Basic static checking to keep tabs on third party JARs
*/ */
public class ThirdPartyAuditTask extends DefaultTask { public class ThirdPartyAuditTask extends AntTask {
// true to be lenient about MISSING CLASSES // true to be lenient about MISSING CLASSES
private boolean missingClasses; private boolean missingClasses;
@ -46,6 +50,10 @@ public class ThirdPartyAuditTask extends DefaultTask {
ThirdPartyAuditTask() { ThirdPartyAuditTask() {
dependsOn(project.configurations.testCompile) dependsOn(project.configurations.testCompile)
description = "Checks third party JAR bytecode for missing classes, use of internal APIs, and other horrors'" description = "Checks third party JAR bytecode for missing classes, use of internal APIs, and other horrors'"
if (ant.project.taskDefinitions.contains('thirdPartyAudit') == false) {
ant.project.addTaskDefinition('thirdPartyAudit', de.thetaphi.forbiddenapis.ant.AntTask)
}
} }
/** /**
@ -84,38 +92,35 @@ public class ThirdPartyAuditTask extends DefaultTask {
return excludes; return excludes;
} }
@TaskAction @Override
public void check() { protected BuildLogger makeLogger(PrintStream stream, int outputLevel) {
AntBuilder ant = new AntBuilder() return new DefaultLogger(
errorPrintStream: stream,
outputPrintStream: stream,
// ignore passed in outputLevel for now, until we are filtering warning messages
messageOutputLevel: Project.MSG_ERR)
}
// we are noisy for many reasons, working around performance problems with forbidden-apis, dealing @Override
// with warnings about missing classes, etc. so we use our own "quiet" AntBuilder protected void runAnt(AntBuilder ant) {
ant.project.buildListeners.each { listener ->
if (listener instanceof BuildLogger) {
listener.messageOutputLevel = Project.MSG_ERR;
}
};
// we only want third party dependencies. // we only want third party dependencies.
FileCollection jars = project.configurations.testCompile.fileCollection({ dependency -> FileCollection jars = project.configurations.testCompile.fileCollection({ dependency ->
dependency.group.startsWith("org.elasticsearch") == false dependency.group.startsWith("org.elasticsearch") == false
}) })
// we don't want provided dependencies, which we have already scanned. e.g. don't // we don't want provided dependencies, which we have already scanned. e.g. don't
// scan ES core's dependencies for every single plugin // scan ES core's dependencies for every single plugin
try { Configuration provided = project.configurations.findByName('provided')
jars -= project.configurations.getByName("provided") if (provided != null) {
} catch (UnknownConfigurationException ignored) {} jars -= provided
}
// no dependencies matched, we are done // no dependencies matched, we are done
if (jars.isEmpty()) { if (jars.isEmpty()) {
return; return;
} }
ant.taskdef(name: "thirdPartyAudit",
classname: "de.thetaphi.forbiddenapis.ant.AntTask",
classpath: project.configurations.buildTools.asPath)
// print which jars we are going to scan, always // print which jars we are going to scan, always
// this is not the time to try to be succinct! Forbidden will print plenty on its own! // this is not the time to try to be succinct! Forbidden will print plenty on its own!
Set<String> names = new HashSet<>() Set<String> names = new HashSet<>()
@ -123,26 +128,26 @@ public class ThirdPartyAuditTask extends DefaultTask {
names.add(jar.getName()) names.add(jar.getName())
} }
logger.error("[thirdPartyAudit] Scanning: " + names) logger.error("[thirdPartyAudit] Scanning: " + names)
// warn that classes are missing // warn that classes are missing
// TODO: move these to excludes list! // TODO: move these to excludes list!
if (missingClasses) { if (missingClasses) {
logger.warn("[thirdPartyAudit] WARNING: CLASSES ARE MISSING! Expect NoClassDefFoundError in bug reports from users!") logger.warn("[thirdPartyAudit] WARNING: CLASSES ARE MISSING! Expect NoClassDefFoundError in bug reports from users!")
} }
// TODO: forbidden-apis + zipfileset gives O(n^2) behavior unless we dump to a tmpdir first, // TODO: forbidden-apis + zipfileset gives O(n^2) behavior unless we dump to a tmpdir first,
// and then remove our temp dir afterwards. don't complain: try it yourself. // and then remove our temp dir afterwards. don't complain: try it yourself.
// we don't use gradle temp dir handling, just google it, or try it yourself. // we don't use gradle temp dir handling, just google it, or try it yourself.
File tmpDir = new File(project.buildDir, 'tmp/thirdPartyAudit') File tmpDir = new File(project.buildDir, 'tmp/thirdPartyAudit')
// clean up any previous mess (if we failed), then unzip everything to one directory // clean up any previous mess (if we failed), then unzip everything to one directory
ant.delete(dir: tmpDir.getAbsolutePath()) ant.delete(dir: tmpDir.getAbsolutePath())
tmpDir.mkdirs() tmpDir.mkdirs()
for (File jar : jars) { for (File jar : jars) {
ant.unzip(src: jar.getAbsolutePath(), dest: tmpDir.getAbsolutePath()) ant.unzip(src: jar.getAbsolutePath(), dest: tmpDir.getAbsolutePath())
} }
// convert exclusion class names to binary file names // convert exclusion class names to binary file names
String[] excludedFiles = new String[excludes.length]; String[] excludedFiles = new String[excludes.length];
for (int i = 0; i < excludes.length; i++) { for (int i = 0; i < excludes.length; i++) {
@ -152,12 +157,12 @@ public class ThirdPartyAuditTask extends DefaultTask {
throw new IllegalStateException("bogus thirdPartyAudit exclusion: '" + excludes[i] + "', not found in any dependency") throw new IllegalStateException("bogus thirdPartyAudit exclusion: '" + excludes[i] + "', not found in any dependency")
} }
} }
// jarHellReprise // jarHellReprise
checkSheistyClasses(tmpDir.toPath(), new HashSet<>(Arrays.asList(excludedFiles))); checkSheistyClasses(tmpDir.toPath(), new HashSet<>(Arrays.asList(excludedFiles)));
ant.thirdPartyAudit(internalRuntimeForbidden: true, ant.thirdPartyAudit(internalRuntimeForbidden: true,
failOnUnsupportedJava: false, failOnUnsupportedJava: false,
failOnMissingClasses: !missingClasses, failOnMissingClasses: !missingClasses,
classpath: project.configurations.testCompile.asPath) { classpath: project.configurations.testCompile.asPath) {
fileset(dir: tmpDir, excludes: excludedFiles.join(',')) fileset(dir: tmpDir, excludes: excludedFiles.join(','))
@ -169,7 +174,7 @@ public class ThirdPartyAuditTask extends DefaultTask {
/** /**
* check for sheisty classes: if they also exist in the extensions classloader, its jar hell with the jdk! * check for sheisty classes: if they also exist in the extensions classloader, its jar hell with the jdk!
*/ */
private void checkSheistyClasses(Path root, Set<String> excluded) { protected void checkSheistyClasses(Path root, Set<String> excluded) {
// system.parent = extensions loader. // system.parent = extensions loader.
// note: for jigsaw, this evilness will need modifications (e.g. use jrt filesystem!). // note: for jigsaw, this evilness will need modifications (e.g. use jrt filesystem!).
// but groovy/gradle needs to work at all first! // but groovy/gradle needs to work at all first!