diff --git a/news/4025.fixed.md b/news/4025.fixed.md new file mode 100644 index 0000000000..80e81333f6 --- /dev/null +++ b/news/4025.fixed.md @@ -0,0 +1,3 @@ +(pypi) Fixed {obj}`RECORD` file paths for extracted `.data` directory contents +so that {obj}`importlib.metadata.files()` correctly locates installed +distribution files ([#4025](https://github.com/bazel-contrib/rules_python/pull/4025)). diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index bb84ff9280..7b8349e174 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -41,6 +41,15 @@ alias( visibility = ["//visibility:public"], ) +alias( + name = "wheel_record_rewriter", + actual = select({ + "@platforms//os:windows": "wheel_record_rewriter.ps1", + "//conditions:default": "wheel_record_rewriter.sh", + }), + visibility = ["//visibility:public"], +) + exports_files( srcs = ["deps.bzl"], visibility = ["//tools/private/update_deps:__pkg__"], @@ -490,11 +499,20 @@ bzl_library( ], ) +bzl_library( + name = "gen_wheel_record", + srcs = ["gen_wheel_record.bzl"], + deps = [ + "//python/private:common", + ], +) + bzl_library( name = "whl_library_targets", srcs = ["whl_library_targets.bzl"], deps = [ ":env_marker_setting", + ":gen_wheel_record", ":labels", ":namespace_pkgs", ":pep508_deps", diff --git a/python/private/pypi/gen_wheel_record.bzl b/python/private/pypi/gen_wheel_record.bzl new file mode 100644 index 0000000000..e709614b91 --- /dev/null +++ b/python/private/pypi/gen_wheel_record.bzl @@ -0,0 +1,83 @@ +"""Rule for generating platform-specific RECORD files.""" + +load("//python/private:common.bzl", "is_windows_platform") + +def _gen_wheel_record_impl(ctx): + is_windows = is_windows_platform(ctx) + rewriter_file = ctx.files._wheel_record_rewriter[0] + out_files = [] + + for in_file in ctx.files.srcs: + dist_info_name = in_file.dirname.rpartition("/")[2] + if dist_info_name: + if dist_info_name.endswith(".dist-info"): + data_dir_basename = ( + dist_info_name[:-len(".dist-info")] + ".data" + ) + else: + data_dir_basename = dist_info_name + ".data" + out_file = ctx.actions.declare_file( + "site-packages/{}/RECORD".format(dist_info_name), + ) + else: + data_dir_basename = "data" + out_file = ctx.actions.declare_file("site-packages/RECORD") + + out_files.append(out_file) + + action_args = ctx.actions.args() + inputs = depset([in_file, rewriter_file]) + + if rewriter_file.path.endswith(".ps1"): + action_exe = "powershell.exe" + action_args.add_all([ + "-ExecutionPolicy", + "Bypass", + "-NoProfile", + "-File", + rewriter_file, + ]) + else: + action_exe = ( + ctx.attr._wheel_record_rewriter[DefaultInfo].files_to_run + ) + + action_args.add(in_file) + action_args.add(out_file) + action_args.add("windows" if is_windows else "unix") + action_args.add(data_dir_basename) + + ctx.actions.run( + inputs = inputs, + outputs = [out_file], + executable = action_exe, + arguments = [action_args], + mnemonic = "PyGenWheelRecord", + progress_message = "Generating wheel RECORD %{output}", + toolchain = None, + ) + + return [ + DefaultInfo(files = depset(out_files)), + ] + +gen_wheel_record = rule( + implementation = _gen_wheel_record_impl, + attrs = { + "srcs": attr.label_list( + doc = "The original RECORD files to rewrite.", + mandatory = True, + allow_files = True, + ), + "_wheel_record_rewriter": attr.label( + default = "//python/private/pypi:wheel_record_rewriter", + allow_files = True, + cfg = "exec", + ), + "_windows_constraints": attr.label_list( + default = [ + "@platforms//os:windows", + ], + ), + }, +) diff --git a/python/private/pypi/wheel_record_rewriter.ps1 b/python/private/pypi/wheel_record_rewriter.ps1 new file mode 100644 index 0000000000..60dd0eca11 --- /dev/null +++ b/python/private/pypi/wheel_record_rewriter.ps1 @@ -0,0 +1,66 @@ +[CmdletBinding()] +param( + [Parameter(Position=0, Mandatory=$true)] + [string]$InFile, + + [Parameter(Position=1, Mandatory=$true)] + [string]$OutFile, + + [Parameter(Position=2, Mandatory=$true)] + [string]$TargetOs, + + [Parameter(Position=3, Mandatory=$true)] + [string]$DataDirBasename +) + +$ErrorActionPreference = "Stop" + +$dataPrefix = "$DataDirBasename/" +$quotedDataPrefix = "`"$DataDirBasename/" + +if ($TargetOs -eq "windows") { + $dataRepl = "../../" + $headersRepl = "../../Include/" + $platlibRepl = "" + $purelibRepl = "" + $scriptsRepl = "../../Scripts/" +} else { + $dataRepl = "../../../" + $headersRepl = "../../../include/" + $platlibRepl = "" + $purelibRepl = "" + $scriptsRepl = "../../../bin/" +} + +$lines = Get-Content -Path $InFile +$outLines = [System.Collections.Generic.List[string]]::new() +$Utf8NoBom = New-Object System.Text.UTF8Encoding $False + +foreach ($line in $lines) { + if ($line.StartsWith($quotedDataPrefix)) { + $quote = "`"" + $rest = $line.Substring($quotedDataPrefix.Length) + } elseif ($line.StartsWith($dataPrefix)) { + $quote = "" + $rest = $line.Substring($dataPrefix.Length) + } else { + $outLines.Add($line) + continue + } + + if ($rest.StartsWith("purelib/")) { + $outLines.Add($quote + $purelibRepl + $rest.Substring(8)) + } elseif ($rest.StartsWith("platlib/")) { + $outLines.Add($quote + $platlibRepl + $rest.Substring(8)) + } elseif ($rest.StartsWith("scripts/")) { + $outLines.Add($quote + $scriptsRepl + $rest.Substring(8)) + } elseif ($rest.StartsWith("headers/")) { + $outLines.Add($quote + $headersRepl + $rest.Substring(8)) + } elseif ($rest.StartsWith("data/")) { + $outLines.Add($quote + $dataRepl + $rest.Substring(5)) + } else { + $outLines.Add($line) + } +} + +[System.IO.File]::WriteAllText($OutFile, ($outLines -join "`n") + "`n", $Utf8NoBom) diff --git a/python/private/pypi/wheel_record_rewriter.sh b/python/private/pypi/wheel_record_rewriter.sh new file mode 100755 index 0000000000..10189435e1 --- /dev/null +++ b/python/private/pypi/wheel_record_rewriter.sh @@ -0,0 +1,60 @@ +#!/bin/sh +set -eu + +IN="$1" +OUT="$2" +TARGET_OS="$3" +DATA_DIR_BASENAME="$4" + +DATA_PREFIX="${DATA_DIR_BASENAME}/" +QUOTED_DATA_PREFIX="\"${DATA_DIR_BASENAME}/" + +if [ "$TARGET_OS" = "windows" ]; then + DATA_REPL="../../" + HEADERS_REPL="../../Include/" + PLATLIB_REPL="" + PURELIB_REPL="" + SCRIPTS_REPL="../../Scripts/" +else + DATA_REPL="../../../" + HEADERS_REPL="../../../include/" + PLATLIB_REPL="" + PURELIB_REPL="" + SCRIPTS_REPL="../../../bin/" +fi + +awk -v data_prefix="$DATA_PREFIX" \ + -v quoted_data_prefix="$QUOTED_DATA_PREFIX" \ + -v data_repl="$DATA_REPL" \ + -v headers_repl="$HEADERS_REPL" \ + -v platlib_repl="$PLATLIB_REPL" \ + -v purelib_repl="$PURELIB_REPL" \ + -v scripts_repl="$SCRIPTS_REPL" ' +{ + line = $0 + quote = "" + if (substr(line, 1, length(quoted_data_prefix)) == quoted_data_prefix) { + quote = "\"" + rest = substr(line, length(quoted_data_prefix) + 1) + } else if (substr(line, 1, length(data_prefix)) == data_prefix) { + rest = substr(line, length(data_prefix) + 1) + } else { + print line + next + } + + if (substr(rest, 1, 8) == "purelib/") { + print quote purelib_repl substr(rest, 9) + } else if (substr(rest, 1, 8) == "platlib/") { + print quote platlib_repl substr(rest, 9) + } else if (substr(rest, 1, 8) == "scripts/") { + print quote scripts_repl substr(rest, 9) + } else if (substr(rest, 1, 8) == "headers/") { + print quote headers_repl substr(rest, 9) + } else if (substr(rest, 1, 5) == "data/") { + print quote data_repl substr(rest, 6) + } else { + print line + } +} +' "$IN" > "$OUT" diff --git a/python/private/pypi/whl_extract.bzl b/python/private/pypi/whl_extract.bzl index 0d61b9a07b..5dd6d218f8 100644 --- a/python/private/pypi/whl_extract.bzl +++ b/python/private/pypi/whl_extract.bzl @@ -36,20 +36,7 @@ def whl_extract(rctx, *, whl_path, logger): # Get the .dist_info dir name data_dir = dist_info_dir.dirname.get_child(dist_info_dir.basename[:-len(".dist-info")] + ".data") if data_dir.exists: - for prefix, dest_prefix in { - # https://docs.python.org/3/library/sysconfig.html#posix-prefix - # We are taking this from the legacy whl installer config - "data": "data", - "headers": "include", - # In theory there may be directory collisions here, so it would be best to - # merge the paths here. We are doing for quite a few levels deep. What is - # more, this code has to be reasonably efficient because some packages like - # to not put everything to the top level, but to indicate explicitly if - # something is in `platlib` or `purelib` (e.g. libclang wheel). - "platlib": "site-packages", - "purelib": "site-packages", - "scripts": "bin", - }.items(): + for prefix, dest_prefix in _DATA_CATEGORIES.items(): src = data_dir.get_child(prefix) if not src.exists: # The prefix does not exist in the wheel, we can continue @@ -61,9 +48,32 @@ def whl_extract(rctx, *, whl_path, logger): logger.debug(lambda: "Renaming: {} -> {}".format(src, dest)) repo_utils.rename(rctx, src, dest) + # Move RECORD to rewrite-record so gen_wheel_record can generate + # the platform-specific RECORD file at build time. + record_file = dist_info_dir.get_child("RECORD") + if record_file.exists: + rewrite_record_dir = rctx.path("rewrite-record/" + dist_info_dir.basename) + repo_utils.mkdir(rctx, rewrite_record_dir) + repo_utils.rename(rctx, record_file, rewrite_record_dir.get_child("RECORD")) + # Ensure that there is no data dir left rctx.delete(data_dir) +# Mapping of wheel .data categories to their extraction destination (relative to +# repository root). +_DATA_CATEGORIES = { + # category: repo_dest_dir + "data": "data", + "headers": "include", + # In theory there may be directory collisions in platlib/purelib, so it is + # best to merge the paths here. What is more, this code has to be reasonably + # efficient because some packages like to explicitly indicate if something + # is in `platlib` or `purelib` (e.g. libclang wheel). + "platlib": "site-packages", + "purelib": "site-packages", + "scripts": "bin", +} + def merge_trees(src, dest): """Merge src into the destination path. diff --git a/python/private/pypi/whl_library_targets.bzl b/python/private/pypi/whl_library_targets.bzl index ee5c781b3d..b7fdbd55e9 100644 --- a/python/private/pypi/whl_library_targets.bzl +++ b/python/private/pypi/whl_library_targets.bzl @@ -19,6 +19,7 @@ load("//python:py_binary.bzl", "py_binary") load("//python:py_library.bzl", "py_library") load("//python/private:normalize_name.bzl", "normalize_name") load(":env_marker_setting.bzl", "env_marker_setting") +load(":gen_wheel_record.bzl", "gen_wheel_record") load( ":labels.bzl", "DATA_LABEL", @@ -159,6 +160,7 @@ def whl_library_srcs( py_library = py_library, venv_entry_point = venv_entry_point, venv_rewrite_shebang = venv_rewrite_shebang, + gen_wheel_record = gen_wheel_record, env_marker_setting = env_marker_setting, create_inits = _create_inits, )): @@ -225,6 +227,16 @@ def whl_library_srcs( bins_for_data_label.append(rewrite_target_name) data.append(rewrite_target_name) + record_srcs = native.glob(["rewrite-record/*/RECORD"], allow_empty = True) + record_target_name = "record" + if record_srcs: + rules.gen_wheel_record( + name = record_target_name, + srcs = record_srcs, + tags = ["manual"], + ) + data.append(record_target_name) + if filegroups == None: filegroups = { EXTRACTED_WHEEL_FILES: dict( @@ -248,6 +260,8 @@ def whl_library_srcs( srcs = native.glob(**glob_kwargs) if filegroup_name == DATA_LABEL: srcs = srcs + bins_for_data_label + if filegroup_name == DIST_INFO_LABEL and record_srcs: + srcs = srcs + [record_target_name] native.filegroup( name = filegroup_name, srcs = srcs, diff --git a/tests/pypi/whl_extract/BUILD.bazel b/tests/pypi/whl_extract/BUILD.bazel new file mode 100644 index 0000000000..3983477f2b --- /dev/null +++ b/tests/pypi/whl_extract/BUILD.bazel @@ -0,0 +1,11 @@ +load("@rules_shell//shell:sh_test.bzl", "sh_test") +load(":whl_extract_tests.bzl", "whl_extract_test_suite") + +whl_extract_test_suite(name = "whl_extract_tests") + +sh_test( + name = "wheel_record_rewriter_test", + srcs = ["wheel_record_rewriter_test.sh"], + args = ["$(location //python/private/pypi:wheel_record_rewriter)"], + data = ["//python/private/pypi:wheel_record_rewriter"], +) diff --git a/tests/pypi/whl_extract/wheel_record_rewriter_test.sh b/tests/pypi/whl_extract/wheel_record_rewriter_test.sh new file mode 100755 index 0000000000..c5880520ad --- /dev/null +++ b/tests/pypi/whl_extract/wheel_record_rewriter_test.sh @@ -0,0 +1,91 @@ +#!/bin/sh +set -eu + +REWRITER="$1" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +run_rewriter() { + case "$REWRITER" in + *.ps1) + in_file="$1" + out_file="$2" + platform_type="$3" + data_dir="$4" + if command -v cygpath >/dev/null 2>&1; then + in_file="$(cygpath -w "$in_file")" + out_file="$(cygpath -w "$out_file")" + fi + powershell.exe -ExecutionPolicy Bypass -NoProfile -File "$REWRITER" "$in_file" "$out_file" "$platform_type" "$data_dir" + ;; + *) + "$REWRITER" "$@" + ;; + esac +} + +INPUT="$TMP_DIR/input_RECORD" +cat <<'EOF' > "$INPUT" +foo-1.0.data/purelib/pkg/__init__.py,sha256=abc,100 +foo-1.0.data/purelib/pkg/module.py,sha256=def,200 +foo-1.0.data/platlib/pkg/_ext.so,sha256=ghi,300 +foo-1.0.data/data/pkg/data.txt,sha256=111,10 +foo-1.0.data/headers/pkg/header.h,sha256=222,20 +foo-1.0.data/scripts/my_script.sh,sha256=333,30 +"foo-1.0.data/purelib/pkg/my file.py",sha256=abc,100 +"foo-1.0.data/scripts/my tool",sha256=def,200 +"foo-1.0.data/headers/my header.h",sha256=ghi,300 +"foo-1.0.data/data/my data.txt",sha256=jkl,400 +foo-1.0.data/custom_dir/custom.txt,sha256=xyz,123 +top_level/__init__.py,sha256=aaa,50 +foo-1.0.dist-info/METADATA,sha256=bbb,60 +foo-1.0.dist-info/RECORD,, +EOF + +# Test Unix rewrite +UNIX_OUT="$TMP_DIR/unix_RECORD" +run_rewriter "$INPUT" "$UNIX_OUT" "unix" "foo-1.0.data" + +EXPECTED_UNIX="$TMP_DIR/expected_unix" +cat <<'EOF' > "$EXPECTED_UNIX" +pkg/__init__.py,sha256=abc,100 +pkg/module.py,sha256=def,200 +pkg/_ext.so,sha256=ghi,300 +../../../pkg/data.txt,sha256=111,10 +../../../include/pkg/header.h,sha256=222,20 +../../../bin/my_script.sh,sha256=333,30 +"pkg/my file.py",sha256=abc,100 +"../../../bin/my tool",sha256=def,200 +"../../../include/my header.h",sha256=ghi,300 +"../../../my data.txt",sha256=jkl,400 +foo-1.0.data/custom_dir/custom.txt,sha256=xyz,123 +top_level/__init__.py,sha256=aaa,50 +foo-1.0.dist-info/METADATA,sha256=bbb,60 +foo-1.0.dist-info/RECORD,, +EOF + +diff -u --strip-trailing-cr "$EXPECTED_UNIX" "$UNIX_OUT" + +# Test Windows rewrite +WIN_OUT="$TMP_DIR/win_RECORD" +run_rewriter "$INPUT" "$WIN_OUT" "windows" "foo-1.0.data" + +EXPECTED_WIN="$TMP_DIR/expected_win" +cat <<'EOF' > "$EXPECTED_WIN" +pkg/__init__.py,sha256=abc,100 +pkg/module.py,sha256=def,200 +pkg/_ext.so,sha256=ghi,300 +../../pkg/data.txt,sha256=111,10 +../../Include/pkg/header.h,sha256=222,20 +../../Scripts/my_script.sh,sha256=333,30 +"pkg/my file.py",sha256=abc,100 +"../../Scripts/my tool",sha256=def,200 +"../../Include/my header.h",sha256=ghi,300 +"../../my data.txt",sha256=jkl,400 +foo-1.0.data/custom_dir/custom.txt,sha256=xyz,123 +top_level/__init__.py,sha256=aaa,50 +foo-1.0.dist-info/METADATA,sha256=bbb,60 +foo-1.0.dist-info/RECORD,, +EOF + +diff -u --strip-trailing-cr "$EXPECTED_WIN" "$WIN_OUT" diff --git a/tests/pypi/whl_extract/whl_extract_tests.bzl b/tests/pypi/whl_extract/whl_extract_tests.bzl new file mode 100644 index 0000000000..f7e1e6d2cf --- /dev/null +++ b/tests/pypi/whl_extract/whl_extract_tests.bzl @@ -0,0 +1,119 @@ +"""Tests for whl_extract and gen_wheel_record.""" + +load("@rules_testing//lib:analysis_test.bzl", "analysis_test") +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("@rules_testing//lib:util.bzl", rt_util = "util") +load( + "//python/private/pypi:gen_wheel_record.bzl", # buildifier: disable=bzl-visibility + "gen_wheel_record", +) +load( + "//tests/support/platforms:platforms.bzl", # buildifier: disable=bzl-visibility + "platform_targets", +) + +_tests = [] + +def _test_gen_wheel_record(name): + rt_util.helper_target( + native.genrule, + name = name + "_src", + outs = [name + "_orig/alpha-1.0.dist-info/RECORD"], + cmd = "echo 'alpha-1.0.data/scripts/foo.sh' > $@", + ) + rt_util.helper_target( + gen_wheel_record, + name = name + "_subject", + srcs = [":" + name + "_src"], + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_gen_wheel_record_impl, + ) + +_tests.append(_test_gen_wheel_record) + +def _test_gen_wheel_record_impl(env, target): + files = target[DefaultInfo].files.to_list() + env.expect.that_collection(files).has_size(1) + env.expect.that_str(files[0].short_path).contains( + "site-packages/alpha-1.0.dist-info/RECORD", + ) + +def _test_gen_wheel_record_windows(name): + rt_util.helper_target( + native.genrule, + name = name + "_src", + outs = [name + "_orig/beta-1.0.dist-info/RECORD"], + cmd = "echo 'beta-1.0.data/scripts/foo.sh' > $@", + ) + rt_util.helper_target( + gen_wheel_record, + name = name + "_subject", + srcs = [":" + name + "_src"], + ) + analysis_test( + name = name, + target = name + "_subject", + config_settings = { + "//command_line_option:platforms": [ + platform_targets.WINDOWS_X86_64, + ], + }, + impl = _test_gen_wheel_record_windows_impl, + ) + +_tests.append(_test_gen_wheel_record_windows) + +def _test_gen_wheel_record_windows_impl(env, target): + files = target[DefaultInfo].files.to_list() + env.expect.that_collection(files).has_size(1) + env.expect.that_str(files[0].short_path).contains( + "site-packages/beta-1.0.dist-info/RECORD", + ) + +def _test_gen_wheel_record_multiple_srcs(name): + rt_util.helper_target( + native.genrule, + name = name + "_src1", + outs = [name + "_orig1/gamma-1.0.dist-info/RECORD"], + cmd = "echo 'gamma-1.0.data/scripts/foo.sh' > $@", + ) + rt_util.helper_target( + native.genrule, + name = name + "_src2", + outs = [name + "_orig2/delta-2.0.dist-info/RECORD"], + cmd = "echo 'delta-2.0.data/scripts/bar.sh' > $@", + ) + rt_util.helper_target( + gen_wheel_record, + name = name + "_subject", + srcs = [":" + name + "_src1", ":" + name + "_src2"], + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_gen_wheel_record_multiple_srcs_impl, + ) + +_tests.append(_test_gen_wheel_record_multiple_srcs) + +def _test_gen_wheel_record_multiple_srcs_impl(env, target): + files = target[DefaultInfo].files.to_list() + env.expect.that_collection(files).has_size(2) + paths = [f.short_path for f in files] + env.expect.that_bool( + any(["site-packages/gamma-1.0.dist-info/RECORD" in p for p in paths]), + ).equals(True) + env.expect.that_bool( + any(["site-packages/delta-2.0.dist-info/RECORD" in p for p in paths]), + ).equals(True) + +def whl_extract_test_suite(name): + """Create the test suite. + + Args: + name: the name of the test suite + """ + test_suite(name = name, tests = _tests) diff --git a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl index d752159b32..3fe1b99768 100644 --- a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl +++ b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl @@ -30,7 +30,7 @@ def _test_filegroups(env): def glob(include, *, exclude = [], allow_empty): _ = exclude # @unused env.expect.that_bool(allow_empty).equals(True) - if include == ["rewrite-bin/*"] or include == ["bin/*"]: + if include in [["rewrite-bin/*"], ["bin/*"], ["rewrite-record/*/RECORD"]]: return [] return include @@ -42,6 +42,7 @@ def _test_filegroups(env): ), rules = struct( venv_rewrite_shebang = lambda **kwargs: None, + gen_wheel_record = lambda **kwargs: None, ), ) @@ -84,6 +85,7 @@ def _test_copy(env): rules = struct( copy_file = lambda **kwargs: calls.append(kwargs), venv_rewrite_shebang = lambda **kwargs: None, + gen_wheel_record = lambda **kwargs: None, ), ) @@ -242,6 +244,7 @@ def _test_sdist_excludes_record(env): m_glob = mocks.glob() m_glob.results.append([]) # bin m_glob.results.append([]) # rewrite-bin + m_glob.results.append([]) # rewrite-record m_glob.results.append([]) # srcs m_glob.results.append([]) # data m_glob.results.append([]) # pyi @@ -259,6 +262,7 @@ def _test_sdist_excludes_record(env): py_library = lambda **kwargs: py_library_calls.append(kwargs), create_inits = lambda **kwargs: [], venv_rewrite_shebang = lambda **kwargs: None, + gen_wheel_record = lambda **kwargs: None, ), ) @@ -284,6 +288,7 @@ def _test_exclude_bazel_files(env): m_glob = mocks.glob() m_glob.results.append([]) # bin m_glob.results.append([]) # rewrite-bin + m_glob.results.append([]) # rewrite-record m_glob.results.append([]) # extracted_whl_files m_glob.results.append([]) # dist_info m_glob.results.append([]) # data @@ -297,6 +302,7 @@ def _test_exclude_bazel_files(env): ), rules = struct( venv_rewrite_shebang = lambda **kwargs: None, + gen_wheel_record = lambda **kwargs: None, ), ) @@ -314,6 +320,7 @@ def _test_exclude_bazel_files(env): env.expect.that_collection(m_glob.calls).contains_exactly([ mocks.glob_call(["bin/*"], allow_empty = True), mocks.glob_call(["rewrite-bin/*"], allow_empty = True), + mocks.glob_call(["rewrite-record/*/RECORD"], allow_empty = True), mocks.glob_call( include = ["**"], exclude = expected_exclude, diff --git a/tests/venv_site_packages_libs/importlib_metadata_test.py b/tests/venv_site_packages_libs/importlib_metadata_test.py index 963d43b6e0..8e73b53141 100644 --- a/tests/venv_site_packages_libs/importlib_metadata_test.py +++ b/tests/venv_site_packages_libs/importlib_metadata_test.py @@ -1,4 +1,5 @@ import importlib.metadata +import sys import unittest @@ -10,13 +11,66 @@ def test_importlib_metadata_files(self): len(files), 0, "importlib.metadata.files returned empty list" ) - # Verify it contains some expected files. - # The RECORD file lists paths relative to the installation root (site-packages). - # whl_with_data1-1.0.data/purelib/data_overlap.py should be installed as data_overlap.py - # whl_with_data1-1.0.data/platlib/whl_with_data1/platlib_file.txt should be whl_with_data1/platlib_file.txt + if sys.platform == "win32": + scripts_prefix = "../../Scripts/" + headers_prefix = "../../Include/" + data_prefix = "../../" + else: + scripts_prefix = "../../../bin/" + headers_prefix = "../../../include/" + data_prefix = "../../../" - file_names = [f.name for f in files] - self.assertIn("data_overlap.py", file_names) + expected_paths = sorted( + [ + scripts_prefix + "data_overlap.sh", + data_prefix + "bin/data_overlap.sh", + scripts_prefix + "overlap/both.sh", + scripts_prefix + "overlap/script1.sh", + scripts_prefix + "whl_script.sh", + scripts_prefix + "whl_with_data1_script", + headers_prefix + "data_overlap.h", + data_prefix + "include/data_overlap.h", + headers_prefix + "overlap/both.h", + headers_prefix + "overlap/header1.h", + headers_prefix + "whl_with_data1/header_file.h", + data_prefix + "overlap/both.txt", + data_prefix + "overlap/data1.txt", + data_prefix + "site-packages/data_overlap.py", + data_prefix + "whl_with_data1/data_data_file.txt", + data_prefix + "whl_with_data1/data_data_file.txt", + "data_overlap.py", + "whl_with_data1/data_file.txt", + "whl_with_data1/platlib_file.txt", + ] + ) + file_paths = sorted(str(f).replace("\\", "/") for f in files) + self.assertEqual(file_paths, expected_paths) + + for f in files: + resolved = f.locate() + if resolved.exists(): + self.assertTrue( + resolved.is_file(), + f"Expected {resolved} to be a regular file", + ) + + # Verify file content can be read both as binary and as text + content = f.read_binary() + self.assertIsNotNone(content) + + text = f.read_text(encoding="utf-8") + self.assertIsNotNone(text) + else: + # On Windows, venv bin scripts have a .bat extension appended. + bat_resolved = resolved.parent / (resolved.name + ".bat") + self.assertTrue( + bat_resolved.exists(), + f"Expected file {f} (resolved to {resolved} or {bat_resolved}) to exist", + ) + self.assertTrue( + bat_resolved.is_file(), + f"Expected {bat_resolved} to be a regular file", + ) if __name__ == "__main__":