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