python_configure.bzl 13 KB

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