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        # TODO(perlarsen): try getting cargo from $RUST_BINDIR set in envsetup.sh
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        # Assuming that this script is in development/scripts
728        linux_dir = os.path.join(
729            TOP_DIR, "prebuilts", "rust", "linux-x86"
730        )
731        if not os.path.isdir(linux_dir):
732            sys.exit("ERROR: cannot find directory " + linux_dir)
733        rust_version = self.find_rust_version(linux_dir)
734        if self.args.verbose:
735            print(f"### INFO: using prebuilt rust version {rust_version}")
736        cargo_bin = os.path.join(linux_dir, rust_version, "bin")
737        self.cargo_path = os.path.join(cargo_bin, "cargo")
738        if not os.path.isfile(self.cargo_path):
739            sys.exit(
740                "ERROR: cannot find cargo in "
741                + cargo_bin
742                + "; please try --cargo_bin= flag."
743            )
744        return
745
746    def find_rust_version(self, linux_dir):
747        """find newest prebuilt rust version."""
748        # find the newest (largest) version number in linux_dir.
749        rust_version = (0, 0, 0)  # the prebuilt version to use
750        version_pat = re.compile(r"([0-9]+)\.([0-9]+)\.([0-9]+)$")
751        for dir_name in os.listdir(linux_dir):
752            result = version_pat.match(dir_name)
753            if not result:
754                continue
755            version = (
756                int(result.group(1)),
757                int(result.group(2)),
758                int(result.group(3)),
759            )
760            if version > rust_version:
761                rust_version = version
762        return ".".join(str(ver) for ver in rust_version)
763
764    def find_out_files(self):
765        # list1 has build.rs output for normal crates
766        list1 = glob.glob(
767            TARGET_TMP + "/*/*/build/" + self.root_pkg + "-*/out/*"
768        )
769        # list2 has build.rs output for proc-macro crates
770        list2 = glob.glob(TARGET_TMP + "/*/build/" + self.root_pkg + "-*/out/*")
771        return list1 + list2
772
773    def copy_out_files(self):
774        """Copy build.rs output files to ./out and set up build_out_files."""
775        if self.checked_out_files:
776            return
777        self.checked_out_files = True
778        cargo_out_files = self.find_out_files()
779        out_files = set()
780        if cargo_out_files:
781            os.makedirs("out", exist_ok=True)
782        for path in cargo_out_files:
783            file_name = path.split("/")[-1]
784            out_files.add(file_name)
785            shutil.copy(path, "out/" + file_name)
786        self.build_out_files = sorted(out_files)
787
788    def has_used_out_dir(self):
789        """Returns true if env!("OUT_DIR") is found."""
790        return 0 == os.system(
791            "grep -rl --exclude build.rs --include \\*.rs"
792            + " 'env!(\"OUT_DIR\")' * > /dev/null"
793        )
794
795    def init_rules_file(self, name):
796        # name could be rules.mk or sub_dir_path/rules.mk
797        if name not in self.mk_files:
798            self.mk_files.add(name)
799            with open(name, "w", encoding="utf-8") as outf:
800                print_args = sys.argv[1:].copy()
801                if "--cargo_bin" in print_args:
802                    index = print_args.index("--cargo_bin")
803                    del print_args[index : index + 2]
804                outf.write(RULES_MK_HEADER.format(args=" ".join(print_args)))
805
806    def find_root_pkg(self):
807        """Read name of [package] in ./Cargo.toml."""
808        if not os.path.exists("./Cargo.toml"):
809            return
810        with open("./Cargo.toml", "r", encoding="utf-8") as inf:
811            pkg_section = re.compile(r"^ *\[package\]")
812            name = re.compile('^ *name *= * "([^"]*)"')
813            in_pkg = False
814            for line in inf:
815                if in_pkg:
816                    if match := name.match(line):
817                        self.root_pkg = match.group(1)
818                        break
819                else:
820                    in_pkg = pkg_section.match(line) is not None
821
822    def run_cargo(self):
823        """Calls cargo -v and save its output to ./cargo.out."""
824        if self.skip_cargo:
825            return self
826        cargo_toml = "./Cargo.toml"
827        cargo_out = "./cargo.out"
828
829        # Do not use Cargo.lock, because Trusty makefile rules are designed
830        # to run with the latest available vendored crates in Trusty.
831        cargo_lock = "./Cargo.lock"
832        cargo_lock_saved = "./cargo.lock.saved"
833        had_cargo_lock = os.path.exists(cargo_lock)
834        if not os.access(cargo_toml, os.R_OK):
835            print("ERROR: Cannot find or read", cargo_toml)
836            return self
837        if not self.dry_run:
838            if os.path.exists(cargo_out):
839                os.remove(cargo_out)
840            if not self.args.use_cargo_lock and had_cargo_lock:  # save it
841                os.rename(cargo_lock, cargo_lock_saved)
842        cmd_tail_target = " --target-dir " + TARGET_TMP
843        cmd_tail_redir = " >> " + cargo_out + " 2>&1"
844        # set up search PATH for cargo to find the correct rustc
845        saved_path = os.environ["PATH"]
846        os.environ["PATH"] = os.path.dirname(self.cargo_path) + ":" + saved_path
847        # Add [workspace] to Cargo.toml if it is not there.
848        added_workspace = False
849        cargo_toml_lines = None
850        if self.args.add_workspace:
851            with open(cargo_toml, "r", encoding="utf-8") as in_file:
852                cargo_toml_lines = in_file.readlines()
853            found_workspace = "[workspace]\n" in cargo_toml_lines
854            if found_workspace:
855                print("### WARNING: found [workspace] in Cargo.toml")
856            else:
857                with open(cargo_toml, "a", encoding="utf-8") as out_file:
858                    out_file.write("\n\n[workspace]\n")
859                    added_workspace = True
860                    if self.args.verbose:
861                        print("### INFO: added [workspace] to Cargo.toml")
862        features = ""
863        for c in self.cargo:
864            features = ""
865            if c != "clean":
866                if self.args.features is not None:
867                    features = " --no-default-features"
868                if self.args.features:
869                    features += " --features " + self.args.features
870            cmd_v_flag = " -vv " if self.args.vv else " -v "
871            cmd = self.cargo_path + cmd_v_flag
872            cmd += c + features + cmd_tail_target + cmd_tail_redir
873            if c != "clean":
874                rustflags = self.args.rustflags if self.args.rustflags else ""
875                # linting issues shouldn't prevent us from generating rules.mk
876                rustflags = f'RUSTFLAGS="{rustflags} --cap-lints allow" '
877                cmd = rustflags + cmd
878            self.run_cmd(cmd, cargo_out)
879        if self.args.tests:
880            cmd = (
881                self.cargo_path
882                + " test"
883                + features
884                + cmd_tail_target
885                + " -- --list"
886                + cmd_tail_redir
887            )
888            self.run_cmd(cmd, cargo_out)
889        if added_workspace:  # restore original Cargo.toml
890            with open(cargo_toml, "w", encoding="utf-8") as out_file:
891                assert cargo_toml_lines
892                out_file.writelines(cargo_toml_lines)
893            if self.args.verbose:
894                print("### INFO: restored original Cargo.toml")
895        os.environ["PATH"] = saved_path
896        if not self.dry_run:
897            if not had_cargo_lock:  # restore to no Cargo.lock state
898                if os.path.exists(cargo_lock):
899                    os.remove(cargo_lock)
900            elif not self.args.use_cargo_lock:  # restore saved Cargo.lock
901                os.rename(cargo_lock_saved, cargo_lock)
902        return self
903
904    def run_cmd(self, cmd, cargo_out):
905        if self.dry_run:
906            print("Dry-run skip:", cmd)
907        else:
908            if self.args.verbose:
909                print("Running:", cmd)
910            with open(cargo_out, "a+", encoding="utf-8") as out_file:
911                out_file.write("### Running: " + cmd + "\n")
912            ret = os.system(cmd)
913            if ret != 0:
914                print(
915                    "*** There was an error while running cargo.  "
916                    + f"See the {cargo_out} file for details."
917                )
918
919    def apply_patch(self):
920        """Apply local patch file if it is given."""
921        if self.args.patch:
922            if self.dry_run:
923                print("Dry-run skip patch file:", self.args.patch)
924            else:
925                if not os.path.exists(self.args.patch):
926                    self.append_to_rules(
927                        "ERROR cannot find patch file: " + self.args.patch
928                    )
929                    return self
930                if self.args.verbose:
931                    print(
932                        "### INFO: applying local patch file:", self.args.patch
933                    )
934                subprocess.run(
935                    [
936                        "patch",
937                        "-s",
938                        "--no-backup-if-mismatch",
939                        "./rules.mk",
940                        self.args.patch,
941                    ],
942                    check=True,
943                )
944        return self
945
946    def gen_rules(self):
947        """Parse cargo.out and generate Trusty makefile rules"""
948        if self.dry_run:
949            print("Dry-run skip: read", CARGO_OUT, "write rules.mk")
950        elif os.path.exists(CARGO_OUT):
951            self.find_root_pkg()
952            if self.args.copy_out:
953                self.copy_out_files()
954            elif self.find_out_files() and self.has_used_out_dir():
955                print(
956                    "WARNING: "
957                    + self.root_pkg
958                    + " has cargo output files; "
959                    + "please rerun with the --copy-out flag."
960                )
961            with open(CARGO_OUT, "r", encoding="utf-8") as cargo_out:
962                self.parse(cargo_out, "rules.mk")
963            self.crates.sort(key=get_module_name)
964            for crate in self.crates:
965                crate.dump()
966            if self.errors:
967                self.append_to_rules("\n" + ERRORS_LINE + "\n" + self.errors)
968            if self.test_errors:
969                self.append_to_rules(
970                    "\n// Errors when listing tests:\n" + self.test_errors
971                )
972        return self
973
974    def add_crate(self, crate: Crate):
975        """Append crate to list unless it meets criteria for being skipped."""
976        if crate.skip_crate():
977            if self.args.debug:  # include debug info of all crates
978                self.crates.append(crate)
979        elif crate.crate_types == set(["bin"]):
980            print("WARNING: skipping binary crate: " + crate.crate_name)
981        else:
982            self.crates.append(crate)
983
984    def find_warning_owners(self):
985        """For each warning file, find its owner crate."""
986        missing_owner = False
987        for f in self.warning_files:
988            cargo_dir = ""  # find lowest crate, with longest path
989            owner = None  # owner crate of this warning
990            for c in self.crates:
991                if f.startswith(c.cargo_dir + "/") and len(cargo_dir) < len(
992                    c.cargo_dir
993                ):
994                    cargo_dir = c.cargo_dir
995                    owner = c
996            if owner:
997                owner.has_warning = True
998            else:
999                missing_owner = True
1000        if missing_owner and os.path.exists("Cargo.toml"):
1001            # owner is the root cargo, with empty cargo_dir
1002            for c in self.crates:
1003                if not c.cargo_dir:
1004                    c.has_warning = True
1005
1006    def rustc_command(self, n, rustc_line, line, outf_name):
1007        """Process a rustc command line from cargo -vv output."""
1008        # cargo build -vv output can have multiple lines for a rustc command
1009        # due to '\n' in strings for environment variables.
1010        # strip removes leading spaces and '\n' at the end
1011        new_rustc = (rustc_line.strip() + line) if rustc_line else line
1012        # Use an heuristic to detect the completions of a multi-line command.
1013        # This might fail for some very rare case, but easy to fix manually.
1014        if not line.endswith("`\n") or (new_rustc.count("`") % 2) != 0:
1015            return new_rustc
1016        if match := RUSTC_VV_CMD_ARGS.match(new_rustc):
1017            args = match.group(2)
1018            self.add_crate(Crate(self, outf_name).parse(n, args))
1019        else:
1020            self.assert_empty_vv_line(new_rustc)
1021        return ""
1022
1023    def append_to_rules(self, line):
1024        self.init_rules_file("rules.mk")
1025        with open("rules.mk", "a", encoding="utf-8") as outf:
1026            outf.write(line)
1027
1028    def assert_empty_vv_line(self, line):
1029        if line:  # report error if line is not empty
1030            self.append_to_rules("ERROR -vv line: " + line)
1031        return ""
1032
1033    def add_empty_test(self, name):
1034        if name.startswith("unittests"):
1035            self.empty_unittests = True
1036        else:
1037            self.empty_tests.add(name)
1038
1039    def should_ignore_test(self, src):
1040        # cargo test outputs the source file for integration tests but
1041        # "unittests" for unit tests. To figure out to which crate this
1042        # corresponds, we check if the current source file is the main source of
1043        # a non-test crate, e.g., a library or a binary.
1044        return (
1045            src in self.args.test_blocklist
1046            or src in self.empty_tests
1047            or (
1048                self.empty_unittests
1049                and src
1050                in [
1051                    c.main_src for c in self.crates if c.crate_types != ["test"]
1052                ]
1053            )
1054        )
1055
1056    def parse(self, inf, outf_name):
1057        """Parse rustc, test, and warning messages in input file."""
1058        n = 0  # line number
1059        # We read the file in two passes, where the first simply checks for
1060        # empty tests. Otherwise we would add and merge tests before seeing
1061        # they're empty.
1062        cur_test_name = None
1063        for line in inf:
1064            if match := CARGO_TEST_LIST_START_PAT.match(line):
1065                cur_test_name = match.group(1)
1066            elif cur_test_name and (
1067                match := CARGO_TEST_LIST_END_PAT.match(line)
1068            ):
1069                if int(match.group(1)) + int(match.group(2)) == 0:
1070                    self.add_empty_test(cur_test_name)
1071                cur_test_name = None
1072        inf.seek(0)
1073        prev_warning = False  # true if the previous line was warning: ...
1074        rustc_line = ""  # previous line(s) matching RUSTC_VV_PAT
1075        in_tests = False
1076        for line in inf:
1077            n += 1
1078            if line.startswith("warning: "):
1079                prev_warning = True
1080                rustc_line = self.assert_empty_vv_line(rustc_line)
1081                continue
1082            new_rustc = ""
1083            if match := RUSTC_PAT.match(line):
1084                args_line = match.group(2)
1085                self.add_crate(Crate(self, outf_name).parse(n, args_line))
1086                self.assert_empty_vv_line(rustc_line)
1087            elif rustc_line or RUSTC_VV_PAT.match(line):
1088                new_rustc = self.rustc_command(n, rustc_line, line, outf_name)
1089            elif CC_AR_VV_PAT.match(line):
1090                raise NotImplementedError("$CC or $AR commands not supported")
1091            elif prev_warning and (match := WARNING_FILE_PAT.match(line)):
1092                self.assert_empty_vv_line(rustc_line)
1093                fpath = match.group(1)
1094                if fpath[0] != "/":  # ignore absolute path
1095                    self.warning_files.add(fpath)
1096            elif line.startswith("error: ") or line.startswith("error[E"):
1097                if not self.args.ignore_cargo_errors:
1098                    if in_tests:
1099                        self.test_errors += "// " + line
1100                    else:
1101                        self.errors += line
1102            elif CARGO2ANDROID_RUNNING_PAT.match(line):
1103                in_tests = "cargo test" in line and "--list" in line
1104            prev_warning = False
1105            rustc_line = new_rustc
1106        self.find_warning_owners()
1107
1108
1109def get_parser():
1110    """Parse main arguments."""
1111    parser = argparse.ArgumentParser("cargo2rulesmk")
1112    parser.add_argument(
1113        "--add_workspace",
1114        action="store_true",
1115        default=False,
1116        help=(
1117            "append [workspace] to Cargo.toml before calling cargo,"
1118            + " to treat current directory as root of package source;"
1119            + " otherwise the relative source file path in generated"
1120            + " rules.mk file will be from the parent directory."
1121        ),
1122    )
1123    parser.add_argument(
1124        "--cargo",
1125        action="append",
1126        metavar="args_string",
1127        help=(
1128            "extra cargo build -v args in a string, "
1129            + "each --cargo flag calls cargo build -v once"
1130        ),
1131    )
1132    parser.add_argument(
1133        "--cargo_bin",
1134        type=str,
1135        help="use cargo in the cargo_bin directory instead of the prebuilt one",
1136    )
1137    parser.add_argument(
1138        "--copy-out",
1139        action="store_true",
1140        default=False,
1141        help=(
1142            "only for root directory, "
1143            + "copy build.rs output to ./out/* and declare source deps "
1144            + "for ./out/*.rs; for crates with code pattern: "
1145            + 'include!(concat!(env!("OUT_DIR"), "/<some_file>.rs"))'
1146        ),
1147    )
1148    parser.add_argument(
1149        "--debug",
1150        action="store_true",
1151        default=False,
1152        help="dump debug info into rules.mk",
1153    )
1154    parser.add_argument(
1155        "--features",
1156        type=str,
1157        help=(
1158            "pass features to cargo build, "
1159            + "empty string means no default features"
1160        ),
1161    )
1162    parser.add_argument(
1163        "--ignore-cargo-errors",
1164        action="store_true",
1165        default=False,
1166        help="do not append cargo/rustc error messages to rules.mk",
1167    )
1168    parser.add_argument(
1169        "--no-subdir",
1170        action="store_true",
1171        default=False,
1172        help="do not output anything for sub-directories",
1173    )
1174    parser.add_argument(
1175        "--onefile",
1176        action="store_true",
1177        default=False,
1178        help=(
1179            "output all into one ./rules.mk, default will generate "
1180            + "one rules.mk per Cargo.toml in subdirectories"
1181        ),
1182    )
1183    parser.add_argument(
1184        "--patch",
1185        type=str,
1186        help="apply the given patch file to generated ./rules.mk",
1187    )
1188    parser.add_argument(
1189        "--run",
1190        action="store_true",
1191        default=False,
1192        help="run it, default is dry-run",
1193    )
1194    parser.add_argument("--rustflags", type=str, help="passing flags to rustc")
1195    parser.add_argument(
1196        "--skipcargo",
1197        action="store_true",
1198        default=False,
1199        help="skip cargo command, parse cargo.out, and generate ./rules.mk",
1200    )
1201    parser.add_argument(
1202        "--tests",
1203        action="store_true",
1204        default=False,
1205        help="run cargo build --tests after normal build",
1206    )
1207    parser.add_argument(
1208        "--use-cargo-lock",
1209        action="store_true",
1210        default=False,
1211        help=(
1212            "run cargo build with existing Cargo.lock "
1213            + "(used when some latest dependent crates failed)"
1214        ),
1215    )
1216    parser.add_argument(
1217        "--test-data",
1218        nargs="*",
1219        default=[],
1220        help=(
1221            "Add the given file to the given test's data property. "
1222            + "Usage: test-path=data-path"
1223        ),
1224    )
1225    parser.add_argument(
1226        "--dependency-blocklist",
1227        nargs="*",
1228        default=[],
1229        help="Do not emit the given dependencies (without lib prefixes).",
1230    )
1231    parser.add_argument(
1232        "--test-blocklist",
1233        nargs="*",
1234        default=[],
1235        help=(
1236            "Do not emit the given tests. "
1237            + "Pass the path to the test file to exclude."
1238        ),
1239    )
1240    parser.add_argument(
1241        "--cfg-blocklist",
1242        nargs="*",
1243        default=[],
1244        help="Do not emit the given cfg.",
1245    )
1246    parser.add_argument(
1247        "--verbose",
1248        action="store_true",
1249        default=False,
1250        help="echo executed commands",
1251    )
1252    parser.add_argument(
1253        "--vv",
1254        action="store_true",
1255        default=False,
1256        help="run cargo with -vv instead of default -v",
1257    )
1258    parser.add_argument(
1259        "--dump-config-and-exit",
1260        type=str,
1261        help=(
1262            "Dump command-line arguments (minus this flag) to a config file and"
1263            " exit. This is intended to help migrate from command line options "
1264            "to config files."
1265        ),
1266    )
1267    parser.add_argument(
1268        "--config",
1269        type=str,
1270        help=(
1271            "Load command-line options from the given config file. Options in "
1272            "this file will override those passed on the command line."
1273        ),
1274    )
1275    return parser
1276
1277
1278def parse_args(parser):
1279    """Parses command-line options."""
1280    args = parser.parse_args()
1281    # Use the values specified in a config file if one was found.
1282    if args.config:
1283        with open(args.config, "r", encoding="utf-8") as f:
1284            config = json.load(f)
1285            args_dict = vars(args)
1286            for arg in config:
1287                args_dict[arg.replace("-", "_")] = config[arg]
1288    return args
1289
1290
1291def dump_config(parser, args):
1292    """Writes the non-default command-line options to the specified file."""
1293    args_dict = vars(args)
1294    # Filter out the arguments that have their default value.
1295    # Also filter certain "temporary" arguments.
1296    non_default_args = {}
1297    for arg in args_dict:
1298        if (
1299            args_dict[arg] != parser.get_default(arg)
1300            and arg != "dump_config_and_exit"
1301            and arg != "config"
1302            and arg != "cargo_bin"
1303        ):
1304            non_default_args[arg.replace("_", "-")] = args_dict[arg]
1305    # Write to the specified file.
1306    with open(args.dump_config_and_exit, "w", encoding="utf-8") as f:
1307        json.dump(non_default_args, f, indent=2, sort_keys=True)
1308
1309
1310def main():
1311    parser = get_parser()
1312    args = parse_args(parser)
1313    if not args.run:  # default is dry-run
1314        print(DRY_RUN_NOTE)
1315    if args.dump_config_and_exit:
1316        dump_config(parser, args)
1317    else:
1318        Runner(args).run_cargo().gen_rules().apply_patch()
1319
1320
1321if __name__ == "__main__":
1322    main()
1323