Skip to content

Commit

Permalink
pycross: Add patching support to py_wheel_library (#1436)
Browse files Browse the repository at this point in the history
This patch adds a few arguments to `py_wheel_library` to simulate how
`http_archive` accepts patch-related arguments.

I also amended the existing test to validate the behaviour at a very
high level.

References: #1360
  • Loading branch information
philsc authored Dec 19, 2023
1 parent d8f6881 commit 6246b8e
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 4 deletions.
17 changes: 17 additions & 0 deletions tests/pycross/0001-Add-new-file-for-testing-patch-support.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
From b2ebe6fe67ff48edaf2ae937d24b1f0b67c16f81 Mon Sep 17 00:00:00 2001
From: Philipp Schrader <[email protected]>
Date: Thu, 28 Sep 2023 09:02:44 -0700
Subject: [PATCH] Add new file for testing patch support

---
site-packages/numpy/file_added_via_patch.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 site-packages/numpy/file_added_via_patch.txt

diff --git a/site-packages/numpy/file_added_via_patch.txt b/site-packages/numpy/file_added_via_patch.txt
new file mode 100644
index 0000000..9d947a4
--- /dev/null
+++ b/site-packages/numpy/file_added_via_patch.txt
@@ -0,0 +1 @@
+Hello from a patch!
30 changes: 30 additions & 0 deletions tests/pycross/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,33 @@ py_test(
"//python/runfiles",
],
)

py_wheel_library(
name = "patched_extracted_wheel_for_testing",
patch_args = [
"-p1",
],
patch_tool = "patch",
patches = [
"0001-Add-new-file-for-testing-patch-support.patch",
],
target_compatible_with = select({
# We don't have `patch` available on the Windows CI machines.
"@platforms//os:windows": ["@platforms//:incompatible"],
"//conditions:default": [],
}),
wheel = "@wheel_for_testing//file",
)

py_test(
name = "patched_py_wheel_library_test",
srcs = [
"patched_py_wheel_library_test.py",
],
data = [
":patched_extracted_wheel_for_testing",
],
deps = [
"//python/runfiles",
],
)
38 changes: 38 additions & 0 deletions tests/pycross/patched_py_wheel_library_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright 2023 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.

import unittest
from pathlib import Path

from python.runfiles import runfiles

RUNFILES = runfiles.Create()


class TestPyWheelLibrary(unittest.TestCase):
def setUp(self):
self.extraction_dir = Path(
RUNFILES.Rlocation("rules_python/tests/pycross/patched_extracted_wheel_for_testing")
)
self.assertTrue(self.extraction_dir.exists(), self.extraction_dir)
self.assertTrue(self.extraction_dir.is_dir(), self.extraction_dir)

def test_patched_file_contents(self):
"""Validate that the patch got applied correctly."""
file = self.extraction_dir / "site-packages/numpy/file_added_via_patch.txt"
self.assertEqual(file.read_text(), "Hello from a patch!\n")


if __name__ == "__main__":
unittest.main()
4 changes: 1 addition & 3 deletions tests/pycross/py_wheel_library_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@
class TestPyWheelLibrary(unittest.TestCase):
def setUp(self):
self.extraction_dir = Path(
RUNFILES.Rlocation(
"rules_python/tests/pycross/extracted_wheel_for_testing"
)
RUNFILES.Rlocation("rules_python/tests/pycross/extracted_wheel_for_testing")
)
self.assertTrue(self.extraction_dir.exists(), self.extraction_dir)
self.assertTrue(self.extraction_dir.is_dir(), self.extraction_dir)
Expand Down
58 changes: 58 additions & 0 deletions third_party/rules_pycross/pycross/private/tools/wheel_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import argparse
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
Expand Down Expand Up @@ -97,6 +98,29 @@ def main(args: Any) -> None:

setup_namespace_pkg_compatibility(lib_dir)

if args.patch:
if not args.patch_tool and not args.patch_tool_target:
raise ValueError("Specify one of 'patch_tool' or 'patch_tool_target'.")

patch_args = [
args.patch_tool or Path.cwd() / args.patch_tool_target
] + args.patch_arg
for patch in args.patch:
with patch.open("r") as stdin:
try:
subprocess.run(
patch_args,
stdin=stdin,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
cwd=args.directory,
)
except subprocess.CalledProcessError as error:
print(f"Patch {patch} failed to apply:")
print(error.stdout.decode("utf-8"))
raise


def parse_flags(argv) -> Any:
parser = argparse.ArgumentParser(description="Extract a Python wheel.")
Expand Down Expand Up @@ -127,6 +151,40 @@ def parse_flags(argv) -> Any:
help="The output path.",
)

parser.add_argument(
"--patch",
type=Path,
default=[],
action="append",
help="A patch file to apply.",
)

parser.add_argument(
"--patch-arg",
type=str,
default=[],
action="append",
help="An argument for the patch tool when applying the patches.",
)

parser.add_argument(
"--patch-tool",
type=str,
help=(
"The tool from PATH to invoke when applying patches. "
"If set, --patch-tool-target is ignored."
),
)

parser.add_argument(
"--patch-tool-target",
type=Path,
help=(
"The path to the tool to invoke when applying patches. "
"Ignored when --patch-tool is set."
),
)

return parser.parse_args(argv[1:])


Expand Down
39 changes: 38 additions & 1 deletion third_party/rules_pycross/pycross/private/wheel_library.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,31 @@ def _py_wheel_library_impl(ctx):
args = ctx.actions.args().use_param_file("--flagfile=%s")
args.add("--wheel", wheel_file)
args.add("--directory", out.path)
args.add_all(ctx.files.patches, format_each = "--patch=%s")
args.add_all(ctx.attr.patch_args, format_each = "--patch-arg=%s")
args.add("--patch-tool", ctx.attr.patch_tool)

inputs = [wheel_file]
tools = []
inputs = [wheel_file] + ctx.files.patches
if name_file:
inputs.append(name_file)
args.add("--wheel-name-file", name_file)

if ctx.attr.patch_tool_target:
args.add("--patch-tool-target", ctx.attr.patch_tool_target.files_to_run.executable)
tools.append(ctx.executable.patch_tool_target)

if ctx.attr.enable_implicit_namespace_pkgs:
args.add("--enable-implicit-namespace-pkgs")

# We apply patches in the same action as the extraction to minimize the
# number of times we cache the wheel contents. If we were to split this
# into 2 actions, then the wheel contents would be cached twice.
ctx.actions.run(
inputs = inputs,
outputs = [out],
executable = ctx.executable._tool,
tools = tools,
arguments = [args],
# Set environment variables to make generated .pyc files reproducible.
env = {
Expand Down Expand Up @@ -119,6 +131,31 @@ and py_test targets must specify either `legacy_create_init=False` or the global
This option is required to support some packages which cannot handle the conversion to pkg-util style.
""",
),
"patch_args": attr.string_list(
default = ["-p0"],
doc =
"The arguments given to the patch tool. Defaults to -p0, " +
"however -p1 will usually be needed for patches generated by " +
"git. If multiple -p arguments are specified, the last one will take effect.",
),
"patch_tool": attr.string(
doc = "The patch(1) utility from the host to use. " +
"If set, overrides `patch_tool_target`. Please note that setting " +
"this means that builds are not completely hermetic.",
),
"patch_tool_target": attr.label(
executable = True,
cfg = "exec",
doc = "The label of the patch(1) utility to use. " +
"Only used if `patch_tool` is not set.",
),
"patches": attr.label_list(
allow_files = True,
default = [],
doc =
"A list of files that are to be applied as patches after " +
"extracting the archive. This will use the patch command line tool.",
),
"python_version": attr.string(
doc = "The python version required for this wheel ('PY2' or 'PY3')",
values = ["PY2", "PY3", ""],
Expand Down

0 comments on commit 6246b8e

Please sign in to comment.