Make ijar keep types/member functions that have annotated with @KeepForCompile.
The inline Kotlin language feature causes method implementations to be a part of the jar's compiletime interface. We have a Kotlin compiler plugin that will annotate inlined methods with @KeepForCompile and ijar can use this to include code that is a part of the library's compiletime interface into the stripped jar.
This iteration keeps the entire class, in the future it only needs to keep the
method implementation and any constant pool entries the method accesses.
PiperOrigin-RevId: 219553768
diff --git a/third_party/ijar/classfile.cc b/third_party/ijar/classfile.cc
index 7c03f51..f9fed61 100644
--- a/third_party/ijar/classfile.cc
+++ b/third_party/ijar/classfile.cc
@@ -434,6 +434,10 @@
Constant *attribute_name_;
};
+struct KeepForCompileAttribute : Attribute {
+ void Write(u1 *&p) { WriteProlog(p, 0); }
+};
+
// See sec.4.7.5 of JVM spec.
struct ExceptionsAttribute : Attribute {
@@ -1335,6 +1339,8 @@
bool ReadConstantPool(const u1 *&p);
+ bool IsExplicitlyKept();
+
bool IsLocalOrAnonymous();
void WriteHeader(u1 *&p) {
@@ -1447,6 +1453,10 @@
} else if (attr_name == "NestMembers") {
attributes.push_back(
NestMembersAttribute::Read(p, attribute_name, attribute_length));
+ } else if (attr_name == "com.google.devtools.ijar.KeepForCompile") {
+ auto attr = new KeepForCompileAttribute;
+ attr->attribute_name_ = attribute_name;
+ attributes.push_back(attr);
} else {
// Skip over unknown attributes with a warning. The JVM spec
// says this is ok, so long as we handle the mandatory attributes.
@@ -1584,6 +1594,28 @@
return false;
}
+static bool HasKeepForCompile(const std::vector<Attribute *> attributes) {
+ for (const Attribute *attribute : attributes) {
+ if (attribute->attribute_name_->Display() ==
+ "com.google.devtools.ijar.KeepForCompile") {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool ClassFile::IsExplicitlyKept() {
+ if (HasKeepForCompile(attributes)) {
+ return true;
+ }
+ for (const Member *method : methods) {
+ if (HasKeepForCompile(method->attributes)) {
+ return true;
+ }
+ }
+ return false;
+}
+
static ClassFile *ReadClass(const void *classdata, size_t length) {
const u1 *p = (u1*) classdata;
@@ -1831,13 +1863,14 @@
bool StripClass(u1 *&classdata_out, const u1 *classdata_in, size_t in_length) {
ClassFile *clazz = ReadClass(classdata_in, in_length);
bool keep = true;
- if (clazz == NULL) {
- // Class is invalid. Simply copy it to the output and call it a day.
+ if (clazz == NULL || clazz->IsExplicitlyKept()) {
+ // Class is invalid or kept. Simply copy it to the output and call it a day.
+ // TODO: If kept, only emit methods marked with KeepForCompile attribute,
+ // as opposed to the entire type.
put_n(classdata_out, classdata_in, in_length);
} else if (clazz->IsLocalOrAnonymous()) {
keep = false;
} else {
-
// Constant pool item zero is a dummy entry. Setting it marks the
// beginning of the output phase; calls to Constant::slot() will
// fail if called prior to this.
diff --git a/third_party/ijar/test/BUILD b/third_party/ijar/test/BUILD
index a19fd2f..151e59e 100644
--- a/third_party/ijar/test/BUILD
+++ b/third_party/ijar/test/BUILD
@@ -35,6 +35,7 @@
# TODO(cushon): build from source once we have a JDK 11
"nestmates/nestmates.jar",
":source_debug_extension.jar",
+ ":keep_for_compile_lib.jar",
":largest_regular.jar",
":smallest_zip64.jar",
":definitely_zip64.jar",
@@ -199,6 +200,24 @@
)
java_binary(
+ name = "GenKeepForCompile",
+ testonly = 1,
+ srcs = ["GenKeepForCompile.java"],
+ main_class = "GenKeepForCompile",
+ deps = [
+ "//third_party:asm",
+ ],
+)
+
+genrule(
+ name = "keep_for_compile_lib",
+ testonly = 1,
+ outs = ["keep_for_compile_lib.jar"],
+ cmd = "$(location :GenKeepForCompile) $@",
+ tools = [":GenKeepForCompile"],
+)
+
+java_binary(
name = "GenModuleInfo",
testonly = 1,
srcs = ["GenModuleInfo.java"],
diff --git a/third_party/ijar/test/GenKeepForCompile.java b/third_party/ijar/test/GenKeepForCompile.java
new file mode 100644
index 0000000..e53429f
--- /dev/null
+++ b/third_party/ijar/test/GenKeepForCompile.java
@@ -0,0 +1,210 @@
+// Copyright 2018 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.
+
+import java.io.FileOutputStream;
+import java.util.jar.JarOutputStream;
+import java.util.zip.ZipEntry;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.Attribute;
+import org.objectweb.asm.ByteVector;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+/**
+ * GenKeepForCompile creates a jarfile containing a class definition that has the KeepForCOmpile
+ * attribute.
+ */
+public class GenKeepForCompile implements Opcodes {
+
+ public static void main(String[] args) throws Exception {
+ try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(args[0]))) {
+ jos.putNextEntry(new ZipEntry("functions/car/CarInlineUtilsKt.class"));
+ jos.write(dump());
+ jos.closeEntry();
+ }
+ }
+
+ public static byte[] dump() throws Exception {
+
+ ClassWriter classWriter = new ClassWriter(0);
+ MethodVisitor methodVisitor;
+ AnnotationVisitor annotationVisitor0;
+
+ classWriter.visit(
+ V1_8,
+ ACC_PUBLIC | ACC_FINAL | ACC_SUPER,
+ "functions/car/CarInlineUtilsKt",
+ null,
+ "java/lang/Object",
+ null);
+
+ classWriter.visitSource("CarInlineUtils.kt", null);
+
+ {
+ annotationVisitor0 = classWriter.visitAnnotation("Lkotlin/Metadata;", true);
+ annotationVisitor0.visit("mv", new int[] {1, 1, 11});
+ annotationVisitor0.visit("bv", new int[] {1, 0, 2});
+ annotationVisitor0.visit("k", Integer.valueOf(2));
+ {
+ AnnotationVisitor annotationVisitor1 = annotationVisitor0.visitArray("d1");
+ annotationVisitor1.visit(
+ null,
+ "\u0000\u0014\n\u0000\n\u0002\u0010\u000e\n\u0002\u0018\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\u0010\u0008\u001a!\u0010\u0000\u001a\u00020\u0001*\u00020\u00022\u0012\u0010\u0003\u001a\u000e\u0012\u0004\u0012\u00020\u0005\u0012\u0004\u0012\u00020\u00010\u0004H\u0086\u0008");
+ annotationVisitor1.visitEnd();
+ }
+ {
+ AnnotationVisitor annotationVisitor1 = annotationVisitor0.visitArray("d2");
+ annotationVisitor1.visit(null, "customName");
+ annotationVisitor1.visit(null, "");
+ annotationVisitor1.visit(null, "Lfunctions/car/Car;");
+ annotationVisitor1.visit(null, "formatYear");
+ annotationVisitor1.visit(null, "Lkotlin/Function1;");
+ annotationVisitor1.visit(null, "");
+ annotationVisitor1.visitEnd();
+ }
+ annotationVisitor0.visitEnd();
+ }
+ {
+ methodVisitor =
+ classWriter.visitMethod(
+ ACC_PUBLIC | ACC_FINAL | ACC_STATIC,
+ "customName",
+ "(Lfunctions/car/Car;Lkotlin/jvm/functions/Function1;)Ljava/lang/String;",
+ "(Lfunctions/car/Car;Lkotlin/jvm/functions/Function1<-Ljava/lang/Integer;Ljava/lang/String;>;)Ljava/lang/String;",
+ null);
+ methodVisitor.visitParameter("$receiver", ACC_MANDATED);
+ methodVisitor.visitParameter("formatYear", 0);
+ {
+ annotationVisitor0 =
+ methodVisitor.visitAnnotation("Lorg/jetbrains/annotations/NotNull;", false);
+ annotationVisitor0.visitEnd();
+ }
+ methodVisitor.visitAnnotableParameterCount(2, false);
+ {
+ annotationVisitor0 =
+ methodVisitor.visitParameterAnnotation(0, "Lorg/jetbrains/annotations/NotNull;", false);
+ annotationVisitor0.visitEnd();
+ }
+ {
+ annotationVisitor0 =
+ methodVisitor.visitParameterAnnotation(1, "Lorg/jetbrains/annotations/NotNull;", false);
+ annotationVisitor0.visitEnd();
+ }
+ // ATTRIBUTE com.google.devtools.ijar.KeepForCompile
+ // ASM-ifier doesn't emit attributes it does not know about :( - emitting by hand here.
+ methodVisitor.visitAttribute(
+ new Attribute("com.google.devtools.ijar.KeepForCompile") {
+ @Override
+ public ByteVector write(
+ ClassWriter cw, byte[] code, int len, int maxStack, int maxLocals) {
+ // nothing to write, just get the attribute's name in the file.
+ return new ByteVector();
+ }
+ });
+
+ methodVisitor.visitCode();
+ Label label0 = new Label();
+ methodVisitor.visitLabel(label0);
+ methodVisitor.visitVarInsn(ALOAD, 0);
+ methodVisitor.visitLdcInsn("$receiver");
+ methodVisitor.visitMethodInsn(
+ INVOKESTATIC,
+ "kotlin/jvm/internal/Intrinsics",
+ "checkParameterIsNotNull",
+ "(Ljava/lang/Object;Ljava/lang/String;)V",
+ false);
+ methodVisitor.visitVarInsn(ALOAD, 1);
+ methodVisitor.visitLdcInsn("formatYear");
+ methodVisitor.visitMethodInsn(
+ INVOKESTATIC,
+ "kotlin/jvm/internal/Intrinsics",
+ "checkParameterIsNotNull",
+ "(Ljava/lang/Object;Ljava/lang/String;)V",
+ false);
+ Label label1 = new Label();
+ methodVisitor.visitLabel(label1);
+ methodVisitor.visitLineNumber(3, label1);
+ methodVisitor.visitTypeInsn(NEW, "java/lang/StringBuilder");
+ methodVisitor.visitInsn(DUP);
+ methodVisitor.visitMethodInsn(
+ INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
+ methodVisitor.visitVarInsn(ALOAD, 1);
+ methodVisitor.visitVarInsn(ALOAD, 0);
+ methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "functions/car/Car", "getYear", "()I", false);
+ methodVisitor.visitMethodInsn(
+ INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
+ methodVisitor.visitMethodInsn(
+ INVOKEINTERFACE,
+ "kotlin/jvm/functions/Function1",
+ "invoke",
+ "(Ljava/lang/Object;)Ljava/lang/Object;",
+ true);
+ methodVisitor.visitTypeInsn(CHECKCAST, "java/lang/String");
+ methodVisitor.visitMethodInsn(
+ INVOKEVIRTUAL,
+ "java/lang/StringBuilder",
+ "append",
+ "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
+ false);
+ methodVisitor.visitIntInsn(BIPUSH, 32);
+ methodVisitor.visitMethodInsn(
+ INVOKEVIRTUAL,
+ "java/lang/StringBuilder",
+ "append",
+ "(C)Ljava/lang/StringBuilder;",
+ false);
+ methodVisitor.visitVarInsn(ALOAD, 0);
+ methodVisitor.visitMethodInsn(
+ INVOKEVIRTUAL, "functions/car/Car", "getMake", "()Ljava/lang/String;", false);
+ methodVisitor.visitMethodInsn(
+ INVOKEVIRTUAL,
+ "java/lang/StringBuilder",
+ "append",
+ "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
+ false);
+ methodVisitor.visitIntInsn(BIPUSH, 32);
+ methodVisitor.visitMethodInsn(
+ INVOKEVIRTUAL,
+ "java/lang/StringBuilder",
+ "append",
+ "(C)Ljava/lang/StringBuilder;",
+ false);
+ methodVisitor.visitVarInsn(ALOAD, 0);
+ methodVisitor.visitMethodInsn(
+ INVOKEVIRTUAL, "functions/car/Car", "getModel", "()Ljava/lang/String;", false);
+ methodVisitor.visitMethodInsn(
+ INVOKEVIRTUAL,
+ "java/lang/StringBuilder",
+ "append",
+ "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
+ false);
+ methodVisitor.visitMethodInsn(
+ INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
+ methodVisitor.visitInsn(ARETURN);
+ Label label2 = new Label();
+ methodVisitor.visitLabel(label2);
+ methodVisitor.visitLocalVariable("$receiver", "Lfunctions/car/Car;", null, label0, label2, 0);
+ methodVisitor.visitLocalVariable(
+ "formatYear", "Lkotlin/jvm/functions/Function1;", null, label0, label2, 1);
+ methodVisitor.visitLocalVariable("$i$f$customName", "I", null, label0, label2, 2);
+ methodVisitor.visitMaxs(3, 3);
+ methodVisitor.visitEnd();
+ }
+ classWriter.visitEnd();
+
+ return classWriter.toByteArray();
+ }
+}
diff --git a/third_party/ijar/test/ijar_test.sh b/third_party/ijar/test/ijar_test.sh
index 20b1d64..4bb075e 100755
--- a/third_party/ijar/test/ijar_test.sh
+++ b/third_party/ijar/test/ijar_test.sh
@@ -82,6 +82,7 @@
CENTRAL_DIR_LARGEST_REGULAR=$IJAR_SRCDIR/test/largest_regular.jar
CENTRAL_DIR_SMALLEST_ZIP64=$IJAR_SRCDIR/test/smallest_zip64.jar
CENTRAL_DIR_ZIP64=$IJAR_SRCDIR/test/definitely_zip64.jar
+KEEP_FOR_COMPILE=$IJAR_SRCDIR/test/keep_for_compile_lib.jar
#### Setup
@@ -540,6 +541,22 @@
expect_not_log "SourceDebugExtension" "SourceDebugExtension preserved!"
}
+function test_keep_for_compile() {
+ $IJAR --strip_jar $KEEP_FOR_COMPILE $TEST_TMPDIR/keep.jar \
+ || fail "ijar failed"
+ lines=$($JAVAP -classpath $TEST_TMPDIR/keep.jar -c -p \
+ functions.car.CarInlineUtilsKt |
+ grep "// Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull" |
+ wc -l)
+ check_eq 2 $lines "Output jar should have kept method body"
+ attr=$($JAVAP -classpath $TEST_TMPDIR/keep.jar -v \
+ functions.car.CarInlineUtilsKt |
+ strings |
+ grep "com.google.devtools.ijar.KeepForCompile" |
+ wc -l)
+ check_eq 2 $attr "Output jar should have kept KeepForCompile attribute."
+}
+
function test_central_dir_largest_regular() {
$IJAR $CENTRAL_DIR_LARGEST_REGULAR $TEST_TMPDIR/ijar.jar || fail "ijar failed"
$ZIP_COUNT $TEST_TMPDIR/ijar.jar 65535 || fail