• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3# Copyright (C) 2019 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16"""Call cargo -v, parse its output, and generate Android.bp.
17
18Usage: Run this script in a crate workspace root directory.
19The Cargo.toml file should work at least for the host platform.
20
21(1) Without other flags, "cargo2android.py --run"
22    calls cargo clean, calls cargo build -v, and generates Android.bp.
23    The cargo build only generates crates for the host,
24    without test crates.
25
26(2) To build crates for both host and device in Android.bp, use the
27    --device flag, for example:
28    cargo2android.py --run --device
29
30    Note that cargo build is only called once with the default target
31    x86_64-unknown-linux-gnu.
32
33(3) To build default and test crates, for host and device, use both
34    --device and --tests flags:
35    cargo2android.py --run --device --tests
36
37    This is equivalent to using the --cargo flag to add extra builds:
38    cargo2android.py --run
39      --cargo "build --target x86_64-unknown-linux-gnu"
40      --cargo "build --tests --target x86_64-unknown-linux-gnu"
41
42If there are rustc warning messages, this script will add
43a warning comment to the owner crate module in Android.bp.
44"""
45
46from __future__ import print_function
47
48import argparse
49import glob
50import json
51import os
52import os.path
53import platform
54import re
55import shutil
56import subprocess
57import sys
58
59# Some Rust packages include extra unwanted crates.
60# This set contains all such excluded crate names.
61EXCLUDED_CRATES = set(['protobuf_bin_gen_rust_do_not_use'])
62
63RENAME_MAP = {
64    # This map includes all changes to the default rust module names
65    # to resolve name conflicts, avoid confusion, or work as plugin.
66    'libash': 'libash_rust',
67    'libbacktrace': 'libbacktrace_rust',
68    'libbase': 'libbase_rust',
69    'libbase64': 'libbase64_rust',
70    'libfuse': 'libfuse_rust',
71    'libgcc': 'libgcc_rust',
72    'liblog': 'liblog_rust',
73    'libminijail': 'libminijail_rust',
74    'libsync': 'libsync_rust',
75    'libx86_64': 'libx86_64_rust',
76    'libxml': 'libxml_rust',
77    'protoc_gen_rust': 'protoc-gen-rust',
78}
79
80RENAME_STEM_MAP = {
81    # This map includes all changes to the default rust module stem names,
82    # which is used for output files when different from the module name.
83    'protoc_gen_rust': 'protoc-gen-rust',
84}
85
86RENAME_DEFAULTS_MAP = {
87    # This map includes all changes to the default prefix of rust_default
88    # module names, to avoid conflict with existing Android modules.
89    'libc': 'rust_libc',
90}
91
92# Header added to all generated Android.bp files.
93ANDROID_BP_HEADER = (
94    '// This file is generated by cargo2android.py {args}.\n' +
95    '// Do not modify this file as changes will be overridden on upgrade.\n')
96
97CARGO_OUT = 'cargo.out'  # Name of file to keep cargo build -v output.
98
99# This should be kept in sync with tools/external_updater/crates_updater.py.
100ERRORS_LINE = 'Errors in ' + CARGO_OUT + ':'
101
102TARGET_TMP = 'target.tmp'  # Name of temporary output directory.
103
104# Message to be displayed when this script is called without the --run flag.
105DRY_RUN_NOTE = (
106    'Dry-run: This script uses ./' + TARGET_TMP + ' for output directory,\n' +
107    'runs cargo clean, runs cargo build -v, saves output to ./cargo.out,\n' +
108    'and writes to Android.bp in the current and subdirectories.\n\n' +
109    'To do do all of the above, use the --run flag.\n' +
110    'See --help for other flags, and more usage notes in this script.\n')
111
112# Cargo -v output of a call to rustc.
113RUSTC_PAT = re.compile('^ +Running `rustc (.*)`$')
114
115# Cargo -vv output of a call to rustc could be split into multiple lines.
116# Assume that the first line will contain some CARGO_* env definition.
117RUSTC_VV_PAT = re.compile('^ +Running `.*CARGO_.*=.*$')
118# The combined -vv output rustc command line pattern.
119RUSTC_VV_CMD_ARGS = re.compile('^ *Running `.*CARGO_.*=.* rustc (.*)`$')
120
121# Cargo -vv output of a "cc" or "ar" command; all in one line.
122CC_AR_VV_PAT = re.compile(r'^\[([^ ]*)[^\]]*\] running:? "(cc|ar)" (.*)$')
123# Some package, such as ring-0.13.5, has pattern '... running "cc"'.
124
125# Rustc output of file location path pattern for a warning message.
126WARNING_FILE_PAT = re.compile('^ *--> ([^:]*):[0-9]+')
127
128# cargo test --list output of the start of running a binary.
129CARGO_TEST_LIST_START_PAT = re.compile('^\s*Running (.*) \(.*\)$')
130
131# cargo test --list output of the end of running a binary.
132CARGO_TEST_LIST_END_PAT = re.compile('^(\d+) tests, (\d+) benchmarks$')
133
134CARGO2ANDROID_RUNNING_PAT = re.compile('^### Running: .*$')
135
136# Rust package name with suffix -d1.d2.d3(+.*)?.
137VERSION_SUFFIX_PAT = re.compile(r'^(.*)-[0-9]+\.[0-9]+\.[0-9]+(?:\+.*)?$')
138
139# Crate types corresponding to a C ABI library
140C_LIBRARY_CRATE_TYPES = ['staticlib', 'cdylib']
141# Crate types corresponding to a Rust ABI library
142RUST_LIBRARY_CRATE_TYPES = ['lib', 'rlib', 'dylib']
143# Crate types corresponding to a library
144LIBRARY_CRATE_TYPES = C_LIBRARY_CRATE_TYPES + RUST_LIBRARY_CRATE_TYPES
145
146def altered_name(name):
147  return RENAME_MAP[name] if (name in RENAME_MAP) else name
148
149
150def altered_stem(name):
151  return RENAME_STEM_MAP[name] if (name in RENAME_STEM_MAP) else name
152
153
154def altered_defaults(name):
155  return RENAME_DEFAULTS_MAP[name] if (name in RENAME_DEFAULTS_MAP) else name
156
157
158def is_build_crate_name(name):
159  # We added special prefix to build script crate names.
160  return name.startswith('build_script_')
161
162
163def is_dependent_file_path(path):
164  # Absolute or dependent '.../' paths are not main files of this crate.
165  return path.startswith('/') or path.startswith('.../')
166
167
168def get_module_name(crate):  # to sort crates in a list
169  return crate.module_name
170
171
172def pkg2crate_name(s):
173  return s.replace('-', '_').replace('.', '_')
174
175
176def file_base_name(path):
177  return os.path.splitext(os.path.basename(path))[0]
178
179
180def test_base_name(path):
181  return pkg2crate_name(file_base_name(path))
182
183
184def unquote(s):  # remove quotes around str
185  if s and len(s) > 1 and s[0] == '"' and s[-1] == '"':
186    return s[1:-1]
187  return s
188
189
190def remove_version_suffix(s):  # remove -d1.d2.d3 suffix
191  if VERSION_SUFFIX_PAT.match(s):
192    return VERSION_SUFFIX_PAT.match(s).group(1)
193  return s
194
195
196def short_out_name(pkg, s):  # replace /.../pkg-*/out/* with .../out/*
197  return re.sub('^/.*/' + pkg + '-[0-9a-f]*/out/', '.../out/', s)
198
199
200def escape_quotes(s):  # replace '"' with '\\"'
201  return s.replace('"', '\\"')
202
203
204class Crate(object):
205  """Information of a Rust crate to collect/emit for an Android.bp module."""
206
207  def __init__(self, runner, outf_name):
208    # Remembered global runner and its members.
209    self.runner = runner
210    self.debug = runner.args.debug
211    self.cargo_dir = ''  # directory of my Cargo.toml
212    self.outf_name = outf_name  # path to Android.bp
213    self.outf = None  # open file handle of outf_name during dump*
214    # Variants/results that could be merged from multiple rustc lines.
215    self.host_supported = False
216    self.device_supported = False
217    self.has_warning = False
218    # Android module properties derived from rustc parameters.
219    self.module_name = ''  # unique in Android build system
220    self.module_type = ''  # rust_{binary,library,test}[_host] etc.
221    self.defaults = ''  # rust_defaults used by rust_test* modules
222    self.default_srcs = False  # use 'srcs' defined in self.defaults
223    self.root_pkg = ''  # parent package name of a sub/test packge, from -L
224    self.srcs = list()  # main_src or merged multiple source files
225    self.stem = ''  # real base name of output file
226    # Kept parsed status
227    self.errors = ''  # all errors found during parsing
228    self.line_num = 1  # runner told input source line number
229    self.line = ''  # original rustc command line parameters
230    # Parameters collected from rustc command line.
231    self.crate_name = ''  # follows --crate-name
232    self.main_src = ''  # follows crate_name parameter, shortened
233    self.crate_types = list()  # follows --crate-type
234    self.cfgs = list()  # follows --cfg, without feature= prefix
235    self.features = list()  # follows --cfg, name in 'feature="..."'
236    self.codegens = list()  # follows -C, some ignored
237    self.externs = list()  # follows --extern
238    self.core_externs = list()  # first part of self.externs elements
239    self.static_libs = list()  # e.g.  -l static=host_cpuid
240    self.shared_libs = list()  # e.g.  -l dylib=wayland-client, -l z
241    self.cap_lints = ''  # follows --cap-lints
242    self.emit_list = ''  # e.g., --emit=dep-info,metadata,link
243    self.edition = '2015'  # rustc default, e.g., --edition=2018
244    self.target = ''  # follows --target
245    self.cargo_env_compat = True
246    self.cargo_pkg_version = ''  # value extracted from Cargo.toml version field
247
248  def write(self, s):
249    # convenient way to output one line at a time with EOL.
250    self.outf.write(s + '\n')
251
252  def same_flags(self, other):
253    # host_supported, device_supported, has_warning are not compared but merged
254    # target is not compared, to merge different target/host modules
255    # externs is not compared; only core_externs is compared
256    return (not self.errors and not other.errors and
257            self.edition == other.edition and
258            self.cap_lints == other.cap_lints and
259            self.emit_list == other.emit_list and
260            self.core_externs == other.core_externs and
261            self.codegens == other.codegens and
262            self.features == other.features and
263            self.static_libs == other.static_libs and
264            self.shared_libs == other.shared_libs and self.cfgs == other.cfgs)
265
266  def merge_host_device(self, other):
267    """Returns true if attributes are the same except host/device support."""
268    return (self.crate_name == other.crate_name and
269            self.crate_types == other.crate_types and
270            self.main_src == other.main_src and
271            # before merge, each test module has an unique module name and stem
272            (self.stem == other.stem or self.crate_types == ['test']) and
273            self.root_pkg == other.root_pkg and not self.skip_crate() and
274            self.same_flags(other))
275
276  def merge_test(self, other):
277    """Returns true if self and other are tests of same root_pkg."""
278    # Before merger, each test has its own crate_name.
279    # A merged test uses its source file base name as output file name,
280    # so a test is mergeable only if its base name equals to its crate name.
281    return (self.crate_types == other.crate_types and
282            self.crate_types == ['test'] and self.root_pkg == other.root_pkg and
283            not self.skip_crate() and
284            other.crate_name == test_base_name(other.main_src) and
285            (len(self.srcs) > 1 or
286             (self.crate_name == test_base_name(self.main_src)) and
287             self.host_supported == other.host_supported and
288             self.device_supported == other.device_supported) and
289            self.same_flags(other))
290
291  def merge(self, other, outf_name):
292    """Try to merge crate into self."""
293    # Cargo build --tests could recompile a library for tests.
294    # We need to merge such duplicated calls to rustc, with
295    # the algorithm in merge_host_device.
296    should_merge_host_device = self.merge_host_device(other)
297    should_merge_test = False
298    if not should_merge_host_device:
299      should_merge_test = self.merge_test(other)
300    if should_merge_host_device or should_merge_test:
301      self.runner.init_bp_file(outf_name)
302      with open(outf_name, 'a') as outf:  # to write debug info
303        self.outf = outf
304        other.outf = outf
305        self.do_merge(other, should_merge_test)
306      return True
307    return False
308
309  def do_merge(self, other, should_merge_test):
310    """Merge attributes of other to self."""
311    if self.debug:
312      self.write('\n// Before merge definition (1):')
313      self.dump_debug_info()
314      self.write('\n// Before merge definition (2):')
315      other.dump_debug_info()
316    # Merge properties of other to self.
317    self.has_warning = self.has_warning or other.has_warning
318    if not self.target:  # okay to keep only the first target triple
319      self.target = other.target
320    # decide_module_type sets up default self.stem,
321    # which can be changed if self is a merged test module.
322    self.decide_module_type()
323    if should_merge_test:
324      if (self.runner.should_ignore_test(self.main_src)
325          and not self.runner.should_ignore_test(other.main_src)):
326        self.main_src = other.main_src
327      self.srcs.append(other.main_src)
328      # use a short unique name as the merged module name.
329      prefix = self.root_pkg + '_tests'
330      self.module_name = self.runner.claim_module_name(prefix, self, 0)
331      self.stem = self.module_name
332      # This normalized root_pkg name although might be the same
333      # as other module's crate_name, it is not actually used for
334      # output file name. A merged test module always have multiple
335      # source files and each source file base name is used as
336      # its output file name.
337      self.crate_name = pkg2crate_name(self.root_pkg)
338    if self.debug:
339      self.write('\n// After merge definition (1):')
340      self.dump_debug_info()
341
342  def find_cargo_dir(self):
343    """Deepest directory with Cargo.toml and contains the main_src."""
344    if not is_dependent_file_path(self.main_src):
345      dir_name = os.path.dirname(self.main_src)
346      while dir_name:
347        if os.path.exists(dir_name + '/Cargo.toml'):
348          self.cargo_dir = dir_name
349          return
350        dir_name = os.path.dirname(dir_name)
351
352  def add_codegens_flag(self, flag):
353    """Ignore options not used in Android."""
354    # 'prefer-dynamic' does not work with common flag -C lto
355    # 'embed-bitcode' is ignored; we might control LTO with other .bp flag
356    # 'codegen-units' is set in Android global config or by default
357    if not (flag.startswith('codegen-units=') or
358            flag.startswith('debuginfo=') or
359            flag.startswith('embed-bitcode=') or
360            flag.startswith('extra-filename=') or
361            flag.startswith('incremental=') or
362            flag.startswith('metadata=') or
363            flag == 'prefer-dynamic'):
364      self.codegens.append(flag)
365
366  def parse(self, line_num, line):
367    """Find important rustc arguments to convert to Android.bp properties."""
368    self.line_num = line_num
369    self.line = line
370    args = line.split()  # Loop through every argument of rustc.
371    i = 0
372    while i < len(args):
373      arg = args[i]
374      if arg == '--crate-name':
375        i += 1
376        self.crate_name = args[i]
377      elif arg == '--crate-type':
378        i += 1
379        # cargo calls rustc with multiple --crate-type flags.
380        # rustc can accept:
381        #   --crate-type [bin|lib|rlib|dylib|cdylib|staticlib|proc-macro]
382        self.crate_types.append(args[i])
383      elif arg == '--test':
384        self.crate_types.append('test')
385      elif arg == '--target':
386        i += 1
387        self.target = args[i]
388      elif arg == '--cfg':
389        i += 1
390        if args[i].startswith('\'feature='):
391          self.features.append(unquote(args[i].replace('\'feature=', '')[:-1]))
392        else:
393          self.cfgs.append(args[i])
394      elif arg == '--extern':
395        i += 1
396        extern_names = re.sub('=/[^ ]*/deps/', ' = ', args[i])
397        self.externs.append(extern_names)
398        self.core_externs.append(re.sub(' = .*', '', extern_names))
399      elif arg == '-C':  # codegen options
400        i += 1
401        self.add_codegens_flag(args[i])
402      elif arg.startswith('-C'):
403        # cargo has been passing "-C <xyz>" flag to rustc,
404        # but newer cargo could pass '-Cembed-bitcode=no' to rustc.
405        self.add_codegens_flag(arg[2:])
406      elif arg == '--cap-lints':
407        i += 1
408        self.cap_lints = args[i]
409      elif arg == '-L':
410        i += 1
411        if args[i].startswith('dependency=') and args[i].endswith('/deps'):
412          if '/' + TARGET_TMP + '/' in args[i]:
413            self.root_pkg = re.sub(
414                '^.*/', '', re.sub('/' + TARGET_TMP + '/.*/deps$', '', args[i]))
415          else:
416            self.root_pkg = re.sub('^.*/', '',
417                                   re.sub('/[^/]+/[^/]+/deps$', '', args[i]))
418          self.root_pkg = remove_version_suffix(self.root_pkg)
419      elif arg == '-l':
420        i += 1
421        if args[i].startswith('static='):
422          self.static_libs.append(re.sub('static=', '', args[i]))
423        elif args[i].startswith('dylib='):
424          self.shared_libs.append(re.sub('dylib=', '', args[i]))
425        else:
426          self.shared_libs.append(args[i])
427      elif arg == '--out-dir' or arg == '--color':  # ignored
428        i += 1
429      elif arg.startswith('--error-format=') or arg.startswith('--json='):
430        _ = arg  # ignored
431      elif arg.startswith('--emit='):
432        self.emit_list = arg.replace('--emit=', '')
433      elif arg.startswith('--edition='):
434        self.edition = arg.replace('--edition=', '')
435      elif arg.startswith('\'-Aclippy'):
436        # TODO: Consider storing these to include in the Android.bp.
437        _ = arg # ignored
438      elif not arg.startswith('-'):
439        # shorten imported crate main source paths like $HOME/.cargo/
440        # registry/src/github.com-1ecc6299db9ec823/memchr-2.3.3/src/lib.rs
441        self.main_src = re.sub(r'^/[^ ]*/registry/src/', '.../', args[i])
442        self.main_src = re.sub(r'^\.\.\./github.com-[0-9a-f]*/', '.../',
443                               self.main_src)
444        self.find_cargo_dir()
445        if self.cargo_dir:  # for a subdirectory
446          if self.runner.args.no_subdir:  # all .bp content to /dev/null
447            self.outf_name = '/dev/null'
448          elif not self.runner.args.onefile:
449            # Write to Android.bp in the subdirectory with Cargo.toml.
450            self.outf_name = self.cargo_dir + '/Android.bp'
451            self.main_src = self.main_src[len(self.cargo_dir) + 1:]
452
453      else:
454        self.errors += 'ERROR: unknown ' + arg + '\n'
455      i += 1
456    if not self.crate_name:
457      self.errors += 'ERROR: missing --crate-name\n'
458    if not self.main_src:
459      self.errors += 'ERROR: missing main source file\n'
460    else:
461      self.srcs.append(self.main_src)
462    if not self.crate_types:
463      # Treat "--cfg test" as "--test"
464      if 'test' in self.cfgs:
465        self.crate_types.append('test')
466      else:
467        self.errors += 'ERROR: missing --crate-type or --test\n'
468    elif len(self.crate_types) > 1:
469      if 'test' in self.crate_types:
470        self.errors += 'ERROR: cannot handle both --crate-type and --test\n'
471      if 'lib' in self.crate_types and 'rlib' in self.crate_types:
472        self.errors += 'ERROR: cannot generate both lib and rlib crate types\n'
473    if not self.root_pkg:
474      self.root_pkg = self.crate_name
475
476    # get the package version from running cargo metadata
477    if not self.runner.args.no_pkg_vers and not self.skip_crate():
478        self.get_pkg_version()
479
480    self.device_supported = self.runner.args.device
481    self.host_supported = not self.runner.args.no_host
482    self.cfgs = sorted(set(self.cfgs))
483    self.features = sorted(set(self.features))
484    self.codegens = sorted(set(self.codegens))
485    self.externs = sorted(set(self.externs))
486    self.core_externs = sorted(set(self.core_externs))
487    self.static_libs = sorted(set(self.static_libs))
488    self.shared_libs = sorted(set(self.shared_libs))
489    self.crate_types = sorted(set(self.crate_types))
490    self.decide_module_type()
491    self.module_name = altered_name(self.stem)
492    return self
493
494  def get_pkg_version(self):
495    """Attempt to retrieve the package version from the Cargo.toml
496
497    If there is only one package, use its version. Otherwise, try to
498    match the emitted `--crate_name` arg against the package name.
499
500    This may fail in cases where multiple packages are defined (workspaces)
501    and where the package name does not match the emitted crate_name
502    (e.g. [lib.name] is set).
503    """
504    cargo_metadata = subprocess.run([self.runner.cargo_path, 'metadata', '--no-deps',
505                                     '--format-version', '1'],
506                                    cwd=os.path.abspath(self.cargo_dir),
507                                    stdout=subprocess.PIPE)
508    if cargo_metadata.returncode:
509        self.errors += ('ERROR: unable to get cargo metadata for package version; ' +
510                'return code ' + cargo_metadata.returncode + '\n')
511    else:
512        metadata_json = json.loads(cargo_metadata.stdout)
513        if len(metadata_json['packages']) > 1:
514            for package in metadata_json['packages']:
515                # package names may contain '-', but is changed to '_' in the crate_name
516                if package['name'].replace('-','_') == self.crate_name:
517                    self.cargo_pkg_version = package['version']
518                    break
519        else:
520            self.cargo_pkg_version = metadata_json['packages'][0]['version']
521
522        if not self.cargo_pkg_version:
523            self.errors += ('ERROR: Unable to retrieve package version; ' +
524                'to disable, run with arg "--no-pkg-vers"\n')
525
526  def dump_line(self):
527    self.write('\n// Line ' + str(self.line_num) + ' ' + self.line)
528
529  def feature_list(self):
530    """Return a string of main_src + "feature_list"."""
531    pkg = self.main_src
532    if pkg.startswith('.../'):  # keep only the main package name
533      pkg = re.sub('/.*', '', pkg[4:])
534    elif pkg.startswith('/'):  # use relative path for a local package
535      pkg = os.path.relpath(pkg)
536    if not self.features:
537      return pkg
538    return pkg + ' "' + ','.join(self.features) + '"'
539
540  def dump_skip_crate(self, kind):
541    if self.debug:
542      self.write('\n// IGNORED: ' + kind + ' ' + self.main_src)
543    return self
544
545  def skip_crate(self):
546    """Return crate_name or a message if this crate should be skipped."""
547    if (is_build_crate_name(self.crate_name) or
548        self.crate_name in EXCLUDED_CRATES):
549      return self.crate_name
550    if is_dependent_file_path(self.main_src):
551      return 'dependent crate'
552    return ''
553
554  def dump(self):
555    """Dump all error/debug/module code to the output .bp file."""
556    self.runner.init_bp_file(self.outf_name)
557    with open(self.outf_name, 'a') as outf:
558      self.outf = outf
559      if self.errors:
560        self.dump_line()
561        self.write(self.errors)
562      elif self.skip_crate():
563        self.dump_skip_crate(self.skip_crate())
564      else:
565        if self.debug:
566          self.dump_debug_info()
567        self.dump_android_module()
568
569  def dump_debug_info(self):
570    """Dump parsed data, when cargo2android is called with --debug."""
571
572    def dump(name, value):
573      self.write('//%12s = %s' % (name, value))
574
575    def opt_dump(name, value):
576      if value:
577        dump(name, value)
578
579    def dump_list(fmt, values):
580      for v in values:
581        self.write(fmt % v)
582
583    self.dump_line()
584    dump('module_name', self.module_name)
585    dump('crate_name', self.crate_name)
586    dump('crate_types', self.crate_types)
587    dump('main_src', self.main_src)
588    dump('has_warning', self.has_warning)
589    dump('for_host', self.host_supported)
590    dump('for_device', self.device_supported)
591    dump('module_type', self.module_type)
592    opt_dump('target', self.target)
593    opt_dump('edition', self.edition)
594    opt_dump('emit_list', self.emit_list)
595    opt_dump('cap_lints', self.cap_lints)
596    dump_list('//         cfg = %s', self.cfgs)
597    dump_list('//         cfg = \'feature "%s"\'', self.features)
598    # TODO(chh): escape quotes in self.features, but not in other dump_list
599    dump_list('//     codegen = %s', self.codegens)
600    dump_list('//     externs = %s', self.externs)
601    dump_list('//   -l static = %s', self.static_libs)
602    dump_list('//  -l (dylib) = %s', self.shared_libs)
603
604  def dump_android_module(self):
605    """Dump one or more Android module definition, depending on crate_types."""
606    if len(self.crate_types) == 1:
607      self.dump_single_type_android_module()
608      return
609    if 'test' in self.crate_types:
610      self.write('\nERROR: multiple crate types cannot include test type')
611      return
612    # Dump one Android module per crate_type.
613    for crate_type in self.crate_types:
614      self.decide_one_module_type(crate_type)
615      self.dump_one_android_module(crate_type)
616
617  def build_default_name(self):
618    """Return a short and readable name for the rust_defaults module."""
619    # Choices: (1) root_pkg + '_test'? + '_defaults',
620    # (2) root_pkg + '_test'? + '_defaults_' + crate_name
621    # (3) root_pkg + '_test'? + '_defaults_' + main_src_basename_path
622    # (4) root_pkg + '_test'? + '_defaults_' + a_positive_sequence_number
623    test = "_test" if self.crate_types == ['test'] else ""
624    name1 = altered_defaults(self.root_pkg) + test + '_defaults'
625    if self.runner.try_claim_module_name(name1, self):
626      return name1
627    name2 = name1 + '_' + self.crate_name
628    if self.runner.try_claim_module_name(name2, self):
629      return name2
630    name3 = name1 + '_' + self.main_src_basename_path()
631    if self.runner.try_claim_module_name(name3, self):
632      return name3
633    return self.runner.claim_module_name(name1, self, 0)
634
635  def dump_srcs_list(self):
636    """Dump the srcs list, for defaults or regular modules."""
637    if len(self.srcs) > 1:
638      srcs = sorted(set(self.srcs))  # make a copy and dedup
639    else:
640      srcs = [self.main_src]
641    copy_out = self.runner.copy_out_module_name()
642    if copy_out:
643      srcs.append(':' + copy_out)
644    self.dump_android_property_list('srcs', '"%s"', srcs)
645
646  def dump_defaults_module(self):
647    """Dump a rust_defaults module to be shared by other modules."""
648    name = self.build_default_name()
649    self.defaults = name
650    self.write('\nrust_defaults {')
651    self.write('    name: "' + name + '",')
652    if self.runner.args.global_defaults:
653      self.write('    defaults: ["' + self.runner.args.global_defaults + '"],')
654    self.write('    crate_name: "' + self.crate_name + '",')
655    if len(self.srcs) == 1:  # only one source file; share it in defaults
656      self.default_srcs = True
657      if self.has_warning and not self.cap_lints:
658        self.write('    // has rustc warnings')
659      self.dump_srcs_list()
660    if self.cargo_env_compat:
661      self.write('    cargo_env_compat: true,')
662      if not self.runner.args.no_pkg_vers:
663        self.write('    cargo_pkg_version: "' + self.cargo_pkg_version + '",')
664    if 'test' in self.crate_types:
665      self.write('    test_suites: ["general-tests"],')
666      self.write('    auto_gen_config: true,')
667    self.dump_edition_flags_libs()
668    if 'test' in self.crate_types and len(self.srcs) == 1:
669      self.dump_test_data()
670    self.write('}')
671
672  def dump_single_type_android_module(self):
673    """Dump one simple Android module, which has only one crate_type."""
674    crate_type = self.crate_types[0]
675    if crate_type != 'test':
676      # do not change self.stem or self.module_name
677      self.dump_one_android_module(crate_type)
678      return
679    # Dump one test module per source file.
680    # crate_type == 'test'
681    self.srcs = [src for src in self.srcs if not self.runner.should_ignore_test(src)]
682    if len(self.srcs) > 1:
683      self.srcs = sorted(set(self.srcs))
684      self.dump_defaults_module()
685    saved_srcs = self.srcs
686    for src in saved_srcs:
687      self.srcs = [src]
688      saved_main_src = self.main_src
689      self.main_src = src
690      self.module_name = self.test_module_name()
691      self.decide_one_module_type(crate_type)
692      self.dump_one_android_module(crate_type)
693      self.main_src = saved_main_src
694    self.srcs = saved_srcs
695
696  def dump_one_android_module(self, crate_type):
697    """Dump one Android module definition."""
698    if not self.module_type:
699      self.write('\nERROR: unknown crate_type ' + crate_type)
700      return
701    self.write('\n' + self.module_type + ' {')
702    self.dump_android_core_properties()
703    if not self.defaults:
704      self.dump_edition_flags_libs()
705    if self.runner.args.host_first_multilib and self.host_supported and crate_type != 'test':
706      self.write('    compile_multilib: "first",')
707    if self.runner.args.exported_c_header_dir and crate_type in C_LIBRARY_CRATE_TYPES:
708      self.write('    include_dirs: [')
709      for header_dir in self.runner.args.exported_c_header_dir:
710        self.write('        "%s",' % header_dir)
711      self.write('    ],')
712    if self.runner.args.apex_available and crate_type in LIBRARY_CRATE_TYPES:
713      self.write('    apex_available: [')
714      for apex in self.runner.args.apex_available:
715        self.write('        "%s",' % apex)
716      self.write('    ],')
717    if crate_type != 'test':
718      if self.runner.args.native_bridge_supported:
719        self.write('    native_bridge_supported: true,')
720      if self.runner.args.product_available:
721        self.write('    product_available: true,')
722      if self.runner.args.recovery_available:
723        self.write('    recovery_available: true,')
724      if self.runner.args.vendor_available:
725        self.write('    vendor_available: true,')
726      if self.runner.args.vendor_ramdisk_available:
727        self.write('    vendor_ramdisk_available: true,')
728      if self.runner.args.ramdisk_available:
729        self.write('    ramdisk_available: true,')
730    if self.runner.args.min_sdk_version and crate_type in LIBRARY_CRATE_TYPES:
731      self.write('    min_sdk_version: "%s",' % self.runner.args.min_sdk_version)
732    if crate_type == 'test' and not self.default_srcs:
733      self.dump_test_data()
734    if self.runner.args.add_module_block:
735      with open(self.runner.args.add_module_block, 'r') as f:
736        self.write('    %s,' % f.read().replace('\n', '\n    '))
737    self.write('}')
738
739  def dump_android_flags(self):
740    """Dump Android module flags property."""
741    if not self.codegens and not self.cap_lints:
742      return
743    self.write('    flags: [')
744    if self.cap_lints:
745      self.write('        "--cap-lints ' + self.cap_lints + '",')
746    codegens_fmt = '"-C %s"'
747    self.dump_android_property_list_items(codegens_fmt, self.codegens)
748    self.write('    ],')
749
750  def dump_edition_flags_libs(self):
751    if self.edition:
752      self.write('    edition: "' + self.edition + '",')
753    self.dump_android_property_list('features', '"%s"', self.features)
754    cfgs = [cfg for cfg in self.cfgs if not cfg in self.runner.args.cfg_blocklist]
755    self.dump_android_property_list('cfgs', '"%s"', cfgs)
756    self.dump_android_flags()
757    if self.externs:
758      self.dump_android_externs()
759    all_static_libs = [lib for lib in self.static_libs if not lib in self.runner.args.lib_blocklist]
760    static_libs = [lib for lib in all_static_libs if not lib in self.runner.args.whole_static_libs]
761    self.dump_android_property_list('static_libs', '"lib%s"', static_libs)
762    whole_static_libs = [lib for lib in all_static_libs if lib in self.runner.args.whole_static_libs]
763    self.dump_android_property_list('whole_static_libs', '"lib%s"', whole_static_libs)
764    shared_libs = [lib for lib in self.shared_libs if not lib in self.runner.args.lib_blocklist]
765    self.dump_android_property_list('shared_libs', '"lib%s"', shared_libs)
766
767  def dump_test_data(self):
768    data = [data for (name, data) in map(lambda kv: kv.split('=', 1), self.runner.args.test_data)
769            if self.srcs == [name]]
770    if data:
771      self.dump_android_property_list('data', '"%s"', data)
772
773  def main_src_basename_path(self):
774    return re.sub('/', '_', re.sub('.rs$', '', self.main_src))
775
776  def test_module_name(self):
777    """Return a unique name for a test module."""
778    # root_pkg+(_host|_device) + '_test_'+source_file_name
779    suffix = self.main_src_basename_path()
780    return self.root_pkg + '_test_' + suffix
781
782  def decide_module_type(self):
783    # Use the first crate type for the default/first module.
784    crate_type = self.crate_types[0] if self.crate_types else ''
785    self.decide_one_module_type(crate_type)
786
787  def decide_one_module_type(self, crate_type):
788    """Decide which Android module type to use."""
789    host = '' if self.device_supported else '_host'
790    rlib = '_rlib' if self.runner.args.force_rlib else ''
791    if crate_type == 'bin':  # rust_binary[_host]
792      self.module_type = 'rust_binary' + host
793      # In rare cases like protobuf-codegen, the output binary name must
794      # be renamed to use as a plugin for protoc.
795      self.stem = altered_stem(self.crate_name)
796      self.module_name = altered_name(self.crate_name)
797    elif crate_type == 'lib':  # rust_library[_host]
798      # TODO(chh): should this be rust_library[_host]?
799      # Assuming that Cargo.toml do not use both 'lib' and 'rlib',
800      # because we map them both to rlib.
801      self.module_type = 'rust_library' + rlib + host
802      self.stem = 'lib' + self.crate_name
803      self.module_name = altered_name(self.stem)
804    elif crate_type == 'rlib':  # rust_library[_host]
805      self.module_type = 'rust_library' + rlib + host
806      self.stem = 'lib' + self.crate_name
807      self.module_name = altered_name(self.stem)
808    elif crate_type == 'dylib':  # rust_library[_host]_dylib
809      self.module_type = 'rust_library' + host + '_dylib'
810      self.stem = 'lib' + self.crate_name
811      self.module_name = altered_name(self.stem) + '_dylib'
812    elif crate_type == 'cdylib':  # rust_library[_host]_shared
813      self.module_type = 'rust_ffi' + host + '_shared'
814      self.stem = 'lib' + self.crate_name
815      self.module_name = altered_name(self.stem) + '_shared'
816    elif crate_type == 'staticlib':  # rust_library[_host]_static
817      self.module_type = 'rust_ffi' + host + '_static'
818      self.stem = 'lib' + self.crate_name
819      self.module_name = altered_name(self.stem) + '_static'
820    elif crate_type == 'test':  # rust_test[_host]
821      self.module_type = 'rust_test' + host
822      # Before do_merge, stem name is based on the --crate-name parameter.
823      # and test module name is based on stem.
824      self.stem = self.test_module_name()
825      # self.stem will be changed after merging with other tests.
826      # self.stem is NOT used for final test binary name.
827      # rust_test uses each source file base name as part of output file name.
828      # In do_merge, this function is called again, with a module_name.
829      # We make sure that the module name is unique in each package.
830      if self.module_name:
831        # Cargo uses "-C extra-filename=..." and "-C metadata=..." to add
832        # different suffixes and distinguish multiple tests of the same
833        # crate name. We ignore -C and use claim_module_name to get
834        # unique sequential suffix.
835        self.module_name = self.runner.claim_module_name(
836            self.module_name, self, 0)
837        # Now the module name is unique, stem should also match and unique.
838        self.stem = self.module_name
839    elif crate_type == 'proc-macro':  # rust_proc_macro
840      self.module_type = 'rust_proc_macro'
841      self.stem = 'lib' + self.crate_name
842      self.module_name = altered_name(self.stem)
843    else:  # unknown module type, rust_prebuilt_dylib? rust_library[_host]?
844      self.module_type = ''
845      self.stem = ''
846
847  def dump_android_property_list_items(self, fmt, values):
848    for v in values:
849      # fmt has quotes, so we need escape_quotes(v)
850      self.write('        ' + (fmt % escape_quotes(v)) + ',')
851
852  def dump_android_property_list(self, name, fmt, values):
853    if not values:
854      return
855    if len(values) > 1:
856      self.write('    ' + name + ': [')
857      self.dump_android_property_list_items(fmt, values)
858      self.write('    ],')
859    else:
860      self.write('    ' + name + ': [' +
861                 (fmt % escape_quotes(values[0])) + '],')
862
863  def dump_android_core_properties(self):
864    """Dump the module header, name, stem, etc."""
865    self.write('    name: "' + self.module_name + '",')
866    # see properties shared by dump_defaults_module
867    if self.defaults:
868      self.write('    defaults: ["' + self.defaults + '"],')
869    elif self.runner.args.global_defaults:
870      self.write('    defaults: ["' + self.runner.args.global_defaults + '"],')
871    if self.stem != self.module_name:
872      self.write('    stem: "' + self.stem + '",')
873    if self.has_warning and not self.cap_lints and not self.default_srcs:
874      self.write('    // has rustc warnings')
875    if self.host_supported and self.device_supported and self.module_type != 'rust_proc_macro':
876      self.write('    host_supported: true,')
877    if not self.defaults:
878      self.write('    crate_name: "' + self.crate_name + '",')
879    if not self.defaults and self.cargo_env_compat:
880      self.write('    cargo_env_compat: true,')
881      if not self.runner.args.no_pkg_vers:
882        self.write('    cargo_pkg_version: "' + self.cargo_pkg_version + '",')
883    if not self.default_srcs:
884      self.dump_srcs_list()
885    if 'test' in self.crate_types and not self.defaults:
886      # self.root_pkg can have multiple test modules, with different *_tests[n]
887      # names, but their executables can all be installed under the same _tests
888      # directory. When built from Cargo.toml, all tests should have different
889      # file or crate names. So we used (root_pkg + '_tests') name as the
890      # relative_install_path.
891      # However, some package like 'slab' can have non-mergeable tests that
892      # must be separated by different module names. So, here we no longer
893      # emit relative_install_path.
894      # self.write('    relative_install_path: "' + self.root_pkg + '_tests",')
895      self.write('    test_suites: ["general-tests"],')
896      self.write('    auto_gen_config: true,')
897    if 'test' in self.crate_types and self.host_supported:
898      self.write('    test_options: {')
899      if self.runner.args.no_presubmit:
900        self.write('        unit_test: false,')
901      else:
902        self.write('        unit_test: true,')
903      self.write('    },')
904
905  def dump_android_externs(self):
906    """Dump the dependent rlibs and dylibs property."""
907    so_libs = list()
908    rust_libs = ''
909    deps_libname = re.compile('^.* = lib(.*)-[0-9a-f]*.(rlib|so|rmeta)$')
910    for lib in self.externs:
911      # normal value of lib: "libc = liblibc-*.rlib"
912      # strange case in rand crate:  "getrandom_package = libgetrandom-*.rlib"
913      # we should use "libgetrandom", not "lib" + "getrandom_package"
914      groups = deps_libname.match(lib)
915      if groups is not None:
916        lib_name = groups.group(1)
917      else:
918        lib_name = re.sub(' .*$', '', lib)
919      if lib_name in self.runner.args.dependency_blocklist:
920        continue
921      if lib.endswith('.rlib') or lib.endswith('.rmeta'):
922        # On MacOS .rmeta is used when Linux uses .rlib or .rmeta.
923        rust_libs += '        "' + altered_name('lib' + lib_name) + '",\n'
924      elif lib.endswith('.so'):
925        so_libs.append(lib_name)
926      elif lib != 'proc_macro':  # --extern proc_macro is special and ignored
927        rust_libs += '        // ERROR: unknown type of lib ' + lib + '\n'
928    if rust_libs:
929      self.write('    rustlibs: [\n' + rust_libs + '    ],')
930    # Are all dependent .so files proc_macros?
931    # TODO(chh): Separate proc_macros and dylib.
932    self.dump_android_property_list('proc_macros', '"lib%s"', so_libs)
933
934
935class ARObject(object):
936  """Information of an "ar" link command."""
937
938  def __init__(self, runner, outf_name):
939    # Remembered global runner and its members.
940    self.runner = runner
941    self.pkg = ''
942    self.outf_name = outf_name  # path to Android.bp
943    # "ar" arguments
944    self.line_num = 1
945    self.line = ''
946    self.flags = ''  # e.g. "crs"
947    self.lib = ''  # e.g. "/.../out/lib*.a"
948    self.objs = list()  # e.g. "/.../out/.../*.o"
949
950  def parse(self, pkg, line_num, args_line):
951    """Collect ar obj/lib file names."""
952    self.pkg = pkg
953    self.line_num = line_num
954    self.line = args_line
955    args = args_line.split()
956    num_args = len(args)
957    if num_args < 3:
958      print('ERROR: "ar" command has too few arguments', args_line)
959    else:
960      self.flags = unquote(args[0])
961      self.lib = unquote(args[1])
962      self.objs = sorted(set(map(unquote, args[2:])))
963    return self
964
965  def write(self, s):
966    self.outf.write(s + '\n')
967
968  def dump_debug_info(self):
969    self.write('\n// Line ' + str(self.line_num) + ' "ar" ' + self.line)
970    self.write('// ar_object for %12s' % self.pkg)
971    self.write('//   flags = %s' % self.flags)
972    self.write('//     lib = %s' % short_out_name(self.pkg, self.lib))
973    for o in self.objs:
974      self.write('//     obj = %s' % short_out_name(self.pkg, o))
975
976  def dump_android_lib(self):
977    """Write cc_library_static into Android.bp."""
978    self.write('\ncc_library_static {')
979    self.write('    name: "' + file_base_name(self.lib) + '",')
980    self.write('    host_supported: true,')
981    if self.flags != 'crs':
982      self.write('    // ar flags = %s' % self.flags)
983    if self.pkg not in self.runner.pkg_obj2cc:
984      self.write('    ERROR: cannot find source files.\n}')
985      return
986    self.write('    srcs: [')
987    obj2cc = self.runner.pkg_obj2cc[self.pkg]
988    # Note: wflags are ignored.
989    dflags = list()
990    fflags = list()
991    for obj in self.objs:
992      self.write('        "' + short_out_name(self.pkg, obj2cc[obj].src) + '",')
993      # TODO(chh): union of dflags and flags of all obj
994      # Now, just a temporary hack that uses the last obj's flags
995      dflags = obj2cc[obj].dflags
996      fflags = obj2cc[obj].fflags
997    self.write('    ],')
998    self.write('    cflags: [')
999    self.write('        "-O3",')  # TODO(chh): is this default correct?
1000    self.write('        "-Wno-error",')
1001    for x in fflags:
1002      self.write('        "-f' + x + '",')
1003    for x in dflags:
1004      self.write('        "-D' + x + '",')
1005    self.write('    ],')
1006    self.write('}')
1007
1008  def dump(self):
1009    """Dump error/debug/module info to the output .bp file."""
1010    self.runner.init_bp_file(self.outf_name)
1011    with open(self.outf_name, 'a') as outf:
1012      self.outf = outf
1013      if self.runner.args.debug:
1014        self.dump_debug_info()
1015      self.dump_android_lib()
1016
1017
1018class CCObject(object):
1019  """Information of a "cc" compilation command."""
1020
1021  def __init__(self, runner, outf_name):
1022    # Remembered global runner and its members.
1023    self.runner = runner
1024    self.pkg = ''
1025    self.outf_name = outf_name  # path to Android.bp
1026    # "cc" arguments
1027    self.line_num = 1
1028    self.line = ''
1029    self.src = ''
1030    self.obj = ''
1031    self.dflags = list()  # -D flags
1032    self.fflags = list()  # -f flags
1033    self.iflags = list()  # -I flags
1034    self.wflags = list()  # -W flags
1035    self.other_args = list()
1036
1037  def parse(self, pkg, line_num, args_line):
1038    """Collect cc compilation flags and src/out file names."""
1039    self.pkg = pkg
1040    self.line_num = line_num
1041    self.line = args_line
1042    args = args_line.split()
1043    i = 0
1044    while i < len(args):
1045      arg = args[i]
1046      if arg == '"-c"':
1047        i += 1
1048        if args[i].startswith('"-o'):
1049          # ring-0.13.5 dumps: ... "-c" "-o/.../*.o" ".../*.c"
1050          self.obj = unquote(args[i])[2:]
1051          i += 1
1052          self.src = unquote(args[i])
1053        else:
1054          self.src = unquote(args[i])
1055      elif arg == '"-o"':
1056        i += 1
1057        self.obj = unquote(args[i])
1058      elif arg == '"-I"':
1059        i += 1
1060        self.iflags.append(unquote(args[i]))
1061      elif arg.startswith('"-D'):
1062        self.dflags.append(unquote(args[i])[2:])
1063      elif arg.startswith('"-f'):
1064        self.fflags.append(unquote(args[i])[2:])
1065      elif arg.startswith('"-W'):
1066        self.wflags.append(unquote(args[i])[2:])
1067      elif not (arg.startswith('"-O') or arg == '"-m64"' or arg == '"-g"' or
1068                arg == '"-g3"'):
1069        # ignore -O -m64 -g
1070        self.other_args.append(unquote(args[i]))
1071      i += 1
1072    self.dflags = sorted(set(self.dflags))
1073    self.fflags = sorted(set(self.fflags))
1074    # self.wflags is not sorted because some are order sensitive
1075    # and we ignore them anyway.
1076    if self.pkg not in self.runner.pkg_obj2cc:
1077      self.runner.pkg_obj2cc[self.pkg] = {}
1078    self.runner.pkg_obj2cc[self.pkg][self.obj] = self
1079    return self
1080
1081  def write(self, s):
1082    self.outf.write(s + '\n')
1083
1084  def dump_debug_flags(self, name, flags):
1085    self.write('//  ' + name + ':')
1086    for f in flags:
1087      self.write('//    %s' % f)
1088
1089  def dump(self):
1090    """Dump only error/debug info to the output .bp file."""
1091    if not self.runner.args.debug:
1092      return
1093    self.runner.init_bp_file(self.outf_name)
1094    with open(self.outf_name, 'a') as outf:
1095      self.outf = outf
1096      self.write('\n// Line ' + str(self.line_num) + ' "cc" ' + self.line)
1097      self.write('// cc_object for %12s' % self.pkg)
1098      self.write('//    src = %s' % short_out_name(self.pkg, self.src))
1099      self.write('//    obj = %s' % short_out_name(self.pkg, self.obj))
1100      self.dump_debug_flags('-I flags', self.iflags)
1101      self.dump_debug_flags('-D flags', self.dflags)
1102      self.dump_debug_flags('-f flags', self.fflags)
1103      self.dump_debug_flags('-W flags', self.wflags)
1104      if self.other_args:
1105        self.dump_debug_flags('other args', self.other_args)
1106
1107
1108class Runner(object):
1109  """Main class to parse cargo -v output and print Android module definitions."""
1110
1111  def __init__(self, args):
1112    self.bp_files = set()  # Remember all output Android.bp files.
1113    self.root_pkg = ''  # name of package in ./Cargo.toml
1114    # Saved flags, modes, and data.
1115    self.args = args
1116    self.dry_run = not args.run
1117    self.skip_cargo = args.skipcargo
1118    self.cargo_path = './cargo'  # path to cargo, will be set later
1119    self.checked_out_files = False  # to check only once
1120    self.build_out_files = []  # output files generated by build.rs
1121    # All cc/ar objects, crates, dependencies, and warning files
1122    self.cc_objects = list()
1123    self.pkg_obj2cc = {}
1124    # pkg_obj2cc[cc_object[i].pkg][cc_objects[i].obj] = cc_objects[i]
1125    self.ar_objects = list()
1126    self.crates = list()
1127    self.warning_files = set()
1128    # Keep a unique mapping from (module name) to crate
1129    self.name_owners = {}
1130    # Save and dump all errors from cargo to Android.bp.
1131    self.errors = ''
1132    self.test_errors = ''
1133    self.setup_cargo_path()
1134    # Default action is cargo clean, followed by build or user given actions.
1135    if args.cargo:
1136      self.cargo = ['clean'] + args.cargo
1137    else:
1138      default_target = '--target x86_64-unknown-linux-gnu'
1139      # Use the same target for both host and default device builds.
1140      # Same target is used as default in host x86_64 Android compilation.
1141      # Note: b/169872957, prebuilt cargo failed to build vsock
1142      # on x86_64-unknown-linux-musl systems.
1143      self.cargo = ['clean', 'build ' + default_target]
1144      if args.tests:
1145        self.cargo.append('build --tests ' + default_target)
1146    self.empty_tests = set()
1147    self.empty_unittests = False
1148
1149  def setup_cargo_path(self):
1150    """Find cargo in the --cargo_bin or prebuilt rust bin directory."""
1151    if self.args.cargo_bin:
1152      self.cargo_path = os.path.join(self.args.cargo_bin, 'cargo')
1153      if not os.path.isfile(self.cargo_path):
1154        sys.exit('ERROR: cannot find cargo in ' + self.args.cargo_bin)
1155      print('INFO: using cargo in ' + self.args.cargo_bin)
1156      return
1157    elif os.environ.get('ANDROID_BUILD_ENVIRONMENT_CONFIG', '') == 'googler':
1158      sys.exit('ERROR: Not executed within the sandbox. Please see '
1159               'go/cargo2android-sandbox for more information.')
1160    else:
1161      sys.exit('ERROR: the prebuilt cargo is not usable; please '
1162               'use the --cargo_bin flag.')
1163    # We have only tested this on Linux.
1164    if platform.system() != 'Linux':
1165      sys.exit('ERROR: this script has only been tested on Linux with cargo.')
1166    # Assuming that this script is in development/scripts.
1167    my_dir = os.path.dirname(os.path.abspath(__file__))
1168    linux_dir = os.path.join(my_dir, '..', '..',
1169                             'prebuilts', 'rust', 'linux-x86')
1170    if not os.path.isdir(linux_dir):
1171      sys.exit('ERROR: cannot find directory ' + linux_dir)
1172    rust_version = self.find_rust_version(my_dir, linux_dir)
1173    cargo_bin = os.path.join(linux_dir, rust_version, 'bin')
1174    self.cargo_path = os.path.join(cargo_bin, 'cargo')
1175    if not os.path.isfile(self.cargo_path):
1176      sys.exit('ERROR: cannot find cargo in ' + cargo_bin
1177               + '; please try --cargo_bin= flag.')
1178    return
1179
1180  def find_rust_version(self, my_dir, linux_dir):
1181    """Use my script directory, find prebuilt rust version."""
1182    # First look up build/soong/rust/config/global.go.
1183    path2global = os.path.join(my_dir, '..', '..',
1184                               'build', 'soong', 'rust', 'config', 'global.go')
1185    if os.path.isfile(path2global):
1186      # try to find: RustDefaultVersion = "1.44.0"
1187      version_pat = re.compile(
1188          r'\s*RustDefaultVersion\s*=\s*"([0-9]+\.[0-9]+\.[0-9]+)".*$')
1189      with open(path2global, 'r') as inf:
1190        for line in inf:
1191          result = version_pat.match(line)
1192          if result:
1193            return result.group(1)
1194    print('WARNING: cannot find RustDefaultVersion in ' + path2global)
1195    # Otherwise, find the newest (largest) version number in linux_dir.
1196    rust_version = (0, 0, 0)  # the prebuilt version to use
1197    version_pat = re.compile(r'([0-9]+)\.([0-9]+)\.([0-9]+)$')
1198    for dir_name in os.listdir(linux_dir):
1199      result = version_pat.match(dir_name)
1200      if not result:
1201        continue
1202      version = (result.group(1), result.group(2), result.group(3))
1203      if version > rust_version:
1204        rust_version = version
1205    return '.'.join(rust_version)
1206
1207  def find_out_files(self):
1208    # list1 has build.rs output for normal crates
1209    list1 = glob.glob(TARGET_TMP + '/*/*/build/' + self.root_pkg + '-*/out/*')
1210    # list2 has build.rs output for proc-macro crates
1211    list2 = glob.glob(TARGET_TMP + '/*/build/' + self.root_pkg + '-*/out/*')
1212    return list1 + list2
1213
1214  def copy_out_files(self):
1215    """Copy build.rs output files to ./out and set up build_out_files."""
1216    if self.checked_out_files:
1217      return
1218    self.checked_out_files = True
1219    cargo_out_files = self.find_out_files()
1220    out_files = set()
1221    if cargo_out_files:
1222      os.makedirs('out', exist_ok=True)
1223    for path in cargo_out_files:
1224      file_name = path.split('/')[-1]
1225      out_files.add(file_name)
1226      shutil.copy(path, 'out/' + file_name)
1227    self.build_out_files = sorted(out_files)
1228
1229  def has_used_out_dir(self):
1230    """Returns true if env!("OUT_DIR") is found."""
1231    return 0 == os.system('grep -rl --exclude build.rs --include \\*.rs' +
1232                          ' \'env!("OUT_DIR")\' * > /dev/null')
1233
1234  def copy_out_module_name(self):
1235    if self.args.copy_out and self.build_out_files:
1236      return 'copy_' + self.root_pkg + '_build_out'
1237    else:
1238      return ''
1239
1240  def read_license(self, name):
1241    if not os.path.isfile(name):
1242      return ''
1243    license = ''
1244    with open(name, 'r') as intf:
1245      line = intf.readline()
1246      # Firstly skip ANDROID_BP_HEADER
1247      while line.startswith('//'):
1248        line = intf.readline()
1249      # Read all lines until we see a rust_* or genrule rule.
1250      while line != '' and not (line.startswith('rust_') or line.startswith('genrule {')):
1251        license += line
1252        line = intf.readline()
1253    return license.strip()
1254
1255  def dump_copy_out_module(self, outf):
1256    """Output the genrule module to copy out/* to $(genDir)."""
1257    copy_out = self.copy_out_module_name()
1258    if not copy_out:
1259      return
1260    outf.write('\ngenrule {\n')
1261    outf.write('    name: "' + copy_out + '",\n')
1262    outf.write('    srcs: ["out/*"],\n')
1263    outf.write('    cmd: "cp $(in) $(genDir)",\n')
1264    if len(self.build_out_files) > 1:
1265      outf.write('    out: [\n')
1266      for f in self.build_out_files:
1267        outf.write('        "' + f + '",\n')
1268      outf.write('    ],\n')
1269    else:
1270      outf.write('    out: ["' + self.build_out_files[0] + '"],\n')
1271    outf.write('}\n')
1272
1273  def init_bp_file(self, name):
1274    # name could be Android.bp or sub_dir_path/Android.bp
1275    if name not in self.bp_files:
1276      self.bp_files.add(name)
1277      license_section = self.read_license(name)
1278      with open(name, 'w') as outf:
1279        print_args = sys.argv[1:].copy()
1280        if '--cargo_bin' in print_args:
1281          index = print_args.index('--cargo_bin')
1282          del print_args[index:index+2]
1283        outf.write(ANDROID_BP_HEADER.format(args=' '.join(print_args)))
1284        outf.write('\n')
1285        outf.write(license_section)
1286        outf.write('\n')
1287        # at most one copy_out module per .bp file
1288        self.dump_copy_out_module(outf)
1289
1290  def try_claim_module_name(self, name, owner):
1291    """Reserve and return True if it has not been reserved yet."""
1292    if name not in self.name_owners or owner == self.name_owners[name]:
1293      self.name_owners[name] = owner
1294      return True
1295    return False
1296
1297  def claim_module_name(self, prefix, owner, counter):
1298    """Return prefix if not owned yet, otherwise, prefix+str(counter)."""
1299    while True:
1300      name = prefix
1301      if counter > 0:
1302        name += '_' + str(counter)
1303      if self.try_claim_module_name(name, owner):
1304        return name
1305      counter += 1
1306
1307  def find_root_pkg(self):
1308    """Read name of [package] in ./Cargo.toml."""
1309    if not os.path.exists('./Cargo.toml'):
1310      return
1311    with open('./Cargo.toml', 'r') as inf:
1312      pkg_section = re.compile(r'^ *\[package\]')
1313      name = re.compile('^ *name *= * "([^"]*)"')
1314      in_pkg = False
1315      for line in inf:
1316        if in_pkg:
1317          if name.match(line):
1318            self.root_pkg = name.match(line).group(1)
1319            break
1320        else:
1321          in_pkg = pkg_section.match(line) is not None
1322
1323  def run_cargo(self):
1324    """Calls cargo -v and save its output to ./cargo.out."""
1325    if self.skip_cargo:
1326      return self
1327    cargo_toml = './Cargo.toml'
1328    cargo_out = './cargo.out'
1329    # Do not use Cargo.lock, because .bp rules are designed to
1330    # run with "latest" crates avaialable on Android.
1331    cargo_lock = './Cargo.lock'
1332    cargo_lock_saved = './cargo.lock.saved'
1333    had_cargo_lock = os.path.exists(cargo_lock)
1334    if not os.access(cargo_toml, os.R_OK):
1335      print('ERROR: Cannot find or read', cargo_toml)
1336      return self
1337    if not self.dry_run:
1338      if os.path.exists(cargo_out):
1339        os.remove(cargo_out)
1340      if not self.args.use_cargo_lock and had_cargo_lock:  # save it
1341        os.rename(cargo_lock, cargo_lock_saved)
1342    cmd_tail_target = ' --target-dir ' + TARGET_TMP
1343    cmd_tail_redir = ' >> ' + cargo_out + ' 2>&1'
1344    # set up search PATH for cargo to find the correct rustc
1345    saved_path = os.environ['PATH']
1346    os.environ['PATH'] = os.path.dirname(self.cargo_path) + ':' + saved_path
1347    # Add [workspace] to Cargo.toml if it is not there.
1348    added_workspace = False
1349    if self.args.add_workspace:
1350      with open(cargo_toml, 'r') as in_file:
1351        cargo_toml_lines = in_file.readlines()
1352      found_workspace = '[workspace]\n' in cargo_toml_lines
1353      if found_workspace:
1354        print('### WARNING: found [workspace] in Cargo.toml')
1355      else:
1356        with open(cargo_toml, 'a') as out_file:
1357          out_file.write('[workspace]\n')
1358          added_workspace = True
1359          if self.args.verbose:
1360            print('### INFO: added [workspace] to Cargo.toml')
1361    for c in self.cargo:
1362      features = ''
1363      if c != 'clean':
1364        if self.args.features is not None:
1365          features = ' --no-default-features'
1366        if self.args.features:
1367          features += ' --features ' + self.args.features
1368      cmd_v_flag = ' -vv ' if self.args.vv else ' -v '
1369      cmd = self.cargo_path + cmd_v_flag
1370      cmd += c + features + cmd_tail_target + cmd_tail_redir
1371      if self.args.rustflags and c != 'clean':
1372        cmd = 'RUSTFLAGS="' + self.args.rustflags + '" ' + cmd
1373      self.run_cmd(cmd, cargo_out)
1374    if self.args.tests:
1375      cmd = self.cargo_path + ' test' + features + cmd_tail_target + ' -- --list' + cmd_tail_redir
1376      self.run_cmd(cmd, cargo_out)
1377    if added_workspace:  # restore original Cargo.toml
1378      with open(cargo_toml, 'w') as out_file:
1379        out_file.writelines(cargo_toml_lines)
1380      if self.args.verbose:
1381        print('### INFO: restored original Cargo.toml')
1382    os.environ['PATH'] = saved_path
1383    if not self.dry_run:
1384      if not had_cargo_lock:  # restore to no Cargo.lock state
1385        if os.path.exists(cargo_lock):
1386          os.remove(cargo_lock)
1387      elif not self.args.use_cargo_lock:  # restore saved Cargo.lock
1388        os.rename(cargo_lock_saved, cargo_lock)
1389    return self
1390
1391  def run_cmd(self, cmd, cargo_out):
1392    if self.dry_run:
1393      print('Dry-run skip:', cmd)
1394    else:
1395      if self.args.verbose:
1396        print('Running:', cmd)
1397      with open(cargo_out, 'a') as out_file:
1398        out_file.write('### Running: ' + cmd + '\n')
1399      ret = os.system(cmd)
1400      if ret != 0:
1401        print('*** There was an error while running cargo.  ' +
1402              'See the cargo.out file for details.')
1403
1404  def dump_pkg_obj2cc(self):
1405    """Dump debug info of the pkg_obj2cc map."""
1406    if not self.args.debug:
1407      return
1408    self.init_bp_file('Android.bp')
1409    with open('Android.bp', 'a') as outf:
1410      sorted_pkgs = sorted(self.pkg_obj2cc.keys())
1411      for pkg in sorted_pkgs:
1412        if not self.pkg_obj2cc[pkg]:
1413          continue
1414        outf.write('\n// obj => src for %s\n' % pkg)
1415        obj2cc = self.pkg_obj2cc[pkg]
1416        for obj in sorted(obj2cc.keys()):
1417          outf.write('//  ' + short_out_name(pkg, obj) + ' => ' +
1418                     short_out_name(pkg, obj2cc[obj].src) + '\n')
1419
1420  def apply_patch(self):
1421    """Apply local patch file if it is given."""
1422    if self.args.patch:
1423      if self.dry_run:
1424        print('Dry-run skip patch file:', self.args.patch)
1425      else:
1426        if not os.path.exists(self.args.patch):
1427          self.append_to_bp('ERROR cannot find patch file: ' + self.args.patch)
1428          return self
1429        if self.args.verbose:
1430          print('### INFO: applying local patch file:', self.args.patch)
1431        subprocess.run(['patch', '-s', '--no-backup-if-mismatch', './Android.bp',
1432                        self.args.patch], check=True)
1433    return self
1434
1435  def gen_bp(self):
1436    """Parse cargo.out and generate Android.bp files."""
1437    if self.dry_run:
1438      print('Dry-run skip: read', CARGO_OUT, 'write Android.bp')
1439    elif os.path.exists(CARGO_OUT):
1440      self.find_root_pkg()
1441      if self.args.copy_out:
1442        self.copy_out_files()
1443      elif self.find_out_files() and self.has_used_out_dir():
1444        print('WARNING: ' + self.root_pkg + ' has cargo output files; ' +
1445              'please rerun with the --copy-out flag.')
1446      with open(CARGO_OUT, 'r') as cargo_out:
1447        self.parse(cargo_out, 'Android.bp')
1448        self.crates.sort(key=get_module_name)
1449        for obj in self.cc_objects:
1450          obj.dump()
1451        self.dump_pkg_obj2cc()
1452        for crate in self.crates:
1453          crate.dump()
1454        dumped_libs = set()
1455        for lib in self.ar_objects:
1456          if lib.pkg == self.root_pkg:
1457            lib_name = file_base_name(lib.lib)
1458            if lib_name not in dumped_libs:
1459              dumped_libs.add(lib_name)
1460              lib.dump()
1461        if self.args.add_toplevel_block:
1462          with open(self.args.add_toplevel_block, 'r') as f:
1463            self.append_to_bp('\n' + f.read() + '\n')
1464        if self.errors:
1465          self.append_to_bp('\n' + ERRORS_LINE + '\n' + self.errors)
1466        if self.test_errors:
1467          self.append_to_bp('\n// Errors when listing tests:\n' + self.test_errors)
1468    return self
1469
1470  def add_ar_object(self, obj):
1471    self.ar_objects.append(obj)
1472
1473  def add_cc_object(self, obj):
1474    self.cc_objects.append(obj)
1475
1476  def add_crate(self, crate):
1477    """Merge crate with someone in crates, or append to it. Return crates."""
1478    if crate.skip_crate():
1479      if self.args.debug:  # include debug info of all crates
1480        self.crates.append(crate)
1481    else:
1482      for c in self.crates:
1483        if c.merge(crate, 'Android.bp'):
1484          return
1485      # If not merged, decide module type and name now.
1486      crate.decide_module_type()
1487      self.crates.append(crate)
1488
1489  def find_warning_owners(self):
1490    """For each warning file, find its owner crate."""
1491    missing_owner = False
1492    for f in self.warning_files:
1493      cargo_dir = ''  # find lowest crate, with longest path
1494      owner = None  # owner crate of this warning
1495      for c in self.crates:
1496        if (f.startswith(c.cargo_dir + '/') and
1497            len(cargo_dir) < len(c.cargo_dir)):
1498          cargo_dir = c.cargo_dir
1499          owner = c
1500      if owner:
1501        owner.has_warning = True
1502      else:
1503        missing_owner = True
1504    if missing_owner and os.path.exists('Cargo.toml'):
1505      # owner is the root cargo, with empty cargo_dir
1506      for c in self.crates:
1507        if not c.cargo_dir:
1508          c.has_warning = True
1509
1510  def rustc_command(self, n, rustc_line, line, outf_name):
1511    """Process a rustc command line from cargo -vv output."""
1512    # cargo build -vv output can have multiple lines for a rustc command
1513    # due to '\n' in strings for environment variables.
1514    # strip removes leading spaces and '\n' at the end
1515    new_rustc = (rustc_line.strip() + line) if rustc_line else line
1516    # Use an heuristic to detect the completions of a multi-line command.
1517    # This might fail for some very rare case, but easy to fix manually.
1518    if not line.endswith('`\n') or (new_rustc.count('`') % 2) != 0:
1519      return new_rustc
1520    if RUSTC_VV_CMD_ARGS.match(new_rustc):
1521      args = RUSTC_VV_CMD_ARGS.match(new_rustc).group(1)
1522      self.add_crate(Crate(self, outf_name).parse(n, args))
1523    else:
1524      self.assert_empty_vv_line(new_rustc)
1525    return ''
1526
1527  def cc_ar_command(self, n, groups, outf_name):
1528    pkg = groups.group(1)
1529    line = groups.group(3)
1530    if groups.group(2) == 'cc':
1531      self.add_cc_object(CCObject(self, outf_name).parse(pkg, n, line))
1532    else:
1533      self.add_ar_object(ARObject(self, outf_name).parse(pkg, n, line))
1534
1535  def append_to_bp(self, line):
1536    self.init_bp_file('Android.bp')
1537    with open('Android.bp', 'a') as outf:
1538      outf.write(line)
1539
1540  def assert_empty_vv_line(self, line):
1541    if line:  # report error if line is not empty
1542      self.append_to_bp('ERROR -vv line: ' + line)
1543    return ''
1544
1545  def add_empty_test(self, name):
1546    if name == 'unittests':
1547      self.empty_unittests = True
1548    else:
1549      self.empty_tests.add(name)
1550
1551  def should_ignore_test(self, src):
1552    # cargo test outputs the source file for integration tests but "unittests"
1553    # for unit tests.  To figure out to which crate this corresponds, we check
1554    # if the current source file is the main source of a non-test crate, e.g.,
1555    # a library or a binary.
1556    return (src in self.args.test_blocklist or src in self.empty_tests
1557            or (self.empty_unittests
1558                and src in [c.main_src for c in self.crates if c.crate_types != ['test']]))
1559
1560  def parse(self, inf, outf_name):
1561    """Parse rustc, test, and warning messages in inf, return a list of Crates."""
1562    n = 0  # line number
1563    # We read the file in two passes, where the first simply checks for empty tests.
1564    # Otherwise we would add and merge tests before seeing they're empty.
1565    cur_test_name = None
1566    for line in inf:
1567      if CARGO_TEST_LIST_START_PAT.match(line):
1568        cur_test_name = CARGO_TEST_LIST_START_PAT.match(line).group(1)
1569      elif cur_test_name and CARGO_TEST_LIST_END_PAT.match(line):
1570        match = CARGO_TEST_LIST_END_PAT.match(line)
1571        if int(match.group(1)) + int(match.group(2)) == 0:
1572          self.add_empty_test(cur_test_name)
1573        cur_test_name = None
1574    inf.seek(0)
1575    prev_warning = False  # true if the previous line was warning: ...
1576    rustc_line = ''  # previous line(s) matching RUSTC_VV_PAT
1577    in_tests = False
1578    for line in inf:
1579      n += 1
1580      if line.startswith('warning: '):
1581        prev_warning = True
1582        rustc_line = self.assert_empty_vv_line(rustc_line)
1583        continue
1584      new_rustc = ''
1585      if RUSTC_PAT.match(line):
1586        args_line = RUSTC_PAT.match(line).group(1)
1587        self.add_crate(Crate(self, outf_name).parse(n, args_line))
1588        self.assert_empty_vv_line(rustc_line)
1589      elif rustc_line or RUSTC_VV_PAT.match(line):
1590        new_rustc = self.rustc_command(n, rustc_line, line, outf_name)
1591      elif CC_AR_VV_PAT.match(line):
1592        self.cc_ar_command(n, CC_AR_VV_PAT.match(line), outf_name)
1593      elif prev_warning and WARNING_FILE_PAT.match(line):
1594        self.assert_empty_vv_line(rustc_line)
1595        fpath = WARNING_FILE_PAT.match(line).group(1)
1596        if fpath[0] != '/':  # ignore absolute path
1597          self.warning_files.add(fpath)
1598      elif line.startswith('error: ') or line.startswith('error[E'):
1599        if not self.args.ignore_cargo_errors:
1600          if in_tests:
1601            self.test_errors += '// ' + line
1602          else:
1603            self.errors += line
1604      elif CARGO2ANDROID_RUNNING_PAT.match(line):
1605        in_tests = "cargo test" in line and "--list" in line
1606      prev_warning = False
1607      rustc_line = new_rustc
1608    self.find_warning_owners()
1609
1610
1611def get_parser():
1612  """Parse main arguments."""
1613  parser = argparse.ArgumentParser('cargo2android')
1614  parser.add_argument(
1615      '--add_workspace',
1616      action='store_true',
1617      default=False,
1618      help=('append [workspace] to Cargo.toml before calling cargo,' +
1619            ' to treat current directory as root of package source;' +
1620            ' otherwise the relative source file path in generated' +
1621            ' .bp file will be from the parent directory.'))
1622  parser.add_argument(
1623      '--cargo',
1624      action='append',
1625      metavar='args_string',
1626      help=('extra cargo build -v args in a string, ' +
1627            'each --cargo flag calls cargo build -v once'))
1628  parser.add_argument(
1629      '--cargo_bin',
1630      type=str,
1631      help='use cargo in the cargo_bin directory instead of the prebuilt one')
1632  parser.add_argument(
1633      '--copy-out',
1634      action='store_true',
1635      default=False,
1636      help=('only for root directory, ' +
1637            'copy build.rs output to ./out/* and add a genrule to copy ' +
1638            './out/* to genrule output; for crates with code pattern: ' +
1639            'include!(concat!(env!("OUT_DIR"), "/<some_file>.rs"))'))
1640  parser.add_argument(
1641      '--debug',
1642      action='store_true',
1643      default=False,
1644      help='dump debug info into Android.bp')
1645  parser.add_argument(
1646      '--dependencies',
1647      action='store_true',
1648      default=False,
1649      help='Deprecated. Has no effect.')
1650  parser.add_argument(
1651      '--device',
1652      action='store_true',
1653      default=False,
1654      help='run cargo also for a default device target')
1655  parser.add_argument(
1656      '--features',
1657      type=str,
1658      help=('pass features to cargo build, ' +
1659            'empty string means no default features'))
1660  parser.add_argument(
1661      '--global_defaults',
1662      type=str,
1663      help='add a defaults name to every module')
1664  parser.add_argument(
1665      '--host-first-multilib',
1666      action='store_true',
1667      default=False,
1668      help=('add a compile_multilib:"first" property ' +
1669            'to Android.bp host modules.'))
1670  parser.add_argument(
1671      '--ignore-cargo-errors',
1672      action='store_true',
1673      default=False,
1674      help='do not append cargo/rustc error messages to Android.bp')
1675  parser.add_argument(
1676      '--no-host',
1677      action='store_true',
1678      default=False,
1679      help='do not run cargo for the host; only for the device target')
1680  parser.add_argument(
1681      '--no-presubmit',
1682      action='store_true',
1683      default=False,
1684      help='set unit_test to false for test targets, to avoid host tests running in presubmit')
1685  parser.add_argument(
1686      '--no-subdir',
1687      action='store_true',
1688      default=False,
1689      help='do not output anything for sub-directories')
1690  parser.add_argument(
1691      '--onefile',
1692      action='store_true',
1693      default=False,
1694      help=('output all into one ./Android.bp, default will generate ' +
1695            'one Android.bp per Cargo.toml in subdirectories'))
1696  parser.add_argument(
1697      '--patch',
1698      type=str,
1699      help='apply the given patch file to generated ./Android.bp')
1700  parser.add_argument(
1701      '--run',
1702      action='store_true',
1703      default=False,
1704      help='run it, default is dry-run')
1705  parser.add_argument('--rustflags', type=str, help='passing flags to rustc')
1706  parser.add_argument(
1707      '--skipcargo',
1708      action='store_true',
1709      default=False,
1710      help='skip cargo command, parse cargo.out, and generate Android.bp')
1711  parser.add_argument(
1712      '--tests',
1713      action='store_true',
1714      default=False,
1715      help='run cargo build --tests after normal build')
1716  parser.add_argument(
1717      '--use-cargo-lock',
1718      action='store_true',
1719      default=False,
1720      help=('run cargo build with existing Cargo.lock ' +
1721            '(used when some latest dependent crates failed)'))
1722  parser.add_argument(
1723      '--exported_c_header_dir',
1724      nargs='*',
1725      help='Directories with headers to export for C usage'
1726  )
1727  parser.add_argument(
1728      '--min-sdk-version',
1729      type=str,
1730      help='Minimum SDK version')
1731  parser.add_argument(
1732      '--apex-available',
1733      nargs='*',
1734      help='Mark the main library as apex_available with the given apexes.')
1735  parser.add_argument(
1736      '--native-bridge-supported',
1737      action='store_true',
1738      default=False,
1739      help='Mark the main library as native_bridge_supported.')
1740  parser.add_argument(
1741      '--product-available',
1742      action='store_true',
1743      default=False,
1744      help='Mark the main library as product_available.')
1745  parser.add_argument(
1746      '--recovery-available',
1747      action='store_true',
1748      default=False,
1749      help='Mark the main library as recovery_available.')
1750  parser.add_argument(
1751      '--vendor-available',
1752      action='store_true',
1753      default=False,
1754      help='Mark the main library as vendor_available.')
1755  parser.add_argument(
1756      '--vendor-ramdisk-available',
1757      action='store_true',
1758      default=False,
1759      help='Mark the main library as vendor_ramdisk_available.')
1760  parser.add_argument(
1761      '--ramdisk-available',
1762      action='store_true',
1763      default=False,
1764      help='Mark the main library as ramdisk_available.')
1765  parser.add_argument(
1766      '--force-rlib',
1767      action='store_true',
1768      default=False,
1769      help='Make the main library an rlib.')
1770  parser.add_argument(
1771      '--whole-static-libs',
1772      nargs='*',
1773      default=[],
1774      help='Make the given libraries (without lib prefixes) whole_static_libs.')
1775  parser.add_argument(
1776      '--no-pkg-vers',
1777      action='store_true',
1778      default=False,
1779      help='Do not attempt to determine the package version automatically.')
1780  parser.add_argument(
1781      '--test-data',
1782      nargs='*',
1783      default=[],
1784      help=('Add the given file to the given test\'s data property. ' +
1785            'Usage: test-path=data-path'))
1786  parser.add_argument(
1787      '--dependency-blocklist',
1788      nargs='*',
1789      default=[],
1790      help='Do not emit the given dependencies (without lib prefixes).')
1791  parser.add_argument(
1792      '--lib-blocklist',
1793      nargs='*',
1794      default=[],
1795      help='Do not emit the given C libraries as dependencies (without lib prefixes).')
1796  parser.add_argument(
1797      '--test-blocklist',
1798      nargs='*',
1799      default=[],
1800      help=('Do not emit the given tests. ' +
1801            'Pass the path to the test file to exclude.'))
1802  parser.add_argument(
1803      '--cfg-blocklist',
1804      nargs='*',
1805      default=[],
1806      help='Do not emit the given cfg.')
1807  parser.add_argument(
1808      '--add-toplevel-block',
1809      type=str,
1810      help=('Add the contents of the given file to the top level of the Android.bp. ' +
1811            'The filename should start with cargo2android to work with the updater.'))
1812  parser.add_argument(
1813      '--add-module-block',
1814      type=str,
1815      help=('Add the contents of the given file to the main module. '+
1816            'The filename should start with cargo2android to work with the updater.'))
1817  parser.add_argument(
1818      '--verbose',
1819      action='store_true',
1820      default=False,
1821      help='echo executed commands')
1822  parser.add_argument(
1823      '--vv',
1824      action='store_true',
1825      default=False,
1826      help='run cargo with -vv instead of default -v')
1827  parser.add_argument(
1828      '--dump-config-and-exit',
1829      type=str,
1830      help=('Dump command-line arguments (minus this flag) to a config file and exit. ' +
1831            'This is intended to help migrate from command line options to config files.'))
1832  parser.add_argument(
1833      '--config',
1834      type=str,
1835      help=('Load command-line options from the given config file. ' +
1836            'Options in this file will override those passed on the command line.'))
1837  return parser
1838
1839
1840def parse_args(parser):
1841  """Parses command-line options."""
1842  args = parser.parse_args()
1843  # Use the values specified in a config file if one was found.
1844  if args.config:
1845    with open(args.config, 'r') as f:
1846      config = json.load(f)
1847      args_dict = vars(args)
1848      for arg in config:
1849        args_dict[arg.replace('-', '_')] = config[arg]
1850  return args
1851
1852
1853def dump_config(parser, args):
1854  """Writes the non-default command-line options to the specified file."""
1855  args_dict = vars(args)
1856  # Filter out the arguments that have their default value.
1857  # Also filter certain "temporary" arguments.
1858  non_default_args = {}
1859  for arg in args_dict:
1860    if (args_dict[arg] != parser.get_default(arg) and arg != 'dump_config_and_exit'
1861        and arg != 'config' and arg != 'cargo_bin'):
1862      non_default_args[arg.replace('_', '-')] = args_dict[arg]
1863  # Write to the specified file.
1864  with open(args.dump_config_and_exit, 'w') as f:
1865    json.dump(non_default_args, f, indent=2, sort_keys=True)
1866
1867
1868def main():
1869  parser = get_parser()
1870  args = parse_args(parser)
1871  if not args.run:  # default is dry-run
1872    print(DRY_RUN_NOTE)
1873  if args.dump_config_and_exit:
1874    dump_config(parser, args)
1875  else:
1876    Runner(args).run_cargo().gen_bp().apply_patch()
1877
1878
1879if __name__ == '__main__':
1880  main()
1881