1# Adapted with modifications from tensorflow/third_party/py/ 2"""Repository rule for Python autoconfiguration. 3 4`python_configure` depends on the following environment variables: 5 6 * `PYTHON3_BIN_PATH`: location of python binary. 7 * `PYTHON3_LIB_PATH`: Location of python libraries. 8""" 9 10_BAZEL_SH = "BAZEL_SH" 11_PYTHON3_BIN_PATH = "PYTHON3_BIN_PATH" 12_PYTHON3_LIB_PATH = "PYTHON3_LIB_PATH" 13 14_HEADERS_HELP = ( 15 "Are Python headers installed? Try installing " + 16 "python3-dev on Debian-based systems. Try python3-devel " + 17 "on Redhat-based systems." 18) 19 20def _tpl(repository_ctx, tpl, substitutions = {}, out = None): 21 if not out: 22 out = tpl 23 repository_ctx.template( 24 out, 25 Label("//third_party/py:%s.tpl" % tpl), 26 substitutions, 27 ) 28 29def _fail(msg): 30 """Output failure message when auto configuration fails.""" 31 red = "\033[0;31m" 32 no_color = "\033[0m" 33 fail("%sPython Configuration Error:%s %s\n" % (red, no_color, msg)) 34 35def _is_windows(repository_ctx): 36 """Returns true if the host operating system is windows.""" 37 os_name = repository_ctx.os.name.lower() 38 return os_name.find("windows") != -1 39 40def _execute( 41 repository_ctx, 42 cmdline, 43 error_msg = None, 44 error_details = None, 45 empty_stdout_fine = False): 46 """Executes an arbitrary shell command. 47 48 Args: 49 repository_ctx: the repository_ctx object 50 cmdline: list of strings, the command to execute 51 error_msg: string, a summary of the error if the command fails 52 error_details: string, details about the error or steps to fix it 53 empty_stdout_fine: bool, if True, an empty stdout result is fine, otherwise 54 it's an error 55 Return: 56 the result of repository_ctx.execute(cmdline) 57 """ 58 result = repository_ctx.execute(cmdline) 59 if result.stderr or not (empty_stdout_fine or result.stdout): 60 _fail("\n".join([ 61 error_msg.strip() if error_msg else "Repository command failed", 62 result.stderr.strip(), 63 error_details if error_details else "", 64 ])) 65 else: 66 return result 67 68def _read_dir(repository_ctx, src_dir): 69 """Returns a string with all files in a directory. 70 71 Finds all files inside a directory, traversing subfolders and following 72 symlinks. The returned string contains the full path of all files 73 separated by line breaks. 74 """ 75 if _is_windows(repository_ctx): 76 src_dir = src_dir.replace("/", "\\") 77 find_result = _execute( 78 repository_ctx, 79 ["cmd.exe", "/c", "dir", src_dir, "/b", "/s", "/a-d"], 80 empty_stdout_fine = True, 81 ) 82 83 # src_files will be used in genrule.outs where the paths must 84 # use forward slashes. 85 return find_result.stdout.replace("\\", "/") 86 else: 87 find_result = _execute( 88 repository_ctx, 89 ["find", src_dir, "-follow", "-type", "f"], 90 empty_stdout_fine = True, 91 ) 92 return find_result.stdout 93 94def _genrule(src_dir, genrule_name, command, outs): 95 """Returns a string with a genrule. 96 97 Genrule executes the given command and produces the given outputs. 98 """ 99 return ("genrule(\n" + ' name = "' + genrule_name + '",\n' + 100 " outs = [\n" + outs + "\n ],\n" + ' cmd = """\n' + 101 command + '\n """,\n' + ")\n") 102 103def _normalize_path(path): 104 """Returns a path with '/' and remove the trailing slash.""" 105 path = path.replace("\\", "/") 106 if path[-1] == "/": 107 path = path[:-1] 108 return path 109 110def _symlink_genrule_for_dir( 111 repository_ctx, 112 src_dir, 113 dest_dir, 114 genrule_name, 115 src_files = [], 116 dest_files = []): 117 """Returns a genrule to symlink(or copy if on Windows) a set of files. 118 119 If src_dir is passed, files will be read from the given directory; otherwise 120 we assume files are in src_files and dest_files 121 """ 122 if src_dir != None: 123 src_dir = _normalize_path(src_dir) 124 dest_dir = _normalize_path(dest_dir) 125 files = "\n".join( 126 sorted(_read_dir(repository_ctx, src_dir).splitlines()), 127 ) 128 129 # Create a list with the src_dir stripped to use for outputs. 130 dest_files = files.replace(src_dir, "").splitlines() 131 src_files = files.splitlines() 132 command = [] 133 outs = [] 134 for i in range(len(dest_files)): 135 if dest_files[i] != "": 136 # If we have only one file to link we do not want to use the dest_dir, as 137 # $(@D) will include the full path to the file. 138 dest = "$(@D)/" + dest_dir + dest_files[i] if len( 139 dest_files, 140 ) != 1 else "$(@D)/" + dest_files[i] 141 142 # On Windows, symlink is not supported, so we just copy all the files. 143 cmd = "cp -f" if _is_windows(repository_ctx) else "ln -s" 144 command.append(cmd + ' "%s" "%s"' % (src_files[i], dest)) 145 outs.append(' "' + dest_dir + dest_files[i] + '",') 146 return _genrule( 147 src_dir, 148 genrule_name, 149 " && ".join(command), 150 "\n".join(outs), 151 ) 152 153def _get_python_bin(repository_ctx, bin_path_key, default_bin_path, allow_absent): 154 """Gets the python bin path.""" 155 python_bin = repository_ctx.os.environ.get(bin_path_key, default_bin_path) 156 if not repository_ctx.path(python_bin).exists: 157 # It's a command, use 'which' to find its path. 158 python_bin_path = repository_ctx.which(python_bin) 159 else: 160 # It's a path, use it as it is. 161 python_bin_path = python_bin 162 if python_bin_path != None: 163 return str(python_bin_path) 164 if not allow_absent: 165 _fail("Cannot find python in PATH, please make sure " + 166 "python is installed and add its directory in PATH, or --define " + 167 "%s='/something/else'.\nPATH=%s" % 168 (bin_path_key, repository_ctx.os.environ.get("PATH", ""))) 169 else: 170 return None 171 172def _get_bash_bin(repository_ctx): 173 """Gets the bash bin path.""" 174 bash_bin = repository_ctx.os.environ.get(_BAZEL_SH) 175 if bash_bin != None: 176 return bash_bin 177 else: 178 bash_bin_path = repository_ctx.which("bash") 179 if bash_bin_path != None: 180 return str(bash_bin_path) 181 else: 182 _fail( 183 "Cannot find bash in PATH, please make sure " + 184 "bash is installed and add its directory in PATH, or --define " + 185 "%s='/path/to/bash'.\nPATH=%s" % 186 (_BAZEL_SH, repository_ctx.os.environ.get("PATH", "")), 187 ) 188 189def _get_python_lib(repository_ctx, python_bin, lib_path_key): 190 """Gets the python lib path.""" 191 python_lib = repository_ctx.os.environ.get(lib_path_key) 192 if python_lib != None: 193 return python_lib 194 print_lib = ( 195 "<<END\n" + "from __future__ import print_function\n" + 196 "import site\n" + "import os\n" + "\n" + "try:\n" + 197 " input = raw_input\n" + "except NameError:\n" + " pass\n" + "\n" + 198 "python_paths = []\n" + "if os.getenv('PYTHONPATH') is not None:\n" + 199 " python_paths = os.getenv('PYTHONPATH').split(':')\n" + "try:\n" + 200 " library_paths = site.getsitepackages()\n" + 201 "except AttributeError:\n" + 202 " import sysconfig\n" + 203 " library_paths = [sysconfig.get_path('purelib')]\n" + 204 "all_paths = set(python_paths + library_paths)\n" + "paths = []\n" + 205 "for path in all_paths:\n" + " if os.path.isdir(path):\n" + 206 " paths.append(path)\n" + "if len(paths) >=1:\n" + 207 " print(paths[0])\n" + "END" 208 ) 209 cmd = '"%s" - %s' % (python_bin, print_lib) 210 result = repository_ctx.execute([_get_bash_bin(repository_ctx), "-c", cmd]) 211 return result.stdout.strip("\n") 212 213def _check_python_lib(repository_ctx, python_lib): 214 """Checks the python lib path.""" 215 cmd = 'test -d "%s" -a -x "%s"' % (python_lib, python_lib) 216 result = repository_ctx.execute([_get_bash_bin(repository_ctx), "-c", cmd]) 217 if result.return_code == 1: 218 _fail("Invalid python library path: %s" % python_lib) 219 220def _check_python_bin(repository_ctx, python_bin, bin_path_key, allow_absent): 221 """Checks the python bin path.""" 222 cmd = '[[ -x "%s" ]] && [[ ! -d "%s" ]]' % (python_bin, python_bin) 223 result = repository_ctx.execute([_get_bash_bin(repository_ctx), "-c", cmd]) 224 if result.return_code == 1: 225 if not allow_absent: 226 _fail("--define %s='%s' is not executable. Is it the python binary?" % 227 (bin_path_key, python_bin)) 228 else: 229 return None 230 return True 231 232def _get_python_include(repository_ctx, python_bin): 233 """Gets the python include path.""" 234 result = _execute( 235 repository_ctx, 236 [ 237 python_bin, 238 "-c", 239 "from __future__ import print_function;" + 240 "import sysconfig;" + 241 "print(sysconfig.get_path('include'))", 242 ], 243 error_msg = "Problem getting python include path for {}.".format(python_bin), 244 error_details = ( 245 "Is the Python binary path set up right? " + "(See ./configure or " + 246 python_bin + ".) " + _HEADERS_HELP 247 ), 248 ) 249 include_path = result.stdout.splitlines()[0] 250 _execute( 251 repository_ctx, 252 [ 253 python_bin, 254 "-c", 255 "import os;" + 256 "main_header = os.path.join(r'{}', 'Python.h');".format(include_path) + 257 "assert os.path.exists(main_header), main_header + ' does not exist.'", 258 ], 259 error_msg = "Unable to find Python headers for {}".format(python_bin), 260 error_details = _HEADERS_HELP, 261 empty_stdout_fine = True, 262 ) 263 return include_path 264 265def _get_python_import_lib_name(repository_ctx, python_bin, bin_path_key): 266 """Get Python import library name (pythonXY.lib) on Windows.""" 267 result = _execute( 268 repository_ctx, 269 [ 270 python_bin, 271 "-c", 272 "import sys;" + 'print("python" + str(sys.version_info[0]) + ' + 273 ' str(sys.version_info[1]) + ".lib")', 274 ], 275 error_msg = "Problem getting python import library.", 276 error_details = ("Is the Python binary path set up right? " + 277 "(See ./configure or " + bin_path_key + ".) "), 278 ) 279 return result.stdout.splitlines()[0] 280 281def _create_single_version_package( 282 repository_ctx, 283 variety_name, 284 bin_path_key, 285 default_bin_path, 286 lib_path_key, 287 allow_absent): 288 """Creates the repository containing files set up to build with Python.""" 289 empty_include_rule = "filegroup(\n name=\"{}_include\",\n srcs=[],\n)".format(variety_name) 290 291 python_bin = _get_python_bin(repository_ctx, bin_path_key, default_bin_path, allow_absent) 292 if (python_bin == None or 293 _check_python_bin(repository_ctx, 294 python_bin, 295 bin_path_key, 296 allow_absent) == None) and allow_absent: 297 python_include_rule = empty_include_rule 298 else: 299 python_lib = _get_python_lib(repository_ctx, python_bin, lib_path_key) 300 _check_python_lib(repository_ctx, python_lib) 301 python_include = _get_python_include(repository_ctx, python_bin) 302 python_include_rule = _symlink_genrule_for_dir( 303 repository_ctx, 304 python_include, 305 "{}_include".format(variety_name), 306 "{}_include".format(variety_name), 307 ) 308 python_import_lib_genrule = "" 309 310 # To build Python C/C++ extension on Windows, we need to link to python import library pythonXY.lib 311 # See https://docs.python.org/3/extending/windows.html 312 if _is_windows(repository_ctx): 313 python_include = _normalize_path(python_include) 314 python_import_lib_name = _get_python_import_lib_name( 315 repository_ctx, 316 python_bin, 317 bin_path_key, 318 ) 319 python_import_lib_src = python_include.rsplit( 320 "/", 321 1, 322 )[0] + "/libs/" + python_import_lib_name 323 python_import_lib_genrule = _symlink_genrule_for_dir( 324 repository_ctx, 325 None, 326 "", 327 "{}_import_lib".format(variety_name), 328 [python_import_lib_src], 329 [python_import_lib_name], 330 ) 331 _tpl( 332 repository_ctx, 333 "variety", 334 { 335 "%{PYTHON_INCLUDE_GENRULE}": python_include_rule, 336 "%{PYTHON_IMPORT_LIB_GENRULE}": python_import_lib_genrule, 337 "%{VARIETY_NAME}": variety_name, 338 }, 339 out = "{}/BUILD".format(variety_name), 340 ) 341 342def _python_autoconf_impl(repository_ctx): 343 """Implementation of the python_autoconf repository rule.""" 344 _create_single_version_package( 345 repository_ctx, 346 "_python3", 347 _PYTHON3_BIN_PATH, 348 "python3" if not _is_windows(repository_ctx) else "python.exe", 349 _PYTHON3_LIB_PATH, 350 False 351 ) 352 _tpl(repository_ctx, "BUILD") 353 354python_configure = repository_rule( 355 implementation = _python_autoconf_impl, 356 environ = [ 357 _BAZEL_SH, 358 _PYTHON3_BIN_PATH, 359 _PYTHON3_LIB_PATH, 360 ], 361 attrs = { 362 "_build_tpl": attr.label( 363 default = Label("//third_party/py:BUILD.tpl"), 364 allow_single_file = True, 365 ), 366 "_variety_tpl": attr.label( 367 default = Label("//third_party/py:variety.tpl"), 368 allow_single_file = True, 369 ), 370 }, 371) 372"""Detects and configures the local Python. 373 374It expects the system have a working Python 3 installation. 375 376Add the following to your WORKSPACE FILE: 377 378```python 379python_configure(name = "local_config_python") 380``` 381 382Args: 383 name: A unique name for this workspace rule. 384""" 385