1# Copyright 2018 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"""Public API for for building wheels.""" 16 17load("@bazel_skylib//rules:native_binary.bzl", "native_binary") 18load("//python:py_binary.bzl", "py_binary") 19load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") 20load("//python/private:py_package.bzl", "py_package_lib") 21load("//python/private:py_wheel.bzl", _PyWheelInfo = "PyWheelInfo", _py_wheel = "py_wheel") 22load("//python/private:util.bzl", "copy_propagating_kwargs") 23 24# Re-export as public API 25PyWheelInfo = _PyWheelInfo 26 27py_package = rule( 28 implementation = py_package_lib.implementation, 29 doc = """\ 30A rule to select all files in transitive dependencies of deps which 31belong to given set of Python packages. 32 33This rule is intended to be used as data dependency to py_wheel rule. 34""", 35 attrs = py_package_lib.attrs, 36) 37 38def _py_wheel_dist_impl(ctx): 39 out = ctx.actions.declare_directory(ctx.attr.out) 40 name_file = ctx.attr.wheel[PyWheelInfo].name_file 41 wheel = ctx.attr.wheel[PyWheelInfo].wheel 42 43 args = ctx.actions.args() 44 args.add("--wheel", wheel) 45 args.add("--name_file", name_file) 46 args.add("--output", out.path) 47 48 ctx.actions.run( 49 mnemonic = "PyWheelDistDir", 50 executable = ctx.executable._copier, 51 inputs = [wheel, name_file], 52 outputs = [out], 53 arguments = [args], 54 ) 55 return [ 56 DefaultInfo( 57 files = depset([out]), 58 runfiles = ctx.runfiles([out]), 59 ), 60 ] 61 62py_wheel_dist = rule( 63 doc = """\ 64Prepare a dist/ folder, following Python's packaging standard practice. 65 66See https://packaging.python.org/en/latest/tutorials/packaging-projects/#generating-distribution-archives 67which recommends a dist/ folder containing the wheel file(s), source distributions, etc. 68 69This also has the advantage that stamping information is included in the wheel's filename. 70""", 71 implementation = _py_wheel_dist_impl, 72 attrs = { 73 "out": attr.string( 74 doc = "name of the resulting directory", 75 mandatory = True, 76 ), 77 "wheel": attr.label( 78 doc = "a [py_wheel target](#py_wheel)", 79 providers = [PyWheelInfo], 80 ), 81 "_copier": attr.label( 82 cfg = "exec", 83 executable = True, 84 default = Label("//python/private:py_wheel_dist"), 85 ), 86 }, 87) 88 89def py_wheel( 90 name, 91 twine = None, 92 twine_binary = Label("//tools/publish:twine") if BZLMOD_ENABLED else None, 93 publish_args = [], 94 **kwargs): 95 """Builds a Python Wheel. 96 97 Wheels are Python distribution format defined in https://www.python.org/dev/peps/pep-0427/. 98 99 This macro packages a set of targets into a single wheel. 100 It wraps the [py_wheel rule](#py_wheel_rule). 101 102 Currently only pure-python wheels are supported. 103 104 Examples: 105 106 ```python 107 # Package some specific py_library targets, without their dependencies 108 py_wheel( 109 name = "minimal_with_py_library", 110 # Package data. We're building "example_minimal_library-0.0.1-py3-none-any.whl" 111 distribution = "example_minimal_library", 112 python_tag = "py3", 113 version = "0.0.1", 114 deps = [ 115 "//examples/wheel/lib:module_with_data", 116 "//examples/wheel/lib:simple_module", 117 ], 118 ) 119 120 # Use py_package to collect all transitive dependencies of a target, 121 # selecting just the files within a specific python package. 122 py_package( 123 name = "example_pkg", 124 # Only include these Python packages. 125 packages = ["examples.wheel"], 126 deps = [":main"], 127 ) 128 129 py_wheel( 130 name = "minimal_with_py_package", 131 # Package data. We're building "example_minimal_package-0.0.1-py3-none-any.whl" 132 distribution = "example_minimal_package", 133 python_tag = "py3", 134 version = "0.0.1", 135 deps = [":example_pkg"], 136 ) 137 ``` 138 139 To publish the wheel to PyPI, the twine package is required and it is installed 140 by default on `bzlmod` setups. On legacy `WORKSPACE`, `rules_python` 141 doesn't provide `twine` itself 142 (see https://github.com/bazelbuild/rules_python/issues/1016), but 143 you can install it with `pip_parse`, just like we do any other dependencies. 144 145 Once you've installed twine, you can pass its label to the `twine` 146 attribute of this macro, to get a "[name].publish" target. 147 148 Example: 149 150 ```python 151 py_wheel( 152 name = "my_wheel", 153 twine = "@publish_deps//twine", 154 ... 155 ) 156 ``` 157 158 Now you can run a command like the following, which publishes to https://test.pypi.org/ 159 160 ```sh 161 % TWINE_USERNAME=__token__ TWINE_PASSWORD=pypi-*** \\ 162 bazel run --stamp --embed_label=1.2.4 -- \\ 163 //path/to:my_wheel.publish --repository testpypi 164 ``` 165 166 Args: 167 name: A unique name for this target. 168 twine: A label of the external location of the py_library target for twine 169 twine_binary: A label of the external location of a binary target for twine. 170 publish_args: arguments passed to twine, e.g. ["--repository-url", "https://pypi.my.org/simple/"]. 171 These are subject to make var expansion, as with the `args` attribute. 172 Note that you can also pass additional args to the bazel run command as in the example above. 173 **kwargs: other named parameters passed to the underlying [py_wheel rule](#py_wheel_rule) 174 """ 175 tags = kwargs.pop("tags", []) 176 manual_tags = depset(tags + ["manual"]).to_list() 177 178 dist_target = "{}.dist".format(name) 179 py_wheel_dist( 180 name = dist_target, 181 wheel = name, 182 out = kwargs.pop("dist_folder", "{}_dist".format(name)), 183 tags = manual_tags, 184 **copy_propagating_kwargs(kwargs) 185 ) 186 187 _py_wheel( 188 name = name, 189 tags = tags, 190 **kwargs 191 ) 192 193 twine_args = [] 194 if twine or twine_binary: 195 twine_args = ["upload"] 196 twine_args.extend(publish_args) 197 twine_args.append("$(rootpath :{})/*".format(dist_target)) 198 199 if twine_binary: 200 native_binary( 201 name = "{}.publish".format(name), 202 src = twine_binary, 203 out = select({ 204 "@platforms//os:windows": "{}.publish_script.exe".format(name), 205 "//conditions:default": "{}.publish_script".format(name), 206 }), 207 args = twine_args, 208 data = [dist_target], 209 tags = manual_tags, 210 visibility = kwargs.get("visibility"), 211 **copy_propagating_kwargs(kwargs) 212 ) 213 elif twine: 214 if not twine.endswith(":pkg"): 215 fail("twine label should look like @my_twine_repo//:pkg") 216 217 twine_main = twine.replace(":pkg", ":rules_python_wheel_entry_point_twine.py") 218 219 py_binary( 220 name = "{}.publish".format(name), 221 srcs = [twine_main], 222 args = twine_args, 223 data = [dist_target], 224 imports = ["."], 225 main = twine_main, 226 deps = [twine], 227 tags = manual_tags, 228 visibility = kwargs.get("visibility"), 229 **copy_propagating_kwargs(kwargs) 230 ) 231 232py_wheel_rule = _py_wheel 233