1#!/usr/bin/env python3 2# Copyright (C) 2023 The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15# 16"""Call cargo -v, parse its output, and generate a Trusty build system module. 17 18Usage: Run this script in a crate workspace root directory. The Cargo.toml file 19should work at least for the host platform. 20 21Without other flags, "cargo2rulesmk.py --run" calls cargo clean, calls cargo 22build -v, and generates makefile rules. The cargo build only generates crates 23for the host without test crates. 24 25If there are rustc warning messages, this script will add a warning comment to 26the owner crate module in rules.mk. 27""" 28 29import argparse 30import glob 31import json 32import os 33import os.path 34import platform 35import re 36import shutil 37import subprocess 38import sys 39 40from typing import List 41 42 43assert "/development/scripts" in os.path.dirname(__file__) 44TOP_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) 45 46# Some Rust packages include extra unwanted crates. 47# This set contains all such excluded crate names. 48EXCLUDED_CRATES = {"protobuf_bin_gen_rust_do_not_use"} 49 50 51CUSTOM_MODULE_CRATES = { 52 # This map tracks Rust crates that have special modules that 53 # were not generated automatically by this script. Examples 54 # include compiler builtins and other foundational libraries. 55 # It also tracks crates tht are not under external/rust/crates. 56 "compiler_builtins": "trusty/user/base/lib/libcompiler_builtins-rust", 57 "core": "trusty/user/base/lib/libcore-rust", 58} 59 60RENAME_STEM_MAP = { 61 # This map includes all changes to the default rust module stem names, 62 # which is used for output files when different from the module name. 63 "protoc_gen_rust": "protoc-gen-rust", 64} 65 66# Header added to all generated rules.mk files. 67RULES_MK_HEADER = ( 68 "# This file is generated by cargo2rulesmk.py {args}.\n" 69 + "# Do not modify this file as changes will be overridden on upgrade.\n\n" 70) 71 72CARGO_OUT = "cargo.out" # Name of file to keep cargo build -v output. 73 74# This should be kept in sync with tools/external_updater/crates_updater.py. 75ERRORS_LINE = "Errors in " + CARGO_OUT + ":" 76 77TARGET_TMP = "target.tmp" # Name of temporary output directory. 78 79# Message to be displayed when this script is called without the --run flag. 80DRY_RUN_NOTE = ( 81 "Dry-run: This script uses ./" 82 + TARGET_TMP 83 + " for output directory,\n" 84 + "runs cargo clean, runs cargo build -v, saves output to ./cargo.out,\n" 85 + "and writes to rules.mk in the current and subdirectories.\n\n" 86 + "To do do all of the above, use the --run flag.\n" 87 + "See --help for other flags, and more usage notes in this script.\n" 88) 89 90# Cargo -v output of a call to rustc. 91RUSTC_PAT = re.compile("^ +Running `(.*\/)?rustc (.*)`$") 92 93# Cargo -vv output of a call to rustc could be split into multiple lines. 94# Assume that the first line will contain some CARGO_* env definition. 95RUSTC_VV_PAT = re.compile("^ +Running `.*CARGO_.*=.*$") 96# The combined -vv output rustc command line pattern. 97RUSTC_VV_CMD_ARGS = re.compile("^ *Running `.*CARGO_.*=.* (.*\/)?rustc (.*)`$") 98 99# Cargo -vv output of a "cc" or "ar" command; all in one line. 100CC_AR_VV_PAT = re.compile(r'^\[([^ ]*)[^\]]*\] running:? "(cc|ar)" (.*)$') 101# Some package, such as ring-0.13.5, has pattern '... running "cc"'. 102 103# Rustc output of file location path pattern for a warning message. 104WARNING_FILE_PAT = re.compile("^ *--> ([^:]*):[0-9]+") 105 106# cargo test --list output of the start of running a binary. 107CARGO_TEST_LIST_START_PAT = re.compile(r"^\s*Running (.*) \(.*\)$") 108 109# cargo test --list output of the end of running a binary. 110CARGO_TEST_LIST_END_PAT = re.compile(r"^(\d+) tests?, (\d+) benchmarks$") 111 112CARGO2ANDROID_RUNNING_PAT = re.compile("^### Running: .*$") 113 114# Rust package name with suffix -d1.d2.d3(+.*)?. 115VERSION_SUFFIX_PAT = re.compile( 116 r"^(.*)-[0-9]+\.[0-9]+\.[0-9]+(?:-(alpha|beta)\.[0-9]+)?(?:\+.*)?$" 117) 118 119# Crate types corresponding to a C ABI library 120C_LIBRARY_CRATE_TYPES = ["staticlib", "cdylib"] 121# Crate types corresponding to a Rust ABI library 122RUST_LIBRARY_CRATE_TYPES = ["lib", "rlib", "dylib", "proc-macro"] 123# Crate types corresponding to a library 124LIBRARY_CRATE_TYPES = C_LIBRARY_CRATE_TYPES + RUST_LIBRARY_CRATE_TYPES 125 126 127def altered_stem(name): 128 return RENAME_STEM_MAP[name] if (name in RENAME_STEM_MAP) else name 129 130 131def is_build_crate_name(name): 132 # We added special prefix to build script crate names. 133 return name.startswith("build_script_") 134 135 136def is_dependent_file_path(path): 137 # Absolute or dependent '.../' paths are not main files of this crate. 138 return path.startswith("/") or path.startswith(".../") 139 140 141def get_module_name(crate): # to sort crates in a list 142 return crate.module_name 143 144 145def pkg2crate_name(s): 146 return s.replace("-", "_").replace(".", "_") 147 148 149def file_base_name(path): 150 return os.path.splitext(os.path.basename(path))[0] 151 152 153def test_base_name(path): 154 return pkg2crate_name(file_base_name(path)) 155 156 157def unquote(s): # remove quotes around str 158 if s and len(s) > 1 and s[0] == s[-1] and s[0] in ('"', "'"): 159 return s[1:-1] 160 return s 161 162 163def remove_version_suffix(s): # remove -d1.d2.d3 suffix 164 if match := VERSION_SUFFIX_PAT.match(s): 165 return match.group(1) 166 return s 167 168 169def short_out_name(pkg, s): # replace /.../pkg-*/out/* with .../out/* 170 return re.sub("^/.*/" + pkg + "-[0-9a-f]*/out/", ".../out/", s) 171 172 173class Crate(object): 174 """Information of a Rust crate to collect/emit for a rules.mk module.""" 175 176 def __init__(self, runner, outf_name): 177 # Remembered global runner and its members. 178 self.runner = runner 179 self.debug = runner.args.debug 180 self.cargo_dir = "" # directory of my Cargo.toml 181 self.outf_name = outf_name # path to rules.mk 182 self.outf = None # open file handle of outf_name during dump* 183 self.has_warning = False 184 # Trusty module properties derived from rustc parameters. 185 self.module_name = "" 186 self.defaults = "" # rust_defaults used by rust_test* modules 187 self.default_srcs = False # use 'srcs' defined in self.defaults 188 self.root_pkg = "" # parent package name of a sub/test packge, from -L 189 self.srcs = [] # main_src or merged multiple source files 190 self.stem = "" # real base name of output file 191 # Kept parsed status 192 self.errors = "" # all errors found during parsing 193 self.line_num = 1 # runner told input source line number 194 self.line = "" # original rustc command line parameters 195 # Parameters collected from rustc command line. 196 self.crate_name = "" # follows --crate-name 197 self.main_src = "" # follows crate_name parameter, shortened 198 self.crate_types = [] # follows --crate-type 199 self.cfgs = [] # follows --cfg, without feature= prefix 200 self.features = [] # follows --cfg, name in 'feature="..."' 201 self.codegens = [] # follows -C, some ignored 202 self.static_libs = [] # e.g. -l static=host_cpuid 203 self.shared_libs = [] # e.g. -l dylib=wayland-client, -l z 204 self.cap_lints = "" # follows --cap-lints 205 self.emit_list = "" # e.g., --emit=dep-info,metadata,link 206 self.edition = "2015" # rustc default, e.g., --edition=2018 207 self.target = "" # follows --target 208 self.cargo_env_compat = True 209 # Parameters collected from cargo metadata output 210 self.dependencies = [] # crate dependencies output by `cargo metadata` 211 self.feature_dependencies: dict[str, List[str]] = {} # maps features to 212 # optional dependencies 213 214 def write(self, s): 215 """convenient way to output one line at a time with EOL.""" 216 assert self.outf 217 self.outf.write(s + "\n") 218 219 def find_cargo_dir(self): 220 """Deepest directory with Cargo.toml and contains the main_src.""" 221 if not is_dependent_file_path(self.main_src): 222 dir_name = os.path.dirname(self.main_src) 223 while dir_name: 224 if os.path.exists(dir_name + "/Cargo.toml"): 225 self.cargo_dir = dir_name 226 return 227 dir_name = os.path.dirname(dir_name) 228 229 def add_codegens_flag(self, flag): 230 """Ignore options not used by Trusty build system""" 231 # 'prefer-dynamic' may be set by library.mk 232 # 'embed-bitcode' is ignored; we might control LTO with other flags 233 # 'codegen-units' is set globally in engine.mk 234 # 'relocation-model' and 'target-feature=+reserve-x18' may be set by 235 # common_flags.mk 236 if not ( 237 flag.startswith("codegen-units=") 238 or flag.startswith("debuginfo=") 239 or flag.startswith("embed-bitcode=") 240 or flag.startswith("extra-filename=") 241 or flag.startswith("incremental=") 242 or flag.startswith("metadata=") 243 or flag.startswith("relocation-model=") 244 or flag == "prefer-dynamic" 245 or flag == "target-feature=+reserve-x18" 246 ): 247 self.codegens.append(flag) 248 249 def get_dependencies(self): 250 """Use output from cargo metadata to determine crate dependencies""" 251 cargo_metadata = subprocess.run( 252 [ 253 self.runner.cargo_path, 254 "metadata", 255 "--no-deps", 256 "--format-version", 257 "1", 258 ], 259 cwd=os.path.abspath(self.cargo_dir), 260 stdout=subprocess.PIPE, 261 check=False, 262 ) 263 if cargo_metadata.returncode: 264 self.errors += ( 265 "ERROR: unable to get cargo metadata to determine " 266 f"dependencies; return code {cargo_metadata.returncode}\n" 267 ) 268 else: 269 metadata_json = json.loads(cargo_metadata.stdout) 270 271 for package in metadata_json["packages"]: 272 # package names containing '-' are changed to '_' in crate_name 273 if package["name"].replace("-", "_") == self.crate_name: 274 self.dependencies = package["dependencies"] 275 for feat, props in package["features"].items(): 276 feat_deps = [ 277 d[4:] for d in props if d.startswith("dep:") 278 ] 279 if feat_deps and feat in self.feature_dependencies: 280 self.feature_dependencies[feat].extend(feat_deps) 281 else: 282 self.feature_dependencies[feat] = feat_deps 283 break 284 else: # package name not found in metadata 285 if is_build_crate_name(self.crate_name): 286 print( 287 "### WARNING: unable to determine dependencies for " 288 + f"{self.crate_name} from cargo metadata" 289 ) 290 291 def parse(self, line_num, line): 292 """Find important rustc arguments to convert to makefile rules.""" 293 self.line_num = line_num 294 self.line = line 295 args = [unquote(l) for l in line.split()] 296 i = 0 297 # Loop through every argument of rustc. 298 while i < len(args): 299 arg = args[i] 300 if arg == "--crate-name": 301 i += 1 302 self.crate_name = args[i] 303 elif arg == "--crate-type": 304 i += 1 305 # cargo calls rustc with multiple --crate-type flags. 306 # rustc can accept: 307 # --crate-type [bin|lib|rlib|dylib|cdylib|staticlib|proc-macro] 308 self.crate_types.append(args[i]) 309 elif arg == "--test": 310 self.crate_types.append("test") 311 elif arg == "--target": 312 i += 1 313 self.target = args[i] 314 elif arg == "--cfg": 315 i += 1 316 if args[i].startswith("feature="): 317 self.features.append( 318 unquote(args[i].replace("feature=", "")) 319 ) 320 else: 321 self.cfgs.append(args[i]) 322 elif arg == "--extern": 323 i += 1 324 pass # ignored; get all dependencies from cargo metadata 325 elif arg == "-C": # codegen options 326 i += 1 327 self.add_codegens_flag(args[i]) 328 elif arg.startswith("-C"): 329 # cargo has been passing "-C <xyz>" flag to rustc, 330 # but newer cargo could pass '-Cembed-bitcode=no' to rustc. 331 self.add_codegens_flag(arg[2:]) 332 elif arg == "--cap-lints": 333 i += 1 334 self.cap_lints = args[i] 335 elif arg == "-L": 336 i += 1 337 if args[i].startswith("dependency=") and args[i].endswith( 338 "/deps" 339 ): 340 if "/" + TARGET_TMP + "/" in args[i]: 341 self.root_pkg = re.sub( 342 "^.*/", 343 "", 344 re.sub("/" + TARGET_TMP + "/.*/deps$", "", args[i]), 345 ) 346 else: 347 self.root_pkg = re.sub( 348 "^.*/", 349 "", 350 re.sub("/[^/]+/[^/]+/deps$", "", args[i]), 351 ) 352 self.root_pkg = remove_version_suffix(self.root_pkg) 353 elif arg == "-l": 354 i += 1 355 if args[i].startswith("static="): 356 self.static_libs.append(re.sub("static=", "", args[i])) 357 elif args[i].startswith("dylib="): 358 self.shared_libs.append(re.sub("dylib=", "", args[i])) 359 else: 360 self.shared_libs.append(args[i]) 361 elif arg in ("--out-dir", "--color"): # ignored 362 i += 1 363 elif arg.startswith("--error-format=") or arg.startswith("--json="): 364 pass # ignored 365 elif arg.startswith("--emit="): 366 self.emit_list = arg.replace("--emit=", "") 367 elif arg.startswith("--edition="): 368 self.edition = arg.replace("--edition=", "") 369 elif arg.startswith("-Aclippy") or arg.startswith("-Wclippy"): 370 pass # TODO: emit these flags in rules.mk 371 elif arg.startswith("-W"): 372 pass # ignored 373 elif arg.startswith("-Z"): 374 pass # ignore unstable flags 375 elif arg.startswith("-D"): 376 pass # TODO: emit these flags in rules.mk 377 elif not arg.startswith("-"): 378 # shorten imported crate main source paths like $HOME/.cargo/ 379 # registry/src/github.com-1ecc6299db9ec823/memchr-2.3.3/src/ 380 # lib.rs 381 self.main_src = re.sub( 382 r"^/[^ ]*/registry/src/", ".../", args[i] 383 ) 384 self.main_src = re.sub( 385 r"^\.\.\./github.com-[0-9a-f]*/", ".../", self.main_src 386 ) 387 self.find_cargo_dir() 388 if self.cargo_dir: # for a subdirectory 389 if ( 390 self.runner.args.no_subdir 391 ): # all .mk content to /dev/null 392 self.outf_name = "/dev/null" 393 elif not self.runner.args.onefile: 394 # Write to rules.mk in the subdirectory with Cargo.toml. 395 self.outf_name = self.cargo_dir + "/rules.mk" 396 self.main_src = self.main_src[len(self.cargo_dir) + 1 :] 397 398 else: 399 self.errors += "ERROR: unknown " + arg + "\n" 400 i += 1 401 if not self.crate_name: 402 self.errors += "ERROR: missing --crate-name\n" 403 if not self.main_src: 404 self.errors += "ERROR: missing main source file\n" 405 else: 406 self.srcs.append(self.main_src) 407 if not self.crate_types: 408 # Treat "--cfg test" as "--test" 409 if "test" in self.cfgs: 410 self.crate_types.append("test") 411 else: 412 self.errors += "ERROR: missing --crate-type or --test\n" 413 elif len(self.crate_types) > 1: 414 if "test" in self.crate_types: 415 self.errors += ( 416 "ERROR: cannot handle both --crate-type and --test\n" 417 ) 418 if "lib" in self.crate_types and "rlib" in self.crate_types: 419 self.errors += ( 420 "ERROR: cannot generate both lib and rlib crate types\n" 421 ) 422 if not self.root_pkg: 423 self.root_pkg = self.crate_name 424 425 # get the package dependencies by running cargo metadata 426 if not self.skip_crate(): 427 self.get_dependencies() 428 self.cfgs = sorted(set(self.cfgs)) 429 self.features = sorted(set(self.features)) 430 self.codegens = sorted(set(self.codegens)) 431 self.static_libs = sorted(set(self.static_libs)) 432 self.shared_libs = sorted(set(self.shared_libs)) 433 self.crate_types = sorted(set(self.crate_types)) 434 self.module_name = self.stem 435 return self 436 437 def dump_line(self): 438 self.write("\n// Line " + str(self.line_num) + " " + self.line) 439 440 def feature_list(self): 441 """Return a string of main_src + "feature_list".""" 442 pkg = self.main_src 443 if pkg.startswith(".../"): # keep only the main package name 444 pkg = re.sub("/.*", "", pkg[4:]) 445 elif pkg.startswith("/"): # use relative path for a local package 446 pkg = os.path.relpath(pkg) 447 if not self.features: 448 return pkg 449 return pkg + ' "' + ",".join(self.features) + '"' 450 451 def dump_skip_crate(self, kind): 452 if self.debug: 453 self.write("\n// IGNORED: " + kind + " " + self.main_src) 454 return self 455 456 def skip_crate(self): 457 """Return crate_name or a message if this crate should be skipped.""" 458 if ( 459 is_build_crate_name(self.crate_name) 460 or self.crate_name in EXCLUDED_CRATES 461 ): 462 return self.crate_name 463 if is_dependent_file_path(self.main_src): 464 return "dependent crate" 465 return "" 466 467 def dump(self): 468 """Dump all error/debug/module code to the output rules.mk file.""" 469 self.runner.init_rules_file(self.outf_name) 470 with open(self.outf_name, "a", encoding="utf-8") as outf: 471 self.outf = outf 472 if self.errors: 473 self.dump_line() 474 self.write(self.errors) 475 elif self.skip_crate(): 476 self.dump_skip_crate(self.skip_crate()) 477 else: 478 if self.debug: 479 self.dump_debug_info() 480 self.dump_trusty_module() 481 self.outf = None 482 483 def dump_debug_info(self): 484 """Dump parsed data, when cargo2rulesmk is called with --debug.""" 485 486 def dump(name, value): 487 self.write(f"//{name:>12} = {value}") 488 489 def opt_dump(name, value): 490 if value: 491 dump(name, value) 492 493 def dump_list(fmt, values): 494 for v in values: 495 self.write(fmt % v) 496 497 self.dump_line() 498 dump("module_name", self.module_name) 499 dump("crate_name", self.crate_name) 500 dump("crate_types", self.crate_types) 501 dump("main_src", self.main_src) 502 dump("has_warning", self.has_warning) 503 opt_dump("target", self.target) 504 opt_dump("edition", self.edition) 505 opt_dump("emit_list", self.emit_list) 506 opt_dump("cap_lints", self.cap_lints) 507 dump_list("// cfg = %s", self.cfgs) 508 dump_list("// cfg = 'feature \"%s\"'", self.features) 509 # TODO(chh): escape quotes in self.features, but not in other dump_list 510 dump_list("// codegen = %s", self.codegens) 511 dump_list("// -l static = %s", self.static_libs) 512 dump_list("// -l (dylib) = %s", self.shared_libs) 513 514 def dump_trusty_module(self): 515 """Dump one or more module definitions, depending on crate_types.""" 516 if len(self.crate_types) > 1: 517 if "test" in self.crate_types: 518 self.write("\nERROR: multiple crate types cannot include test type") 519 return 520 521 if "lib" in self.crate_types: 522 print(f"### WARNING: crate {self.crate_name} has multiple " 523 f"crate types ({str(self.crate_types)}). Treating as 'lib'") 524 self.crate_types = ["lib"] 525 else: 526 self.write("\nERROR: don't know how to handle crate types of " 527 f"crate {self.crate_name}: {str(self.crate_types)}") 528 return 529 530 self.dump_single_type_trusty_module() 531 532 def dump_srcs_list(self): 533 """Dump the srcs list, for defaults or regular modules.""" 534 if len(self.srcs) > 1: 535 srcs = sorted(set(self.srcs)) # make a copy and dedup 536 else: 537 srcs = [self.main_src] 538 self.write("MODULE_SRCS := \\") 539 for src in srcs: 540 self.write(f"\t$(LOCAL_DIR)/{src} \\") 541 self.write("") 542 543 # add rust file generated by build.rs to MODULE_SRCDEPS, if any 544 # TODO(perlarsen): is there a need to support more than one output file? 545 if srcdeps := [ 546 f for f in self.runner.build_out_files if f.endswith(".rs") 547 ]: 548 assert len(srcdeps) == 1 549 outfile = srcdeps.pop() 550 lines = [ 551 f"OUT_FILE := $(call TOBUILDDIR,$(MODULE))/{outfile}", 552 f"$(OUT_FILE): $(MODULE)/out/{outfile}", 553 "\t@echo copying $< to $@", 554 "\t@$(MKDIR)", 555 "\tcp $< $@", 556 "", 557 "MODULE_RUST_ENV += OUT_DIR=$(dir $(OUT_FILE))", 558 "", 559 "MODULE_SRCDEPS := $(OUT_FILE)", 560 ] 561 self.write("\n".join(lines)) 562 563 def dump_single_type_trusty_module(self): 564 """Dump one simple Trusty module, which has only one crate_type.""" 565 crate_type = self.crate_types[0] 566 assert crate_type != "test" 567 self.dump_one_trusty_module(crate_type) 568 569 def dump_one_trusty_module(self, crate_type): 570 """Dump one Trusty module definition.""" 571 if crate_type in ["test", "bin"]: # TODO: support test crates 572 print( 573 f"### WARNING: ignoring {crate_type} crate: {self.crate_name}") 574 return 575 if self.codegens: # TODO: support crates that require codegen flags 576 print( 577 f"ERROR: {self.crate_name} uses unexpected codegen flags: " + 578 str(self.codegens) 579 ) 580 return 581 582 self.dump_core_properties() 583 if not self.defaults: 584 self.dump_edition_flags_libs() 585 586 # NOTE: a crate may list the same dependency as required and optional 587 library_deps = set() 588 for dependency in self.dependencies: 589 if dependency["kind"] in ["dev", "build"]: 590 continue 591 name = ( 592 rename 593 if (rename := dependency["rename"]) 594 else dependency["name"] 595 ) 596 if dependency["target"]: 597 print( 598 f"### WARNING: ignoring target-specific dependency: {name}") 599 continue 600 path = CUSTOM_MODULE_CRATES.get( 601 name, f"external/rust/crates/{name}" 602 ) 603 if dependency["optional"]: 604 if not any( 605 name in self.feature_dependencies.get(f, []) 606 for f in self.features 607 ): 608 continue 609 library_deps.add(path) 610 if library_deps: 611 self.write("MODULE_LIBRARY_DEPS := \\") 612 for path in sorted(library_deps): 613 self.write(f"\t{path} \\") 614 self.write("") 615 if crate_type == "test" and not self.default_srcs: 616 raise NotImplementedError("Crates with test data are not supported") 617 618 assert crate_type in LIBRARY_CRATE_TYPES 619 self.write("include make/library.mk") 620 621 def dump_edition_flags_libs(self): 622 if self.edition: 623 self.write(f"MODULE_RUST_EDITION := {self.edition}") 624 if self.features or self.cfgs: 625 self.write("MODULE_RUSTFLAGS += \\") 626 for feature in self.features: 627 self.write(f"\t--cfg 'feature=\"{feature}\"' \\") 628 for cfg in self.cfgs: 629 self.write(f"\t--cfg '{cfg}' \\") 630 self.write("") 631 632 if self.static_libs or self.shared_libs: 633 print("### WARNING: Crates with depend on static or shared " 634 "libraries are not supported") 635 636 def main_src_basename_path(self): 637 return re.sub("/", "_", re.sub(".rs$", "", self.main_src)) 638 639 def test_module_name(self): 640 """Return a unique name for a test module.""" 641 # root_pkg+(_host|_device) + '_test_'+source_file_name 642 suffix = self.main_src_basename_path() 643 return self.root_pkg + "_test_" + suffix 644 645 def dump_core_properties(self): 646 """Dump the module header, name, stem, etc.""" 647 self.write("LOCAL_DIR := $(GET_LOCAL_DIR)") 648 self.write("MODULE := $(LOCAL_DIR)") 649 self.write(f"MODULE_CRATE_NAME := {self.crate_name}") 650 651 # Trusty's module system only supports bin, rlib, and proc-macro so map 652 # lib->rlib 653 if self.crate_types != ["lib"]: 654 crate_types = set( 655 "rlib" if ct == "lib" else ct for ct in self.crate_types 656 ) 657 self.write(f'MODULE_RUST_CRATE_TYPES := {" ".join(crate_types)}') 658 659 if not self.default_srcs: 660 self.dump_srcs_list() 661 662 if hasattr(self.runner.args, "module_add_implicit_deps"): 663 if hasattr(self.runner.args, "module_add_implicit_deps_reason"): 664 self.write(self.runner.args.module_add_implicit_deps_reason) 665 666 if self.runner.args.module_add_implicit_deps in [True, "yes"]: 667 self.write("MODULE_ADD_IMPLICIT_DEPS := true") 668 elif self.runner.args.module_add_implicit_deps in [False, "no"]: 669 self.write("MODULE_ADD_IMPLICIT_DEPS := false") 670 else: 671 sys.exit( 672 "ERROR: invalid value for module_add_implicit_deps: " + 673 str(self.runner.args.module_add_implicit_deps) 674 ) 675 676 677class Runner(object): 678 """Main class to parse cargo -v output and print Trusty makefile modules.""" 679 680 def __init__(self, args): 681 self.mk_files = set() # Remember all Trusty module files. 682 self.root_pkg = "" # name of package in ./Cargo.toml 683 # Saved flags, modes, and data. 684 self.args = args 685 self.dry_run = not args.run 686 self.skip_cargo = args.skipcargo 687 self.cargo_path = "./cargo" # path to cargo, will be set later 688 self.checked_out_files = False # to check only once 689 self.build_out_files = [] # output files generated by build.rs 690 self.crates: List[Crate] = [] 691 self.warning_files = set() 692 # Keep a unique mapping from (module name) to crate 693 self.name_owners = {} 694 # Save and dump all errors from cargo to rules.mk. 695 self.errors = "" 696 self.test_errors = "" 697 self.setup_cargo_path() 698 # Default action is cargo clean, followed by build or user given actions 699 if args.cargo: 700 self.cargo = ["clean"] + args.cargo 701 else: 702 default_target = "--target x86_64-unknown-linux-gnu" 703 # Use the same target for both host and default device builds. 704 # Same target is used as default in host x86_64 Android compilation. 705 # Note: b/169872957, prebuilt cargo failed to build vsock 706 # on x86_64-unknown-linux-musl systems. 707 self.cargo = ["clean", "build " + default_target] 708 if args.tests: 709 self.cargo.append("build --tests " + default_target) 710 self.empty_tests = set() 711 self.empty_unittests = False 712 713 def setup_cargo_path(self): 714 """Find cargo in the --cargo_bin or prebuilt rust bin directory.""" 715 if self.args.cargo_bin: 716 self.cargo_path = os.path.join(self.args.cargo_bin, "cargo") 717 if not os.path.isfile(self.cargo_path): 718 sys.exit("ERROR: cannot find cargo in " + self.args.cargo_bin) 719 print("INFO: using cargo in " + self.args.cargo_bin) 720 return 721 722 # We have only tested this on Linux. 723 if platform.system() != "Linux": 724 sys.exit( 725 "ERROR: this script has only been tested on Linux with cargo." 726 ) 727 728 # Assuming that this script is in development/scripts 729 env_setup_sh = os.path.join( 730 TOP_DIR, "trusty/vendor/google/aosp/scripts/envsetup.sh" 731 ) 732 if not os.path.exists(env_setup_sh): 733 sys.exit("ERROR: missing " + env_setup_sh) 734 rust_version = self.find_rust_version(env_setup_sh) 735 self.cargo_path = os.path.join( 736 TOP_DIR, f"prebuilts/rust/linux-x86/{rust_version}/bin/cargo" 737 ) 738 739 if not os.path.isfile(self.cargo_path): 740 sys.exit( 741 "ERROR: no cargo at " 742 + self.cargo_path 743 + "; consider using the --cargo_bin= flag." 744 ) 745 746 if self.args.verbose: 747 print(f"### INFO: using cargo from {self.cargo_path}") 748 749 def find_rust_version(self, env_setup_sh): 750 """find the Rust version used by Trusty from envsetup.sh""" 751 752 version_pat = re.compile(r"prebuilts/rust/linux-x86/([0-9]+\.[0-9]+\..+)/bin") 753 754 with open(env_setup_sh) as fh: 755 for line in fh.readlines(): 756 if line.lstrip().startswith("export RUST_BINDIR"): 757 if not (result := version_pat.search(line)): 758 sys.exit("ERROR: failed to parse rust version " 759 + "from RUST_BINDIR in envsetup.sh: " 760 + line 761 ) 762 version = result.group(1) 763 764 if self.args.verbose: 765 print(f"### INFO: using rust version {version}") 766 767 return version 768 769 sys.exit("ERROR: failed to parse {env_setup_sh}; is RUST_BINDIR exported?") 770 771 def find_out_files(self): 772 # list1 has build.rs output for normal crates 773 list1 = glob.glob( 774 TARGET_TMP + "/*/*/build/" + self.root_pkg + "-*/out/*" 775 ) 776 # list2 has build.rs output for proc-macro crates 777 list2 = glob.glob(TARGET_TMP + "/*/build/" + self.root_pkg + "-*/out/*") 778 return list1 + list2 779 780 def copy_out_files(self): 781 """Copy build.rs output files to ./out and set up build_out_files.""" 782 if self.checked_out_files: 783 return 784 self.checked_out_files = True 785 cargo_out_files = self.find_out_files() 786 out_files = set() 787 if cargo_out_files: 788 os.makedirs("out", exist_ok=True) 789 for path in cargo_out_files: 790 file_name = path.split("/")[-1] 791 out_files.add(file_name) 792 shutil.copy(path, "out/" + file_name) 793 self.build_out_files = sorted(out_files) 794 795 def has_used_out_dir(self): 796 """Returns true if env!("OUT_DIR") is found.""" 797 return 0 == os.system( 798 "grep -rl --exclude build.rs --include \\*.rs" 799 + " 'env!(\"OUT_DIR\")' * > /dev/null" 800 ) 801 802 def init_rules_file(self, name): 803 # name could be rules.mk or sub_dir_path/rules.mk 804 if name not in self.mk_files: 805 self.mk_files.add(name) 806 with open(name, "w", encoding="utf-8") as outf: 807 print_args = sys.argv[1:].copy() 808 if "--cargo_bin" in print_args: 809 index = print_args.index("--cargo_bin") 810 del print_args[index : index + 2] 811 outf.write(RULES_MK_HEADER.format(args=" ".join(print_args))) 812 813 def find_root_pkg(self): 814 """Read name of [package] in ./Cargo.toml.""" 815 if not os.path.exists("./Cargo.toml"): 816 return 817 with open("./Cargo.toml", "r", encoding="utf-8") as inf: 818 pkg_section = re.compile(r"^ *\[package\]") 819 name = re.compile('^ *name *= * "([^"]*)"') 820 in_pkg = False 821 for line in inf: 822 if in_pkg: 823 if match := name.match(line): 824 self.root_pkg = match.group(1) 825 break 826 else: 827 in_pkg = pkg_section.match(line) is not None 828 829 def run_cargo(self): 830 """Calls cargo -v and save its output to ./cargo.out.""" 831 if self.skip_cargo: 832 return self 833 cargo_toml = "./Cargo.toml" 834 cargo_out = "./cargo.out" 835 836 # Do not use Cargo.lock, because Trusty makefile rules are designed 837 # to run with the latest available vendored crates in Trusty. 838 cargo_lock = "./Cargo.lock" 839 cargo_lock_saved = "./cargo.lock.saved" 840 had_cargo_lock = os.path.exists(cargo_lock) 841 if not os.access(cargo_toml, os.R_OK): 842 print("ERROR: Cannot find or read", cargo_toml) 843 return self 844 if not self.dry_run: 845 if os.path.exists(cargo_out): 846 os.remove(cargo_out) 847 if not self.args.use_cargo_lock and had_cargo_lock: # save it 848 os.rename(cargo_lock, cargo_lock_saved) 849 cmd_tail_target = " --target-dir " + TARGET_TMP 850 cmd_tail_redir = " >> " + cargo_out + " 2>&1" 851 # set up search PATH for cargo to find the correct rustc 852 saved_path = os.environ["PATH"] 853 os.environ["PATH"] = os.path.dirname(self.cargo_path) + ":" + saved_path 854 # Add [workspace] to Cargo.toml if it is not there. 855 added_workspace = False 856 cargo_toml_lines = None 857 if self.args.add_workspace: 858 with open(cargo_toml, "r", encoding="utf-8") as in_file: 859 cargo_toml_lines = in_file.readlines() 860 found_workspace = "[workspace]\n" in cargo_toml_lines 861 if found_workspace: 862 print("### WARNING: found [workspace] in Cargo.toml") 863 else: 864 with open(cargo_toml, "a", encoding="utf-8") as out_file: 865 out_file.write("\n\n[workspace]\n") 866 added_workspace = True 867 if self.args.verbose: 868 print("### INFO: added [workspace] to Cargo.toml") 869 features = "" 870 for c in self.cargo: 871 features = "" 872 if c != "clean": 873 if self.args.features is not None: 874 features = " --no-default-features" 875 if self.args.features: 876 features += " --features " + self.args.features 877 cmd_v_flag = " -vv " if self.args.vv else " -v " 878 cmd = self.cargo_path + cmd_v_flag 879 cmd += c + features + cmd_tail_target + cmd_tail_redir 880 if c != "clean": 881 rustflags = self.args.rustflags if self.args.rustflags else "" 882 # linting issues shouldn't prevent us from generating rules.mk 883 rustflags = f'RUSTFLAGS="{rustflags} --cap-lints allow" ' 884 cmd = rustflags + cmd 885 self.run_cmd(cmd, cargo_out) 886 if self.args.tests: 887 cmd = ( 888 self.cargo_path 889 + " test" 890 + features 891 + cmd_tail_target 892 + " -- --list" 893 + cmd_tail_redir 894 ) 895 self.run_cmd(cmd, cargo_out) 896 if added_workspace: # restore original Cargo.toml 897 with open(cargo_toml, "w", encoding="utf-8") as out_file: 898 assert cargo_toml_lines 899 out_file.writelines(cargo_toml_lines) 900 if self.args.verbose: 901 print("### INFO: restored original Cargo.toml") 902 os.environ["PATH"] = saved_path 903 if not self.dry_run: 904 if not had_cargo_lock: # restore to no Cargo.lock state 905 if os.path.exists(cargo_lock): 906 os.remove(cargo_lock) 907 elif not self.args.use_cargo_lock: # restore saved Cargo.lock 908 os.rename(cargo_lock_saved, cargo_lock) 909 return self 910 911 def run_cmd(self, cmd, cargo_out): 912 if self.dry_run: 913 print("Dry-run skip:", cmd) 914 else: 915 if self.args.verbose: 916 print("Running:", cmd) 917 with open(cargo_out, "a+", encoding="utf-8") as out_file: 918 out_file.write("### Running: " + cmd + "\n") 919 ret = os.system(cmd) 920 if ret != 0: 921 print( 922 "*** There was an error while running cargo. " 923 + f"See the {cargo_out} file for details." 924 ) 925 926 def apply_patch(self): 927 """Apply local patch file if it is given.""" 928 if self.args.patch: 929 if self.dry_run: 930 print("Dry-run skip patch file:", self.args.patch) 931 else: 932 if not os.path.exists(self.args.patch): 933 self.append_to_rules( 934 "ERROR cannot find patch file: " + self.args.patch 935 ) 936 return self 937 if self.args.verbose: 938 print( 939 "### INFO: applying local patch file:", self.args.patch 940 ) 941 subprocess.run( 942 [ 943 "patch", 944 "-s", 945 "--no-backup-if-mismatch", 946 "./rules.mk", 947 self.args.patch, 948 ], 949 check=True, 950 ) 951 return self 952 953 def gen_rules(self): 954 """Parse cargo.out and generate Trusty makefile rules""" 955 if self.dry_run: 956 print("Dry-run skip: read", CARGO_OUT, "write rules.mk") 957 elif os.path.exists(CARGO_OUT): 958 self.find_root_pkg() 959 if self.args.copy_out: 960 self.copy_out_files() 961 elif self.find_out_files() and self.has_used_out_dir(): 962 print( 963 "WARNING: " 964 + self.root_pkg 965 + " has cargo output files; " 966 + "please rerun with the --copy-out flag." 967 ) 968 with open(CARGO_OUT, "r", encoding="utf-8") as cargo_out: 969 self.parse(cargo_out, "rules.mk") 970 self.crates.sort(key=get_module_name) 971 for crate in self.crates: 972 crate.dump() 973 if self.errors: 974 self.append_to_rules("\n" + ERRORS_LINE + "\n" + self.errors) 975 if self.test_errors: 976 self.append_to_rules( 977 "\n// Errors when listing tests:\n" + self.test_errors 978 ) 979 return self 980 981 def add_crate(self, crate: Crate): 982 """Append crate to list unless it meets criteria for being skipped.""" 983 if crate.skip_crate(): 984 if self.args.debug: # include debug info of all crates 985 self.crates.append(crate) 986 elif crate.crate_types == set(["bin"]): 987 print("WARNING: skipping binary crate: " + crate.crate_name) 988 else: 989 self.crates.append(crate) 990 991 def find_warning_owners(self): 992 """For each warning file, find its owner crate.""" 993 missing_owner = False 994 for f in self.warning_files: 995 cargo_dir = "" # find lowest crate, with longest path 996 owner = None # owner crate of this warning 997 for c in self.crates: 998 if f.startswith(c.cargo_dir + "/") and len(cargo_dir) < len( 999 c.cargo_dir 1000 ): 1001 cargo_dir = c.cargo_dir 1002 owner = c 1003 if owner: 1004 owner.has_warning = True 1005 else: 1006 missing_owner = True 1007 if missing_owner and os.path.exists("Cargo.toml"): 1008 # owner is the root cargo, with empty cargo_dir 1009 for c in self.crates: 1010 if not c.cargo_dir: 1011 c.has_warning = True 1012 1013 def rustc_command(self, n, rustc_line, line, outf_name): 1014 """Process a rustc command line from cargo -vv output.""" 1015 # cargo build -vv output can have multiple lines for a rustc command 1016 # due to '\n' in strings for environment variables. 1017 # strip removes leading spaces and '\n' at the end 1018 new_rustc = (rustc_line.strip() + line) if rustc_line else line 1019 # Use an heuristic to detect the completions of a multi-line command. 1020 # This might fail for some very rare case, but easy to fix manually. 1021 if not line.endswith("`\n") or (new_rustc.count("`") % 2) != 0: 1022 return new_rustc 1023 if match := RUSTC_VV_CMD_ARGS.match(new_rustc): 1024 args = match.group(2) 1025 self.add_crate(Crate(self, outf_name).parse(n, args)) 1026 else: 1027 self.assert_empty_vv_line(new_rustc) 1028 return "" 1029 1030 def append_to_rules(self, line): 1031 self.init_rules_file("rules.mk") 1032 with open("rules.mk", "a", encoding="utf-8") as outf: 1033 outf.write(line) 1034 1035 def assert_empty_vv_line(self, line): 1036 if line: # report error if line is not empty 1037 self.append_to_rules("ERROR -vv line: " + line) 1038 return "" 1039 1040 def add_empty_test(self, name): 1041 if name.startswith("unittests"): 1042 self.empty_unittests = True 1043 else: 1044 self.empty_tests.add(name) 1045 1046 def should_ignore_test(self, src): 1047 # cargo test outputs the source file for integration tests but 1048 # "unittests" for unit tests. To figure out to which crate this 1049 # corresponds, we check if the current source file is the main source of 1050 # a non-test crate, e.g., a library or a binary. 1051 return ( 1052 src in self.args.test_blocklist 1053 or src in self.empty_tests 1054 or ( 1055 self.empty_unittests 1056 and src 1057 in [ 1058 c.main_src for c in self.crates if c.crate_types != ["test"] 1059 ] 1060 ) 1061 ) 1062 1063 def parse(self, inf, outf_name): 1064 """Parse rustc, test, and warning messages in input file.""" 1065 n = 0 # line number 1066 # We read the file in two passes, where the first simply checks for 1067 # empty tests. Otherwise we would add and merge tests before seeing 1068 # they're empty. 1069 cur_test_name = None 1070 for line in inf: 1071 if match := CARGO_TEST_LIST_START_PAT.match(line): 1072 cur_test_name = match.group(1) 1073 elif cur_test_name and ( 1074 match := CARGO_TEST_LIST_END_PAT.match(line) 1075 ): 1076 if int(match.group(1)) + int(match.group(2)) == 0: 1077 self.add_empty_test(cur_test_name) 1078 cur_test_name = None 1079 inf.seek(0) 1080 prev_warning = False # true if the previous line was warning: ... 1081 rustc_line = "" # previous line(s) matching RUSTC_VV_PAT 1082 in_tests = False 1083 for line in inf: 1084 n += 1 1085 if line.startswith("warning: "): 1086 prev_warning = True 1087 rustc_line = self.assert_empty_vv_line(rustc_line) 1088 continue 1089 new_rustc = "" 1090 if match := RUSTC_PAT.match(line): 1091 args_line = match.group(2) 1092 self.add_crate(Crate(self, outf_name).parse(n, args_line)) 1093 self.assert_empty_vv_line(rustc_line) 1094 elif rustc_line or RUSTC_VV_PAT.match(line): 1095 new_rustc = self.rustc_command(n, rustc_line, line, outf_name) 1096 elif CC_AR_VV_PAT.match(line): 1097 raise NotImplementedError("$CC or $AR commands not supported") 1098 elif prev_warning and (match := WARNING_FILE_PAT.match(line)): 1099 self.assert_empty_vv_line(rustc_line) 1100 fpath = match.group(1) 1101 if fpath[0] != "/": # ignore absolute path 1102 self.warning_files.add(fpath) 1103 elif line.startswith("error: ") or line.startswith("error[E"): 1104 if not self.args.ignore_cargo_errors: 1105 if in_tests: 1106 self.test_errors += "// " + line 1107 else: 1108 self.errors += line 1109 elif CARGO2ANDROID_RUNNING_PAT.match(line): 1110 in_tests = "cargo test" in line and "--list" in line 1111 prev_warning = False 1112 rustc_line = new_rustc 1113 self.find_warning_owners() 1114 1115 1116def get_parser(): 1117 """Parse main arguments.""" 1118 parser = argparse.ArgumentParser("cargo2rulesmk") 1119 parser.add_argument( 1120 "--add_workspace", 1121 action="store_true", 1122 default=False, 1123 help=( 1124 "append [workspace] to Cargo.toml before calling cargo," 1125 + " to treat current directory as root of package source;" 1126 + " otherwise the relative source file path in generated" 1127 + " rules.mk file will be from the parent directory." 1128 ), 1129 ) 1130 parser.add_argument( 1131 "--cargo", 1132 action="append", 1133 metavar="args_string", 1134 help=( 1135 "extra cargo build -v args in a string, " 1136 + "each --cargo flag calls cargo build -v once" 1137 ), 1138 ) 1139 parser.add_argument( 1140 "--cargo_bin", 1141 type=str, 1142 help="use cargo in the cargo_bin directory instead of the prebuilt one", 1143 ) 1144 parser.add_argument( 1145 "--copy-out", 1146 action="store_true", 1147 default=False, 1148 help=( 1149 "only for root directory, " 1150 + "copy build.rs output to ./out/* and declare source deps " 1151 + "for ./out/*.rs; for crates with code pattern: " 1152 + 'include!(concat!(env!("OUT_DIR"), "/<some_file>.rs"))' 1153 ), 1154 ) 1155 parser.add_argument( 1156 "--debug", 1157 action="store_true", 1158 default=False, 1159 help="dump debug info into rules.mk", 1160 ) 1161 parser.add_argument( 1162 "--features", 1163 type=str, 1164 help=( 1165 "pass features to cargo build, " 1166 + "empty string means no default features" 1167 ), 1168 ) 1169 parser.add_argument( 1170 "--ignore-cargo-errors", 1171 action="store_true", 1172 default=False, 1173 help="do not append cargo/rustc error messages to rules.mk", 1174 ) 1175 parser.add_argument( 1176 "--no-subdir", 1177 action="store_true", 1178 default=False, 1179 help="do not output anything for sub-directories", 1180 ) 1181 parser.add_argument( 1182 "--onefile", 1183 action="store_true", 1184 default=False, 1185 help=( 1186 "output all into one ./rules.mk, default will generate " 1187 + "one rules.mk per Cargo.toml in subdirectories" 1188 ), 1189 ) 1190 parser.add_argument( 1191 "--patch", 1192 type=str, 1193 help="apply the given patch file to generated ./rules.mk", 1194 ) 1195 parser.add_argument( 1196 "--run", 1197 action="store_true", 1198 default=False, 1199 help="run it, default is dry-run", 1200 ) 1201 parser.add_argument("--rustflags", type=str, help="passing flags to rustc") 1202 parser.add_argument( 1203 "--skipcargo", 1204 action="store_true", 1205 default=False, 1206 help="skip cargo command, parse cargo.out, and generate ./rules.mk", 1207 ) 1208 parser.add_argument( 1209 "--tests", 1210 action="store_true", 1211 default=False, 1212 help="run cargo build --tests after normal build", 1213 ) 1214 parser.add_argument( 1215 "--use-cargo-lock", 1216 action="store_true", 1217 default=False, 1218 help=( 1219 "run cargo build with existing Cargo.lock " 1220 + "(used when some latest dependent crates failed)" 1221 ), 1222 ) 1223 parser.add_argument( 1224 "--test-data", 1225 nargs="*", 1226 default=[], 1227 help=( 1228 "Add the given file to the given test's data property. " 1229 + "Usage: test-path=data-path" 1230 ), 1231 ) 1232 parser.add_argument( 1233 "--dependency-blocklist", 1234 nargs="*", 1235 default=[], 1236 help="Do not emit the given dependencies (without lib prefixes).", 1237 ) 1238 parser.add_argument( 1239 "--test-blocklist", 1240 nargs="*", 1241 default=[], 1242 help=( 1243 "Do not emit the given tests. " 1244 + "Pass the path to the test file to exclude." 1245 ), 1246 ) 1247 parser.add_argument( 1248 "--cfg-blocklist", 1249 nargs="*", 1250 default=[], 1251 help="Do not emit the given cfg.", 1252 ) 1253 parser.add_argument( 1254 "--verbose", 1255 action="store_true", 1256 default=False, 1257 help="echo executed commands", 1258 ) 1259 parser.add_argument( 1260 "--vv", 1261 action="store_true", 1262 default=False, 1263 help="run cargo with -vv instead of default -v", 1264 ) 1265 parser.add_argument( 1266 "--dump-config-and-exit", 1267 type=str, 1268 help=( 1269 "Dump command-line arguments (minus this flag) to a config file and" 1270 " exit. This is intended to help migrate from command line options " 1271 "to config files." 1272 ), 1273 ) 1274 parser.add_argument( 1275 "--config", 1276 type=str, 1277 help=( 1278 "Load command-line options from the given config file. Options in " 1279 "this file will override those passed on the command line." 1280 ), 1281 ) 1282 return parser 1283 1284 1285def parse_args(parser): 1286 """Parses command-line options.""" 1287 args = parser.parse_args() 1288 # Use the values specified in a config file if one was found. 1289 if args.config: 1290 with open(args.config, "r", encoding="utf-8") as f: 1291 config = json.load(f) 1292 args_dict = vars(args) 1293 for arg in config: 1294 args_dict[arg.replace("-", "_")] = config[arg] 1295 return args 1296 1297 1298def dump_config(parser, args): 1299 """Writes the non-default command-line options to the specified file.""" 1300 args_dict = vars(args) 1301 # Filter out the arguments that have their default value. 1302 # Also filter certain "temporary" arguments. 1303 non_default_args = {} 1304 for arg in args_dict: 1305 if ( 1306 args_dict[arg] != parser.get_default(arg) 1307 and arg != "dump_config_and_exit" 1308 and arg != "config" 1309 and arg != "cargo_bin" 1310 ): 1311 non_default_args[arg.replace("_", "-")] = args_dict[arg] 1312 # Write to the specified file. 1313 with open(args.dump_config_and_exit, "w", encoding="utf-8") as f: 1314 json.dump(non_default_args, f, indent=2, sort_keys=True) 1315 1316 1317def main(): 1318 parser = get_parser() 1319 args = parse_args(parser) 1320 if not args.run: # default is dry-run 1321 print(DRY_RUN_NOTE) 1322 if args.dump_config_and_exit: 1323 dump_config(parser, args) 1324 else: 1325 Runner(args).run_cargo().gen_rules().apply_patch() 1326 1327 1328if __name__ == "__main__": 1329 main() 1330