diff options
author | Colin Cross <ccross@google.com> | 2017-02-22 23:02:44 +0000 |
---|---|---|
committer | Yue Gan <yueg@google.com> | 2017-02-23 11:31:00 +0000 |
commit | 19e126a1a7643e170ceec65a11f6a4d330ae1b3b (patch) | |
tree | 122df8eaa766e0eae39dab89aabca53772d131d3 /src/tools | |
parent | 1d7db156291762b46550b9610ee1b1c73fde58ad (diff) |
Add a flag to enable desugaring java.* classes by rewriting package names
java.* and sun.* classes cannot be desugared directly because there are hard coded restrictions in the JVM that prevent using loading or using reflection on them. Add a --core_library flag that rewrites package names to have a __desugar__ prefix when reading them, and strips the prefix when writing them back out.
--
PiperOrigin-RevId: 148273386
MOS_MIGRATED_REVID=148273386
Diffstat (limited to 'src/tools')
5 files changed, 209 insertions, 24 deletions
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/BUILD b/src/tools/android/java/com/google/devtools/build/android/desugar/BUILD index cc2dcead72..73c28453de 100644 --- a/src/tools/android/java/com/google/devtools/build/android/desugar/BUILD +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/BUILD @@ -18,6 +18,7 @@ java_library( "//src/main/protobuf:worker_protocol_java_proto", "//src/tools/android/java/com/google/devtools/build/android:android_builder_lib", "//third_party:asm", + "//third_party:asm-commons", "//third_party:asm-tree", "//third_party:auto_value", "//third_party:guava", diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java b/src/tools/android/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java index d324723853..6ec4e0d4c9 100644 --- a/src/tools/android/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java @@ -22,9 +22,11 @@ import org.objectweb.asm.ClassReader; class ClassReaderFactory { private final ZipFile jar; + private final CoreLibraryRewriter rewriter; - public ClassReaderFactory(ZipFile jar) { + public ClassReaderFactory(ZipFile jar, CoreLibraryRewriter rewriter) { this.jar = jar; + this.rewriter = rewriter; } /** @@ -34,13 +36,13 @@ class ClassReaderFactory { */ @Nullable public ClassReader readIfKnown(String internalClassName) { - ZipEntry entry = jar.getEntry(internalClassName + ".class"); + ZipEntry entry = jar.getEntry(rewriter.unprefix(internalClassName) + ".class"); if (entry == null) { return null; } try (InputStream bytecode = jar.getInputStream(entry)) { // ClassReader doesn't take ownership and instead eagerly reads the stream's contents - return new ClassReader(bytecode); + return rewriter.reader(bytecode); } catch (IOException e) { // We should've already read through all files in the Jar once at this point, so we don't // expect failures reading some files a second time. diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java b/src/tools/android/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java new file mode 100644 index 0000000000..4a2510908d --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java @@ -0,0 +1,155 @@ +// Copyright 2017 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar; + +import java.io.IOException; +import java.io.InputStream; +import org.objectweb.asm.Attribute; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.commons.ClassRemapper; +import org.objectweb.asm.commons.Remapper; + +/** Utility class to prefix or unprefix class names of core library classes */ +class CoreLibraryRewriter { + private final String prefix; + + public CoreLibraryRewriter(String prefix) { + this.prefix = prefix; + } + + /** + * Factory method that returns either a normal ClassReader if prefix is empty, or a ClassReader + * with a ClassRemapper that prefixes class names of core library classes if prefix is not empty. + */ + public ClassReader reader(InputStream content) throws IOException { + if (prefix.length() != 0) { + return new PrefixingClassReader(content); + } else { + return new ClassReader(content); + } + } + + /** + * Factory method that returns a ClassVisitor that delegates to a ClassWriter, removing prefix + * from core library class names if it is not empty. + */ + public UnprefixingClassWriter writer(int flags) { + return new UnprefixingClassWriter(flags); + } + + private static boolean shouldPrefix(String typeName) { + return (typeName.startsWith("java/") || typeName.startsWith("sun/")) && !except(typeName); + } + + private static boolean except(String typeName) { + if (typeName.startsWith("java/lang/invoke/")) { + return true; + } + + switch (typeName) { + // Autoboxed types + case "java/lang/Boolean": + case "java/lang/Byte": + case "java/lang/Character": + case "java/lang/Double": + case "java/lang/Float": + case "java/lang/Integer": + case "java/lang/Long": + case "java/lang/Number": + case "java/lang/Short": + + // Special types + case "java/lang/Class": + case "java/lang/Object": + case "java/lang/String": + case "java/lang/Throwable": + return true; + + default: // fall out + } + + return false; + } + + /** Prefixes core library class names with prefix */ + public String prefix(String typeName) { + if (prefix.length() > 0 && shouldPrefix(typeName)) { + return prefix + typeName; + } + return typeName; + } + + /** Removes prefix from class names */ + public String unprefix(String typeName) { + if (prefix.length() == 0 || !typeName.startsWith(prefix)) { + return typeName; + } + return typeName.substring(prefix.length()); + } + + /** + * ClassReader that prefixes core library class names as they are read + */ + private class PrefixingClassReader extends ClassReader { + PrefixingClassReader(InputStream content) throws IOException { + super(content); + } + + @Override + public void accept(ClassVisitor cv, Attribute[] attrs, int flags) { + cv = + new ClassRemapper( + cv, + new Remapper() { + @Override + public String map(String typeName) { + return prefix(typeName); + } + }); + super.accept(cv, attrs, flags); + } + } + + /** + * ClassVisitor that delegates to a ClassWriter, but removes a prefix as each class is written. + * The unprefixing is optimized out if prefix is empty. + */ + public class UnprefixingClassWriter extends ClassVisitor { + private final ClassWriter writer; + + UnprefixingClassWriter(int flags) { + super(Opcodes.ASM5); + this.writer = new ClassWriter(flags); + this.cv = this.writer; + if (prefix.length() != 0) { + this.cv = + new ClassRemapper( + this.cv, + new Remapper() { + @Override + public String map(String typeName) { + return unprefix(typeName); + } + }); + } + } + + byte[] toByteArray() { + return writer.toByteArray(); + } + } +} diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java b/src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java index 687e513937..9fdd82988e 100644 --- a/src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java @@ -105,6 +105,12 @@ class Desugar { help = "Minimum targeted sdk version. If >= 24, enables default methods in interfaces." ) public int minSdkVersion; + + @Option(name = "core_library", + defaultValue = "false", + category = "undocumented", + help = "Enables rewriting to desugar java.* classes.") + public boolean coreLibrary; } public static void main(String[] args) throws Exception { @@ -142,13 +148,19 @@ class Desugar { parent = new ThrowingClassLoader(); } + CoreLibraryRewriter rewriter = + new CoreLibraryRewriter(options.coreLibrary ? "__desugar__/" : ""); + ClassLoader loader = - createClassLoader(options.bootclasspath, options.inputJar, options.classpath, parent); + createClassLoader( + rewriter, options.bootclasspath, options.inputJar, options.classpath, parent); + try (ZipFile in = new ZipFile(options.inputJar.toFile()); ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream( Files.newOutputStream(options.outputJar)))) { LambdaClassMaker lambdas = new LambdaClassMaker(dumpDirectory); - ClassReaderFactory readerFactory = new ClassReaderFactory(in); + ClassReaderFactory readerFactory = new ClassReaderFactory(in, rewriter); + ImmutableSet.Builder<String> interfaceLambdaMethodCollector = ImmutableSet.builder(); // Process input Jar, desugaring as we go @@ -159,16 +171,23 @@ class Desugar { // Android anyways. Resources are written as they were in the input jar to avoid any // danger of accidentally uncompressed resources ending up in an .apk. if (entry.getName().endsWith(".class")) { - ClassReader reader = new ClassReader(content); - ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS /*for bridge methods*/); + ClassReader reader = rewriter.reader(content); + CoreLibraryRewriter.UnprefixingClassWriter writer = + rewriter.writer(ClassWriter.COMPUTE_MAXS /*for bridge methods*/); ClassVisitor visitor = writer; if (!allowDefaultMethods) { visitor = new Java7Compatibility(visitor, readerFactory); } - reader.accept( - new LambdaDesugaring( - visitor, loader, lambdas, interfaceLambdaMethodCollector, allowDefaultMethods), - 0); + + visitor = new LambdaDesugaring( + visitor, + loader, + lambdas, + interfaceLambdaMethodCollector, + allowDefaultMethods); + + reader.accept(visitor, 0); + writeStoredEntry(out, entry.getName(), writer.toByteArray()); } else { // TODO(bazel-team): Avoid de- and re-compressing resource files @@ -191,13 +210,16 @@ class Desugar { for (Map.Entry<Path, LambdaInfo> lambdaClass : lambdas.drain().entrySet()) { try (InputStream bytecode = Files.newInputStream(dumpDirectory.resolve(lambdaClass.getKey()))) { - ClassReader reader = new ClassReader(bytecode); - ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS /*for invoking bridges*/); + ClassReader reader = rewriter.reader(bytecode); + CoreLibraryRewriter.UnprefixingClassWriter writer = + rewriter.writer(ClassWriter.COMPUTE_MAXS /*for invoking bridges*/); ClassVisitor visitor = writer; + if (!allowDefaultMethods) { // null ClassReaderFactory b/c we don't expect to need it for lambda classes visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null); } + LambdaClassFixer lambdaFixer = new LambdaClassFixer( visitor, @@ -209,7 +231,8 @@ class Desugar { // instructions in generated lambda classes (checkState below will fail) reader.accept( new LambdaDesugaring(lambdaFixer, loader, lambdas, null, allowDefaultMethods), 0); - writeStoredEntry(out, lambdaFixer.getInternalName() + ".class", writer.toByteArray()); + String name = rewriter.unprefix(lambdaFixer.getInternalName() + ".class"); + writeStoredEntry(out, name, writer.toByteArray()); } } @@ -237,8 +260,9 @@ class Desugar { out.closeEntry(); } - private static ClassLoader createClassLoader(List<Path> bootclasspath, Path inputJar, - List<Path> classpath, ClassLoader parent) throws IOException { + private static ClassLoader createClassLoader(CoreLibraryRewriter rewriter, + List<Path> bootclasspath, Path inputJar, List<Path> classpath, + ClassLoader parent) throws IOException { // Prepend classpath with input jar itself so LambdaDesugaring can load classes with lambdas. // Note that inputJar and classpath need to be in the same classloader because we typically get // the header Jar for inputJar on the classpath and having the header Jar in a parent loader @@ -247,9 +271,9 @@ class Desugar { // Use a classloader that as much as possible uses the provided bootclasspath instead of // the tool's system classloader. Unfortunately we can't do that for java. classes. if (!bootclasspath.isEmpty()) { - parent = HeaderClassLoader.fromClassPath(bootclasspath, parent); + parent = HeaderClassLoader.fromClassPath(bootclasspath, rewriter, parent); } - return HeaderClassLoader.fromClassPath(classpath, parent); + return HeaderClassLoader.fromClassPath(classpath, rewriter, parent); } private static class ThrowingClassLoader extends ClassLoader { diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java b/src/tools/android/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java index 90abfd44ec..053d52d4b2 100644 --- a/src/tools/android/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java @@ -42,11 +42,12 @@ import org.objectweb.asm.Opcodes; class HeaderClassLoader extends ClassLoader { private final Map<String, JarFile> jarfiles; + private final CoreLibraryRewriter rewriter; /** Creates a classloader from the given classpath with the given parent. */ - public static HeaderClassLoader fromClassPath(List<Path> classpath, ClassLoader parent) - throws IOException { - return new HeaderClassLoader(indexJars(classpath), parent); + public static HeaderClassLoader fromClassPath( + List<Path> classpath, CoreLibraryRewriter rewriter, ClassLoader parent) throws IOException { + return new HeaderClassLoader(indexJars(classpath), rewriter, parent); } /** @@ -67,14 +68,16 @@ class HeaderClassLoader extends ClassLoader { return result; } - private HeaderClassLoader(Map<String, JarFile> jarfiles, ClassLoader parent) { + private HeaderClassLoader( + Map<String, JarFile> jarfiles, CoreLibraryRewriter rewriter, ClassLoader parent) { super(parent); + this.rewriter = rewriter; this.jarfiles = jarfiles; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { - String filename = name.replace('.', '/') + ".class"; + String filename = rewriter.unprefix(name.replace('.', '/') + ".class"); JarFile jarfile = jarfiles.get(filename); if (jarfile == null) { throw new ClassNotFoundException(); @@ -82,7 +85,7 @@ class HeaderClassLoader extends ClassLoader { ZipEntry entry = jarfile.getEntry(filename); byte[] bytecode; try (InputStream content = jarfile.getInputStream(entry)) { - ClassReader reader = new ClassReader(content); + ClassReader reader = rewriter.reader(content); // Have ASM compute maxs so we don't need to figure out how many formal parameters there are ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); reader.accept(new CodeStubber(writer), 0); |