Add options to work in a subdirectory.

* Problem to solve:
  When cargo2android.py is run under external/crosvm,
  it does not generate .bp file in some subdirectories like kvm.
* Use --add_workspace flag to append [workspace] in Cargo.toml
  temporarily to generate correct relative source file paths.
  Otherwise, the path will be based on parent/root package
  such as external/crosvm.
* Use --global_defaults=default_name flag to insert a global
  default module name like "crosvm_defaults" in every module.
* Use --no-subdir flag to generate .bp file in one directory,
  and skip all changes to subdirectories.
* Sort the option names so they show up in order with --help.
* Use relative path for local dependent packages.
* Example: run in external/crosvm with flags;
    --run --tests --dependencies --no-subdir
  fix-up external/crosvm/Android.bp file,
  and then run in each subdirectory with flags:
    --run --tests --dependencies --add_workspace
    --global_defaults=crosvm_defaults
* Add rename mapping:
    libminijail ==> libminijail_rust

Bug: 161716839
Test: regen .bp files in external/crosvm subdirectories
Test: make && atest -m -c --include-subdirs external/crosvm
Change-Id: I0c08d358cc2f88f66e99b59032613d2a5b4ea5eb
diff --git a/scripts/cargo2android.py b/scripts/cargo2android.py
index 2e93521..54d63ae 100755
--- a/scripts/cargo2android.py
+++ b/scripts/cargo2android.py
@@ -74,6 +74,7 @@
     'libbacktrace': 'libbacktrace_rust',
     'libgcc': 'libgcc_rust',
     'liblog': 'liblog_rust',
+    'libminijail': 'libminijail_rust',
     'libsync': 'libsync_rust',
     'libx86_64': 'libx86_64_rust',
     'protoc_gen_rust': 'protoc-gen-rust',
@@ -450,10 +451,13 @@
         self.main_src = re.sub(r'^\.\.\./github.com-[0-9a-f]*/', '.../',
                                self.main_src)
         self.find_cargo_dir()
-        if self.cargo_dir and not self.runner.args.onefile:
-          # Write to Android.bp in the subdirectory with Cargo.toml.
-          self.outf_name = self.cargo_dir + '/Android.bp'
-          self.main_src = self.main_src[len(self.cargo_dir) + 1:]
+        if self.cargo_dir:  # for a subdirectory
+          if self.runner.args.no_subdir:  # all .bp content to /dev/null
+            self.outf_name = '/dev/null'
+          elif not self.runner.args.onefile:
+            # Write to Android.bp in the subdirectory with Cargo.toml.
+            self.outf_name = self.cargo_dir + '/Android.bp'
+            self.main_src = self.main_src[len(self.cargo_dir) + 1:]
       else:
         self.errors += 'ERROR: unknown ' + arg + '\n'
       i += 1
@@ -501,6 +505,8 @@
     pkg = self.main_src
     if pkg.startswith('.../'):  # keep only the main package name
       pkg = re.sub('/.*', '', pkg[4:])
+    elif pkg.startswith('/'):  # use relative path for a local package
+      pkg = os.path.relpath(pkg)
     if not self.features:
       return pkg
     return pkg + ' "' + ','.join(self.features) + '"'
@@ -605,6 +611,8 @@
     self.defaults = name
     self.write('\nrust_defaults {')
     self.write('    name: "' + name + '",')
+    if self.runner.args.global_defaults:
+      self.write('    defaults: ["' + self.runner.args.global_defaults + '"],')
     self.write('    crate_name: "' + self.crate_name + '",')
     if len(self.srcs) == 1:  # only one source file; share it in defaults
       self.default_srcs = True
@@ -784,6 +792,8 @@
     # see properties shared by dump_defaults_module
     if self.defaults:
       self.write('    defaults: ["' + self.defaults + '"],')
+    elif self.runner.args.global_defaults:
+      self.write('    defaults: ["' + self.runner.args.global_defaults + '"],')
     if self.stem != self.module_name:
       self.write('    stem: "' + self.stem + '",')
     if self.has_warning and not self.cap_lints and not self.default_srcs:
@@ -1112,12 +1122,14 @@
         outf.write(ANDROID_BP_HEADER.format(args=' '.join(sys.argv[1:])))
 
   def dump_test_mapping_files(self):
+    """Dump all TEST_MAPPING files."""
     if self.dry_run:
       print('Dry-run skip dump of TEST_MAPPING')
     else:
       for bp_file_name in self.test_mappings:
-        name = os.path.join(os.path.dirname(bp_file_name), 'TEST_MAPPING')
-        self.test_mappings[bp_file_name].dump(name)
+        if bp_file_name != '/dev/null':
+          name = os.path.join(os.path.dirname(bp_file_name), 'TEST_MAPPING')
+          self.test_mappings[bp_file_name].dump(name)
     return self
 
   def add_test(self, bp_file_name, test_name, host):
@@ -1163,16 +1175,31 @@
     """Calls cargo -v and save its output to ./cargo.out."""
     if self.skip_cargo:
       return self
-    cargo = './Cargo.toml'
-    if not os.access(cargo, os.R_OK):
-      print('ERROR: Cannot find or read', cargo)
+    cargo_toml = './Cargo.toml'
+    cargo_out = './cargo.out'
+    if not os.access(cargo_toml, os.R_OK):
+      print('ERROR: Cannot find or read', cargo_toml)
       return self
-    if not self.dry_run and os.path.exists('cargo.out'):
-      os.remove('cargo.out')
-    cmd_tail = ' --target-dir ' + TARGET_TMP + ' >> cargo.out 2>&1'
+    if not self.dry_run and os.path.exists(cargo_out):
+      os.remove(cargo_out)
+    cmd_tail = ' --target-dir ' + TARGET_TMP + ' >> ' + cargo_out + ' 2>&1'
     # set up search PATH for cargo to find the correct rustc
     saved_path = os.environ['PATH']
     os.environ['PATH'] = os.path.dirname(self.cargo_path) + ':' + saved_path
+    # Add [workspace] to Cargo.toml if it is not there.
+    added_workspace = False
+    if self.args.add_workspace:
+      with open(cargo_toml, 'r') as in_file:
+        cargo_toml_lines = in_file.readlines()
+      found_workspace = '[workspace]\n' in cargo_toml_lines
+      if found_workspace:
+        print('### WARNING: found [workspace] in Cargo.toml')
+      else:
+        with open(cargo_toml, 'a') as out_file:
+          out_file.write('[workspace]\n')
+          added_workspace = True
+          if self.args.verbose:
+            print('### INFO: added [workspace] to Cargo.toml')
     for c in self.cargo:
       features = ''
       if c != 'clean':
@@ -1190,9 +1217,14 @@
       else:
         if self.args.verbose:
           print('Running:', cmd)
-        with open('cargo.out', 'a') as cargo_out:
-          cargo_out.write('### Running: ' + cmd + '\n')
+        with open(cargo_out, 'a') as out_file:
+          out_file.write('### Running: ' + cmd + '\n')
         os.system(cmd)
+    if added_workspace:  # restore original Cargo.toml
+      with open(cargo_toml, 'w') as out_file:
+        out_file.writelines(cargo_toml_lines)
+      if self.args.verbose:
+        print('### INFO: restored original Cargo.toml')
     os.environ['PATH'] = saved_path
     return self
 
@@ -1368,6 +1400,14 @@
   """Parse main arguments."""
   parser = argparse.ArgumentParser('cargo2android')
   parser.add_argument(
+      '--add_workspace',
+      action='store_true',
+      default=False,
+      help=('append [workspace] to Cargo.toml before calling cargo,' +
+            ' to treat current directory as root of package source;' +
+            ' otherwise the relative source file path in generated' +
+            ' .bp file will be from the parent directory.'))
+  parser.add_argument(
       '--cargo',
       action='append',
       metavar='args_string',
@@ -1393,10 +1433,14 @@
       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')
+      '--features',
+      type=str,
+      help=('pass features to cargo build, ' +
+            'empty string means no default features'))
+  parser.add_argument(
+      '--global_defaults',
+      type=str,
+      help='add a defaults name to every module')
   parser.add_argument(
       '--host-first-multilib',
       action='store_true',
@@ -1404,10 +1448,15 @@
       help=('add a compile_multilib:"first" property ' +
             'to Android.bp host modules.'))
   parser.add_argument(
-      '--features',
-      type=str,
-      help=('pass features to cargo build, ' +
-            'empty string means no default features'))
+      '--no-host',
+      action='store_true',
+      default=False,
+      help='do not run cargo for the host; only for the device target')
+  parser.add_argument(
+      '--no-subdir',
+      action='store_true',
+      default=False,
+      help='do not output anything for sub-directories')
   parser.add_argument(
       '--onefile',
       action='store_true',