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