• 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 os import system;"
128    cmd += "system(\"%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            "-c",
157            "from __future__ import print_function;" +
158            "from distutils import sysconfig;" +
159            "print(sysconfig.get_python_inc())",
160        ],
161        error_msg = "Problem getting python include path.",
162        error_details = ("Is the Python binary path set up right? " +
163                         "(See ./configure or " + PYTHON_BIN_PATH + ".) " +
164                         "Is distutils installed?"),
165    )
166    return result.stdout.splitlines()[0]
167
168def _get_python_import_lib_name(repository_ctx, python_bin):
169    """Get Python import library name (pythonXY.lib) on Windows."""
170    result = execute(
171        repository_ctx,
172        [
173            python_bin,
174            "-c",
175            "import sys;" +
176            'print("python" + str(sys.version_info[0]) + ' +
177            '      str(sys.version_info[1]) + ".lib")',
178        ],
179        error_msg = "Problem getting python import library.",
180        error_details = ("Is the Python binary path set up right? " +
181                         "(See ./configure or " + PYTHON_BIN_PATH + ".) "),
182    )
183    return result.stdout.splitlines()[0]
184
185def _get_numpy_include(repository_ctx, python_bin):
186    """Gets the numpy include path."""
187    return execute(
188        repository_ctx,
189        [
190            python_bin,
191            "-c",
192            "from __future__ import print_function;" +
193            "import numpy;" +
194            " print(numpy.get_include());",
195        ],
196        error_msg = "Problem getting numpy include path.",
197        error_details = "Is numpy installed?",
198    ).stdout.splitlines()[0]
199
200def _create_local_python_repository(repository_ctx):
201    """Creates the repository containing files set up to build with Python."""
202
203    # Resolve all labels before doing any real work. Resolving causes the
204    # function to be restarted with all previous state being lost. This
205    # can easily lead to a O(n^2) runtime in the number of labels.
206    build_tpl = repository_ctx.path(Label("//third_party/py:BUILD.tpl"))
207
208    python_bin = get_python_bin(repository_ctx)
209    _check_python_bin(repository_ctx, python_bin)
210    python_lib = _get_python_lib(repository_ctx, python_bin)
211    _check_python_lib(repository_ctx, python_lib)
212    python_include = _get_python_include(repository_ctx, python_bin)
213    numpy_include = _get_numpy_include(repository_ctx, python_bin) + "/numpy"
214    python_include_rule = _symlink_genrule_for_dir(
215        repository_ctx,
216        python_include,
217        "python_include",
218        "python_include",
219    )
220    python_import_lib_genrule = ""
221
222    # To build Python C/C++ extension on Windows, we need to link to python import library pythonXY.lib
223    # See https://docs.python.org/3/extending/windows.html
224    if is_windows(repository_ctx):
225        python_include = _norm_path(python_include)
226        python_import_lib_name = _get_python_import_lib_name(repository_ctx, python_bin)
227        python_import_lib_src = python_include.rsplit("/", 1)[0] + "/libs/" + python_import_lib_name
228        python_import_lib_genrule = _symlink_genrule_for_dir(
229            repository_ctx,
230            None,
231            "",
232            "python_import_lib",
233            [python_import_lib_src],
234            [python_import_lib_name],
235        )
236    numpy_include_rule = _symlink_genrule_for_dir(
237        repository_ctx,
238        numpy_include,
239        "numpy_include/numpy",
240        "numpy_include",
241    )
242
243    platform_constraint = ""
244    if repository_ctx.attr.platform_constraint:
245        platform_constraint = "\"%s\"" % repository_ctx.attr.platform_constraint
246    repository_ctx.template("BUILD", build_tpl, {
247        "%{PYTHON_BIN_PATH}": python_bin,
248        "%{PYTHON_INCLUDE_GENRULE}": python_include_rule,
249        "%{PYTHON_IMPORT_LIB_GENRULE}": python_import_lib_genrule,
250        "%{NUMPY_INCLUDE_GENRULE}": numpy_include_rule,
251        "%{PLATFORM_CONSTRAINT}": platform_constraint,
252    })
253
254def _create_remote_python_repository(repository_ctx, remote_config_repo):
255    """Creates pointers to a remotely configured repo set up to build with Python.
256    """
257    repository_ctx.template("BUILD", config_repo_label(remote_config_repo, ":BUILD"), {})
258
259def _python_autoconf_impl(repository_ctx):
260    """Implementation of the python_autoconf repository rule."""
261    if get_host_environ(repository_ctx, TF_PYTHON_CONFIG_REPO) != None:
262        _create_remote_python_repository(
263            repository_ctx,
264            get_host_environ(repository_ctx, TF_PYTHON_CONFIG_REPO),
265        )
266    else:
267        _create_local_python_repository(repository_ctx)
268
269_ENVIRONS = [
270    BAZEL_SH,
271    PYTHON_BIN_PATH,
272    PYTHON_LIB_PATH,
273]
274
275local_python_configure = repository_rule(
276    implementation = _create_local_python_repository,
277    environ = _ENVIRONS,
278    attrs = {
279        "environ": attr.string_dict(),
280        "platform_constraint": attr.string(),
281    },
282)
283
284remote_python_configure = repository_rule(
285    implementation = _create_local_python_repository,
286    environ = _ENVIRONS,
287    remotable = True,
288    attrs = {
289        "environ": attr.string_dict(),
290        "platform_constraint": attr.string(),
291    },
292)
293
294python_configure = repository_rule(
295    implementation = _python_autoconf_impl,
296    environ = _ENVIRONS + [TF_PYTHON_CONFIG_REPO],
297    attrs = {
298        "platform_constraint": attr.string(),
299    },
300)
301"""Detects and configures the local Python.
302
303Add the following to your WORKSPACE FILE:
304
305```python
306python_configure(name = "local_config_python")
307```
308
309Args:
310  name: A unique name for this workspace rule.
311"""
312