Add script to print R8 API not covered by API usage sample
Add a script api_sample_coverage.py that runs two programs PrintUses and
PrintSeeds and prints the difference in the outputs.
PrintUses prints the entry points in a given library (e.g. r8.jar)
used by a given program (e.g. d8_api_usage_sample.jar).
PrintSeeds prints the entry points in a given library (e.g. r8.jar)
kept by a given ProGuard configuration (e.g. src/main/keep.txt).
* Add 'include' predicate to RootSetBuilder.writeSeeds() to allow
PrintSeeds to skip printing classes in the Java library.
* Enqueuer: Add a helpful assertion error for when PrintSeeds discovers
that a library class is missing.
* Sort SwissArmyKnife to reduce the opportunity for merge conflicts
* Add utils.R8LIB_KEEP_RULES with path to keep-rules for keeping @Keep
Change-Id: I5963a1094a5bb9a99795a5cdeee6b6698551726a
diff --git a/src/main/java/com/android/tools/r8/PrintSeeds.java b/src/main/java/com/android/tools/r8/PrintSeeds.java
new file mode 100644
index 0000000..b1bc7ca
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/PrintSeeds.java
@@ -0,0 +1,83 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.shaking.Enqueuer;
+import com.android.tools.r8.shaking.RootSetBuilder;
+import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
+import com.android.tools.r8.utils.ExceptionUtils;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.Timing;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+public class PrintSeeds {
+
+ private static final String USAGE =
+ "Arguments: <rt.jar> <r8.jar> <pg-conf.txt>\n"
+ + "\n"
+ + "PrintSeeds prints the classes, interfaces, methods and fields selected by\n"
+ + "<pg-conf.txt> when compiling <r8.jar> alongside <rt.jar>.";
+
+ public static void main(String[] args) throws Exception {
+ if (args.length != 3) {
+ System.out.println(USAGE);
+ System.exit(1);
+ }
+ Path rtJar = Paths.get(args[0]);
+ Path r8Jar = Paths.get(args[1]);
+ Path pgConf = Paths.get(args[2]);
+ R8Command command =
+ R8Command.builder()
+ .addLibraryFiles(rtJar)
+ .addProgramFiles(r8Jar)
+ .addProguardConfigurationFiles(pgConf)
+ .setProgramConsumer(ClassFileConsumer.emptyConsumer())
+ .build();
+ Set<String> descriptors = new ArchiveClassFileProvider(r8Jar).getClassDescriptors();
+ InternalOptions options = command.getInternalOptions();
+ ExecutorService executorService = ThreadUtils.getExecutorService(options);
+ ExceptionUtils.withR8CompilationHandler(
+ command.getReporter(),
+ () -> {
+ try {
+ run(command, descriptors, options, executorService);
+ } finally {
+ executorService.shutdown();
+ }
+ });
+ }
+
+ private static void run(
+ R8Command command, Set<String> descriptors, InternalOptions options, ExecutorService executor)
+ throws IOException {
+ Timing timing = new Timing("PrintSeeds");
+ try {
+ DexApplication application =
+ new ApplicationReader(command.getInputApp(), options, timing).read(executor).toDirect();
+ AppInfoWithSubtyping appInfo = new AppInfoWithSubtyping(application);
+ RootSet rootSet =
+ new RootSetBuilder(
+ appInfo, application, options.proguardConfiguration.getRules(), options)
+ .run(executor);
+ Enqueuer enqueuer = new Enqueuer(appInfo, options, false);
+ appInfo = enqueuer.traceApplication(rootSet, executor, timing);
+ RootSetBuilder.writeSeeds(
+ appInfo.withLiveness(),
+ System.out,
+ type -> descriptors.contains(type.toDescriptorString()));
+ } catch (ExecutionException e) {
+ R8.unwrapExecutionException(e);
+ throw new AssertionError(e); // unwrapping method should have thrown
+ }
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/PrintUses.java b/src/main/java/com/android/tools/r8/PrintUses.java
new file mode 100644
index 0000000..0e69de6
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/PrintUses.java
@@ -0,0 +1,307 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.graph.AppInfo.ResolutionResult;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Timing;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class PrintUses {
+
+ private static final String USAGE =
+ "Arguments: <rt.jar> <r8.jar> <sample.jar>\n"
+ + "\n"
+ + "PrintUses prints the classes, interfaces, methods and fields used by <sample.jar>,\n"
+ + "restricted to classes and interfaces in <r8.jar> that are not in <sample.jar>.\n"
+ + "<rt.jar> and <r8.jar> should point to libraries used by <sample.jar>.";
+
+ private final Set<String> descriptors;
+ private final PrintStream out;
+ private Set<DexType> types = Sets.newIdentityHashSet();
+ private Map<DexType, Set<DexMethod>> methods = Maps.newIdentityHashMap();
+ private Map<DexType, Set<DexField>> fields = Maps.newIdentityHashMap();
+ private final DexApplication application;
+ private final AppInfoWithSubtyping appInfo;
+ private int errors;
+
+ class UseCollector extends UseRegistry {
+
+ @Override
+ public boolean registerInvokeVirtual(DexMethod method) {
+ DexEncodedMethod target = appInfo.lookupVirtualTarget(method.holder, method);
+ if (target != null && target.method != method) {
+ addType(method.holder);
+ addMethod(target.method);
+ } else {
+ addMethod(method);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean registerInvokeDirect(DexMethod method) {
+ addMethod(method);
+ return false;
+ }
+
+ @Override
+ public boolean registerInvokeStatic(DexMethod method) {
+ DexEncodedMethod target = appInfo.lookupStaticTarget(method);
+ if (target != null && target.method != method) {
+ addType(method.holder);
+ addMethod(target.method);
+ } else {
+ addMethod(method);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean registerInvokeInterface(DexMethod method) {
+ return registerInvokeVirtual(method);
+ }
+
+ @Override
+ public boolean registerInvokeSuper(DexMethod method) {
+ addMethod(method);
+ return false;
+ }
+
+ @Override
+ public boolean registerInstanceFieldWrite(DexField field) {
+ addField(field);
+ return false;
+ }
+
+ @Override
+ public boolean registerInstanceFieldRead(DexField field) {
+ addField(field);
+ return false;
+ }
+
+ @Override
+ public boolean registerNewInstance(DexType type) {
+ addType(type);
+ return false;
+ }
+
+ @Override
+ public boolean registerStaticFieldRead(DexField field) {
+ addField(field);
+ return false;
+ }
+
+ @Override
+ public boolean registerStaticFieldWrite(DexField field) {
+ addField(field);
+ return false;
+ }
+
+ @Override
+ public boolean registerTypeReference(DexType type) {
+ addType(type);
+ return false;
+ }
+
+ private void addType(DexType type) {
+ if (isTargetType(type) && types.add(type)) {
+ methods.put(type, Sets.newIdentityHashSet());
+ fields.put(type, Sets.newIdentityHashSet());
+ }
+ }
+
+ private boolean isTargetType(DexType type) {
+ return descriptors.contains(type.toDescriptorString());
+ }
+
+ private void addField(DexField field) {
+ addType(field.type);
+ addType(field.clazz);
+ Set<DexField> typeFields = fields.get(field.clazz);
+ if (typeFields != null) {
+ typeFields.add(field);
+ }
+ }
+
+ private void addMethod(DexMethod method) {
+ addType(method.holder);
+ for (DexType parameterType : method.proto.parameters.values) {
+ addType(parameterType);
+ }
+ addType(method.proto.returnType);
+ Set<DexMethod> typeMethods = methods.get(method.holder);
+ if (typeMethods != null) {
+ typeMethods.add(method);
+ }
+ }
+
+ private void registerField(DexEncodedField field) {
+ registerTypeReference(field.field.type);
+ }
+
+ private void registerMethod(DexEncodedMethod method) {
+ DexEncodedMethod superTarget = appInfo.lookupSuperTarget(method.method, method.method.holder);
+ if (superTarget != null) {
+ registerInvokeSuper(superTarget.method);
+ }
+ for (DexType type : method.method.proto.parameters.values) {
+ registerTypeReference(type);
+ }
+ registerTypeReference(method.method.proto.returnType);
+ method.registerCodeReferences(this);
+ }
+
+ private void registerSuperType(DexProgramClass clazz, DexType superType) {
+ registerTypeReference(superType);
+ // If clazz overrides any methods in superType, we should keep those as well.
+ clazz.forEachMethod(
+ method -> {
+ ResolutionResult resolutionResult = appInfo.resolveMethod(superType, method.method);
+ for (DexEncodedMethod dexEncodedMethod : resolutionResult.asListOfTargets()) {
+ addMethod(dexEncodedMethod.method);
+ }
+ });
+ }
+ }
+
+ public static void main(String[] args) throws Exception {
+ if (args.length != 3) {
+ System.out.println(USAGE);
+ return;
+ }
+ AndroidApp.Builder builder = AndroidApp.builder();
+ Path rtJar = Paths.get(args[0]);
+ builder.addLibraryFile(rtJar);
+ Path r8Jar = Paths.get(args[1]);
+ builder.addLibraryFile(r8Jar);
+ Path sampleJar = Paths.get(args[2]);
+ builder.addProgramFile(sampleJar);
+ Set<String> descriptors = new HashSet<>(getDescriptors(r8Jar));
+ descriptors.removeAll(getDescriptors(sampleJar));
+ PrintUses printUses = new PrintUses(descriptors, builder.build(), System.out);
+ printUses.analyze();
+ printUses.print();
+ if (printUses.errors > 0) {
+ System.err.println(printUses.errors + " errors");
+ System.exit(1);
+ }
+ }
+
+ private static Set<String> getDescriptors(Path path) throws IOException {
+ return new ArchiveClassFileProvider(path).getClassDescriptors();
+ }
+
+ private PrintUses(Set<String> descriptors, AndroidApp inputApp, PrintStream out)
+ throws Exception {
+ this.descriptors = descriptors;
+ this.out = out;
+ InternalOptions options = new InternalOptions();
+ application =
+ new ApplicationReader(inputApp, options, new Timing("PrintUses")).read().toDirect();
+ appInfo = new AppInfoWithSubtyping(application);
+ }
+
+ private void analyze() {
+ UseCollector useCollector = new UseCollector();
+ for (DexProgramClass dexProgramClass : application.classes()) {
+ useCollector.registerSuperType(dexProgramClass, dexProgramClass.superType);
+ for (DexType implementsType : dexProgramClass.interfaces.values) {
+ useCollector.registerSuperType(dexProgramClass, implementsType);
+ }
+ dexProgramClass.forEachMethod(useCollector::registerMethod);
+ dexProgramClass.forEachField(useCollector::registerField);
+ }
+ }
+
+ private void print() {
+ List<DexType> types = new ArrayList<>(this.types);
+ types.sort(Comparator.comparing(DexType::toSourceString));
+ for (DexType type : types) {
+ String typeName = type.toSourceString();
+ DexClass dexClass = application.definitionFor(type);
+ if (dexClass == null) {
+ error("Could not find definition for type " + type.toSourceString());
+ continue;
+ }
+ out.println(typeName);
+ List<DexMethod> methods = new ArrayList<>(this.methods.get(type));
+ List<String> methodDefinitions = new ArrayList<>(methods.size());
+ for (DexMethod method : methods) {
+ DexEncodedMethod encodedMethod = dexClass.lookupMethod(method);
+ if (encodedMethod == null) {
+ error("Could not find definition for method " + method.toSourceString());
+ continue;
+ }
+ methodDefinitions.add(getMethodSourceString(encodedMethod));
+ }
+ methodDefinitions.sort(Comparator.naturalOrder());
+ for (String encodedMethod : methodDefinitions) {
+ out.println(typeName + ": " + encodedMethod);
+ }
+ List<DexField> fields = new ArrayList<>(this.fields.get(type));
+ fields.sort(Comparator.comparing(DexField::toSourceString));
+ for (DexField field : fields) {
+ out.println(
+ typeName + ": " + field.type.toSourceString() + " " + field.name.toSourceString());
+ }
+ }
+ }
+
+ private void error(String message) {
+ out.println("# Error: " + message);
+ errors += 1;
+ }
+
+ private static String getMethodSourceString(DexEncodedMethod encodedMethod) {
+ DexMethod method = encodedMethod.method;
+ StringBuilder builder = new StringBuilder();
+ if (encodedMethod.accessFlags.isConstructor()) {
+ if (encodedMethod.accessFlags.isStatic()) {
+ builder.append("<clinit>");
+ } else {
+ String holderName = method.holder.toSourceString();
+ String constructorName = holderName.substring(holderName.lastIndexOf('.') + 1);
+ builder.append(constructorName);
+ }
+ } else {
+ builder
+ .append(method.proto.returnType.toSourceString())
+ .append(" ")
+ .append(method.name.toSourceString());
+ }
+ builder.append("(");
+ for (int i = 0; i < method.getArity(); i++) {
+ if (i != 0) {
+ builder.append(",");
+ }
+ builder.append(method.proto.parameters.values[i].toSourceString());
+ }
+ builder.append(")");
+ return builder.toString();
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 077e220..e42d8b0 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -287,7 +287,7 @@
if (options.proguardConfiguration.isPrintSeeds()) {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
PrintStream out = new PrintStream(bytes);
- RootSetBuilder.writeSeeds(appInfo.withLiveness(), out);
+ RootSetBuilder.writeSeeds(appInfo.withLiveness(), out, type -> true);
out.flush();
proguardSeedsData = bytes.toString();
}
diff --git a/src/main/java/com/android/tools/r8/SwissArmyKnife.java b/src/main/java/com/android/tools/r8/SwissArmyKnife.java
index 0e35847..28e1f93 100644
--- a/src/main/java/com/android/tools/r8/SwissArmyKnife.java
+++ b/src/main/java/com/android/tools/r8/SwissArmyKnife.java
@@ -28,38 +28,32 @@
return;
}
switch (args[0]) {
- case "r8":
- R8.main(shift(args));
- break;
- case "d8":
- D8.main(shift(args));
+ case "bisect":
+ Bisect.main(shift(args));
break;
case "compatdx":
CompatDx.main(shift(args));
break;
- case "dexfilemerger":
- DexFileMerger.main(shift(args));
- break;
- case "dexsplitter":
- DexSplitter.main(shift(args));
- break;
case "compatproguard":
CompatProguard.main(shift(args));
break;
+ case "d8":
+ D8.main(shift(args));
+ break;
case "d8logger":
D8Logger.main(shift(args));
break;
- case "disasm":
- Disassemble.main(shift(args));
- break;
- case "bisect":
- Bisect.main(shift(args));
+ case "dexfilemerger":
+ DexFileMerger.main(shift(args));
break;
case "dexsegments":
DexSegments.main(shift(args));
break;
- case "maindex":
- GenerateMainDexList.main(shift(args));
+ case "dexsplitter":
+ DexSplitter.main(shift(args));
+ break;
+ case "disasm":
+ Disassemble.main(shift(args));
break;
case "extractmarker":
ExtractMarker.main(shift(args));
@@ -67,6 +61,18 @@
case "jardiff":
JarDiff.main(shift(args));
break;
+ case "maindex":
+ GenerateMainDexList.main(shift(args));
+ break;
+ case "printseeds":
+ PrintSeeds.main(shift(args));
+ break;
+ case "printuses":
+ PrintUses.main(shift(args));
+ break;
+ case "r8":
+ R8.main(shift(args));
+ break;
default:
runDefault(args);
break;
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 085d9b3..51feb9d 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -734,6 +734,7 @@
private void transitionDefaultMethodsForInstantiatedClass(DexType iface, DexType instantiatedType,
ScopedDexMethodSet seen) {
DexClass clazz = appInfo.definitionFor(iface);
+ assert clazz != null : "Missing class " + iface.toSourceString();
assert clazz.accessFlags.isInterface();
SetWithReason<DexEncodedMethod> reachableMethods = reachableVirtualMethods.get(iface);
if (reachableMethods != null) {
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
index 700e146..c860d83 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
@@ -43,6 +43,7 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.function.Function;
+import java.util.function.Predicate;
import java.util.stream.Collectors;
public class RootSetBuilder {
@@ -441,17 +442,28 @@
}
// TODO(67934426): Test this code.
- public static void writeSeeds(AppInfoWithLiveness appInfo, PrintStream out) {
+ public static void writeSeeds(
+ AppInfoWithLiveness appInfo, PrintStream out, Predicate<DexType> include) {
for (DexItem seed : appInfo.getPinnedItems()) {
if (seed instanceof DexType) {
- out.println(seed.toSourceString());
+ if (include.test((DexType) seed)) {
+ out.println(seed.toSourceString());
+ }
} else if (seed instanceof DexField) {
DexField field = ((DexField) seed);
- out.println(
- field.clazz.toSourceString() + ": " + field.type.toSourceString() + " " + field.name
- .toSourceString());
+ if (include.test(field.clazz)) {
+ out.println(
+ field.clazz.toSourceString()
+ + ": "
+ + field.type.toSourceString()
+ + " "
+ + field.name.toSourceString());
+ }
} else if (seed instanceof DexMethod) {
DexMethod method = (DexMethod) seed;
+ if (!include.test(method.holder)) {
+ continue;
+ }
out.print(method.holder.toSourceString() + ": ");
DexEncodedMethod encodedMethod = appInfo.definitionFor(method);
if (encodedMethod.accessFlags.isConstructor()) {
diff --git a/tools/api_sample_coverage.py b/tools/api_sample_coverage.py
new file mode 100755
index 0000000..b14ad1a
--- /dev/null
+++ b/tools/api_sample_coverage.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python
+# Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+'''
+Compare the R8 API used by the API usage sample to the API kept by @Keep.
+'''
+
+import argparse
+import os
+import subprocess
+import utils
+
+parser = argparse.ArgumentParser(description=__doc__.strip(),
+ formatter_class=argparse.RawTextHelpFormatter)
+parser.add_argument('-o', '--output-dir')
+
+API_SAMPLE_JAR = 'tests/d8_api_usage_sample.jar'
+
+
+def main(output_dir=None):
+ if output_dir is None:
+ output_dir = ''
+
+ printseeds_path = os.path.join(output_dir, 'keep-seeds.txt')
+ printseeds_args = [
+ 'java', '-jar', utils.R8_JAR, 'printseeds',
+ utils.RT_JAR, utils.R8_JAR, utils.R8LIB_KEEP_RULES,
+ ]
+ write_sorted_lines(printseeds_args, printseeds_path)
+
+ printuses_path = os.path.join(output_dir, 'sample-uses.txt')
+ printuses_args = [
+ 'java', '-jar', utils.R8_JAR, 'printuses',
+ utils.RT_JAR, utils.R8_JAR, API_SAMPLE_JAR,
+ ]
+ write_sorted_lines(printuses_args, printuses_path)
+
+ print_diff(printseeds_path, printuses_path)
+
+
+def write_sorted_lines(cmd_args, output_path):
+ utils.PrintCmd(cmd_args)
+ output_lines = subprocess.check_output(cmd_args).splitlines(True)
+ print("Write output to %s" % output_path)
+ output_lines.sort()
+ with open(output_path, 'w') as fp:
+ for line in output_lines:
+ fp.write(line)
+
+
+def print_diff(printseeds_path, printuses_path):
+ with open(printseeds_path) as fp:
+ seeds = set(fp.read().splitlines())
+ with open(printuses_path) as fp:
+ uses = set(fp.read().splitlines())
+ only_in_seeds = seeds - uses
+ only_in_uses = uses - seeds
+ if only_in_seeds:
+ print("%s lines with '-' are marked @Keep " % len(only_in_seeds) +
+ "but not used by sample.")
+ if only_in_uses:
+ print("%s lines with '+' are used by sample " % len(only_in_uses) +
+ "but are missing @Keep annotations.")
+ for line in sorted(only_in_seeds):
+ print('-' + line)
+ for line in sorted(only_in_uses):
+ print('+' + line)
+ if not only_in_seeds and not only_in_uses:
+ print('Sample uses the entire set of members marked @Keep. Well done!')
+
+
+if __name__ == '__main__':
+ main(**vars(parser.parse_args()))
diff --git a/tools/build_r8lib.py b/tools/build_r8lib.py
index abaa619..c855bc5 100755
--- a/tools/build_r8lib.py
+++ b/tools/build_r8lib.py
@@ -18,7 +18,6 @@
formatter_class=argparse.RawTextHelpFormatter)
SAMPLE_JAR = os.path.join(utils.REPO_ROOT, 'tests/d8_api_usage_sample.jar')
-KEEP_RULES = os.path.join(utils.REPO_ROOT, 'src/main/keep.txt')
R8LIB_JAR = os.path.join(utils.LIBS, 'r8lib.jar')
R8LIB_MAP_FILE = os.path.join(utils.LIBS, 'r8lib-map.txt')
@@ -34,7 +33,7 @@
'--lib', utils.RT_JAR,
utils.R8_JAR,
'--output', R8LIB_JAR,
- '--pg-conf', KEEP_RULES,
+ '--pg-conf', utils.R8LIB_KEEP_RULES,
'--pg-map-output', R8LIB_MAP_FILE))
diff --git a/tools/printseeds.py b/tools/printseeds.py
new file mode 100755
index 0000000..4f389d5
--- /dev/null
+++ b/tools/printseeds.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import sys
+import toolhelper
+
+if __name__ == '__main__':
+ sys.exit(toolhelper.run('printseeds', sys.argv[1:]))
diff --git a/tools/printuses.py b/tools/printuses.py
new file mode 100755
index 0000000..17d3df1
--- /dev/null
+++ b/tools/printuses.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import sys
+import toolhelper
+
+if __name__ == '__main__':
+ sys.exit(toolhelper.run('printuses', sys.argv[1:]))
diff --git a/tools/utils.py b/tools/utils.py
index 859da97..851f3bd 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -37,6 +37,7 @@
MAVEN_ZIP = os.path.join(LIBS, 'r8.zip')
GENERATED_LICENSE = os.path.join(GENERATED_LICENSE_DIR, 'LICENSE')
RT_JAR = os.path.join(REPO_ROOT, 'third_party/openjdk/openjdk-rt-1.8/rt.jar')
+R8LIB_KEEP_RULES = os.path.join(REPO_ROOT, 'src/main/keep.txt')
def PrintCmd(s):
if type(s) is list: