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