# python3 # Copyright 2021 The Chromium Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. from __future__ import annotations from lib import consts from lib import common from lib import cargo from lib.compiler import BuildConditionSet import argparse from typing import Optional, Tuple class BuildRuleUsage: """Container for data that differs depending how the crate is consumed. There are 3 ways a crate can be consumed, and data for generating the resulting build rule will differ: * For normal usage. The crate is used from another crate, in their library or executable outputs. * For build scripts. The crate is used from build.rs in another crate. * For tests. The crate is used from tests in another crate. """ def __init__(self): # Whether the crate should be allowed for direct use from first-party # code or not. self.for_first_party: bool = False # Set of architectures where another crate consumes this crate. If # empty, no GN target needs to be written. self.used_on_archs: BuildConditionSet = BuildConditionSet.EMPTY() # Dictionary of features, each defining a feature for building each of # the crate's targets (lib, binaries, build.rs, and tests). This list # includes "default" if default features should be used, or omits it # otherwise, which differs slightly from how rustc and cargo represent # including or excluding default features (they use a separate bool # and command line flag. # key: The name of the feature as string # value: `BuildConditionSet` for where the feature is enabled. self.features: list[tuple[str, BuildConditionSet]] = [] # List of dictionaries, each defining a dependency for building the # crate's library and binaries. # deppath: GN target path of the dependency as string. # compile_modes: `BuildConditionSet` for where the dependency is used. self.deps: list[dict] = [] # List of dictionaries, each defining a dependency for building the # crate's build.rs (or equivalent) file, defined in `build_root` if it # exists. # deppath: GN target path of the dependency as string. # compile_modes: `BuildConditionSet` for where the dependency is used. self.build_deps: list[dict] = [] # List of dictionaries, each defining a dependency for building the # crate's tests. # deppath: GN target path of the dependency as string. # compile_modes: `BuildConditionSet` for where the dependency is used. self.dev_deps: list[dict] = [] def sort_internals(self): """Sorts any sortable containers to help make reproducible output.""" self.features.sort(key=lambda t: t[0]) self.build_deps.sort(key=lambda d: d["deppath"]) self.deps.sort(key=lambda d: d["deppath"]) self.dev_deps.sort(key=lambda d: d["deppath"]) class BuildRule: """A structured representation of the data used to generate a BUILD file. Contains data from the crate's Cargo.toml as well as data gathered from cargo with this tool.""" def __init__(self, crate_name: str, epoch: str): # Name of the crate, not normalized. This is how rust code would refer # to the crate. self.crate_name: str = crate_name # The version epoch of the crate, which is used to generate metadata for # the crate's output. self.epoch: str = epoch # None if there is no lib build target, or the relative GN file path # string to the lib's root .rs file. self.lib_root: Optional[str] = None # Empty if there is no lib build target, or the relative GN file path # string to all Rust files that are part of the lib build, including # files generated by the build script if any. self.lib_sources: list[str] = [] # If lib_root is not None, then this is "rlib" or "proc-macro", # specifying the type of lib. self.lib_type: Optional[str] = None # Empty if there are no bin targets. List of dictionaries with keys: # name: executable name # root: The relative GN path string to the bin's root .rs file. # sources: A list of relative GN path strings to all Rust files that # are part of the bin build, including files generated by # the build script if any. self.bins: list[dict] = [] # Stuff shared between the lib (if present) and bins (if present). # If not none, a GN file path string to the build.rs file (or # equivalent) which is the root module of the build script. self.build_root: Optional[str] = None # The full set of source files including the root .rs file and those # used by it. self.build_sources: list[str] = [] # The set of output files the build.rs would create, if there are any. A # human has to go and figure this out, so it must come from # third_party.toml. self.build_script_outputs: list[str] = [] # The rust edition to build the crate with. self.edition: Optional[str] = None self.cargo_pkg_name: Optional[str] = None self.cargo_pkg_description: Optional[str] = None self.cargo_pkg_version: Optional[str] = None self.cargo_pkg_authors: Optional[str] = None self.normal_usage = BuildRuleUsage() self.buildrs_usage = BuildRuleUsage() self.test_usage = BuildRuleUsage() def get_usage(self, usage: cargo.CrateUsage) -> BuildRuleUsage: """Returns a `BuildRuleUsage` for a `cargo.CrateUsage`. These are also accessible directly as fields on the class, but this method helps to choose the correct one based on `cargo.CrateUsage`.""" if usage == cargo.CrateUsage.FOR_NORMAL: return self.normal_usage if usage == cargo.CrateUsage.FOR_BUILDRS: return self.buildrs_usage if usage == cargo.CrateUsage.FOR_TESTS: return self.test_usage assert False # Unhandled CrateUsage? def sort_internals(self): """Sorts any sortable containers to help make reproducible output.""" self.bins.sort(key=lambda d: d["name"]) self.build_sources.sort() self.build_script_outputs.sort() self.lib_sources.sort() self.normal_usage.sort_internals() self.buildrs_usage.sort_internals() self.test_usage.sort_internals() def _write(self, indent: int, s: str): """Append a string onto `self.out`. The string is indented with a number of spaces based on the `indent` argument.""" self.out.append("{}{}\n".format(" " * indent, s)) def _write_compile_modes_conditions(self, indent: int, compile_modes: BuildConditionSet): """Write a GN if statement that is true for the `BuildConditionSet`. This appends the generated text to `out`.""" conds = compile_modes.get_gn_conditions() if len(conds) == 1: self._write(indent, "if ({}) {{".format(conds[0])) elif len(conds) > 1: self._write(indent, "if (({}) ||".format(conds[0])) for cond in conds[1:-1]: self._write(indent + 4, "({}) ||".format(cond)) self._write(indent + 4, "({})) {{".format(conds[-1])) def _write_for_compile_modes(self, indent: int, compile_modes: BuildConditionSet, to_write: list[tuple[int, str]]): """Write a GN if statement and body for a `BuildConditionSet` This appends the generated text to `out` and returns the result. Args: indent: How much to indent each line generated by this function. compile_modes: Defines when the if statement should resolve to true. to_write: A list of strings to place in the body of the if statement. Each string comes with an indent value, which will be added to the top level `indent`. """ self._write_compile_modes_conditions(indent, compile_modes) conds = compile_modes.get_gn_conditions() if conds: indent += 2 for (write_indent, write_str) in to_write: self._write(indent + write_indent, write_str) if conds: indent -= 2 self._write(indent, "}") def _write_common(self, indent: int, build_rule: BuildRule, sources: list, usage: cargo.CrateUsage): """Write the GN content that's common for libraries and executables. This appends the generated text to `out` and returns the result. Args: build_rule: The BuildRule being written from. sources: The set of Rust source files. This is passed separately since it is stored differently for libraries vs executables in the BuildRule. usage: How this GN target is going to be used by other crates (or cargo.CrateUsage.FOR_NORMAL if it's generating an executable). """ assert sources # There's always a root source at least. self._write(indent, "sources = [") for s in sources: self._write(indent + 2, "\"{}\",".format(s)) self._write(indent, "]") self._write(indent, "edition = \"{}\"".format(build_rule.edition)) if build_rule.cargo_pkg_version: self._write( indent, "cargo_pkg_version = \"{}\"".format( build_rule.cargo_pkg_version)) if build_rule.cargo_pkg_authors: self._write( indent, "cargo_pkg_authors = \"{}\"".format(", ".join( build_rule.cargo_pkg_authors))) if build_rule.cargo_pkg_name: self._write( indent, "cargo_pkg_name = \"{}\"".format( build_rule.cargo_pkg_name.replace('\n', ''))) if build_rule.cargo_pkg_description: self._write( indent, "cargo_pkg_description = \"{}\"".format( build_rule.cargo_pkg_description.replace('\n', '').replace( '"', '\''))) # Add these if, in the future, we want to explicitly mark each # third-party crate instead of doing so from the GN template. # # self._write(indent, # "configs -= [ \"//build/config/compiler:chromium_code\" ]") # self._write(indent, # "configs += [ \"//build/config/compiler:no_chromium_code\" ]") build_rule_usage = build_rule.get_usage(usage) for (deps, gn_name) in [(build_rule_usage.deps, "deps"), (build_rule_usage.build_deps, "build_deps"), (build_rule_usage.dev_deps, "test_deps")]: if not deps: continue global_deps = [] specific_deps = [] for d in deps: compile_modes = d["compile_modes"] if compile_modes.is_always_true( ) or compile_modes == build_rule_usage.used_on_archs: global_deps += [d] else: specific_deps += [d] if global_deps or specific_deps: self._write(indent, "{} = [".format(gn_name)) for d in global_deps: self._write(indent + 2, "\"{}\",".format(d["deppath"])) if global_deps or specific_deps: self._write(indent, "]") for d in specific_deps: compile_modes = d["compile_modes"] self._write_for_compile_modes( indent, compile_modes, [(0, "deps += [ \"{}\" ]".format(d["deppath"]))]) global_features = [] specific_features = [] for (feature, compile_modes) in build_rule_usage.features: # The default feature is a cargo thing, and just translates to # other features specified by the Cargo.toml. if feature == "default": continue if compile_modes.is_always_true( ) or compile_modes == build_rule_usage.used_on_archs: global_features += [feature] else: specific_features += [(feature, compile_modes)] if global_features or specific_features: self._write(indent, "features = [") for f in global_features: self._write(indent + 2, "\"{}\",".format(f)) if global_features or specific_features: self._write(indent, "]") for (f, compile_modes) in specific_features: self._write_for_compile_modes( 2, compile_modes, [(0, "features += [ \"{}\" ]".format(f))]) if build_rule.build_root: self._write(indent, "build_root = \"{}\"".format(build_rule.build_root)) self._write(indent, "build_sources = [") for s in build_rule.build_sources: self._write(indent + 2, "\"{}\",".format(s)) self._write(indent, "]") if build_rule.build_script_outputs: self._write(indent, "build_script_outputs = [") for o in build_rule.build_script_outputs: self._write(indent + 2, "\"{}\",".format(o)) self._write(indent, "]") def generate_gn(self, args: argparse.Namespace, copyright_year: str) -> str: """Generate a BUILD.gn file contents. The BuildRule has all data needed to construct a BUILD file. This generates a BUILD.gn file. Args: args: The command-line arguments. copyright_year: The year as a string. """ self.out: list[str] = [] self._write(0, consts.GN_HEADER.format(year=copyright_year)) for bin in self.bins: self._write(0, "cargo_crate(\"{}\") {{".format(bin["name"])) self._write(2, "crate_type = \"bin\"") self._write(2, "crate_root = \"{}\"".format(bin["root"])) self._write_common(2, self, bin["sources"], cargo.CrateUsage.FOR_NORMAL) self._write(0, "}") if self.lib_root: for usage in cargo.CrateUsage: # TODO(danakj): If the BuildRuleUsage is the same as another we # should only generate one, and point the duplicate over by # using a GN group() target. This would avoid building a crate # multiple times if the same features will be used each time. used_on_archs = self.get_usage(usage).used_on_archs if not used_on_archs: continue indent = 0 if not used_on_archs.is_always_true(): self._write_compile_modes_conditions(indent, used_on_archs) indent += 2 self._write( indent, "cargo_crate(\"{}\") {{".format(usage.gn_target_name())) self._write(indent + 2, "crate_name = \"{}\"".format( common.crate_name_normalized( self.crate_name))) # yapf: disable self._write(indent + 2, "epoch = \"{}\"".format(self.epoch)) self._write(indent + 2, "crate_type = \"{}\"".format(self.lib_type)) if not self.get_usage(usage).for_first_party: for c in consts.GN_VISIBILITY_COMMENT.split("\n"): self._write(indent + 2, c) self._write(indent + 2, "visibility = [ \"{}\" ]".format( common.gn_third_party_path( rel_path=["*"]))) # yapf: disable if usage == cargo.CrateUsage.FOR_TESTS: self._write(indent + 2, "testonly = \"true\"") self._write(indent + 2, "crate_root = \"{}\"".format(self.lib_root)) if usage == cargo.CrateUsage.FOR_NORMAL: if args.with_tests: self._write(indent + 2, "build_native_rust_unit_tests = true") else: for c in consts.GN_TESTS_COMMENT.split("\n"): self._write(indent + 2, c) self._write(indent + 2, "build_native_rust_unit_tests = false") else: self._write(indent + 2, "build_native_rust_unit_tests = false") self._write_common(indent + 2, self, self.lib_sources, usage) self._write(indent, "}") if not used_on_archs.is_always_true(): indent -= 2 self._write(indent, "}") return ''.join(self.out)