From ede2edd8acabe9c8c8a25b0cbdd3b6f3c6a61098 Mon Sep 17 00:00:00 2001 From: Onno Steenbergen Date: Mon, 28 Sep 2026 12:09:36 +0200 Subject: [PATCH 1/3] Use explicit __init__.py files instead of implicit creation rules_python now warns for every executable target that relies on implicit __init__.py creation (bazel-contrib/rules_python#2945), so depending on rules_pkg prints that warning for build_tar, build_zip, make_deb, and for targets created by pkg_install and verify_archive_test. Add the missing __init__.py files (pkg/private/tar, pkg/private/deb), put every __init__.py in the srcs of a py_library (//pkg:init, //pkg/private:init) that the importing libraries depend on, and set legacy_create_init = 0 on all py_binary and py_test targets under pkg/. The per-target attribute is used rather than the module-wide explicit_init_py setting because the latter requires rules_python 2.3. --- pkg/BUILD | 15 +++++++++++++++ pkg/install.bzl | 1 + pkg/private/BUILD | 27 +++++++++++++++----------- pkg/private/deb/BUILD | 11 +++++++++-- pkg/private/deb/__init__.py | 0 pkg/private/tar/BUILD | 3 +++ pkg/private/tar/__init__.py | 0 pkg/private/zip/BUILD | 1 + pkg/releasing/BUILD | 4 ++++ pkg/rpm/BUILD | 1 + pkg/verify_archive.bzl | 1 + tests/BUILD | 12 ++++++++++++ tests/explicit_init_test.py | 38 +++++++++++++++++++++++++++++++++++++ 13 files changed, 101 insertions(+), 13 deletions(-) create mode 100644 pkg/private/deb/__init__.py create mode 100644 pkg/private/tar/__init__.py create mode 100644 tests/explicit_init_test.py diff --git a/pkg/BUILD b/pkg/BUILD index 58ac8771..36d35d01 100644 --- a/pkg/BUILD +++ b/pkg/BUILD @@ -67,11 +67,22 @@ filegroup( visibility = ["//visibility:public"], ) +# The top level package marker. Everything importing from `pkg.` must depend on +# this, since we do not rely on implicit __init__.py creation. +py_library( + name = "init", + srcs = ["__init__.py"], + imports = [".."], + srcs_version = "PY3", + visibility = ["//:__subpackages__"], +) + # Used by pkg_rpm in rpm.bzl. py_binary( name = "make_rpm", srcs = ["make_rpm.py"], imports = [".."], + legacy_create_init = 0, python_version = "PY3", srcs_version = "PY3", target_compatible_with = select({ @@ -92,6 +103,7 @@ py_library( "//tests:__subpackages__", ], deps = [ + ":init", "//pkg/private:archive", "//pkg/private:build_info", "//pkg/private:helpers", @@ -101,6 +113,7 @@ py_library( py_binary( name = "filter_directory", srcs = ["filter_directory.py"], + legacy_create_init = 0, python_version = "PY3", visibility = ["//visibility:public"], ) @@ -108,8 +121,10 @@ py_binary( py_library( name = "filter_directory_lib", srcs = ["filter_directory.py"], + imports = [".."], srcs_version = "PY3", visibility = ["//visibility:public"], + deps = [":init"], ) exports_files(["verify_archive_test_main.py.tpl"]) diff --git a/pkg/install.bzl b/pkg/install.bzl index 97320f2e..39099c6e 100644 --- a/pkg/install.bzl +++ b/pkg/install.bzl @@ -204,6 +204,7 @@ def pkg_install(name, srcs, destdir = None, destdir_flag = None, **kwargs): name = name, srcs = [":" + name + "_install_script"], main = name + "_install_script.py", + legacy_create_init = 0, deps = [Label("//pkg/private:manifest"), Label("@rules_python//python/runfiles")], srcs_version = "PY3", python_version = "PY3", diff --git a/pkg/private/BUILD b/pkg/private/BUILD index 8f3ca4c6..1de882a9 100644 --- a/pkg/private/BUILD +++ b/pkg/private/BUILD @@ -60,45 +60,49 @@ config_setting( visibility = ["//visibility:public"], ) +py_library( + name = "init", + srcs = ["__init__.py"], + imports = ["../.."], + srcs_version = "PY3", + visibility = ["//:__subpackages__"], + deps = ["//pkg:init"], +) + py_library( name = "build_info", - srcs = [ - "build_info.py", - ], + srcs = ["build_info.py"], imports = ["../.."], srcs_version = "PY3", visibility = [ "//:__subpackages__", "//tests:__pkg__", ], + deps = [":init"], ) py_library( name = "archive", - srcs = [ - "__init__.py", - "archive.py", - ], + srcs = ["archive.py"], imports = ["../.."], srcs_version = "PY3", visibility = [ "//:__subpackages__", "//tests:__subpackages__", ], + deps = [":init"], ) py_library( name = "helpers", - srcs = [ - "__init__.py", - "helpers.py", - ], + srcs = ["helpers.py"], imports = ["../.."], srcs_version = "PY3", visibility = [ "//:__subpackages__", "//tests:__pkg__", ], + deps = [":init"], ) py_library( @@ -107,4 +111,5 @@ py_library( imports = ["../.."], srcs_version = "PY3", visibility = ["//visibility:public"], + deps = [":init"], ) diff --git a/pkg/private/deb/BUILD b/pkg/private/deb/BUILD index 646877b1..0f176758 100644 --- a/pkg/private/deb/BUILD +++ b/pkg/private/deb/BUILD @@ -49,6 +49,7 @@ py_binary( name = "make_deb", srcs = ["make_deb.py"], imports = ["../../.."], + legacy_create_init = 0, python_version = "PY3", visibility = ["//visibility:public"], deps = [ @@ -58,10 +59,16 @@ py_binary( py_library( name = "make_deb_lib", - srcs = ["make_deb.py"], + srcs = [ + "__init__.py", + "make_deb.py", + ], imports = ["../../.."], srcs_version = "PY3", - visibility = ["//tests/deb:__pkg__"], + visibility = [ + "//tests:__pkg__", + "//tests/deb:__pkg__", + ], deps = [ "//pkg/private:helpers", ], diff --git a/pkg/private/deb/__init__.py b/pkg/private/deb/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pkg/private/tar/BUILD b/pkg/private/tar/BUILD index 68a2b437..040aa491 100644 --- a/pkg/private/tar/BUILD +++ b/pkg/private/tar/BUILD @@ -48,6 +48,7 @@ py_binary( name = "build_tar", srcs = ["build_tar.py"], imports = ["../../.."], + legacy_create_init = 0, python_version = "PY3", srcs_version = "PY3", visibility = ["//visibility:public"], @@ -63,6 +64,7 @@ py_binary( py_library( name = "tar_writer", srcs = [ + "__init__.py", "tar_writer.py", ], imports = ["../../.."], @@ -70,4 +72,5 @@ py_library( visibility = [ "//tests:__subpackages__", ], + deps = ["//pkg/private:init"], ) diff --git a/pkg/private/tar/__init__.py b/pkg/private/tar/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pkg/private/zip/BUILD b/pkg/private/zip/BUILD index 4823e90c..8fdb5a88 100644 --- a/pkg/private/zip/BUILD +++ b/pkg/private/zip/BUILD @@ -49,6 +49,7 @@ py_binary( name = "build_zip", srcs = ["build_zip.py"], imports = ["../../.."], + legacy_create_init = 0, python_version = "PY3", srcs_version = "PY3", visibility = ["//visibility:public"], diff --git a/pkg/releasing/BUILD b/pkg/releasing/BUILD index 4036c0fd..b1652743 100644 --- a/pkg/releasing/BUILD +++ b/pkg/releasing/BUILD @@ -36,6 +36,7 @@ py_library( ], imports = ["../.."], srcs_version = "PY3", + deps = ["//pkg:init"], ) py_binary( @@ -44,6 +45,7 @@ py_binary( "print_rel_notes.py", ], imports = ["../.."], + legacy_create_init = 0, python_version = "PY3", deps = [ ":release_utils", @@ -53,6 +55,7 @@ py_binary( py_test( name = "release_tools_test", srcs = ["release_tools_test.py"], + legacy_create_init = 0, python_version = "PY3", deps = [ ":release_utils", @@ -65,6 +68,7 @@ py_binary( srcs = [ "git_changelog_private.py", ], + legacy_create_init = 0, srcs_version = "PY3", # TODO(https://github.com/bazelbuild/bazel/issues/7377): Make this private. visibility = ["//visibility:public"], diff --git a/pkg/rpm/BUILD b/pkg/rpm/BUILD index a2742f42..f2e9af25 100644 --- a/pkg/rpm/BUILD +++ b/pkg/rpm/BUILD @@ -39,5 +39,6 @@ filegroup( py_binary( name = "augment_rpm_files_install", srcs = ["augment_rpm_files_install.py"], + legacy_create_init = 0, visibility = ["//visibility:public"], ) diff --git a/pkg/verify_archive.bzl b/pkg/verify_archive.bzl index 89d054bf..b594b243 100644 --- a/pkg/verify_archive.bzl +++ b/pkg/verify_archive.bzl @@ -148,6 +148,7 @@ def verify_archive_test( srcs = [":" + test_src], main = test_src, data = [target], + legacy_create_init = 0, python_version = "PY3", **{key: kwargs[key] for key in COMMON_TEST_ATTR_NAMES if key in kwargs} ) diff --git a/tests/BUILD b/tests/BUILD index 93b4626c..0b14c7b6 100644 --- a/tests/BUILD +++ b/tests/BUILD @@ -101,6 +101,18 @@ py_test( ], ) +py_test( + name = "explicit_init_test", + srcs = ["explicit_init_test.py"], + legacy_create_init = 0, + python_version = "PY3", + srcs_version = "PY3", + deps = [ + "//pkg/private/deb:make_deb_lib", + "//pkg/private/tar:tar_writer", + ], +) + py_test( name = "path_test", srcs = ["path_test.py"], diff --git a/tests/explicit_init_test.py b/tests/explicit_init_test.py new file mode 100644 index 00000000..24f4dedb --- /dev/null +++ b/tests/explicit_init_test.py @@ -0,0 +1,38 @@ +# 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. +"""Checks that our Python packages ship explicit __init__.py files. + +This test runs with legacy_create_init = 0, so any package whose +__init__.py is not in the runfiles would be imported as an implicit +namespace package, which has no __file__. +""" + +import importlib +import unittest + + +class ExplicitInitTest(unittest.TestCase): + + def test_packages_are_regular_packages(self): + for name in ("pkg", "pkg.private", "pkg.private.deb", "pkg.private.tar"): + with self.subTest(package=name): + module = importlib.import_module(name) + self.assertIsNotNone( + module.__file__, + "%s is a namespace package; add its __init__.py to srcs" % name, + ) + + +if __name__ == "__main__": + unittest.main() From 8c5b416f679ddadf74ed1f5b2d4a5f15cbdc2b6f Mon Sep 17 00:00:00 2001 From: Onno Steenbergen Date: Wed, 7 Oct 2026 09:22:33 +0200 Subject: [PATCH 2/3] Set explicit_init_py module-wide instead of legacy_create_init Drop legacy_create_init = 0 from all targets and instead enable the rules_python explicit_init_py setting for the rules_pkg module, which requires raising the minimum rules_python version to 2.3.0. The setting only applies to targets in rules_pkg itself. Targets created by pkg_install and verify_archive_test belong to the calling module, so users silence those by setting explicit_init_py in their own MODULE.bazel (or with --incompatible_default_to_explicit_init_py). --- MODULE.bazel | 7 ++++++- pkg/BUILD | 2 -- pkg/install.bzl | 1 - pkg/private/deb/BUILD | 1 - pkg/private/tar/BUILD | 1 - pkg/private/zip/BUILD | 1 - pkg/releasing/BUILD | 3 --- pkg/rpm/BUILD | 1 - pkg/verify_archive.bzl | 1 - tests/BUILD | 1 - 10 files changed, 6 insertions(+), 13 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index f47565ff..e0c78d02 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -6,9 +6,14 @@ module( # Do not update to newer versions until you need a specific new feature. bazel_dep(name = "rules_license", version = "1.0.0") -bazel_dep(name = "rules_python", version = "1.7.0") +bazel_dep(name = "rules_python", version = "2.3.0") bazel_dep(name = "bazel_skylib", version = "1.9.0") +# Do not generate __init__.py files for our py_binary and py_test targets. +# We ship them explicitly. +rules_python_config = use_extension("@rules_python//python/extensions:config.bzl", "config") +rules_python_config.explicit_init_py(default = True) + # Only for development bazel_dep(name = "platforms", version = "1.0.0", dev_dependency = True) bazel_dep(name = "rules_cc", version = "0.2.16", dev_dependency = True) diff --git a/pkg/BUILD b/pkg/BUILD index 36d35d01..83caa591 100644 --- a/pkg/BUILD +++ b/pkg/BUILD @@ -82,7 +82,6 @@ py_binary( name = "make_rpm", srcs = ["make_rpm.py"], imports = [".."], - legacy_create_init = 0, python_version = "PY3", srcs_version = "PY3", target_compatible_with = select({ @@ -113,7 +112,6 @@ py_library( py_binary( name = "filter_directory", srcs = ["filter_directory.py"], - legacy_create_init = 0, python_version = "PY3", visibility = ["//visibility:public"], ) diff --git a/pkg/install.bzl b/pkg/install.bzl index 39099c6e..97320f2e 100644 --- a/pkg/install.bzl +++ b/pkg/install.bzl @@ -204,7 +204,6 @@ def pkg_install(name, srcs, destdir = None, destdir_flag = None, **kwargs): name = name, srcs = [":" + name + "_install_script"], main = name + "_install_script.py", - legacy_create_init = 0, deps = [Label("//pkg/private:manifest"), Label("@rules_python//python/runfiles")], srcs_version = "PY3", python_version = "PY3", diff --git a/pkg/private/deb/BUILD b/pkg/private/deb/BUILD index 0f176758..5f58c6b9 100644 --- a/pkg/private/deb/BUILD +++ b/pkg/private/deb/BUILD @@ -49,7 +49,6 @@ py_binary( name = "make_deb", srcs = ["make_deb.py"], imports = ["../../.."], - legacy_create_init = 0, python_version = "PY3", visibility = ["//visibility:public"], deps = [ diff --git a/pkg/private/tar/BUILD b/pkg/private/tar/BUILD index 040aa491..31dcdf67 100644 --- a/pkg/private/tar/BUILD +++ b/pkg/private/tar/BUILD @@ -48,7 +48,6 @@ py_binary( name = "build_tar", srcs = ["build_tar.py"], imports = ["../../.."], - legacy_create_init = 0, python_version = "PY3", srcs_version = "PY3", visibility = ["//visibility:public"], diff --git a/pkg/private/zip/BUILD b/pkg/private/zip/BUILD index 8fdb5a88..4823e90c 100644 --- a/pkg/private/zip/BUILD +++ b/pkg/private/zip/BUILD @@ -49,7 +49,6 @@ py_binary( name = "build_zip", srcs = ["build_zip.py"], imports = ["../../.."], - legacy_create_init = 0, python_version = "PY3", srcs_version = "PY3", visibility = ["//visibility:public"], diff --git a/pkg/releasing/BUILD b/pkg/releasing/BUILD index b1652743..d1581bd2 100644 --- a/pkg/releasing/BUILD +++ b/pkg/releasing/BUILD @@ -45,7 +45,6 @@ py_binary( "print_rel_notes.py", ], imports = ["../.."], - legacy_create_init = 0, python_version = "PY3", deps = [ ":release_utils", @@ -55,7 +54,6 @@ py_binary( py_test( name = "release_tools_test", srcs = ["release_tools_test.py"], - legacy_create_init = 0, python_version = "PY3", deps = [ ":release_utils", @@ -68,7 +66,6 @@ py_binary( srcs = [ "git_changelog_private.py", ], - legacy_create_init = 0, srcs_version = "PY3", # TODO(https://github.com/bazelbuild/bazel/issues/7377): Make this private. visibility = ["//visibility:public"], diff --git a/pkg/rpm/BUILD b/pkg/rpm/BUILD index f2e9af25..a2742f42 100644 --- a/pkg/rpm/BUILD +++ b/pkg/rpm/BUILD @@ -39,6 +39,5 @@ filegroup( py_binary( name = "augment_rpm_files_install", srcs = ["augment_rpm_files_install.py"], - legacy_create_init = 0, visibility = ["//visibility:public"], ) diff --git a/pkg/verify_archive.bzl b/pkg/verify_archive.bzl index b594b243..89d054bf 100644 --- a/pkg/verify_archive.bzl +++ b/pkg/verify_archive.bzl @@ -148,7 +148,6 @@ def verify_archive_test( srcs = [":" + test_src], main = test_src, data = [target], - legacy_create_init = 0, python_version = "PY3", **{key: kwargs[key] for key in COMMON_TEST_ATTR_NAMES if key in kwargs} ) diff --git a/tests/BUILD b/tests/BUILD index 0b14c7b6..8b83b19e 100644 --- a/tests/BUILD +++ b/tests/BUILD @@ -104,7 +104,6 @@ py_test( py_test( name = "explicit_init_test", srcs = ["explicit_init_test.py"], - legacy_create_init = 0, python_version = "PY3", srcs_version = "PY3", deps = [ From 2b941e64baa0841d8e8596607a8ac9aeafa06094 Mon Sep 17 00:00:00 2001 From: Onno Steenbergen Date: Wed, 7 Oct 2026 09:34:10 +0200 Subject: [PATCH 3/3] Follow Windows runfiles junctions when opening runfiles Since rules_python 1.9, py_binary/py_test default to --enable_runfiles on Windows. Bazel may then create a directory junction for a file runfile, which can not be opened or executed ("Permission denied"). Raising the minimum rules_python to 2.3.0 exposed this in our Windows tests and in verify_archive_test. os.path.realpath does not help: on Windows it fails on such a junction with WinError 267. Read the junction target with os.readlink instead, in verify_archive_test, in pkg_install (replacing the realpath from #1076) and, through //tests/util:runfiles_util, in the tests. --- pkg/private/install.py.tpl | 24 ++++++++++++++--- pkg/verify_archive_test_main.py.tpl | 23 +++++++++++++++- tests/BUILD | 2 ++ tests/archive_test.py | 6 +++-- tests/deb/BUILD | 2 ++ tests/deb/pkg_deb_test.py | 8 +++--- tests/deb/pkg_deb_variables_test.py | 3 ++- tests/install/BUILD | 1 + tests/install/test.py | 17 +++++++----- tests/mappings/BUILD | 1 + tests/mappings/manifest_test_lib.py | 4 ++- tests/stamp_test.py | 7 +++-- tests/tar/BUILD | 3 +++ tests/tar/is_compressed_test.py | 3 ++- tests/tar/pkg_tar_test.py | 13 ++++++--- tests/tar/tar_writer_test.py | 24 ++++++++++------- tests/util/BUILD | 10 ++++++- tests/util/runfiles_util.py | 42 +++++++++++++++++++++++++++++ tests/zip/BUILD | 1 + tests/zip/zip_test_lib.py | 4 ++- 20 files changed, 161 insertions(+), 37 deletions(-) create mode 100644 tests/util/runfiles_util.py diff --git a/pkg/private/install.py.tpl b/pkg/private/install.py.tpl index 4c66caa4..949b94f5 100644 --- a/pkg/private/install.py.tpl +++ b/pkg/private/install.py.tpl @@ -45,6 +45,25 @@ def locate(short_path, repository): return RUNFILES.Rlocation(posixpath.normpath(posixpath.join(repository, short_path))) +def _resolve_runfile(path): + """Return the real file behind a runfile, so that it can be opened. + + With --enable_runfiles on Windows, Bazel may create a directory junction + for a file runfile. Such a junction can not be opened, and + os.path.realpath fails on it, so we read the junction target instead. + """ + if os.name != "nt": + return os.path.realpath(path) + try: + target = os.readlink(path) + except OSError: + # Not a link. + return path + if target.startswith("\\\\?\\"): + target = target[4:] + return os.path.join(os.path.dirname(path), target) + + # This is named "NativeInstaller" because it makes use of "native" python # functionality for installing files that should be cross-platform. # @@ -81,10 +100,7 @@ class NativeInstaller(object): def _do_file_copy(self, src, dest): logging.debug("COPY %s <- %s", dest, src) - # Windows --enable_runfiles uses directory junctions for every runfile. - # Junctions cannot point at files, so open() fails with Permission denied. - # Follow the reparse point to the real bazel-out file. - src = os.path.realpath(src) + src = _resolve_runfile(src) # Copy to a temporary directory and then move it to the destination. # This ensures code-signed executables on certain platforms # behave correctly. diff --git a/pkg/verify_archive_test_main.py.tpl b/pkg/verify_archive_test_main.py.tpl index 03338d45..baa1e068 100644 --- a/pkg/verify_archive_test_main.py.tpl +++ b/pkg/verify_archive_test_main.py.tpl @@ -13,10 +13,31 @@ # limitations under the License. """Tests for generated content manifest.""" +import os import re import tarfile import unittest + +def _resolve_junction(path): + """Returns the target of path if it is a Windows junction, else path. + + With --enable_runfiles on Windows, Bazel may create a directory junction + for a file runfile. Such a junction can not be opened, and + os.path.realpath fails on it, so we read the junction target instead. + """ + if os.name != 'nt': + return path + try: + target = os.readlink(path) + except OSError: + # Not a link. + return path + if target.startswith('\\\\?\\'): + target = target[4:] + return os.path.join(os.path.dirname(path), target) + + class VerifyArchiveTest(unittest.TestCase): """Test harness to see if we wrote the content manifest correctly.""" @@ -37,7 +58,7 @@ class VerifyArchiveTest(unittest.TestCase): def load_tar(self, path): self.paths = [] self.links = {} - with tarfile.open(path, 'r:*') as f: + with tarfile.open(_resolve_junction(path), 'r:*') as f: i = 0 for info in f: self.paths.append(info.name) diff --git a/tests/BUILD b/tests/BUILD index 8b83b19e..5c74176f 100644 --- a/tests/BUILD +++ b/tests/BUILD @@ -97,6 +97,7 @@ py_test( srcs_version = "PY3", deps = [ "//pkg/private:archive", + "//tests/util:runfiles_util", "@rules_python//python/runfiles", ], ) @@ -227,6 +228,7 @@ py_test( ], python_version = "PY3", deps = [ + "//tests/util:runfiles_util", "@rules_python//python/runfiles", ], ) diff --git a/tests/archive_test.py b/tests/archive_test.py index 4c604c6f..fe387422 100644 --- a/tests/archive_test.py +++ b/tests/archive_test.py @@ -16,6 +16,7 @@ import unittest from python.runfiles import runfiles +from tests.util import runfiles_util from pkg.private import archive @@ -62,11 +63,12 @@ def assertArFileContent(self, arfile, content): def testEmptyArFile(self): self.assertArFileContent( - self.data_files.Rlocation("rules_pkg/tests/testdata/empty.ar"), + runfiles_util.rlocation( + self.data_files, "rules_pkg/tests/testdata/empty.ar"), []) def assertSimpleFileContent(self, names): - datafile = self.data_files.Rlocation( + datafile = runfiles_util.rlocation(self.data_files, "rules_pkg/tests/testdata/" + "_".join(names) + ".ar") # pylint: disable=g-complex-comprehension content = [{"filename": n, diff --git a/tests/deb/BUILD b/tests/deb/BUILD index 305d3860..ca2fe82a 100644 --- a/tests/deb/BUILD +++ b/tests/deb/BUILD @@ -126,6 +126,7 @@ py_test( python_version = "PY3", deps = [ "//pkg/private:archive", + "//tests/util:runfiles_util", "@rules_python//python/runfiles", ], ) @@ -204,6 +205,7 @@ py_test( python_version = "PY3", deps = [ "//pkg/private:archive", + "//tests/util:runfiles_util", "@rules_python//python/runfiles", ], ) diff --git a/tests/deb/pkg_deb_test.py b/tests/deb/pkg_deb_test.py index 120ada1a..3edaf0bc 100644 --- a/tests/deb/pkg_deb_test.py +++ b/tests/deb/pkg_deb_test.py @@ -23,6 +23,7 @@ import unittest from python.runfiles import runfiles +from tests.util import runfiles_util from pkg.private import archive @@ -63,9 +64,10 @@ def setUp(self): super(PkgDebTest, self).setUp() self.runfiles = runfiles.Create() # Note: Rlocation requires forward slashes. os.path.join() will not work. - self.deb_path = self.runfiles.Rlocation('rules_pkg/tests/deb/fizzbuzz_4.5.6_all.deb') + self.deb_path = runfiles_util.rlocation( + self.runfiles, 'rules_pkg/tests/deb/fizzbuzz_4.5.6_all.deb') self.deb_file = DebInspect(self.deb_path) - self.installed_size_file_deb_path = self.runfiles.Rlocation( + self.installed_size_file_deb_path = runfiles_util.rlocation(self.runfiles, 'rules_pkg/tests/deb/fizzbuzz-installed-size-file_1.0.0_all.deb') self.installed_size_file_deb_file = DebInspect(self.installed_size_file_deb_path) @@ -235,7 +237,7 @@ def test_triggers(self): 'some-trigger\n') def test_changes(self): - changes_path = self.runfiles.Rlocation( + changes_path = runfiles_util.rlocation(self.runfiles, 'rules_pkg/tests/deb/fizzbuzz_4.5.6_all.changes') with open(changes_path, 'r', encoding='utf-8') as f: content = f.read() diff --git a/tests/deb/pkg_deb_variables_test.py b/tests/deb/pkg_deb_variables_test.py index f014d2a2..c4904648 100644 --- a/tests/deb/pkg_deb_variables_test.py +++ b/tests/deb/pkg_deb_variables_test.py @@ -21,6 +21,7 @@ import unittest from python.runfiles import runfiles +from tests.util import runfiles_util from pkg.private import archive @@ -62,7 +63,7 @@ def setUp(self): # my_package_variables provides arch=target_arch, label="some_value", so: # package = "pkg-$(label)" -> "pkg-some_value" # architecture = "$(arch)" -> "target_arch" - deb_path = self.runfiles.Rlocation( + deb_path = runfiles_util.rlocation(self.runfiles, "rules_pkg/tests/deb/pkg-some_value_1.0_target_arch.deb" ) self.deb_file = DebInspect(deb_path) diff --git a/tests/install/BUILD b/tests/install/BUILD index 81019397..83cb554a 100644 --- a/tests/install/BUILD +++ b/tests/install/BUILD @@ -40,6 +40,7 @@ py_test( ], deps = [ "//pkg/private:manifest", + "//tests/util:runfiles_util", "@rules_python//python/runfiles", ], ) diff --git a/tests/install/test.py b/tests/install/test.py index e6e35a6a..207e5e6a 100644 --- a/tests/install/test.py +++ b/tests/install/test.py @@ -23,16 +23,21 @@ from pkg.private import manifest from python.runfiles import runfiles +from tests.util import runfiles_util class PkgInstallTestBase(unittest.TestCase): _extension = ".exe" if os.name == "nt" else "" + @classmethod + def rlocation(cls, path): + return runfiles_util.rlocation(cls.runfiles, path) + @classmethod def setUpClass(cls): cls.runfiles = runfiles.Create() # Somewhat of an implementation detail, but it works. I think. - manifest_file = cls.runfiles.Rlocation("rules_pkg/tests/install/test_installer_install_script-install-manifest.json") + manifest_file = cls.rlocation("rules_pkg/tests/install/test_installer_install_script-install-manifest.json") cls.manifest_data = {pathlib.Path(e.dest): e for e in manifest.read_entries_from(manifest_file)} cls.installdir = pathlib.Path(os.getenv("TEST_TMPDIR")) / "installdir" @@ -44,7 +49,7 @@ def setUpClass(cls): env = {} env.update(cls.runfiles.EnvVars()) subprocess.check_call([ - cls.runfiles.Rlocation(f"rules_pkg/tests/install/test_installer{cls._extension}"), + cls.rlocation(f"rules_pkg/tests/install/test_installer{cls._extension}"), "--destdir", cls.installdir, "--verbose", ], @@ -212,7 +217,7 @@ class DestdirFlagTest(unittest.TestCase): def setUpClass(cls): super().setUpClass() r = runfiles.Create() - cls.script_path = r.Rlocation( + cls.script_path = runfiles_util.rlocation(r, f"rules_pkg/tests/install/test_installer_flag_install_script.py" ) @@ -231,7 +236,7 @@ def test_wipe(self): (self.installdir / "should_be_deleted.txt").touch() subprocess.check_call([ - self.runfiles.Rlocation(f"rules_pkg/tests/install/test_installer{self._extension}"), + self.rlocation(f"rules_pkg/tests/install/test_installer{self._extension}"), "--destdir", self.installdir, "--wipe_destdir", ], @@ -249,7 +254,7 @@ def test_overwrite_existing_symlink(self): if not link.is_symlink(): link.symlink_to("old_target") subprocess.check_call([ - self.runfiles.Rlocation(f"rules_pkg/tests/install/test_installer{self._extension}"), + self.rlocation(f"rules_pkg/tests/install/test_installer{self._extension}"), "--destdir", self.installdir, ], env=self.runfiles.EnvVars()) @@ -275,7 +280,7 @@ def test_external_repo_installs_file_from_main_repo(self): destdir = test_tmpdir / f"cross_repo_{case}" subprocess.check_call( [ - runfiles_.Rlocation( + runfiles_util.rlocation(runfiles_, f"mappings_test_external_repo/pkg/install_cross_repo{PkgInstallTestBase._extension}" ), f"--destdir={destdir}", diff --git a/tests/mappings/BUILD b/tests/mappings/BUILD index 8d6b7b60..5ba42227 100644 --- a/tests/mappings/BUILD +++ b/tests/mappings/BUILD @@ -37,6 +37,7 @@ py_library( imports = ["../.."], srcs_version = "PY3", deps = [ + "//tests/util:runfiles_util", "@rules_python//python/runfiles", ], ) diff --git a/tests/mappings/manifest_test_lib.py b/tests/mappings/manifest_test_lib.py index 7e6c7b79..4fa1042b 100644 --- a/tests/mappings/manifest_test_lib.py +++ b/tests/mappings/manifest_test_lib.py @@ -17,6 +17,7 @@ import unittest from python.runfiles import runfiles +from tests.util import runfiles_util class ContentManifestTest(unittest.TestCase): """Test harness to see if we wrote the content manifest correctly.""" @@ -25,7 +26,8 @@ class ContentManifestTest(unittest.TestCase): @classmethod def _read_manifest(cls, path, to_string): - with open(cls.run_files.Rlocation('rules_pkg/' + path), 'rb') as f: + path = runfiles_util.rlocation(cls.run_files, 'rules_pkg/' + path) + with open(path, 'rb') as f: raw = f.read() return {x['dest']: x for x in json.loads(to_string(raw))} diff --git a/tests/stamp_test.py b/tests/stamp_test.py index 3709a1b1..bca24717 100644 --- a/tests/stamp_test.py +++ b/tests/stamp_test.py @@ -20,6 +20,7 @@ import zipfile from python.runfiles import runfiles +from tests.util import runfiles_util # keep in sync with archive.py PORTABLE_MTIME = 946684800 # 2000-01-01 00:00:00.000 UTC @@ -79,7 +80,8 @@ def assertTarFilesAreAlmostNew(self, file_name): Args: file_name: the path to the TAR file to test. """ - file_path = runfiles.Create().Rlocation('rules_pkg/tests/' + file_name) + file_path = runfiles_util.rlocation( + runfiles.Create(), 'rules_pkg/tests/' + file_name) with tarfile.open(file_path, 'r:*') as f: for info in f: self.check_mtime(info.mtime, file_path, info.name) @@ -95,7 +97,8 @@ def assertZipFilesAreAlmostNew(self, file_name): Args: file_name: the path to the ZIP file to test. """ - file_path = runfiles.Create().Rlocation('rules_pkg/tests/' + file_name) + file_path = runfiles_util.rlocation( + runfiles.Create(), 'rules_pkg/tests/' + file_name) target_mtime = int(time.time()) with zipfile.ZipFile(file_path, mode='r') as f: for info in f.infolist(): diff --git a/tests/tar/BUILD b/tests/tar/BUILD index ec020bbb..99777c3c 100644 --- a/tests/tar/BUILD +++ b/tests/tar/BUILD @@ -53,6 +53,7 @@ py_test( srcs_version = "PY3", deps = [ "//pkg/private/tar:tar_writer", + "//tests/util:runfiles_util", "@rules_python//python/runfiles", ], ) @@ -527,6 +528,7 @@ py_test( python_version = "PY3", deps = [ "//pkg/private/tar:tar_writer", + "//tests/util:runfiles_util", "@rules_python//python/runfiles", ], ) @@ -610,6 +612,7 @@ py_test( python_version = "PY3", srcs_version = "PY3", deps = [ + "//tests/util:runfiles_util", "@rules_python//python/runfiles", ], ) diff --git a/tests/tar/is_compressed_test.py b/tests/tar/is_compressed_test.py index 5e55a06f..5dac1507 100644 --- a/tests/tar/is_compressed_test.py +++ b/tests/tar/is_compressed_test.py @@ -16,6 +16,7 @@ import unittest from python.runfiles import runfiles +from tests.util import runfiles_util class IsCompressedTest(unittest.TestCase): @@ -26,7 +27,7 @@ def setUp(self): def get_file_under_test(self, file_name): """Get the file path to a generated archive in the runfiles.""" - return self.data_files.Rlocation( + return runfiles_util.rlocation(self.data_files, "rules_pkg/tests/tar/" + file_name ) diff --git a/tests/tar/pkg_tar_test.py b/tests/tar/pkg_tar_test.py index 7923ab67..f25e2f31 100644 --- a/tests/tar/pkg_tar_test.py +++ b/tests/tar/pkg_tar_test.py @@ -18,6 +18,7 @@ import unittest from python.runfiles import runfiles +from tests.util import runfiles_util from pkg.private.tar import tar_writer PORTABLE_MTIME = 946684800 # 2000-01-01 00:00:00.000 UTC @@ -40,7 +41,8 @@ def assertTarFileContent(self, file_name, content, verbose=False): """ # NOTE: This is portable to Windows. os.path.join('rules_pkg', 'tests', # filename) is not. - file_path = runfiles.Create().Rlocation('rules_pkg/tests/tar/' + file_name) + file_path = runfiles_util.rlocation( + runfiles.Create(), 'rules_pkg/tests/tar/' + file_name) got = [] with tarfile.open(file_path, 'r:*') as f: i = 0 @@ -306,7 +308,8 @@ def test_compression_level(self): ('test-tar-xz-compression_level-9.tar.xz', 67156), ] for file_name, expected_size in cases: - file_path = runfiles.Create().Rlocation('rules_pkg/tests/tar/' + file_name) + file_path = runfiles_util.rlocation( + runfiles.Create(), 'rules_pkg/tests/tar/' + file_name) file_size = os.stat(file_path).st_size self.assertEqual(file_size, expected_size, 'size error for ' + file_name) @@ -322,7 +325,8 @@ def test_preserve_mode(self): ('test-tar-preserve_mode-True.tar', "0o644"), # chmod 644 = rw- r-- r-- ] for file_name, expected_mode in expected_mode: - file_path = runfiles.Create().Rlocation('rules_pkg/tests/tar/' + file_name) + file_path = runfiles_util.rlocation( + runfiles.Create(), 'rules_pkg/tests/tar/' + file_name) with tarfile.open(file_path, 'r') as f: for member in f.getmembers(): self.assertEqual(member.name, "hello.txt", "unexpected file name for " + file_name) @@ -335,7 +339,8 @@ def test_preserve_mtime(self): ('test-tar-preserve_mtime-True.tar', False), ] for file_name, should_be_equal_to_portable_mtime in test_cases: - file_path = runfiles.Create().Rlocation('rules_pkg/tests/tar/' + file_name) + file_path = runfiles_util.rlocation( + runfiles.Create(), 'rules_pkg/tests/tar/' + file_name) with tarfile.open(file_path, 'r') as f: for member in f.getmembers(): self.assertEqual(member.name, "hello.txt", "unexpected file name for " + file_name) diff --git a/tests/tar/tar_writer_test.py b/tests/tar/tar_writer_test.py index fcc58897..8657d70b 100644 --- a/tests/tar/tar_writer_test.py +++ b/tests/tar/tar_writer_test.py @@ -18,6 +18,7 @@ import unittest from python.runfiles import runfiles +from tests.util import runfiles_util from pkg.private.tar import tar_writer from tests.tar import compressor @@ -73,6 +74,9 @@ def setUp(self): self.tempfile = os.path.join(os.environ["TEST_TMPDIR"], "test.tar") self.data_files = runfiles.Create() + def data_file(self, path): + return runfiles_util.rlocation(self.data_files, path) + def tearDown(self): super(TarFileWriterTest, self).tearDown() if os.path.exists(self.tempfile): @@ -127,7 +131,7 @@ def testMergeTar(self): ] for ext in [("." + comp if comp else "") for comp in tar_writer.COMPRESSIONS]: with tar_writer.TarFileWriter(self.tempfile) as f: - datafile = self.data_files.Rlocation( + datafile = self.data_file( "rules_pkg/tests/testdata/tar_test.tar" + ext) f.add_tar(datafile, name_filter=lambda n: n != "./b") self.assertTarFileContent(self.tempfile, content) @@ -139,7 +143,7 @@ def testMergeTarRelocated(self): {"name": "foo/ab", "data": b"ab"}, ] with tar_writer.TarFileWriter(self.tempfile, create_parents=True, allow_dups_from_deps=False) as f: - datafile = self.data_files.Rlocation( + datafile = self.data_file( "rules_pkg/tests/testdata/tar_test.tar") f.add_tar(datafile, name_filter=lambda n: n != "./b", prefix="foo") self.assertTarFileContent(self.tempfile, content) @@ -158,7 +162,7 @@ def testPortableMtime(self): def testPreserveTarMtimesTrueByDefault(self): with tar_writer.TarFileWriter(self.tempfile) as f: - input_tar_path = self.data_files.Rlocation( + input_tar_path = self.data_file( "rules_pkg/tests/testdata/tar_test.tar") f.add_tar(input_tar_path) input_tar = tarfile.open(input_tar_path, "r") @@ -169,7 +173,7 @@ def testPreserveTarMtimesTrueByDefault(self): def testPreserveTarMtimesFalse(self): with tar_writer.TarFileWriter(self.tempfile, preserve_tar_mtimes=False) as f: - input_tar_path = self.data_files.Rlocation( + input_tar_path = self.data_file( "rules_pkg/tests/testdata/tar_test.tar") f.add_tar(input_tar_path) for output_file in f.tar: @@ -225,7 +229,7 @@ def testAddingOnlySpecifiedFiles(self): def testPackageDirAttribute(self): """Tests package_dir of pkg_tar.""" - package_dir = self.data_files.Rlocation( + package_dir = self.data_file( "rules_pkg/tests/tar/test_tar_package_dir.tar") expected_content = [ {"name": "my"}, @@ -237,7 +241,7 @@ def testPackageDirAttribute(self): def testPackageDirFileAttribute(self): """Tests package_dir_file attributes of pkg_tar.""" - package_dir_file = self.data_files.Rlocation( + package_dir_file = self.data_file( "rules_pkg/tests/tar/test_tar_package_dir_file.tar") expected_content = [ {"name": "package"}, @@ -246,9 +250,9 @@ def testPackageDirFileAttribute(self): self.assertTarFileContent(package_dir_file, expected_content) def testCustomCompression(self): - original = self.data_files.Rlocation( + original = self.data_file( "rules_pkg/tests/testdata/tar_test.tar") - compressed = self.data_files.Rlocation( + compressed = self.data_file( "rules_pkg/tests/tar/test_tar_compression.tar") with open(compressed, "rb") as f_in, open(self.tempfile, "wb") as f_out: # "Decompress" by skipping garbage bytes @@ -267,7 +271,7 @@ def testAdditionOfDuplicatePath(self): {"name": "./b", "data": "q".encode("utf-8")} ] with tar_writer.TarFileWriter(self.tempfile) as f: - datafile = self.data_files.Rlocation( + datafile = self.data_file( "rules_pkg/tests/testdata/tar_test.tar") f.add_tar(datafile) f.add_file('./b', content="q") @@ -280,7 +284,7 @@ def testAdditionOfArchives(self): {"name": "./" + x} for x in ["a", "b", "ab", "a", "b", "ab"] ] with tar_writer.TarFileWriter(self.tempfile) as f: - datafile = self.data_files.Rlocation( + datafile = self.data_file( "rules_pkg/tests/testdata/tar_test.tar") f.add_tar(datafile) diff --git a/tests/util/BUILD b/tests/util/BUILD index 0be64823..562494d6 100644 --- a/tests/util/BUILD +++ b/tests/util/BUILD @@ -14,7 +14,7 @@ # Test utilities -load("@rules_python//python:defs.bzl", "py_binary") +load("@rules_python//python:defs.bzl", "py_binary", "py_library") package(default_applicable_licenses = ["//:license"]) @@ -35,3 +35,11 @@ py_binary( srcs_version = "PY3", visibility = ["//visibility:public"], ) + +py_library( + name = "runfiles_util", + srcs = ["runfiles_util.py"], + imports = ["../.."], + srcs_version = "PY3", + visibility = ["//tests:__subpackages__"], +) diff --git a/tests/util/runfiles_util.py b/tests/util/runfiles_util.py new file mode 100644 index 00000000..9ba11612 --- /dev/null +++ b/tests/util/runfiles_util.py @@ -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. +"""Helpers for using runfiles in tests.""" + +import os + + +def rlocation(runfiles, path): + """Like runfiles.Rlocation(path), but the result can be opened on Windows. + + With --enable_runfiles on Windows, Bazel may create a directory junction + for a file runfile. Such a junction can not be opened or executed, and + os.path.realpath fails on it, so we read the junction target instead. + + Args: + runfiles: A runfiles object, from runfiles.Create(). + path: The runfiles path to look up. + Returns: + The location of path, with a Windows junction replaced by its target. + """ + location = runfiles.Rlocation(path) + if os.name != "nt": + return location + try: + target = os.readlink(location) + except OSError: + # Not a link. + return location + if target.startswith("\\\\?\\"): + target = target[4:] + return os.path.join(os.path.dirname(location), target) diff --git a/tests/zip/BUILD b/tests/zip/BUILD index c2c42761..a6afbc09 100644 --- a/tests/zip/BUILD +++ b/tests/zip/BUILD @@ -31,6 +31,7 @@ py_library( imports = ["../.."], srcs_version = "PY3", deps = [ + "//tests/util:runfiles_util", "@rules_python//python/runfiles", ], ) diff --git a/tests/zip/zip_test_lib.py b/tests/zip/zip_test_lib.py index ba80930c..4d5d6d80 100644 --- a/tests/zip/zip_test_lib.py +++ b/tests/zip/zip_test_lib.py @@ -18,6 +18,7 @@ import zipfile from python.runfiles import runfiles +from tests.util import runfiles_util # Unix dir bit and Windows dir bit. Magic from zip spec @@ -45,7 +46,8 @@ def setUp(self): def get_test_zip(self, zip_file): """Get the file path to a generated zip in the runfiles.""" - return self.data_files.Rlocation("rules_pkg/tests/zip/" + zip_file) + return runfiles_util.rlocation( + self.data_files, "rules_pkg/tests/zip/" + zip_file) class ZipContentsTestBase(ZipTest):