| #!/usr/bin/env fuchsia-vendored-python |
| # Copyright 2018 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 json |
| import os |
| import sys |
| |
| FUCHSIA_MODULE = "go.fuchsia.dev/fuchsia" |
| |
| |
| class Source(object): |
| def __init__(self, name, path, file): |
| self.name = name |
| self.path = path |
| self.file = file |
| |
| def __str__(self): |
| return "%s[%s]" % (self.name, self.path) |
| |
| def __hash__(self): |
| return hash((self.name, self.path)) |
| |
| def __eq__(self, other): |
| return self.name == other.name and self.path == other.path |
| |
| |
| def get_sources(dep_files, extra_sources=None): |
| # Aggregate source data from dependencies. |
| sources = set() |
| if extra_sources: |
| sources.update(extra_sources) |
| for dep in dep_files: |
| with open(dep, "r") as dep_file: |
| for name, path in json.load(dep_file)["sources"].items(): |
| sources.add(Source(name, path, dep)) |
| |
| # Verify duplicates. |
| sources_by_name = {} |
| for src in sources: |
| sources_by_name.setdefault(src.name, []).append(src) |
| for name, srcs in sources_by_name.items(): |
| if len(srcs) <= 1: |
| continue |
| print('Error: source "%s" has multiple paths.' % name) |
| for src in srcs: |
| print(" - %s (%s)" % (src.path, src.file)) |
| raise Exception("Could not aggregate sources") |
| |
| return {s.name: s.path for s in sources} |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser() |
| name_group = parser.add_mutually_exclusive_group(required=True) |
| name_group.add_argument("--name", help="Name of the current library") |
| name_group.add_argument( |
| "--name-file", |
| help="Path to a file containing the name of the current library", |
| ) |
| parser.add_argument( |
| "--root-build-dir", |
| help="Path to the root build directory", |
| required=True, |
| ) |
| parser.add_argument( |
| "--source-dir", |
| help="Path to the library's source directory", |
| required=True, |
| ) |
| sources_group = parser.add_mutually_exclusive_group(required=True) |
| sources_group.add_argument( |
| "--sources", help="List of source files", nargs="*" |
| ) |
| sources_group.add_argument( |
| "--allow-globbing", |
| action="store_true", |
| help="Allow globbing the entire source directory", |
| ) |
| parser.add_argument( |
| "--output", help="Path to the file to generate", required=True |
| ) |
| parser.add_argument( |
| "--deps", help="Dependencies of the current library", nargs="*" |
| ) |
| args = parser.parse_args() |
| if args.name: |
| name = args.name |
| elif args.name_file: |
| with open(args.name_file, "r") as name_file: |
| name = name_file.read() |
| |
| build_dir = os.path.abspath(args.root_build_dir) |
| |
| # Find source_root from library source_dir, i.e. do not assume |
| # that the build directory is two levels down the source root |
| source_root = build_dir |
| while not os.path.exists(os.path.join(source_root, ".jiri_manifest")): |
| source_root = os.path.dirname(source_root) |
| if source_root in ("", "/"): |
| parser.error("Cannot find Fuchsia source directory!") |
| |
| third_party_dir = os.path.join(source_root, "third_party") |
| |
| # For Fuchsia sources, the declared package name must correspond to the |
| # source directory so that raw `go` commands (e.g., as an IDE would use) can |
| # resolve the source path based on the package name. |
| # TODO(olivernewman): Stop exempting package names that don't start with |
| # `FUCHSIA_MODULE`; all packages should use absolute names that start with |
| # the module name. |
| if name.startswith(FUCHSIA_MODULE) and not os.path.abspath( |
| args.source_dir |
| ).startswith((build_dir, third_party_dir)): |
| expected_name = ( |
| FUCHSIA_MODULE |
| + "/" |
| + os.path.relpath(args.source_dir, source_root).replace( |
| os.path.sep, "/" |
| ) |
| ) |
| if name not in (expected_name, expected_name + "/..."): |
| raise ValueError( |
| f"go_library name must correspond to the source dir: " |
| f"got {name!r}, expected {expected_name!r}" |
| ) |
| |
| current_sources = [] |
| if args.sources: |
| for source in args.sources: |
| p = os.path.join(args.source_dir, source) |
| # Explicit sources must be files. |
| if not os.path.isfile(p): |
| raise ValueError(f"Source {p} is not a file") |
| current_sources.append( |
| Source(os.path.join(name, source), p, args.output) |
| ) |
| if not name.endswith("/..."): |
| go_sources = {f for f in args.sources if f.endswith(".go")} |
| |
| # Go sources are constrained to live top-level under `source_dir`; |
| # others (e.g., template files) are free to live further down. |
| for s in go_sources: |
| if os.path.dirname(s): |
| raise ValueError( |
| f'Source "{s}" for "{name}" comes from a subdirectory.' |
| f" Specify source_dir instead." |
| ) |
| |
| # Require all non-generated Go files to be listed as sources. |
| if not os.path.abspath(args.source_dir).startswith(build_dir): |
| # TODO: Use `glob.glob("*.go", root_dir=args.source_dir)` |
| # instead of os.listdir after upgrading to Python 3.10. |
| go_files = { |
| f for f in os.listdir(args.source_dir) if f.endswith(".go") |
| } |
| missing = go_files - go_sources |
| if missing: |
| raise ValueError( |
| f"go_library requires that all Go files in source_dir" |
| f" be listed as sources, but the following files are" |
| f" missing from sources for target {name}:" |
| f' {", ".join(sorted(missing))}' |
| ) |
| elif args.allow_globbing: |
| current_sources.append(Source(name, args.source_dir, args.output)) |
| result = get_sources(args.deps, extra_sources=current_sources) |
| with open(args.output, "w") as output_file: |
| json.dump( |
| { |
| "package": name, |
| "sources": result, |
| }, |
| output_file, |
| indent=2, |
| sort_keys=True, |
| ) |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |