summaryrefslogtreecommitdiffstats
path: root/third_party/waf/waflib/extras/genpybind.py
blob: ac206ee8a8b0d057d36d8840ae7a5e0b3e705f5e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
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