| #!/usr/bin/env fuchsia-vendored-python | |
| # | |
| # Copyright 2020 The Fuchsia Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| import argparse | |
| import collections | |
| import concurrent.futures | |
| import copy | |
| import datetime | |
| import enum | |
| import functools | |
| import hashlib | |
| import json | |
| import re | |
| import sys | |
| import textwrap | |
| import tomllib | |
| import typing as T | |
| from collections.abc import Iterable, Iterator | |
| from pathlib import Path | |
| CARGO_PACKAGE_CONTENTS = """\ | |
| # Copyright %(year)s The Fuchsia Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| # source GN: %(target)s" | |
| cargo-features = ["per-package-target"] | |
| [package] | |
| name = "%(package_name)s" | |
| version = "%(version)s" | |
| license = "BSD-3-Clause" | |
| authors = ["rust-fuchsia@fuchsia.com"] | |
| description = "Rust crate for Fuchsia" | |
| repository = "https://fuchsia.googlesource.com" | |
| edition = "%(edition)s" | |
| %(default_target)s | |
| %(bin_or_lib)s | |
| %(is_proc_macro)s | |
| name = "%(crate_name)s" | |
| path = "%(source_root)s" | |
| """ | |
| CARGO_PACKAGE_NO_WORKSPACE = """\ | |
| [workspace] | |
| # empty workspace table excludes this crate from thinking it should be in a workspace | |
| """ | |
| CARGO_PACKAGE_DEP = """\ | |
| [%(dep_type)s.%(crate_name)s] | |
| version = "%(version)s" | |
| path = "%(crate_path)s" | |
| """ | |
| FEATURE_PAT = re.compile(r'--cfg=feature="(.*)"$') | |
| CHECK_CFG_PAT = re.compile(r"--check-cfg=(.*)$") | |
| CFG_PAT = re.compile(r"--cfg=([^=]*)=(.*)$") | |
| RUST_CRATES_PAT = re.compile(r"rust_crates:([\w-]*)") | |
| class ToolchainType(enum.Enum): | |
| TARGET = 1 | |
| HOST = 2 | |
| class Project(object): | |
| """Represents a GN project loaded from project.json. | |
| Encapsulates the GN build graph metadata, providing helpers and cached | |
| indexes to query Rust targets, identify test targets, expand intermediate | |
| group/source_set targets, and compute reachability from the default build root. | |
| Attributes: | |
| targets: A mapping of GN target labels to target metadata dictionaries. | |
| patches_toml_block_str: Formatted TOML patch table string for | |
| crates.io dependencies, or None if no patches are applied. | |
| """ | |
| def __init__(self, project_json: dict[str, T.Any]) -> None: | |
| self.targets: dict[str, dict[str, T.Any]] = project_json["targets"] | |
| self.patches_toml_block_str: str | None = None | |
| @functools.cached_property | |
| def rust_targets(self) -> dict[str, dict[str, T.Any]]: | |
| return { | |
| target: meta | |
| for target, meta in self.targets.items() | |
| if "crate_root" in meta | |
| } | |
| @functools.cached_property | |
| def rust_targets_by_source_root(self) -> dict[str, list[str]]: | |
| result: dict[str, list[str]] = collections.defaultdict(list) | |
| for target, meta in self.rust_targets.items(): | |
| source_root = meta["crate_root"] | |
| result[source_root].append(target) | |
| return dict(result) | |
| @functools.cached_property | |
| def reachable_targets(self) -> set[str]: | |
| result: set[str] = set(["//:default"]) | |
| pending: list[str] = ["//:default"] | |
| while pending: | |
| current = pending.pop() | |
| meta: dict[str, T.Any] = self.targets[current] | |
| for dep_kind in ["deps", "public_deps", "data_deps"]: | |
| for dep in meta.get(dep_kind, []): | |
| if dep not in result: | |
| result.add(dep) | |
| pending.append(dep) | |
| return result | |
| def expand_source_set_or_group(self, target: str) -> list[str] | None: | |
| """Returns a list of dependencies if the target is a source_set. | |
| Returns dependencies as a list of strings if the target is a | |
| source_set, or None otherwise. | |
| """ | |
| meta: dict[str, T.Any] = self.targets[target] | |
| if meta["type"] in ("source_set", "group"): | |
| return meta["deps"] | |
| return None | |
| def find_test_targets(self, source_root: str) -> list[str]: | |
| overlapping_targets = self.rust_targets_by_source_root.get( | |
| source_root, [] | |
| ) | |
| return [ | |
| t | |
| for t in overlapping_targets | |
| if "--test" in self.targets[t]["rustflags"] | |
| ] | |
| def strip_toolchain(target: str) -> str: | |
| match = re.search("[^(]*", target) | |
| assert match is not None | |
| return match[0] | |
| def extract_toolchain(target: str) -> str | None: | |
| """Return the toolchain part of the provided label, or None if it doesn't have one.""" | |
| if "(" not in target: | |
| # target has no toolchain specified | |
| return None | |
| substr = target[target.find("(") + 1 :] | |
| if not target.endswith(")"): | |
| raise ValueError("target %s missing closing `)`") | |
| return substr[:-1] # remove closing `)` | |
| def version_from_toolchain(toolchain: str | None) -> str: | |
| """Return a version to use to allow host and target crates to coexist.""" | |
| version = "0.0.1" | |
| if toolchain is not None and toolchain.startswith( | |
| "//build/toolchain:host_" | |
| ): | |
| version = "0.0.2" | |
| return version | |
| def classify_toolchain(toolchain: str | None) -> ToolchainType: | |
| if toolchain is not None and toolchain.startswith( | |
| "//build/toolchain:host_" | |
| ): | |
| return ToolchainType.HOST | |
| else: | |
| return ToolchainType.TARGET | |
| def lookup_gn_pkg_name( | |
| project: Project, | |
| target: str, | |
| for_workspace: bool, | |
| ) -> str: | |
| if for_workspace: | |
| return mangle_label(target) | |
| metadata: dict[str, T.Any] = project.targets[target] | |
| return metadata["output_name"] | |
| def rebase_gn_path( | |
| root_path: Path, location: str, directory: bool = False | |
| ) -> Path: | |
| assert location.startswith("//") | |
| # remove the prefix // | |
| path = location.removeprefix("//") | |
| target = Path(path).parent if directory else Path(path) | |
| return root_path / target | |
| def mangle_label(label: str) -> str: | |
| assert label[0:2] == "//" | |
| # remove the prefix // | |
| label = label[2:] | |
| result: list[str] = [] | |
| for c in label: | |
| if c == "-": | |
| result.append("--") | |
| elif c == "_": | |
| result.append("__") | |
| elif c == "/": | |
| result.append("_-_") | |
| elif c == ":": | |
| result.append("-_-") | |
| elif c == ".": | |
| result.append("_--") | |
| elif c == "(" or c == ")": | |
| result.append("-__") | |
| else: | |
| result.append(c) | |
| return "".join(result) | |
| def get_features(rustflags: list[str]) -> list[str]: | |
| features: list[str] = [] | |
| for flag in rustflags: | |
| if match := FEATURE_PAT.match(flag): | |
| features.append(match.group(1)) | |
| return features | |
| def get_check_cfgs(rustflags: list[str]) -> list[str]: | |
| check_cfg: list[str] = [] | |
| for flag in rustflags: | |
| if match := CHECK_CFG_PAT.match(flag): | |
| check_cfg.append(match.group(1)) | |
| return check_cfg | |
| def get_cfgs(rustflags: list[str], api_level_cfgs: list[str]) -> list[str]: | |
| cfgs: list[str] = [] | |
| for flag in rustflags: | |
| if flag.startswith("--cfg=feature"): | |
| continue | |
| if match := CFG_PAT.match(flag): | |
| # __rust_toolchain is for a cfg that's used to invalidate old | |
| # toolchain versions by changing the ninja command line, which is | |
| # of no use to cargo. | |
| if match.group(1) != "__rust_toolchain": | |
| cfgs.append(f"{match.group(1)}={match.group(2)}") | |
| elif flag.startswith("--cfg="): | |
| # Pass cfgs through directly (minus '--cfg=' prefix) | |
| cfgs.append(flag[len("--cfg=") :]) | |
| elif flag == "@rust_api_level_cfg_flags.txt": | |
| # This is a special response file used to provide all the api level | |
| # cfg flags. We've already read that file, so insert those directly | |
| # into the cfgs. | |
| cfgs.extend(api_level_cfgs) | |
| elif flag.startswith("@"): | |
| # These response files cannot be read, as they aren't known to the | |
| # Bazel sandbox. | |
| pass | |
| return cfgs | |
| def write_toml_file( | |
| cargo_toml_dir: Path, | |
| metadata: dict[str, T.Any], | |
| project: Project, | |
| target: str, | |
| lookup: dict[str, str], | |
| root_path: Path, | |
| gn_cargo_dir: Path, | |
| version: str, | |
| api_level_cfgs: list[str], | |
| for_workspace: bool, | |
| ) -> None: | |
| editions: list[str] = [ | |
| flag.split("=")[1] | |
| for flag in metadata["rustflags"] | |
| if flag.startswith("--edition=") | |
| ] | |
| edition = editions[0] if editions else "2015" | |
| if metadata["type"] in [ | |
| "rust_library", | |
| "rust_proc_macro", | |
| "static_library", | |
| "shared_library", | |
| ]: | |
| target_type = "[lib]" | |
| else: | |
| if "--test" in metadata["rustflags"]: | |
| target_type = "[[test]]" | |
| else: | |
| target_type = "[[bin]]" | |
| if metadata["type"] == "rust_proc_macro": | |
| is_proc_macro = "proc-macro = true" | |
| else: | |
| is_proc_macro = "" | |
| features = get_features(metadata["rustflags"]) | |
| extra_configs = get_cfgs(metadata["rustflags"], api_level_cfgs) | |
| check_cfgs = get_check_cfgs(metadata["rustflags"]) | |
| crate_type = "rlib" | |
| package_name = lookup_gn_pkg_name(project, target, for_workspace) | |
| default_target = "" | |
| if classify_toolchain(extract_toolchain(target)) == ToolchainType.TARGET: | |
| default_target = 'default-target = "x86_64-unknown-fuchsia"' | |
| with open(cargo_toml_dir / "Cargo.toml", "w") as fout: | |
| fout.write( | |
| CARGO_PACKAGE_CONTENTS | |
| % { | |
| "target": target, | |
| "package_name": package_name, | |
| "crate_name": metadata["crate_name"], | |
| "version": version, | |
| "year": datetime.datetime.now().year, | |
| "bin_or_lib": target_type, | |
| "is_proc_macro": is_proc_macro, | |
| "lib_crate_type": crate_type, | |
| "edition": edition, | |
| "source_root": rebase_gn_path( | |
| root_path, metadata["crate_root"] | |
| ), | |
| "rust_crates_path": root_path / "third_party/rust_crates", | |
| "default_target": default_target, | |
| } | |
| ) | |
| env_vars = metadata.get("rustenv", []) | |
| if extra_configs or env_vars: | |
| with open(cargo_toml_dir / "build.rs", "w") as buildfile: | |
| template = textwrap.dedent( | |
| """\ | |
| //! build script for {target} | |
| fn main() {{ | |
| // build script does not read any files | |
| println!("cargo:rerun-if-changed=build.rs"); | |
| {body} | |
| {env_vars} | |
| }} | |
| """ | |
| ) | |
| body = "\n".join( | |
| f'println!(r#"cargo:rustc-cfg={cfg}"#);' | |
| for cfg in extra_configs | |
| ) | |
| env_vars = "\n".join( | |
| f'println!("cargo:rustc-env={env}");' for env in env_vars | |
| ) | |
| buildfile.write( | |
| template.format(target=target, body=body, env_vars=env_vars) | |
| ) | |
| extra_test_deps: list[str] = [] | |
| if target_type in {"[lib]", "[[bin]]"}: | |
| test_targets = project.find_test_targets(metadata["crate_root"]) | |
| # hack to filter to just matching toolchains: | |
| test_targets = [ | |
| t | |
| for t in test_targets | |
| if t.split("(")[1:] == target.split("(")[1:] | |
| ] | |
| test_deps: set[str] = set() | |
| for test_target in test_targets: | |
| test_deps.update(project.targets[test_target]["deps"]) | |
| unreachable_test_deps = sorted( | |
| [ | |
| dep | |
| for dep in test_deps | |
| if dep not in project.reachable_targets | |
| ] | |
| ) | |
| if unreachable_test_deps: | |
| fout.write( | |
| "# Note: disabling tests because test deps are not included in the build: %s\n" | |
| % unreachable_test_deps | |
| ) | |
| fout.write("test = false\n") | |
| elif not test_targets: | |
| fout.write( | |
| "# Note: disabling tests because no test target was found with the same source root\n" | |
| ) | |
| fout.write("test = false\n") | |
| else: | |
| fout.write( | |
| "# Note: using extra deps from discovered test target(s): %s\n" | |
| % test_targets | |
| ) | |
| extra_test_deps = sorted(test_deps - set(metadata["deps"])) | |
| if not for_workspace: | |
| fout.write(CARGO_PACKAGE_NO_WORKSPACE) | |
| if not for_workspace and project.patches_toml_block_str: | |
| # In a workspace, patches are ignored, so we skip emitting all the patch lines to cut down on warning spam | |
| fout.write(project.patches_toml_block_str) | |
| def expand_and_deduplicate( | |
| deps: Iterable[str], | |
| visited: set[str] | None = None, | |
| ) -> Iterator[str]: | |
| if visited is None: | |
| visited = set() | |
| for dep in deps: | |
| if dep in visited: | |
| continue | |
| visited.add(dep) | |
| expanded = project.expand_source_set_or_group(dep) | |
| if expanded: | |
| for exp in expand_and_deduplicate(expanded, visited): | |
| yield exp | |
| else: | |
| yield dep | |
| # collect all dependencies | |
| deps = list(expand_and_deduplicate(metadata["deps"])) | |
| dep_crate_names = set() | |
| def write_deps(deps: list[str], dep_type: str) -> None: | |
| while deps: | |
| dep = deps.pop() | |
| # If a dependency points to a source set or group, expand it into a list | |
| # of its deps, and append them to the deps list. Finally, continue | |
| # to the next item, since a source set itself is not considered a | |
| # dependency for our purposes. | |
| expanded_deps = project.expand_source_set_or_group(dep) | |
| if expanded_deps: | |
| deps.extend(expanded_deps) | |
| continue | |
| # ignore non-rust deps | |
| if "crate_name" not in project.targets[dep]: | |
| continue | |
| # this is a third-party dependency | |
| # TODO remove this when all things use GN. temporary hack? | |
| if "third_party/rust_crates:" in dep: | |
| match = RUST_CRATES_PAT.search(dep) | |
| assert match is not None | |
| crate_name, version = str(match.group(1)).rsplit("-v", 1) | |
| if crate_name in dep_crate_names: | |
| # Don't add the same crate twice. Can happen with many | |
| # versions of the same crate declared with different | |
| # features. | |
| continue | |
| # Remove the trailing suffix from proc-macro crates | |
| version = version.removesuffix("_proc_macro") | |
| dep_crate_names.add(crate_name) | |
| version = version.replace("_", ".") | |
| fout.write('[%s."%s"]\n' % (dep_type, crate_name)) | |
| fout.write('version = "%s"\n' % version) | |
| fout.write("default-features = false\n") | |
| if dep_features := get_features( | |
| project.targets[dep]["rustflags"] | |
| ): | |
| # Filter out features that break our unusual build until they're properly fixed | |
| # upstream | |
| # TODO(376501054): fix ahash upstream and then remove this. | |
| if crate_name == "ahash": | |
| dep_features = [ | |
| f | |
| for f in dep_features | |
| if f != "folded_multiply" | |
| ] | |
| fout.write("features = %s\n" % json.dumps(dep_features)) | |
| if crate_name in features: | |
| # Make the dependency optional if there is a feature with | |
| # the same name. Later, we'll make sure to list the feature | |
| # in the default feature list so that we do actually include | |
| # the dependency. | |
| fout.write("optional = true\n") | |
| # this is a in-tree rust target | |
| else: | |
| toolchain = extract_toolchain(dep) | |
| version = version_from_toolchain(toolchain) | |
| crate_name = lookup_gn_pkg_name(project, dep, for_workspace) | |
| if crate_name in dep_crate_names: | |
| # Don't add the same crate twice. Can happen with many | |
| # versions of the same crate declared with different | |
| # features. | |
| continue | |
| dep_crate_names.add(crate_name) | |
| dep_dir = gn_cargo_dir / lookup[dep] | |
| fout.write( | |
| CARGO_PACKAGE_DEP | |
| % { | |
| "dep_type": dep_type, | |
| "crate_path": dep_dir, | |
| "crate_name": crate_name, | |
| "version": version, | |
| } | |
| ) | |
| write_deps(deps, "dependencies") | |
| write_deps(extra_test_deps, "dev-dependencies") | |
| if features: | |
| fout.write("\n[features]\n") | |
| # Filter 'default' feature out to avoid generating a duplicated entry. | |
| features = [x for x in features if x != "default"] | |
| fout.write("default = %s\n" % json.dumps(features)) | |
| for feature in features: | |
| # Filter features that are also dependencies | |
| # https://users.rust-lang.org/t/features-and-dependencies-cannot-have-the-same-name/47746/2 | |
| if feature not in dep_crate_names: | |
| fout.write("%s = []\n" % feature) | |
| if not for_workspace: | |
| if check_cfgs: | |
| fout.write("\n[lints.rust]\n") | |
| fout.write('unexpected_cfgs = { level = "warn", check-cfg = [') | |
| for check_cfg in check_cfgs: | |
| fout.write("'%s'," % check_cfg) | |
| fout.write("] }\n") | |
| else: | |
| # Disable check-cfg in cargo if not available to avoid the noise. | |
| fout.write("\n[lints.rust]\n") | |
| fout.write('unexpected_cfgs = { level = "allow" }\n') | |
| class PatchEntry(T.NamedTuple): | |
| """An entry in the [patch.crates-io] section of a Cargo.toml manifest.""" | |
| crate: str | |
| path: str | |
| package: str | None = None | |
| def patch_entry_to_string( | |
| rust_crates_path: Path, | |
| patch_entry: PatchEntry, | |
| ) -> str: | |
| if patch_entry.package is not None: | |
| return f'{patch_entry.crate} = {{ path = "{rust_crates_path / patch_entry.path}", package = "{patch_entry.package}" }}' | |
| else: | |
| return f'{patch_entry.crate} = {{ path = "{rust_crates_path / patch_entry.path}" }}' | |
| def patches_entries_toml_block( | |
| rust_crates_path: Path, | |
| patch_entries: list[PatchEntry], | |
| ) -> str: | |
| return "\n".join( | |
| [ | |
| "", | |
| "[patch.crates-io]", | |
| *( | |
| patch_entry_to_string(rust_crates_path, patch_entry) | |
| for patch_entry in patch_entries | |
| ), | |
| "", | |
| "", | |
| ] | |
| ) | |
| def main() -> int: | |
| # TODO(tmandry): Remove all hardcoded paths and replace with args. | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument( | |
| "--project_json", | |
| type=Path, | |
| required=True, | |
| help="Path to project.json", | |
| ) | |
| parser.add_argument( | |
| "--output_dir", | |
| type=Path, | |
| required=True, | |
| help="Directory where output files are written", | |
| ) | |
| parser.add_argument( | |
| "--cargo_toml", | |
| type=Path, | |
| required=True, | |
| help="Path to third_party/rust_crates/Cargo.toml", | |
| ) | |
| parser.add_argument( | |
| "--api_level_cfg_flags", | |
| type=Path, | |
| required=True, | |
| help="Path to rust_api_level_cfg_flags.txt response file", | |
| ) | |
| args = parser.parse_args() | |
| try: | |
| with open(args.project_json, "r") as json_file: | |
| project_json_raw = json.loads(json_file.read()) | |
| project = Project(project_json_raw) | |
| except (IOError, json.decoder.JSONDecodeError) as err: | |
| print("Failed to generate Cargo.toml files") | |
| print("No project.json in the root of your out directory!") | |
| print(f"Caused by: Could not parse file {args.project_json}: {err}") | |
| return 1 | |
| root_path = Path(project_json_raw["build_settings"]["root_path"]) | |
| gn_build_dir = project_json_raw["build_settings"]["build_dir"] | |
| # These need to be absolute paths in the generated Cargo.toml files. | |
| root_build_dir = rebase_gn_path(root_path, gn_build_dir).resolve() | |
| gn_cargo_dir = root_build_dir / "cargo" | |
| # In Bazel, this directory is created before the script is run. | |
| output_dir = args.output_dir | |
| # Create the "for_workspace" directory explicitly so we can | |
| # create subdirectories without using `parents=True`, and | |
| # skip all the extra syscalls that entails. | |
| for_workspace_dir = output_dir / "for_workspace" | |
| for_workspace_dir.mkdir() | |
| # this will be removed eventually? | |
| with open(args.cargo_toml, "rb") as cargo_toml_file: | |
| patches_toml = tomllib.load(cargo_toml_file) | |
| # Create a list of patch entries: | |
| # - name | |
| # - path | |
| # - package (optional) | |
| patch_entries: list[PatchEntry] = [ | |
| PatchEntry(crate, entry["path"], entry.get("package")) | |
| for crate, entry in patches_toml["patch"]["crates-io"].items() | |
| ] | |
| # Pre-render the patches section since it's used in every file. | |
| if patch_entries: | |
| project.patches_toml_block_str = patches_entries_toml_block( | |
| root_path / "third_party/rust_crates", | |
| patch_entries, | |
| ) | |
| # Parse the api_level_cfgs response file into a list of strings | |
| api_level_cfgs: list[str] = [] | |
| with open(args.api_level_cfg_flags) as f: | |
| for line in f: | |
| if line.startswith("--cfg="): | |
| api_level_cfgs.append(line[len("--cfg=") :]) | |
| lookup: dict[str, str] = {} | |
| for target in project.rust_targets: | |
| # hash is the GN target name without the prefixed // | |
| lookup[target] = hashlib.sha1(target[2:].encode("utf-8")).hexdigest() | |
| # a dict of "toolchain label" to list of Cargo.toml files in it | |
| # special case: the key None means the default toolchain | |
| workspace_dirs_by_toolchain: dict[ | |
| str | None, list[tuple[str, str]] | |
| ] = collections.defaultdict(list) | |
| # Pre-cache cached properties before worker threads run | |
| _ = project.rust_targets_by_source_root | |
| _ = project.reachable_targets | |
| def process_target( | |
| target: str, | |
| ) -> tuple[str | None, tuple[str, str]] | None: | |
| toolchain = extract_toolchain(target) | |
| version = version_from_toolchain(toolchain) | |
| cargo_toml_dir = output_dir / lookup[target] | |
| try: | |
| cargo_toml_dir.mkdir() | |
| except OSError: | |
| print(f"Failed to create directory for Cargo: {cargo_toml_dir}") | |
| for_workspace_cargo_toml_dir = ( | |
| output_dir / "for_workspace" / lookup[target] | |
| ) | |
| try: | |
| for_workspace_cargo_toml_dir.mkdir() | |
| except OSError: | |
| print( | |
| f"Failed to create directory for Cargo: {for_workspace_cargo_toml_dir}" | |
| ) | |
| metadata: dict[str, T.Any] = project.targets[target] | |
| write_toml_file( | |
| cargo_toml_dir, | |
| metadata, | |
| project, | |
| target, | |
| lookup, | |
| root_path, | |
| gn_cargo_dir, | |
| version, | |
| api_level_cfgs, | |
| for_workspace=False, | |
| ) | |
| if ( | |
| not target.startswith("//third_party/rust_crates:") | |
| ) and target in project.reachable_targets: | |
| write_toml_file( | |
| for_workspace_cargo_toml_dir, | |
| metadata, | |
| project, | |
| target, | |
| lookup, | |
| root_path, | |
| gn_cargo_dir / "for_workspace", | |
| version, | |
| api_level_cfgs, | |
| for_workspace=True, | |
| ) | |
| return ( | |
| toolchain, | |
| ( | |
| target, | |
| (gn_cargo_dir / "for_workspace" / lookup[target]) | |
| .relative_to(root_path) | |
| .as_posix(), | |
| ), | |
| ) | |
| return None | |
| # This ends up being heavily gated by the GIL, but a few extra threads | |
| # ends up being about twice as fast as single-threaded. | |
| with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: | |
| for result in executor.map(process_target, project.rust_targets): | |
| if result is not None: | |
| toolchain, entry = result | |
| workspace_dirs_by_toolchain[toolchain].append(entry) | |
| # TODO: refactor into separate function | |
| for toolchain, workspace_dirs in workspace_dirs_by_toolchain.items(): | |
| subdir = for_workspace_dir | |
| if toolchain: | |
| # Strip off the leading "//" from the toolchain label so we don't | |
| # accidentally use it as an absolute path. | |
| path_safe_toolchain = toolchain.lstrip("/") | |
| subdir = subdir / "toolchain" / path_safe_toolchain | |
| else: | |
| # the workspace for the default toolchain (None in the dict) just | |
| # lives in for_workspace directly. | |
| pass | |
| try: | |
| subdir.mkdir(parents=True, exist_ok=True) | |
| except OSError: | |
| print(f"Failed to create directory for Cargo: {subdir}") | |
| raise | |
| with open(subdir / "Cargo_for_fuchsia_dir.toml", "w") as fout: | |
| fout.write("[workspace]\nmembers = [\n") | |
| for target, dir in workspace_dirs: | |
| fout.write(" # %s\n" % target) | |
| fout.write(" %s,\n" % json.dumps(dir)) | |
| fout.write("]\n") | |
| fout.write('exclude = ["third_party/rust_crates",]\n') | |
| if patch_entries: | |
| fout.write( | |
| patches_entries_toml_block( | |
| Path("third_party/rust_crates"), patch_entries | |
| ) | |
| ) | |
| rust_targets: list[dict[str, T.Any]] = sorted( | |
| [ | |
| { | |
| "label": t, | |
| "crate_name": project.targets[t]["crate_name"], | |
| "type": project.targets[t]["type"], | |
| "cargo_manifest_dir": lookup[t], | |
| "crate_root": project.targets[t]["crate_root"], | |
| } | |
| for t in project.rust_targets | |
| if t in project.reachable_targets | |
| ], | |
| key=lambda t: t["label"], | |
| ) | |
| # Returns a single rust target per "base" label (not including toolchain), | |
| # either for fuchsia toolchains or host toolchains. This is used for rustdoc | |
| # where we only want to document each crate once for fuchsia and once for host. | |
| def rustdoc_targets( | |
| host: bool, | |
| ) -> list[dict[str, T.Any]]: | |
| cur = "" | |
| result = [] | |
| for t in rust_targets: | |
| disable = None | |
| if meta := project.rust_targets[t["label"]].get("metadata"): | |
| disable = meta.get("disable_rustdoc") | |
| if disable == [True] or ( | |
| t["type"] == "executable" and disable != [False] | |
| ): | |
| continue | |
| l = t["label"].replace(".actual", "") | |
| base = l.split("(")[0] | |
| is_host = "(//build/toolchain:host" in l | |
| if host == is_host: | |
| if base == cur: | |
| continue | |
| cur = base | |
| result.append(copy.deepcopy(t)) | |
| result[-1]["label"] = l | |
| return result | |
| def dump_json( | |
| obj: T.Any, | |
| filename: str, | |
| ) -> None: | |
| with open(output_dir / filename, "w") as f: | |
| json.dump(obj, f) | |
| dump_json(rust_targets, "rust_targets.json") | |
| dump_json(rustdoc_targets(host=False), "rustdoc_targets.json") | |
| dump_json(rustdoc_targets(host=True), "rustdoc_host_targets.json") | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) |