Path: blob/trunk/java/src/dev/selenium/tools/modules/ModuleGenerator.java
1865 views
// Licensed to the Software Freedom Conservancy (SFC) under one1// or more contributor license agreements. See the NOTICE file2// distributed with this work for additional information3// regarding copyright ownership. The SFC licenses this file4// to you under the Apache License, Version 2.0 (the5// "License"); you may not use this file except in compliance6// with the License. You may obtain a copy of the License at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing,11// software distributed under the License is distributed on an12// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13// KIND, either express or implied. See the License for the14// specific language governing permissions and limitations15// under the License.1617package dev.selenium.tools.modules;1819import static com.github.javaparser.ParseStart.COMPILATION_UNIT;20import static net.bytebuddy.jar.asm.Opcodes.ACC_MANDATED;21import static net.bytebuddy.jar.asm.Opcodes.ACC_MODULE;22import static net.bytebuddy.jar.asm.Opcodes.ACC_OPEN;23import static net.bytebuddy.jar.asm.Opcodes.ACC_STATIC_PHASE;24import static net.bytebuddy.jar.asm.Opcodes.ACC_TRANSITIVE;25import static net.bytebuddy.jar.asm.Opcodes.ASM9;26import static net.bytebuddy.jar.asm.Opcodes.V11;2728import com.github.bazelbuild.rules_jvm_external.zip.StableZipEntry;29import com.github.javaparser.JavaParser;30import com.github.javaparser.ParseResult;31import com.github.javaparser.ParserConfiguration;32import com.github.javaparser.Provider;33import com.github.javaparser.Providers;34import com.github.javaparser.ast.CompilationUnit;35import com.github.javaparser.ast.Modifier;36import com.github.javaparser.ast.NodeList;37import com.github.javaparser.ast.expr.Name;38import com.github.javaparser.ast.modules.ModuleDeclaration;39import com.github.javaparser.ast.modules.ModuleExportsDirective;40import com.github.javaparser.ast.modules.ModuleOpensDirective;41import com.github.javaparser.ast.modules.ModuleProvidesDirective;42import com.github.javaparser.ast.modules.ModuleRequiresDirective;43import com.github.javaparser.ast.modules.ModuleUsesDirective;44import com.github.javaparser.ast.visitor.VoidVisitorAdapter;45import java.io.ByteArrayOutputStream;46import java.io.File;47import java.io.IOException;48import java.io.InputStream;49import java.io.OutputStream;50import java.io.PrintStream;51import java.io.UncheckedIOException;52import java.net.MalformedURLException;53import java.net.URL;54import java.net.URLClassLoader;55import java.nio.file.FileVisitResult;56import java.nio.file.Files;57import java.nio.file.Path;58import java.nio.file.Paths;59import java.nio.file.SimpleFileVisitor;60import java.nio.file.StandardCopyOption;61import java.nio.file.attribute.BasicFileAttributes;62import java.util.Arrays;63import java.util.Collection;64import java.util.HashSet;65import java.util.LinkedList;66import java.util.List;67import java.util.Map;68import java.util.Objects;69import java.util.ServiceLoader;70import java.util.Set;71import java.util.TreeMap;72import java.util.TreeSet;73import java.util.concurrent.atomic.AtomicReference;74import java.util.jar.Attributes;75import java.util.jar.JarEntry;76import java.util.jar.JarInputStream;77import java.util.jar.JarOutputStream;78import java.util.jar.Manifest;79import java.util.spi.ToolProvider;80import java.util.stream.Collectors;81import java.util.stream.Stream;82import java.util.zip.ZipEntry;83import java.util.zip.ZipOutputStream;84import net.bytebuddy.jar.asm.ClassReader;85import net.bytebuddy.jar.asm.ClassVisitor;86import net.bytebuddy.jar.asm.ClassWriter;87import net.bytebuddy.jar.asm.MethodVisitor;88import net.bytebuddy.jar.asm.ModuleVisitor;89import net.bytebuddy.jar.asm.Type;90import org.openqa.selenium.io.TemporaryFilesystem;9192public class ModuleGenerator {9394private static final String SERVICE_LOADER = ServiceLoader.class.getName().replace('.', '/');9596public static void main(String[] args) throws IOException {97Path outJar = null;98Path inJar = null;99String moduleName = null;100Set<Path> modulePath = new TreeSet<>();101Set<String> exports = new TreeSet<>();102Set<String> hides = new TreeSet<>();103Set<String> uses = new TreeSet<>();104105// There is no way at all these two having similar names will cause problems106Map<String, Set<String>> opensTo = new TreeMap<>();107Set<String> openTo = new TreeSet<>();108boolean isOpen = false;109110int argCount = args.length;111for (int i = 0; i < argCount; i++) {112String flag = args[i];113String next = args[++i];114switch (flag) {115case "--exports":116exports.add(next);117break;118119case "--hides":120hides.add(next);121break;122123case "--in":124inJar = Paths.get(next);125break;126127case "--is-open":128isOpen = Boolean.parseBoolean(next);129break;130131case "--module-name":132moduleName = next;133break;134135case "--module-path":136modulePath.add(Paths.get(next));137break;138139case "--open-to":140openTo.add(next);141break;142143case "--opens-to":144opensTo.computeIfAbsent(next, str -> new TreeSet<>()).add(args[++i]);145break;146147case "--output":148outJar = Paths.get(next);149break;150151case "--uses":152uses.add(next);153break;154155default:156throw new IllegalArgumentException(String.format("Unknown argument: %s", flag));157}158}159Objects.requireNonNull(moduleName, "Module name must be set.");160Objects.requireNonNull(outJar, "Output jar must be set.");161Objects.requireNonNull(inJar, "Input jar must be set.");162163ToolProvider jdeps = ToolProvider.findFirst("jdeps").orElseThrow();164File tempDir = TemporaryFilesystem.getDefaultTmpFS().createTempDir("module-dir", "");165Path temp = tempDir.toPath();166167// It doesn't matter what we use for writing to the stream: jdeps doesn't use it. *facepalm*168List<String> jdepsArgs = new LinkedList<>(List.of("--api-only", "--multi-release", "9"));169if (!modulePath.isEmpty()) {170Path tmp = Files.createTempDirectory("automatic_module_jars");171jdepsArgs.addAll(172List.of(173"--module-path",174modulePath.stream()175.map(176(s) -> {177String file = s.getFileName().toString();178179if (file.startsWith("processed_")) {180Path copy = tmp.resolve(file.substring(10));181182try {183Files.copy(s, copy, StandardCopyOption.REPLACE_EXISTING);184} catch (IOException e) {185throw new UncheckedIOException(e);186}187188return copy.toString();189}190191return s.toString();192})193.collect(Collectors.joining(File.pathSeparator))));194}195jdepsArgs.addAll(List.of("--generate-module-info", temp.toAbsolutePath().toString()));196jdepsArgs.add(inJar.toAbsolutePath().toString());197198PrintStream origOut = System.out;199PrintStream origErr = System.err;200201ByteArrayOutputStream bos = new ByteArrayOutputStream();202PrintStream printStream = new PrintStream(bos);203204int result;205try {206System.setOut(printStream);207System.setErr(printStream);208result = jdeps.run(printStream, printStream, jdepsArgs.toArray(new String[0]));209} finally {210System.setOut(origOut);211System.setErr(origErr);212}213if (result != 0) {214throw new RuntimeException(215"Unable to process module:\n"216+ "jdeps "217+ String.join(" ", jdepsArgs)218+ "\n"219+ new String(bos.toByteArray()));220}221222AtomicReference<Path> moduleInfo = new AtomicReference<>();223// Fortunately, we know the directory where the output is written224Files.walkFileTree(225temp,226new SimpleFileVisitor<>() {227@Override228public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {229if ("module-info.java".equals(file.getFileName().toString())) {230moduleInfo.set(file);231}232return FileVisitResult.TERMINATE;233}234});235236if (moduleInfo.get() == null) {237throw new RuntimeException("Unable to read module info");238}239240ParserConfiguration parserConfig =241new ParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_11);242243Provider provider = Providers.provider(moduleInfo.get());244245ParseResult<CompilationUnit> parseResult =246new JavaParser(parserConfig).parse(COMPILATION_UNIT, provider);247248CompilationUnit unit =249parseResult250.getResult()251.orElseThrow(() -> new RuntimeException("Unable to parse " + moduleInfo.get()));252253ModuleDeclaration moduleDeclaration =254unit.getModule()255.orElseThrow(256() -> new RuntimeException("No module declaration in " + moduleInfo.get()));257258moduleDeclaration.setName(moduleName);259moduleDeclaration.setOpen(isOpen);260261Set<String> allUses = new TreeSet<>(uses);262allUses.addAll(readServicesFromClasses(inJar));263allUses.forEach(264service -> moduleDeclaration.addDirective(new ModuleUsesDirective(new Name(service))));265266// Prepare a classloader to help us find classes.267ClassLoader classLoader;268if (modulePath != null) {269URL[] urls =270Stream.concat(Stream.of(inJar.toAbsolutePath()), modulePath.stream())271.map(272path -> {273try {274return path.toUri().toURL();275} catch (MalformedURLException e) {276throw new UncheckedIOException(e);277}278})279.toArray(URL[]::new);280281classLoader = new URLClassLoader(urls);282} else {283classLoader = new URLClassLoader(new URL[0]);284}285286Set<String> packages = inferPackages(inJar);287288// Determine packages to export289Set<String> exportedPackages = new HashSet<>();290if (!isOpen) {291if (!exports.isEmpty()) {292exports.forEach(293export -> {294if (!packages.contains(export)) {295throw new RuntimeException(296String.format("Exported package '%s' not found in jar. %s", export, packages));297}298exportedPackages.add(export);299moduleDeclaration.addDirective(300new ModuleExportsDirective(new Name(export), new NodeList<>()));301});302} else {303packages.forEach(304export -> {305if (!hides.contains(export)) {306exportedPackages.add(export);307moduleDeclaration.addDirective(308new ModuleExportsDirective(new Name(export), new NodeList<>()));309}310});311}312}313314openTo.forEach(315module ->316moduleDeclaration.addDirective(317new ModuleOpensDirective(318new Name(module),319new NodeList(320exportedPackages.stream().map(Name::new).collect(Collectors.toSet())))));321322ClassWriter classWriter = new ClassWriter(0);323classWriter.visit(V11, ACC_MODULE, "module-info", null, null, null);324ModuleVisitor moduleVisitor = classWriter.visitModule(moduleName, isOpen ? ACC_OPEN : 0, null);325moduleVisitor.visitRequire("java.base", ACC_MANDATED, null);326327moduleDeclaration.accept(328new MyModuleVisitor(classLoader, exportedPackages, hides, moduleVisitor), null);329330moduleVisitor.visitEnd();331332classWriter.visitEnd();333334Manifest manifest = new Manifest();335manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");336337try (OutputStream os = Files.newOutputStream(outJar);338JarOutputStream jos = new JarOutputStream(os, manifest)) {339jos.setLevel(ZipOutputStream.STORED);340341byte[] bytes = classWriter.toByteArray();342343ZipEntry entry = new StableZipEntry("module-info.class");344entry.setSize(bytes.length);345346jos.putNextEntry(entry);347jos.write(bytes);348jos.closeEntry();349}350351TemporaryFilesystem.getDefaultTmpFS().deleteTempDir(tempDir);352}353354private static Collection<String> readServicesFromClasses(Path inJar) {355Set<String> serviceNames = new HashSet<>();356357try (InputStream is = Files.newInputStream(inJar);358JarInputStream jis = new JarInputStream(is)) {359for (JarEntry entry = jis.getNextJarEntry(); entry != null; entry = jis.getNextJarEntry()) {360if (entry.isDirectory() || !entry.getName().endsWith(".class")) {361continue;362}363364ClassReader reader = new ClassReader(jis);365reader.accept(366new ClassVisitor(ASM9) {367private Type serviceClass;368369@Override370public MethodVisitor visitMethod(371int access,372String name,373String descriptor,374String signature,375String[] exceptions) {376return new MethodVisitor(ASM9) {377@Override378public void visitMethodInsn(379int opcode,380String owner,381String name,382String descriptor,383boolean isInterface) {384if (SERVICE_LOADER.equals(owner) && "load".equals(name)) {385if (serviceClass != null) {386serviceNames.add(serviceClass.getClassName());387serviceClass = null;388}389}390}391392@Override393public void visitLdcInsn(Object value) {394if (value instanceof Type) {395serviceClass = (Type) value;396}397}398};399}400},4010);402}403} catch (IOException e) {404throw new UncheckedIOException(e);405}406407return serviceNames;408}409410private static Set<String> inferPackages(Path inJar) {411Set<String> packageNames = new TreeSet<>();412413try (InputStream is = Files.newInputStream(inJar);414JarInputStream jis = new JarInputStream(is)) {415for (JarEntry entry = jis.getNextJarEntry(); entry != null; entry = jis.getNextJarEntry()) {416417if (entry.isDirectory()) {418continue;419}420421if (!entry.getName().endsWith(".class")) {422continue;423}424425String name = entry.getName();426427int index = name.lastIndexOf('/');428if (index == -1) {429continue;430}431name = name.substring(0, index);432433// If we've a multi-release jar, remove that too434if (name.startsWith("META-INF/versions/")) {435String[] segments = name.split("/");436if (segments.length < 3) {437continue;438}439440name =441Arrays.stream(Arrays.copyOfRange(segments, 3, segments.length))442.collect(Collectors.joining("/"));443}444445name = name.replace("/", ".");446447packageNames.add(name);448}449450return packageNames;451} catch (IOException e) {452throw new UncheckedIOException(e);453}454}455456private static class MyModuleVisitor extends VoidVisitorAdapter<Void> {457458private final ClassLoader classLoader;459private final Set<String> seenExports;460private final Set<String> packages;461private final ModuleVisitor byteBuddyVisitor;462463MyModuleVisitor(464ClassLoader classLoader,465Set<String> packages,466Set<String> excluded,467ModuleVisitor byteBuddyVisitor) {468this.classLoader = classLoader;469this.byteBuddyVisitor = byteBuddyVisitor;470471// Set is modifiable472this.packages = new HashSet<>(packages);473this.seenExports = new HashSet<>(excluded);474}475476@Override477public void visit(ModuleRequiresDirective n, Void arg) {478String name = n.getNameAsString();479if (name.startsWith("processed.")) {480// When 'Automatic-Module-Name' is not set, we must derive the module name from the jar file481// name. Therefore, the 'processed.' prefix added by bazel must be removed to get the name.482name = name.substring(10);483}484int modifiers = getByteBuddyModifier(n.getModifiers());485if (!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 these487// modules static,488// otherwise a 'module not found' error while compiling their code would be the consequence.489modifiers |= ACC_STATIC_PHASE;490}491byteBuddyVisitor.visitRequire(name, modifiers, null);492}493494@Override495public void visit(ModuleExportsDirective n, Void arg) {496if (seenExports.contains(n.getNameAsString())) {497return;498}499500seenExports.add(n.getNameAsString());501502byteBuddyVisitor.visitExport(503n.getNameAsString().replace('.', '/'),5040,505n.getModuleNames().stream().map(Name::asString).toArray(String[]::new));506}507508@Override509public void visit(ModuleProvidesDirective n, Void arg) {510byteBuddyVisitor.visitProvide(511getClassName(n.getNameAsString()),512n.getWith().stream().map(type -> getClassName(type.asString())).toArray(String[]::new));513}514515@Override516public void visit(ModuleUsesDirective n, Void arg) {517byteBuddyVisitor.visitUse(n.getNameAsString().replace('.', '/'));518}519520@Override521public void visit(ModuleOpensDirective n, Void arg) {522packages.forEach(523pkg -> byteBuddyVisitor.visitOpen(pkg.replace('.', '/'), 0, n.getNameAsString()));524}525526private int getByteBuddyModifier(NodeList<Modifier> modifiers) {527return modifiers.stream()528.mapToInt(529mod -> {530switch (mod.getKeyword()) {531case STATIC:532return ACC_STATIC_PHASE;533case TRANSITIVE:534return ACC_TRANSITIVE;535}536throw new RuntimeException("Unknown modifier: " + mod);537})538.reduce(0, (l, r) -> l | r);539}540541private String getClassName(String possibleClassName) {542String name = possibleClassName.replace('/', '.');543if (lookup(name)) {544return name.replace('.', '/');545}546547int index = name.lastIndexOf('.');548if (index != -1) {549name = name.substring(0, index) + "$" + name.substring(index + 1);550if (lookup(name)) {551return name.replace('.', '/');552}553}554555throw new RuntimeException("Cannot find class: " + name);556}557558private boolean lookup(String className) {559try {560Class.forName(className, false, classLoader);561return true;562} catch (ClassNotFoundException e) {563return false;564}565}566}567}568569570