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