1# Copyright 2023 Jeremy Volkman. All rights reserved. 2# Copyright 2023 The Bazel Authors. All rights reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16"""Implementation of the py_wheel_library rule.""" 17 18load("@bazel_skylib//lib:paths.bzl", "paths") 19load("//python:defs.bzl", "PyInfo") 20load(":providers.bzl", "PyWheelInfo") 21 22def _py_wheel_library_impl(ctx): 23 out = ctx.actions.declare_directory(ctx.attr.name) 24 25 wheel_target = ctx.attr.wheel 26 if PyWheelInfo in wheel_target: 27 wheel_file = wheel_target[PyWheelInfo].wheel_file 28 name_file = wheel_target[PyWheelInfo].name_file 29 else: 30 wheel_file = ctx.file.wheel 31 name_file = None 32 33 args = ctx.actions.args().use_param_file("--flagfile=%s") 34 args.add("--wheel", wheel_file) 35 args.add("--directory", out.path) 36 args.add_all(ctx.files.patches, format_each = "--patch=%s") 37 args.add_all(ctx.attr.patch_args, format_each = "--patch-arg=%s") 38 args.add("--patch-tool", ctx.attr.patch_tool) 39 40 tools = [] 41 inputs = [wheel_file] + ctx.files.patches 42 if name_file: 43 inputs.append(name_file) 44 args.add("--wheel-name-file", name_file) 45 46 if ctx.attr.patch_tool_target: 47 args.add("--patch-tool-target", ctx.attr.patch_tool_target.files_to_run.executable) 48 tools.append(ctx.executable.patch_tool_target) 49 50 if ctx.attr.enable_implicit_namespace_pkgs: 51 args.add("--enable-implicit-namespace-pkgs") 52 53 # We apply patches in the same action as the extraction to minimize the 54 # number of times we cache the wheel contents. If we were to split this 55 # into 2 actions, then the wheel contents would be cached twice. 56 ctx.actions.run( 57 inputs = inputs, 58 outputs = [out], 59 executable = ctx.executable._tool, 60 tools = tools, 61 arguments = [args], 62 # Set environment variables to make generated .pyc files reproducible. 63 env = { 64 "PYTHONHASHSEED": "0", 65 "SOURCE_DATE_EPOCH": "315532800", 66 }, 67 mnemonic = "WheelInstall", 68 progress_message = "Installing %s" % ctx.file.wheel.basename, 69 ) 70 71 has_py2_only_sources = ctx.attr.python_version == "PY2" 72 has_py3_only_sources = ctx.attr.python_version == "PY3" 73 if not has_py2_only_sources: 74 for d in ctx.attr.deps: 75 if d[PyInfo].has_py2_only_sources: 76 has_py2_only_sources = True 77 break 78 if not has_py3_only_sources: 79 for d in ctx.attr.deps: 80 if d[PyInfo].has_py3_only_sources: 81 has_py3_only_sources = True 82 break 83 84 # TODO: Is there a more correct way to get this runfiles-relative import path? 85 imp = paths.join( 86 ctx.label.workspace_name or ctx.workspace_name, # Default to the local workspace. 87 ctx.label.package, 88 ctx.label.name, 89 "site-packages", # we put lib files in this subdirectory. 90 ) 91 92 imports = depset( 93 direct = [imp], 94 transitive = [d[PyInfo].imports for d in ctx.attr.deps], 95 ) 96 transitive_sources = depset( 97 direct = [out], 98 transitive = [dep[PyInfo].transitive_sources for dep in ctx.attr.deps if PyInfo in dep], 99 ) 100 runfiles = ctx.runfiles(files = [out]) 101 for d in ctx.attr.deps: 102 runfiles = runfiles.merge(d[DefaultInfo].default_runfiles) 103 104 return [ 105 DefaultInfo( 106 files = depset(direct = [out]), 107 runfiles = runfiles, 108 ), 109 PyInfo( 110 has_py2_only_sources = has_py2_only_sources, 111 has_py3_only_sources = has_py3_only_sources, 112 imports = imports, 113 transitive_sources = transitive_sources, 114 uses_shared_libraries = True, # Docs say this is unused 115 ), 116 ] 117 118py_wheel_library = rule( 119 implementation = _py_wheel_library_impl, 120 attrs = { 121 "deps": attr.label_list( 122 doc = "A list of this wheel's Python library dependencies.", 123 providers = [DefaultInfo, PyInfo], 124 ), 125 "enable_implicit_namespace_pkgs": attr.bool( 126 default = True, 127 doc = """ 128If true, disables conversion of native namespace packages into pkg-util style namespace packages. When set all py_binary 129and py_test targets must specify either `legacy_create_init=False` or the global Bazel option 130`--incompatible_default_to_explicit_init_py` to prevent `__init__.py` being automatically generated in every directory. 131This option is required to support some packages which cannot handle the conversion to pkg-util style. 132 """, 133 ), 134 "patch_args": attr.string_list( 135 default = ["-p0"], 136 doc = 137 "The arguments given to the patch tool. Defaults to -p0, " + 138 "however -p1 will usually be needed for patches generated by " + 139 "git. If multiple -p arguments are specified, the last one will take effect.", 140 ), 141 "patch_tool": attr.string( 142 doc = "The patch(1) utility from the host to use. " + 143 "If set, overrides `patch_tool_target`. Please note that setting " + 144 "this means that builds are not completely hermetic.", 145 ), 146 "patch_tool_target": attr.label( 147 executable = True, 148 cfg = "exec", 149 doc = "The label of the patch(1) utility to use. " + 150 "Only used if `patch_tool` is not set.", 151 ), 152 "patches": attr.label_list( 153 allow_files = True, 154 default = [], 155 doc = 156 "A list of files that are to be applied as patches after " + 157 "extracting the archive. This will use the patch command line tool.", 158 ), 159 "python_version": attr.string( 160 doc = "The python version required for this wheel ('PY2' or 'PY3')", 161 values = ["PY2", "PY3", ""], 162 ), 163 "wheel": attr.label( 164 doc = "The wheel file.", 165 allow_single_file = [".whl"], 166 mandatory = True, 167 ), 168 "_tool": attr.label( 169 default = Label("//third_party/rules_pycross/pycross/private/tools:wheel_installer"), 170 cfg = "exec", 171 executable = True, 172 ), 173 }, 174) 175