• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Repository rule for Python autoconfiguration.
2
3`python_configure` depends on the following environment variables:
4
5  * `PYTHON_BIN_PATH`: location of python binary.
6  * `PYTHON_LIB_PATH`: Location of python libraries.
7"""
8
9load(
10    "//third_party/remote_config:common.bzl",
11    "BAZEL_SH",
12    "PYTHON_BIN_PATH",
13    "PYTHON_LIB_PATH",
14    "TF_PYTHON_CONFIG_REPO",
15    "auto_config_fail",
16    "config_repo_label",
17    "execute",
18    "get_bash_bin",
19    "get_host_environ",
20    "get_python_bin",
21    "is_windows",
22    "raw_exec",
23    "read_dir",
24)
25
26def _genrule(src_dir, genrule_name, command, outs):
27    """Returns a string with a genrule.
28
29    Genrule executes the given command and produces the given outputs.
30    """
31    return (
32        "genrule(\n" +
33        '    name = "' +
34        genrule_name + '",\n' +
35        "    outs = [\n" +
36        outs +
37        "\n    ],\n" +
38        '    cmd = """\n' +
39        command +
40        '\n   """,\n' +
41        ")\n"
42    )
43
44def _norm_path(path):
45    """Returns a path with '/' and remove the trailing slash."""
46    path = path.replace("\\", "/")
47    if path[-1] == "/":
48        path = path[:-1]
49    return path
50
51def _symlink_genrule_for_dir(
52        repository_ctx,
53        src_dir,
54        dest_dir,
55        genrule_name,
56        src_files = [],
57        dest_files = []):
58    """Returns a genrule to symlink(or copy if on Windows) a set of files.
59
60    If src_dir is passed, files will be read from the given directory; otherwise
61    we assume files are in src_files and dest_files
62    """
63    if src_dir != None:
64        src_dir = _norm_path(src_dir)
65        dest_dir = _norm_path(dest_dir)
66        files = "\n".join(read_dir(repository_ctx, src_dir))
67
68        # Create a list with the src_dir stripped to use for outputs.
69        dest_files = files.replace(src_dir, "").splitlines()
70        src_files = files.splitlines()
71    command = []
72    outs = []
73    for i in range(len(dest_files)):
74        if dest_files[i] != "":
75            # If we have only one file to link we do not want to use the dest_dir, as
76            # $(@D) will include the full path to the file.
77            dest = "$(@D)/" + dest_dir + dest_files[i] if len(dest_files) != 1 else "$(@D)/" + dest_files[i]
78
79            # Copy the headers to create a sandboxable setup.
80            cmd = "cp -f"
81            command.append(cmd + ' "%s" "%s"' % (src_files[i], dest))
82            outs.append('        "' + dest_dir + dest_files[i] + '",')
83    genrule = _genrule(
84        src_dir,
85        genrule_name,
86        " && ".join(command),
87        "\n".join(outs),
88    )
89    return genrule
90
91def _get_python_lib(repository_ctx, python_bin):
92    """Gets the python lib path."""
93    python_lib = get_host_environ(repository_ctx, PYTHON_LIB_PATH)
94    if python_lib != None:
95        return python_lib
96
97    # The interesting program to execute.
98    print_lib = [
99        "from __future__ import print_function",
100        "import site",
101        "import os",
102        "python_paths = []",
103        "if os.getenv('PYTHONPATH') is not None:",
104        "  python_paths = os.getenv('PYTHONPATH').split(':')",
105        "try:",
106        "  library_paths = site.getsitepackages()",
107        "except AttributeError:",
108        "  from distutils.sysconfig import get_python_lib",
109        "  library_paths = [get_python_lib()]",
110        "all_paths = set(python_paths + library_paths)",
111        "paths = []",
112        "for path in all_paths:",
113        "  if os.path.isdir(path):",
114        "    paths.append(path)",
115        "if len(paths) >=1:",
116        "  print(paths[0])",
117    ]
118
119    # The below script writes the above program to a file
120    # and executes it. This is to work around the limitation
121    # of not being able to upload files as part of execute.
122    cmd = "from os import linesep;"
123    cmd += "f = open('script.py', 'w');"
124    for line in print_lib:
125        cmd += "f.write(\"%s\" + linesep);" % line
126    cmd += "f.close();"
127    cmd += "from subprocess import call;"
128    cmd += "call([\"%s\", \"script.py\"]);" % python_bin
129
130    result = execute(repository_ctx, [python_bin, "-c", cmd])
131    return result.stdout.strip()
132
133def _check_python_lib(repository_ctx, python_lib):
134    """Checks the python lib path."""
135    cmd = 'test -d "%s" -a -x "%s"' % (python_lib, python_lib)
136    result = raw_exec(repository_ctx, [get_bash_bin(repository_ctx), "-c", cmd])
137    if result.return_code == 1:
138        auto_config_fail("Invalid python library path: %s" % python_lib)
139
140def _check_python_bin(repository_ctx, python_bin):
141    """Checks the python bin path."""
142    cmd = '[[ -x "%s" ]] && [[ ! -d "%s" ]]' % (python_bin, python_bin)
143    result = raw_exec(repository_ctx, [get_bash_bin(repository_ctx), "-c", cmd])
144    if result.return_code == 1:
145        auto_config_fail("--define %s='%s' is not executable. Is it the python binary?" % (
146            PYTHON_BIN_PATH,
147            python_bin,
148        ))
149
150def _get_python_include(repository_ctx, python_bin):
151    """Gets the python include path."""
152    result = execute(
153        repository_ctx,
154        [
155            python_bin,
156            "-Wignore",
157            "-c",
158            "import importlib; " +
159            "import importlib.util; " +
160            "print(importlib.import_module('distutils.sysconfig').get_python_inc() " +
161            "if importlib.util.find_spec('distutils.sysconfig') " +
162            "else importlib.import_module('sysconfig').get_path('include'))",
163        ],
164        error_msg = "Problem getting python include path.",
165        error_details = ("Is the Python binary path set up right? " +
166                         "(See ./configure or " + PYTHON_BIN_PATH + ".) " +
167                         "Is distutils installed?"),
168    )
169    return result.stdout.splitlines()[0]
170
171def _get_python_import_lib_name(repository_ctx, python_bin):
172    """Get Python import library name (pythonXY.lib) on Windows."""
173    result = execute(
174        repository_ctx,
175        [
176            python_bin,
177            "-c",
178            "import sys;" +
179            'print("python" + str(sys.version_info[0]) + ' +
180            '      str(sys.version_info[1]) + ".lib")',
181        ],
182        error_msg = "Problem getting python import library.",
183        error_details = ("Is the Python binary path set up right? " +
184                         "(See ./configure or " + PYTHON_BIN_PATH + ".) "),
185    )
186    return result.stdout.splitlines()[0]
187
188def _get_numpy_include(repository_ctx, python_bin):
189    """Gets the numpy include path."""
190    return execute(
191        repository_ctx,
192        [
193            python_bin,
194            "-c",
195            "from __future__ import print_function;" +
196            "import numpy;" +
197            " print(numpy.get_include());",
198        ],
199        error_msg = "Problem getting numpy include path.",
200        error_details = "Is numpy installed?",
201    ).stdout.splitlines()[0]
202
203def _create_local_python_repository(repository_ctx):
204    """Creates the repository containing files set up to build with Python."""
205
206    # Resolve all labels before doing any real work. Resolving causes the
207    # function to be restarted with all previous state being lost. This
208    # can easily lead to a O(n^2) runtime in the number of labels.
209    build_tpl = repository_ctx.path(Label("//third_party/py:BUILD.tpl"))
210
211    python_bin = get_python_bin(repository_ctx)
212    _check_python_bin(repository_ctx, python_bin)
213    python_lib = _get_python_lib(repository_ctx, python_bin)
214    _check_python_lib(repository_ctx, python_lib)
215    python_include = _get_python_include(repository_ctx, python_bin)
216    numpy_include = _get_numpy_include(repository_ctx, python_bin) + "/numpy"
217    python_include_rule = _symlink_genrule_for_dir(
218        repository_ctx,
219        python_include,
220        "python_include",
221        "python_include",
222    )
223    python_import_lib_genrule = ""
224
225    # To build Python C/C++ extension on Windows, we need to link to python import library pythonXY.lib
226    # See https://docs.python.org/3/extending/windows.html
227    if is_windows(repository_ctx):
228        python_bin = python_bin.replace("\\", "/")
229        python_include = _norm_path(python_include)
230        python_import_lib_name = _get_python_import_lib_name(repository_ctx, python_bin)
231        python_import_lib_src = python_include.rsplit("/", 1)[0] + "/libs/" + python_import_lib_name
232        python_import_lib_genrule = _symlink_genrule_for_dir(
233            repository_ctx,
234            None,
235            "",
236            "python_import_lib",
237            [python_import_lib_src],
238            [python_import_lib_name],
239        )
240    numpy_include_rule = _symlink_genrule_for_dir(
241        repository_ctx,
242        numpy_include,
243        "numpy_include/numpy",
244        "numpy_include",
245    )
246
247    platform_constraint = ""
248    if repository_ctx.attr.platform_constraint:
249        platform_constraint = "\"%s\"" % repository_ctx.attr.platform_constraint
250    repository_ctx.template("BUILD", build_tpl, {
251        "%{PYTHON_BIN_PATH}": python_bin,
252        "%{PYTHON_INCLUDE_GENRULE}": python_include_rule,
253        "%{PYTHON_IMPORT_LIB_GENRULE}": python_import_lib_genrule,
254        "%{NUMPY_INCLUDE_GENRULE}": numpy_include_rule,
255        "%{PLATFORM_CONSTRAINT}": platform_constraint,
256    })
257
258def _create_remote_python_repository(repository_ctx, remote_config_repo):
259    """Creates pointers to a remotely configured repo set up to build with Python.
260    """
261    repository_ctx.template("BUILD", config_repo_label(remote_config_repo, ":BUILD"), {})
262
263def _python_autoconf_impl(repository_ctx):
264    """Implementation of the python_autoconf repository rule."""
265    if get_host_environ(repository_ctx, TF_PYTHON_CONFIG_REPO) != None:
266        _create_remote_python_repository(
267            repository_ctx,
268            get_host_environ(repository_ctx, TF_PYTHON_CONFIG_REPO),
269        )
270    else:
271        _create_local_python_repository(repository_ctx)
272
273_ENVIRONS = [
274    BAZEL_SH,
275    PYTHON_BIN_PATH,
276    PYTHON_LIB_PATH,
277]
278
279local_python_configure = repository_rule(
280    implementation = _create_local_python_repository,
281    environ = _ENVIRONS,
282    attrs = {
283        "environ": attr.string_dict(),
284        "platform_constraint": attr.string(),
285    },
286)
287
288remote_python_configure = repository_rule(
289    implementation = _create_local_python_repository,
290    environ = _ENVIRONS,
291    remotable = True,
292    attrs = {
293        "environ": attr.string_dict(),
294        "platform_constraint": attr.string(),
295    },
296)
297
298python_configure = repository_rule(
299    implementation = _python_autoconf_impl,
300    environ = _ENVIRONS + [TF_PYTHON_CONFIG_REPO],
301    attrs = {
302        "platform_constraint": attr.string(),
303    },
304)
305"""Detects and configures the local Python.
306
307Add the following to your WORKSPACE FILE:
308
309```python
310python_configure(name = "local_config_python")
311```
312
313Args:
314  name: A unique name for this workspace rule.
315"""
316