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