diff --git a/.bazelrc b/.bazelrc index 163e89c..29ed0b5 100644 --- a/.bazelrc +++ b/.bazelrc @@ -23,6 +23,10 @@ build --enable_runfiles common --enable_platform_specific_config +# For testing our --stamp behavior. +# Normally users would use a --workspace_status_command with a script that calls `git describe`. +build --embed_label=v1.2.3 + # Turn off legacy external runfiles on all platforms except Windows. # This allows our users to turn on this flag as well, which is a performance win. # Skylib's diff_test implementation for Windows depends on legacy external diff --git a/docs/jq.md b/docs/jq.md index 4baad06..eb21450 100644 --- a/docs/jq.md +++ b/docs/jq.md @@ -86,6 +86,19 @@ jq( ], filter = "{ deps: split("\n") | map(select(. | length > 0)) }", ) + +# With --stamp, causes properties to be replaced by version control info. +jq( + name = "stamped", + srcs = ["package.json"], + filter = "|".join([ + # Don't directly reference $STAMP as it's only set when stamping + # This 'as' syntax results in $stamp being null in unstamped builds. + "$ARGS.named.STAMP as $stamp", + # Provide a default using the "alternative operator" in case $stamp is null. + ".version = ($stamp.BUILD_EMBED_LABEL // "")", + ]), +) ``` @@ -96,7 +109,7 @@ jq( | :------------- | :------------- | :------------- | | name | Name of the rule | none | | srcs | List of input files | none | -| filter | Filter expression (https://stedolan.github.io/jq/manual/#Basicfilters) | None | +| filter | Filter expression (https://stedolan.github.io/jq/manual/#Basicfilters). Subject to stamp variable replacements, see [Stamping](./stamping.md). When stamping is enabled, a variable named "STAMP" will be available in the filter.

Be careful to write the filter so that it handles unstamped builds, as in the example above. | None | | filter_file | File containing filter expression (alternative to filter) | None | | args | Additional args to pass to jq | [] | | out | Name of the output json file; defaults to the rule name plus ".json" | None | diff --git a/docs/stamping.md b/docs/stamping.md index 06e7ea0..09a53c5 100755 --- a/docs/stamping.md +++ b/docs/stamping.md @@ -1,6 +1,6 @@ -Version Stamping +# Version Stamping Bazel is generally only a build tool, and is unaware of your version control system. However, when publishing releases, you may want to embed version information in the resulting distribution. @@ -23,7 +23,8 @@ They will usually say something like "subject to stamp variable replacements". ## Stamping with a Workspace status script -To define additional statuses, pass the `--workspace_status_command` argument to `bazel`. +To define additional statuses, pass the `--workspace_status_command` flag to `bazel`. +This slows down every build, so you should avoid passing this flag unless you need to stamp this build. The value of this flag is a path to a script that prints space-separated key/value pairs, one per line, such as ```bash @@ -68,9 +69,9 @@ def _rule_impl(ctx): inputs = [] stamp = maybe_stamp(ctx) if stamp: - args.add("--volatile_status_file", stamp.volatile_status) - args.add("--stable_status_file", stamp.stable_status) - inputs.extend([stamp.volatile_status, stamp.stable_status]) + args.add("--volatile_status_file", stamp.volatile_status_file.path) + args.add("--stable_status_file", stamp.stable_status_file.path) + inputs.extend([stamp.volatile_status_file, stamp.stable_status_file]) # ... call actions which parse the stamp files and do something with the values ... ``` diff --git a/lib/jq.bzl b/lib/jq.bzl index 0304112..26bc77d 100644 --- a/lib/jq.bzl +++ b/lib/jq.bzl @@ -85,12 +85,30 @@ def jq(name, srcs, filter = None, filter_file = None, args = [], out = None, **k ], filter = "{ deps: split(\"\\n\") | map(select(. | length > 0)) }", ) + + # With --stamp, causes properties to be replaced by version control info. + jq( + name = "stamped", + srcs = ["package.json"], + filter = "|".join([ + # Don't directly reference $STAMP as it's only set when stamping + # This 'as' syntax results in $stamp being null in unstamped builds. + "$ARGS.named.STAMP as $stamp", + # Provide a default using the "alternative operator" in case $stamp is null. + ".version = ($stamp.BUILD_EMBED_LABEL // \"\")", + ]), + ) ``` Args: name: Name of the rule srcs: List of input files - filter: Filter expression (https://stedolan.github.io/jq/manual/#Basicfilters) + filter: Filter expression (https://stedolan.github.io/jq/manual/#Basicfilters). + Subject to stamp variable replacements, see [Stamping](./stamping.md). + When stamping is enabled, a variable named "STAMP" will be available in the filter. + + Be careful to write the filter so that it handles unstamped builds, as in the example above. + filter_file: File containing filter expression (alternative to `filter`) args: Additional args to pass to jq out: Name of the output json file; defaults to the rule name plus ".json" diff --git a/lib/private/BUILD.bazel b/lib/private/BUILD.bazel index 2c22b35..1a790af 100644 --- a/lib/private/BUILD.bazel +++ b/lib/private/BUILD.bazel @@ -7,6 +7,11 @@ exports_files( visibility = ["//docs:__pkg__"], ) +exports_files( + ["parse_status_file.jq"], + visibility = ["//visibility:public"], +) + bzl_library( name = "copy_common", srcs = ["copy_common.bzl"], @@ -103,6 +108,7 @@ bzl_library( bzl_library( name = "jq", srcs = ["jq.bzl"], + deps = ["//lib:stamping"], ) bzl_library( diff --git a/lib/private/jq.bzl b/lib/private/jq.bzl index 12ef71e..4c7827e 100644 --- a/lib/private/jq.bzl +++ b/lib/private/jq.bzl @@ -1,6 +1,8 @@ """Implementation for jq rule""" -_jq_attrs = { +load("//lib:stamping.bzl", "STAMP_ATTRS", "maybe_stamp") + +_jq_attrs = dict({ "srcs": attr.label_list( allow_files = True, mandatory = True, @@ -10,7 +12,11 @@ _jq_attrs = { "filter_file": attr.label(allow_single_file = True), "args": attr.string_list(), "out": attr.output(), -} + "_parse_status_file_filter": attr.label( + allow_single_file = True, + default = Label("//lib/private:parse_status_file.jq"), + ), +}, **STAMP_ATTRS) def _jq_impl(ctx): jq_bin = ctx.toolchains["@aspect_bazel_lib//lib:jq_toolchain_type"].jqinfo.bin @@ -32,6 +38,34 @@ def _jq_impl(ctx): args = args + ["--from-file '%s'" % ctx.file.filter_file.path] inputs.append(ctx.file.filter_file) + stamp = maybe_stamp(ctx) + if stamp: + # create an action that gives a JSON representation of the stamp keys + stamp_json = ctx.actions.declare_file("_%s_stamp.json" % ctx.label.name) + ctx.actions.run_shell( + tools = [jq_bin], + inputs = [stamp.stable_status_file, stamp.volatile_status_file, ctx.file._parse_status_file_filter], + outputs = [stamp_json], + command = "{jq} -s -R -f {filter} {stable} {volatile} > {out}".format( + jq = jq_bin.path, + filter = ctx.file._parse_status_file_filter.path, + stable = stamp.stable_status_file.path, + volatile = stamp.volatile_status_file.path, + out = stamp_json.path, + ), + mnemonic = "ConvertStatusToJson", + ) + inputs.append(stamp_json) + + # jq says of --argfile: + # > Do not use. Use --slurpfile instead. + # > (This option is like --slurpfile, but when the file has just one text, + # > then that is used, else an array of texts is used as in --slurpfile.) + # However there's no indication that it's deprecated. Maybe it's a style convention. + # For our purposes, "$STAMP.BUILD_TIMESTAMP" looks a lot more sensible in a BUILD file + # than "$STAMP[0].BUILD_TIMESTAMP". + args = args + ["--argfile", "STAMP", stamp_json.path] + cmd = "{jq} {args} {filter} {sources} > {out}".format( jq = jq_bin.path, args = " ".join(args), diff --git a/lib/private/parse_status_file.jq b/lib/private/parse_status_file.jq new file mode 100644 index 0000000..4008f1f --- /dev/null +++ b/lib/private/parse_status_file.jq @@ -0,0 +1,5 @@ +[ + split("\n")[] # Convert lines to array + | capture("(?[^\\s]+)\\s+(?.*)"; "x") # capture {"key": [everything before first whitespace], "value": [remainder of line]} +] +| from_entries # Convert [{"key": "a", "value": "b"}] to map {"a": "b"} diff --git a/lib/stamping.bzl b/lib/stamping.bzl index 9b3ec51..76f0195 100644 --- a/lib/stamping.bzl +++ b/lib/stamping.bzl @@ -1,4 +1,4 @@ -"""Version Stamping +"""# Version Stamping Bazel is generally only a build tool, and is unaware of your version control system. However, when publishing releases, you may want to embed version information in the resulting distribution. @@ -21,7 +21,8 @@ They will usually say something like "subject to stamp variable replacements". ## Stamping with a Workspace status script -To define additional statuses, pass the `--workspace_status_command` argument to `bazel`. +To define additional statuses, pass the `--workspace_status_command` flag to `bazel`. +This slows down every build, so you should avoid passing this flag unless you need to stamp this build. The value of this flag is a path to a script that prints space-separated key/value pairs, one per line, such as ```bash @@ -66,9 +67,9 @@ def _rule_impl(ctx): inputs = [] stamp = maybe_stamp(ctx) if stamp: - args.add("--volatile_status_file", stamp.volatile_status) - args.add("--stable_status_file", stamp.stable_status) - inputs.extend([stamp.volatile_status, stamp.stable_status]) + args.add("--volatile_status_file", stamp.volatile_status_file.path) + args.add("--stable_status_file", stamp.stable_status_file.path) + inputs.extend([stamp.volatile_status_file, stamp.stable_status_file]) # ... call actions which parse the stamp files and do something with the values ... ``` diff --git a/lib/tests/jq/BUILD.bazel b/lib/tests/jq/BUILD.bazel index e4df735..d2d5fa2 100644 --- a/lib/tests/jq/BUILD.bazel +++ b/lib/tests/jq/BUILD.bazel @@ -1,5 +1,6 @@ load("//lib/tests/jq:diff_test.bzl", "diff_test") load("//lib:jq.bzl", "jq") +load("//lib:testing.bzl", "assert_contains") exports_files([ "a_pretty.json", @@ -95,6 +96,39 @@ diff_test( file2 = ":case_filter_file", ) +# Filter that uses a stamp variable +[ + jq( + name = ("" if stamp else "un") + "stamped", + srcs = ["a.json"], + filter = "|".join([ + # Don't directly reference $STAMP as it's only set when stamping + "$ARGS.named.STAMP as $stamp", + # Provide a default using the "alternative operator" + ".foo = ($stamp.BUILD_EMBED_LABEL // \"\")", + ".value = ($stamp.BUILD_TIMESTAMP // 1 | tonumber)", + ]), + stamp = stamp, + ) + for stamp in [ + 0, + 1, + ] +] + +assert_contains( + name = "check_stamped", + actual = "stamped.json", + # v1.2.3 comes from the --embed_label flag in .bazelrc + expected = """foo": "v1.2.3""", +) + +assert_contains( + name = "check_unstamped", + actual = "unstamped.json", + expected = """foo": """", +) + # Call jq within a genrule genrule( name = "case_genrule",