feat: add stamp support for pkg_rpm Release tag (#1039)
* feat: add stamp support for pkg_rpm Release tag
Allow `{VARIABLE_NAME}` placeholders in the `release` attribute of
`pkg_rpm`. When `stamp = 1` (or `stamp = -1` with `--stamp`), workspace
status variables from volatile-status.txt are substituted at build time.
- Add `get_status_vars()` to `pkg/private/build_info.py`
- Add `--volatile_status_file` and `--stable_status_file` args to
`make_rpm.py`; apply substitution in `RpmBuilder.__init__` via the
new `stamp_vars` parameter
- Add `stamp` and `private_stamp_detect` attrs to the `pkg_rpm` rule;
rename the rule to `_pkg_rpm_rule` and expose a `pkg_rpm` macro that
injects `private_stamp_detect` via `select()`, matching the pattern
used by `pkg_tar` and `pkg_zip`
- Avoid embedding template placeholders in the output filename when stamp
is active
Fixes #1038
* clean up uses of str.strip() in build_info.py
We don't need to call it multiple times on the same line value, we can
just call it once and save it.
* Address PR #1039 review feedback
- Add docstring to get_status_vars() in build_info.py
- Use get_stamp_detect() from util.bzl instead of inline select()
to match the pattern from PR #1035
- Expand release attr docstring to document stamp variable substitution
- Add stamped RPM example to examples/rpm/system_rpmbuild_pfgdiff --git a/doc_build/BUILD b/doc_build/BUILD
index 55837af..b144f5a 100644
--- a/doc_build/BUILD
+++ b/doc_build/BUILD
@@ -44,6 +44,7 @@
("pkg_deb_impl", "//pkg/private/deb:deb.bzl"),
("pkg_sub_rpm", "//pkg:rpm_pfg.bzl"),
("pkg_rpm", "//pkg:rpm_pfg.bzl"),
+ ("pkg_rpm_impl", "//pkg:rpm_pfg.bzl"),
("pkg_tar", "//pkg/private/tar:tar.bzl"),
("pkg_tar_impl", "//pkg/private/tar:tar.bzl"),
("pkg_zip", "//pkg/private/zip:zip.bzl"),
diff --git a/examples/rpm/system_rpmbuild_stamp_bzlmod/BUILD b/examples/rpm/system_rpmbuild_stamp_bzlmod/BUILD
new file mode 100644
index 0000000..5619cc8
--- /dev/null
+++ b/examples/rpm/system_rpmbuild_stamp_bzlmod/BUILD
@@ -0,0 +1,42 @@
+# Copyright 2026 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Example: use stamp = 1 to substitute workspace status variables in the
+# release tag. Build with `bazel build --stamp :test-rpm-stamped` to embed
+# the build timestamp.
+
+load("@rules_pkg//pkg:mappings.bzl", "pkg_files")
+load("@rules_pkg//pkg:rpm.bzl", "pkg_rpm")
+
+pkg_files(
+ name = "rpm_content",
+ srcs = [
+ "BUILD",
+ "MODULE.bazel",
+ "README.md",
+ ],
+ prefix = "/usr/share",
+)
+
+pkg_rpm(
+ name = "test-rpm-stamped",
+ srcs = ["rpm_content"],
+ architecture = "noarch",
+ description = "placeholder 2",
+ license = "Apache License, v2.0",
+ release = "0.{BUILD_TIMESTAMP}",
+ stamp = 1,
+ summary = "rules_pkg example RPM with stamped release",
+ version = "1",
+)
diff --git a/examples/rpm/system_rpmbuild_stamp_bzlmod/MODULE.bazel b/examples/rpm/system_rpmbuild_stamp_bzlmod/MODULE.bazel
new file mode 100644
index 0000000..3b1faa3
--- /dev/null
+++ b/examples/rpm/system_rpmbuild_stamp_bzlmod/MODULE.bazel
@@ -0,0 +1,33 @@
+# Copyright 2026 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+module(name = "rules_pkg_example_rpm_system_rpmbuild_pfg")
+
+bazel_dep(name = "bazel_skylib", version = "1.8.1")
+bazel_dep(name = "rules_cc", version = "0.1.2")
+bazel_dep(name = "rules_pkg")
+local_path_override(
+ module_name = "rules_pkg",
+ path = "../../..",
+)
+
+find_rpmbuild = use_extension(
+ "@rules_pkg//toolchains/rpm:rpmbuild_configure.bzl",
+ "find_system_rpmbuild_bzlmod",
+)
+use_repo(find_rpmbuild, "rules_pkg_rpmbuild")
+
+register_toolchains(
+ "@rules_pkg_rpmbuild//:all",
+)
diff --git a/examples/rpm/system_rpmbuild_stamp_bzlmod/README.md b/examples/rpm/system_rpmbuild_stamp_bzlmod/README.md
new file mode 100644
index 0000000..e100da8
--- /dev/null
+++ b/examples/rpm/system_rpmbuild_stamp_bzlmod/README.md
@@ -0,0 +1,19 @@
+# Using system rpmbuild with bzlmod and stamp = 1
+
+## Summary
+
+Target declarations may use the `stamp` attribute to control the time
+stamping of files in an archive. The behavior follows the pattern of
+the cc_binary rule:
+
+https://docs.bazel.build/versions/main/be/c-cpp.html#cc_binary
+
+Read the BUILD file for more details.
+
+## To Demonstrate
+
+```
+bazel build test-rpm-stamped
+# look for the build timestamp in the Release field:
+rpm -qip bazel-bin/test-rpm-stamped-1.noarch.rpm
+```
diff --git a/pkg/BUILD b/pkg/BUILD
index 0de5b7b..58ac877 100644
--- a/pkg/BUILD
+++ b/pkg/BUILD
@@ -79,10 +79,7 @@
"//conditions:default": [":not_compatible"],
}),
visibility = ["//visibility:public"],
- deps = [
- "//pkg:make_rpm_lib",
- "//pkg/private:archive",
- ],
+ deps = ["//pkg:make_rpm_lib"],
)
py_library(
@@ -96,6 +93,7 @@
],
deps = [
"//pkg/private:archive",
+ "//pkg/private:build_info",
"//pkg/private:helpers",
],
)
diff --git a/pkg/make_rpm.py b/pkg/make_rpm.py
index d4a7c37..a6a84b8 100644
--- a/pkg/make_rpm.py
+++ b/pkg/make_rpm.py
@@ -29,6 +29,7 @@
import tempfile
from string import Template
+from pkg.private import build_info
from pkg.private import helpers
@@ -188,11 +189,14 @@
}
def __init__(self, name, version, release, arch, rpmbuild_path,
- source_date_epoch=None,
+ source_date_epoch=None, stamp_vars=None,
debug=False):
self.name = name
self.version = helpers.GetFlagValue(version)
self.release = helpers.GetFlagValue(release)
+ if stamp_vars and self.release:
+ for key, val in stamp_vars.items():
+ self.release = self.release.replace('{' + key + '}', val)
self.arch = arch
self.files = []
self.rpmbuild_path = FindRpmbuild(rpmbuild_path)
@@ -560,6 +564,10 @@
'environment variable')
parser.add_argument('--debug', action='store_true', default=False,
help='Print debug messages.')
+ parser.add_argument('--volatile_status_file', default='',
+ help='Path to volatile-status.txt for stamp variable substitution.')
+ parser.add_argument('--stable_status_file', default='',
+ help='Path to stable-status.txt for stamp variable substitution. ')
# Options currently used experimental/rpm.bzl:
parser.add_argument('--install_script',
@@ -595,10 +603,16 @@
options = parser.parse_args(argv or ())
try:
+ stamp_vars = {}
+ if options.volatile_status_file:
+ stamp_vars = build_info.get_status_vars(options.volatile_status_file)
+ if options.stable_status_file:
+ stamp_vars.update(build_info.get_status_vars(options.stable_status_file))
builder = RpmBuilder(options.name,
options.version, options.release,
options.arch, options.rpmbuild,
source_date_epoch=options.source_date_epoch,
+ stamp_vars=stamp_vars,
debug=options.debug)
builder.AddFiles(options.files)
return builder.Build(options.spec_file, options.out_file,
diff --git a/pkg/private/build_info.py b/pkg/private/build_info.py
index 8b8f3a0..22334a3 100644
--- a/pkg/private/build_info.py
+++ b/pkg/private/build_info.py
@@ -11,14 +11,41 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-"""Get BUILD_TIMESTAMP."""
+"""Workspace status file utilities."""
+def get_status_vars(status_file, include_empty=True):
+ """Read workspace status variables from a status file.
+
+ Reads a file of "name<space>value" pairs and returns a dict of all
+ variables found. The file should be in the workspace status format:
+ https://docs.bazel.build/versions/master/user-manual.html#workspace_status
+
+ Args:
+ status_file: path to a workspace status file. Typically
+ ctx.info_file.path (stable) or ctx.version_file.path (volatile).
+ include_empty: if True (default), keys with no value are included
+ in the result with an empty-string value.
+ Returns:
+ dict: mapping of variable name to value.
+ """
+ result = {}
+ with open(status_file, 'r') as f:
+ for line in f:
+ stripped = line.strip()
+ if not stripped:
+ continue
+ parts = stripped.split(' ', 1)
+ if len(parts) == 2:
+ result[parts[0]] = parts[1]
+ elif include_empty:
+ result[parts[0]] = ""
+
+ return result
def get_timestamp(volatile_status_file):
- """Get BUILD_TIMESTAMP as an integer.
+ """Get BUILD_TIMESTAMP as an integer from volatile-status.txt.
- Reads a file of "name<space>value" pairs and returns the value
- of the BUILD_TIMESTAMP. The file should be in the workspace status
+ The file should be in the workspace status
format: https://docs.bazel.build/versions/master/user-manual.html#workspace_status
Args:
@@ -28,10 +55,10 @@
Exceptions:
Exception: Raised if there is no BUILD_TIMESTAMP or if it is not a number.
"""
- with open(volatile_status_file, 'r') as status_f:
- for line in status_f:
- parts = line.strip().split(' ')
- if len(parts) > 1 and parts[0] == 'BUILD_TIMESTAMP':
- return int(parts[1])
+ status_vars = get_status_vars(volatile_status_file, include_empty=False)
+ key = 'BUILD_TIMESTAMP'
+ ts = status_vars.get(key)
+ if ts is not None:
+ return int(ts)
raise Exception(
- "Invalid status file <%s>. Expected to find BUILD_TIMESTAMP" % volatile_status_file)
+ "Invalid status file <%s>. Expected to find %s" % (volatile_status_file, key))
diff --git a/pkg/rpm_pfg.bzl b/pkg/rpm_pfg.bzl
index 205a620..9533ad0 100644
--- a/pkg/rpm_pfg.bzl
+++ b/pkg/rpm_pfg.bzl
@@ -33,7 +33,7 @@
"PackageSymlinkInfo",
"PackageVariablesInfo",
)
-load("//pkg/private:util.bzl", "setup_output_files", "substitute_package_variables")
+load("//pkg/private:util.bzl", "get_stamp_detect", "setup_output_files", "substitute_package_variables")
load(
"//toolchains/rpm:rpmbuild_configure.bzl",
"DEBUGINFO_TYPE_FEDORA",
@@ -343,7 +343,7 @@
rpm_ctx,
)
-def _process_subrpm(ctx, rpm_name, rpm_info, rpm_ctx, debuginfo_type):
+def _process_subrpm(ctx, rpm_name, rpm_info, rpm_ctx, debuginfo_type, effective_release = None):
sub_rpm_ctx = struct(
dest_check_map = {},
install_script_pieces = [],
@@ -424,7 +424,7 @@
version = rpm_info.version or ctx.attr.version,
architecture = rpm_info.architecture or ctx.attr.architecture,
package_name = rpm_info.package_name,
- release = ctx.attr.release,
+ release = effective_release,
)
default_file = ctx.actions.declare_file("{}-{}.rpm".format(rpm_name, rpm_info.package_name))
@@ -479,6 +479,7 @@
debuginfo_type = DEBUGINFO_TYPE_NONE
name = ctx.attr.package_name if ctx.attr.package_name else ctx.label.name
rpm_ctx.make_rpm_args.append("--name=" + name)
+ _stamp_active = ctx.attr.stamp == 1 or (ctx.attr.stamp == -1 and ctx.attr.private_stamp_detect)
if ctx.attr.debug:
rpm_ctx.make_rpm_args.append("--debug")
@@ -516,13 +517,15 @@
default_file = ctx.actions.declare_file("{}.rpm".format(rpm_name))
+ # When stamp is active, don't embed template placeholders in the filename.
+ effective_release = ctx.attr.release if (ctx.attr.release and not _stamp_active) else None
package_file_name = ctx.attr.package_file_name
if not package_file_name:
package_file_name = _make_rpm_filename(
rpm_name,
ctx.attr.version,
ctx.attr.architecture,
- release = ctx.attr.release,
+ release = effective_release,
)
#### rpm spec "preamble"
@@ -552,10 +555,21 @@
rpm_ctx.make_rpm_args.append("--release=@" + ctx.file.release_file.path)
files.append(ctx.file.release_file)
elif ctx.attr.release:
- preamble_pieces.append("Release: " + ctx.attr.release)
+ if _stamp_active:
+ # Route through make_rpm.py so stamp vars can be substituted
+ preamble_pieces.append("Release: ${{RELEASE_FROM_FILE}}")
+ rpm_ctx.make_rpm_args.append("--release=" + ctx.attr.release)
+ else:
+ preamble_pieces.append("Release: " + ctx.attr.release)
else:
fail("None of the release or release_file attributes were specified")
+ if _stamp_active:
+ rpm_ctx.make_rpm_args.append("--volatile_status_file=" + ctx.version_file.path)
+ files.append(ctx.version_file)
+ rpm_ctx.make_rpm_args.append("--stable_status_file=" + ctx.info_file.path)
+ files.append(ctx.info_file)
+
# source_date_epoch is an integer, and Bazel (as of 4.2.2) does not allow
# you to put "None" as the default for an "int" attribute. See also
# https://github.com/bazelbuild/bazel/issues/14434.
@@ -758,6 +772,7 @@
s[PackageSubRPMInfo],
rpm_ctx,
debuginfo_type,
+ effective_release = effective_release,
))
subrpm_file = ctx.actions.declare_file(
@@ -779,7 +794,7 @@
ctx.attr.version,
ctx.attr.architecture,
package_name = "debuginfo",
- release = ctx.attr.release,
+ release = effective_release,
)
_, debuginfo_output_file, _ = setup_output_files(
@@ -916,7 +931,7 @@
]
# Define the rule.
-pkg_rpm = rule(
+pkg_rpm_impl = rule(
doc = """Creates an RPM format package via `pkg_filegroup` and friends.
The uses the outputs of the rules in `mappings.bzl` to construct arbitrary
@@ -994,6 +1009,14 @@
doc = """RPM "Release" tag
Exactly one of `release` or `release_file` must be provided.
+
+ When `stamp` is enabled, workspace status variable placeholders
+ of the form `{VARIABLE_NAME}` will be substituted at build time
+ using values from the stable and volatile status files. For
+ example, setting `release = "0.{BUILD_TIMESTAMP}"` with
+ `stamp = 1` will embed the build timestamp in the release tag.
+ See https://bazel.build/docs/user-manual#workspace-status for
+ details on workspace status variables.
""",
),
"release_file": attr.label(
@@ -1293,6 +1316,18 @@
doc = """Extra files that are needed by rpmbuild or find-debuginfo""",
allow_files = True,
),
+ "stamp": attr.int(
+ doc = """Enable stamping for volatile release values. Possible values:
+<li>stamp = 1: Substitute workspace status variables in the release tag.
+<li>stamp = 0: No substitution; release tag used as-is.
+<li>stamp = -1: Controlled by the --[no]stamp flag.
+""",
+ values = [-1, 0, 1],
+ default = 0,
+ ),
+ # Is --stamp set on the command line?
+ # TODO(https://github.com/bazelbuild/rules_pkg/issues/340): Remove this.
+ "private_stamp_detect": attr.bool(default = False),
# Implicit dependencies.
"_make_rpm": attr.label(
default = Label("//pkg:make_rpm"),
@@ -1312,6 +1347,17 @@
toolchains = ["@rules_pkg//toolchains/rpm:rpmbuild_toolchain_type"],
)
+def pkg_rpm(name, **kwargs):
+ """Creates an RPM format package via `pkg_filegroup` and friends.
+
+ @wraps(pkg_rpm_impl)
+ """
+ pkg_rpm_impl(
+ name = name,
+ private_stamp_detect = get_stamp_detect(kwargs.get("stamp", 0)),
+ **kwargs
+ )
+
def _pkg_sub_rpm_impl(ctx):
mapped_files_depsets = []