#!/usr/bin/env python3 # Copyright (c) Meta Platforms, Inc. and affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. import argparse import copy import os import re from enum import Enum from typing import Any, List, Optional, Sequence from buck_util import Buck2Runner try: import tomllib # Standard in 3.11 and later except ModuleNotFoundError: import tomli as tomllib """Extracts source lists from the buck2 build system and writes them to a file. The config file is in TOML format and should contains one or more `[targets.]` entries, along with an optional `[target_base]` entry. All of these may have the following lists of strings: - buck_targets: The list of buck targets that map to ``. - deps: A list of other `` entries that this target depends on. Used to prune sources that are provided by those other targets. - filters: A list of regular expressions. This tool will only emit source files whose relative paths match all entries. - excludes: A list of regular expressions. This tool will not emit source files whose relative paths match any entry. The special `[target_base]` entry provides default lists that are inherited by the `[target.]` entries. When the `[target.]` entry defines a key that is already present in `[target_base]`, the target-specific entries are appended to the base list. Example config: [target_base] excludes = [ "^third-party", ] [targets.schema] buck_targets = [ "//schema:schema", ] filters = [ ".fbs$", ] [targets.executorch] buck_targets = [ "//runtime/executor:program", ] deps = [ "schema", ] filters = [ ".cpp$", ] """ class Target: """Parsed [targets.*] entry from the TOML file. Can query buck for its list of source files. """ class _InitState(Enum): UNINITIALIZED = 0 INITIALIZING = 1 READY = 2 def __init__( self, name: str, target_dict: dict[str, Sequence[str]], base_dict: Optional[dict] = None, ) -> None: self._state: Target._InitState = Target._InitState.UNINITIALIZED self._sources = frozenset() self.name = name # Extend the base lists with the target-specific entries. self._config = copy.deepcopy(base_dict or {}) for k, v in target_dict.items(): if k in self._config: self._config[k].extend(v) else: self._config[k] = v def get_sources( self, graph: "Graph", runner: Buck2Runner, buck_args: Optional[List[str]] ) -> frozenset[str]: if buck_args is None: buck_args = [] if self._state == Target._InitState.READY: return self._sources # Detect cycles. assert self._state != Target._InitState.INITIALIZING # Assemble the query. query = "inputs({})".format( "+".join( [ "deps('{}')".format(target) for target in self._config.get("buck_targets", []) ] ) ) # Get the complete list of source files that this target depends on. sources: set[str] = set(runner.run(["cquery", query] + buck_args)) # Keep entries that match all of the filters. filters = [re.compile(p) for p in self._config.get("filters", [])] sources = {s for s in sources if all(p.search(s) for p in filters)} # Remove entries that match any of the excludes. excludes = [re.compile(p) for p in self._config.get("excludes", [])] sources = {s for s in sources if not any(p.search(s) for p in excludes)} # The buck query will give us the complete list of sources that this # target depends on, but that list includes sources that are owned by # its deps. Remove entries that are already covered by the transitive # set of dependencies. for dep in self._config.get("deps", []): sources.difference_update( graph.by_name[dep].get_sources(graph, runner, buck_args) ) self._sources = frozenset(sources) self._state = Target._InitState.READY return self._sources class Graph: """Graph of targets.""" def __init__(self, config_dict: dict[str, Any]) -> None: base = config_dict.get("target_base", {}) targets = config_dict.get("targets", {}) self.by_name = {} for k, v in targets.items(): self.by_name[k] = Target(k, v, base) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Extracts deps from the buck2 build system", ) parser.add_argument( "--buck2", default="buck2", help="'buck2' command to use", ) parser.add_argument( "--config", metavar="config.toml", required=True, help="Path to the input TOML configuration file", ) parser.add_argument( "--format", default="cmake", choices=["cmake"], help="Format to generate.", ) parser.add_argument( "--out", metavar="file", help="Path to the file to generate.", ) parser.add_argument( "--target-platforms", help="--target-platforms to pass to buck cquery, if any." ) return parser.parse_args() def generate_cmake(target_to_srcs: dict[str, list[str]]) -> bytes: lines: list[str] = [] lines.append("# @" + f"generated by {os.path.basename(__file__)}") for target, srcs in target_to_srcs.items(): lines.append("") lines.append(f"set(_{target}__srcs") for src in srcs: lines.append(f" {src}") lines.append(")") return "\n".join(lines).encode("utf-8") def main(): args = parse_args() # Load and parse the TOML configuration with open(args.config, mode="rb") as fp: config_dict = tomllib.load(fp) graph = Graph(config_dict) # Run the queries and get the lists of source files. target_to_srcs: dict[str, list[str]] = {} runner: Buck2Runner = Buck2Runner(args.buck2) buck_args = [] if args.target_platforms: buck_args = ["--target-platforms"] buck_args.append(args.target_platforms) for name, target in graph.by_name.items(): target_to_srcs[name] = sorted(target.get_sources(graph, runner, buck_args)) # Generate the requested format. output: bytes if args.format == "cmake": output = generate_cmake(target_to_srcs) else: raise ValueError("Unknown format: {}".format(args.format)) # Write the output. with open(args.out, "wb") as fp: fp.write(output) if __name__ == "__main__": main()