| """Repository rule for Python autoconfiguration. | |
| `python_configure` depends on the following environment variables: | |
| * `PYTHON_BIN_PATH`: location of python binary. | |
| * `PYTHON_LIB_PATH`: Location of python libraries. | |
| """ | |
| load( | |
| "//third_party/remote_config:common.bzl", | |
| "BAZEL_SH", | |
| "PYTHON_BIN_PATH", | |
| "PYTHON_LIB_PATH", | |
| "TF_PYTHON_CONFIG_REPO", | |
| "auto_config_fail", | |
| "config_repo_label", | |
| "execute", | |
| "get_bash_bin", | |
| "get_host_environ", | |
| "get_python_bin", | |
| "is_windows", | |
| "raw_exec", | |
| "read_dir", | |
| ) | |
| def _genrule(src_dir, genrule_name, command, outs): | |
| """Returns a string with a genrule. | |
| Genrule executes the given command and produces the given outputs. | |
| """ | |
| return ( | |
| "genrule(\n" + | |
| ' name = "' + | |
| genrule_name + '",\n' + | |
| " outs = [\n" + | |
| outs + | |
| "\n ],\n" + | |
| ' cmd = """\n' + | |
| command + | |
| '\n """,\n' + | |
| ")\n" | |
| ) | |
| def _norm_path(path): | |
| """Returns a path with '/' and remove the trailing slash.""" | |
| path = path.replace("\\", "/") | |
| if path[-1] == "/": | |
| path = path[:-1] | |
| return path | |
| def _symlink_genrule_for_dir( | |
| repository_ctx, | |
| src_dir, | |
| dest_dir, | |
| genrule_name, | |
| src_files = [], | |
| dest_files = []): | |
| """Returns a genrule to symlink(or copy if on Windows) a set of files. | |
| If src_dir is passed, files will be read from the given directory; otherwise | |
| we assume files are in src_files and dest_files | |
| """ | |
| if src_dir != None: | |
| src_dir = _norm_path(src_dir) | |
| dest_dir = _norm_path(dest_dir) | |
| files = "\n".join(read_dir(repository_ctx, src_dir)) | |
| # Create a list with the src_dir stripped to use for outputs. | |
| dest_files = files.replace(src_dir, "").splitlines() | |
| src_files = files.splitlines() | |
| command = [] | |
| outs = [] | |
| for i in range(len(dest_files)): | |
| if dest_files[i] != "": | |
| # If we have only one file to link we do not want to use the dest_dir, as | |
| # $(@D) will include the full path to the file. | |
| dest = "$(@D)/" + dest_dir + dest_files[i] if len(dest_files) != 1 else "$(@D)/" + dest_files[i] | |
| # Copy the headers to create a sandboxable setup. | |
| cmd = "cp -f" | |
| command.append(cmd + ' "%s" "%s"' % (src_files[i], dest)) | |
| outs.append(' "' + dest_dir + dest_files[i] + '",') | |
| genrule = _genrule( | |
| src_dir, | |
| genrule_name, | |
| " && ".join(command), | |
| "\n".join(outs), | |
| ) | |
| return genrule | |
| def _get_python_lib(repository_ctx, python_bin): | |
| """Gets the python lib path.""" | |
| python_lib = get_host_environ(repository_ctx, PYTHON_LIB_PATH) | |
| if python_lib != None: | |
| return python_lib | |
| # The interesting program to execute. | |
| print_lib = [ | |
| "from __future__ import print_function", | |
| "import site", | |
| "import os", | |
| "python_paths = []", | |
| "if os.getenv('PYTHONPATH') is not None:", | |
| " python_paths = os.getenv('PYTHONPATH').split(':')", | |
| "try:", | |
| " library_paths = site.getsitepackages()", | |
| "except AttributeError:", | |
| " from distutils.sysconfig import get_python_lib", | |
| " library_paths = [get_python_lib()]", | |
| "all_paths = set(python_paths + library_paths)", | |
| "paths = []", | |
| "for path in all_paths:", | |
| " if os.path.isdir(path):", | |
| " paths.append(path)", | |
| "if len(paths) >=1:", | |
| " print(paths[0])", | |
| ] | |
| # The below script writes the above program to a file | |
| # and executes it. This is to work around the limitation | |
| # of not being able to upload files as part of execute. | |
| cmd = "from os import linesep;" | |
| cmd += "f = open('script.py', 'w');" | |
| for line in print_lib: | |
| cmd += "f.write(\"%s\" + linesep);" % line | |
| cmd += "f.close();" | |
| cmd += "from os import system;" | |
| cmd += "system(r\"%s script.py\");" % python_bin | |
| result = execute(repository_ctx, [python_bin, "-c", cmd]) | |
| return result.stdout.strip() | |
| def _check_python_lib(repository_ctx, python_lib): | |
| """Checks the python lib path.""" | |
| cmd = 'test -d "%s" -a -x "%s"' % (python_lib, python_lib) | |
| result = raw_exec(repository_ctx, [get_bash_bin(repository_ctx), "-c", cmd]) | |
| if result.return_code == 1: | |
| auto_config_fail("Invalid python library path: %s" % python_lib) | |
| def _check_python_bin(repository_ctx, python_bin): | |
| """Checks the python bin path.""" | |
| cmd = '[[ -x "%s" ]] && [[ ! -d "%s" ]]' % (python_bin, python_bin) | |
| result = raw_exec(repository_ctx, [get_bash_bin(repository_ctx), "-c", cmd]) | |
| if result.return_code == 1: | |
| auto_config_fail("--define %s='%s' is not executable. Is it the python binary?" % ( | |
| PYTHON_BIN_PATH, | |
| python_bin, | |
| )) | |
| def _get_python_include(repository_ctx, python_bin): | |
| """Gets the python include path.""" | |
| result = execute( | |
| repository_ctx, | |
| [ | |
| python_bin, | |
| "-W ignore", | |
| "-c", | |
| "from __future__ import print_function;" + | |
| "from distutils import sysconfig;" + | |
| "print(sysconfig.get_python_inc())", | |
| ], | |
| error_msg = "Problem getting python include path.", | |
| error_details = ("Is the Python binary path set up right? " + | |
| "(See ./configure or " + PYTHON_BIN_PATH + ".) " + | |
| "Is distutils installed?"), | |
| ) | |
| return result.stdout.splitlines()[0] | |
| def _get_python_import_lib_name(repository_ctx, python_bin): | |
| """Get Python import library name (pythonXY.lib) on Windows.""" | |
| result = execute( | |
| repository_ctx, | |
| [ | |
| python_bin, | |
| "-c", | |
| "import sys;" + | |
| 'print("python" + str(sys.version_info[0]) + ' + | |
| ' str(sys.version_info[1]) + ".lib")', | |
| ], | |
| error_msg = "Problem getting python import library.", | |
| error_details = ("Is the Python binary path set up right? " + | |
| "(See ./configure or " + PYTHON_BIN_PATH + ".) "), | |
| ) | |
| return result.stdout.splitlines()[0] | |
| def _get_numpy_include(repository_ctx, python_bin): | |
| """Gets the numpy include path.""" | |
| return execute( | |
| repository_ctx, | |
| [ | |
| python_bin, | |
| "-W ignore", | |
| "-c", | |
| "from __future__ import print_function;" + | |
| "import numpy;" + | |
| " print(numpy.get_include());", | |
| ], | |
| error_msg = "Problem getting numpy include path.", | |
| error_details = "Is numpy installed?", | |
| ).stdout.splitlines()[0] | |
| def _create_local_python_repository(repository_ctx): | |
| """Creates the repository containing files set up to build with Python.""" | |
| # Resolve all labels before doing any real work. Resolving causes the | |
| # function to be restarted with all previous state being lost. This | |
| # can easily lead to a O(n^2) runtime in the number of labels. | |
| build_tpl = repository_ctx.path(Label("//third_party/py:BUILD.tpl")) | |
| python_bin = get_python_bin(repository_ctx) | |
| _check_python_bin(repository_ctx, python_bin) | |
| python_lib = _get_python_lib(repository_ctx, python_bin) | |
| _check_python_lib(repository_ctx, python_lib) | |
| python_include = _get_python_include(repository_ctx, python_bin) | |
| numpy_include = _get_numpy_include(repository_ctx, python_bin) + "/numpy" | |
| python_include_rule = _symlink_genrule_for_dir( | |
| repository_ctx, | |
| python_include, | |
| "python_include", | |
| "python_include", | |
| ) | |
| python_import_lib_genrule = "" | |
| # To build Python C/C++ extension on Windows, we need to link to python import library pythonXY.lib | |
| # See https://docs.python.org/3/extending/windows.html | |
| if is_windows(repository_ctx): | |
| python_include = _norm_path(python_include) | |
| python_import_lib_name = _get_python_import_lib_name(repository_ctx, python_bin) | |
| python_import_lib_src = python_include.rsplit("/", 1)[0] + "/libs/" + python_import_lib_name | |
| python_import_lib_genrule = _symlink_genrule_for_dir( | |
| repository_ctx, | |
| None, | |
| "", | |
| "python_import_lib", | |
| [python_import_lib_src], | |
| [python_import_lib_name], | |
| ) | |
| numpy_include_rule = _symlink_genrule_for_dir( | |
| repository_ctx, | |
| numpy_include, | |
| "numpy_include/numpy", | |
| "numpy_include", | |
| ) | |
| platform_constraint = "" | |
| if repository_ctx.attr.platform_constraint: | |
| platform_constraint = "\"%s\"" % repository_ctx.attr.platform_constraint | |
| repository_ctx.template("BUILD", build_tpl, { | |
| "%{PYTHON_BIN_PATH}": python_bin, | |
| "%{PYTHON_INCLUDE_GENRULE}": python_include_rule, | |
| "%{PYTHON_IMPORT_LIB_GENRULE}": python_import_lib_genrule, | |
| "%{NUMPY_INCLUDE_GENRULE}": numpy_include_rule, | |
| "%{PLATFORM_CONSTRAINT}": platform_constraint, | |
| }) | |
| def _create_remote_python_repository(repository_ctx, remote_config_repo): | |
| """Creates pointers to a remotely configured repo set up to build with Python. | |
| """ | |
| repository_ctx.template("BUILD", config_repo_label(remote_config_repo, ":BUILD"), {}) | |
| def _python_autoconf_impl(repository_ctx): | |
| """Implementation of the python_autoconf repository rule.""" | |
| if get_host_environ(repository_ctx, TF_PYTHON_CONFIG_REPO) != None: | |
| _create_remote_python_repository( | |
| repository_ctx, | |
| get_host_environ(repository_ctx, TF_PYTHON_CONFIG_REPO), | |
| ) | |
| else: | |
| _create_local_python_repository(repository_ctx) | |
| _ENVIRONS = [ | |
| BAZEL_SH, | |
| PYTHON_BIN_PATH, | |
| PYTHON_LIB_PATH, | |
| ] | |
| local_python_configure = repository_rule( | |
| implementation = _create_local_python_repository, | |
| environ = _ENVIRONS, | |
| attrs = { | |
| "environ": attr.string_dict(), | |
| "platform_constraint": attr.string(), | |
| }, | |
| ) | |
| remote_python_configure = repository_rule( | |
| implementation = _create_local_python_repository, | |
| environ = _ENVIRONS, | |
| remotable = True, | |
| attrs = { | |
| "environ": attr.string_dict(), | |
| "platform_constraint": attr.string(), | |
| }, | |
| ) | |
| python_configure = repository_rule( | |
| implementation = _python_autoconf_impl, | |
| environ = _ENVIRONS + [TF_PYTHON_CONFIG_REPO], | |
| attrs = { | |
| "platform_constraint": attr.string(), | |
| }, | |
| ) | |
| """Detects and configures the local Python. | |
| Add the following to your WORKSPACE FILE: | |
| ```python | |
| python_configure(name = "local_config_python") | |
| ``` | |
| Args: | |
| name: A unique name for this workspace rule. | |
| """ | |