• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2024 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"""Macro to generate all of the targets present in a {obj}`whl_library`."""
16
17load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
18load("//python:py_binary.bzl", "py_binary")
19load("//python:py_library.bzl", "py_library")
20load("//python/private:glob_excludes.bzl", "glob_excludes")
21load("//python/private:normalize_name.bzl", "normalize_name")
22load(
23    ":labels.bzl",
24    "DATA_LABEL",
25    "DIST_INFO_LABEL",
26    "PY_LIBRARY_IMPL_LABEL",
27    "PY_LIBRARY_PUBLIC_LABEL",
28    "WHEEL_ENTRY_POINT_PREFIX",
29    "WHEEL_FILE_IMPL_LABEL",
30    "WHEEL_FILE_PUBLIC_LABEL",
31)
32
33def whl_library_targets(
34        *,
35        name,
36        dep_template,
37        data_exclude = [],
38        srcs_exclude = [],
39        tags = [],
40        filegroups = {
41            DIST_INFO_LABEL: ["site-packages/*.dist-info/**"],
42            DATA_LABEL: ["data/**"],
43        },
44        dependencies = [],
45        dependencies_by_platform = {},
46        group_deps = [],
47        group_name = "",
48        data = [],
49        copy_files = {},
50        copy_executables = {},
51        entry_points = {},
52        native = native,
53        rules = struct(
54            copy_file = copy_file,
55            py_binary = py_binary,
56            py_library = py_library,
57        )):
58    """Create all of the whl_library targets.
59
60    Args:
61        name: {type}`str` The file to match for including it into the `whl`
62            filegroup. This may be also parsed to generate extra metadata.
63        dep_template: {type}`str` The dep_template to use for dependency
64            interpolation.
65        tags: {type}`list[str]` The tags set on the `py_library`.
66        dependencies: {type}`list[str]` A list of dependencies.
67        dependencies_by_platform: {type}`dict[str, list[str]]` A list of
68            dependencies by platform key.
69        filegroups: {type}`dict[str, list[str]]` A dictionary of the target
70            names and the glob matches.
71        group_name: {type}`str` name of the dependency group (if any) which
72            contains this library. If set, this library will behave as a shim
73            to group implementation rules which will provide simultaneously
74            installed dependencies which would otherwise form a cycle.
75        group_deps: {type}`list[str]` names of fellow members of the group (if
76            any). These will be excluded from generated deps lists so as to avoid
77            direct cycles. These dependencies will be provided at runtime by the
78            group rules which wrap this library and its fellows together.
79        copy_executables: {type}`dict[str, str]` The mapping between src and
80            dest locations for the targets.
81        copy_files: {type}`dict[str, str]` The mapping between src and
82            dest locations for the targets.
83        data_exclude: {type}`list[str]` The globs for data attribute exclusion
84            in `py_library`.
85        srcs_exclude: {type}`list[str]` The globs for srcs attribute exclusion
86            in `py_library`.
87        data: {type}`list[str]` A list of labels to include as part of the `data` attribute in `py_library`.
88        entry_points: {type}`dict[str, str]` The mapping between the script
89            name and the python file to use. DEPRECATED.
90        native: {type}`native` The native struct for overriding in tests.
91        rules: {type}`struct` A struct with references to rules for creating targets.
92    """
93    _ = name  # buildifier: @unused
94
95    dependencies = sorted([normalize_name(d) for d in dependencies])
96    dependencies_by_platform = {
97        platform: sorted([normalize_name(d) for d in deps])
98        for platform, deps in dependencies_by_platform.items()
99    }
100    tags = sorted(tags)
101    data = [] + data
102
103    for filegroup_name, glob in filegroups.items():
104        native.filegroup(
105            name = filegroup_name,
106            srcs = native.glob(glob, allow_empty = True),
107            visibility = ["//visibility:public"],
108        )
109
110    for src, dest in copy_files.items():
111        rules.copy_file(
112            name = dest + ".copy",
113            src = src,
114            out = dest,
115            visibility = ["//visibility:public"],
116        )
117        data.append(dest)
118    for src, dest in copy_executables.items():
119        rules.copy_file(
120            name = dest + ".copy",
121            src = src,
122            out = dest,
123            is_executable = True,
124            visibility = ["//visibility:public"],
125        )
126        data.append(dest)
127
128    _config_settings(
129        dependencies_by_platform.keys(),
130        native = native,
131        visibility = ["//visibility:private"],
132    )
133
134    # TODO @aignas 2024-10-25: remove the entry_point generation once
135    # `py_console_script_binary` is the only way to use entry points.
136    for entry_point, entry_point_script_name in entry_points.items():
137        rules.py_binary(
138            name = "{}_{}".format(WHEEL_ENTRY_POINT_PREFIX, entry_point),
139            # Ensure that this works on Windows as well - script may have Windows path separators.
140            srcs = [entry_point_script_name.replace("\\", "/")],
141            # This makes this directory a top-level in the python import
142            # search path for anything that depends on this.
143            imports = ["."],
144            deps = [":" + PY_LIBRARY_PUBLIC_LABEL],
145            visibility = ["//visibility:public"],
146        )
147
148    # Ensure this list is normalized
149    # Note: mapping used as set
150    group_deps = {
151        normalize_name(d): True
152        for d in group_deps
153    }
154
155    dependencies = [
156        d
157        for d in dependencies
158        if d not in group_deps
159    ]
160    dependencies_by_platform = {
161        p: deps
162        for p, deps in dependencies_by_platform.items()
163        for deps in [[d for d in deps if d not in group_deps]]
164        if deps
165    }
166
167    # If this library is a member of a group, its public label aliases need to
168    # point to the group implementation rule not the implementation rules. We
169    # also need to mark the implementation rules as visible to the group
170    # implementation.
171    if group_name and "//:" in dep_template:
172        # This is the legacy behaviour where the group library is outside the hub repo
173        label_tmpl = dep_template.format(
174            name = "_groups",
175            target = normalize_name(group_name) + "_{}",
176        )
177        impl_vis = [dep_template.format(
178            name = "_groups",
179            target = "__pkg__",
180        )]
181
182        native.alias(
183            name = PY_LIBRARY_PUBLIC_LABEL,
184            actual = label_tmpl.format(PY_LIBRARY_PUBLIC_LABEL),
185            visibility = ["//visibility:public"],
186        )
187        native.alias(
188            name = WHEEL_FILE_PUBLIC_LABEL,
189            actual = label_tmpl.format(WHEEL_FILE_PUBLIC_LABEL),
190            visibility = ["//visibility:public"],
191        )
192        py_library_label = PY_LIBRARY_IMPL_LABEL
193        whl_file_label = WHEEL_FILE_IMPL_LABEL
194
195    elif group_name:
196        py_library_label = PY_LIBRARY_PUBLIC_LABEL
197        whl_file_label = WHEEL_FILE_PUBLIC_LABEL
198        impl_vis = [dep_template.format(name = "", target = "__subpackages__")]
199
200    else:
201        py_library_label = PY_LIBRARY_PUBLIC_LABEL
202        whl_file_label = WHEEL_FILE_PUBLIC_LABEL
203        impl_vis = ["//visibility:public"]
204
205    if hasattr(native, "filegroup"):
206        native.filegroup(
207            name = whl_file_label,
208            srcs = [name],
209            data = _deps(
210                deps = dependencies,
211                deps_by_platform = dependencies_by_platform,
212                tmpl = dep_template.format(name = "{}", target = WHEEL_FILE_PUBLIC_LABEL),
213                # NOTE @aignas 2024-10-28: Actually, `select` is not part of
214                # `native`, but in order to support bazel 6.4 in unit tests, I
215                # have to somehow pass the `select` implementation in the unit
216                # tests and I chose this to be routed through the `native`
217                # struct. So, tests` will be successful in `getattr` and the
218                # real code will use the fallback provided here.
219                select = getattr(native, "select", select),
220            ),
221            visibility = impl_vis,
222        )
223
224    if hasattr(rules, "py_library"):
225        _data_exclude = [
226            "**/*.py",
227            "**/*.pyc",
228            "**/*.pyc.*",  # During pyc creation, temp files named *.pyc.NNNN are created
229            # RECORD is known to contain sha256 checksums of files which might include the checksums
230            # of generated files produced when wheels are installed. The file is ignored to avoid
231            # Bazel caching issues.
232            "**/*.dist-info/RECORD",
233        ] + glob_excludes.version_dependent_exclusions()
234        for item in data_exclude:
235            if item not in _data_exclude:
236                _data_exclude.append(item)
237
238        rules.py_library(
239            name = py_library_label,
240            srcs = native.glob(
241                ["site-packages/**/*.py"],
242                exclude = srcs_exclude,
243                # Empty sources are allowed to support wheels that don't have any
244                # pure-Python code, e.g. pymssql, which is written in Cython.
245                allow_empty = True,
246            ),
247            data = data + native.glob(
248                ["site-packages/**/*"],
249                exclude = _data_exclude,
250            ),
251            # This makes this directory a top-level in the python import
252            # search path for anything that depends on this.
253            imports = ["site-packages"],
254            deps = _deps(
255                deps = dependencies,
256                deps_by_platform = dependencies_by_platform,
257                tmpl = dep_template.format(name = "{}", target = PY_LIBRARY_PUBLIC_LABEL),
258                select = getattr(native, "select", select),
259            ),
260            tags = tags,
261            visibility = impl_vis,
262        )
263
264def _config_settings(dependencies_by_platform, native = native, **kwargs):
265    """Generate config settings for the targets.
266
267    Args:
268        dependencies_by_platform: {type}`list[str]` platform keys, can be
269            one of the following formats:
270            * `//conditions:default`
271            * `@platforms//os:{value}`
272            * `@platforms//cpu:{value}`
273            * `@//python/config_settings:is_python_3.{minor_version}`
274            * `{os}_{cpu}`
275            * `cp3{minor_version}_{os}_{cpu}`
276        native: {type}`native` The native struct for overriding in tests.
277        **kwargs: Extra kwargs to pass to the rule.
278    """
279    for p in dependencies_by_platform:
280        if p.startswith("@") or p.endswith("default"):
281            continue
282
283        abi, _, tail = p.partition("_")
284        if not abi.startswith("cp"):
285            tail = p
286            abi = ""
287
288        os, _, arch = tail.partition("_")
289        os = "" if os == "anyos" else os
290        arch = "" if arch == "anyarch" else arch
291
292        _kwargs = dict(kwargs)
293        if arch:
294            _kwargs.setdefault("constraint_values", []).append("@platforms//cpu:{}".format(arch))
295        if os:
296            _kwargs.setdefault("constraint_values", []).append("@platforms//os:{}".format(os))
297
298        if abi:
299            _kwargs["flag_values"] = {
300                "@rules_python//python/config_settings:python_version_major_minor": "3.{minor_version}".format(
301                    minor_version = abi[len("cp3"):],
302                ),
303            }
304
305        native.config_setting(
306            name = "is_{name}".format(
307                name = p.replace("cp3", "python_3."),
308            ),
309            **_kwargs
310        )
311
312def _plat_label(plat):
313    if plat.endswith("default"):
314        return plat
315    elif plat.startswith("@//"):
316        return Label(plat.strip("@"))
317    elif plat.startswith("@"):
318        return plat
319    else:
320        return ":is_" + plat.replace("cp3", "python_3.")
321
322def _deps(deps, deps_by_platform, tmpl, select = select):
323    deps = [tmpl.format(d) for d in sorted(deps)]
324
325    if not deps_by_platform:
326        return deps
327
328    deps_by_platform = {
329        _plat_label(p): [
330            tmpl.format(d)
331            for d in sorted(deps)
332        ]
333        for p, deps in sorted(deps_by_platform.items())
334    }
335
336    # Add the default, which means that we will be just using the dependencies in
337    # `deps` for platforms that are not handled in a special way by the packages
338    deps_by_platform.setdefault("//conditions:default", [])
339
340    if not deps:
341        return select(deps_by_platform)
342    else:
343        return deps + select(deps_by_platform)
344