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 58ac8771..83caa591 100644 --- a/pkg/BUILD +++ b/pkg/BUILD @@ -67,6 +67,16 @@ 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", @@ -92,6 +102,7 @@ py_library( "//tests:__subpackages__", ], deps = [ + ":init", "//pkg/private:archive", "//pkg/private:build_info", "//pkg/private:helpers", @@ -108,8 +119,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/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..5f58c6b9 100644 --- a/pkg/private/deb/BUILD +++ b/pkg/private/deb/BUILD @@ -58,10 +58,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/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/private/tar/BUILD b/pkg/private/tar/BUILD index 68a2b437..31dcdf67 100644 --- a/pkg/private/tar/BUILD +++ b/pkg/private/tar/BUILD @@ -63,6 +63,7 @@ py_binary( py_library( name = "tar_writer", srcs = [ + "__init__.py", "tar_writer.py", ], imports = ["../../.."], @@ -70,4 +71,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/releasing/BUILD b/pkg/releasing/BUILD index 4036c0fd..d1581bd2 100644 --- a/pkg/releasing/BUILD +++ b/pkg/releasing/BUILD @@ -36,6 +36,7 @@ py_library( ], imports = ["../.."], srcs_version = "PY3", + deps = ["//pkg:init"], ) py_binary( 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 93b4626c..5c74176f 100644 --- a/tests/BUILD +++ b/tests/BUILD @@ -97,10 +97,22 @@ py_test( srcs_version = "PY3", deps = [ "//pkg/private:archive", + "//tests/util:runfiles_util", "@rules_python//python/runfiles", ], ) +py_test( + name = "explicit_init_test", + srcs = ["explicit_init_test.py"], + 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"], @@ -216,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/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() 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):