summaryrefslogtreecommitdiffstats
path: root/extras/genpybind.py
diff options
context:
space:
mode:
Diffstat (limited to 'extras/genpybind.py')
-rw-r--r--extras/genpybind.py194
1 files changed, 194 insertions, 0 deletions
diff --git a/extras/genpybind.py b/extras/genpybind.py
new file mode 100644
index 0000000..ac206ee
--- /dev/null
+++ b/extras/genpybind.py
@@ -0,0 +1,194 @@
+import os
+import pipes
+import subprocess
+import sys
+
+from waflib import Logs, Task, Context
+from waflib.Tools.c_preproc import scan as scan_impl
+# ^-- Note: waflib.extras.gccdeps.scan does not work for us,
+# due to its current implementation:
+# The -MD flag is injected into the {C,CXX}FLAGS environment variable and
+# dependencies are read out in a separate step after compiling by reading
+# the .d file saved alongside the object file.
+# As the genpybind task refers to a header file that is never compiled itself,
+# gccdeps will not be able to extract the list of dependencies.
+
+from waflib.TaskGen import feature, before_method
+
+
+def join_args(args):
+ return " ".join(pipes.quote(arg) for arg in args)
+
+
+def configure(cfg):
+ cfg.load("compiler_cxx")
+ cfg.load("python")
+ cfg.check_python_version(minver=(2, 7))
+ if not cfg.env.LLVM_CONFIG:
+ cfg.find_program("llvm-config", var="LLVM_CONFIG")
+ if not cfg.env.GENPYBIND:
+ cfg.find_program("genpybind", var="GENPYBIND")
+
+ # find clang reasource dir for builtin headers
+ cfg.env.GENPYBIND_RESOURCE_DIR = os.path.join(
+ cfg.cmd_and_log(cfg.env.LLVM_CONFIG + ["--libdir"]).strip(),
+ "clang",
+ cfg.cmd_and_log(cfg.env.LLVM_CONFIG + ["--version"]).strip())
+ if os.path.exists(cfg.env.GENPYBIND_RESOURCE_DIR):
+ cfg.msg("Checking clang resource dir", cfg.env.GENPYBIND_RESOURCE_DIR)
+ else:
+ cfg.fatal("Clang resource dir not found")
+
+
+@feature("genpybind")
+@before_method("process_source")
+def generate_genpybind_source(self):
+ """
+ Run genpybind on the headers provided in `source` and compile/link the
+ generated code instead. This works by generating the code on the fly and
+ swapping the source node before `process_source` is run.
+ """
+ # name of module defaults to name of target
+ module = getattr(self, "module", self.target)
+
+ # create temporary source file in build directory to hold generated code
+ out = "genpybind-%s.%d.cpp" % (module, self.idx)
+ out = self.path.get_bld().find_or_declare(out)
+
+ task = self.create_task("genpybind", self.to_nodes(self.source), out)
+ # used to detect whether CFLAGS or CXXFLAGS should be passed to genpybind
+ task.features = self.features
+ task.module = module
+ # can be used to select definitions to include in the current module
+ # (when header files are shared by more than one module)
+ task.genpybind_tags = self.to_list(getattr(self, "genpybind_tags", []))
+ # additional include directories
+ task.includes = self.to_list(getattr(self, "includes", []))
+ task.genpybind = self.env.GENPYBIND
+
+ # Tell waf to compile/link the generated code instead of the headers
+ # originally passed-in via the `source` parameter. (see `process_source`)
+ self.source = [out]
+
+
+class genpybind(Task.Task): # pylint: disable=invalid-name
+ """
+ Runs genpybind on headers provided as input to this task.
+ Generated code will be written to the first (and only) output node.
+ """
+ quiet = True
+ color = "PINK"
+ scan = scan_impl
+
+ @staticmethod
+ def keyword():
+ return "Analyzing"
+
+ def run(self):
+ if not self.inputs:
+ return
+
+ args = self.find_genpybind() + self._arguments(
+ resource_dir=self.env.GENPYBIND_RESOURCE_DIR)
+
+ output = self.run_genpybind(args)
+
+ # For debugging / log output
+ pasteable_command = join_args(args)
+
+ # write generated code to file in build directory
+ # (will be compiled during process_source stage)
+ (output_node,) = self.outputs
+ output_node.write("// {}\n{}\n".format(
+ pasteable_command.replace("\n", "\n// "), output))
+
+ def find_genpybind(self):
+ return self.genpybind
+
+ def run_genpybind(self, args):
+ bld = self.generator.bld
+
+ kwargs = dict(cwd=bld.variant_dir)
+ if hasattr(bld, "log_command"):
+ bld.log_command(args, kwargs)
+ else:
+ Logs.debug("runner: {!r}".format(args))
+ proc = subprocess.Popen(
+ args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
+ stdout, stderr = proc.communicate()
+
+ if not isinstance(stdout, str):
+ stdout = stdout.decode(sys.stdout.encoding, errors="replace")
+ if not isinstance(stderr, str):
+ stderr = stderr.decode(sys.stderr.encoding, errors="replace")
+
+ if proc.returncode != 0:
+ bld.fatal(
+ "genpybind returned {code} during the following call:"
+ "\n{command}\n\n{stdout}\n\n{stderr}".format(
+ code=proc.returncode,
+ command=join_args(args),
+ stdout=stdout,
+ stderr=stderr,
+ ))
+
+ if stderr.strip():
+ Logs.debug("non-fatal warnings during genpybind run:\n{}".format(stderr))
+
+ return stdout
+
+ def _include_paths(self):
+ return self.generator.to_incnodes(self.includes + self.env.INCLUDES)
+
+ def _inputs_as_relative_includes(self):
+ include_paths = self._include_paths()
+ relative_includes = []
+ for node in self.inputs:
+ for inc in include_paths:
+ if node.is_child_of(inc):
+ relative_includes.append(node.path_from(inc))
+ break
+ else:
+ self.generator.bld.fatal("could not resolve {}".format(node))
+ return relative_includes
+
+ def _arguments(self, genpybind_parse=None, resource_dir=None):
+ args = []
+ relative_includes = self._inputs_as_relative_includes()
+ is_cxx = "cxx" in self.features
+
+ # options for genpybind
+ args.extend(["--genpybind-module", self.module])
+ if self.genpybind_tags:
+ args.extend(["--genpybind-tag"] + self.genpybind_tags)
+ if relative_includes:
+ args.extend(["--genpybind-include"] + relative_includes)
+ if genpybind_parse:
+ args.extend(["--genpybind-parse", genpybind_parse])
+
+ args.append("--")
+
+ # headers to be processed by genpybind
+ args.extend(node.abspath() for node in self.inputs)
+
+ args.append("--")
+
+ # options for clang/genpybind-parse
+ args.append("-D__GENPYBIND__")
+ args.append("-xc++" if is_cxx else "-xc")
+ has_std_argument = False
+ for flag in self.env["CXXFLAGS" if is_cxx else "CFLAGS"]:
+ flag = flag.replace("-std=gnu", "-std=c")
+ if flag.startswith("-std=c"):
+ has_std_argument = True
+ args.append(flag)
+ if not has_std_argument:
+ args.append("-std=c++14")
+ args.extend("-I{}".format(n.abspath()) for n in self._include_paths())
+ args.extend("-D{}".format(p) for p in self.env.DEFINES)
+
+ # point to clang resource dir, if specified
+ if resource_dir:
+ args.append("-resource-dir={}".format(resource_dir))
+
+ return args