• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2023 The Bazel Authors. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#    http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""A small utility to patch a file in the repository context and repackage it using a Python interpreter
16
17Note, because we are patching a wheel file and we need a new RECORD file, this
18function will print a diff of the RECORD and will ask the user to include a
19RECORD patch in their patches that they maintain. This is to ensure that we can
20satisfy the following usecases:
21* Patch an invalid RECORD file.
22* Patch files within a wheel.
23
24If we were silently regenerating the RECORD file, we may be vulnerable to supply chain
25attacks (it is a very small chance) and keeping the RECORD patches next to the
26other patches ensures that the users have overview on exactly what has changed
27within the wheel.
28"""
29
30load("//python/private:repo_utils.bzl", "repo_utils")
31load(":parse_whl_name.bzl", "parse_whl_name")
32
33_rules_python_root = Label("//:BUILD.bazel")
34
35def patched_whl_name(original_whl_name):
36    """Return the new filename to output the patched wheel.
37
38    Args:
39        original_whl_name: {type}`str` the whl name of the original file.
40
41    Returns:
42        {type}`str` an output name to write the patched wheel to.
43    """
44    parsed_whl = parse_whl_name(original_whl_name)
45    version = parsed_whl.version
46    suffix = "patched"
47    if "+" in version:
48        # This already has some local version, so we just append one more
49        # identifier here. We comply with the spec and mark the file as patched
50        # by adding a local version identifier at the end.
51        #
52        # By doing this we can still install the package using most of the package
53        # managers
54        #
55        # See https://packaging.python.org/en/latest/specifications/version-specifiers/#local-version-identifiers
56        version = "{}.{}".format(version, suffix)
57    else:
58        version = "{}+{}".format(version, suffix)
59
60    return "{distribution}-{version}-{python_tag}-{abi_tag}-{platform_tag}.whl".format(
61        distribution = parsed_whl.distribution,
62        version = version,
63        python_tag = parsed_whl.python_tag,
64        abi_tag = parsed_whl.abi_tag,
65        platform_tag = parsed_whl.platform_tag,
66    )
67
68def patch_whl(rctx, *, python_interpreter, whl_path, patches, **kwargs):
69    """Patch a whl file and repack it to ensure that the RECORD metadata stays correct.
70
71    Args:
72        rctx: repository_ctx
73        python_interpreter: the python interpreter to use.
74        whl_path: The whl file name to be patched.
75        patches: a label-keyed-int dict that has the patch files as keys and
76            the patch_strip as the value.
77        **kwargs: extras passed to repo_utils.execute_checked.
78
79    Returns:
80        value of the repackaging action.
81    """
82
83    # extract files into the current directory for patching as rctx.patch
84    # does not support patching in another directory.
85    whl_input = rctx.path(whl_path)
86
87    # symlink to a zip file to use bazel's extract so that we can use bazel's
88    # repository_ctx patch implementation. The whl file may be in a different
89    # external repository.
90    whl_file_zip = whl_input.basename + ".zip"
91    rctx.symlink(whl_input, whl_file_zip)
92    rctx.extract(whl_file_zip)
93    if not rctx.delete(whl_file_zip):
94        fail("Failed to remove the symlink after extracting")
95
96    if not patches:
97        fail("Trying to patch wheel without any patches")
98
99    for patch_file, patch_strip in patches.items():
100        rctx.patch(patch_file, strip = patch_strip)
101
102    record_patch = rctx.path("RECORD.patch")
103    whl_patched = patched_whl_name(whl_input.basename)
104
105    repo_utils.execute_checked(
106        rctx,
107        arguments = [
108            python_interpreter,
109            "-m",
110            "python.private.pypi.repack_whl",
111            "--record-patch",
112            record_patch,
113            whl_input,
114            whl_patched,
115        ],
116        environment = {
117            "PYTHONPATH": str(rctx.path(_rules_python_root).dirname),
118        },
119        **kwargs
120    )
121
122    if record_patch.exists:
123        record_patch_contents = rctx.read(record_patch)
124        warning_msg = """WARNING: the resultant RECORD file of the patch wheel is different
125
126    If you are patching on Windows, you may see this warning because of
127    a known issue (bazelbuild/rules_python#1639) with file endings.
128
129    If you would like to silence the warning, you can apply the patch that is stored in
130      {record_patch}. The contents of the file are below:
131{record_patch_contents}""".format(
132            record_patch = record_patch,
133            record_patch_contents = record_patch_contents,
134        )
135        print(warning_msg)  # buildifier: disable=print
136
137    return rctx.path(whl_patched)
138