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: