Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(whl_library): move bazel file generation to Starlark #1336

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions python/pip_install/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ filegroup(
"BUILD.bazel",
"//python/pip_install/private:distribution",
"//python/pip_install/tools/dependency_resolver:distribution",
"//python/pip_install/tools/lib:distribution",
"//python/pip_install/tools/wheel_installer:distribution",
],
visibility = ["//:__pkg__"],
Expand All @@ -22,7 +21,6 @@ filegroup(
name = "py_srcs",
srcs = [
"//python/pip_install/tools/dependency_resolver:py_srcs",
"//python/pip_install/tools/lib:py_srcs",
"//python/pip_install/tools/wheel_installer:py_srcs",
],
visibility = ["//python/pip_install/private:__pkg__"],
Expand Down
76 changes: 67 additions & 9 deletions python/pip_install/pip_repository.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ load("//python:repositories.bzl", "get_interpreter_dirname", "is_standalone_inte
load("//python:versions.bzl", "WINDOWS_NAME")
load("//python/pip_install:repositories.bzl", "all_requirements")
load("//python/pip_install:requirements_parser.bzl", parse_requirements = "parse")
load("//python/pip_install/private:generate_whl_library_build_bazel.bzl", "generate_whl_library_build_bazel")
load("//python/pip_install/private:srcs.bzl", "PIP_INSTALL_PY_SRCS")
load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED")
load("//python/private:normalize_name.bzl", "normalize_name")
Expand All @@ -27,6 +28,8 @@ CPPFLAGS = "CPPFLAGS"

COMMAND_LINE_TOOLS_PATH_SLUG = "commandlinetools"

_WHEEL_ENTRY_POINT_PREFIX = "rules_python_wheel_entry_point"

def _construct_pypath(rctx):
"""Helper function to construct a PYTHONPATH.

Expand Down Expand Up @@ -663,16 +666,7 @@ def _whl_library_impl(rctx):
"python.pip_install.tools.wheel_installer.wheel_installer",
"--requirement",
rctx.attr.requirement,
"--repo",
rctx.attr.repo,
"--repo-prefix",
rctx.attr.repo_prefix,
]
if rctx.attr.annotation:
args.extend([
"--annotation",
rctx.path(rctx.attr.annotation),
])

args = _parse_optional_attrs(rctx, args)

Expand All @@ -687,8 +681,72 @@ def _whl_library_impl(rctx):
if result.return_code:
fail("whl_library %s failed: %s (%s) error code: '%s'" % (rctx.attr.name, result.stdout, result.stderr, result.return_code))

metadata = json.decode(rctx.read("metadata.json"))
rctx.delete("metadata.json")

entry_points = {}
for item in metadata["entry_points"]:
name = item["name"]
module = item["module"]
attribute = item["attribute"]

# There is an extreme edge-case with entry_points that end with `.py`
# See: https://github.com/bazelbuild/bazel/blob/09c621e4cf5b968f4c6cdf905ab142d5961f9ddc/src/test/java/com/google/devtools/build/lib/rules/python/PyBinaryConfiguredTargetTest.java#L174
entry_point_without_py = name[:-3] + "_py" if name.endswith(".py") else name
entry_point_target_name = (
_WHEEL_ENTRY_POINT_PREFIX + "_" + entry_point_without_py
)
entry_point_script_name = entry_point_target_name + ".py"

rctx.file(
entry_point_script_name,
_generate_entry_point_contents(module, attribute),
)
entry_points[entry_point_without_py] = entry_point_script_name

build_file_contents = generate_whl_library_build_bazel(
repo_prefix = rctx.attr.repo_prefix,
dependencies = metadata["deps"],
data_exclude = rctx.attr.pip_data_exclude,
tags = [
"pypi_name=" + metadata["name"],
"pypi_version=" + metadata["version"],
],
entry_points = entry_points,
annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))),
)
rctx.file("BUILD.bazel", build_file_contents)

return

def _generate_entry_point_contents(
module,
attribute,
shebang = "#!/usr/bin/env python3"):
"""Generate the contents of an entry point script.

Args:
module (str): The name of the module to use.
attribute (str): The name of the attribute to call.
shebang (str, optional): The shebang to use for the entry point python
file.

Returns:
str: A string of python code.
"""
contents = """\
{shebang}
import sys
from {module} import {attribute}
if __name__ == "__main__":
sys.exit({attribute}())
""".format(
shebang = shebang,
module = module,
attribute = attribute,
)
return contents

whl_library_attrs = {
"annotation": attr.label(
doc = (
Expand Down
224 changes: 224 additions & 0 deletions python/pip_install/private/generate_whl_library_build_bazel.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# Copyright 2023 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.

"""Generate the BUILD.bazel contents for a repo defined by a whl_library."""

load("//python/private:normalize_name.bzl", "normalize_name")

_WHEEL_FILE_LABEL = "whl"
_PY_LIBRARY_LABEL = "pkg"
_DATA_LABEL = "data"
_DIST_INFO_LABEL = "dist_info"
_WHEEL_ENTRY_POINT_PREFIX = "rules_python_wheel_entry_point"

_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 = """\
load("@rules_python//python:defs.bzl", "py_library", "py_binary")
load("@bazel_skylib//rules:copy_file.bzl", "copy_file")

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 = glob(["*.whl"], allow_empty = True),
data = {whl_file_deps},
)

py_library(
name = "{name}",
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,
aignas marked this conversation as resolved.
Show resolved Hide resolved
),
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},
)
"""

def generate_whl_library_build_bazel(
repo_prefix,
dependencies,
data_exclude,
tags,
entry_points,
annotation = None):
"""Generate a BUILD file for an unzipped Wheel

Args:
repo_prefix: the repo prefix that should be used for dependency lists.
dependencies: a list of PyPI packages that are dependencies to the py_library.
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.

Returns:
A complete BUILD file as a string
"""

additional_content = []
data = []
srcs_exclude = []
data_exclude = [] + data_exclude
dependencies = sorted(dependencies)
tags = sorted(tags)

for entry_point, entry_point_script_name in entry_points.items():
additional_content.append(
_generate_entry_point_rule(
aignas marked this conversation as resolved.
Show resolved Hide resolved
name = "{}_{}".format(_WHEEL_ENTRY_POINT_PREFIX, entry_point),
script = entry_point_script_name,
pkg = ":" + _PY_LIBRARY_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)
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)

lib_dependencies = [
"@" + repo_prefix + normalize_name(d) + "//:" + _PY_LIBRARY_LABEL
for d in dependencies
]
whl_file_deps = [
"@" + repo_prefix + normalize_name(d) + "//:" + _WHEEL_FILE_LABEL
for d in dependencies
]

contents = "\n".join(
[
_BUILD_TEMPLATE.format(
name = _PY_LIBRARY_LABEL,
dependencies = repr(lib_dependencies),
data_exclude = repr(_data_exclude),
whl_file_label = _WHEEL_FILE_LABEL,
whl_file_deps = repr(whl_file_deps),
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),
),
] + 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,
)
5 changes: 1 addition & 4 deletions python/pip_install/private/srcs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ This file is auto-generated from the `@rules_python//python/pip_install/private:
PIP_INSTALL_PY_SRCS = [
"@rules_python//python/pip_install/tools/dependency_resolver:__init__.py",
"@rules_python//python/pip_install/tools/dependency_resolver:dependency_resolver.py",
"@rules_python//python/pip_install/tools/lib:__init__.py",
"@rules_python//python/pip_install/tools/lib:annotation.py",
"@rules_python//python/pip_install/tools/lib:arguments.py",
"@rules_python//python/pip_install/tools/lib:bazel.py",
"@rules_python//python/pip_install/tools/wheel_installer:arguments.py",
"@rules_python//python/pip_install/tools/wheel_installer:namespace_pkgs.py",
"@rules_python//python/pip_install/tools/wheel_installer:wheel.py",
"@rules_python//python/pip_install/tools/wheel_installer:wheel_installer.py",
Expand Down
Loading