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/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: