More options and rename maps; one file per rust_test

* Add a --no-host option.
* Add a --host-first-multilib option.
* Add special renaming for protoc_gen_rust.
* Dump a rust_defaults module to be shared by rust_test modules.
  * Every test source file is in a rust_test module.
  * Add RENAME_DEFAULTS_MAP for altered rust_defaults names.
* Suppress old error message on --extern proc_macro.

Test: regenerate Android.bp files and check differences
Bug: 161090277
Bug: 161089524
Change-Id: I8c70f568facf58d282ea2a2e72b60c8a2972b27d
diff --git a/scripts/cargo2android.py b/scripts/cargo2android.py
index 992872c..08f23fa 100755
--- a/scripts/cargo2android.py
+++ b/scripts/cargo2android.py
@@ -61,13 +61,26 @@
 import sys
 
 RENAME_MAP = {
-    # This map includes all changes to the default rust library module
-    # names to resolve name conflicts or avoid confusion.
+    # This map includes all changes to the default rust module names
+    # to resolve name conflicts, avoid confusion, or work as plugin.
     'libbacktrace': 'libbacktrace_rust',
     'libgcc': 'libgcc_rust',
     'liblog': 'liblog_rust',
     'libsync': 'libsync_rust',
     'libx86_64': 'libx86_64_rust',
+    'protoc_gen_rust': 'protoc-gen-rust',
+}
+
+RENAME_STEM_MAP = {
+    # This map includes all changes to the default rust module stem names,
+    # which is used for output files when different from the module name.
+    'protoc_gen_rust': 'protoc-gen-rust',
+}
+
+RENAME_DEFAULTS_MAP = {
+    # This map includes all changes to the default prefix of rust_default
+    # module names, to avoid conflict with existing Android modules.
+    'libc': 'rust_libc',
 }
 
 # Header added to all generated Android.bp files.
@@ -109,6 +122,14 @@
   return RENAME_MAP[name] if (name in RENAME_MAP) else name
 
 
+def altered_stem(name):
+  return RENAME_STEM_MAP[name] if (name in RENAME_STEM_MAP) else name
+
+
+def altered_defaults(name):
+  return RENAME_DEFAULTS_MAP[name] if (name in RENAME_DEFAULTS_MAP) else name
+
+
 def is_build_crate_name(name):
   # We added special prefix to build script crate names.
   return name.startswith('build_script_')
@@ -172,6 +193,7 @@
     # Android module properties derived from rustc parameters.
     self.module_name = ''  # unique in Android build system
     self.module_type = ''  # rust_{binary,library,test}[_host] etc.
+    self.defaults = ''  # rust_defaults used by rust_test* modules
     self.root_pkg = ''  # parent package name of a sub/test packge, from -L
     self.srcs = list()  # main_src or merged multiple source files
     self.stem = ''  # real base name of output file
@@ -407,6 +429,8 @@
     if self.target:
       self.device_supported = True
     self.host_supported = True  # assume host supported for all builds
+    if self.runner.args.no_host:  # unless --no-host was specified
+      self.host_supported = False
     self.cfgs = sorted(set(self.cfgs))
     self.features = sorted(set(self.features))
     self.codegens = sorted(set(self.codegens))
@@ -495,15 +519,68 @@
     dump_list('//  -l (dylib) = %s', self.shared_libs)
 
   def dump_android_module(self):
-    # Dump one Android module per crate_type.
+    """Dump one or more Android module definition, depending on crate_types."""
     if len(self.crate_types) == 1:
-      # do not change self.stem or self.module_name
-      self.dump_one_android_module(self.crate_types[0])
+      self.dump_single_type_android_module()
       return
+    if 'test' in self.crate_types:
+      self.write('\nERROR: multiple crate types cannot include test type')
+      return
+    # Dump one Android module per crate_type.
     for crate_type in self.crate_types:
       self.decide_one_module_type(crate_type)
       self.dump_one_android_module(crate_type)
 
+  def dump_defaults_module(self):
+    """Dump a rust_defaults module to be shared by other modules."""
+    name = altered_defaults(self.root_pkg) + '_defaults'
+    name = self.runner.claim_module_name(name, self, 0)
+    self.defaults = name
+    self.write('\nrust_defaults {')
+    self.write('    name: "' + name + '",')
+    self.write('    crate_name: "' + self.crate_name + '",')
+    if 'test' in self.crate_types:
+      self.write('    test_suites: ["general-tests"],')
+      self.write('    auto_gen_config: true,')
+    self.dump_edition_flags_libs()
+    self.write('}')
+
+  def dump_single_type_android_module(self):
+    """Dump one simple Android module, which has only one crate_type."""
+    crate_type = self.crate_types[0]
+    if crate_type != 'test':
+      # do not change self.stem or self.module_name
+      self.dump_one_android_module(crate_type)
+      return
+    # Dump one test module per source file, and separate host and device tests.
+    # crate_type == 'test'
+    if (self.host_supported and self.device_supported) or len(self.srcs) > 1:
+      self.srcs = sorted(set(self.srcs))
+      self.dump_defaults_module()
+    saved_srcs = self.srcs
+    for src in saved_srcs:
+      self.srcs = [src]
+      saved_device_supported = self.device_supported
+      saved_host_supported = self.host_supported
+      saved_main_src = self.main_src
+      self.main_src = src
+      if saved_host_supported:
+        self.device_supported = False
+        self.host_supported = True
+        self.module_name = self.test_module_name()
+        self.decide_one_module_type(crate_type)
+        self.dump_one_android_module(crate_type)
+      if saved_device_supported:
+        self.device_supported = True
+        self.host_supported = False
+        self.module_name = self.test_module_name()
+        self.decide_one_module_type(crate_type)
+        self.dump_one_android_module(crate_type)
+      self.host_supported = saved_host_supported
+      self.device_supported = saved_device_supported
+      self.main_src = saved_main_src
+    self.srcs = saved_srcs
+
   def dump_one_android_module(self, crate_type):
     """Dump one Android module definition."""
     if not self.module_type:
@@ -511,9 +588,14 @@
       return
     self.write('\n' + self.module_type + ' {')
     self.dump_android_core_properties()
-    if self.edition:
-      self.write('    edition: "' + self.edition + '",')
-    self.dump_android_property_list('features', '"%s"', self.features)
+    if not self.defaults:
+      self.dump_edition_flags_libs()
+    if self.runner.args.host_first_multilib and self.host_supported and crate_type != 'test':
+      self.write('    compile_multilib: "first",')
+    self.write('}')
+
+  def dump_android_flags(self):
+    """Dump Android module flags property."""
     cfg_fmt = '"--cfg %s"'
     if self.cap_lints:
       allowed = '"--cap-lints ' + self.cap_lints + '"'
@@ -525,19 +607,25 @@
         self.write('    ],')
     else:
       self.dump_android_property_list('flags', cfg_fmt, self.cfgs)
+
+  def dump_edition_flags_libs(self):
+    if self.edition:
+      self.write('    edition: "' + self.edition + '",')
+    self.dump_android_property_list('features', '"%s"', self.features)
+    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)
-    self.write('}')
 
   def test_module_name(self):
     """Return a unique name for a test module."""
-    # root_pkg+'_tests_'+(crate_name|source_file_path)
-    suffix = self.crate_name
-    if not suffix:
-      suffix = re.sub('/', '_', re.sub('.rs$', '', self.main_src))
-    return self.root_pkg + '_tests_' + suffix
+    # root_pkg+(_host|_device) + '_test_'+source_file_name
+    suffix = re.sub('/', '_', re.sub('.rs$', '', self.main_src))
+    host_device = '_host'
+    if self.device_supported:
+      host_device = '_device'
+    return self.root_pkg + host_device + '_test_' + suffix
 
   def decide_module_type(self):
     # Use the first crate type for the default/first module.
@@ -549,8 +637,10 @@
     host = '' if self.device_supported else '_host'
     if crate_type == 'bin':  # rust_binary[_host]
       self.module_type = 'rust_binary' + host
-      self.stem = self.crate_name
-      self.module_name = altered_name(self.stem)
+      # In rare cases like protobuf-codegen, the output binary name must
+      # be renamed to use as a plugin for protoc.
+      self.stem = altered_stem(self.crate_name)
+      self.module_name = altered_name(self.crate_name)
     elif crate_type == 'lib':  # rust_library[_host]
       # TODO(chh): should this be rust_library[_host]?
       # Assuming that Cargo.toml do not use both 'lib' and 'rlib',
@@ -581,8 +671,7 @@
       self.stem = self.test_module_name()
       # self.stem will be changed after merging with other tests.
       # self.stem is NOT used for final test binary name.
-      # rust_test uses each source file base name as its output file name,
-      # unless crate_name is specified by user in Cargo.toml.
+      # rust_test uses each source file base name as part of output file name.
       # In do_merge, this function is called again, with a module_name.
       # We make sure that the module name is unique in each package.
       if self.module_name:
@@ -616,19 +705,23 @@
   def dump_android_core_properties(self):
     """Dump the module header, name, stem, etc."""
     self.write('    name: "' + self.module_name + '",')
+    # see properties shared by dump_defaults_module
+    if self.defaults:
+      self.write('    defaults: ["' + self.defaults + '"],')
     if self.stem != self.module_name:
       self.write('    stem: "' + self.stem + '",')
     if self.has_warning and not self.cap_lints:
       self.write('    // has rustc warnings')
     if self.host_supported and self.device_supported:
       self.write('    host_supported: true,')
-    self.write('    crate_name: "' + self.crate_name + '",')
+    if not self.defaults:
+      self.write('    crate_name: "' + self.crate_name + '",')
     if len(self.srcs) > 1:
       self.srcs = sorted(set(self.srcs))
       self.dump_android_property_list('srcs', '"%s"', self.srcs)
     else:
       self.write('    srcs: ["' + self.main_src + '"],')
-    if 'test' in self.crate_types:
+    if 'test' in self.crate_types and not self.defaults:
       # self.root_pkg can have multiple test modules, with different *_tests[n]
       # names, but their executables can all be installed under the same _tests
       # directory. When built from Cargo.toml, all tests should have different
@@ -660,8 +753,8 @@
         rust_libs += '        "' + altered_name('lib' + lib_name) + '",\n'
       elif lib.endswith('.so'):
         so_libs.append(lib_name)
-      else:
-        rust_libs += '        // ERROR: unknown type of lib ' + lib_name + '\n'
+      elif lib != 'proc_macro':  # --extern proc_macro is special and ignored
+        rust_libs += '        // ERROR: unknown type of lib ' + lib + '\n'
     if rust_libs:
       self.write('    rustlibs: [\n' + rust_libs + '    ],')
     # Are all dependent .so files proc_macros?
@@ -869,13 +962,16 @@
       self.cargo = ['clean'] + args.cargo
     else:
       self.cargo = ['clean', 'build']
+      if args.no_host:  # do not run "cargo build" for host
+        self.cargo = ['clean']
       default_target = '--target x86_64-unknown-linux-gnu'
       if args.device:
         self.cargo.append('build ' + default_target)
         if args.tests:
-          self.cargo.append('build --tests')
+          if not args.no_host:
+            self.cargo.append('build --tests')
           self.cargo.append('build --tests ' + default_target)
-      elif args.tests:
+      elif args.tests and not args.no_host:
         self.cargo.append('build --tests')
 
   def init_bp_file(self, name):
@@ -1138,6 +1234,17 @@
       default=False,
       help='run cargo also for a default device target')
   parser.add_argument(
+      '--no-host',
+      action='store_true',
+      default=False,
+      help='do not run cargo for the host; only for the device target')
+  parser.add_argument(
+      '--host-first-multilib',
+      action='store_true',
+      default=False,
+      help=('add a compile_multilib:"first" property ' +
+            'to Android.bp host modules.'))
+  parser.add_argument(
       '--features',
       type=str,
       help=('pass features to cargo build, ' +