Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/java/src/dev/selenium/tools/modules/ModuleGenerator.java
1865 views
1
// Licensed to the Software Freedom Conservancy (SFC) under one
2
// or more contributor license agreements. See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership. The SFC licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License. You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied. See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
package dev.selenium.tools.modules;
19
20
import static com.github.javaparser.ParseStart.COMPILATION_UNIT;
21
import static net.bytebuddy.jar.asm.Opcodes.ACC_MANDATED;
22
import static net.bytebuddy.jar.asm.Opcodes.ACC_MODULE;
23
import static net.bytebuddy.jar.asm.Opcodes.ACC_OPEN;
24
import static net.bytebuddy.jar.asm.Opcodes.ACC_STATIC_PHASE;
25
import static net.bytebuddy.jar.asm.Opcodes.ACC_TRANSITIVE;
26
import static net.bytebuddy.jar.asm.Opcodes.ASM9;
27
import static net.bytebuddy.jar.asm.Opcodes.V11;
28
29
import com.github.bazelbuild.rules_jvm_external.zip.StableZipEntry;
30
import com.github.javaparser.JavaParser;
31
import com.github.javaparser.ParseResult;
32
import com.github.javaparser.ParserConfiguration;
33
import com.github.javaparser.Provider;
34
import com.github.javaparser.Providers;
35
import com.github.javaparser.ast.CompilationUnit;
36
import com.github.javaparser.ast.Modifier;
37
import com.github.javaparser.ast.NodeList;
38
import com.github.javaparser.ast.expr.Name;
39
import com.github.javaparser.ast.modules.ModuleDeclaration;
40
import com.github.javaparser.ast.modules.ModuleExportsDirective;
41
import com.github.javaparser.ast.modules.ModuleOpensDirective;
42
import com.github.javaparser.ast.modules.ModuleProvidesDirective;
43
import com.github.javaparser.ast.modules.ModuleRequiresDirective;
44
import com.github.javaparser.ast.modules.ModuleUsesDirective;
45
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
46
import java.io.ByteArrayOutputStream;
47
import java.io.File;
48
import java.io.IOException;
49
import java.io.InputStream;
50
import java.io.OutputStream;
51
import java.io.PrintStream;
52
import java.io.UncheckedIOException;
53
import java.net.MalformedURLException;
54
import java.net.URL;
55
import java.net.URLClassLoader;
56
import java.nio.file.FileVisitResult;
57
import java.nio.file.Files;
58
import java.nio.file.Path;
59
import java.nio.file.Paths;
60
import java.nio.file.SimpleFileVisitor;
61
import java.nio.file.StandardCopyOption;
62
import java.nio.file.attribute.BasicFileAttributes;
63
import java.util.Arrays;
64
import java.util.Collection;
65
import java.util.HashSet;
66
import java.util.LinkedList;
67
import java.util.List;
68
import java.util.Map;
69
import java.util.Objects;
70
import java.util.ServiceLoader;
71
import java.util.Set;
72
import java.util.TreeMap;
73
import java.util.TreeSet;
74
import java.util.concurrent.atomic.AtomicReference;
75
import java.util.jar.Attributes;
76
import java.util.jar.JarEntry;
77
import java.util.jar.JarInputStream;
78
import java.util.jar.JarOutputStream;
79
import java.util.jar.Manifest;
80
import java.util.spi.ToolProvider;
81
import java.util.stream.Collectors;
82
import java.util.stream.Stream;
83
import java.util.zip.ZipEntry;
84
import java.util.zip.ZipOutputStream;
85
import net.bytebuddy.jar.asm.ClassReader;
86
import net.bytebuddy.jar.asm.ClassVisitor;
87
import net.bytebuddy.jar.asm.ClassWriter;
88
import net.bytebuddy.jar.asm.MethodVisitor;
89
import net.bytebuddy.jar.asm.ModuleVisitor;
90
import net.bytebuddy.jar.asm.Type;
91
import org.openqa.selenium.io.TemporaryFilesystem;
92
93
public class ModuleGenerator {
94
95
private static final String SERVICE_LOADER = ServiceLoader.class.getName().replace('.', '/');
96
97
public static void main(String[] args) throws IOException {
98
Path outJar = null;
99
Path inJar = null;
100
String moduleName = null;
101
Set<Path> modulePath = new TreeSet<>();
102
Set<String> exports = new TreeSet<>();
103
Set<String> hides = new TreeSet<>();
104
Set<String> uses = new TreeSet<>();
105
106
// There is no way at all these two having similar names will cause problems
107
Map<String, Set<String>> opensTo = new TreeMap<>();
108
Set<String> openTo = new TreeSet<>();
109
boolean isOpen = false;
110
111
int argCount = args.length;
112
for (int i = 0; i < argCount; i++) {
113
String flag = args[i];
114
String next = args[++i];
115
switch (flag) {
116
case "--exports":
117
exports.add(next);
118
break;
119
120
case "--hides":
121
hides.add(next);
122
break;
123
124
case "--in":
125
inJar = Paths.get(next);
126
break;
127
128
case "--is-open":
129
isOpen = Boolean.parseBoolean(next);
130
break;
131
132
case "--module-name":
133
moduleName = next;
134
break;
135
136
case "--module-path":
137
modulePath.add(Paths.get(next));
138
break;
139
140
case "--open-to":
141
openTo.add(next);
142
break;
143
144
case "--opens-to":
145
opensTo.computeIfAbsent(next, str -> new TreeSet<>()).add(args[++i]);
146
break;
147
148
case "--output":
149
outJar = Paths.get(next);
150
break;
151
152
case "--uses":
153
uses.add(next);
154
break;
155
156
default:
157
throw new IllegalArgumentException(String.format("Unknown argument: %s", flag));
158
}
159
}
160
Objects.requireNonNull(moduleName, "Module name must be set.");
161
Objects.requireNonNull(outJar, "Output jar must be set.");
162
Objects.requireNonNull(inJar, "Input jar must be set.");
163
164
ToolProvider jdeps = ToolProvider.findFirst("jdeps").orElseThrow();
165
File tempDir = TemporaryFilesystem.getDefaultTmpFS().createTempDir("module-dir", "");
166
Path temp = tempDir.toPath();
167
168
// It doesn't matter what we use for writing to the stream: jdeps doesn't use it. *facepalm*
169
List<String> jdepsArgs = new LinkedList<>(List.of("--api-only", "--multi-release", "9"));
170
if (!modulePath.isEmpty()) {
171
Path tmp = Files.createTempDirectory("automatic_module_jars");
172
jdepsArgs.addAll(
173
List.of(
174
"--module-path",
175
modulePath.stream()
176
.map(
177
(s) -> {
178
String file = s.getFileName().toString();
179
180
if (file.startsWith("processed_")) {
181
Path copy = tmp.resolve(file.substring(10));
182
183
try {
184
Files.copy(s, copy, StandardCopyOption.REPLACE_EXISTING);
185
} catch (IOException e) {
186
throw new UncheckedIOException(e);
187
}
188
189
return copy.toString();
190
}
191
192
return s.toString();
193
})
194
.collect(Collectors.joining(File.pathSeparator))));
195
}
196
jdepsArgs.addAll(List.of("--generate-module-info", temp.toAbsolutePath().toString()));
197
jdepsArgs.add(inJar.toAbsolutePath().toString());
198
199
PrintStream origOut = System.out;
200
PrintStream origErr = System.err;
201
202
ByteArrayOutputStream bos = new ByteArrayOutputStream();
203
PrintStream printStream = new PrintStream(bos);
204
205
int result;
206
try {
207
System.setOut(printStream);
208
System.setErr(printStream);
209
result = jdeps.run(printStream, printStream, jdepsArgs.toArray(new String[0]));
210
} finally {
211
System.setOut(origOut);
212
System.setErr(origErr);
213
}
214
if (result != 0) {
215
throw new RuntimeException(
216
"Unable to process module:\n"
217
+ "jdeps "
218
+ String.join(" ", jdepsArgs)
219
+ "\n"
220
+ new String(bos.toByteArray()));
221
}
222
223
AtomicReference<Path> moduleInfo = new AtomicReference<>();
224
// Fortunately, we know the directory where the output is written
225
Files.walkFileTree(
226
temp,
227
new SimpleFileVisitor<>() {
228
@Override
229
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
230
if ("module-info.java".equals(file.getFileName().toString())) {
231
moduleInfo.set(file);
232
}
233
return FileVisitResult.TERMINATE;
234
}
235
});
236
237
if (moduleInfo.get() == null) {
238
throw new RuntimeException("Unable to read module info");
239
}
240
241
ParserConfiguration parserConfig =
242
new ParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_11);
243
244
Provider provider = Providers.provider(moduleInfo.get());
245
246
ParseResult<CompilationUnit> parseResult =
247
new JavaParser(parserConfig).parse(COMPILATION_UNIT, provider);
248
249
CompilationUnit unit =
250
parseResult
251
.getResult()
252
.orElseThrow(() -> new RuntimeException("Unable to parse " + moduleInfo.get()));
253
254
ModuleDeclaration moduleDeclaration =
255
unit.getModule()
256
.orElseThrow(
257
() -> new RuntimeException("No module declaration in " + moduleInfo.get()));
258
259
moduleDeclaration.setName(moduleName);
260
moduleDeclaration.setOpen(isOpen);
261
262
Set<String> allUses = new TreeSet<>(uses);
263
allUses.addAll(readServicesFromClasses(inJar));
264
allUses.forEach(
265
service -> moduleDeclaration.addDirective(new ModuleUsesDirective(new Name(service))));
266
267
// Prepare a classloader to help us find classes.
268
ClassLoader classLoader;
269
if (modulePath != null) {
270
URL[] urls =
271
Stream.concat(Stream.of(inJar.toAbsolutePath()), modulePath.stream())
272
.map(
273
path -> {
274
try {
275
return path.toUri().toURL();
276
} catch (MalformedURLException e) {
277
throw new UncheckedIOException(e);
278
}
279
})
280
.toArray(URL[]::new);
281
282
classLoader = new URLClassLoader(urls);
283
} else {
284
classLoader = new URLClassLoader(new URL[0]);
285
}
286
287
Set<String> packages = inferPackages(inJar);
288
289
// Determine packages to export
290
Set<String> exportedPackages = new HashSet<>();
291
if (!isOpen) {
292
if (!exports.isEmpty()) {
293
exports.forEach(
294
export -> {
295
if (!packages.contains(export)) {
296
throw new RuntimeException(
297
String.format("Exported package '%s' not found in jar. %s", export, packages));
298
}
299
exportedPackages.add(export);
300
moduleDeclaration.addDirective(
301
new ModuleExportsDirective(new Name(export), new NodeList<>()));
302
});
303
} else {
304
packages.forEach(
305
export -> {
306
if (!hides.contains(export)) {
307
exportedPackages.add(export);
308
moduleDeclaration.addDirective(
309
new ModuleExportsDirective(new Name(export), new NodeList<>()));
310
}
311
});
312
}
313
}
314
315
openTo.forEach(
316
module ->
317
moduleDeclaration.addDirective(
318
new ModuleOpensDirective(
319
new Name(module),
320
new NodeList(
321
exportedPackages.stream().map(Name::new).collect(Collectors.toSet())))));
322
323
ClassWriter classWriter = new ClassWriter(0);
324
classWriter.visit(V11, ACC_MODULE, "module-info", null, null, null);
325
ModuleVisitor moduleVisitor = classWriter.visitModule(moduleName, isOpen ? ACC_OPEN : 0, null);
326
moduleVisitor.visitRequire("java.base", ACC_MANDATED, null);
327
328
moduleDeclaration.accept(
329
new MyModuleVisitor(classLoader, exportedPackages, hides, moduleVisitor), null);
330
331
moduleVisitor.visitEnd();
332
333
classWriter.visitEnd();
334
335
Manifest manifest = new Manifest();
336
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
337
338
try (OutputStream os = Files.newOutputStream(outJar);
339
JarOutputStream jos = new JarOutputStream(os, manifest)) {
340
jos.setLevel(ZipOutputStream.STORED);
341
342
byte[] bytes = classWriter.toByteArray();
343
344
ZipEntry entry = new StableZipEntry("module-info.class");
345
entry.setSize(bytes.length);
346
347
jos.putNextEntry(entry);
348
jos.write(bytes);
349
jos.closeEntry();
350
}
351
352
TemporaryFilesystem.getDefaultTmpFS().deleteTempDir(tempDir);
353
}
354
355
private static Collection<String> readServicesFromClasses(Path inJar) {
356
Set<String> serviceNames = new HashSet<>();
357
358
try (InputStream is = Files.newInputStream(inJar);
359
JarInputStream jis = new JarInputStream(is)) {
360
for (JarEntry entry = jis.getNextJarEntry(); entry != null; entry = jis.getNextJarEntry()) {
361
if (entry.isDirectory() || !entry.getName().endsWith(".class")) {
362
continue;
363
}
364
365
ClassReader reader = new ClassReader(jis);
366
reader.accept(
367
new ClassVisitor(ASM9) {
368
private Type serviceClass;
369
370
@Override
371
public MethodVisitor visitMethod(
372
int access,
373
String name,
374
String descriptor,
375
String signature,
376
String[] exceptions) {
377
return new MethodVisitor(ASM9) {
378
@Override
379
public void visitMethodInsn(
380
int opcode,
381
String owner,
382
String name,
383
String descriptor,
384
boolean isInterface) {
385
if (SERVICE_LOADER.equals(owner) && "load".equals(name)) {
386
if (serviceClass != null) {
387
serviceNames.add(serviceClass.getClassName());
388
serviceClass = null;
389
}
390
}
391
}
392
393
@Override
394
public void visitLdcInsn(Object value) {
395
if (value instanceof Type) {
396
serviceClass = (Type) value;
397
}
398
}
399
};
400
}
401
},
402
0);
403
}
404
} catch (IOException e) {
405
throw new UncheckedIOException(e);
406
}
407
408
return serviceNames;
409
}
410
411
private static Set<String> inferPackages(Path inJar) {
412
Set<String> packageNames = new TreeSet<>();
413
414
try (InputStream is = Files.newInputStream(inJar);
415
JarInputStream jis = new JarInputStream(is)) {
416
for (JarEntry entry = jis.getNextJarEntry(); entry != null; entry = jis.getNextJarEntry()) {
417
418
if (entry.isDirectory()) {
419
continue;
420
}
421
422
if (!entry.getName().endsWith(".class")) {
423
continue;
424
}
425
426
String name = entry.getName();
427
428
int index = name.lastIndexOf('/');
429
if (index == -1) {
430
continue;
431
}
432
name = name.substring(0, index);
433
434
// If we've a multi-release jar, remove that too
435
if (name.startsWith("META-INF/versions/")) {
436
String[] segments = name.split("/");
437
if (segments.length < 3) {
438
continue;
439
}
440
441
name =
442
Arrays.stream(Arrays.copyOfRange(segments, 3, segments.length))
443
.collect(Collectors.joining("/"));
444
}
445
446
name = name.replace("/", ".");
447
448
packageNames.add(name);
449
}
450
451
return packageNames;
452
} catch (IOException e) {
453
throw new UncheckedIOException(e);
454
}
455
}
456
457
private static class MyModuleVisitor extends VoidVisitorAdapter<Void> {
458
459
private final ClassLoader classLoader;
460
private final Set<String> seenExports;
461
private final Set<String> packages;
462
private final ModuleVisitor byteBuddyVisitor;
463
464
MyModuleVisitor(
465
ClassLoader classLoader,
466
Set<String> packages,
467
Set<String> excluded,
468
ModuleVisitor byteBuddyVisitor) {
469
this.classLoader = classLoader;
470
this.byteBuddyVisitor = byteBuddyVisitor;
471
472
// Set is modifiable
473
this.packages = new HashSet<>(packages);
474
this.seenExports = new HashSet<>(excluded);
475
}
476
477
@Override
478
public void visit(ModuleRequiresDirective n, Void arg) {
479
String name = n.getNameAsString();
480
if (name.startsWith("processed.")) {
481
// When 'Automatic-Module-Name' is not set, we must derive the module name from the jar file
482
// name. Therefore, the 'processed.' prefix added by bazel must be removed to get the name.
483
name = name.substring(10);
484
}
485
int modifiers = getByteBuddyModifier(n.getModifiers());
486
if (!name.startsWith("org.seleniumhq.selenium.") && !name.startsWith("java.")) {
487
// Some people like to exclude jars from the classpath. To allow this we need to make these
488
// modules static,
489
// otherwise a 'module not found' error while compiling their code would be the consequence.
490
modifiers |= ACC_STATIC_PHASE;
491
}
492
byteBuddyVisitor.visitRequire(name, modifiers, null);
493
}
494
495
@Override
496
public void visit(ModuleExportsDirective n, Void arg) {
497
if (seenExports.contains(n.getNameAsString())) {
498
return;
499
}
500
501
seenExports.add(n.getNameAsString());
502
503
byteBuddyVisitor.visitExport(
504
n.getNameAsString().replace('.', '/'),
505
0,
506
n.getModuleNames().stream().map(Name::asString).toArray(String[]::new));
507
}
508
509
@Override
510
public void visit(ModuleProvidesDirective n, Void arg) {
511
byteBuddyVisitor.visitProvide(
512
getClassName(n.getNameAsString()),
513
n.getWith().stream().map(type -> getClassName(type.asString())).toArray(String[]::new));
514
}
515
516
@Override
517
public void visit(ModuleUsesDirective n, Void arg) {
518
byteBuddyVisitor.visitUse(n.getNameAsString().replace('.', '/'));
519
}
520
521
@Override
522
public void visit(ModuleOpensDirective n, Void arg) {
523
packages.forEach(
524
pkg -> byteBuddyVisitor.visitOpen(pkg.replace('.', '/'), 0, n.getNameAsString()));
525
}
526
527
private int getByteBuddyModifier(NodeList<Modifier> modifiers) {
528
return modifiers.stream()
529
.mapToInt(
530
mod -> {
531
switch (mod.getKeyword()) {
532
case STATIC:
533
return ACC_STATIC_PHASE;
534
case TRANSITIVE:
535
return ACC_TRANSITIVE;
536
}
537
throw new RuntimeException("Unknown modifier: " + mod);
538
})
539
.reduce(0, (l, r) -> l | r);
540
}
541
542
private String getClassName(String possibleClassName) {
543
String name = possibleClassName.replace('/', '.');
544
if (lookup(name)) {
545
return name.replace('.', '/');
546
}
547
548
int index = name.lastIndexOf('.');
549
if (index != -1) {
550
name = name.substring(0, index) + "$" + name.substring(index + 1);
551
if (lookup(name)) {
552
return name.replace('.', '/');
553
}
554
}
555
556
throw new RuntimeException("Cannot find class: " + name);
557
}
558
559
private boolean lookup(String className) {
560
try {
561
Class.forName(className, false, classLoader);
562
return true;
563
} catch (ClassNotFoundException e) {
564
return false;
565
}
566
}
567
}
568
}
569
570