blob: bb9e5ea166aec8e9aa47b74a1d20bcb868e926d9 [file] [log] [blame]
// Copyright 2016 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.resources;
import static java.lang.Math.max;
import static java.nio.file.StandardOpenOption.CREATE_NEW;
import com.android.SdkConstants;
import com.android.resources.ResourceType;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.InstructionAdapter;
/**
* Writes out bytecode for an R.class directly, rather than go through an R.java and compile. This
* avoids re-parsing huge R.java files and other time spent in the java compiler (e.g., plugins like
* ErrorProne). A difference is that this doesn't generate line number tables and other debugging
* information. Also, the order of the constant pool tends to be different.
*/
public class RClassGenerator {
private static final int JAVA_VERSION = Opcodes.V1_7;
private static final String SUPER_CLASS = "java/lang/Object";
static final String PROVENANCE_ANNOTATION_CLASS_DESCRIPTOR =
"Lcom/google/devtools/build/android/resources/Provenance;";
static final String PROVENANCE_ANNOTATION_LABEL_KEY = "label";
private static final int MAX_BYTES_PER_METHOD = 65535;
private final String label;
private final Path outFolder;
private final FieldInitializers initializers;
private final boolean finalFields;
private final boolean annotateTransitiveFields;
private static final Splitter PACKAGE_SPLITTER = Splitter.on('.');
/**
* Create an RClassGenerator given a collection of initializers.
*
* @param label Bazel target which owns the generated R class
* @param outFolder base folder to place the output R class files.
* @param initializers the list of initializers to use for each inner class
* @param finalFields true if the fields should be marked final
* @param annotateTransitiveFields whether the R class and fields from transitive dependencies
* should be annotated.
*/
public static RClassGenerator with(
String label,
Path outFolder,
FieldInitializers initializers,
boolean finalFields,
boolean annotateTransitiveFields) {
return new RClassGenerator(
label, outFolder, initializers, finalFields, annotateTransitiveFields);
}
@VisibleForTesting
static RClassGenerator with(Path outFolder, FieldInitializers initializers, boolean finalFields) {
return new RClassGenerator(
/* label= */ null,
outFolder,
initializers,
finalFields,
/*annotateTransitiveFields=*/ false);
}
private RClassGenerator(
String label,
Path outFolder,
FieldInitializers initializers,
boolean finalFields,
boolean annotateTransitiveFields) {
this.label = label;
this.outFolder = outFolder;
this.initializers = initializers;
this.finalFields = finalFields;
this.annotateTransitiveFields = annotateTransitiveFields;
}
/**
* Builds bytecode and writes out R.class file, and R$inner.class files for provided package and
* symbols.
*/
public void write(String packageName, FieldInitializers symbolsToWrite) throws IOException {
writeClasses(packageName, initializers.filter(symbolsToWrite));
}
/** Builds bytecode and writes out R.class file, and R$inner.class files for provided package. */
public void write(String packageName) throws IOException {
writeClasses(packageName, initializers);
}
private void writeClasses(
String packageName,
Iterable<Map.Entry<ResourceType, Collection<FieldInitializer>>> initializersToWrite)
throws IOException {
Iterable<String> folders = PACKAGE_SPLITTER.split(packageName);
Path packageDir = outFolder;
for (String folder : folders) {
packageDir = packageDir.resolve(folder);
}
// At least create the outFolder that was requested. However, if there are no symbols, don't
// create the R.class and inner class files (no need to have an empty class).
Files.createDirectories(packageDir);
if (Iterables.isEmpty(initializersToWrite)) {
return;
}
Path rClassFile = packageDir.resolve(SdkConstants.FN_COMPILED_RESOURCE_CLASS);
String packageWithSlashes = packageName.replaceAll("\\.", "/");
String rClassName = packageWithSlashes.isEmpty() ? "R" : (packageWithSlashes + "/R");
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classWriter.visit(
JAVA_VERSION,
Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_SUPER,
rClassName,
null, /* signature */
SUPER_CLASS,
null /* interfaces */);
if (annotateTransitiveFields) {
AnnotationVisitor av =
classWriter.visitAnnotation(PROVENANCE_ANNOTATION_CLASS_DESCRIPTOR, /*visible=*/ true);
av.visit(PROVENANCE_ANNOTATION_LABEL_KEY, label);
av.visitEnd();
}
classWriter.visitSource(SdkConstants.FN_RESOURCE_CLASS, null);
writeConstructor(classWriter);
// Build the R.class w/ the inner classes, then later build the individual R$inner.class.
for (Map.Entry<ResourceType, Collection<FieldInitializer>> entry : initializersToWrite) {
String innerClassName = rClassName + "$" + entry.getKey().toString();
classWriter.visitInnerClass(
innerClassName,
rClassName,
entry.getKey().toString(),
Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_STATIC);
}
classWriter.visitEnd();
Files.write(rClassFile, classWriter.toByteArray(), CREATE_NEW);
// Now generate the R$inner.class files.
for (Map.Entry<ResourceType, Collection<FieldInitializer>> entry : initializersToWrite) {
writeInnerClass(entry.getValue(), packageDir, rClassName, entry.getKey().toString());
}
}
private void writeInnerClass(
Collection<FieldInitializer> initializers,
Path packageDir,
String fullyQualifiedOuterClass,
String innerClass)
throws IOException {
ClassWriter innerClassWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
String fullyQualifiedInnerClass =
writeInnerClassHeader(fullyQualifiedOuterClass, innerClass, innerClassWriter);
List<FieldInitializer> deferredInitializers = new ArrayList<>();
for (FieldInitializer init : initializers) {
JavaIdentifierValidator.validate(
init.getFieldName(), "in class:", fullyQualifiedInnerClass, "and package:", packageDir);
if (init.writeFieldDefinition(innerClassWriter, finalFields, annotateTransitiveFields)) {
deferredInitializers.add(init);
}
}
if (!deferredInitializers.isEmpty()) {
writeStaticClassInit(innerClassWriter, fullyQualifiedInnerClass, deferredInitializers);
}
innerClassWriter.visitEnd();
Path innerFile = packageDir.resolve("R$" + innerClass + ".class");
Files.write(innerFile, innerClassWriter.toByteArray(), CREATE_NEW);
}
private String writeInnerClassHeader(
String fullyQualifiedOuterClass, String innerClass, ClassWriter innerClassWriter) {
String fullyQualifiedInnerClass = fullyQualifiedOuterClass + "$" + innerClass;
innerClassWriter.visit(
JAVA_VERSION,
Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_SUPER,
fullyQualifiedInnerClass,
null, /* signature */
SUPER_CLASS,
null /* interfaces */);
innerClassWriter.visitSource(SdkConstants.FN_RESOURCE_CLASS, null);
writeConstructor(innerClassWriter);
innerClassWriter.visitInnerClass(
fullyQualifiedInnerClass,
fullyQualifiedOuterClass,
innerClass,
Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_STATIC);
return fullyQualifiedInnerClass;
}
private static void writeConstructor(ClassWriter classWriter) {
MethodVisitor constructor =
classWriter.visitMethod(
Opcodes.ACC_PUBLIC, "<init>", "()V", null, /* signature */ null /* exceptions */);
constructor.visitCode();
constructor.visitVarInsn(Opcodes.ALOAD, 0);
constructor.visitMethodInsn(Opcodes.INVOKESPECIAL, SUPER_CLASS, "<init>", "()V", false);
constructor.visitInsn(Opcodes.RETURN);
constructor.visitMaxs(1, 1);
constructor.visitEnd();
}
/*
* Writes out <clinit> and if fields are non-final, creates code to delegate to another static
* method if <clinit> becomes too large.
*/
private void writeStaticClassInit(
ClassWriter classWriter, String className, List<FieldInitializer> deferredInitializers) {
int accessFlags = Opcodes.ACC_STATIC;
// MAX_BYTES_PER_METHOD - INVOKESTATIC(3) - RETURN(1)
int maxBytesBeforeInvokeAndReturn = MAX_BYTES_PER_METHOD - 3 - 1;
int staticInitNameSuffix = 0;
String methodName = "<clinit>";
ListIterator<FieldInitializer> iterator = deferredInitializers.listIterator();
while (iterator.hasNext()) {
int currentMethodSize = 0;
int stackSlotsNeeded = 0;
// This first time around the method name is <clinit> and after that, the method name is
// created in the previous iteration.
MethodVisitor visitor =
classWriter.visitMethod(
accessFlags, methodName, "()V", null, /* signature */ null /* exceptions */);
visitor.visitCode();
InstructionAdapter insts = new InstructionAdapter(visitor);
while (iterator.hasNext()) {
FieldInitializer fieldInit = iterator.next();
// Only limit fields per method if fields are non-final since otherwise fields have to be
// initialized in clinit.
if (!finalFields
&& currentMethodSize + fieldInit.getMaxBytecodeSize() > maxBytesBeforeInvokeAndReturn) {
Preconditions.checkState(
currentMethodSize != 0,
"Field %s.%s is too big to initialize.",
className,
fieldInit.getFieldName());
// Backtrack to prepare field for next method.
iterator.previous();
break;
}
stackSlotsNeeded = max(stackSlotsNeeded, fieldInit.writeCLInit(insts, className));
currentMethodSize += fieldInit.getMaxBytecodeSize();
}
if (iterator.hasNext()) {
// If there are more fields, delegate to new method that will contain their initializations.
methodName = "staticInit" + staticInitNameSuffix++;
visitor.visitMethodInsn(
Opcodes.INVOKESTATIC, className, methodName, "()V", /*isInterface=*/ false);
accessFlags = Opcodes.ACC_STATIC | Opcodes.ACC_PRIVATE | Opcodes.ACC_SYNTHETIC;
}
insts.areturn(Type.VOID_TYPE);
visitor.visitMaxs(stackSlotsNeeded, 0);
visitor.visitEnd();
}
}
}