Path: blob/trunk/java/src/dev/selenium/tools/modules/ModuleGenerator.java
3990 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.charset.StandardCharsets;56import java.nio.file.FileVisitResult;57import java.nio.file.Files;58import java.nio.file.Path;59import java.nio.file.Paths;60import java.nio.file.SimpleFileVisitor;61import java.nio.file.StandardCopyOption;62import java.nio.file.attribute.BasicFileAttributes;63import java.util.Arrays;64import java.util.Collection;65import java.util.HashSet;66import java.util.LinkedList;67import java.util.List;68import java.util.Map;69import java.util.Objects;70import java.util.ServiceLoader;71import java.util.Set;72import java.util.TreeMap;73import java.util.TreeSet;74import java.util.concurrent.atomic.AtomicReference;75import java.util.jar.Attributes;76import java.util.jar.JarEntry;77import java.util.jar.JarInputStream;78import java.util.jar.JarOutputStream;79import java.util.jar.Manifest;80import java.util.spi.ToolProvider;81import java.util.stream.Collectors;82import java.util.stream.Stream;83import java.util.zip.ZipEntry;84import java.util.zip.ZipOutputStream;85import net.bytebuddy.jar.asm.ClassReader;86import net.bytebuddy.jar.asm.ClassVisitor;87import net.bytebuddy.jar.asm.ClassWriter;88import net.bytebuddy.jar.asm.MethodVisitor;89import net.bytebuddy.jar.asm.ModuleVisitor;90import net.bytebuddy.jar.asm.Type;91import org.openqa.selenium.io.TemporaryFilesystem;9293public class ModuleGenerator {9495private static final String SERVICE_LOADER = ServiceLoader.class.getName().replace('.', '/');9697public static void main(String[] args) throws IOException {98Path outJar = null;99Path inJar = null;100String moduleName = null;101Set<Path> modulePath = new TreeSet<>();102Set<String> exports = new TreeSet<>();103Set<String> hides = new TreeSet<>();104Set<String> uses = new TreeSet<>();105106// There is no way at all these two having similar names will cause problems107Map<String, Set<String>> opensTo = new TreeMap<>();108Set<String> openTo = new TreeSet<>();109boolean isOpen = false;110111int argCount = args.length;112for (int i = 0; i < argCount; i++) {113String flag = args[i];114String next = args[++i];115switch (flag) {116case "--exports":117exports.add(next);118break;119120case "--hides":121hides.add(next);122break;123124case "--in":125inJar = Paths.get(next);126break;127128case "--is-open":129isOpen = Boolean.parseBoolean(next);130break;131132case "--module-name":133moduleName = next;134break;135136case "--module-path":137modulePath.add(Paths.get(next));138break;139140case "--open-to":141openTo.add(next);142break;143144case "--opens-to":145opensTo.computeIfAbsent(next, str -> new TreeSet<>()).add(args[++i]);146break;147148case "--output":149outJar = Paths.get(next);150break;151152case "--uses":153uses.add(next);154break;155156default:157throw new IllegalArgumentException(String.format("Unknown argument: %s", flag));158}159}160Objects.requireNonNull(moduleName, "Module name must be set.");161Objects.requireNonNull(outJar, "Output jar must be set.");162Objects.requireNonNull(inJar, "Input jar must be set.");163164ToolProvider jdeps = ToolProvider.findFirst("jdeps").orElseThrow();165File tempDir = TemporaryFilesystem.getDefaultTmpFS().createTempDir("module-dir", "");166Path temp = tempDir.toPath();167168// It doesn't matter what we use for writing to the stream: jdeps doesn't use it. *facepalm*169List<String> jdepsArgs = new LinkedList<>(List.of("--api-only", "--multi-release", "9"));170if (!modulePath.isEmpty()) {171Path tmp = Files.createTempDirectory("automatic_module_jars");172jdepsArgs.addAll(173List.of(174"--module-path",175modulePath.stream()176.map(177(s) -> {178String file = s.getFileName().toString();179180if (file.startsWith("processed_")) {181Path copy = tmp.resolve(file.substring(10));182183try {184Files.copy(s, copy, StandardCopyOption.REPLACE_EXISTING);185} catch (IOException e) {186throw new UncheckedIOException(e);187}188189return copy.toString();190}191192return s.toString();193})194.collect(Collectors.joining(File.pathSeparator))));195}196jdepsArgs.addAll(List.of("--generate-module-info", temp.toAbsolutePath().toString()));197jdepsArgs.add(inJar.toAbsolutePath().toString());198199PrintStream origOut = System.out;200PrintStream origErr = System.err;201202ByteArrayOutputStream bos = new ByteArrayOutputStream();203PrintStream printStream = new PrintStream(bos);204205int result;206try {207System.setOut(printStream);208System.setErr(printStream);209result = jdeps.run(printStream, printStream, jdepsArgs.toArray(new String[0]));210} finally {211System.setOut(origOut);212System.setErr(origErr);213}214if (result != 0) {215throw new RuntimeException(216String.format(217"Unable to process module:%njdeps %s%n%s",218String.join(" ", jdepsArgs), bos.toString(StandardCharsets.UTF_8)));219}220221AtomicReference<Path> moduleInfo = new AtomicReference<>();222// Fortunately, we know the directory where the output is written223Files.walkFileTree(224temp,225new SimpleFileVisitor<>() {226@Override227public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {228if ("module-info.java".equals(file.getFileName().toString())) {229moduleInfo.set(file);230}231return FileVisitResult.TERMINATE;232}233});234235if (moduleInfo.get() == null) {236throw new RuntimeException("Unable to read module info");237}238239ParserConfiguration parserConfig =240new ParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_11);241242Provider provider = Providers.provider(moduleInfo.get());243244ParseResult<CompilationUnit> parseResult =245new JavaParser(parserConfig).parse(COMPILATION_UNIT, provider);246247CompilationUnit unit =248parseResult249.getResult()250.orElseThrow(() -> new RuntimeException("Unable to parse " + moduleInfo.get()));251252ModuleDeclaration moduleDeclaration =253unit.getModule()254.orElseThrow(255() -> new RuntimeException("No module declaration in " + moduleInfo.get()));256257moduleDeclaration.setName(moduleName);258moduleDeclaration.setOpen(isOpen);259260Set<String> allUses = new TreeSet<>(uses);261allUses.addAll(readServicesFromClasses(inJar));262allUses.forEach(263service -> moduleDeclaration.addDirective(new ModuleUsesDirective(new Name(service))));264265// Prepare a classloader to help us find classes.266ClassLoader classLoader;267if (modulePath != null) {268URL[] urls =269Stream.concat(Stream.of(inJar.toAbsolutePath()), modulePath.stream())270.map(271path -> {272try {273return path.toUri().toURL();274} catch (MalformedURLException e) {275throw new UncheckedIOException(e);276}277})278.toArray(URL[]::new);279280classLoader = new URLClassLoader(urls);281} else {282classLoader = new URLClassLoader(new URL[0]);283}284285Set<String> packages = inferPackages(inJar);286287// Determine packages to export288Set<String> exportedPackages = new HashSet<>();289if (!isOpen) {290if (!exports.isEmpty()) {291exports.forEach(292export -> {293if (!packages.contains(export)) {294throw new RuntimeException(295String.format("Exported package '%s' not found in jar. %s", export, packages));296}297exportedPackages.add(export);298moduleDeclaration.addDirective(299new ModuleExportsDirective(new Name(export), new NodeList<>()));300});301} else {302packages.forEach(303export -> {304if (!hides.contains(export)) {305exportedPackages.add(export);306moduleDeclaration.addDirective(307new ModuleExportsDirective(new Name(export), new NodeList<>()));308}309});310}311}312313openTo.forEach(314module ->315moduleDeclaration.addDirective(316new ModuleOpensDirective(317new Name(module),318new NodeList(319exportedPackages.stream().map(Name::new).collect(Collectors.toSet())))));320321ClassWriter classWriter = new ClassWriter(0);322classWriter.visit(V11, ACC_MODULE, "module-info", null, null, null);323ModuleVisitor moduleVisitor = classWriter.visitModule(moduleName, isOpen ? ACC_OPEN : 0, null);324moduleVisitor.visitRequire("java.base", ACC_MANDATED, null);325326moduleDeclaration.accept(327new MyModuleVisitor(classLoader, exportedPackages, hides, moduleVisitor), null);328329moduleVisitor.visitEnd();330331classWriter.visitEnd();332333Manifest manifest = new Manifest();334manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");335336try (OutputStream os = Files.newOutputStream(outJar);337JarOutputStream jos = new JarOutputStream(os, manifest)) {338jos.setLevel(ZipOutputStream.STORED);339340byte[] bytes = classWriter.toByteArray();341342ZipEntry entry = new StableZipEntry("module-info.class");343entry.setSize(bytes.length);344345jos.putNextEntry(entry);346jos.write(bytes);347jos.closeEntry();348}349350TemporaryFilesystem.getDefaultTmpFS().deleteTempDir(tempDir);351}352353private static Collection<String> readServicesFromClasses(Path inJar) {354Set<String> serviceNames = new HashSet<>();355356try (InputStream is = Files.newInputStream(inJar);357JarInputStream jis = new JarInputStream(is)) {358for (JarEntry entry = jis.getNextJarEntry(); entry != null; entry = jis.getNextJarEntry()) {359if (entry.isDirectory() || !entry.getName().endsWith(".class")) {360continue;361}362363ClassReader reader = new ClassReader(jis);364reader.accept(365new ClassVisitor(ASM9) {366private Type serviceClass;367368@Override369public MethodVisitor visitMethod(370int access,371String name,372String descriptor,373String signature,374String[] exceptions) {375return new MethodVisitor(ASM9) {376@Override377public void visitMethodInsn(378int opcode,379String owner,380String name,381String descriptor,382boolean isInterface) {383if (SERVICE_LOADER.equals(owner) && "load".equals(name)) {384if (serviceClass != null) {385serviceNames.add(serviceClass.getClassName());386serviceClass = null;387}388}389}390391@Override392public void visitLdcInsn(Object value) {393if (value instanceof Type) {394serviceClass = (Type) value;395}396}397};398}399},4000);401}402} catch (IOException e) {403throw new UncheckedIOException(e);404}405406return serviceNames;407}408409private static Set<String> inferPackages(Path inJar) {410Set<String> packageNames = new TreeSet<>();411412try (InputStream is = Files.newInputStream(inJar);413JarInputStream jis = new JarInputStream(is)) {414for (JarEntry entry = jis.getNextJarEntry(); entry != null; entry = jis.getNextJarEntry()) {415416if (entry.isDirectory()) {417continue;418}419420if (!entry.getName().endsWith(".class")) {421continue;422}423424String name = entry.getName();425426int index = name.lastIndexOf('/');427if (index == -1) {428continue;429}430name = name.substring(0, index);431432// If we've a multi-release jar, remove that too433if (name.startsWith("META-INF/versions/")) {434String[] segments = name.split("/");435if (segments.length < 3) {436continue;437}438439name =440Arrays.stream(Arrays.copyOfRange(segments, 3, segments.length))441.collect(Collectors.joining("/"));442}443444name = name.replace("/", ".");445446packageNames.add(name);447}448449return packageNames;450} catch (IOException e) {451throw new UncheckedIOException(e);452}453}454455private static class MyModuleVisitor extends VoidVisitorAdapter<Void> {456457private final ClassLoader classLoader;458private final Set<String> seenExports;459private final Set<String> packages;460private final ModuleVisitor byteBuddyVisitor;461462MyModuleVisitor(463ClassLoader classLoader,464Set<String> packages,465Set<String> excluded,466ModuleVisitor byteBuddyVisitor) {467this.classLoader = classLoader;468this.byteBuddyVisitor = byteBuddyVisitor;469470// Set is modifiable471this.packages = new HashSet<>(packages);472this.seenExports = new HashSet<>(excluded);473}474475@Override476public void visit(ModuleRequiresDirective n, Void arg) {477String name = n.getNameAsString();478if (name.startsWith("processed.")) {479// When 'Automatic-Module-Name' is not set, we must derive the module name from the jar file480// name. Therefore, the 'processed.' prefix added by bazel must be removed to get the name.481name = name.substring(10);482}483int modifiers = getByteBuddyModifier(n.getModifiers());484if (!name.startsWith("org.seleniumhq.selenium.") && !name.startsWith("java.")) {485// Some people like to exclude jars from the classpath. To allow this we need to make these486// modules static,487// otherwise a 'module not found' error while compiling their code would be the consequence.488modifiers |= ACC_STATIC_PHASE;489}490byteBuddyVisitor.visitRequire(name, modifiers, null);491}492493@Override494public void visit(ModuleExportsDirective n, Void arg) {495if (seenExports.contains(n.getNameAsString())) {496return;497}498499seenExports.add(n.getNameAsString());500501byteBuddyVisitor.visitExport(502n.getNameAsString().replace('.', '/'),5030,504n.getModuleNames().stream().map(Name::asString).toArray(String[]::new));505}506507@Override508public void visit(ModuleProvidesDirective n, Void arg) {509byteBuddyVisitor.visitProvide(510getClassName(n.getNameAsString()),511n.getWith().stream().map(type -> getClassName(type.asString())).toArray(String[]::new));512}513514@Override515public void visit(ModuleUsesDirective n, Void arg) {516byteBuddyVisitor.visitUse(n.getNameAsString().replace('.', '/'));517}518519@Override520public void visit(ModuleOpensDirective n, Void arg) {521packages.forEach(522pkg -> byteBuddyVisitor.visitOpen(pkg.replace('.', '/'), 0, n.getNameAsString()));523}524525private int getByteBuddyModifier(NodeList<Modifier> modifiers) {526return modifiers.stream()527.mapToInt(528mod -> {529switch (mod.getKeyword()) {530case STATIC:531return ACC_STATIC_PHASE;532case TRANSITIVE:533return ACC_TRANSITIVE;534}535throw new RuntimeException("Unknown modifier: " + mod);536})537.reduce(0, (l, r) -> l | r);538}539540private String getClassName(String possibleClassName) {541String name = possibleClassName.replace('/', '.');542if (lookup(name)) {543return name.replace('.', '/');544}545546int index = name.lastIndexOf('.');547if (index != -1) {548name = name.substring(0, index) + "$" + name.substring(index + 1);549if (lookup(name)) {550return name.replace('.', '/');551}552}553554throw new RuntimeException("Cannot find class: " + name);555}556557private boolean lookup(String className) {558try {559Class.forName(className, false, classLoader);560return true;561} catch (ClassNotFoundException e) {562return false;563}564}565}566}567568569