• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2022 The Bazel Authors. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#    http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Create a repository to hold the toolchains.
16
17This follows guidance here:
18https://docs.bazel.build/versions/main/skylark/deploying.html#registering-toolchains
19
20The "complex computation" in our case is simply downloading large artifacts.
21This guidance tells us how to avoid that: we put the toolchain targets in the
22alias repository with only the toolchain attribute pointing into the
23platform-specific repositories.
24"""
25
26load(
27    "//python:versions.bzl",
28    "LINUX_NAME",
29    "MACOS_NAME",
30    "PLATFORMS",
31    "WINDOWS_NAME",
32)
33load(":which.bzl", "which_with_fail")
34
35def get_repository_name(repository_workspace):
36    dummy_label = "//:_"
37    return str(repository_workspace.relative(dummy_label))[:-len(dummy_label)] or "@"
38
39def python_toolchain_build_file_content(
40        prefix,
41        python_version,
42        set_python_version_constraint,
43        user_repository_name,
44        rules_python):
45    """Creates the content for toolchain definitions for a build file.
46
47    Args:
48        prefix: Python toolchain name prefixes
49        python_version: Python versions for the toolchains
50        set_python_version_constraint: string, "True" if the toolchain should
51            have the Python version constraint added as a requirement for
52            matching the toolchain, "False" if not.
53        user_repository_name: names for the user repos
54        rules_python: rules_python label
55
56    Returns:
57        build_content: Text containing toolchain definitions
58    """
59    if set_python_version_constraint == "True":
60        constraint = "{rules_python}//python/config_settings:is_python_{python_version}".format(
61            rules_python = rules_python,
62            python_version = python_version,
63        )
64        target_settings = '["{}"]'.format(constraint)
65    elif set_python_version_constraint == "False":
66        target_settings = "[]"
67    else:
68        fail(("Invalid set_python_version_constraint value: got {} {}, wanted " +
69              "either the string 'True' or the string 'False'; " +
70              "(did you convert bool to string?)").format(
71            type(set_python_version_constraint),
72            repr(set_python_version_constraint),
73        ))
74
75    # We create a list of toolchain content from iterating over
76    # the enumeration of PLATFORMS.  We enumerate PLATFORMS in
77    # order to get us an index to increment the increment.
78    return "".join([
79        """
80toolchain(
81    name = "{prefix}{platform}_toolchain",
82    target_compatible_with = {compatible_with},
83    target_settings = {target_settings},
84    toolchain = "@{user_repository_name}_{platform}//:python_runtimes",
85    toolchain_type = "@bazel_tools//tools/python:toolchain_type",
86)
87
88toolchain(
89    name = "{prefix}{platform}_py_cc_toolchain",
90    target_compatible_with = {compatible_with},
91    target_settings = {target_settings},
92    toolchain = "@{user_repository_name}_{platform}//:py_cc_toolchain",
93    toolchain_type = "@rules_python//python/cc:toolchain_type",
94
95)
96""".format(
97            compatible_with = meta.compatible_with,
98            platform = platform,
99            # We have to use a String value here because bzlmod is passing in a
100            # string as we cannot have list of bools in build rule attribues.
101            # This if statement does not appear to work unless it is in the
102            # toolchain file.
103            target_settings = target_settings,
104            user_repository_name = user_repository_name,
105            prefix = prefix,
106        )
107        for platform, meta in PLATFORMS.items()
108    ])
109
110def _toolchains_repo_impl(rctx):
111    build_content = """\
112# Generated by python/private/toolchains_repo.bzl
113#
114# These can be registered in the workspace file or passed to --extra_toolchains
115# flag. By default all these toolchains are registered by the
116# python_register_toolchains macro so you don't normally need to interact with
117# these targets.
118
119"""
120
121    # Get the repository name
122    rules_python = get_repository_name(rctx.attr._rules_python_workspace)
123
124    toolchains = python_toolchain_build_file_content(
125        prefix = "",
126        python_version = rctx.attr.python_version,
127        set_python_version_constraint = str(rctx.attr.set_python_version_constraint),
128        user_repository_name = rctx.attr.user_repository_name,
129        rules_python = rules_python,
130    )
131
132    rctx.file("BUILD.bazel", build_content + toolchains)
133
134toolchains_repo = repository_rule(
135    _toolchains_repo_impl,
136    doc = "Creates a repository with toolchain definitions for all known platforms " +
137          "which can be registered or selected.",
138    attrs = {
139        "python_version": attr.string(doc = "The Python version."),
140        "set_python_version_constraint": attr.bool(doc = "if target_compatible_with for the toolchain should set the version constraint"),
141        "user_repository_name": attr.string(doc = "what the user chose for the base name"),
142        "_rules_python_workspace": attr.label(default = Label("//:WORKSPACE")),
143    },
144)
145
146def _toolchain_aliases_impl(rctx):
147    (os_name, arch) = get_host_os_arch(rctx)
148
149    host_platform = get_host_platform(os_name, arch)
150
151    is_windows = (os_name == WINDOWS_NAME)
152    python3_binary_path = "python.exe" if is_windows else "bin/python3"
153
154    # Base BUILD file for this repository.
155    build_contents = """\
156# Generated by python/private/toolchains_repo.bzl
157package(default_visibility = ["//visibility:public"])
158load("@rules_python//python:versions.bzl", "PLATFORMS", "gen_python_config_settings")
159gen_python_config_settings()
160exports_files(["defs.bzl"])
161alias(name = "files",           actual = select({{":" + item: "@{py_repository}_" + item + "//:files" for item in PLATFORMS.keys()}}))
162alias(name = "includes",        actual = select({{":" + item: "@{py_repository}_" + item + "//:includes" for item in PLATFORMS.keys()}}))
163alias(name = "libpython",       actual = select({{":" + item: "@{py_repository}_" + item + "//:libpython" for item in PLATFORMS.keys()}}))
164alias(name = "py3_runtime",     actual = select({{":" + item: "@{py_repository}_" + item + "//:py3_runtime" for item in PLATFORMS.keys()}}))
165alias(name = "python_headers",  actual = select({{":" + item: "@{py_repository}_" + item + "//:python_headers" for item in PLATFORMS.keys()}}))
166alias(name = "python_runtimes", actual = select({{":" + item: "@{py_repository}_" + item + "//:python_runtimes" for item in PLATFORMS.keys()}}))
167alias(name = "python3",         actual = select({{":" + item: "@{py_repository}_" + item + "//:" + ("python.exe" if "windows" in item else "bin/python3") for item in PLATFORMS.keys()}}))
168""".format(
169        py_repository = rctx.attr.user_repository_name,
170    )
171    if not is_windows:
172        build_contents += """\
173alias(name = "pip",             actual = select({{":" + item: "@{py_repository}_" + item + "//:python_runtimes" for item in PLATFORMS.keys() if "windows" not in item}}))
174""".format(
175            py_repository = rctx.attr.user_repository_name,
176            host_platform = host_platform,
177        )
178    rctx.file("BUILD.bazel", build_contents)
179
180    # Expose a Starlark file so rules can know what host platform we used and where to find an interpreter
181    # when using repository_ctx.path, which doesn't understand aliases.
182    rctx.file("defs.bzl", content = """\
183# Generated by python/private/toolchains_repo.bzl
184
185load("{rules_python}//python/config_settings:transition.bzl", _py_binary = "py_binary", _py_test = "py_test")
186load("{rules_python}//python:pip.bzl", _compile_pip_requirements = "compile_pip_requirements")
187
188host_platform = "{host_platform}"
189interpreter = "@{py_repository}_{host_platform}//:{python3_binary_path}"
190
191def py_binary(name, **kwargs):
192    return _py_binary(
193        name = name,
194        python_version = "{python_version}",
195        **kwargs
196    )
197
198def py_test(name, **kwargs):
199    return _py_test(
200        name = name,
201        python_version = "{python_version}",
202        **kwargs
203    )
204
205def compile_pip_requirements(name, **kwargs):
206    return _compile_pip_requirements(
207        name = name,
208        py_binary = py_binary,
209        py_test = py_test,
210        **kwargs
211    )
212
213""".format(
214        host_platform = host_platform,
215        py_repository = rctx.attr.user_repository_name,
216        python_version = rctx.attr.python_version,
217        python3_binary_path = python3_binary_path,
218        rules_python = get_repository_name(rctx.attr._rules_python_workspace),
219    ))
220
221toolchain_aliases = repository_rule(
222    _toolchain_aliases_impl,
223    doc = """Creates a repository with a shorter name meant for the host platform, which contains
224    a BUILD.bazel file declaring aliases to the host platform's targets.
225    """,
226    attrs = {
227        "python_version": attr.string(doc = "The Python version."),
228        "user_repository_name": attr.string(
229            mandatory = True,
230            doc = "The base name for all created repositories, like 'python38'.",
231        ),
232        "_rules_python_workspace": attr.label(default = Label("//:WORKSPACE")),
233    },
234)
235
236def _multi_toolchain_aliases_impl(rctx):
237    rules_python = rctx.attr._rules_python_workspace.workspace_name
238
239    for python_version, repository_name in rctx.attr.python_versions.items():
240        file = "{}/defs.bzl".format(python_version)
241        rctx.file(file, content = """\
242# Generated by python/private/toolchains_repo.bzl
243
244load(
245    "@{repository_name}//:defs.bzl",
246    _compile_pip_requirements = "compile_pip_requirements",
247    _host_platform = "host_platform",
248    _interpreter = "interpreter",
249    _py_binary = "py_binary",
250    _py_test = "py_test",
251)
252
253compile_pip_requirements = _compile_pip_requirements
254host_platform = _host_platform
255interpreter = _interpreter
256py_binary = _py_binary
257py_test = _py_test
258""".format(
259            repository_name = repository_name,
260        ))
261        rctx.file("{}/BUILD.bazel".format(python_version), "")
262
263    pip_bzl = """\
264# Generated by python/private/toolchains_repo.bzl
265
266load("@{rules_python}//python:pip.bzl", "pip_parse", _multi_pip_parse = "multi_pip_parse")
267
268def multi_pip_parse(name, requirements_lock, **kwargs):
269    return _multi_pip_parse(
270        name = name,
271        python_versions = {python_versions},
272        requirements_lock = requirements_lock,
273        **kwargs
274    )
275
276""".format(
277        python_versions = rctx.attr.python_versions.keys(),
278        rules_python = rules_python,
279    )
280    rctx.file("pip.bzl", content = pip_bzl)
281    rctx.file("BUILD.bazel", "")
282
283multi_toolchain_aliases = repository_rule(
284    _multi_toolchain_aliases_impl,
285    attrs = {
286        "python_versions": attr.string_dict(doc = "The Python versions."),
287        "_rules_python_workspace": attr.label(default = Label("//:WORKSPACE")),
288    },
289)
290
291def sanitize_platform_name(platform):
292    return platform.replace("-", "_")
293
294def get_host_platform(os_name, arch):
295    """Gets the host platform.
296
297    Args:
298        os_name: the host OS name.
299        arch: the host arch.
300    Returns:
301        The host platform.
302    """
303    host_platform = None
304    for platform, meta in PLATFORMS.items():
305        if meta.os_name == os_name and meta.arch == arch:
306            host_platform = platform
307    if not host_platform:
308        fail("No platform declared for host OS {} on arch {}".format(os_name, arch))
309    return host_platform
310
311def get_host_os_arch(rctx):
312    """Infer the host OS name and arch from a repository context.
313
314    Args:
315        rctx: Bazel's repository_ctx.
316    Returns:
317        A tuple with the host OS name and arch.
318    """
319    os_name = rctx.os.name
320
321    # We assume the arch for Windows is always x86_64.
322    if "windows" in os_name.lower():
323        arch = "x86_64"
324
325        # Normalize the os_name. E.g. os_name could be "OS windows server 2019".
326        os_name = WINDOWS_NAME
327    else:
328        # This is not ideal, but bazel doesn't directly expose arch.
329        arch = rctx.execute([which_with_fail("uname", rctx), "-m"]).stdout.strip()
330
331        # Normalize the os_name.
332        if "mac" in os_name.lower():
333            os_name = MACOS_NAME
334        elif "linux" in os_name.lower():
335            os_name = LINUX_NAME
336
337    return (os_name, arch)
338