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