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