1import os
2import pipes
3import subprocess
4import sys
5
6from waflib import Logs, Task, Context
7from waflib.Tools.c_preproc import scan as scan_impl
8# ^-- Note: waflib.extras.gccdeps.scan does not work for us,
9# due to its current implementation:
10# The -MD flag is injected into the {C,CXX}FLAGS environment variable and
11# dependencies are read out in a separate step after compiling by reading
12# the .d file saved alongside the object file.
13# As the genpybind task refers to a header file that is never compiled itself,
14# gccdeps will not be able to extract the list of dependencies.
15
16from waflib.TaskGen import feature, before_method
17
18
19def join_args(args):
20    return " ".join(pipes.quote(arg) for arg in args)
21
22
23def configure(cfg):
24    cfg.load("compiler_cxx")
25    cfg.load("python")
26    cfg.check_python_version(minver=(2, 7))
27    if not cfg.env.LLVM_CONFIG:
28        cfg.find_program("llvm-config", var="LLVM_CONFIG")
29    if not cfg.env.GENPYBIND:
30        cfg.find_program("genpybind", var="GENPYBIND")
31
32    # find clang reasource dir for builtin headers
33    cfg.env.GENPYBIND_RESOURCE_DIR = os.path.join(
34            cfg.cmd_and_log(cfg.env.LLVM_CONFIG + ["--libdir"]).strip(),
35            "clang",
36            cfg.cmd_and_log(cfg.env.LLVM_CONFIG + ["--version"]).strip())
37    if os.path.exists(cfg.env.GENPYBIND_RESOURCE_DIR):
38        cfg.msg("Checking clang resource dir", cfg.env.GENPYBIND_RESOURCE_DIR)
39    else:
40        cfg.fatal("Clang resource dir not found")
41
42
43@feature("genpybind")
44@before_method("process_source")
45def generate_genpybind_source(self):
46    """
47    Run genpybind on the headers provided in `source` and compile/link the
48    generated code instead.  This works by generating the code on the fly and
49    swapping the source node before `process_source` is run.
50    """
51    # name of module defaults to name of target
52    module = getattr(self, "module", self.target)
53
54    # create temporary source file in build directory to hold generated code
55    out = "genpybind-%s.%d.cpp" % (module, self.idx)
56    out = self.path.get_bld().find_or_declare(out)
57
58    task = self.create_task("genpybind", self.to_nodes(self.source), out)
59    # used to detect whether CFLAGS or CXXFLAGS should be passed to genpybind
60    task.features = self.features
61    task.module = module
62    # can be used to select definitions to include in the current module
63    # (when header files are shared by more than one module)
64    task.genpybind_tags = self.to_list(getattr(self, "genpybind_tags", []))
65    # additional include directories
66    task.includes = self.to_list(getattr(self, "includes", []))
67    task.genpybind = self.env.GENPYBIND
68
69    # Tell waf to compile/link the generated code instead of the headers
70    # originally passed-in via the `source` parameter. (see `process_source`)
71    self.source = [out]
72
73
74class genpybind(Task.Task): # pylint: disable=invalid-name
75    """
76    Runs genpybind on headers provided as input to this task.
77    Generated code will be written to the first (and only) output node.
78    """
79    quiet = True
80    color = "PINK"
81    scan = scan_impl
82
83    @staticmethod
84    def keyword():
85        return "Analyzing"
86
87    def run(self):
88        if not self.inputs:
89            return
90
91        args = self.find_genpybind() + self._arguments(
92                resource_dir=self.env.GENPYBIND_RESOURCE_DIR)
93
94        output = self.run_genpybind(args)
95
96        # For debugging / log output
97        pasteable_command = join_args(args)
98
99        # write generated code to file in build directory
100        # (will be compiled during process_source stage)
101        (output_node,) = self.outputs
102        output_node.write("// {}\n{}\n".format(
103            pasteable_command.replace("\n", "\n// "), output))
104
105    def find_genpybind(self):
106        return self.genpybind
107
108    def run_genpybind(self, args):
109        bld = self.generator.bld
110
111        kwargs = dict(cwd=bld.variant_dir)
112        if hasattr(bld, "log_command"):
113            bld.log_command(args, kwargs)
114        else:
115            Logs.debug("runner: {!r}".format(args))
116        proc = subprocess.Popen(
117            args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
118        stdout, stderr = proc.communicate()
119
120        if not isinstance(stdout, str):
121            stdout = stdout.decode(sys.stdout.encoding, errors="replace")
122        if not isinstance(stderr, str):
123            stderr = stderr.decode(sys.stderr.encoding, errors="replace")
124
125        if proc.returncode != 0:
126            bld.fatal(
127                "genpybind returned {code} during the following call:"
128                "\n{command}\n\n{stdout}\n\n{stderr}".format(
129                    code=proc.returncode,
130                    command=join_args(args),
131                    stdout=stdout,
132                    stderr=stderr,
133                ))
134
135        if stderr.strip():
136            Logs.debug("non-fatal warnings during genpybind run:\n{}".format(stderr))
137
138        return stdout
139
140    def _include_paths(self):
141        return self.generator.to_incnodes(self.includes + self.env.INCLUDES)
142
143    def _inputs_as_relative_includes(self):
144        include_paths = self._include_paths()
145        relative_includes = []
146        for node in self.inputs:
147            for inc in include_paths:
148                if node.is_child_of(inc):
149                    relative_includes.append(node.path_from(inc))
150                    break
151            else:
152                self.generator.bld.fatal("could not resolve {}".format(node))
153        return relative_includes
154
155    def _arguments(self, genpybind_parse=None, resource_dir=None):
156        args = []
157        relative_includes = self._inputs_as_relative_includes()
158        is_cxx = "cxx" in self.features
159
160        # options for genpybind
161        args.extend(["--genpybind-module", self.module])
162        if self.genpybind_tags:
163            args.extend(["--genpybind-tag"] + self.genpybind_tags)
164        if relative_includes:
165            args.extend(["--genpybind-include"] + relative_includes)
166        if genpybind_parse:
167            args.extend(["--genpybind-parse", genpybind_parse])
168
169        args.append("--")
170
171        # headers to be processed by genpybind
172        args.extend(node.abspath() for node in self.inputs)
173
174        args.append("--")
175
176        # options for clang/genpybind-parse
177        args.append("-D__GENPYBIND__")
178        args.append("-xc++" if is_cxx else "-xc")
179        has_std_argument = False
180        for flag in self.env["CXXFLAGS" if is_cxx else "CFLAGS"]:
181            flag = flag.replace("-std=gnu", "-std=c")
182            if flag.startswith("-std=c"):
183                has_std_argument = True
184            args.append(flag)
185        if not has_std_argument:
186            args.append("-std=c++14")
187        args.extend("-I{}".format(n.abspath()) for n in self._include_paths())
188        args.extend("-D{}".format(p) for p in self.env.DEFINES)
189
190        # point to clang resource dir, if specified
191        if resource_dir:
192            args.append("-resource-dir={}".format(resource_dir))
193
194        return args
195