• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2023 Jeremy Volkman. All rights reserved.
2# Copyright 2023 The Bazel Authors. All rights reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#     http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16"""Implementation of the py_wheel_library rule."""
17
18load("@bazel_skylib//lib:paths.bzl", "paths")
19load("//python:defs.bzl", "PyInfo")
20load(":providers.bzl", "PyWheelInfo")
21
22def _py_wheel_library_impl(ctx):
23    out = ctx.actions.declare_directory(ctx.attr.name)
24
25    wheel_target = ctx.attr.wheel
26    if PyWheelInfo in wheel_target:
27        wheel_file = wheel_target[PyWheelInfo].wheel_file
28        name_file = wheel_target[PyWheelInfo].name_file
29    else:
30        wheel_file = ctx.file.wheel
31        name_file = None
32
33    args = ctx.actions.args().use_param_file("--flagfile=%s")
34    args.add("--wheel", wheel_file)
35    args.add("--directory", out.path)
36    args.add_all(ctx.files.patches, format_each = "--patch=%s")
37    args.add_all(ctx.attr.patch_args, format_each = "--patch-arg=%s")
38    args.add("--patch-tool", ctx.attr.patch_tool)
39
40    tools = []
41    inputs = [wheel_file] + ctx.files.patches
42    if name_file:
43        inputs.append(name_file)
44        args.add("--wheel-name-file", name_file)
45
46    if ctx.attr.patch_tool_target:
47        args.add("--patch-tool-target", ctx.attr.patch_tool_target.files_to_run.executable)
48        tools.append(ctx.executable.patch_tool_target)
49
50    if ctx.attr.enable_implicit_namespace_pkgs:
51        args.add("--enable-implicit-namespace-pkgs")
52
53    # We apply patches in the same action as the extraction to minimize the
54    # number of times we cache the wheel contents. If we were to split this
55    # into 2 actions, then the wheel contents would be cached twice.
56    ctx.actions.run(
57        inputs = inputs,
58        outputs = [out],
59        executable = ctx.executable._tool,
60        tools = tools,
61        arguments = [args],
62        # Set environment variables to make generated .pyc files reproducible.
63        env = {
64            "PYTHONHASHSEED": "0",
65            "SOURCE_DATE_EPOCH": "315532800",
66        },
67        mnemonic = "WheelInstall",
68        progress_message = "Installing %s" % ctx.file.wheel.basename,
69    )
70
71    has_py2_only_sources = ctx.attr.python_version == "PY2"
72    has_py3_only_sources = ctx.attr.python_version == "PY3"
73    if not has_py2_only_sources:
74        for d in ctx.attr.deps:
75            if d[PyInfo].has_py2_only_sources:
76                has_py2_only_sources = True
77                break
78    if not has_py3_only_sources:
79        for d in ctx.attr.deps:
80            if d[PyInfo].has_py3_only_sources:
81                has_py3_only_sources = True
82                break
83
84    # TODO: Is there a more correct way to get this runfiles-relative import path?
85    imp = paths.join(
86        ctx.label.workspace_name or ctx.workspace_name,  # Default to the local workspace.
87        ctx.label.package,
88        ctx.label.name,
89        "site-packages",  # we put lib files in this subdirectory.
90    )
91
92    imports = depset(
93        direct = [imp],
94        transitive = [d[PyInfo].imports for d in ctx.attr.deps],
95    )
96    transitive_sources = depset(
97        direct = [out],
98        transitive = [dep[PyInfo].transitive_sources for dep in ctx.attr.deps if PyInfo in dep],
99    )
100    runfiles = ctx.runfiles(files = [out])
101    for d in ctx.attr.deps:
102        runfiles = runfiles.merge(d[DefaultInfo].default_runfiles)
103
104    return [
105        DefaultInfo(
106            files = depset(direct = [out]),
107            runfiles = runfiles,
108        ),
109        PyInfo(
110            has_py2_only_sources = has_py2_only_sources,
111            has_py3_only_sources = has_py3_only_sources,
112            imports = imports,
113            transitive_sources = transitive_sources,
114            uses_shared_libraries = True,  # Docs say this is unused
115        ),
116    ]
117
118py_wheel_library = rule(
119    implementation = _py_wheel_library_impl,
120    attrs = {
121        "deps": attr.label_list(
122            doc = "A list of this wheel's Python library dependencies.",
123            providers = [DefaultInfo, PyInfo],
124        ),
125        "enable_implicit_namespace_pkgs": attr.bool(
126            default = True,
127            doc = """
128If true, disables conversion of native namespace packages into pkg-util style namespace packages. When set all py_binary
129and py_test targets must specify either `legacy_create_init=False` or the global Bazel option
130`--incompatible_default_to_explicit_init_py` to prevent `__init__.py` being automatically generated in every directory.
131This option is required to support some packages which cannot handle the conversion to pkg-util style.
132            """,
133        ),
134        "patch_args": attr.string_list(
135            default = ["-p0"],
136            doc =
137                "The arguments given to the patch tool. Defaults to -p0, " +
138                "however -p1 will usually be needed for patches generated by " +
139                "git. If multiple -p arguments are specified, the last one will take effect.",
140        ),
141        "patch_tool": attr.string(
142            doc = "The patch(1) utility from the host to use. " +
143                  "If set, overrides `patch_tool_target`. Please note that setting " +
144                  "this means that builds are not completely hermetic.",
145        ),
146        "patch_tool_target": attr.label(
147            executable = True,
148            cfg = "exec",
149            doc = "The label of the patch(1) utility to use. " +
150                  "Only used if `patch_tool` is not set.",
151        ),
152        "patches": attr.label_list(
153            allow_files = True,
154            default = [],
155            doc =
156                "A list of files that are to be applied as patches after " +
157                "extracting the archive. This will use the patch command line tool.",
158        ),
159        "python_version": attr.string(
160            doc = "The python version required for this wheel ('PY2' or 'PY3')",
161            values = ["PY2", "PY3", ""],
162        ),
163        "wheel": attr.label(
164            doc = "The wheel file.",
165            allow_single_file = [".whl"],
166            mandatory = True,
167        ),
168        "_tool": attr.label(
169            default = Label("//third_party/rules_pycross/pycross/private/tools:wheel_installer"),
170            cfg = "exec",
171            executable = True,
172        ),
173    },
174)
175