Merge "Add support for partial update and target library."
diff --git a/scripts/cargo2android.py b/scripts/cargo2android.py
index 64af845..908caf5 100755
--- a/scripts/cargo2android.py
+++ b/scripts/cargo2android.py
@@ -681,8 +681,10 @@
     self.dump_android_flags()
     if self.externs:
       self.dump_android_externs()
-    self.dump_android_property_list('static_libs', '"lib%s"', self.static_libs)
-    self.dump_android_property_list('shared_libs', '"lib%s"', self.shared_libs)
+    static_libs = [lib for lib in self.static_libs if not lib in self.runner.args.lib_blocklist]
+    self.dump_android_property_list('static_libs', '"lib%s"', static_libs)
+    shared_libs = [lib for lib in self.shared_libs if not lib in self.runner.args.lib_blocklist]
+    self.dump_android_property_list('shared_libs', '"lib%s"', shared_libs)
 
   def main_src_basename_path(self):
     return re.sub('/', '_', re.sub('.rs$', '', self.main_src))
@@ -704,6 +706,7 @@
   def decide_one_module_type(self, crate_type):
     """Decide which Android module type to use."""
     host = '' if self.device_supported else '_host'
+    rlib = '_rlib' if self.runner.args.force_rlib else ''
     if crate_type == 'bin':  # rust_binary[_host]
       self.module_type = 'rust_binary' + host
       # In rare cases like protobuf-codegen, the output binary name must
@@ -714,11 +717,11 @@
       # TODO(chh): should this be rust_library[_host]?
       # Assuming that Cargo.toml do not use both 'lib' and 'rlib',
       # because we map them both to rlib.
-      self.module_type = 'rust_library' + host
+      self.module_type = 'rust_library' + rlib + host
       self.stem = 'lib' + self.crate_name
       self.module_name = altered_name(self.stem)
     elif crate_type == 'rlib':  # rust_library[_host]
-      self.module_type = 'rust_library' + host
+      self.module_type = 'rust_library' + rlib + host
       self.stem = 'lib' + self.crate_name
       self.module_name = altered_name(self.stem)
     elif crate_type == 'dylib':  # rust_library[_host]_dylib
@@ -1607,11 +1610,21 @@
       nargs='*',
       help='Mark the main library as apex_available with the given apexes.')
   parser.add_argument(
+      '--force-rlib',
+      action='store_true',
+      default=False,
+      help='Make the main library an rlib.')
+  parser.add_argument(
       '--dependency-blocklist',
       nargs='*',
       default=[],
       help='Do not emit the given dependencies.')
   parser.add_argument(
+      '--lib-blocklist',
+      nargs='*',
+      default=[],
+      help='Do not emit the given C libraries as dependencies.')
+  parser.add_argument(
       '--test-blocklist',
       nargs='*',
       default=[],
diff --git a/scripts/update_crate_tests.py b/scripts/update_crate_tests.py
index 53bfd83..192f50e 100755
--- a/scripts/update_crate_tests.py
+++ b/scripts/update_crate_tests.py
@@ -13,170 +13,251 @@
 # 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.
-"""Add tests to TEST_MAPPING. Include tests for reverse dependencies."""
+"""Add or update tests to TEST_MAPPING.
+
+This script uses Bazel to find reverse dependencies on a crate and generates a
+TEST_MAPPING file. It accepts the absolute path to a crate as argument. If no
+argument is provided, it assumes the crate is the current directory.
+
+  Usage:
+  $ . build/envsetup.sh
+  $ lunch aosp_arm64-eng
+  $ update_crate_tests.py $ANDROID_BUILD_TOP/external/rust/crates/libc
+
+This script is automatically called by external_updater.
+"""
+
 import json
 import os
 import platform
 import subprocess
 import sys
 
-test_options = {
+# Some tests requires specific options. Consider fixing the upstream crate
+# before updating this dictionary.
+TEST_OPTIONS = {
     "ring_device_test_tests_digest_tests": [{"test-timeout": "600000"}],
     "ring_device_test_src_lib": [{"test-timeout": "100000"}],
 }
-test_exclude = [
+
+# Excluded tests. These tests will be ignored by this script.
+TEST_EXCLUDE = [
         "aidl_test_rust_client",
         "aidl_test_rust_service"
-    ]
-exclude_paths = [
+]
+
+# Excluded modules.
+EXCLUDE_PATHS = [
         "//external/adhd",
         "//external/crosvm",
         "//external/libchromeos-rs",
         "//external/vm_tools"
-    ]
+]
+
+
+class UpdaterException(Exception):
+    """Exception generated by this script."""
+
 
 class Env(object):
-    def __init__(self, path):
+    """Env captures the execution environment.
+
+    It ensures this script is executed within an AOSP repository.
+
+    Attributes:
+      ANDROID_BUILD_TOP: A string representing the absolute path to the top
+        of the repository.
+    """
+    def __init__(self):
         try:
             self.ANDROID_BUILD_TOP = os.environ['ANDROID_BUILD_TOP']
-        except:
-            sys.exit('ERROR: this script must be run from an Android tree.')
-        if path == None:
-            self.cwd = os.getcwd()
-        else:
-            self.cwd = path
-        try:
-            self.cwd_relative = self.cwd.split(self.ANDROID_BUILD_TOP)[1]
-            self.setup = True
-        except:
-            # Mark setup as failed if a path to a rust crate is not provided.
-            self.setup = False
+        except KeyError:
+            raise UpdaterException('$ANDROID_BUILD_TOP is not defined; you '
+                                   'must first source build/envsetup.sh and '
+                                   'select a target.')
+
 
 class Bazel(object):
-    # set up the Bazel queryview
+    """Bazel wrapper.
+
+    The wrapper is used to call bazel queryview and generate the list of
+    reverse dependencies.
+
+    Attributes:
+      path: The path to the bazel executable.
+    """
     def __init__(self, env):
-        os.chdir(env.ANDROID_BUILD_TOP)
-        print("Building Bazel Queryview. This can take a couple of minutes...")
-        cmd = "./build/soong/soong_ui.bash --build-mode --all-modules --dir=. queryview"
-        try:
-            out = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT)
-            self.setup = True
-        except subprocess.CalledProcessError as e:
-            print("Error: Unable to update TEST_MAPPING due to the following build error:")
-            print(e.output)
-            # Mark setup as failed if the Bazel queryview fails to build.
-            self.setup = False
-        os.chdir(env.cwd)
+        """Constructor.
 
-    def path(self):
-        # Only tested on Linux.
+        Note that the current directory is changed to ANDROID_BUILD_TOP.
+
+        Args:
+          env: An instance of Env.
+
+        Raises:
+          UpdaterException: an error occurred while calling soong_ui.
+        """
         if platform.system() != 'Linux':
-            sys.exit('ERROR: this script has only been tested on Linux.')
-        return "/usr/bin/bazel"
+            raise UpdaterException('This script has only been tested on Linux.')
+        self.path = os.path.join(env.ANDROID_BUILD_TOP, "tools", "bazel")
+        soong_ui = os.path.join(env.ANDROID_BUILD_TOP, "build", "soong", "soong_ui.bash")
 
-    # Return all modules for a given path.
+        # soong_ui requires to be at the root of the repository.
+        os.chdir(env.ANDROID_BUILD_TOP)
+        print("Generating Bazel files...")
+        cmd = [soong_ui, "--make-mode", "GENERATE_BAZEL_FILES=1", "nothing"]
+        try:
+            subprocess.check_output(cmd, stderr=subprocess.STDOUT, text=True)
+        except subprocess.CalledProcessError as e:
+            raise UpdaterException('Unable to generate bazel workspace: ' + e.output)
+
+        print("Building Bazel Queryview. This can take a couple of minutes...")
+        cmd = [soong_ui, "--build-mode", "--all-modules", "--dir=.", "queryview"]
+        try:
+            subprocess.check_output(cmd, stderr=subprocess.STDOUT, text=True)
+        except subprocess.CalledProcessError as e:
+            raise UpdaterException('Unable to update TEST_MAPPING: ' + e.output)
+
     def query_modules(self, path):
-        with open(os.devnull, 'wb') as DEVNULL:
-            cmd = self.path() + " query --config=queryview /" + path + ":all"
-            out = subprocess.check_output(cmd, shell=True, stderr=DEVNULL, text=True).strip().split("\n")
-            modules = set()
-            for line in out:
-                # speed up by excluding unused modules.
-                if "windows_x86" in line:
-                    continue
-                modules.add(line)
-            return modules
+        """Returns all modules for a given path."""
+        cmd = self.path + " query --config=queryview /" + path + ":all"
+        out = subprocess.check_output(cmd, shell=True, stderr=subprocess.DEVNULL, text=True).strip().split("\n")
+        modules = set()
+        for line in out:
+            # speed up by excluding unused modules.
+            if "windows_x86" in line:
+                continue
+            modules.add(line)
+        return modules
 
-    # Return all reverse dependencies for a single module.
     def query_rdeps(self, module):
-        with open(os.devnull, 'wb') as DEVNULL:
-            cmd = (self.path() + " query --config=queryview \'rdeps(//..., " +
-                    module + ")\' --output=label_kind")
-            out = (subprocess.check_output(cmd, shell=True, stderr=DEVNULL, text=True)
-                    .strip().split("\n"))
-            if '' in out:
-                out.remove('')
-            return out
+        """Returns all reverse dependencies for a single module."""
+        cmd = (self.path + " query --config=queryview \'rdeps(//..., " +
+                module + ")\' --output=label_kind")
+        out = (subprocess.check_output(cmd, shell=True, stderr=subprocess.DEVNULL, text=True)
+                .strip().split("\n"))
+        if '' in out:
+            out.remove('')
+        return out
 
     def exclude_module(self, module):
-        for path in exclude_paths:
+        for path in EXCLUDE_PATHS:
             if module.startswith(path):
                 return True
         return False
 
-    # Return all reverse dependency tests for modules in this package.
     def query_rdep_tests(self, modules):
+        """Returns all reverse dependency tests for modules in this package."""
         rdep_tests = set()
         for module in modules:
             for rdep in self.query_rdeps(module):
-                rule_type, tmp, mod = rdep.split(" ")
+                rule_type, _, mod = rdep.split(" ")
                 if rule_type == "rust_test_" or rule_type == "rust_test":
                     if self.exclude_module(mod) == False:
                         rdep_tests.add(mod.split(":")[1].split("--")[0])
         return rdep_tests
 
 
-class Crate(object):
-    def __init__(self, path, bazel):
-        self.modules = bazel.query_modules(path)
-        self.rdep_tests = bazel.query_rdep_tests(self.modules)
+class Package(object):
+    """A Bazel package.
+
+    Attributes:
+      dir: The absolute path to this package.
+      dir_rel: The relative path to this package.
+      rdep_tests: The list of computed reverse dependencies.
+    """
+    def __init__(self, path, env, bazel):
+        """Constructor.
+
+        Note that the current directory is changed to the package location when
+        called.
+
+        Args:
+          path: Path to the package.
+          env: An instance of Env.
+          bazel: An instance of Bazel.
+
+        Raises:
+          UpdaterException: the package does not appear to belong to the
+            current repository.
+        """
+        if path == None:
+            self.dir = os.getcwd()
+        else:
+            self.dir = path
+        try:
+            self.dir_rel = self.dir.split(env.ANDROID_BUILD_TOP)[1]
+        except IndexError:
+            raise UpdaterException('The path ' + self.dir + ' is not under ' +
+                            env.ANDROID_BUILD_TOP + '; You must be in the '
+                            'directory of a crate or pass its absolute path '
+                            'as first argument.')
+
+        # Move to the package_directory.
+        os.chdir(self.dir)
+        modules = bazel.query_modules(self.dir_rel)
+        self.rdep_tests = bazel.query_rdep_tests(modules)
 
     def get_rdep_tests(self):
         return self.rdep_tests
 
 
 class TestMapping(object):
+    """A TEST_MAPPING file.
+
+    Attributes:
+      package: The package associated with this TEST_MAPPING file.
+    """
     def __init__(self, path):
-        self.env = Env(path)
-        self.bazel = Bazel(self.env)
+        """Constructor.
 
-    def is_setup(self):
-        return self.env.setup and self.bazel.setup
+        Args:
+          path: The absolute path to the package.
+        """
+        env = Env()
+        bazel = Bazel(env)
+        self.package = Package(path, env, bazel)
 
-    def create_test_mapping(self, path):
-        if not self.is_setup():
-            return
-        tests = self.get_tests(path)
+    def create(self):
+        """Generates the TEST_MAPPING file."""
+        tests = self.package.get_rdep_tests()
         if not bool(tests):
             return
         test_mapping = self.tests_to_mapping(tests)
         self.write_test_mapping(test_mapping)
 
-    def get_tests(self, path):
-        # for each path collect local Rust modules.
-        if path is not None and path != "":
-            return Crate(self.env.cwd_relative + "/" + path, self.bazel).get_rdep_tests()
-        else:
-            return Crate(self.env.cwd_relative, self.bazel).get_rdep_tests()
-
     def tests_to_mapping(self, tests):
+        """Translate the test list into a dictionary."""
         test_mapping = {"presubmit": []}
         for test in tests:
-            if test in test_exclude:
+            if test in TEST_EXCLUDE:
                 continue
-            if test in test_options:
-                test_mapping["presubmit"].append({"name": test, "options": test_options[test]})
+            if test in TEST_OPTIONS:
+                test_mapping["presubmit"].append({"name": test, "options": TEST_OPTIONS[test]})
             else:
                 test_mapping["presubmit"].append({"name": test})
         test_mapping["presubmit"] = sorted(test_mapping["presubmit"], key=lambda t: t["name"])
         return test_mapping
 
     def write_test_mapping(self, test_mapping):
+        """Writes the TEST_MAPPING file."""
         with open("TEST_MAPPING", "w") as json_file:
             json_file.write("// Generated by update_crate_tests.py for tests that depend on this crate.\n")
             json.dump(test_mapping, json_file, indent=2, separators=(',', ': '), sort_keys=True)
             json_file.write("\n")
         print("TEST_MAPPING successfully updated!")
 
+
 def main():
     if len(sys.argv) == 2:
         path = sys.argv[1]
     else:
         path = None
-    test_mapping = TestMapping(path)
-    test_mapping.create_test_mapping(None)
-    if not test_mapping.is_setup():
-        raise ValueError('Error getting crate tests.')
+    try:
+        test_mapping = TestMapping(path)
+    except UpdaterException as err:
+        sys.exit("Error: " + str(err))
+    test_mapping.create()
 
 if __name__ == '__main__':
   main()