diff --git a/examples/time_stamping/BUILD b/examples/time_stamping/BUILD new file mode 100644 index 00000000..57d5023a --- /dev/null +++ b/examples/time_stamping/BUILD @@ -0,0 +1,41 @@ +# Copyright 2021 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. +# -*- coding: utf-8 -*- + +licenses(["notice"]) + +load("@rules_pkg//:pkg.bzl", "pkg_tar") + +pkg_tar( + name = "never_stamped", + srcs = [ + ":BUILD", + ], +) + +pkg_tar( + name = "always_stamped", + srcs = [ + ":BUILD", + ], + stamp = 1, +) + +pkg_tar( + name = "controlled_by_stamp_option", + srcs = [ + ":BUILD", + ], + stamp = -1, +) diff --git a/examples/time_stamping/WORKSPACE b/examples/time_stamping/WORKSPACE new file mode 100644 index 00000000..acdadab4 --- /dev/null +++ b/examples/time_stamping/WORKSPACE @@ -0,0 +1,26 @@ +# Copyright 2021 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. + +workspace(name = "rules_pkg_examples") + +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +local_repository( + name = "rules_pkg", + path = "../../pkg", +) + +load("@rules_pkg//:deps.bzl", "rules_pkg_dependencies") + +rules_pkg_dependencies() diff --git a/examples/time_stamping/readme.md b/examples/time_stamping/readme.md new file mode 100644 index 00000000..8743ba92 --- /dev/null +++ b/examples/time_stamping/readme.md @@ -0,0 +1,52 @@ +# Examples of how time stamping works. + +## How it works + +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. + +## Try this + +``` +bazel build :* +for tarball in bazel-bin/*.tar ; do + echo ==== $tarball + tar tvf $tarball +done + +bazel build :* --stamp=1 +for tarball in bazel-bin/*.tar ; do + echo ==== $tarball + tar tvf $tarball +done +``` + +You should see something like: +``` +INFO: Build completed successfully, 3 total actions +==== bazel-bin/always_stamped.tar +drwxr-xr-x 0 0 0 0 May 3 17:34 ./ +-r-xr-xr-x 0 0 0 968 May 3 17:34 ./BUILD +==== bazel-bin/controlled_by_stamp_option.tar +drwxr-xr-x 0 0 0 0 Dec 31 1999 ./ +-r-xr-xr-x 0 0 0 968 Dec 31 1999 ./BUILD +==== bazel-bin/never_stamped.tar +drwxr-xr-x 0 0 0 0 Dec 31 1999 ./ +-r-xr-xr-x 0 0 0 968 Dec 31 1999 ./BUILD +INFO: Build option --stamp has changed, discarding analysis cache. +INFO: Build completed successfully, 3 total actions +==== bazel-bin/always_stamped.tar +drwxr-xr-x 0 0 0 0 May 3 17:34 ./ +-r-xr-xr-x 0 0 0 968 May 3 17:34 ./BUILD +==== bazel-bin/controlled_by_stamp_option.tar +drwxr-xr-x 0 0 0 0 May 6 17:42 ./ +-r-xr-xr-x 0 0 0 968 May 6 17:42 ./BUILD +==== bazel-bin/never_stamped.tar +drwxr-xr-x 0 0 0 0 Dec 31 1999 ./ +-r-xr-xr-x 0 0 0 968 Dec 31 1999 ./BUILD +``` diff --git a/pkg/BUILD b/pkg/BUILD index 532df9f4..79be251a 100644 --- a/pkg/BUILD +++ b/pkg/BUILD @@ -20,7 +20,6 @@ licenses(["notice"]) exports_files( glob([ "*.bzl", - "private/**", ]), visibility = ["//visibility:public"], ) @@ -88,6 +87,7 @@ py_binary( deps = [ ":archive", ":helpers", + "//private:build_info", ], ) @@ -148,6 +148,6 @@ py_library( py_binary( name = "filter_directory", srcs = ["filter_directory.py"], - visibility = ["//visibility:public"], python_version = "PY3", + visibility = ["//visibility:public"], ) diff --git a/pkg/build_tar.py b/pkg/build_tar.py index f4117748..78f41f44 100644 --- a/pkg/build_tar.py +++ b/pkg/build_tar.py @@ -21,6 +21,7 @@ import archive import helpers +from private import build_info class TarFile(object): @@ -264,6 +265,8 @@ def main(): 'path/to/file=root.root.') parser.add_argument('--root_directory', default='./', help='Default root directory is named "."') + parser.add_argument('--stamp_from', default='', + help='File to find BUILD_STAMP in') options = parser.parse_args() # Parse modes arguments @@ -303,11 +306,15 @@ def main(): f = f[1:] ids_map[f] = (int(user), int(group)) + default_mtime = options.mtime + if options.stamp_from: + default_mtime = build_info.get_timestamp(options.stamp_from) + # Add objects to the tar file with TarFile( options.output, helpers.GetFlagValue(options.directory), options.compression, options.compressor, options.root_directory, - options.mtime) as output: + default_mtime=default_mtime) as output: def file_attributes(filename): if filename.startswith('/'): diff --git a/pkg/distro/BUILD b/pkg/distro/BUILD index d5bccce8..7b3d861e 100644 --- a/pkg/distro/BUILD +++ b/pkg/distro/BUILD @@ -36,6 +36,7 @@ pkg_tar( srcs = [ ":small_workspace", "//:standard_package", + "//private:standard_package", "//releasing:standard_package", "//toolchains:standard_package", ], @@ -97,7 +98,7 @@ genrule( bzl_library( name = "rules_pkg_lib", srcs = [ - "//:private/util.bzl", + "//private:util.bzl", "//:package_variables.bzl", "//:path.bzl", "//:pkg.bzl", diff --git a/pkg/pkg.bzl b/pkg/pkg.bzl index d9fe7103..b835b86e 100644 --- a/pkg/pkg.bzl +++ b/pkg/pkg.bzl @@ -15,7 +15,7 @@ load(":path.bzl", "compute_data_path", "dest_path") load(":providers.bzl", "PackageArtifactInfo", "PackageVariablesInfo") -load("private/util.bzl", "setup_output_files", "substitute_package_variables") +load("//private:util.bzl", "setup_output_files", "substitute_package_variables") # TODO(aiuto): Figure out how to get this from the python toolchain. # See check for lzma in archive.py for a hint at a method. @@ -30,6 +30,7 @@ SUPPORTED_TAR_COMPRESSIONS = ( ) deb_filetype = [".deb", ".udeb"] _DEFAULT_MTIME = -1 +_stamp_condition = str(Label("//private:private_stamp_detect")) def _remap(remap_paths, path): """If path starts with a key in remap_paths, rewrite it.""" @@ -151,6 +152,10 @@ def _pkg_tar_impl(ctx): "--link=%s:%s" % (_quote(k, protect = ":"), ctx.attr.symlinks[k]) for k in ctx.attr.symlinks ] + if ctx.attr.stamp == 1 or (ctx.attr.stamp == -1 and + ctx.attr.private_stamp_detect): + args.append("--stamp_from=%s" % ctx.version_file.path) + files.append(ctx.version_file) arg_file = ctx.actions.declare_file(ctx.label.name + ".args") files.append(arg_file) ctx.actions.write(arg_file, "\n".join(args)) @@ -337,6 +342,7 @@ def _pkg_deb_impl(ctx): ), ] + # A rule for creating a tar file, see README.md pkg_tar_impl = rule( implementation = _pkg_tar_impl, @@ -372,6 +378,10 @@ pkg_tar_impl = rule( doc = "See Common Attributes", providers = [PackageVariablesInfo], ), + "stamp": attr.int(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. "build_tar": attr.label( @@ -413,6 +423,10 @@ def pkg_tar(name, **kwargs): pkg_tar_impl( name = name, out = kwargs.pop("out", None) or (name + "." + extension), + private_stamp_detect = select({ + _stamp_condition: True, + "//conditions:default": False, + }), **kwargs ) diff --git a/pkg/private/BUILD b/pkg/private/BUILD new file mode 100644 index 00000000..38ca4581 --- /dev/null +++ b/pkg/private/BUILD @@ -0,0 +1,55 @@ +# Copyright 2021 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. +"""rules_pkg internal code. + +This is subject to change at any time. +""" + +load("@rules_python//python:defs.bzl", "py_library") + +licenses(["notice"]) + +filegroup( + name = "standard_package", + srcs = glob([ + "BUILD", + "*.bzl", + "*.py", + ]), + visibility = ["//distro:__pkg__"], +) + +exports_files( + glob([ + "*.bzl", + ]), + visibility = ["//distro:__pkg__"], +) + +config_setting( + name = "private_stamp_detect", + values = {"stamp": "1"}, +) + +py_library( + name = "build_info", + srcs = [ + "build_info.py", + ], + srcs_version = "PY3", + visibility = [ + "//:__pkg__", + "//tests:__pkg__", + ], +) diff --git a/pkg/private/build_info.py b/pkg/private/build_info.py new file mode 100644 index 00000000..6780623e --- /dev/null +++ b/pkg/private/build_info.py @@ -0,0 +1,37 @@ +# Copyright 2021 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. +"""Get BUILD_TIMESTAMP.""" + + +def get_timestamp(volatile_status_file): + """Get BUILD_TIMESTAMP as an integer. + + Reads a file of "namevalue" pairs and returns the value + of the BUILD_TIMESTAMP. The file should be in the workspace status + format: https://docs.bazel.build/versions/master/user-manual.html#workspace_status + + Args: + volatile_status_file: path to input file. Typically ctx.version_file.path. + Returns: + int: value of BUILD_TIMESTAMP + 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]) + raise Exception( + "Invalid status file <%s>. Expected to find BUILD_TIMESTAMP" % volatile_status_file) diff --git a/pkg/private/util.bzl b/pkg/private/util.bzl index b283c839..db46e45a 100644 --- a/pkg/private/util.bzl +++ b/pkg/private/util.bzl @@ -13,7 +13,7 @@ # limitations under the License. """Internal utilities for rules_pkg.""" -load(":providers.bzl", "PackageVariablesInfo") +load("//:providers.bzl", "PackageVariablesInfo") def setup_output_files(ctx, package_file_name = None, default_output_file = None): """Provide output file metadata for common packaging rules diff --git a/pkg/rpm_pfg.bzl b/pkg/rpm_pfg.bzl index c754272b..8ae46cfa 100644 --- a/pkg/rpm_pfg.bzl +++ b/pkg/rpm_pfg.bzl @@ -25,7 +25,7 @@ find_system_rpmbuild(name="rules_pkg_rpmbuild") ``` """ -load("//:private/util.bzl", "setup_output_files") +load("//private:util.bzl", "setup_output_files") load("//:providers.bzl", "PackageArtifactInfo", "PackageFilegroupInfo", "PackageVariablesInfo") rpm_filetype = [".rpm"] diff --git a/pkg/tests/BUILD b/pkg/tests/BUILD index 8bb777af..82128780 100644 --- a/pkg/tests/BUILD +++ b/pkg/tests/BUILD @@ -575,3 +575,22 @@ py_test( "@bazel_tools//tools/python/runfiles", ], ) + +pkg_tar( + name = "stamped_tar", + srcs = ["BUILD"], + stamp = 1, +) + +py_test( + name = "stamp_test", + srcs = [ + "stamp_test.py", + ], + data = [ + "stamped_tar.tar", + ], + deps = [ + "@bazel_tools//tools/python/runfiles", + ], +) diff --git a/pkg/tests/stamp_test.py b/pkg/tests/stamp_test.py new file mode 100644 index 00000000..0f076927 --- /dev/null +++ b/pkg/tests/stamp_test.py @@ -0,0 +1,59 @@ +# Copyright 2021 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. +"""Test time stamping in pkg_tar""" + +import tarfile +import time +import unittest + +from bazel_tools.tools.python.runfiles import runfiles + +# keep in sync with archive.py +PORTABLE_MTIME = 946684800 # 2000-01-01 00:00:00.000 UTC + +class PkgTarTest(unittest.TestCase): + """Testing for pkg_tar rule.""" + + def assertTarFilesAreAlmostNew(self, file_name): + """Assert that tarfile contains files with an mtime of roughly now. + + This is used to prove that the test data was a file which was presumably: + built with 'stamp=1' or ('stamp=-1' and --stamp) contains files which + all have a fairly recent mtime, thus indicating they are "current" time + rather than the epoch or some other time. + + Args: + file_name: the path to the TAR file to test. + """ + file_path = runfiles.Create().Rlocation('rules_pkg/tests/' + file_name) + target_mtime = int(time.time()) + with tarfile.open(file_path, 'r:*') as f: + i = 0 + for info in f: + if info.mtime == PORTABLE_MTIME: + self.fail('Archive %s contains file %s with portable mtime' % ( + file_path, info.name)) + if ((info.mtime < target_mtime - 10000) + or (info.mtime > target_mtime + 10000)): + self.fail( + 'Archive %s contains file %s with mtime:%d, expected:%d' % ( + file_path, info.name, info.mtime, target_mtime)) + + + def test_not_epoch_times(self): + self.assertTarFilesAreAlmostNew('stamped_tar.tar') + + +if __name__ == '__main__': + unittest.main()