diff --git a/.bazelrc b/.bazelrc index 9901e3ce..181d2c45 100644 --- a/.bazelrc +++ b/.bazelrc @@ -1,6 +1,9 @@ # Use platforms for compiling C/C++ code. build --incompatible_enable_cc_toolchain_resolution +# Make git version information available to bazel rules. +build --workspace_status_command=./tools/bin/workspace_status.sh + # Allow the user to switch to various Clang version for compiling everything. build:clang11 --platform_suffix=clang11 build:clang11 --//build:requested_compiler_flag=clang11 diff --git a/BUILD b/BUILD index dade571a..a45419f1 100644 --- a/BUILD +++ b/BUILD @@ -37,7 +37,9 @@ BASE_UNITS = [ BASE_UNIT_STRING = " ".join(BASE_UNITS) -CMD_ROOT = "$(location tools/bin/make-single-file) {extra_opts} --units {units} > $(OUTS)" +GIT_ID_CMD = "cat bazel-out/stable-status.txt | grep STABLE_GIT_ID | sed 's/STABLE_GIT_ID \\(.*\\)/\\1/' | tr -d '\\n'" + +CMD_ROOT = "$(location tools/bin/make-single-file) {extra_opts} --units {units} --version-id $$({id_cmd}) > $(OUTS)" ################################################################################ # Release single-file package `au.hh` @@ -49,7 +51,9 @@ genrule( cmd = CMD_ROOT.format( extra_opts = "", units = BASE_UNIT_STRING, + id_cmd = GIT_ID_CMD, ), + stamp = True, tools = ["tools/bin/make-single-file"], ) @@ -69,7 +73,9 @@ genrule( cmd = CMD_ROOT.format( extra_opts = "--noio", units = BASE_UNIT_STRING, + id_cmd = GIT_ID_CMD, ), + stamp = True, tools = ["tools/bin/make-single-file"], visibility = ["//release:__pkg__"], ) diff --git a/tools/bin/make-single-file b/tools/bin/make-single-file index 46dbc9c1..a0f8c8aa 100755 --- a/tools/bin/make-single-file +++ b/tools/bin/make-single-file @@ -5,6 +5,7 @@ import argparse import datetime import re +import subprocess import sys @@ -20,25 +21,26 @@ def main(argv=None): `#include` directives (such as standard library headers) untouched. """ args = parse_command_line_args(argv) - files = parse_files(filenames=filenames(args=args)) - print_unified_file(files) + files = parse_files( + filenames=filenames( + main_files=args.main_files, units=args.units, include_io=args.include_io + ) + ) + print_unified_file(files, args=args) return 0 -def filenames(args): +def filenames(main_files, units, include_io): """Construct the list of project filenames to include. The script will be sure to include all of these, and will also include any transitive dependencies from within the project. - - :param args: A Namespace object whose fields include the lists `main_files` - and `units`, and the boolean `include_io`. """ names = ( - ["au/au.hh"] + [f"au/units/{unit}.hh" for unit in args.units] + args.main_files + ["au/au.hh"] + [f"au/units/{unit}.hh" for unit in units] + main_files ) - if args.include_io: + if include_io: names.append("au/io.hh") return names @@ -51,6 +53,12 @@ def parse_command_line_args(argv): parser.add_argument("--units", nargs="*", default=[], help="The units to include") + parser.add_argument( + "--version-id", + default=git_id_description(), + help="An identifier for the version of code used to build this file", + ) + parser.add_argument( "--noio", action="store_false", @@ -161,7 +169,7 @@ def include_lines(files): return set(line for f in files for line in files[f].global_includes) -def print_unified_file(files): +def print_unified_file(files, args): """Print the single-file output to stdout.""" print(f"// {AURORA_COPYRIGHT.format(year=datetime.datetime.now().year)}") print() @@ -171,10 +179,76 @@ def print_unified_file(files): for i in sorted(include_lines(files)): print(i) + print() + for line in manifest(args=args): + print(f"// {line}") + for f in sort_topologically(files): for line in files[f].lines: print(line) +def manifest(args): + """A sequence of lines describing the options that generated this file.""" + args = CheckArgs(args) + + lines = [ + f"Version identifier: {args.version_id}", + f' support: {"INCLUDED" if args.include_io else "EXCLUDED"}', + "List of included units:", + ] + [f" {u}" for u in sorted(args.units)] + + if args.main_files: + lines.append("Extra files included:") + lines.extend(f" {f}" for f in sorted(args.main_files)) + + assert args.are_all_used() + return lines + + +class CheckArgs(dict): + def __init__(self, args): + self.vars = vars(args) + self.used = set() + + def are_all_used(self): + return not set(self.vars.keys()).difference(self.used) + + def __getattr__(self, k): + self.used.add(k) + return self.vars[k] + + +def git_id_description(): + """A description of the ID (commit or tag) in git, and whether it's clean.""" + # TODO(#21): The logic for the identifier currently lives in two places. + # After `make-single-file` becomes a thin wrapper on the underlying logic in + # python, we can fix this by making the `--version-id` argument _required_, + # and deleting this function. (We can have the underlying python rule + # depend on the existence of the stable version stamp, and simply use that + # always.) + try: + return ( + subprocess.check_output( + ["git", "describe", "--always", "--dirty"], stderr=subprocess.DEVNULL + ) + .decode("ascii") + .strip() + ) + except subprocess.CalledProcessError: + # We do not ever expect this line to affect the generated file in normal + # operations. If users are running the script manually, then the above + # command will succeed, because currently the command is not sandboxed. + # And if users are running a genrule for a pre-built single-file + # package, then that command is constructed to supply the version + # information, which means that the below value will be _returned_ but + # not _used_. + # + # Really, the point of the following line is to generate a valid default + # value (so that the script does not crash) in the cases where we know + # the default value won't be used. + return "(Unknown version)" + + if __name__ == "__main__": sys.exit(main()) diff --git a/tools/bin/workspace_status.sh b/tools/bin/workspace_status.sh new file mode 100755 index 00000000..c2b886ac --- /dev/null +++ b/tools/bin/workspace_status.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +echo STABLE_GIT_ID $(git describe --always --dirty)