Handle -Cflag syntax and use prebuilt cargo.

* Accept both "-C flag" and "-Cflag" syntax.
  * Newer cargo uses "-Cembed-bitcode=no".
  * Some -C flags are filtered out, not used in .bp file,
    because they are meaningless in .bp file build system.
  * Remaining -C flags are passed to rustc in .bp files,
    which was missing before this change.
* Look up cargo in the prebuilts directory.
  * Now limit this script to run only on linux.
  * Try to find cargo/rust version from
    build/soong/rust/config/global.go first.
  * Otherwise, use the latest (largest) version in prebuilt.
* Add a new --cargo_bin flag to use any user-selected cargo.
* Add the selected cargo directory to PATH before calling cargo,
  so it can find rustc in the same directory.

Bug: 161825397
Bug: 161927172
Test: regenerate and check .bp files in external/rust/crates
Change-Id: Ica46f536c2b37b62238d1245ced59685deebad33
diff --git a/scripts/cargo2android.py b/scripts/cargo2android.py
index 4a05eeb..2e93521 100755
--- a/scripts/cargo2android.py
+++ b/scripts/cargo2android.py
@@ -60,6 +60,7 @@
 import argparse
 import os
 import os.path
+import platform
 import re
 import sys
 
@@ -363,6 +364,16 @@
           return
         dir_name = os.path.dirname(dir_name)
 
+  def add_codegens_flag(self, flag):
+    # ignore options not used in Android
+    # 'prefer-dynamic' does not work with common flag -C lto
+    if not (flag.startswith('debuginfo=') or
+            flag.startswith('extra-filename=') or
+            flag.startswith('incremental=') or
+            flag.startswith('metadata=') or
+            flag == 'prefer-dynamic'):
+      self.codegens.append(flag)
+
   def parse(self, line_num, line):
     """Find important rustc arguments to convert to Android.bp properties."""
     self.line_num = line_num
@@ -398,12 +409,11 @@
         self.core_externs.append(re.sub(' = .*', '', extern_names))
       elif arg == '-C':  # codegen options
         i += 1
-        # ignore options not used in Android
-        if not (args[i].startswith('debuginfo=') or
-                args[i].startswith('extra-filename=') or
-                args[i].startswith('incremental=') or
-                args[i].startswith('metadata=')):
-          self.codegens.append(args[i])
+        self.add_codegens_flag(args[i])
+      elif arg.startswith('-C'):
+        # cargo has been passing "-C <xyz>" flag to rustc,
+        # but newer cargo could pass '-Cembed-bitcode=no' to rustc.
+        self.add_codegens_flag(arg[2:])
       elif arg == '--cap-lints':
         i += 1
         self.cap_lints = args[i]
@@ -660,17 +670,16 @@
 
   def dump_android_flags(self):
     """Dump Android module flags property."""
-    cfg_fmt = '"--cfg %s"'
+    if not self.cfgs and not self.codegens and not self.cap_lints:
+      return
+    self.write('    flags: [')
     if self.cap_lints:
-      allowed = '"--cap-lints ' + self.cap_lints + '"'
-      if not self.cfgs:
-        self.write('    flags: [' + allowed + '],')
-      else:
-        self.write('    flags: [\n       ' + allowed + ',')
-        self.dump_android_property_list_items(cfg_fmt, self.cfgs)
-        self.write('    ],')
-    else:
-      self.dump_android_property_list('flags', cfg_fmt, self.cfgs)
+      self.write('        "--cap-lints ' + self.cap_lints + '",')
+    cfg_fmt = '"--cfg %s"'
+    codegens_fmt = '"-C %s"'
+    self.dump_android_property_list_items(cfg_fmt, self.cfgs)
+    self.dump_android_property_list_items(codegens_fmt, self.codegens)
+    self.write('    ],')
 
   def dump_edition_flags_libs(self):
     if self.edition:
@@ -1013,6 +1022,7 @@
     self.args = args
     self.dry_run = not args.run
     self.skip_cargo = args.skipcargo
+    self.cargo_path = './cargo'  # path to cargo, will be set later
     # All cc/ar objects, crates, dependencies, and warning files
     self.cc_objects = list()
     self.pkg_obj2cc = {}
@@ -1025,6 +1035,7 @@
     self.name_owners = {}
     # Save and dump all errors from cargo to Android.bp.
     self.errors = ''
+    self.setup_cargo_path()
     # Default action is cargo clean, followed by build or user given actions.
     if args.cargo:
       self.cargo = ['clean'] + args.cargo
@@ -1042,6 +1053,58 @@
       elif args.tests and not args.no_host:
         self.cargo.append('build --tests')
 
+  def setup_cargo_path(self):
+    """Find cargo in the --cargo_bin or prebuilt rust bin directory."""
+    if self.args.cargo_bin:
+      self.cargo_path = os.path.join(self.args.cargo_bin, 'cargo')
+      if not os.path.isfile(self.cargo_path):
+        sys.exit('ERROR: cannot find cargo in ' + self.args.cargo_bin)
+      print('WARNING: using cargo in ' + self.args.cargo_bin)
+      return
+    # We have only tested this on Linux.
+    if platform.system() != 'Linux':
+      sys.exit('ERROR: this script has only been tested on Linux with cargo.')
+    # Assuming that this script is in development/scripts.
+    my_dir = os.path.dirname(os.path.abspath(__file__))
+    linux_dir = os.path.join(my_dir, '..', '..',
+                             'prebuilts', 'rust', 'linux-x86')
+    if not os.path.isdir(linux_dir):
+      sys.exit('ERROR: cannot find directory ' + linux_dir)
+    rust_version = self.find_rust_version(my_dir, linux_dir)
+    cargo_bin = os.path.join(linux_dir, rust_version, 'bin')
+    self.cargo_path = os.path.join(cargo_bin, 'cargo')
+    if not os.path.isfile(self.cargo_path):
+      sys.exit('ERROR: cannot find cargo in ' + cargo_bin
+               + '; please try --cargo_bin= flag.')
+    return
+
+  def find_rust_version(self, my_dir, linux_dir):
+    """Use my script directory, find prebuilt rust version."""
+    # First look up build/soong/rust/config/global.go.
+    path2global = os.path.join(my_dir, '..', '..',
+                               'build', 'soong', 'rust', 'config', 'global.go')
+    if os.path.isfile(path2global):
+      # try to find: RustDefaultVersion = "1.44.0"
+      version_pat = re.compile(
+          r'\s*RustDefaultVersion\s*=\s*"([0-9]+\.[0-9]+\.[0-9]+)".*$')
+      with open(path2global, 'r') as inf:
+        for line in inf:
+          result = version_pat.match(line)
+          if result:
+            return result.group(1)
+    print('WARNING: cannot find RustDefaultVersion in ' + path2global)
+    # Otherwise, find the newest (largest) version number in linux_dir.
+    rust_version = (0, 0, 0)  # the prebuilt version to use
+    version_pat = re.compile(r'([0-9]+)\.([0-9]+)\.([0-9]+)$')
+    for dir_name in os.listdir(linux_dir):
+      result = version_pat.match(dir_name)
+      if not result:
+        continue
+      version = (result.group(1), result.group(2), result.group(3))
+      if version > rust_version:
+        rust_version = version
+    return '.'.join(rust_version)
+
   def init_bp_file(self, name):
     if name not in self.bp_files:
       self.bp_files.add(name)
@@ -1107,6 +1170,9 @@
     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
     for c in self.cargo:
       features = ''
       if c != 'clean':
@@ -1114,7 +1180,8 @@
           features = ' --no-default-features'
         if self.args.features:
           features += ' --features ' + self.args.features
-      cmd = 'cargo -vv ' if self.args.vv else 'cargo -v '
+      cmd_v_flag = ' -vv ' if self.args.vv else ' -v '
+      cmd = self.cargo_path + cmd_v_flag
       cmd += c + features + cmd_tail
       if self.args.rustflags and c != 'clean':
         cmd = 'RUSTFLAGS="' + self.args.rustflags + '" ' + cmd
@@ -1126,6 +1193,7 @@
         with open('cargo.out', 'a') as cargo_out:
           cargo_out.write('### Running: ' + cmd + '\n')
         os.system(cmd)
+    os.environ['PATH'] = saved_path
     return self
 
   def dump_dependencies(self):
@@ -1306,6 +1374,10 @@
       help=('extra cargo build -v args in a string, ' +
             'each --cargo flag calls cargo build -v once'))
   parser.add_argument(
+      '--cargo_bin',
+      type=str,
+      help='use cargo in the cargo_bin directory instead of the prebuilt one')
+  parser.add_argument(
       '--debug',
       action='store_true',
       default=False,