Skip to content

Commit 180ab24

Browse files
committed
Improve thirdPartyAudit check, round 3
1 parent c6182cb commit 180ab24

File tree

33 files changed

+3140
-901
lines changed

33 files changed

+3140
-901
lines changed

buildSrc/src/main/groovy/org/elasticsearch/gradle/AntTask.groovy

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ public abstract class AntTask extends DefaultTask {
5858
ant.project.removeBuildListener(listener)
5959
}
6060

61+
// otherwise groovy replaces System.out, and you have no chance to debug
62+
// ant.saveStreams = false
63+
6164
final int outputLevel = logger.isDebugEnabled() ? Project.MSG_DEBUG : Project.MSG_INFO
6265
final PrintStream stream = useStdout() ? System.out : new PrintStream(outputBuffer, true, Charset.defaultCharset().name())
6366
BuildLogger antLogger = makeLogger(stream, outputLevel)

buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/ThirdPartyAuditTask.groovy

Lines changed: 141 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -16,51 +16,39 @@
1616
* specific language governing permissions and limitations
1717
* under the License.
1818
*/
19-
package org.elasticsearch.gradle.precommit
19+
package org.elasticsearch.gradle.precommit;
2020

21-
import org.apache.tools.ant.BuildLogger
22-
import org.apache.tools.ant.DefaultLogger
23-
import org.apache.tools.ant.Project
24-
import org.elasticsearch.gradle.AntTask
25-
import org.gradle.api.artifacts.Configuration
26-
import org.gradle.api.file.FileCollection
21+
import org.apache.tools.ant.BuildEvent;
22+
import org.apache.tools.ant.BuildException;
23+
import org.apache.tools.ant.BuildListener;
24+
import org.apache.tools.ant.BuildLogger;
25+
import org.apache.tools.ant.DefaultLogger;
26+
import org.apache.tools.ant.Project;
27+
import org.elasticsearch.gradle.AntTask;
28+
import org.gradle.api.artifacts.Configuration;
29+
import org.gradle.api.file.FileCollection;
2730

28-
import java.nio.file.FileVisitResult
29-
import java.nio.file.Files
30-
import java.nio.file.Path
31-
import java.nio.file.SimpleFileVisitor
32-
import java.nio.file.attribute.BasicFileAttributes
31+
import java.nio.file.FileVisitResult;
32+
import java.nio.file.Files;
33+
import java.nio.file.Path;
34+
import java.nio.file.SimpleFileVisitor;
35+
import java.nio.file.attribute.BasicFileAttributes;
36+
import java.util.regex.Matcher;
37+
import java.util.regex.Pattern;
3338

3439
/**
3540
* Basic static checking to keep tabs on third party JARs
3641
*/
3742
public class ThirdPartyAuditTask extends AntTask {
38-
39-
// true to be lenient about MISSING CLASSES
40-
private boolean missingClasses;
4143

4244
// patterns for classes to exclude, because we understand their issues
4345
private String[] excludes = new String[0];
4446

4547
ThirdPartyAuditTask() {
46-
dependsOn(project.configurations.testCompile)
47-
description = "Checks third party JAR bytecode for missing classes, use of internal APIs, and other horrors'"
48-
}
49-
50-
/**
51-
* Set to true to be lenient with missing classes. By default this check will fail if it finds
52-
* MISSING CLASSES. This means the set of jars is incomplete. However, in some cases
53-
* this can be due to intentional exclusions that are well-tested and understood.
54-
*/
55-
public void setMissingClasses(boolean value) {
56-
missingClasses = value;
57-
}
58-
59-
/**
60-
* Returns true if leniency about missing classes is enabled.
61-
*/
62-
public boolean isMissingClasses() {
63-
return missingClasses;
48+
// we depend on this because its the only reliable configuration
49+
// this probably makes the build slower: gradle you suck here when it comes to configurations, you pay the price.
50+
dependsOn(project.configurations.testCompile);
51+
description = "Checks third party JAR bytecode for missing classes, use of internal APIs, and other horrors'";
6452
}
6553

6654
/**
@@ -70,7 +58,7 @@ public class ThirdPartyAuditTask extends AntTask {
7058
public void setExcludes(String[] classes) {
7159
for (String s : classes) {
7260
if (s.indexOf('*') != -1) {
73-
throw new IllegalArgumentException("illegal third party audit exclusion: '" + s + "', wildcards are not permitted!")
61+
throw new IllegalArgumentException("illegal third party audit exclusion: '" + s + "', wildcards are not permitted!");
7462
}
7563
}
7664
excludes = classes;
@@ -83,102 +71,180 @@ public class ThirdPartyAuditTask extends AntTask {
8371
return excludes;
8472
}
8573

74+
// yes, we parse Uwe Schindler's errors to find missing classes, and to keep a continuous audit. Just don't let him know!
75+
static final Pattern MISSING_CLASS_PATTERN =
76+
Pattern.compile(/WARNING: The referenced class '(.*)' cannot be loaded\. Please fix the classpath\!/);
77+
78+
static final Pattern VIOLATION_PATTERN =
79+
Pattern.compile(/\s\sin ([a-zA-Z0-9\$\.]+) \(.*\)/);
80+
81+
// we log everything and capture errors and handle them with our whitelist
82+
// this is important, as we detect stale whitelist entries, workaround forbidden apis bugs,
83+
// and it also allows whitelisting missing classes!
84+
static class EvilLogger extends DefaultLogger {
85+
final Set<String> missingClasses = new TreeSet<>();
86+
final Map<String,List<String>> violations = new TreeMap<>();
87+
String previousLine = null;
88+
89+
@Override
90+
public void messageLogged(BuildEvent event) {
91+
if (event.getTask().getClass() == de.thetaphi.forbiddenapis.ant.AntTask.class) {
92+
if (event.getPriority() == Project.MSG_WARN) {
93+
Matcher m = MISSING_CLASS_PATTERN.matcher(event.getMessage());
94+
if (m.matches()) {
95+
missingClasses.add(m.group(1).replace('.', '/') + ".class");
96+
}
97+
} else if (event.getPriority() == Project.MSG_ERR) {
98+
Matcher m = VIOLATION_PATTERN.matcher(event.getMessage());
99+
if (m.matches()) {
100+
String violation = previousLine + '\n' + event.getMessage();
101+
String clazz = m.group(1).replace('.', '/') + ".class";
102+
List<String> current = violations.get(clazz);
103+
if (current == null) {
104+
current = new ArrayList<>();
105+
violations.put(clazz, current);
106+
}
107+
current.add(violation);
108+
}
109+
previousLine = event.getMessage();
110+
}
111+
}
112+
super.messageLogged(event);
113+
}
114+
}
115+
86116
@Override
87117
protected BuildLogger makeLogger(PrintStream stream, int outputLevel) {
88-
return new DefaultLogger(
89-
errorPrintStream: stream,
90-
outputPrintStream: stream,
91-
// ignore passed in outputLevel for now, until we are filtering warning messages
92-
messageOutputLevel: Project.MSG_ERR)
118+
DefaultLogger log = new EvilLogger();
119+
log.errorPrintStream = stream;
120+
log.outputPrintStream = stream;
121+
log.messageOutputLevel = outputLevel;
122+
return log;
93123
}
94124

95125
@Override
96126
protected void runAnt(AntBuilder ant) {
97-
ant.project.addTaskDefinition('thirdPartyAudit', de.thetaphi.forbiddenapis.ant.AntTask)
127+
Configuration configuration = project.configurations.findByName('runtime');
128+
if (configuration == null) {
129+
// some projects apparently do not have 'runtime'? what a nice inconsistency,
130+
// basically only serves to waste time in build logic!
131+
configuration = project.configurations.findByName('testCompile');
132+
}
133+
assert configuration != null;
134+
ant.project.addTaskDefinition('thirdPartyAudit', de.thetaphi.forbiddenapis.ant.AntTask);
98135

99136
// we only want third party dependencies.
100-
FileCollection jars = project.configurations.testCompile.fileCollection({ dependency ->
137+
FileCollection jars = configuration.fileCollection({ dependency ->
101138
dependency.group.startsWith("org.elasticsearch") == false
102-
})
139+
});
103140

104141
// we don't want provided dependencies, which we have already scanned. e.g. don't
105142
// scan ES core's dependencies for every single plugin
106-
Configuration provided = project.configurations.findByName('provided')
143+
Configuration provided = project.configurations.findByName('provided');
107144
if (provided != null) {
108-
jars -= provided
145+
jars -= provided;
109146
}
110147

111148
// no dependencies matched, we are done
112149
if (jars.isEmpty()) {
113150
return;
114151
}
115152

116-
117153
// print which jars we are going to scan, always
118154
// this is not the time to try to be succinct! Forbidden will print plenty on its own!
119-
Set<String> names = new HashSet<>()
155+
Set<String> names = new TreeSet<>();
120156
for (File jar : jars) {
121-
names.add(jar.getName())
122-
}
123-
logger.error("[thirdPartyAudit] Scanning: " + names)
124-
125-
// warn that classes are missing
126-
// TODO: move these to excludes list!
127-
if (missingClasses) {
128-
logger.warn("[thirdPartyAudit] WARNING: CLASSES ARE MISSING! Expect NoClassDefFoundError in bug reports from users!")
157+
names.add(jar.getName());
129158
}
130159

131160
// TODO: forbidden-apis + zipfileset gives O(n^2) behavior unless we dump to a tmpdir first,
132161
// and then remove our temp dir afterwards. don't complain: try it yourself.
133162
// we don't use gradle temp dir handling, just google it, or try it yourself.
134163

135-
File tmpDir = new File(project.buildDir, 'tmp/thirdPartyAudit')
164+
File tmpDir = new File(project.buildDir, 'tmp/thirdPartyAudit');
136165

137166
// clean up any previous mess (if we failed), then unzip everything to one directory
138-
ant.delete(dir: tmpDir.getAbsolutePath())
139-
tmpDir.mkdirs()
167+
ant.delete(dir: tmpDir.getAbsolutePath());
168+
tmpDir.mkdirs();
140169
for (File jar : jars) {
141-
ant.unzip(src: jar.getAbsolutePath(), dest: tmpDir.getAbsolutePath())
170+
ant.unzip(src: jar.getAbsolutePath(), dest: tmpDir.getAbsolutePath());
142171
}
143172

144173
// convert exclusion class names to binary file names
145174
String[] excludedFiles = new String[excludes.length];
146175
for (int i = 0; i < excludes.length; i++) {
147-
excludedFiles[i] = excludes[i].replace('.', '/') + ".class"
148-
// check if the excluded file exists, if not, sure sign things are outdated
149-
if (! new File(tmpDir, excludedFiles[i]).exists()) {
150-
throw new IllegalStateException("bogus thirdPartyAudit exclusion: '" + excludes[i] + "', not found in any dependency")
151-
}
176+
excludedFiles[i] = excludes[i].replace('.', '/') + ".class";
152177
}
178+
Set<String> excludedSet = new TreeSet<>(Arrays.asList(excludedFiles));
153179

154180
// jarHellReprise
155-
checkSheistyClasses(tmpDir.toPath(), new HashSet<>(Arrays.asList(excludedFiles)));
181+
Set<String> sheistySet = getSheistyClasses(tmpDir.toPath());
156182

157-
ant.thirdPartyAudit(internalRuntimeForbidden: true,
183+
try {
184+
ant.thirdPartyAudit(internalRuntimeForbidden: false,
158185
failOnUnsupportedJava: false,
159-
failOnMissingClasses: !missingClasses,
160-
classpath: project.configurations.testCompile.asPath) {
161-
fileset(dir: tmpDir, excludes: excludedFiles.join(','))
186+
failOnMissingClasses: false,
187+
signaturesFile: new File(getClass().getResource('/forbidden/third-party-audit.txt').toURI()),
188+
classpath: configuration.asPath) {
189+
fileset(dir: tmpDir)
190+
}
191+
} catch (BuildException ignore) {}
192+
193+
EvilLogger evilLogger = null;
194+
for (BuildListener listener : ant.project.getBuildListeners()) {
195+
if (listener instanceof EvilLogger) {
196+
evilLogger = (EvilLogger) listener;
197+
break;
198+
}
199+
}
200+
assert evilLogger != null;
201+
202+
// keep our whitelist up to date
203+
Set<String> bogusExclusions = new TreeSet<>(excludedSet);
204+
bogusExclusions.removeAll(sheistySet);
205+
bogusExclusions.removeAll(evilLogger.missingClasses);
206+
bogusExclusions.removeAll(evilLogger.violations.keySet());
207+
if (!bogusExclusions.isEmpty()) {
208+
throw new IllegalStateException("Invalid exclusions, nothing is wrong with these classes: " + bogusExclusions);
209+
}
210+
211+
// don't duplicate classes with the JDK
212+
sheistySet.removeAll(excludedSet);
213+
if (!sheistySet.isEmpty()) {
214+
throw new IllegalStateException("JAR HELL WITH JDK! " + sheistySet);
215+
}
216+
217+
// don't allow a broken classpath
218+
evilLogger.missingClasses.removeAll(excludedSet);
219+
if (!evilLogger.missingClasses.isEmpty()) {
220+
throw new IllegalStateException("CLASSES ARE MISSING! " + evilLogger.missingClasses);
221+
}
222+
223+
// don't use internal classes
224+
evilLogger.violations.keySet().removeAll(excludedSet);
225+
if (!evilLogger.violations.isEmpty()) {
226+
throw new IllegalStateException("VIOLATIONS WERE FOUND! " + evilLogger.violations);
162227
}
228+
163229
// clean up our mess (if we succeed)
164-
ant.delete(dir: tmpDir.getAbsolutePath())
230+
ant.delete(dir: tmpDir.getAbsolutePath());
165231
}
166-
232+
167233
/**
168234
* check for sheisty classes: if they also exist in the extensions classloader, its jar hell with the jdk!
169235
*/
170-
private void checkSheistyClasses(Path root, Set<String> excluded) {
236+
private Set<String> getSheistyClasses(Path root) {
171237
// system.parent = extensions loader.
172238
// note: for jigsaw, this evilness will need modifications (e.g. use jrt filesystem!).
173239
// but groovy/gradle needs to work at all first!
174-
ClassLoader ext = ClassLoader.getSystemClassLoader().getParent()
175-
assert ext != null
240+
ClassLoader ext = ClassLoader.getSystemClassLoader().getParent();
241+
assert ext != null;
176242

177243
Set<String> sheistySet = new TreeSet<>();
178244
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
179245
@Override
180246
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
181-
String entry = root.relativize(file).toString()
247+
String entry = root.relativize(file).toString();
182248
if (entry.endsWith(".class")) {
183249
if (ext.getResource(entry) != null) {
184250
sheistySet.add(entry);
@@ -187,19 +253,6 @@ public class ThirdPartyAuditTask extends AntTask {
187253
return FileVisitResult.CONTINUE;
188254
}
189255
});
190-
191-
// check if we are ok
192-
if (sheistySet.isEmpty()) {
193-
return;
194-
}
195-
196-
// leniency against exclusions list
197-
sheistySet.removeAll(excluded);
198-
199-
if (sheistySet.isEmpty()) {
200-
logger.warn("[thirdPartyAudit] WARNING: JAR HELL WITH JDK! Expect insanely hard-to-debug problems!")
201-
} else {
202-
throw new IllegalStateException("JAR HELL WITH JDK! " + sheistySet);
203-
}
256+
return sheistySet;
204257
}
205258
}

0 commit comments

Comments
 (0)