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