xref: /aosp_15_r20/development/scripts/cargo2rulesmk.py (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
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