xref: /aosp_15_r20/external/grpc-grpc/third_party/py/python_configure.bzl (revision cc02d7e222339f7a4f6ba5f422e6413f4bd931f2)
1# Adapted with modifications from tensorflow/third_party/py/
2"""Repository rule for Python autoconfiguration.
3
4`python_configure` depends on the following environment variables:
5
6  * `PYTHON3_BIN_PATH`: location of python binary.
7  * `PYTHON3_LIB_PATH`: Location of python libraries.
8"""
9
10_BAZEL_SH = "BAZEL_SH"
11_PYTHON3_BIN_PATH = "PYTHON3_BIN_PATH"
12_PYTHON3_LIB_PATH = "PYTHON3_LIB_PATH"
13
14_HEADERS_HELP = (
15    "Are Python headers installed? Try installing " +
16    "python3-dev on Debian-based systems. Try python3-devel " +
17    "on Redhat-based systems."
18)
19
20def _tpl(repository_ctx, tpl, substitutions = {}, out = None):
21    if not out:
22        out = tpl
23    repository_ctx.template(
24        out,
25        Label("//third_party/py:%s.tpl" % tpl),
26        substitutions,
27    )
28
29def _fail(msg):
30    """Output failure message when auto configuration fails."""
31    red = "\033[0;31m"
32    no_color = "\033[0m"
33    fail("%sPython Configuration Error:%s %s\n" % (red, no_color, msg))
34
35def _is_windows(repository_ctx):
36    """Returns true if the host operating system is windows."""
37    os_name = repository_ctx.os.name.lower()
38    return os_name.find("windows") != -1
39
40def _execute(
41        repository_ctx,
42        cmdline,
43        error_msg = None,
44        error_details = None,
45        empty_stdout_fine = False):
46    """Executes an arbitrary shell command.
47
48    Args:
49        repository_ctx: the repository_ctx object
50        cmdline: list of strings, the command to execute
51        error_msg: string, a summary of the error if the command fails
52        error_details: string, details about the error or steps to fix it
53        empty_stdout_fine: bool, if True, an empty stdout result is fine, otherwise
54        it's an error
55    Return:
56        the result of repository_ctx.execute(cmdline)
57  """
58    result = repository_ctx.execute(cmdline)
59    if result.stderr or not (empty_stdout_fine or result.stdout):
60        _fail("\n".join([
61            error_msg.strip() if error_msg else "Repository command failed",
62            result.stderr.strip(),
63            error_details if error_details else "",
64        ]))
65    else:
66        return result
67
68def _read_dir(repository_ctx, src_dir):
69    """Returns a string with all files in a directory.
70
71  Finds all files inside a directory, traversing subfolders and following
72  symlinks. The returned string contains the full path of all files
73  separated by line breaks.
74  """
75    if _is_windows(repository_ctx):
76        src_dir = src_dir.replace("/", "\\")
77        find_result = _execute(
78            repository_ctx,
79            ["cmd.exe", "/c", "dir", src_dir, "/b", "/s", "/a-d"],
80            empty_stdout_fine = True,
81        )
82
83        # src_files will be used in genrule.outs where the paths must
84        # use forward slashes.
85        return find_result.stdout.replace("\\", "/")
86    else:
87        find_result = _execute(
88            repository_ctx,
89            ["find", src_dir, "-follow", "-type", "f"],
90            empty_stdout_fine = True,
91        )
92        return find_result.stdout
93
94def _genrule(src_dir, genrule_name, command, outs):
95    """Returns a string with a genrule.
96
97  Genrule executes the given command and produces the given outputs.
98  """
99    return ("genrule(\n" + '    name = "' + genrule_name + '",\n' +
100            "    outs = [\n" + outs + "\n    ],\n" + '    cmd = """\n' +
101            command + '\n   """,\n' + ")\n")
102
103def _normalize_path(path):
104    """Returns a path with '/' and remove the trailing slash."""
105    path = path.replace("\\", "/")
106    if path[-1] == "/":
107        path = path[:-1]
108    return path
109
110def _symlink_genrule_for_dir(
111        repository_ctx,
112        src_dir,
113        dest_dir,
114        genrule_name,
115        src_files = [],
116        dest_files = []):
117    """Returns a genrule to symlink(or copy if on Windows) a set of files.
118
119  If src_dir is passed, files will be read from the given directory; otherwise
120  we assume files are in src_files and dest_files
121  """
122    if src_dir != None:
123        src_dir = _normalize_path(src_dir)
124        dest_dir = _normalize_path(dest_dir)
125        files = "\n".join(
126            sorted(_read_dir(repository_ctx, src_dir).splitlines()),
127        )
128
129        # Create a list with the src_dir stripped to use for outputs.
130        dest_files = files.replace(src_dir, "").splitlines()
131        src_files = files.splitlines()
132    command = []
133    outs = []
134    for i in range(len(dest_files)):
135        if dest_files[i] != "":
136            # If we have only one file to link we do not want to use the dest_dir, as
137            # $(@D) will include the full path to the file.
138            dest = "$(@D)/" + dest_dir + dest_files[i] if len(
139                dest_files,
140            ) != 1 else "$(@D)/" + dest_files[i]
141
142            # On Windows, symlink is not supported, so we just copy all the files.
143            cmd = "cp -f" if _is_windows(repository_ctx) else "ln -s"
144            command.append(cmd + ' "%s" "%s"' % (src_files[i], dest))
145            outs.append('        "' + dest_dir + dest_files[i] + '",')
146    return _genrule(
147        src_dir,
148        genrule_name,
149        " && ".join(command),
150        "\n".join(outs),
151    )
152
153def _get_python_bin(repository_ctx, bin_path_key, default_bin_path, allow_absent):
154    """Gets the python bin path."""
155    python_bin = repository_ctx.os.environ.get(bin_path_key, default_bin_path)
156    if not repository_ctx.path(python_bin).exists:
157        # It's a command, use 'which' to find its path.
158        python_bin_path = repository_ctx.which(python_bin)
159    else:
160        # It's a path, use it as it is.
161        python_bin_path = python_bin
162    if python_bin_path != None:
163        return str(python_bin_path)
164    if not allow_absent:
165        _fail("Cannot find python in PATH, please make sure " +
166              "python is installed and add its directory in PATH, or --define " +
167              "%s='/something/else'.\nPATH=%s" %
168              (bin_path_key, repository_ctx.os.environ.get("PATH", "")))
169    else:
170        return None
171
172def _get_bash_bin(repository_ctx):
173    """Gets the bash bin path."""
174    bash_bin = repository_ctx.os.environ.get(_BAZEL_SH)
175    if bash_bin != None:
176        return bash_bin
177    else:
178        bash_bin_path = repository_ctx.which("bash")
179        if bash_bin_path != None:
180            return str(bash_bin_path)
181        else:
182            _fail(
183                "Cannot find bash in PATH, please make sure " +
184                "bash is installed and add its directory in PATH, or --define " +
185                "%s='/path/to/bash'.\nPATH=%s" %
186                (_BAZEL_SH, repository_ctx.os.environ.get("PATH", "")),
187            )
188
189def _get_python_lib(repository_ctx, python_bin, lib_path_key):
190    """Gets the python lib path."""
191    python_lib = repository_ctx.os.environ.get(lib_path_key)
192    if python_lib != None:
193        return python_lib
194    print_lib = (
195        "<<END\n" + "from __future__ import print_function\n" +
196        "import site\n" + "import os\n" + "\n" + "try:\n" +
197        "  input = raw_input\n" + "except NameError:\n" + "  pass\n" + "\n" +
198        "python_paths = []\n" + "if os.getenv('PYTHONPATH') is not None:\n" +
199        "  python_paths = os.getenv('PYTHONPATH').split(':')\n" + "try:\n" +
200        "  library_paths = site.getsitepackages()\n" +
201        "except AttributeError:\n" +
202        " import sysconfig\n" +
203        " library_paths = [sysconfig.get_path('purelib')]\n" +
204        "all_paths = set(python_paths + library_paths)\n" + "paths = []\n" +
205        "for path in all_paths:\n" + "  if os.path.isdir(path):\n" +
206        "    paths.append(path)\n" + "if len(paths) >=1:\n" +
207        "  print(paths[0])\n" + "END"
208    )
209    cmd = '"%s" - %s' % (python_bin, print_lib)
210    result = repository_ctx.execute([_get_bash_bin(repository_ctx), "-c", cmd])
211    return result.stdout.strip("\n")
212
213def _check_python_lib(repository_ctx, python_lib):
214    """Checks the python lib path."""
215    cmd = 'test -d "%s" -a -x "%s"' % (python_lib, python_lib)
216    result = repository_ctx.execute([_get_bash_bin(repository_ctx), "-c", cmd])
217    if result.return_code == 1:
218        _fail("Invalid python library path: %s" % python_lib)
219
220def _check_python_bin(repository_ctx, python_bin, bin_path_key, allow_absent):
221    """Checks the python bin path."""
222    cmd = '[[ -x "%s" ]] && [[ ! -d "%s" ]]' % (python_bin, python_bin)
223    result = repository_ctx.execute([_get_bash_bin(repository_ctx), "-c", cmd])
224    if result.return_code == 1:
225        if not allow_absent:
226            _fail("--define %s='%s' is not executable. Is it the python binary?" %
227                  (bin_path_key, python_bin))
228        else:
229            return None
230    return True
231
232def _get_python_include(repository_ctx, python_bin):
233    """Gets the python include path."""
234    result = _execute(
235        repository_ctx,
236        [
237            python_bin,
238            "-c",
239            "from __future__ import print_function;" +
240            "import sysconfig;" +
241            "print(sysconfig.get_path('include'))",
242        ],
243        error_msg = "Problem getting python include path for {}.".format(python_bin),
244        error_details = (
245            "Is the Python binary path set up right? " + "(See ./configure or " +
246            python_bin + ".) " + _HEADERS_HELP
247        ),
248    )
249    include_path = result.stdout.splitlines()[0]
250    _execute(
251        repository_ctx,
252        [
253            python_bin,
254            "-c",
255            "import os;" +
256            "main_header = os.path.join(r'{}', 'Python.h');".format(include_path) +
257            "assert os.path.exists(main_header), main_header + ' does not exist.'",
258        ],
259        error_msg = "Unable to find Python headers for {}".format(python_bin),
260        error_details = _HEADERS_HELP,
261        empty_stdout_fine = True,
262    )
263    return include_path
264
265def _get_python_import_lib_name(repository_ctx, python_bin, bin_path_key):
266    """Get Python import library name (pythonXY.lib) on Windows."""
267    result = _execute(
268        repository_ctx,
269        [
270            python_bin,
271            "-c",
272            "import sys;" + 'print("python" + str(sys.version_info[0]) + ' +
273            '      str(sys.version_info[1]) + ".lib")',
274        ],
275        error_msg = "Problem getting python import library.",
276        error_details = ("Is the Python binary path set up right? " +
277                         "(See ./configure or " + bin_path_key + ".) "),
278    )
279    return result.stdout.splitlines()[0]
280
281def _create_single_version_package(
282        repository_ctx,
283        variety_name,
284        bin_path_key,
285        default_bin_path,
286        lib_path_key,
287        allow_absent):
288    """Creates the repository containing files set up to build with Python."""
289    empty_include_rule = "filegroup(\n  name=\"{}_include\",\n  srcs=[],\n)".format(variety_name)
290
291    python_bin = _get_python_bin(repository_ctx, bin_path_key, default_bin_path, allow_absent)
292    if (python_bin == None or
293        _check_python_bin(repository_ctx,
294                          python_bin,
295                          bin_path_key,
296                          allow_absent) == None) and allow_absent:
297            python_include_rule = empty_include_rule
298    else:
299        python_lib = _get_python_lib(repository_ctx, python_bin, lib_path_key)
300        _check_python_lib(repository_ctx, python_lib)
301        python_include = _get_python_include(repository_ctx, python_bin)
302        python_include_rule = _symlink_genrule_for_dir(
303            repository_ctx,
304            python_include,
305            "{}_include".format(variety_name),
306            "{}_include".format(variety_name),
307        )
308    python_import_lib_genrule = ""
309
310    # To build Python C/C++ extension on Windows, we need to link to python import library pythonXY.lib
311    # See https://docs.python.org/3/extending/windows.html
312    if _is_windows(repository_ctx):
313        python_include = _normalize_path(python_include)
314        python_import_lib_name = _get_python_import_lib_name(
315            repository_ctx,
316            python_bin,
317            bin_path_key,
318        )
319        python_import_lib_src = python_include.rsplit(
320            "/",
321            1,
322        )[0] + "/libs/" + python_import_lib_name
323        python_import_lib_genrule = _symlink_genrule_for_dir(
324            repository_ctx,
325            None,
326            "",
327            "{}_import_lib".format(variety_name),
328            [python_import_lib_src],
329            [python_import_lib_name],
330        )
331    _tpl(
332        repository_ctx,
333        "variety",
334        {
335            "%{PYTHON_INCLUDE_GENRULE}": python_include_rule,
336            "%{PYTHON_IMPORT_LIB_GENRULE}": python_import_lib_genrule,
337            "%{VARIETY_NAME}": variety_name,
338        },
339        out = "{}/BUILD".format(variety_name),
340    )
341
342def _python_autoconf_impl(repository_ctx):
343    """Implementation of the python_autoconf repository rule."""
344    _create_single_version_package(
345        repository_ctx,
346        "_python3",
347        _PYTHON3_BIN_PATH,
348        "python3" if not _is_windows(repository_ctx) else "python.exe",
349        _PYTHON3_LIB_PATH,
350        False
351    )
352    _tpl(repository_ctx, "BUILD")
353
354python_configure = repository_rule(
355    implementation = _python_autoconf_impl,
356    environ = [
357        _BAZEL_SH,
358        _PYTHON3_BIN_PATH,
359        _PYTHON3_LIB_PATH,
360    ],
361    attrs = {
362        "_build_tpl": attr.label(
363            default = Label("//third_party/py:BUILD.tpl"),
364            allow_single_file = True,
365        ),
366        "_variety_tpl": attr.label(
367            default = Label("//third_party/py:variety.tpl"),
368            allow_single_file = True,
369        ),
370    },
371)
372"""Detects and configures the local Python.
373
374It expects the system have a working Python 3 installation.
375
376Add the following to your WORKSPACE FILE:
377
378```python
379python_configure(name = "local_config_python")
380```
381
382Args:
383  name: A unique name for this workspace rule.
384"""
385