blob: 166e1d06ebcca43bea769240b3d59d93582ac08d [file] [log] [blame]
# Copyright 2023 Jeremy Volkman. All rights reserved.
# 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.
"""Implementation of the py_wheel_library rule."""
load("@bazel_skylib//lib:paths.bzl", "paths")
load("//python:defs.bzl", "PyInfo")
load(":providers.bzl", "PyWheelInfo")
def _py_wheel_library_impl(ctx):
out = ctx.actions.declare_directory(ctx.attr.name)
wheel_target = ctx.attr.wheel
if PyWheelInfo in wheel_target:
wheel_file = wheel_target[PyWheelInfo].wheel_file
name_file = wheel_target[PyWheelInfo].name_file
else:
wheel_file = ctx.file.wheel
name_file = None
args = ctx.actions.args().use_param_file("--flagfile=%s")
args.add("--wheel", wheel_file)
args.add("--directory", out.path)
args.add_all(ctx.files.patches, format_each = "--patch=%s")
args.add_all(ctx.attr.patch_args, format_each = "--patch-arg=%s")
args.add("--patch-tool", ctx.attr.patch_tool)
tools = []
inputs = [wheel_file] + ctx.files.patches
if name_file:
inputs.append(name_file)
args.add("--wheel-name-file", name_file)
if ctx.attr.patch_tool_target:
args.add("--patch-tool-target", ctx.attr.patch_tool_target.files_to_run.executable)
tools.append(ctx.executable.patch_tool_target)
if ctx.attr.enable_implicit_namespace_pkgs:
args.add("--enable-implicit-namespace-pkgs")
# We apply patches in the same action as the extraction to minimize the
# number of times we cache the wheel contents. If we were to split this
# into 2 actions, then the wheel contents would be cached twice.
ctx.actions.run(
inputs = inputs,
outputs = [out],
executable = ctx.executable._tool,
tools = tools,
arguments = [args],
# Set environment variables to make generated .pyc files reproducible.
env = {
"PYTHONHASHSEED": "0",
"SOURCE_DATE_EPOCH": "315532800",
},
mnemonic = "WheelInstall",
progress_message = "Installing %s" % ctx.file.wheel.basename,
)
has_py2_only_sources = ctx.attr.python_version == "PY2"
has_py3_only_sources = ctx.attr.python_version == "PY3"
if not has_py2_only_sources:
for d in ctx.attr.deps:
if d[PyInfo].has_py2_only_sources:
has_py2_only_sources = True
break
if not has_py3_only_sources:
for d in ctx.attr.deps:
if d[PyInfo].has_py3_only_sources:
has_py3_only_sources = True
break
# TODO: Is there a more correct way to get this runfiles-relative import path?
imp = paths.join(
ctx.label.workspace_name or ctx.workspace_name, # Default to the local workspace.
ctx.label.package,
ctx.label.name,
"site-packages", # we put lib files in this subdirectory.
)
imports = depset(
direct = [imp],
transitive = [d[PyInfo].imports for d in ctx.attr.deps],
)
transitive_sources = depset(
direct = [out],
transitive = [dep[PyInfo].transitive_sources for dep in ctx.attr.deps if PyInfo in dep],
)
runfiles = ctx.runfiles(files = [out])
for d in ctx.attr.deps:
runfiles = runfiles.merge(d[DefaultInfo].default_runfiles)
return [
DefaultInfo(
files = depset(direct = [out]),
runfiles = runfiles,
),
PyInfo(
has_py2_only_sources = has_py2_only_sources,
has_py3_only_sources = has_py3_only_sources,
imports = imports,
transitive_sources = transitive_sources,
uses_shared_libraries = True, # Docs say this is unused
),
]
py_wheel_library = rule(
implementation = _py_wheel_library_impl,
attrs = {
"deps": attr.label_list(
doc = "A list of this wheel's Python library dependencies.",
providers = [DefaultInfo, PyInfo],
),
"enable_implicit_namespace_pkgs": attr.bool(
default = True,
doc = """
If true, disables conversion of native namespace packages into pkg-util style namespace packages. When set all py_binary
and py_test targets must specify either `legacy_create_init=False` or the global Bazel option
`--incompatible_default_to_explicit_init_py` to prevent `__init__.py` being automatically generated in every directory.
This option is required to support some packages which cannot handle the conversion to pkg-util style.
""",
),
"patch_args": attr.string_list(
default = ["-p0"],
doc =
"The arguments given to the patch tool. Defaults to -p0, " +
"however -p1 will usually be needed for patches generated by " +
"git. If multiple -p arguments are specified, the last one will take effect.",
),
"patch_tool": attr.string(
doc = "The patch(1) utility from the host to use. " +
"If set, overrides `patch_tool_target`. Please note that setting " +
"this means that builds are not completely hermetic.",
),
"patch_tool_target": attr.label(
executable = True,
cfg = "exec",
doc = "The label of the patch(1) utility to use. " +
"Only used if `patch_tool` is not set.",
),
"patches": attr.label_list(
allow_files = True,
default = [],
doc =
"A list of files that are to be applied as patches after " +
"extracting the archive. This will use the patch command line tool.",
),
"python_version": attr.string(
doc = "The python version required for this wheel ('PY2' or 'PY3')",
values = ["PY2", "PY3", ""],
),
"wheel": attr.label(
doc = "The wheel file.",
allow_single_file = [".whl"],
mandatory = True,
),
"_tool": attr.label(
default = Label("//third_party/rules_pycross/pycross/private/tools:wheel_installer"),
cfg = "exec",
executable = True,
),
},
)