From 462399013e6dc32ba2a8e38e0adce50a58d92361 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 28 Oct 2024 11:22:17 +0900 Subject: [PATCH] refactor(pypi): use a macro to define whl_library targets (#2347) Summary: - refactor: Start implementing whl_library_targets - refactor: start using whl_library_targets macro - refactor: generate config settings in the new macro - refactor: copy_files in the new macro - refactor: move entry_point generation to the macro - refactor: move the py_library and whl generation to the new macro This makes the code more maintainable by reducing the amount of tests that are comparing BUILD.bazel outputs. --- examples/bzlmod/MODULE.bazel.lock | 4 +- python/private/pypi/BUILD.bazel | 3 +- .../pypi/generate_whl_library_build_bazel.bzl | 403 +----------- python/private/pypi/whl_library.bzl | 2 +- python/private/pypi/whl_library_targets.bzl | 343 +++++++++++ ...generate_whl_library_build_bazel_tests.bzl | 579 ++---------------- tests/pypi/whl_library_targets/BUILD.bazel | 5 + .../whl_library_targets_tests.bzl | 349 +++++++++++ 8 files changed, 786 insertions(+), 902 deletions(-) create mode 100644 python/private/pypi/whl_library_targets.bzl create mode 100644 tests/pypi/whl_library_targets/BUILD.bazel create mode 100644 tests/pypi/whl_library_targets/whl_library_targets_tests.bzl diff --git a/examples/bzlmod/MODULE.bazel.lock b/examples/bzlmod/MODULE.bazel.lock index 286e8c0b01..c41380c6be 100644 --- a/examples/bzlmod/MODULE.bazel.lock +++ b/examples/bzlmod/MODULE.bazel.lock @@ -1392,7 +1392,7 @@ }, "@@rules_python~//python/extensions:pip.bzl%pip": { "general": { - "bzlTransitiveDigest": "KZzbwT5y7SPbM+MgbQWr309EUGjGXvXvQ4/FMn+fEGE=", + "bzlTransitiveDigest": "g9NnJTZcM2BjPelxHHLy0ZyhFd+8XAb86u9OvNIOhFo=", "usagesDigest": "MChlcSw99EuW3K7OOoMcXQIdcJnEh6YmfyjJm+9mxIg=", "recordedFileInputs": { "@@other_module~//requirements_lock_3_11.txt": "a7d0061366569043d5efcf80e34a32c732679367cb3c831c4cdc606adc36d314", @@ -6299,7 +6299,7 @@ }, "@@rules_python~//python/private/pypi:pip.bzl%pip_internal": { "general": { - "bzlTransitiveDigest": "mzsyVW4M380vwEPTn/pDXFMh5gtTHsv0sbqZCE7a1SY=", + "bzlTransitiveDigest": "ctc7nzMsQfNG16wSXLqbix2k99rf614qJRwcd/2RxGI=", "usagesDigest": "LYtSAPzhPjmfD9vF39mCED1UQSvHEo2Hv+aK5Z4ZWWc=", "recordedFileInputs": { "@@rules_python~//tools/publish/requirements_linux.txt": "8175b4c8df50ae2f22d1706961884beeb54e7da27bd2447018314a175981997d", diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index e76f9d36b1..9be355c0c3 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -110,8 +110,7 @@ bzl_library( name = "generate_whl_library_build_bazel_bzl", srcs = ["generate_whl_library_build_bazel.bzl"], deps = [ - ":labels_bzl", - "//python/private:normalize_name_bzl", + "//python/private:text_util_bzl", ], ) diff --git a/python/private/pypi/generate_whl_library_build_bazel.bzl b/python/private/pypi/generate_whl_library_build_bazel.bzl index 934fa00c69..8050cd22ad 100644 --- a/python/private/pypi/generate_whl_library_build_bazel.bzl +++ b/python/private/pypi/generate_whl_library_build_bazel.bzl @@ -14,406 +14,69 @@ """Generate the BUILD.bazel contents for a repo defined by a whl_library.""" -load("//python/private:normalize_name.bzl", "normalize_name") load("//python/private:text_util.bzl", "render") -load( - ":labels.bzl", - "DATA_LABEL", - "DIST_INFO_LABEL", - "PY_LIBRARY_IMPL_LABEL", - "PY_LIBRARY_PUBLIC_LABEL", - "WHEEL_ENTRY_POINT_PREFIX", - "WHEEL_FILE_IMPL_LABEL", - "WHEEL_FILE_PUBLIC_LABEL", -) - -_COPY_FILE_TEMPLATE = """\ -copy_file( - name = "{dest}.copy", - src = "{src}", - out = "{dest}", - is_executable = {is_executable}, -) -""" -_ENTRY_POINT_RULE_TEMPLATE = """\ -py_binary( - name = "{name}", - srcs = ["{src}"], - # This makes this directory a top-level in the python import - # search path for anything that depends on this. - imports = ["."], - deps = ["{pkg}"], -) -""" - -_BUILD_TEMPLATE = """\ -{loads} +_RENDER = { + "copy_executables": render.dict, + "copy_files": render.dict, + "data": render.list, + "data_exclude": render.list, + "dependencies": render.list, + "dependencies_by_platform": lambda x: render.dict(x, value_repr = render.list), + "entry_points": render.dict, + "group_deps": render.list, + "srcs_exclude": render.list, + "tags": render.list, +} + +# NOTE @aignas 2024-10-25: We have to keep this so that files in +# this repository can be publicly visible without the need for +# export_files +_TEMPLATE = """\ +load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets") package(default_visibility = ["//visibility:public"]) -filegroup( - name = "{dist_info_label}", - srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True), -) - -filegroup( - name = "{data_label}", - srcs = glob(["data/**"], allow_empty = True), -) - -filegroup( - name = "{whl_file_label}", - srcs = ["{whl_name}"], - data = {whl_file_deps}, - visibility = {impl_vis}, -) - -py_library( - name = "{py_library_label}", - srcs = glob( - ["site-packages/**/*.py"], - exclude={srcs_exclude}, - # Empty sources are allowed to support wheels that don't have any - # pure-Python code, e.g. pymssql, which is written in Cython. - allow_empty = True, - ), - data = {data} + glob( - ["site-packages/**/*"], - exclude={data_exclude}, - ), - # This makes this directory a top-level in the python import - # search path for anything that depends on this. - imports = ["site-packages"], - deps = {dependencies}, - tags = {tags}, - visibility = {impl_vis}, +whl_library_targets( +{kwargs} ) """ -def _plat_label(plat): - if plat.endswith("default"): - return plat - if plat.startswith("@//"): - return "@@" + str(Label("//:BUILD.bazel")).partition("//")[0].strip("@") + plat.strip("@") - elif plat.startswith("@"): - return str(Label(plat)) - else: - return ":is_" + plat.replace("cp3", "python_3.") - -def _render_list_and_select(deps, deps_by_platform, tmpl): - deps = render.list([tmpl.format(d) for d in sorted(deps)]) - - if not deps_by_platform: - return deps - - deps_by_platform = { - _plat_label(p): [ - tmpl.format(d) - for d in sorted(deps) - ] - for p, deps in sorted(deps_by_platform.items()) - } - - # Add the default, which means that we will be just using the dependencies in - # `deps` for platforms that are not handled in a special way by the packages - deps_by_platform.setdefault("//conditions:default", []) - deps_by_platform = render.select(deps_by_platform, value_repr = render.list) - - if deps == "[]": - return deps_by_platform - else: - return "{} + {}".format(deps, deps_by_platform) - -def _render_config_settings(dependencies_by_platform): - loads = [] - additional_content = [] - for p in dependencies_by_platform: - # p can be one of the following formats: - # * //conditions:default - # * @platforms//os:{value} - # * @platforms//cpu:{value} - # * @//python/config_settings:is_python_3.{minor_version} - # * {os}_{cpu} - # * cp3{minor_version}_{os}_{cpu} - if p.startswith("@") or p.endswith("default"): - continue - - abi, _, tail = p.partition("_") - if not abi.startswith("cp"): - tail = p - abi = "" - - os, _, arch = tail.partition("_") - os = "" if os == "anyos" else os - arch = "" if arch == "anyarch" else arch - - constraint_values = [] - if arch: - constraint_values.append("@platforms//cpu:{}".format(arch)) - if os: - constraint_values.append("@platforms//os:{}".format(os)) - - constraint_values_str = render.indent(render.list(constraint_values)).lstrip() - - if abi: - additional_content.append( - """\ -config_setting( - name = "is_{name}", - flag_values = {{ - "@rules_python//python/config_settings:python_version_major_minor": "3.{minor_version}", - }}, - constraint_values = {constraint_values}, - visibility = ["//visibility:private"], -)""".format( - name = p.replace("cp3", "python_3."), - minor_version = abi[len("cp3"):], - constraint_values = constraint_values_str, - ), - ) - else: - additional_content.append( - """\ -config_setting( - name = "is_{name}", - constraint_values = {constraint_values}, - visibility = ["//visibility:private"], -)""".format( - name = p.replace("cp3", "python_3."), - constraint_values = constraint_values_str, - ), - ) - - return loads, "\n\n".join(additional_content) - def generate_whl_library_build_bazel( *, - dep_template, - whl_name, - dependencies, - dependencies_by_platform, - data_exclude, - tags, - entry_points, annotation = None, - group_name = None, - group_deps = []): + **kwargs): """Generate a BUILD file for an unzipped Wheel Args: - dep_template: the dependency template that should be used for dependency lists. - whl_name: the whl_name that this is generated for. - dependencies: a list of PyPI packages that are dependencies to the py_library. - dependencies_by_platform: a dict[str, list] of PyPI packages that may vary by platform. - data_exclude: more patterns to exclude from the data attribute of generated py_library rules. - tags: list of tags to apply to generated py_library rules. - entry_points: A dict of entry points to add py_binary rules for. annotation: The annotation for the build file. - group_name: Optional[str]; name of the dependency group (if any) which contains this library. - If set, this library will behave as a shim to group implementation rules which will provide - simultaneously installed dependencies which would otherwise form a cycle. - group_deps: List[str]; names of fellow members of the group (if any). These will be excluded - from generated deps lists so as to avoid direct cycles. These dependencies will be provided - at runtime by the group rules which wrap this library and its fellows together. + **kwargs: Extra args serialized to be passed to the + {obj}`whl_library_targets`. Returns: A complete BUILD file as a string """ additional_content = [] - data = [] - srcs_exclude = [] - data_exclude = [] + data_exclude - dependencies = sorted([normalize_name(d) for d in dependencies]) - dependencies_by_platform = { - platform: sorted([normalize_name(d) for d in deps]) - for platform, deps in dependencies_by_platform.items() - } - tags = sorted(tags) - - for entry_point, entry_point_script_name in entry_points.items(): - additional_content.append( - _generate_entry_point_rule( - name = "{}_{}".format(WHEEL_ENTRY_POINT_PREFIX, entry_point), - script = entry_point_script_name, - pkg = ":" + PY_LIBRARY_PUBLIC_LABEL, - ), - ) - if annotation: - for src, dest in annotation.copy_files.items(): - data.append(dest) - additional_content.append(_generate_copy_commands(src, dest)) - for src, dest in annotation.copy_executables.items(): - data.append(dest) - additional_content.append( - _generate_copy_commands(src, dest, is_executable = True), - ) - data.extend(annotation.data) - data_exclude.extend(annotation.data_exclude_glob) - srcs_exclude.extend(annotation.srcs_exclude_glob) + kwargs["data"] = annotation.data + kwargs["copy_files"] = annotation.copy_files + kwargs["copy_executables"] = annotation.copy_executables + kwargs["data_exclude"] = kwargs.get("data_exclude", []) + annotation.data_exclude_glob + kwargs["srcs_exclude"] = annotation.srcs_exclude_glob if annotation.additive_build_content: additional_content.append(annotation.additive_build_content) - _data_exclude = [ - "**/* *", - "**/*.py", - "**/*.pyc", - "**/*.pyc.*", # During pyc creation, temp files named *.pyc.NNNN are created - # RECORD is known to contain sha256 checksums of files which might include the checksums - # of generated files produced when wheels are installed. The file is ignored to avoid - # Bazel caching issues. - "**/*.dist-info/RECORD", - ] - for item in data_exclude: - if item not in _data_exclude: - _data_exclude.append(item) - - # Ensure this list is normalized - # Note: mapping used as set - group_deps = { - normalize_name(d): True - for d in group_deps - } - - dependencies = [ - d - for d in dependencies - if d not in group_deps - ] - dependencies_by_platform = { - p: deps - for p, deps in dependencies_by_platform.items() - for deps in [[d for d in deps if d not in group_deps]] - if deps - } - - loads = [ - """load("@rules_python//python:defs.bzl", "py_library", "py_binary")""", - """load("@bazel_skylib//rules:copy_file.bzl", "copy_file")""", - ] - - loads_, config_settings_content = _render_config_settings(dependencies_by_platform) - if config_settings_content: - for line in loads_: - if line not in loads: - loads.append(line) - additional_content.append(config_settings_content) - - lib_dependencies = _render_list_and_select( - deps = dependencies, - deps_by_platform = dependencies_by_platform, - tmpl = dep_template.format(name = "{}", target = PY_LIBRARY_PUBLIC_LABEL), - ) - - whl_file_deps = _render_list_and_select( - deps = dependencies, - deps_by_platform = dependencies_by_platform, - tmpl = dep_template.format(name = "{}", target = WHEEL_FILE_PUBLIC_LABEL), - ) - - # If this library is a member of a group, its public label aliases need to - # point to the group implementation rule not the implementation rules. We - # also need to mark the implementation rules as visible to the group - # implementation. - if group_name and "//:" in dep_template: - # This is the legacy behaviour where the group library is outside the hub repo - label_tmpl = dep_template.format( - name = "_groups", - target = normalize_name(group_name) + "_{}", - ) - impl_vis = [dep_template.format( - name = "_groups", - target = "__pkg__", - )] - additional_content.extend([ - "", - render.alias( - name = PY_LIBRARY_PUBLIC_LABEL, - actual = repr(label_tmpl.format(PY_LIBRARY_PUBLIC_LABEL)), - ), - "", - render.alias( - name = WHEEL_FILE_PUBLIC_LABEL, - actual = repr(label_tmpl.format(WHEEL_FILE_PUBLIC_LABEL)), - ), - ]) - py_library_label = PY_LIBRARY_IMPL_LABEL - whl_file_label = WHEEL_FILE_IMPL_LABEL - - elif group_name: - py_library_label = PY_LIBRARY_PUBLIC_LABEL - whl_file_label = WHEEL_FILE_PUBLIC_LABEL - impl_vis = [dep_template.format(name = "", target = "__subpackages__")] - - else: - py_library_label = PY_LIBRARY_PUBLIC_LABEL - whl_file_label = WHEEL_FILE_PUBLIC_LABEL - impl_vis = ["//visibility:public"] - contents = "\n".join( [ - _BUILD_TEMPLATE.format( - loads = "\n".join(sorted(loads)), - py_library_label = py_library_label, - dependencies = render.indent(lib_dependencies, " " * 4).lstrip(), - whl_file_deps = render.indent(whl_file_deps, " " * 4).lstrip(), - data_exclude = repr(_data_exclude), - whl_name = whl_name, - whl_file_label = whl_file_label, - tags = repr(tags), - data_label = DATA_LABEL, - dist_info_label = DIST_INFO_LABEL, - entry_point_prefix = WHEEL_ENTRY_POINT_PREFIX, - srcs_exclude = repr(srcs_exclude), - data = repr(data), - impl_vis = repr(impl_vis), + _TEMPLATE.format( + kwargs = render.indent("\n".join([ + "{} = {},".format(k, _RENDER.get(k, repr)(v)) + for k, v in sorted(kwargs.items()) + ])), ), ] + additional_content, ) # NOTE: Ensure that we terminate with a new line return contents.rstrip() + "\n" - -def _generate_copy_commands(src, dest, is_executable = False): - """Generate a [@bazel_skylib//rules:copy_file.bzl%copy_file][cf] target - - [cf]: https://github.com/bazelbuild/bazel-skylib/blob/1.1.1/docs/copy_file_doc.md - - Args: - src (str): The label for the `src` attribute of [copy_file][cf] - dest (str): The label for the `out` attribute of [copy_file][cf] - is_executable (bool, optional): Whether or not the file being copied is executable. - sets `is_executable` for [copy_file][cf] - - Returns: - str: A `copy_file` instantiation. - """ - return _COPY_FILE_TEMPLATE.format( - src = src, - dest = dest, - is_executable = is_executable, - ) - -def _generate_entry_point_rule(*, name, script, pkg): - """Generate a Bazel `py_binary` rule for an entry point script. - - Note that the script is used to determine the name of the target. The name of - entry point targets should be uniuqe to avoid conflicts with existing sources or - directories within a wheel. - - Args: - name (str): The name of the generated py_binary. - script (str): The path to the entry point's python file. - pkg (str): The package owning the entry point. This is expected to - match up with the `py_library` defined for each repository. - - Returns: - str: A `py_binary` instantiation. - """ - return _ENTRY_POINT_RULE_TEMPLATE.format( - name = name, - src = script.replace("\\", "/"), - pkg = pkg, - ) diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 82fe072655..62c0c6ded5 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -332,8 +332,8 @@ def _whl_library_impl(rctx): entry_points[entry_point_without_py] = entry_point_script_name build_file_contents = generate_whl_library_build_bazel( + name = whl_path.basename, dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format(rctx.attr.repo_prefix), - whl_name = whl_path.basename, dependencies = metadata["deps"], dependencies_by_platform = metadata["deps_by_platform"], group_name = rctx.attr.group_name, diff --git a/python/private/pypi/whl_library_targets.bzl b/python/private/pypi/whl_library_targets.bzl new file mode 100644 index 0000000000..1798b9d775 --- /dev/null +++ b/python/private/pypi/whl_library_targets.bzl @@ -0,0 +1,343 @@ +# Copyright 2024 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Macro to generate all of the targets present in a {obj}`whl_library`.""" + +load("@bazel_skylib//rules:copy_file.bzl", "copy_file") +load("//python:py_binary.bzl", "py_binary") +load("//python:py_library.bzl", "py_library") +load("//python/private:normalize_name.bzl", "normalize_name") +load( + ":labels.bzl", + "DATA_LABEL", + "DIST_INFO_LABEL", + "PY_LIBRARY_IMPL_LABEL", + "PY_LIBRARY_PUBLIC_LABEL", + "WHEEL_ENTRY_POINT_PREFIX", + "WHEEL_FILE_IMPL_LABEL", + "WHEEL_FILE_PUBLIC_LABEL", +) + +def whl_library_targets( + *, + name, + dep_template, + data_exclude = [], + srcs_exclude = [], + tags = [], + filegroups = { + DIST_INFO_LABEL: ["site-packages/*.dist-info/**"], + DATA_LABEL: ["data/**"], + }, + dependencies = [], + dependencies_by_platform = {}, + group_deps = [], + group_name = "", + data = [], + copy_files = {}, + copy_executables = {}, + entry_points = {}, + native = native, + rules = struct( + copy_file = copy_file, + py_binary = py_binary, + py_library = py_library, + )): + """Create all of the whl_library targets. + + Args: + name: {type}`str` The file to match for including it into the `whl` + filegroup. This may be also parsed to generate extra metadata. + dep_template: {type}`str` The dep_template to use for dependency + interpolation. + tags: {type}`list[str]` The tags set on the `py_library`. + dependencies: {type}`list[str]` A list of dependencies. + dependencies_by_platform: {type}`dict[str, list[str]]` A list of + dependencies by platform key. + filegroups: {type}`dict[str, list[str]]` A dictionary of the target + names and the glob matches. + group_name: {type}`str` name of the dependency group (if any) which + contains this library. If set, this library will behave as a shim + to group implementation rules which will provide simultaneously + installed dependencies which would otherwise form a cycle. + group_deps: {type}`list[str]` names of fellow members of the group (if + any). These will be excluded from generated deps lists so as to avoid + direct cycles. These dependencies will be provided at runtime by the + group rules which wrap this library and its fellows together. + copy_executables: {type}`dict[str, str]` The mapping between src and + dest locations for the targets. + copy_files: {type}`dict[str, str]` The mapping between src and + dest locations for the targets. + data_exclude: {type}`list[str]` The globs for data attribute exclusion + in `py_library`. + srcs_exclude: {type}`list[str]` The globs for srcs attribute exclusion + in `py_library`. + data: {type}`list[str]` A list of labels to include as part of the `data` attribute in `py_library`. + entry_points: {type}`dict[str, str]` The mapping between the script + name and the python file to use. DEPRECATED. + native: {type}`native` The native struct for overriding in tests. + rules: {type}`struct` A struct with references to rules for creating targets. + """ + _ = name # buildifier: @unused + + dependencies = sorted([normalize_name(d) for d in dependencies]) + dependencies_by_platform = { + platform: sorted([normalize_name(d) for d in deps]) + for platform, deps in dependencies_by_platform.items() + } + tags = sorted(tags) + data = [] + data + + for filegroup_name, glob in filegroups.items(): + native.filegroup( + name = filegroup_name, + srcs = native.glob(glob, allow_empty = True), + visibility = ["//visibility:public"], + ) + + for src, dest in copy_files.items(): + rules.copy_file( + name = dest + ".copy", + src = src, + out = dest, + visibility = ["//visibility:public"], + ) + data.append(dest) + for src, dest in copy_executables.items(): + rules.copy_file( + name = dest + ".copy", + src = src, + out = dest, + is_executable = True, + visibility = ["//visibility:public"], + ) + data.append(dest) + + _config_settings( + dependencies_by_platform.keys(), + native = native, + visibility = ["//visibility:private"], + ) + + # TODO @aignas 2024-10-25: remove the entry_point generation once + # `py_console_script_binary` is the only way to use entry points. + for entry_point, entry_point_script_name in entry_points.items(): + rules.py_binary( + name = "{}_{}".format(WHEEL_ENTRY_POINT_PREFIX, entry_point), + # Ensure that this works on Windows as well - script may have Windows path separators. + srcs = [entry_point_script_name.replace("\\", "/")], + # This makes this directory a top-level in the python import + # search path for anything that depends on this. + imports = ["."], + deps = [":" + PY_LIBRARY_PUBLIC_LABEL], + visibility = ["//visibility:public"], + ) + + # Ensure this list is normalized + # Note: mapping used as set + group_deps = { + normalize_name(d): True + for d in group_deps + } + + dependencies = [ + d + for d in dependencies + if d not in group_deps + ] + dependencies_by_platform = { + p: deps + for p, deps in dependencies_by_platform.items() + for deps in [[d for d in deps if d not in group_deps]] + if deps + } + + # If this library is a member of a group, its public label aliases need to + # point to the group implementation rule not the implementation rules. We + # also need to mark the implementation rules as visible to the group + # implementation. + if group_name and "//:" in dep_template: + # This is the legacy behaviour where the group library is outside the hub repo + label_tmpl = dep_template.format( + name = "_groups", + target = normalize_name(group_name) + "_{}", + ) + impl_vis = [dep_template.format( + name = "_groups", + target = "__pkg__", + )] + + native.alias( + name = PY_LIBRARY_PUBLIC_LABEL, + actual = label_tmpl.format(PY_LIBRARY_PUBLIC_LABEL), + visibility = ["//visibility:public"], + ) + native.alias( + name = WHEEL_FILE_PUBLIC_LABEL, + actual = label_tmpl.format(WHEEL_FILE_PUBLIC_LABEL), + visibility = ["//visibility:public"], + ) + py_library_label = PY_LIBRARY_IMPL_LABEL + whl_file_label = WHEEL_FILE_IMPL_LABEL + + elif group_name: + py_library_label = PY_LIBRARY_PUBLIC_LABEL + whl_file_label = WHEEL_FILE_PUBLIC_LABEL + impl_vis = [dep_template.format(name = "", target = "__subpackages__")] + + else: + py_library_label = PY_LIBRARY_PUBLIC_LABEL + whl_file_label = WHEEL_FILE_PUBLIC_LABEL + impl_vis = ["//visibility:public"] + + if hasattr(native, "filegroup"): + native.filegroup( + name = whl_file_label, + srcs = [name], + data = _deps( + deps = dependencies, + deps_by_platform = dependencies_by_platform, + tmpl = dep_template.format(name = "{}", target = WHEEL_FILE_PUBLIC_LABEL), + # NOTE @aignas 2024-10-28: Actually, `select` is not part of + # `native`, but in order to support bazel 6.4 in unit tests, I + # have to somehow pass the `select` implementation in the unit + # tests and I chose this to be routed through the `native` + # struct. So, tests` will be successful in `getattr` and the + # real code will use the fallback provided here. + select = getattr(native, "select", select), + ), + visibility = impl_vis, + ) + + if hasattr(rules, "py_library"): + _data_exclude = [ + "**/* *", + "**/*.py", + "**/*.pyc", + "**/*.pyc.*", # During pyc creation, temp files named *.pyc.NNNN are created + # RECORD is known to contain sha256 checksums of files which might include the checksums + # of generated files produced when wheels are installed. The file is ignored to avoid + # Bazel caching issues. + "**/*.dist-info/RECORD", + ] + for item in data_exclude: + if item not in _data_exclude: + _data_exclude.append(item) + + rules.py_library( + name = py_library_label, + srcs = native.glob( + ["site-packages/**/*.py"], + exclude = srcs_exclude, + # Empty sources are allowed to support wheels that don't have any + # pure-Python code, e.g. pymssql, which is written in Cython. + allow_empty = True, + ), + data = data + native.glob( + ["site-packages/**/*"], + exclude = _data_exclude, + ), + # This makes this directory a top-level in the python import + # search path for anything that depends on this. + imports = ["site-packages"], + deps = _deps( + deps = dependencies, + deps_by_platform = dependencies_by_platform, + tmpl = dep_template.format(name = "{}", target = PY_LIBRARY_PUBLIC_LABEL), + select = getattr(native, "select", select), + ), + tags = tags, + visibility = impl_vis, + ) + +def _config_settings(dependencies_by_platform, native = native, **kwargs): + """Generate config settings for the targets. + + Args: + dependencies_by_platform: {type}`list[str]` platform keys, can be + one of the following formats: + * `//conditions:default` + * `@platforms//os:{value}` + * `@platforms//cpu:{value}` + * `@//python/config_settings:is_python_3.{minor_version}` + * `{os}_{cpu}` + * `cp3{minor_version}_{os}_{cpu}` + native: {type}`native` The native struct for overriding in tests. + **kwargs: Extra kwargs to pass to the rule. + """ + for p in dependencies_by_platform: + if p.startswith("@") or p.endswith("default"): + continue + + abi, _, tail = p.partition("_") + if not abi.startswith("cp"): + tail = p + abi = "" + + os, _, arch = tail.partition("_") + os = "" if os == "anyos" else os + arch = "" if arch == "anyarch" else arch + + _kwargs = dict(kwargs) + if arch: + _kwargs.setdefault("constraint_values", []).append("@platforms//cpu:{}".format(arch)) + if os: + _kwargs.setdefault("constraint_values", []).append("@platforms//os:{}".format(os)) + + if abi: + _kwargs["flag_values"] = { + "@rules_python//python/config_settings:python_version_major_minor": "3.{minor_version}".format( + minor_version = abi[len("cp3"):], + ), + } + + native.config_setting( + name = "is_{name}".format( + name = p.replace("cp3", "python_3."), + ), + **_kwargs + ) + +def _plat_label(plat): + if plat.endswith("default"): + return plat + elif plat.startswith("@//"): + return Label(plat.strip("@")) + elif plat.startswith("@"): + return plat + else: + return ":is_" + plat.replace("cp3", "python_3.") + +def _deps(deps, deps_by_platform, tmpl, select = select): + deps = [tmpl.format(d) for d in sorted(deps)] + + if not deps_by_platform: + return deps + + deps_by_platform = { + _plat_label(p): [ + tmpl.format(d) + for d in sorted(deps) + ] + for p, deps in sorted(deps_by_platform.items()) + } + + # Add the default, which means that we will be just using the dependencies in + # `deps` for platforms that are not handled in a special way by the packages + deps_by_platform.setdefault("//conditions:default", []) + + if not deps: + return select(deps_by_platform) + else: + return deps + select(deps_by_platform) diff --git a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl index 94530117cd..b0d8f6d17e 100644 --- a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl +++ b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl @@ -19,560 +19,85 @@ load("//python/private/pypi:generate_whl_library_build_bazel.bzl", "generate_whl _tests = [] -def _test_simple(env): +def _test_all(env): want = """\ -load("@bazel_skylib//rules:copy_file.bzl", "copy_file") -load("@rules_python//python:defs.bzl", "py_library", "py_binary") +load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets") package(default_visibility = ["//visibility:public"]) -filegroup( - name = "dist_info", - srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True), -) - -filegroup( - name = "data", - srcs = glob(["data/**"], allow_empty = True), -) - -filegroup( - name = "whl", - srcs = ["foo.whl"], - data = [ - "@pypi_bar_baz//:whl", - "@pypi_foo//:whl", - ], - visibility = ["//visibility:public"], -) - -py_library( - name = "pkg", - srcs = glob( - ["site-packages/**/*.py"], - exclude=[], - # Empty sources are allowed to support wheels that don't have any - # pure-Python code, e.g. pymssql, which is written in Cython. - allow_empty = True, - ), - data = [] + glob( - ["site-packages/**/*"], - exclude=["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD"], - ), - # This makes this directory a top-level in the python import - # search path for anything that depends on this. - imports = ["site-packages"], - deps = [ - "@pypi_bar_baz//:pkg", - "@pypi_foo//:pkg", - ], - tags = ["tag1", "tag2"], - visibility = ["//visibility:public"], -) -""" - actual = generate_whl_library_build_bazel( - dep_template = "@pypi_{name}//:{target}", - whl_name = "foo.whl", - dependencies = ["foo", "bar-baz"], - dependencies_by_platform = {}, - data_exclude = [], - tags = ["tag1", "tag2"], - entry_points = {}, - annotation = None, - ) - env.expect.that_str(actual).equals(want) - -_tests.append(_test_simple) - -def _test_dep_selects(env): - want = """\ -load("@bazel_skylib//rules:copy_file.bzl", "copy_file") -load("@rules_python//python:defs.bzl", "py_library", "py_binary") - -package(default_visibility = ["//visibility:public"]) - -filegroup( - name = "dist_info", - srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True), -) - -filegroup( - name = "data", - srcs = glob(["data/**"], allow_empty = True), -) - -filegroup( - name = "whl", - srcs = ["foo.whl"], - data = [ - "@pypi_bar_baz//:whl", - "@pypi_foo//:whl", - ] + select( - { - "@//python/config_settings:is_python_3.9": ["@pypi_py39_dep//:whl"], - "@platforms//cpu:aarch64": ["@pypi_arm_dep//:whl"], - "@platforms//os:windows": ["@pypi_win_dep//:whl"], - ":is_python_3.10_linux_ppc": ["@pypi_py310_linux_ppc_dep//:whl"], - ":is_python_3.9_anyos_aarch64": ["@pypi_py39_arm_dep//:whl"], - ":is_python_3.9_linux_anyarch": ["@pypi_py39_linux_dep//:whl"], - ":is_linux_x86_64": ["@pypi_linux_intel_dep//:whl"], - "//conditions:default": [], - }, - ), - visibility = ["//visibility:public"], -) - -py_library( - name = "pkg", - srcs = glob( - ["site-packages/**/*.py"], - exclude=[], - # Empty sources are allowed to support wheels that don't have any - # pure-Python code, e.g. pymssql, which is written in Cython. - allow_empty = True, - ), - data = [] + glob( - ["site-packages/**/*"], - exclude=["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD"], - ), - # This makes this directory a top-level in the python import - # search path for anything that depends on this. - imports = ["site-packages"], - deps = [ - "@pypi_bar_baz//:pkg", - "@pypi_foo//:pkg", - ] + select( - { - "@//python/config_settings:is_python_3.9": ["@pypi_py39_dep//:pkg"], - "@platforms//cpu:aarch64": ["@pypi_arm_dep//:pkg"], - "@platforms//os:windows": ["@pypi_win_dep//:pkg"], - ":is_python_3.10_linux_ppc": ["@pypi_py310_linux_ppc_dep//:pkg"], - ":is_python_3.9_anyos_aarch64": ["@pypi_py39_arm_dep//:pkg"], - ":is_python_3.9_linux_anyarch": ["@pypi_py39_linux_dep//:pkg"], - ":is_linux_x86_64": ["@pypi_linux_intel_dep//:pkg"], - "//conditions:default": [], - }, - ), - tags = ["tag1", "tag2"], - visibility = ["//visibility:public"], -) - -config_setting( - name = "is_python_3.10_linux_ppc", - flag_values = { - "@rules_python//python/config_settings:python_version_major_minor": "3.10", +whl_library_targets( + copy_executables = { + "exec_src": "exec_dest", + }, + copy_files = { + "file_src": "file_dest", }, - constraint_values = [ - "@platforms//cpu:ppc", - "@platforms//os:linux", + data = ["extra_target"], + data_exclude = [ + "exclude_via_attr", + "data_exclude_all", ], - visibility = ["//visibility:private"], -) - -config_setting( - name = "is_python_3.9_anyos_aarch64", - flag_values = { - "@rules_python//python/config_settings:python_version_major_minor": "3.9", + dep_template = "@pypi//{name}:{target}", + dependencies = [ + "foo", + "bar-baz", + "qux", + ], + dependencies_by_platform = { + "linux_x86_64": [ + "box", + "box-amd64", + ], + "windows_x86_64": ["fox"], + "@platforms//os:linux": ["box"], }, - constraint_values = ["@platforms//cpu:aarch64"], - visibility = ["//visibility:private"], -) - -config_setting( - name = "is_python_3.9_linux_anyarch", - flag_values = { - "@rules_python//python/config_settings:python_version_major_minor": "3.9", + entry_points = { + "foo": "bar.py", }, - constraint_values = ["@platforms//os:linux"], - visibility = ["//visibility:private"], -) - -config_setting( - name = "is_linux_x86_64", - constraint_values = [ - "@platforms//cpu:x86_64", - "@platforms//os:linux", + group_deps = [ + "foo", + "fox", + "qux", ], - visibility = ["//visibility:private"], -) -""" - actual = generate_whl_library_build_bazel( - dep_template = "@pypi_{name}//:{target}", - whl_name = "foo.whl", - dependencies = ["foo", "bar-baz"], - dependencies_by_platform = { - "@//python/config_settings:is_python_3.9": ["py39_dep"], - "@platforms//cpu:aarch64": ["arm_dep"], - "@platforms//os:windows": ["win_dep"], - "cp310_linux_ppc": ["py310_linux_ppc_dep"], - "cp39_anyos_aarch64": ["py39_arm_dep"], - "cp39_linux_anyarch": ["py39_linux_dep"], - "linux_x86_64": ["linux_intel_dep"], - }, - data_exclude = [], - tags = ["tag1", "tag2"], - entry_points = {}, - annotation = None, - ) - env.expect.that_str(actual.replace("@@", "@")).equals(want) - -_tests.append(_test_dep_selects) - -def _test_with_annotation(env): - want = """\ -load("@bazel_skylib//rules:copy_file.bzl", "copy_file") -load("@rules_python//python:defs.bzl", "py_library", "py_binary") - -package(default_visibility = ["//visibility:public"]) - -filegroup( - name = "dist_info", - srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True), -) - -filegroup( - name = "data", - srcs = glob(["data/**"], allow_empty = True), -) - -filegroup( - name = "whl", - srcs = ["foo.whl"], - data = [ - "@pypi_bar_baz//:whl", - "@pypi_foo//:whl", + group_name = "qux", + name = "foo.whl", + srcs_exclude = ["srcs_exclude_all"], + tags = [ + "tag2", + "tag1", ], - visibility = ["//visibility:public"], -) - -py_library( - name = "pkg", - srcs = glob( - ["site-packages/**/*.py"], - exclude=["srcs_exclude_all"], - # Empty sources are allowed to support wheels that don't have any - # pure-Python code, e.g. pymssql, which is written in Cython. - allow_empty = True, - ), - data = ["file_dest", "exec_dest"] + glob( - ["site-packages/**/*"], - exclude=["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD", "data_exclude_all"], - ), - # This makes this directory a top-level in the python import - # search path for anything that depends on this. - imports = ["site-packages"], - deps = [ - "@pypi_bar_baz//:pkg", - "@pypi_foo//:pkg", - ], - tags = ["tag1", "tag2"], - visibility = ["//visibility:public"], -) - -copy_file( - name = "file_dest.copy", - src = "file_src", - out = "file_dest", - is_executable = False, -) - -copy_file( - name = "exec_dest.copy", - src = "exec_src", - out = "exec_dest", - is_executable = True, ) # SOMETHING SPECIAL AT THE END -""" - actual = generate_whl_library_build_bazel( - dep_template = "@pypi_{name}//:{target}", - whl_name = "foo.whl", - dependencies = ["foo", "bar-baz"], - dependencies_by_platform = {}, - data_exclude = [], - tags = ["tag1", "tag2"], - entry_points = {}, - annotation = struct( - copy_files = {"file_src": "file_dest"}, - copy_executables = {"exec_src": "exec_dest"}, - data = [], - data_exclude_glob = ["data_exclude_all"], - srcs_exclude_glob = ["srcs_exclude_all"], - additive_build_content = """# SOMETHING SPECIAL AT THE END""", - ), - ) - env.expect.that_str(actual).equals(want) - -_tests.append(_test_with_annotation) - -def _test_with_entry_points(env): - want = """\ -load("@bazel_skylib//rules:copy_file.bzl", "copy_file") -load("@rules_python//python:defs.bzl", "py_library", "py_binary") - -package(default_visibility = ["//visibility:public"]) - -filegroup( - name = "dist_info", - srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True), -) - -filegroup( - name = "data", - srcs = glob(["data/**"], allow_empty = True), -) - -filegroup( - name = "whl", - srcs = ["foo.whl"], - data = [ - "@pypi_bar_baz//:whl", - "@pypi_foo//:whl", - ], - visibility = ["//visibility:public"], -) - -py_library( - name = "pkg", - srcs = glob( - ["site-packages/**/*.py"], - exclude=[], - # Empty sources are allowed to support wheels that don't have any - # pure-Python code, e.g. pymssql, which is written in Cython. - allow_empty = True, - ), - data = [] + glob( - ["site-packages/**/*"], - exclude=["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD"], - ), - # This makes this directory a top-level in the python import - # search path for anything that depends on this. - imports = ["site-packages"], - deps = [ - "@pypi_bar_baz//:pkg", - "@pypi_foo//:pkg", - ], - tags = ["tag1", "tag2"], - visibility = ["//visibility:public"], -) - -py_binary( - name = "rules_python_wheel_entry_point_fizz", - srcs = ["buzz.py"], - # This makes this directory a top-level in the python import - # search path for anything that depends on this. - imports = ["."], - deps = [":pkg"], -) -""" - actual = generate_whl_library_build_bazel( - dep_template = "@pypi_{name}//:{target}", - whl_name = "foo.whl", - dependencies = ["foo", "bar-baz"], - dependencies_by_platform = {}, - data_exclude = [], - tags = ["tag1", "tag2"], - entry_points = {"fizz": "buzz.py"}, - annotation = None, - ) - env.expect.that_str(actual).equals(want) - -_tests.append(_test_with_entry_points) - -def _test_group_member(env): - want = """\ -load("@bazel_skylib//rules:copy_file.bzl", "copy_file") -load("@rules_python//python:defs.bzl", "py_library", "py_binary") - -package(default_visibility = ["//visibility:public"]) - -filegroup( - name = "dist_info", - srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True), -) - -filegroup( - name = "data", - srcs = glob(["data/**"], allow_empty = True), -) - -filegroup( - name = "_whl", - srcs = ["foo.whl"], - data = ["@pypi_bar_baz//:whl"] + select( - { - "@platforms//os:linux": ["@pypi_box//:whl"], - ":is_linux_x86_64": [ - "@pypi_box//:whl", - "@pypi_box_amd64//:whl", - ], - "//conditions:default": [], - }, - ), - visibility = ["@pypi__groups//:__pkg__"], -) - -py_library( - name = "_pkg", - srcs = glob( - ["site-packages/**/*.py"], - exclude=[], - # Empty sources are allowed to support wheels that don't have any - # pure-Python code, e.g. pymssql, which is written in Cython. - allow_empty = True, - ), - data = [] + glob( - ["site-packages/**/*"], - exclude=["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD"], - ), - # This makes this directory a top-level in the python import - # search path for anything that depends on this. - imports = ["site-packages"], - deps = ["@pypi_bar_baz//:pkg"] + select( - { - "@platforms//os:linux": ["@pypi_box//:pkg"], - ":is_linux_x86_64": [ - "@pypi_box//:pkg", - "@pypi_box_amd64//:pkg", - ], - "//conditions:default": [], - }, - ), - tags = [], - visibility = ["@pypi__groups//:__pkg__"], -) - -config_setting( - name = "is_linux_x86_64", - constraint_values = [ - "@platforms//cpu:x86_64", - "@platforms//os:linux", - ], - visibility = ["//visibility:private"], -) - -alias( - name = "pkg", - actual = "@pypi__groups//:qux_pkg", -) - -alias( - name = "whl", - actual = "@pypi__groups//:qux_whl", -) -""" - actual = generate_whl_library_build_bazel( - dep_template = "@pypi_{name}//:{target}", - whl_name = "foo.whl", - dependencies = ["foo", "bar-baz", "qux"], - dependencies_by_platform = { - "linux_x86_64": ["box", "box-amd64"], - "windows_x86_64": ["fox"], - "@platforms//os:linux": ["box"], # buildifier: disable=unsorted-dict-items to check that we sort inside the test - }, - tags = [], - entry_points = {}, - data_exclude = [], - annotation = None, - group_name = "qux", - group_deps = ["foo", "fox", "qux"], - ) - env.expect.that_str(actual.replace("@@", "@")).equals(want) - -_tests.append(_test_group_member) - -def _test_group_member_deps_to_hub(env): - want = """\ -load("@bazel_skylib//rules:copy_file.bzl", "copy_file") -load("@rules_python//python:defs.bzl", "py_library", "py_binary") - -package(default_visibility = ["//visibility:public"]) - -filegroup( - name = "dist_info", - srcs = glob(["site-packages/*.dist-info/**"], allow_empty = True), -) - -filegroup( - name = "data", - srcs = glob(["data/**"], allow_empty = True), -) - -filegroup( - name = "whl", - srcs = ["foo.whl"], - data = ["@pypi//bar_baz:whl"] + select( - { - "@platforms//os:linux": ["@pypi//box:whl"], - ":is_linux_x86_64": [ - "@pypi//box:whl", - "@pypi//box_amd64:whl", - ], - "//conditions:default": [], - }, - ), - visibility = ["@pypi//:__subpackages__"], -) - -py_library( - name = "pkg", - srcs = glob( - ["site-packages/**/*.py"], - exclude=[], - # Empty sources are allowed to support wheels that don't have any - # pure-Python code, e.g. pymssql, which is written in Cython. - allow_empty = True, - ), - data = [] + glob( - ["site-packages/**/*"], - exclude=["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD"], - ), - # This makes this directory a top-level in the python import - # search path for anything that depends on this. - imports = ["site-packages"], - deps = ["@pypi//bar_baz:pkg"] + select( - { - "@platforms//os:linux": ["@pypi//box:pkg"], - ":is_linux_x86_64": [ - "@pypi//box:pkg", - "@pypi//box_amd64:pkg", - ], - "//conditions:default": [], - }, - ), - tags = [], - visibility = ["@pypi//:__subpackages__"], -) - -config_setting( - name = "is_linux_x86_64", - constraint_values = [ - "@platforms//cpu:x86_64", - "@platforms//os:linux", - ], - visibility = ["//visibility:private"], -) """ actual = generate_whl_library_build_bazel( dep_template = "@pypi//{name}:{target}", - whl_name = "foo.whl", + name = "foo.whl", dependencies = ["foo", "bar-baz", "qux"], dependencies_by_platform = { "linux_x86_64": ["box", "box-amd64"], "windows_x86_64": ["fox"], "@platforms//os:linux": ["box"], # buildifier: disable=unsorted-dict-items to check that we sort inside the test }, - tags = [], - entry_points = {}, - data_exclude = [], - annotation = None, + tags = ["tag2", "tag1"], + entry_points = { + "foo": "bar.py", + }, + data_exclude = ["exclude_via_attr"], + annotation = struct( + copy_files = {"file_src": "file_dest"}, + copy_executables = {"exec_src": "exec_dest"}, + data = ["extra_target"], + data_exclude_glob = ["data_exclude_all"], + srcs_exclude_glob = ["srcs_exclude_all"], + additive_build_content = """# SOMETHING SPECIAL AT THE END""", + ), group_name = "qux", group_deps = ["foo", "fox", "qux"], ) env.expect.that_str(actual.replace("@@", "@")).equals(want) -_tests.append(_test_group_member_deps_to_hub) +_tests.append(_test_all) def generate_whl_library_build_bazel_test_suite(name): """Create the test suite. diff --git a/tests/pypi/whl_library_targets/BUILD.bazel b/tests/pypi/whl_library_targets/BUILD.bazel new file mode 100644 index 0000000000..f3d25c2a52 --- /dev/null +++ b/tests/pypi/whl_library_targets/BUILD.bazel @@ -0,0 +1,5 @@ +load(":whl_library_targets_tests.bzl", "whl_library_targets_test_suite") + +whl_library_targets_test_suite( + name = "whl_library_targets_tests", +) diff --git a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl new file mode 100644 index 0000000000..9694eeec48 --- /dev/null +++ b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl @@ -0,0 +1,349 @@ +# Copyright 2024 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"" + +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("//python/private/pypi:whl_library_targets.bzl", "whl_library_targets") # buildifier: disable=bzl-visibility + +_tests = [] + +def _test_filegroups(env): + calls = [] + + def glob(match, *, allow_empty): + env.expect.that_bool(allow_empty).equals(True) + return match + + whl_library_targets( + name = "", + dep_template = "", + native = struct( + filegroup = lambda **kwargs: calls.append(kwargs), + glob = glob, + ), + rules = struct(), + ) + + env.expect.that_collection(calls).contains_exactly([ + { + "name": "dist_info", + "srcs": ["site-packages/*.dist-info/**"], + "visibility": ["//visibility:public"], + }, + { + "name": "data", + "srcs": ["data/**"], + "visibility": ["//visibility:public"], + }, + { + "name": "whl", + "srcs": [""], + "data": [], + "visibility": ["//visibility:public"], + }, + ]) # buildifier: @unsorted-dict-items + +_tests.append(_test_filegroups) + +def _test_platforms(env): + calls = [] + + whl_library_targets( + name = "", + dep_template = None, + dependencies_by_platform = { + "@//python/config_settings:is_python_3.9": ["py39_dep"], + "@platforms//cpu:aarch64": ["arm_dep"], + "@platforms//os:windows": ["win_dep"], + "cp310_linux_ppc": ["py310_linux_ppc_dep"], + "cp39_anyos_aarch64": ["py39_arm_dep"], + "cp39_linux_anyarch": ["py39_linux_dep"], + "linux_x86_64": ["linux_intel_dep"], + }, + filegroups = {}, + native = struct( + config_setting = lambda **kwargs: calls.append(kwargs), + ), + rules = struct(), + ) + + env.expect.that_collection(calls).contains_exactly([ + { + "name": "is_python_3.10_linux_ppc", + "flag_values": { + "@rules_python//python/config_settings:python_version_major_minor": "3.10", + }, + "constraint_values": [ + "@platforms//cpu:ppc", + "@platforms//os:linux", + ], + "visibility": ["//visibility:private"], + }, + { + "name": "is_python_3.9_anyos_aarch64", + "flag_values": { + "@rules_python//python/config_settings:python_version_major_minor": "3.9", + }, + "constraint_values": ["@platforms//cpu:aarch64"], + "visibility": ["//visibility:private"], + }, + { + "name": "is_python_3.9_linux_anyarch", + "flag_values": { + "@rules_python//python/config_settings:python_version_major_minor": "3.9", + }, + "constraint_values": ["@platforms//os:linux"], + "visibility": ["//visibility:private"], + }, + { + "name": "is_linux_x86_64", + "constraint_values": [ + "@platforms//cpu:x86_64", + "@platforms//os:linux", + ], + "visibility": ["//visibility:private"], + }, + ]) # buildifier: @unsorted-dict-items + +_tests.append(_test_platforms) + +def _test_copy(env): + calls = [] + + whl_library_targets( + name = "", + dep_template = None, + dependencies_by_platform = {}, + filegroups = {}, + copy_files = {"file_src": "file_dest"}, + copy_executables = {"exec_src": "exec_dest"}, + native = struct(), + rules = struct( + copy_file = lambda **kwargs: calls.append(kwargs), + ), + ) + + env.expect.that_collection(calls).contains_exactly([ + { + "name": "file_dest.copy", + "out": "file_dest", + "src": "file_src", + "visibility": ["//visibility:public"], + }, + { + "is_executable": True, + "name": "exec_dest.copy", + "out": "exec_dest", + "src": "exec_src", + "visibility": ["//visibility:public"], + }, + ]) + +_tests.append(_test_copy) + +def _test_entrypoints(env): + calls = [] + + whl_library_targets( + name = "", + dep_template = None, + dependencies_by_platform = {}, + filegroups = {}, + entry_points = { + "fizz": "buzz.py", + }, + native = struct(), + rules = struct( + py_binary = lambda **kwargs: calls.append(kwargs), + ), + ) + + env.expect.that_collection(calls).contains_exactly([ + { + "name": "rules_python_wheel_entry_point_fizz", + "srcs": ["buzz.py"], + "deps": [":pkg"], + "imports": ["."], + "visibility": ["//visibility:public"], + }, + ]) # buildifier: @unsorted-dict-items + +_tests.append(_test_entrypoints) + +def _test_whl_and_library_deps(env): + filegroup_calls = [] + py_library_calls = [] + + whl_library_targets( + name = "foo.whl", + dep_template = "@pypi_{name}//:{target}", + dependencies = ["foo", "bar-baz"], + dependencies_by_platform = { + "@//python/config_settings:is_python_3.9": ["py39_dep"], + "@platforms//cpu:aarch64": ["arm_dep"], + "@platforms//os:windows": ["win_dep"], + "cp310_linux_ppc": ["py310_linux_ppc_dep"], + "cp39_anyos_aarch64": ["py39_arm_dep"], + "cp39_linux_anyarch": ["py39_linux_dep"], + "linux_x86_64": ["linux_intel_dep"], + }, + data_exclude = [], + tags = ["tag1", "tag2"], + # Overrides for testing + filegroups = {}, + native = struct( + filegroup = lambda **kwargs: filegroup_calls.append(kwargs), + config_setting = lambda **_: None, + glob = _glob, + select = _select, + ), + rules = struct( + py_library = lambda **kwargs: py_library_calls.append(kwargs), + ), + ) + + env.expect.that_collection(filegroup_calls).contains_exactly([ + { + "name": "whl", + "srcs": ["foo.whl"], + "data": [ + "@pypi_bar_baz//:whl", + "@pypi_foo//:whl", + ] + _select( + { + Label("//python/config_settings:is_python_3.9"): ["@pypi_py39_dep//:whl"], + "@platforms//cpu:aarch64": ["@pypi_arm_dep//:whl"], + "@platforms//os:windows": ["@pypi_win_dep//:whl"], + ":is_python_3.10_linux_ppc": ["@pypi_py310_linux_ppc_dep//:whl"], + ":is_python_3.9_anyos_aarch64": ["@pypi_py39_arm_dep//:whl"], + ":is_python_3.9_linux_anyarch": ["@pypi_py39_linux_dep//:whl"], + ":is_linux_x86_64": ["@pypi_linux_intel_dep//:whl"], + "//conditions:default": [], + }, + ), + "visibility": ["//visibility:public"], + }, + ]) # buildifier: @unsorted-dict-items + env.expect.that_collection(py_library_calls).contains_exactly([ + { + "name": "pkg", + "srcs": _glob( + ["site-packages/**/*.py"], + exclude = [], + allow_empty = True, + ), + "data": [] + _glob( + ["site-packages/**/*"], + exclude = ["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD"], + ), + "imports": ["site-packages"], + "deps": [ + "@pypi_bar_baz//:pkg", + "@pypi_foo//:pkg", + ] + _select( + { + Label("//python/config_settings:is_python_3.9"): ["@pypi_py39_dep//:pkg"], + "@platforms//cpu:aarch64": ["@pypi_arm_dep//:pkg"], + "@platforms//os:windows": ["@pypi_win_dep//:pkg"], + ":is_python_3.10_linux_ppc": ["@pypi_py310_linux_ppc_dep//:pkg"], + ":is_python_3.9_anyos_aarch64": ["@pypi_py39_arm_dep//:pkg"], + ":is_python_3.9_linux_anyarch": ["@pypi_py39_linux_dep//:pkg"], + ":is_linux_x86_64": ["@pypi_linux_intel_dep//:pkg"], + "//conditions:default": [], + }, + ), + "tags": ["tag1", "tag2"], + "visibility": ["//visibility:public"], + }, + ]) # buildifier: @unsorted-dict-items + +_tests.append(_test_whl_and_library_deps) + +def _test_group(env): + alias_calls = [] + py_library_calls = [] + + whl_library_targets( + name = "foo.whl", + dep_template = "@pypi_{name}//:{target}", + dependencies = ["foo", "bar-baz", "qux"], + dependencies_by_platform = { + "linux_x86_64": ["box", "box-amd64"], + "windows_x86_64": ["fox"], + "@platforms//os:linux": ["box"], # buildifier: disable=unsorted-dict-items to check that we sort inside the test + }, + tags = [], + entry_points = {}, + data_exclude = [], + group_name = "qux", + group_deps = ["foo", "fox", "qux"], + # Overrides for testing + filegroups = {}, + native = struct( + config_setting = lambda **_: None, + glob = _glob, + alias = lambda **kwargs: alias_calls.append(kwargs), + select = _select, + ), + rules = struct( + py_library = lambda **kwargs: py_library_calls.append(kwargs), + ), + ) + + env.expect.that_collection(alias_calls).contains_exactly([ + {"name": "pkg", "actual": "@pypi__groups//:qux_pkg", "visibility": ["//visibility:public"]}, + {"name": "whl", "actual": "@pypi__groups//:qux_whl", "visibility": ["//visibility:public"]}, + ]) # buildifier: @unsorted-dict-items + env.expect.that_collection(py_library_calls).contains_exactly([ + { + "name": "_pkg", + "srcs": _glob(["site-packages/**/*.py"], exclude = [], allow_empty = True), + "data": [] + _glob( + ["site-packages/**/*"], + exclude = ["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD"], + ), + "imports": ["site-packages"], + "deps": ["@pypi_bar_baz//:pkg"] + _select({ + "@platforms//os:linux": ["@pypi_box//:pkg"], + ":is_linux_x86_64": ["@pypi_box//:pkg", "@pypi_box_amd64//:pkg"], + "//conditions:default": [], + }), + "tags": [], + "visibility": ["@pypi__groups//:__pkg__"], + }, + ]) # buildifier: @unsorted-dict-items + +_tests.append(_test_group) + +def _glob(*args, **kwargs): + return [struct( + glob = args, + kwargs = kwargs, + )] + +def _select(*args, **kwargs): + """We need to have this mock select because we still need to support bazel 6.""" + return [struct( + select = args, + kwargs = kwargs, + )] + +def whl_library_targets_test_suite(name): + """create the test suite. + + args: + name: the name of the test suite + """ + test_suite(name = name, basic_tests = _tests)