16
16
* specific language governing permissions and limitations
17
17
* under the License.
18
18
*/
19
- package org.elasticsearch.gradle.precommit
19
+ package org.elasticsearch.gradle.precommit ;
20
20
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 ;
27
30
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 ;
33
38
34
39
/**
35
40
* Basic static checking to keep tabs on third party JARs
36
41
*/
37
42
public class ThirdPartyAuditTask extends AntTask {
38
-
39
- // true to be lenient about MISSING CLASSES
40
- private boolean missingClasses;
41
43
42
44
// patterns for classes to exclude, because we understand their issues
43
45
private String [] excludes = new String [0 ];
44
46
45
47
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'" ;
64
52
}
65
53
66
54
/**
@@ -70,7 +58,7 @@ public class ThirdPartyAuditTask extends AntTask {
70
58
public void setExcludes (String [] classes ) {
71
59
for (String s : classes) {
72
60
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!" );
74
62
}
75
63
}
76
64
excludes = classes;
@@ -83,102 +71,180 @@ public class ThirdPartyAuditTask extends AntTask {
83
71
return excludes;
84
72
}
85
73
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\s in ([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
+
86
116
@Override
87
117
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;
93
123
}
94
124
95
125
@Override
96
126
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 );
98
135
99
136
// we only want third party dependencies.
100
- FileCollection jars = project . configurations . testCompile . fileCollection({ dependency ->
137
+ FileCollection jars = configuration . fileCollection({ dependency ->
101
138
dependency. group. startsWith(" org.elasticsearch" ) == false
102
- })
139
+ });
103
140
104
141
// we don't want provided dependencies, which we have already scanned. e.g. don't
105
142
// scan ES core's dependencies for every single plugin
106
- Configuration provided = project. configurations. findByName(' provided' )
143
+ Configuration provided = project. configurations. findByName(' provided' );
107
144
if (provided != null ) {
108
- jars - = provided
145
+ jars - = provided;
109
146
}
110
147
111
148
// no dependencies matched, we are done
112
149
if (jars. isEmpty()) {
113
150
return ;
114
151
}
115
152
116
-
117
153
// print which jars we are going to scan, always
118
154
// 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 <> ();
120
156
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());
129
158
}
130
159
131
160
// TODO: forbidden-apis + zipfileset gives O(n^2) behavior unless we dump to a tmpdir first,
132
161
// and then remove our temp dir afterwards. don't complain: try it yourself.
133
162
// we don't use gradle temp dir handling, just google it, or try it yourself.
134
163
135
- File tmpDir = new File (project. buildDir, ' tmp/thirdPartyAudit' )
164
+ File tmpDir = new File (project. buildDir, ' tmp/thirdPartyAudit' );
136
165
137
166
// 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();
140
169
for (File jar : jars) {
141
- ant. unzip(src : jar. getAbsolutePath(), dest : tmpDir. getAbsolutePath())
170
+ ant. unzip(src : jar. getAbsolutePath(), dest : tmpDir. getAbsolutePath());
142
171
}
143
172
144
173
// convert exclusion class names to binary file names
145
174
String [] excludedFiles = new String [excludes. length];
146
175
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" ;
152
177
}
178
+ Set<String > excludedSet = new TreeSet<> (Arrays . asList(excludedFiles));
153
179
154
180
// jarHellReprise
155
- checkSheistyClasses (tmpDir. toPath(), new HashSet<> ( Arrays . asList(excludedFiles) ));
181
+ Set< String > sheistySet = getSheistyClasses (tmpDir. toPath());
156
182
157
- ant. thirdPartyAudit(internalRuntimeForbidden : true ,
183
+ try {
184
+ ant. thirdPartyAudit(internalRuntimeForbidden : false ,
158
185
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);
162
227
}
228
+
163
229
// clean up our mess (if we succeed)
164
- ant. delete(dir : tmpDir. getAbsolutePath())
230
+ ant. delete(dir : tmpDir. getAbsolutePath());
165
231
}
166
-
232
+
167
233
/**
168
234
* check for sheisty classes: if they also exist in the extensions classloader, its jar hell with the jdk!
169
235
*/
170
- private void checkSheistyClasses ( Path root , Set<String > excluded ) {
236
+ private Set<String > getSheistyClasses ( Path root ) {
171
237
// system.parent = extensions loader.
172
238
// note: for jigsaw, this evilness will need modifications (e.g. use jrt filesystem!).
173
239
// 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 ;
176
242
177
243
Set<String > sheistySet = new TreeSet<> ();
178
244
Files . walkFileTree(root, new SimpleFileVisitor<Path > () {
179
245
@Override
180
246
public FileVisitResult visitFile (Path file , BasicFileAttributes attrs ) throws IOException {
181
- String entry = root. relativize(file). toString()
247
+ String entry = root. relativize(file). toString();
182
248
if (entry. endsWith(" .class" )) {
183
249
if (ext. getResource(entry) != null ) {
184
250
sheistySet. add(entry);
@@ -187,19 +253,6 @@ public class ThirdPartyAuditTask extends AntTask {
187
253
return FileVisitResult . CONTINUE ;
188
254
}
189
255
});
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;
204
257
}
205
258
}
0 commit comments