Remove the check performed by cargo2rulesmk.py
whether the external/rust/crates/{name} directories
exist for all dependencies of the current crate.
These directories only exist in the Android tree,
not in the minimal checkout inside the redshell sandbox.
Bug: 281857510
Test: cargo2rulesmk.py on thiserror-impl
Change-Id: I1e5d46b4de3129770ab87ebc5e58563010ae6605
1317 lines
50 KiB
Python
Executable File
1317 lines
50 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# Copyright (C) 2023 The Android Open Source Project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
"""Call cargo -v, parse its output, and generate a Trusty build system module.
|
|
|
|
Usage: Run this script in a crate workspace root directory. The Cargo.toml file
|
|
should work at least for the host platform.
|
|
|
|
Without other flags, "cargo2rulesmk.py --run" calls cargo clean, calls cargo
|
|
build -v, and generates makefile rules. The cargo build only generates crates
|
|
for the host without test crates.
|
|
|
|
If there are rustc warning messages, this script will add a warning comment to
|
|
the owner crate module in rules.mk.
|
|
"""
|
|
|
|
import argparse
|
|
import glob
|
|
import json
|
|
import os
|
|
import os.path
|
|
import platform
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
from typing import List
|
|
|
|
|
|
assert "/development/scripts" in os.path.dirname(__file__)
|
|
TOP_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
|
|
|
|
# Some Rust packages include extra unwanted crates.
|
|
# This set contains all such excluded crate names.
|
|
EXCLUDED_CRATES = {"protobuf_bin_gen_rust_do_not_use"}
|
|
|
|
|
|
CUSTOM_MODULE_CRATES = {
|
|
# This map tracks Rust crates that have special modules that
|
|
# were not generated automatically by this script. Examples
|
|
# include compiler builtins and other foundational libraries.
|
|
# It also tracks crates tht are not under external/rust/crates.
|
|
"compiler_builtins": "trusty/user/base/lib/libcompiler_builtins-rust",
|
|
"core": "trusty/user/base/lib/libcore-rust",
|
|
}
|
|
|
|
RENAME_STEM_MAP = {
|
|
# This map includes all changes to the default rust module stem names,
|
|
# which is used for output files when different from the module name.
|
|
"protoc_gen_rust": "protoc-gen-rust",
|
|
}
|
|
|
|
# Header added to all generated rules.mk files.
|
|
RULES_MK_HEADER = (
|
|
"# This file is generated by cargo2rulesmk.py {args}.\n"
|
|
+ "# Do not modify this file as changes will be overridden on upgrade.\n\n"
|
|
)
|
|
|
|
CARGO_OUT = "cargo.out" # Name of file to keep cargo build -v output.
|
|
|
|
# This should be kept in sync with tools/external_updater/crates_updater.py.
|
|
ERRORS_LINE = "Errors in " + CARGO_OUT + ":"
|
|
|
|
TARGET_TMP = "target.tmp" # Name of temporary output directory.
|
|
|
|
# Message to be displayed when this script is called without the --run flag.
|
|
DRY_RUN_NOTE = (
|
|
"Dry-run: This script uses ./"
|
|
+ TARGET_TMP
|
|
+ " for output directory,\n"
|
|
+ "runs cargo clean, runs cargo build -v, saves output to ./cargo.out,\n"
|
|
+ "and writes to rules.mk in the current and subdirectories.\n\n"
|
|
+ "To do do all of the above, use the --run flag.\n"
|
|
+ "See --help for other flags, and more usage notes in this script.\n"
|
|
)
|
|
|
|
# Cargo -v output of a call to rustc.
|
|
RUSTC_PAT = re.compile("^ +Running `(.*\/)?rustc (.*)`$")
|
|
|
|
# Cargo -vv output of a call to rustc could be split into multiple lines.
|
|
# Assume that the first line will contain some CARGO_* env definition.
|
|
RUSTC_VV_PAT = re.compile("^ +Running `.*CARGO_.*=.*$")
|
|
# The combined -vv output rustc command line pattern.
|
|
RUSTC_VV_CMD_ARGS = re.compile("^ *Running `.*CARGO_.*=.* (.*\/)?rustc (.*)`$")
|
|
|
|
# Cargo -vv output of a "cc" or "ar" command; all in one line.
|
|
CC_AR_VV_PAT = re.compile(r'^\[([^ ]*)[^\]]*\] running:? "(cc|ar)" (.*)$')
|
|
# Some package, such as ring-0.13.5, has pattern '... running "cc"'.
|
|
|
|
# Rustc output of file location path pattern for a warning message.
|
|
WARNING_FILE_PAT = re.compile("^ *--> ([^:]*):[0-9]+")
|
|
|
|
# cargo test --list output of the start of running a binary.
|
|
CARGO_TEST_LIST_START_PAT = re.compile(r"^\s*Running (.*) \(.*\)$")
|
|
|
|
# cargo test --list output of the end of running a binary.
|
|
CARGO_TEST_LIST_END_PAT = re.compile(r"^(\d+) tests?, (\d+) benchmarks$")
|
|
|
|
CARGO2ANDROID_RUNNING_PAT = re.compile("^### Running: .*$")
|
|
|
|
# Rust package name with suffix -d1.d2.d3(+.*)?.
|
|
VERSION_SUFFIX_PAT = re.compile(
|
|
r"^(.*)-[0-9]+\.[0-9]+\.[0-9]+(?:-(alpha|beta)\.[0-9]+)?(?:\+.*)?$"
|
|
)
|
|
|
|
# Crate types corresponding to a C ABI library
|
|
C_LIBRARY_CRATE_TYPES = ["staticlib", "cdylib"]
|
|
# Crate types corresponding to a Rust ABI library
|
|
RUST_LIBRARY_CRATE_TYPES = ["lib", "rlib", "dylib", "proc-macro"]
|
|
# Crate types corresponding to a library
|
|
LIBRARY_CRATE_TYPES = C_LIBRARY_CRATE_TYPES + RUST_LIBRARY_CRATE_TYPES
|
|
|
|
|
|
def altered_stem(name):
|
|
return RENAME_STEM_MAP[name] if (name in RENAME_STEM_MAP) else name
|
|
|
|
|
|
def is_build_crate_name(name):
|
|
# We added special prefix to build script crate names.
|
|
return name.startswith("build_script_")
|
|
|
|
|
|
def is_dependent_file_path(path):
|
|
# Absolute or dependent '.../' paths are not main files of this crate.
|
|
return path.startswith("/") or path.startswith(".../")
|
|
|
|
|
|
def get_module_name(crate): # to sort crates in a list
|
|
return crate.module_name
|
|
|
|
|
|
def pkg2crate_name(s):
|
|
return s.replace("-", "_").replace(".", "_")
|
|
|
|
|
|
def file_base_name(path):
|
|
return os.path.splitext(os.path.basename(path))[0]
|
|
|
|
|
|
def test_base_name(path):
|
|
return pkg2crate_name(file_base_name(path))
|
|
|
|
|
|
def unquote(s): # remove quotes around str
|
|
if s and len(s) > 1 and s[0] == s[-1] and s[0] in ('"', "'"):
|
|
return s[1:-1]
|
|
return s
|
|
|
|
|
|
def remove_version_suffix(s): # remove -d1.d2.d3 suffix
|
|
if match := VERSION_SUFFIX_PAT.match(s):
|
|
return match.group(1)
|
|
return s
|
|
|
|
|
|
def short_out_name(pkg, s): # replace /.../pkg-*/out/* with .../out/*
|
|
return re.sub("^/.*/" + pkg + "-[0-9a-f]*/out/", ".../out/", s)
|
|
|
|
|
|
class Crate(object):
|
|
"""Information of a Rust crate to collect/emit for a rules.mk module."""
|
|
|
|
def __init__(self, runner, outf_name):
|
|
# Remembered global runner and its members.
|
|
self.runner = runner
|
|
self.debug = runner.args.debug
|
|
self.cargo_dir = "" # directory of my Cargo.toml
|
|
self.outf_name = outf_name # path to rules.mk
|
|
self.outf = None # open file handle of outf_name during dump*
|
|
self.has_warning = False
|
|
# Trusty module properties derived from rustc parameters.
|
|
self.module_name = ""
|
|
self.defaults = "" # rust_defaults used by rust_test* modules
|
|
self.default_srcs = False # use 'srcs' defined in self.defaults
|
|
self.root_pkg = "" # parent package name of a sub/test packge, from -L
|
|
self.srcs = [] # main_src or merged multiple source files
|
|
self.stem = "" # real base name of output file
|
|
# Kept parsed status
|
|
self.errors = "" # all errors found during parsing
|
|
self.line_num = 1 # runner told input source line number
|
|
self.line = "" # original rustc command line parameters
|
|
# Parameters collected from rustc command line.
|
|
self.crate_name = "" # follows --crate-name
|
|
self.main_src = "" # follows crate_name parameter, shortened
|
|
self.crate_types = [] # follows --crate-type
|
|
self.cfgs = [] # follows --cfg, without feature= prefix
|
|
self.features = [] # follows --cfg, name in 'feature="..."'
|
|
self.codegens = [] # follows -C, some ignored
|
|
self.static_libs = [] # e.g. -l static=host_cpuid
|
|
self.shared_libs = [] # e.g. -l dylib=wayland-client, -l z
|
|
self.cap_lints = "" # follows --cap-lints
|
|
self.emit_list = "" # e.g., --emit=dep-info,metadata,link
|
|
self.edition = "2015" # rustc default, e.g., --edition=2018
|
|
self.target = "" # follows --target
|
|
self.cargo_env_compat = True
|
|
# Parameters collected from cargo metadata output
|
|
self.dependencies = [] # crate dependencies output by `cargo metadata`
|
|
self.feature_dependencies: dict[str, List[str]] = {} # maps features to
|
|
# optional dependencies
|
|
|
|
def write(self, s):
|
|
"""convenient way to output one line at a time with EOL."""
|
|
assert self.outf
|
|
self.outf.write(s + "\n")
|
|
|
|
def find_cargo_dir(self):
|
|
"""Deepest directory with Cargo.toml and contains the main_src."""
|
|
if not is_dependent_file_path(self.main_src):
|
|
dir_name = os.path.dirname(self.main_src)
|
|
while dir_name:
|
|
if os.path.exists(dir_name + "/Cargo.toml"):
|
|
self.cargo_dir = dir_name
|
|
return
|
|
dir_name = os.path.dirname(dir_name)
|
|
|
|
def add_codegens_flag(self, flag):
|
|
"""Ignore options not used by Trusty build system"""
|
|
# 'prefer-dynamic' may be set by library.mk
|
|
# 'embed-bitcode' is ignored; we might control LTO with other flags
|
|
# 'codegen-units' is set globally in engine.mk
|
|
# 'relocation-model' and 'target-feature=+reserve-x18' may be set by
|
|
# common_flags.mk
|
|
if not (
|
|
flag.startswith("codegen-units=")
|
|
or flag.startswith("debuginfo=")
|
|
or flag.startswith("embed-bitcode=")
|
|
or flag.startswith("extra-filename=")
|
|
or flag.startswith("incremental=")
|
|
or flag.startswith("metadata=")
|
|
or flag.startswith("relocation-model=")
|
|
or flag == "prefer-dynamic"
|
|
or flag == "target-feature=+reserve-x18"
|
|
):
|
|
self.codegens.append(flag)
|
|
|
|
def get_dependencies(self):
|
|
"""Use output from cargo metadata to determine crate dependencies"""
|
|
cargo_metadata = subprocess.run(
|
|
[
|
|
self.runner.cargo_path,
|
|
"metadata",
|
|
"--no-deps",
|
|
"--format-version",
|
|
"1",
|
|
],
|
|
cwd=os.path.abspath(self.cargo_dir),
|
|
stdout=subprocess.PIPE,
|
|
check=False,
|
|
)
|
|
if cargo_metadata.returncode:
|
|
self.errors += (
|
|
"ERROR: unable to get cargo metadata to determine "
|
|
f"dependencies; return code {cargo_metadata.returncode}\n"
|
|
)
|
|
else:
|
|
metadata_json = json.loads(cargo_metadata.stdout)
|
|
|
|
for package in metadata_json["packages"]:
|
|
# package names containing '-' are changed to '_' in crate_name
|
|
if package["name"].replace("-", "_") == self.crate_name:
|
|
self.dependencies = package["dependencies"]
|
|
for feat, props in package["features"].items():
|
|
feat_deps = [
|
|
d[4:] for d in props if d.startswith("dep:")
|
|
]
|
|
if feat_deps and feat in self.feature_dependencies:
|
|
self.feature_dependencies[feat].extend(feat_deps)
|
|
else:
|
|
self.feature_dependencies[feat] = feat_deps
|
|
break
|
|
else: # package name not found in metadata
|
|
if is_build_crate_name(self.crate_name):
|
|
print(
|
|
"### WARNING: unable to determine dependencies for "
|
|
+ f"{self.crate_name} from cargo metadata"
|
|
)
|
|
|
|
def parse(self, line_num, line):
|
|
"""Find important rustc arguments to convert to makefile rules."""
|
|
self.line_num = line_num
|
|
self.line = line
|
|
args = [unquote(l) for l in line.split()]
|
|
i = 0
|
|
# Loop through every argument of rustc.
|
|
while i < len(args):
|
|
arg = args[i]
|
|
if arg == "--crate-name":
|
|
i += 1
|
|
self.crate_name = args[i]
|
|
elif arg == "--crate-type":
|
|
i += 1
|
|
# cargo calls rustc with multiple --crate-type flags.
|
|
# rustc can accept:
|
|
# --crate-type [bin|lib|rlib|dylib|cdylib|staticlib|proc-macro]
|
|
self.crate_types.append(args[i])
|
|
elif arg == "--test":
|
|
self.crate_types.append("test")
|
|
elif arg == "--target":
|
|
i += 1
|
|
self.target = args[i]
|
|
elif arg == "--cfg":
|
|
i += 1
|
|
if args[i].startswith("feature="):
|
|
self.features.append(
|
|
unquote(args[i].replace("feature=", ""))
|
|
)
|
|
else:
|
|
self.cfgs.append(args[i])
|
|
elif arg == "--extern":
|
|
i += 1
|
|
pass # ignored; get all dependencies from cargo metadata
|
|
elif arg == "-C": # codegen options
|
|
i += 1
|
|
self.add_codegens_flag(args[i])
|
|
elif arg.startswith("-C"):
|
|
# cargo has been passing "-C <xyz>" flag to rustc,
|
|
# but newer cargo could pass '-Cembed-bitcode=no' to rustc.
|
|
self.add_codegens_flag(arg[2:])
|
|
elif arg == "--cap-lints":
|
|
i += 1
|
|
self.cap_lints = args[i]
|
|
elif arg == "-L":
|
|
i += 1
|
|
if args[i].startswith("dependency=") and args[i].endswith(
|
|
"/deps"
|
|
):
|
|
if "/" + TARGET_TMP + "/" in args[i]:
|
|
self.root_pkg = re.sub(
|
|
"^.*/",
|
|
"",
|
|
re.sub("/" + TARGET_TMP + "/.*/deps$", "", args[i]),
|
|
)
|
|
else:
|
|
self.root_pkg = re.sub(
|
|
"^.*/",
|
|
"",
|
|
re.sub("/[^/]+/[^/]+/deps$", "", args[i]),
|
|
)
|
|
self.root_pkg = remove_version_suffix(self.root_pkg)
|
|
elif arg == "-l":
|
|
i += 1
|
|
if args[i].startswith("static="):
|
|
self.static_libs.append(re.sub("static=", "", args[i]))
|
|
elif args[i].startswith("dylib="):
|
|
self.shared_libs.append(re.sub("dylib=", "", args[i]))
|
|
else:
|
|
self.shared_libs.append(args[i])
|
|
elif arg in ("--out-dir", "--color"): # ignored
|
|
i += 1
|
|
elif arg.startswith("--error-format=") or arg.startswith("--json="):
|
|
pass # ignored
|
|
elif arg.startswith("--emit="):
|
|
self.emit_list = arg.replace("--emit=", "")
|
|
elif arg.startswith("--edition="):
|
|
self.edition = arg.replace("--edition=", "")
|
|
elif arg.startswith("-Aclippy") or arg.startswith("-Wclippy"):
|
|
pass # TODO: emit these flags in rules.mk
|
|
elif arg.startswith("-W"):
|
|
pass # ignored
|
|
elif arg.startswith("-Z"):
|
|
pass # ignore unstable flags
|
|
elif arg.startswith("-D"):
|
|
pass # TODO: emit these flags in rules.mk
|
|
elif not arg.startswith("-"):
|
|
# shorten imported crate main source paths like $HOME/.cargo/
|
|
# registry/src/github.com-1ecc6299db9ec823/memchr-2.3.3/src/
|
|
# lib.rs
|
|
self.main_src = re.sub(
|
|
r"^/[^ ]*/registry/src/", ".../", args[i]
|
|
)
|
|
self.main_src = re.sub(
|
|
r"^\.\.\./github.com-[0-9a-f]*/", ".../", self.main_src
|
|
)
|
|
self.find_cargo_dir()
|
|
if self.cargo_dir: # for a subdirectory
|
|
if (
|
|
self.runner.args.no_subdir
|
|
): # all .mk content to /dev/null
|
|
self.outf_name = "/dev/null"
|
|
elif not self.runner.args.onefile:
|
|
# Write to rules.mk in the subdirectory with Cargo.toml.
|
|
self.outf_name = self.cargo_dir + "/rules.mk"
|
|
self.main_src = self.main_src[len(self.cargo_dir) + 1 :]
|
|
|
|
else:
|
|
self.errors += "ERROR: unknown " + arg + "\n"
|
|
i += 1
|
|
if not self.crate_name:
|
|
self.errors += "ERROR: missing --crate-name\n"
|
|
if not self.main_src:
|
|
self.errors += "ERROR: missing main source file\n"
|
|
else:
|
|
self.srcs.append(self.main_src)
|
|
if not self.crate_types:
|
|
# Treat "--cfg test" as "--test"
|
|
if "test" in self.cfgs:
|
|
self.crate_types.append("test")
|
|
else:
|
|
self.errors += "ERROR: missing --crate-type or --test\n"
|
|
elif len(self.crate_types) > 1:
|
|
if "test" in self.crate_types:
|
|
self.errors += (
|
|
"ERROR: cannot handle both --crate-type and --test\n"
|
|
)
|
|
if "lib" in self.crate_types and "rlib" in self.crate_types:
|
|
self.errors += (
|
|
"ERROR: cannot generate both lib and rlib crate types\n"
|
|
)
|
|
if not self.root_pkg:
|
|
self.root_pkg = self.crate_name
|
|
|
|
# get the package dependencies by running cargo metadata
|
|
if not self.skip_crate():
|
|
self.get_dependencies()
|
|
self.cfgs = sorted(set(self.cfgs))
|
|
self.features = sorted(set(self.features))
|
|
self.codegens = sorted(set(self.codegens))
|
|
self.static_libs = sorted(set(self.static_libs))
|
|
self.shared_libs = sorted(set(self.shared_libs))
|
|
self.crate_types = sorted(set(self.crate_types))
|
|
self.module_name = self.stem
|
|
return self
|
|
|
|
def dump_line(self):
|
|
self.write("\n// Line " + str(self.line_num) + " " + self.line)
|
|
|
|
def feature_list(self):
|
|
"""Return a string of main_src + "feature_list"."""
|
|
pkg = self.main_src
|
|
if pkg.startswith(".../"): # keep only the main package name
|
|
pkg = re.sub("/.*", "", pkg[4:])
|
|
elif pkg.startswith("/"): # use relative path for a local package
|
|
pkg = os.path.relpath(pkg)
|
|
if not self.features:
|
|
return pkg
|
|
return pkg + ' "' + ",".join(self.features) + '"'
|
|
|
|
def dump_skip_crate(self, kind):
|
|
if self.debug:
|
|
self.write("\n// IGNORED: " + kind + " " + self.main_src)
|
|
return self
|
|
|
|
def skip_crate(self):
|
|
"""Return crate_name or a message if this crate should be skipped."""
|
|
if (
|
|
is_build_crate_name(self.crate_name)
|
|
or self.crate_name in EXCLUDED_CRATES
|
|
):
|
|
return self.crate_name
|
|
if is_dependent_file_path(self.main_src):
|
|
return "dependent crate"
|
|
return ""
|
|
|
|
def dump(self):
|
|
"""Dump all error/debug/module code to the output rules.mk file."""
|
|
self.runner.init_rules_file(self.outf_name)
|
|
with open(self.outf_name, "a", encoding="utf-8") as outf:
|
|
self.outf = outf
|
|
if self.errors:
|
|
self.dump_line()
|
|
self.write(self.errors)
|
|
elif self.skip_crate():
|
|
self.dump_skip_crate(self.skip_crate())
|
|
else:
|
|
if self.debug:
|
|
self.dump_debug_info()
|
|
self.dump_trusty_module()
|
|
self.outf = None
|
|
|
|
def dump_debug_info(self):
|
|
"""Dump parsed data, when cargo2rulesmk is called with --debug."""
|
|
|
|
def dump(name, value):
|
|
self.write(f"//{name:>12} = {value}")
|
|
|
|
def opt_dump(name, value):
|
|
if value:
|
|
dump(name, value)
|
|
|
|
def dump_list(fmt, values):
|
|
for v in values:
|
|
self.write(fmt % v)
|
|
|
|
self.dump_line()
|
|
dump("module_name", self.module_name)
|
|
dump("crate_name", self.crate_name)
|
|
dump("crate_types", self.crate_types)
|
|
dump("main_src", self.main_src)
|
|
dump("has_warning", self.has_warning)
|
|
opt_dump("target", self.target)
|
|
opt_dump("edition", self.edition)
|
|
opt_dump("emit_list", self.emit_list)
|
|
opt_dump("cap_lints", self.cap_lints)
|
|
dump_list("// cfg = %s", self.cfgs)
|
|
dump_list("// cfg = 'feature \"%s\"'", self.features)
|
|
# TODO(chh): escape quotes in self.features, but not in other dump_list
|
|
dump_list("// codegen = %s", self.codegens)
|
|
dump_list("// -l static = %s", self.static_libs)
|
|
dump_list("// -l (dylib) = %s", self.shared_libs)
|
|
|
|
def dump_trusty_module(self):
|
|
"""Dump one or more module definitions, depending on crate_types."""
|
|
if len(self.crate_types) > 1:
|
|
if "test" in self.crate_types:
|
|
self.write("\nERROR: multiple crate types cannot include test type")
|
|
return
|
|
|
|
if "lib" in self.crate_types:
|
|
print(f"### WARNING: crate {self.crate_name} has multiple "
|
|
f"crate types ({str(self.crate_types)}). Treating as 'lib'")
|
|
self.crate_types = ["lib"]
|
|
else:
|
|
self.write("\nERROR: don't know how to handle crate types of "
|
|
f"crate {self.crate_name}: {str(self.crate_types)}")
|
|
return
|
|
|
|
self.dump_single_type_trusty_module()
|
|
|
|
def dump_srcs_list(self):
|
|
"""Dump the srcs list, for defaults or regular modules."""
|
|
if len(self.srcs) > 1:
|
|
srcs = sorted(set(self.srcs)) # make a copy and dedup
|
|
else:
|
|
srcs = [self.main_src]
|
|
self.write("MODULE_SRCS := \\")
|
|
for src in srcs:
|
|
self.write(f"\t$(LOCAL_DIR)/{src} \\")
|
|
self.write("")
|
|
|
|
# add rust file generated by build.rs to MODULE_SRCDEPS, if any
|
|
# TODO(perlarsen): is there a need to support more than one output file?
|
|
if srcdeps := [
|
|
f for f in self.runner.build_out_files if f.endswith(".rs")
|
|
]:
|
|
assert len(srcdeps) == 1
|
|
outfile = srcdeps.pop()
|
|
lines = [
|
|
f"OUT_FILE := $(call TOBUILDDIR,$(MODULE))/{outfile}",
|
|
f"$(OUT_FILE): $(MODULE)/out/{outfile}",
|
|
"\t@echo copying $< to $@",
|
|
"\t@$(MKDIR)",
|
|
"\tcp $< $@",
|
|
"",
|
|
"MODULE_RUST_ENV += OUT_DIR=$(dir $(OUT_FILE))",
|
|
"",
|
|
"MODULE_SRCDEPS := $(OUT_FILE)",
|
|
]
|
|
self.write("\n".join(lines))
|
|
|
|
def dump_single_type_trusty_module(self):
|
|
"""Dump one simple Trusty module, which has only one crate_type."""
|
|
crate_type = self.crate_types[0]
|
|
assert crate_type != "test"
|
|
self.dump_one_trusty_module(crate_type)
|
|
|
|
def dump_one_trusty_module(self, crate_type):
|
|
"""Dump one Trusty module definition."""
|
|
if crate_type in ["test", "bin"]: # TODO: support test crates
|
|
print(
|
|
f"### WARNING: ignoring {crate_type} crate: {self.crate_name}")
|
|
return
|
|
if self.codegens: # TODO: support crates that require codegen flags
|
|
print(
|
|
f"ERROR: {self.crate_name} uses unexpected codegen flags: " +
|
|
str(self.codegens)
|
|
)
|
|
return
|
|
|
|
self.dump_core_properties()
|
|
if not self.defaults:
|
|
self.dump_edition_flags_libs()
|
|
|
|
# TODO(perlarsen): improve and clean up dependency handling
|
|
library_deps = []
|
|
for dependency in self.dependencies:
|
|
if dependency["kind"] in ["dev", "build"]:
|
|
continue
|
|
name = (
|
|
rename
|
|
if (rename := dependency["rename"])
|
|
else dependency["name"]
|
|
)
|
|
path = CUSTOM_MODULE_CRATES.get(
|
|
name, f"external/rust/crates/{name}"
|
|
)
|
|
if dependency["optional"]:
|
|
if feats := [
|
|
f
|
|
for f in self.features
|
|
if name in self.feature_dependencies.get(f, [])
|
|
]:
|
|
continue
|
|
library_deps.append(path)
|
|
if library_deps:
|
|
self.write("MODULE_LIBRARY_DEPS := \\")
|
|
for path in library_deps:
|
|
self.write(f"\t{path} \\")
|
|
self.write("")
|
|
if crate_type == "test" and not self.default_srcs:
|
|
raise NotImplementedError("Crates with test data are not supported")
|
|
|
|
assert crate_type in LIBRARY_CRATE_TYPES
|
|
self.write("include make/library.mk")
|
|
|
|
def dump_edition_flags_libs(self):
|
|
if self.edition:
|
|
self.write(f"MODULE_RUST_EDITION := {self.edition}")
|
|
if self.features or self.cfgs:
|
|
self.write("MODULE_RUSTFLAGS += \\")
|
|
for feature in self.features:
|
|
self.write(f"\t--cfg 'feature=\"{feature}\"' \\")
|
|
for cfg in self.cfgs:
|
|
self.write(f"\t--cfg '{cfg}' \\")
|
|
self.write("")
|
|
|
|
if self.static_libs or self.shared_libs:
|
|
print("### WARNING: Crates with depend on static or shared "
|
|
"libraries are not supported")
|
|
|
|
def main_src_basename_path(self):
|
|
return re.sub("/", "_", re.sub(".rs$", "", self.main_src))
|
|
|
|
def test_module_name(self):
|
|
"""Return a unique name for a test module."""
|
|
# root_pkg+(_host|_device) + '_test_'+source_file_name
|
|
suffix = self.main_src_basename_path()
|
|
return self.root_pkg + "_test_" + suffix
|
|
|
|
def dump_core_properties(self):
|
|
"""Dump the module header, name, stem, etc."""
|
|
self.write("LOCAL_DIR := $(GET_LOCAL_DIR)")
|
|
self.write("MODULE := $(LOCAL_DIR)")
|
|
self.write(f"MODULE_CRATE_NAME := {self.crate_name}")
|
|
|
|
# Trusty's module system only supports bin, rlib, and proc-macro so map
|
|
# lib->rlib
|
|
if self.crate_types != ["lib"]:
|
|
crate_types = set(
|
|
"rlib" if ct == "lib" else ct for ct in self.crate_types
|
|
)
|
|
self.write(f'MODULE_RUST_CRATE_TYPES := {" ".join(crate_types)}')
|
|
|
|
if not self.default_srcs:
|
|
self.dump_srcs_list()
|
|
|
|
if hasattr(self.runner.args, "module_add_implicit_deps"):
|
|
if hasattr(self.runner.args, "module_add_implicit_deps_reason"):
|
|
self.write(self.runner.args.module_add_implicit_deps_reason)
|
|
|
|
if self.runner.args.module_add_implicit_deps in [True, "yes"]:
|
|
self.write("MODULE_ADD_IMPLICIT_DEPS := true")
|
|
elif self.runner.args.module_add_implicit_deps in [False, "no"]:
|
|
self.write("MODULE_ADD_IMPLICIT_DEPS := false")
|
|
else:
|
|
sys.exit(
|
|
"ERROR: invalid value for module_add_implicit_deps: " +
|
|
str(self.runner.args.module_add_implicit_deps)
|
|
)
|
|
|
|
|
|
class Runner(object):
|
|
"""Main class to parse cargo -v output and print Trusty makefile modules."""
|
|
|
|
def __init__(self, args):
|
|
self.mk_files = set() # Remember all Trusty module files.
|
|
self.root_pkg = "" # name of package in ./Cargo.toml
|
|
# Saved flags, modes, and data.
|
|
self.args = args
|
|
self.dry_run = not args.run
|
|
self.skip_cargo = args.skipcargo
|
|
self.cargo_path = "./cargo" # path to cargo, will be set later
|
|
self.checked_out_files = False # to check only once
|
|
self.build_out_files = [] # output files generated by build.rs
|
|
self.crates: List[Crate] = []
|
|
self.warning_files = set()
|
|
# Keep a unique mapping from (module name) to crate
|
|
self.name_owners = {}
|
|
# Save and dump all errors from cargo to rules.mk.
|
|
self.errors = ""
|
|
self.test_errors = ""
|
|
self.setup_cargo_path()
|
|
# Default action is cargo clean, followed by build or user given actions
|
|
if args.cargo:
|
|
self.cargo = ["clean"] + args.cargo
|
|
else:
|
|
default_target = "--target x86_64-unknown-linux-gnu"
|
|
# Use the same target for both host and default device builds.
|
|
# Same target is used as default in host x86_64 Android compilation.
|
|
# Note: b/169872957, prebuilt cargo failed to build vsock
|
|
# on x86_64-unknown-linux-musl systems.
|
|
self.cargo = ["clean", "build " + default_target]
|
|
if args.tests:
|
|
self.cargo.append("build --tests " + default_target)
|
|
self.empty_tests = set()
|
|
self.empty_unittests = False
|
|
|
|
def setup_cargo_path(self):
|
|
"""Find cargo in the --cargo_bin or prebuilt rust bin directory."""
|
|
if self.args.cargo_bin:
|
|
self.cargo_path = os.path.join(self.args.cargo_bin, "cargo")
|
|
if not os.path.isfile(self.cargo_path):
|
|
sys.exit("ERROR: cannot find cargo in " + self.args.cargo_bin)
|
|
print("INFO: using cargo in " + self.args.cargo_bin)
|
|
return
|
|
# TODO(perlarsen): try getting cargo from $RUST_BINDIR set in envsetup.sh
|
|
# We have only tested this on Linux.
|
|
if platform.system() != "Linux":
|
|
sys.exit(
|
|
"ERROR: this script has only been tested on Linux with cargo."
|
|
)
|
|
# Assuming that this script is in development/scripts
|
|
linux_dir = os.path.join(
|
|
TOP_DIR, "prebuilts", "rust", "linux-x86"
|
|
)
|
|
if not os.path.isdir(linux_dir):
|
|
sys.exit("ERROR: cannot find directory " + linux_dir)
|
|
rust_version = self.find_rust_version(linux_dir)
|
|
if self.args.verbose:
|
|
print(f"### INFO: using prebuilt rust version {rust_version}")
|
|
cargo_bin = os.path.join(linux_dir, rust_version, "bin")
|
|
self.cargo_path = os.path.join(cargo_bin, "cargo")
|
|
if not os.path.isfile(self.cargo_path):
|
|
sys.exit(
|
|
"ERROR: cannot find cargo in "
|
|
+ cargo_bin
|
|
+ "; please try --cargo_bin= flag."
|
|
)
|
|
return
|
|
|
|
def find_rust_version(self, linux_dir):
|
|
"""find newest prebuilt rust version."""
|
|
# find the newest (largest) version number in linux_dir.
|
|
rust_version = (0, 0, 0) # the prebuilt version to use
|
|
version_pat = re.compile(r"([0-9]+)\.([0-9]+)\.([0-9]+)$")
|
|
for dir_name in os.listdir(linux_dir):
|
|
result = version_pat.match(dir_name)
|
|
if not result:
|
|
continue
|
|
version = (
|
|
int(result.group(1)),
|
|
int(result.group(2)),
|
|
int(result.group(3)),
|
|
)
|
|
if version > rust_version:
|
|
rust_version = version
|
|
return ".".join(str(ver) for ver in rust_version)
|
|
|
|
def find_out_files(self):
|
|
# list1 has build.rs output for normal crates
|
|
list1 = glob.glob(
|
|
TARGET_TMP + "/*/*/build/" + self.root_pkg + "-*/out/*"
|
|
)
|
|
# list2 has build.rs output for proc-macro crates
|
|
list2 = glob.glob(TARGET_TMP + "/*/build/" + self.root_pkg + "-*/out/*")
|
|
return list1 + list2
|
|
|
|
def copy_out_files(self):
|
|
"""Copy build.rs output files to ./out and set up build_out_files."""
|
|
if self.checked_out_files:
|
|
return
|
|
self.checked_out_files = True
|
|
cargo_out_files = self.find_out_files()
|
|
out_files = set()
|
|
if cargo_out_files:
|
|
os.makedirs("out", exist_ok=True)
|
|
for path in cargo_out_files:
|
|
file_name = path.split("/")[-1]
|
|
out_files.add(file_name)
|
|
shutil.copy(path, "out/" + file_name)
|
|
self.build_out_files = sorted(out_files)
|
|
|
|
def has_used_out_dir(self):
|
|
"""Returns true if env!("OUT_DIR") is found."""
|
|
return 0 == os.system(
|
|
"grep -rl --exclude build.rs --include \\*.rs"
|
|
+ " 'env!(\"OUT_DIR\")' * > /dev/null"
|
|
)
|
|
|
|
def init_rules_file(self, name):
|
|
# name could be rules.mk or sub_dir_path/rules.mk
|
|
if name not in self.mk_files:
|
|
self.mk_files.add(name)
|
|
with open(name, "w", encoding="utf-8") as outf:
|
|
print_args = sys.argv[1:].copy()
|
|
if "--cargo_bin" in print_args:
|
|
index = print_args.index("--cargo_bin")
|
|
del print_args[index : index + 2]
|
|
outf.write(RULES_MK_HEADER.format(args=" ".join(print_args)))
|
|
|
|
def find_root_pkg(self):
|
|
"""Read name of [package] in ./Cargo.toml."""
|
|
if not os.path.exists("./Cargo.toml"):
|
|
return
|
|
with open("./Cargo.toml", "r", encoding="utf-8") as inf:
|
|
pkg_section = re.compile(r"^ *\[package\]")
|
|
name = re.compile('^ *name *= * "([^"]*)"')
|
|
in_pkg = False
|
|
for line in inf:
|
|
if in_pkg:
|
|
if match := name.match(line):
|
|
self.root_pkg = match.group(1)
|
|
break
|
|
else:
|
|
in_pkg = pkg_section.match(line) is not None
|
|
|
|
def run_cargo(self):
|
|
"""Calls cargo -v and save its output to ./cargo.out."""
|
|
if self.skip_cargo:
|
|
return self
|
|
cargo_toml = "./Cargo.toml"
|
|
cargo_out = "./cargo.out"
|
|
|
|
# Do not use Cargo.lock, because Trusty makefile rules are designed
|
|
# to run with the latest available vendored crates in Trusty.
|
|
cargo_lock = "./Cargo.lock"
|
|
cargo_lock_saved = "./cargo.lock.saved"
|
|
had_cargo_lock = os.path.exists(cargo_lock)
|
|
if not os.access(cargo_toml, os.R_OK):
|
|
print("ERROR: Cannot find or read", cargo_toml)
|
|
return self
|
|
if not self.dry_run:
|
|
if os.path.exists(cargo_out):
|
|
os.remove(cargo_out)
|
|
if not self.args.use_cargo_lock and had_cargo_lock: # save it
|
|
os.rename(cargo_lock, cargo_lock_saved)
|
|
cmd_tail_target = " --target-dir " + TARGET_TMP
|
|
cmd_tail_redir = " >> " + cargo_out + " 2>&1"
|
|
# set up search PATH for cargo to find the correct rustc
|
|
saved_path = os.environ["PATH"]
|
|
os.environ["PATH"] = os.path.dirname(self.cargo_path) + ":" + saved_path
|
|
# Add [workspace] to Cargo.toml if it is not there.
|
|
added_workspace = False
|
|
cargo_toml_lines = None
|
|
if self.args.add_workspace:
|
|
with open(cargo_toml, "r", encoding="utf-8") as in_file:
|
|
cargo_toml_lines = in_file.readlines()
|
|
found_workspace = "[workspace]\n" in cargo_toml_lines
|
|
if found_workspace:
|
|
print("### WARNING: found [workspace] in Cargo.toml")
|
|
else:
|
|
with open(cargo_toml, "a", encoding="utf-8") as out_file:
|
|
out_file.write("\n\n[workspace]\n")
|
|
added_workspace = True
|
|
if self.args.verbose:
|
|
print("### INFO: added [workspace] to Cargo.toml")
|
|
features = ""
|
|
for c in self.cargo:
|
|
features = ""
|
|
if c != "clean":
|
|
if self.args.features is not None:
|
|
features = " --no-default-features"
|
|
if self.args.features:
|
|
features += " --features " + self.args.features
|
|
cmd_v_flag = " -vv " if self.args.vv else " -v "
|
|
cmd = self.cargo_path + cmd_v_flag
|
|
cmd += c + features + cmd_tail_target + cmd_tail_redir
|
|
if self.args.rustflags and c != "clean":
|
|
cmd = 'RUSTFLAGS="' + self.args.rustflags + '" ' + cmd
|
|
self.run_cmd(cmd, cargo_out)
|
|
if self.args.tests:
|
|
cmd = (
|
|
self.cargo_path
|
|
+ " test"
|
|
+ features
|
|
+ cmd_tail_target
|
|
+ " -- --list"
|
|
+ cmd_tail_redir
|
|
)
|
|
self.run_cmd(cmd, cargo_out)
|
|
if added_workspace: # restore original Cargo.toml
|
|
with open(cargo_toml, "w", encoding="utf-8") as out_file:
|
|
assert cargo_toml_lines
|
|
out_file.writelines(cargo_toml_lines)
|
|
if self.args.verbose:
|
|
print("### INFO: restored original Cargo.toml")
|
|
os.environ["PATH"] = saved_path
|
|
if not self.dry_run:
|
|
if not had_cargo_lock: # restore to no Cargo.lock state
|
|
if os.path.exists(cargo_lock):
|
|
os.remove(cargo_lock)
|
|
elif not self.args.use_cargo_lock: # restore saved Cargo.lock
|
|
os.rename(cargo_lock_saved, cargo_lock)
|
|
return self
|
|
|
|
def run_cmd(self, cmd, cargo_out):
|
|
if self.dry_run:
|
|
print("Dry-run skip:", cmd)
|
|
else:
|
|
if self.args.verbose:
|
|
print("Running:", cmd)
|
|
with open(cargo_out, "a+", encoding="utf-8") as out_file:
|
|
out_file.write("### Running: " + cmd + "\n")
|
|
ret = os.system(cmd)
|
|
if ret != 0:
|
|
print(
|
|
"*** There was an error while running cargo. "
|
|
+ f"See the {cargo_out} file for details."
|
|
)
|
|
|
|
def apply_patch(self):
|
|
"""Apply local patch file if it is given."""
|
|
if self.args.patch:
|
|
if self.dry_run:
|
|
print("Dry-run skip patch file:", self.args.patch)
|
|
else:
|
|
if not os.path.exists(self.args.patch):
|
|
self.append_to_rules(
|
|
"ERROR cannot find patch file: " + self.args.patch
|
|
)
|
|
return self
|
|
if self.args.verbose:
|
|
print(
|
|
"### INFO: applying local patch file:", self.args.patch
|
|
)
|
|
subprocess.run(
|
|
[
|
|
"patch",
|
|
"-s",
|
|
"--no-backup-if-mismatch",
|
|
"./rules.mk",
|
|
self.args.patch,
|
|
],
|
|
check=True,
|
|
)
|
|
return self
|
|
|
|
def gen_rules(self):
|
|
"""Parse cargo.out and generate Trusty makefile rules"""
|
|
if self.dry_run:
|
|
print("Dry-run skip: read", CARGO_OUT, "write rules.mk")
|
|
elif os.path.exists(CARGO_OUT):
|
|
self.find_root_pkg()
|
|
if self.args.copy_out:
|
|
self.copy_out_files()
|
|
elif self.find_out_files() and self.has_used_out_dir():
|
|
print(
|
|
"WARNING: "
|
|
+ self.root_pkg
|
|
+ " has cargo output files; "
|
|
+ "please rerun with the --copy-out flag."
|
|
)
|
|
with open(CARGO_OUT, "r", encoding="utf-8") as cargo_out:
|
|
self.parse(cargo_out, "rules.mk")
|
|
self.crates.sort(key=get_module_name)
|
|
for crate in self.crates:
|
|
crate.dump()
|
|
if self.errors:
|
|
self.append_to_rules("\n" + ERRORS_LINE + "\n" + self.errors)
|
|
if self.test_errors:
|
|
self.append_to_rules(
|
|
"\n// Errors when listing tests:\n" + self.test_errors
|
|
)
|
|
return self
|
|
|
|
def add_crate(self, crate: Crate):
|
|
"""Append crate to list unless it meets criteria for being skipped."""
|
|
if crate.skip_crate():
|
|
if self.args.debug: # include debug info of all crates
|
|
self.crates.append(crate)
|
|
elif crate.crate_types == set(["bin"]):
|
|
print("WARNING: skipping binary crate: " + crate.crate_name)
|
|
else:
|
|
self.crates.append(crate)
|
|
|
|
def find_warning_owners(self):
|
|
"""For each warning file, find its owner crate."""
|
|
missing_owner = False
|
|
for f in self.warning_files:
|
|
cargo_dir = "" # find lowest crate, with longest path
|
|
owner = None # owner crate of this warning
|
|
for c in self.crates:
|
|
if f.startswith(c.cargo_dir + "/") and len(cargo_dir) < len(
|
|
c.cargo_dir
|
|
):
|
|
cargo_dir = c.cargo_dir
|
|
owner = c
|
|
if owner:
|
|
owner.has_warning = True
|
|
else:
|
|
missing_owner = True
|
|
if missing_owner and os.path.exists("Cargo.toml"):
|
|
# owner is the root cargo, with empty cargo_dir
|
|
for c in self.crates:
|
|
if not c.cargo_dir:
|
|
c.has_warning = True
|
|
|
|
def rustc_command(self, n, rustc_line, line, outf_name):
|
|
"""Process a rustc command line from cargo -vv output."""
|
|
# cargo build -vv output can have multiple lines for a rustc command
|
|
# due to '\n' in strings for environment variables.
|
|
# strip removes leading spaces and '\n' at the end
|
|
new_rustc = (rustc_line.strip() + line) if rustc_line else line
|
|
# Use an heuristic to detect the completions of a multi-line command.
|
|
# This might fail for some very rare case, but easy to fix manually.
|
|
if not line.endswith("`\n") or (new_rustc.count("`") % 2) != 0:
|
|
return new_rustc
|
|
if match := RUSTC_VV_CMD_ARGS.match(new_rustc):
|
|
args = match.group(2)
|
|
self.add_crate(Crate(self, outf_name).parse(n, args))
|
|
else:
|
|
self.assert_empty_vv_line(new_rustc)
|
|
return ""
|
|
|
|
def append_to_rules(self, line):
|
|
self.init_rules_file("rules.mk")
|
|
with open("rules.mk", "a", encoding="utf-8") as outf:
|
|
outf.write(line)
|
|
|
|
def assert_empty_vv_line(self, line):
|
|
if line: # report error if line is not empty
|
|
self.append_to_rules("ERROR -vv line: " + line)
|
|
return ""
|
|
|
|
def add_empty_test(self, name):
|
|
if name.startswith("unittests"):
|
|
self.empty_unittests = True
|
|
else:
|
|
self.empty_tests.add(name)
|
|
|
|
def should_ignore_test(self, src):
|
|
# cargo test outputs the source file for integration tests but
|
|
# "unittests" for unit tests. To figure out to which crate this
|
|
# corresponds, we check if the current source file is the main source of
|
|
# a non-test crate, e.g., a library or a binary.
|
|
return (
|
|
src in self.args.test_blocklist
|
|
or src in self.empty_tests
|
|
or (
|
|
self.empty_unittests
|
|
and src
|
|
in [
|
|
c.main_src for c in self.crates if c.crate_types != ["test"]
|
|
]
|
|
)
|
|
)
|
|
|
|
def parse(self, inf, outf_name):
|
|
"""Parse rustc, test, and warning messages in input file."""
|
|
n = 0 # line number
|
|
# We read the file in two passes, where the first simply checks for
|
|
# empty tests. Otherwise we would add and merge tests before seeing
|
|
# they're empty.
|
|
cur_test_name = None
|
|
for line in inf:
|
|
if match := CARGO_TEST_LIST_START_PAT.match(line):
|
|
cur_test_name = match.group(1)
|
|
elif cur_test_name and (
|
|
match := CARGO_TEST_LIST_END_PAT.match(line)
|
|
):
|
|
if int(match.group(1)) + int(match.group(2)) == 0:
|
|
self.add_empty_test(cur_test_name)
|
|
cur_test_name = None
|
|
inf.seek(0)
|
|
prev_warning = False # true if the previous line was warning: ...
|
|
rustc_line = "" # previous line(s) matching RUSTC_VV_PAT
|
|
in_tests = False
|
|
for line in inf:
|
|
n += 1
|
|
if line.startswith("warning: "):
|
|
prev_warning = True
|
|
rustc_line = self.assert_empty_vv_line(rustc_line)
|
|
continue
|
|
new_rustc = ""
|
|
if match := RUSTC_PAT.match(line):
|
|
args_line = match.group(2)
|
|
self.add_crate(Crate(self, outf_name).parse(n, args_line))
|
|
self.assert_empty_vv_line(rustc_line)
|
|
elif rustc_line or RUSTC_VV_PAT.match(line):
|
|
new_rustc = self.rustc_command(n, rustc_line, line, outf_name)
|
|
elif CC_AR_VV_PAT.match(line):
|
|
raise NotImplementedError("$CC or $AR commands not supported")
|
|
elif prev_warning and (match := WARNING_FILE_PAT.match(line)):
|
|
self.assert_empty_vv_line(rustc_line)
|
|
fpath = match.group(1)
|
|
if fpath[0] != "/": # ignore absolute path
|
|
self.warning_files.add(fpath)
|
|
elif line.startswith("error: ") or line.startswith("error[E"):
|
|
if not self.args.ignore_cargo_errors:
|
|
if in_tests:
|
|
self.test_errors += "// " + line
|
|
else:
|
|
self.errors += line
|
|
elif CARGO2ANDROID_RUNNING_PAT.match(line):
|
|
in_tests = "cargo test" in line and "--list" in line
|
|
prev_warning = False
|
|
rustc_line = new_rustc
|
|
self.find_warning_owners()
|
|
|
|
|
|
def get_parser():
|
|
"""Parse main arguments."""
|
|
parser = argparse.ArgumentParser("cargo2rulesmk")
|
|
parser.add_argument(
|
|
"--add_workspace",
|
|
action="store_true",
|
|
default=False,
|
|
help=(
|
|
"append [workspace] to Cargo.toml before calling cargo,"
|
|
+ " to treat current directory as root of package source;"
|
|
+ " otherwise the relative source file path in generated"
|
|
+ " rules.mk file will be from the parent directory."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--cargo",
|
|
action="append",
|
|
metavar="args_string",
|
|
help=(
|
|
"extra cargo build -v args in a string, "
|
|
+ "each --cargo flag calls cargo build -v once"
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--cargo_bin",
|
|
type=str,
|
|
help="use cargo in the cargo_bin directory instead of the prebuilt one",
|
|
)
|
|
parser.add_argument(
|
|
"--copy-out",
|
|
action="store_true",
|
|
default=False,
|
|
help=(
|
|
"only for root directory, "
|
|
+ "copy build.rs output to ./out/* and declare source deps "
|
|
+ "for ./out/*.rs; for crates with code pattern: "
|
|
+ 'include!(concat!(env!("OUT_DIR"), "/<some_file>.rs"))'
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--debug",
|
|
action="store_true",
|
|
default=False,
|
|
help="dump debug info into rules.mk",
|
|
)
|
|
parser.add_argument(
|
|
"--features",
|
|
type=str,
|
|
help=(
|
|
"pass features to cargo build, "
|
|
+ "empty string means no default features"
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--ignore-cargo-errors",
|
|
action="store_true",
|
|
default=False,
|
|
help="do not append cargo/rustc error messages to rules.mk",
|
|
)
|
|
parser.add_argument(
|
|
"--no-subdir",
|
|
action="store_true",
|
|
default=False,
|
|
help="do not output anything for sub-directories",
|
|
)
|
|
parser.add_argument(
|
|
"--onefile",
|
|
action="store_true",
|
|
default=False,
|
|
help=(
|
|
"output all into one ./rules.mk, default will generate "
|
|
+ "one rules.mk per Cargo.toml in subdirectories"
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--patch",
|
|
type=str,
|
|
help="apply the given patch file to generated ./rules.mk",
|
|
)
|
|
parser.add_argument(
|
|
"--run",
|
|
action="store_true",
|
|
default=False,
|
|
help="run it, default is dry-run",
|
|
)
|
|
parser.add_argument("--rustflags", type=str, help="passing flags to rustc")
|
|
parser.add_argument(
|
|
"--skipcargo",
|
|
action="store_true",
|
|
default=False,
|
|
help="skip cargo command, parse cargo.out, and generate ./rules.mk",
|
|
)
|
|
parser.add_argument(
|
|
"--tests",
|
|
action="store_true",
|
|
default=False,
|
|
help="run cargo build --tests after normal build",
|
|
)
|
|
parser.add_argument(
|
|
"--use-cargo-lock",
|
|
action="store_true",
|
|
default=False,
|
|
help=(
|
|
"run cargo build with existing Cargo.lock "
|
|
+ "(used when some latest dependent crates failed)"
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--test-data",
|
|
nargs="*",
|
|
default=[],
|
|
help=(
|
|
"Add the given file to the given test's data property. "
|
|
+ "Usage: test-path=data-path"
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--dependency-blocklist",
|
|
nargs="*",
|
|
default=[],
|
|
help="Do not emit the given dependencies (without lib prefixes).",
|
|
)
|
|
parser.add_argument(
|
|
"--test-blocklist",
|
|
nargs="*",
|
|
default=[],
|
|
help=(
|
|
"Do not emit the given tests. "
|
|
+ "Pass the path to the test file to exclude."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--cfg-blocklist",
|
|
nargs="*",
|
|
default=[],
|
|
help="Do not emit the given cfg.",
|
|
)
|
|
parser.add_argument(
|
|
"--verbose",
|
|
action="store_true",
|
|
default=False,
|
|
help="echo executed commands",
|
|
)
|
|
parser.add_argument(
|
|
"--vv",
|
|
action="store_true",
|
|
default=False,
|
|
help="run cargo with -vv instead of default -v",
|
|
)
|
|
parser.add_argument(
|
|
"--dump-config-and-exit",
|
|
type=str,
|
|
help=(
|
|
"Dump command-line arguments (minus this flag) to a config file and"
|
|
" exit. This is intended to help migrate from command line options "
|
|
"to config files."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--config",
|
|
type=str,
|
|
help=(
|
|
"Load command-line options from the given config file. Options in "
|
|
"this file will override those passed on the command line."
|
|
),
|
|
)
|
|
return parser
|
|
|
|
|
|
def parse_args(parser):
|
|
"""Parses command-line options."""
|
|
args = parser.parse_args()
|
|
# Use the values specified in a config file if one was found.
|
|
if args.config:
|
|
with open(args.config, "r", encoding="utf-8") as f:
|
|
config = json.load(f)
|
|
args_dict = vars(args)
|
|
for arg in config:
|
|
args_dict[arg.replace("-", "_")] = config[arg]
|
|
return args
|
|
|
|
|
|
def dump_config(parser, args):
|
|
"""Writes the non-default command-line options to the specified file."""
|
|
args_dict = vars(args)
|
|
# Filter out the arguments that have their default value.
|
|
# Also filter certain "temporary" arguments.
|
|
non_default_args = {}
|
|
for arg in args_dict:
|
|
if (
|
|
args_dict[arg] != parser.get_default(arg)
|
|
and arg != "dump_config_and_exit"
|
|
and arg != "config"
|
|
and arg != "cargo_bin"
|
|
):
|
|
non_default_args[arg.replace("_", "-")] = args_dict[arg]
|
|
# Write to the specified file.
|
|
with open(args.dump_config_and_exit, "w", encoding="utf-8") as f:
|
|
json.dump(non_default_args, f, indent=2, sort_keys=True)
|
|
|
|
|
|
def main():
|
|
parser = get_parser()
|
|
args = parse_args(parser)
|
|
if not args.run: # default is dry-run
|
|
print(DRY_RUN_NOTE)
|
|
if args.dump_config_and_exit:
|
|
dump_config(parser, args)
|
|
else:
|
|
Runner(args).run_cargo().gen_rules().apply_patch()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|