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