xref: /aosp_15_r20/external/bazelbuild-rules_python/python/packaging.bzl (revision 60517a1edbc8ecf509223e9af94a7adec7d736b8)
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