Merge pull request #1 from rmmh/add-rules
Add Bazel rules
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..5c4e8ab
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,4 @@
+# Bazel extensions for pybind11
+licenses(["notice"])
+
+exports_files(["LICENSE"])
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..dd6849a
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,38 @@
+Thank you for your interest in this project! Please refer to the following
+sections on how to contribute code and bug reports.
+
+### Reporting bugs
+
+Assuming that you have identified a previously unknown problem or an important
+question, it's essential that you submit a self-contained and minimal piece of
+code that reproduces the problem. In other words: no external dependencies,
+isolate the function(s) that cause breakage, submit matched and complete C++
+and Python snippets that can be easily compiled and run on my end.
+
+## Pull requests
+Contributions are submitted, reviewed, and accepted using Github pull requests.
+Please refer to [this
+article](https://help.github.com/articles/using-pull-requests) for details and
+adhere to the following rules to make the process as smooth as possible:
+
+* Make a new branch for every feature you're working on.
+* Make small and clean pull requests that are easy to review but make sure they
+ do add value by themselves.
+* This project has a strong focus on providing general solutions using a
+ minimal amount of code, thus small pull requests are greatly preferred.
+
+### Licensing of contributions
+
+pybind11_bazel is provided under a BSD-style license that can be found in the
+``LICENSE`` file. By using, distributing, or contributing to this project, you
+agree to the terms and conditions of this license.
+
+You are under no obligation whatsoever to provide any bug fixes, patches, or
+upgrades to the features, functionality or performance of the source code
+("Enhancements") to anyone; however, if you choose to make your Enhancements
+available either publicly, or directly to the author of this software, without
+imposing a separate written license agreement for such Enhancements, then you
+hereby grant the following license: a non-exclusive, royalty-free perpetual
+license to install, use, modify, prepare derivative works, incorporate into
+other computer software, distribute, and sublicense such enhancements or
+derivative works thereof, in binary and source code form.
diff --git a/README.md b/README.md
index e8d6bda..2949919 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,21 @@
# Bazel extensions for pybind11
+
+In your build file:
+
+```
+load("//third_party/pybind11_bazel:build_defs.bzl", "pybind_extension")
+```
+
+Provided rules:
+
+- `pybind_extension`: Builds a python extension, automatically adding the
+ required build flags and pybind11 dependencies. It also defines a .so target
+ which can be manually built and copied. The arguments match a `py_extension`.
+- `pybind_library`: Builds a C++ library, automatically adding the required
+ build flags and pybind11 dependencies. This library can then be used as a
+ dependency of a `pybind_extension`. The arguments match a `cc_library`.
+- `pybind_library_test`: Builds a C++ test for a `pybind_library`. The arguments
+ match a cc_test.
+
+To test a `pybind_extension`, the most common approach is to write the test in
+python and use the standard `py_test` build rule.
diff --git a/build_defs.bzl b/build_defs.bzl
new file mode 100644
index 0000000..651b800
--- /dev/null
+++ b/build_defs.bzl
@@ -0,0 +1,108 @@
+# Copyright (c) 2019 The Pybind Development Team. All rights reserved.
+#
+# All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+"""Build rules for pybind11."""
+
+def register_extension_info(**kwargs):
+ pass
+
+PYBIND_COPTS = [
+ "-fexceptions",
+]
+
+PYBIND_FEATURES = [
+ "-use_header_modules", # Required for pybind11.
+ "-parse_headers",
+]
+
+PYBIND_DEPS = [
+ "@pybind11",
+ "@local_config_python//:python_headers",
+]
+
+# Builds a Python extension module using pybind11.
+# This can be directly used in python with the import statement.
+# This adds rules for a .so binary file, which must be built manually.
+def pybind_extension(
+ name,
+ copts = [],
+ features = [],
+ tags = [],
+ deps = [],
+ **kwargs):
+ # Mark common dependencies as required for build_cleaner.
+ tags = tags + ["req_dep=%s" % dep for dep in PYBIND_DEPS]
+
+ native.cc_binary(
+ name = name + ".so",
+ copts = copts + PYBIND_COPTS + ["-fvisibility=hidden"],
+ features = features + PYBIND_FEATURES,
+ linkopts = [
+ "-Wl,-Bsymbolic",
+ ],
+ linkshared = 1,
+ tags = tags + ["local", "manual"],
+ deps = deps + PYBIND_DEPS,
+ **kwargs
+ )
+
+# Builds a pybind11 compatible library. This can be linked to a pybind_extension.
+def pybind_library(
+ name,
+ copts = [],
+ features = [],
+ tags = [],
+ deps = [],
+ **kwargs):
+ # Mark common dependencies as required for build_cleaner.
+ tags = tags + ["req_dep=%s" % dep for dep in PYBIND_DEPS]
+
+ native.cc_library(
+ name = name,
+ copts = copts + PYBIND_COPTS,
+ features = features + PYBIND_FEATURES,
+ tags = tags,
+ deps = deps + PYBIND_DEPS,
+ **kwargs
+ )
+
+# Builds a C++ test for a pybind_library.
+def pybind_library_test(
+ name,
+ copts = [],
+ features = [],
+ tags = [],
+ deps = [],
+ **kwargs):
+ # Mark common dependencies as required for build_cleaner.
+ tags = tags + ["req_dep=%s" % dep for dep in PYBIND_DEPS]
+
+ native.cc_test(
+ name = name,
+ copts = copts + PYBIND_COPTS,
+ features = features + PYBIND_FEATURES,
+ tags = tags,
+ deps = deps + PYBIND_DEPS + [
+ "//util/python:python_impl",
+ "//util/python:test_main",
+ ],
+ **kwargs
+ )
+
+# Register extension with build_cleaner.
+register_extension_info(
+ extension = pybind_extension,
+ label_regex_for_dep = "{extension_name}",
+)
+
+register_extension_info(
+ extension = pybind_library,
+ label_regex_for_dep = "{extension_name}",
+)
+
+register_extension_info(
+ extension = pybind_library_test,
+ label_regex_for_dep = "{extension_name}",
+)
diff --git a/py/BUILD b/py/BUILD
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/py/BUILD
diff --git a/py/BUILD.tpl b/py/BUILD.tpl
new file mode 100644
index 0000000..8bd1374
--- /dev/null
+++ b/py/BUILD.tpl
@@ -0,0 +1,68 @@
+licenses(["restricted"])
+
+package(default_visibility = ["//visibility:public"])
+
+# Point both runtimes to the same python binary to ensure we always
+# use the python binary specified by ./configure.py script.
+load("@bazel_tools//tools/python:toolchain.bzl", "py_runtime_pair")
+
+py_runtime(
+ name = "py2_runtime",
+ interpreter_path = "%{PYTHON_BIN_PATH}",
+ python_version = "PY2",
+)
+
+py_runtime(
+ name = "py3_runtime",
+ interpreter_path = "%{PYTHON_BIN_PATH}",
+ python_version = "PY3",
+)
+
+py_runtime_pair(
+ name = "py_runtime_pair",
+ py2_runtime = ":py2_runtime",
+ py3_runtime = ":py3_runtime",
+)
+
+toolchain(
+ name = "py_toolchain",
+ toolchain = ":py_runtime_pair",
+ toolchain_type = "@bazel_tools//tools/python:toolchain_type",
+)
+
+# 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
+cc_import(
+ name = "python_lib",
+ interface_library = select({
+ ":windows": ":python_import_lib",
+ # A placeholder for Unix platforms which makes --no_build happy.
+ "//conditions:default": "not-existing.lib",
+ }),
+ system_provided = 1,
+)
+
+cc_library(
+ name = "python_headers",
+ hdrs = [":python_include"],
+ deps = select({
+ ":windows": [":python_lib"],
+ "//conditions:default": [],
+ }),
+ includes = ["python_include"],
+)
+
+cc_library(
+ name = "numpy_headers",
+ hdrs = [":numpy_include"],
+ includes = ["numpy_include"],
+)
+
+config_setting(
+ name = "windows",
+ values = {"cpu": "x64_windows"},
+ visibility = ["//visibility:public"],
+)
+
+%{PYTHON_INCLUDE_GENRULE}
+%{PYTHON_IMPORT_LIB_GENRULE}
diff --git a/python_configure.bzl b/python_configure.bzl
new file mode 100644
index 0000000..ac41ef8
--- /dev/null
+++ b/python_configure.bzl
@@ -0,0 +1,335 @@
+"""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.
+"""
+
+_BAZEL_SH = "BAZEL_SH"
+_PYTHON_BIN_PATH = "PYTHON_BIN_PATH"
+_PYTHON_LIB_PATH = "PYTHON_LIB_PATH"
+
+def _tpl(repository_ctx, tpl, substitutions = {}, out = None):
+ if not out:
+ out = tpl
+ repository_ctx.template(
+ out,
+ Label("//py:%s.tpl" % tpl),
+ substitutions,
+ )
+
+def _fail(msg):
+ """Output failure message when auto configuration fails."""
+ red = "\033[0;31m"
+ no_color = "\033[0m"
+ fail("%sPython Configuration Error:%s %s\n" % (red, no_color, msg))
+
+def _is_windows(repository_ctx):
+ """Returns true if the host operating system is windows."""
+ os_name = repository_ctx.os.name.lower()
+ if os_name.find("windows") != -1:
+ return True
+ return False
+
+def _execute(
+ repository_ctx,
+ cmdline,
+ error_msg = None,
+ error_details = None,
+ empty_stdout_fine = False):
+ """Executes an arbitrary shell command.
+
+ Args:
+ repository_ctx: the repository_ctx object
+ cmdline: list of strings, the command to execute
+ error_msg: string, a summary of the error if the command fails
+ error_details: string, details about the error or steps to fix it
+ empty_stdout_fine: bool, if True, an empty stdout result is fine, otherwise
+ it's an error
+ Return:
+ the result of repository_ctx.execute(cmdline)
+ """
+ result = repository_ctx.execute(cmdline)
+ if result.stderr or not (empty_stdout_fine or result.stdout):
+ _fail("\n".join([
+ error_msg.strip() if error_msg else "Repository command failed",
+ result.stderr.strip(),
+ error_details if error_details else "",
+ ]))
+ return result
+
+def _read_dir(repository_ctx, src_dir):
+ """Returns a string with all files in a directory.
+
+ Finds all files inside a directory, traversing subfolders and following
+ symlinks. The returned string contains the full path of all files
+ separated by line breaks.
+ """
+ if _is_windows(repository_ctx):
+ src_dir = src_dir.replace("/", "\\")
+ find_result = _execute(
+ repository_ctx,
+ ["cmd.exe", "/c", "dir", src_dir, "/b", "/s", "/a-d"],
+ empty_stdout_fine = True,
+ )
+
+ # src_files will be used in genrule.outs where the paths must
+ # use forward slashes.
+ result = find_result.stdout.replace("\\", "/")
+ else:
+ find_result = _execute(
+ repository_ctx,
+ ["find", src_dir, "-follow", "-type", "f"],
+ empty_stdout_fine = True,
+ )
+ result = find_result.stdout
+ return result
+
+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(sorted(_read_dir(repository_ctx, src_dir).splitlines()))
+
+ # 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_bin(repository_ctx):
+ """Gets the python bin path."""
+ python_bin = repository_ctx.os.environ.get(_PYTHON_BIN_PATH)
+ if python_bin != None:
+ return python_bin
+ python_bin_path = repository_ctx.which("python")
+ if python_bin_path != None:
+ return str(python_bin_path)
+ _fail("Cannot find python in PATH, please make sure " +
+ "python is installed and add its directory in PATH, or --define " +
+ "%s='/something/else'.\nPATH=%s" % (
+ _PYTHON_BIN_PATH,
+ repository_ctx.os.environ.get("PATH", ""),
+ ))
+
+def _get_bash_bin(repository_ctx):
+ """Gets the bash bin path."""
+ bash_bin = repository_ctx.os.environ.get(_BAZEL_SH)
+ if bash_bin != None:
+ return bash_bin
+ else:
+ bash_bin_path = repository_ctx.which("bash")
+ if bash_bin_path != None:
+ return str(bash_bin_path)
+ else:
+ _fail("Cannot find bash in PATH, please make sure " +
+ "bash is installed and add its directory in PATH, or --define " +
+ "%s='/path/to/bash'.\nPATH=%s" % (
+ _BAZEL_SH,
+ repository_ctx.os.environ.get("PATH", ""),
+ ))
+
+def _get_python_lib(repository_ctx, python_bin):
+ """Gets the python lib path."""
+ python_lib = repository_ctx.os.environ.get(_PYTHON_LIB_PATH)
+ if python_lib != None:
+ return python_lib
+ print_lib = ("<<END\n" +
+ "from __future__ import print_function\n" +
+ "import site\n" +
+ "import os\n" +
+ "\n" +
+ "try:\n" +
+ " input = raw_input\n" +
+ "except NameError:\n" +
+ " pass\n" +
+ "\n" +
+ "python_paths = []\n" +
+ "if os.getenv('PYTHONPATH') is not None:\n" +
+ " python_paths = os.getenv('PYTHONPATH').split(':')\n" +
+ "try:\n" +
+ " library_paths = site.getsitepackages()\n" +
+ "except AttributeError:\n" +
+ " from distutils.sysconfig import get_python_lib\n" +
+ " library_paths = [get_python_lib()]\n" +
+ "all_paths = set(python_paths + library_paths)\n" +
+ "paths = []\n" +
+ "for path in all_paths:\n" +
+ " if os.path.isdir(path):\n" +
+ " paths.append(path)\n" +
+ "if len(paths) >=1:\n" +
+ " print(paths[0])\n" +
+ "END")
+ cmd = "%s - %s" % (python_bin, print_lib)
+ result = repository_ctx.execute([_get_bash_bin(repository_ctx), "-c", cmd])
+ return result.stdout.strip("\n")
+
+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 = repository_ctx.execute([_get_bash_bin(repository_ctx), "-c", cmd])
+ if result.return_code == 1:
+ _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 = repository_ctx.execute([_get_bash_bin(repository_ctx), "-c", cmd])
+ if result.return_code == 1:
+ _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,
+ "-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 _create_local_python_repository(repository_ctx):
+ """Creates the repository containing files set up to build with Python."""
+ 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)
+ 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],
+ )
+ _tpl(repository_ctx, "BUILD", {
+ "%{PYTHON_BIN_PATH}": python_bin,
+ "%{PYTHON_INCLUDE_GENRULE}": python_include_rule,
+ "%{PYTHON_IMPORT_LIB_GENRULE}": python_import_lib_genrule,
+ })
+
+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", Label(remote_config_repo + ":BUILD"), {})
+
+def _python_autoconf_impl(repository_ctx):
+ """Implementation of the python_autoconf repository rule."""
+ _create_local_python_repository(repository_ctx)
+
+python_configure = repository_rule(
+ implementation = _python_autoconf_impl,
+ environ = [
+ _BAZEL_SH,
+ _PYTHON_BIN_PATH,
+ _PYTHON_LIB_PATH,
+ ],
+)
+"""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.
+"""