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.
diff --git a/examples/bzlmod/MODULE.bazel.lock b/examples/bzlmod/MODULE.bazel.lock
index 286e8c0..c41380c 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 e76f9d3..9be355c 100644
--- a/python/private/pypi/BUILD.bazel
+++ b/python/private/pypi/BUILD.bazel
@@ -110,8 +110,7 @@
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 934fa00..8050cd2 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},
-)
-"""
+_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,
+}
-_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}
+# 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 82fe072..62c0c6d 100644
--- a/python/private/pypi/whl_library.bzl
+++ b/python/private/pypi/whl_library.bzl
@@ -332,8 +332,8 @@
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 0000000..1798b9d
--- /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 9453011..b0d8f6d 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 @@
_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",
},
- constraint_values = [
- "@platforms//cpu:ppc",
- "@platforms//os:linux",
- ],
- 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",
+ copy_files = {
+ "file_src": "file_dest",
},
- 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",
+ data = ["extra_target"],
+ data_exclude = [
+ "exclude_via_attr",
+ "data_exclude_all",
+ ],
+ 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//os:linux"],
- visibility = ["//visibility:private"],
-)
-
-config_setting(
- name = "is_linux_x86_64",
- constraint_values = [
- "@platforms//cpu:x86_64",
- "@platforms//os:linux",
+ entry_points = {
+ "foo": "bar.py",
+ },
+ 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 = {},
+ dep_template = "@pypi//{name}:{target}",
+ 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 = ["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 = [],
+ data = ["extra_target"],
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",
- 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_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 0000000..f3d25c2
--- /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 0000000..9694eee
--- /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)