|
| 1 | +# |
| 2 | +# Copyright 2020 Free Software Foundation, Inc. |
| 3 | +# |
| 4 | +# This file is part of GNU Radio |
| 5 | +# |
| 6 | +# SPDX-License-Identifier: GPL-3.0-or-later |
| 7 | +# |
| 8 | +# |
| 9 | + |
| 10 | +from .base import BindTool |
| 11 | +from gnuradio.blocktool import BlockHeaderParser, GenericHeaderParser |
| 12 | + |
| 13 | +import os |
| 14 | +import pathlib |
| 15 | +import json |
| 16 | +from mako.template import Template |
| 17 | +from datetime import datetime |
| 18 | + |
| 19 | + |
| 20 | +class BindingGenerator: |
| 21 | + |
| 22 | + def __init__(self, prefix, namespace, prefix_include_root, output_dir="", addl_includes="", match_include_structure=False): |
| 23 | + """Initialize BindingGenerator |
| 24 | + prefix -- path to installed gnuradio prefix (use gr.prefix() if unsure) |
| 25 | + namespace -- desired namespace to parse e.g. ['gr','module_name'] |
| 26 | + module_name is stored as the last element of namespace |
| 27 | + prefix_include_root -- relative path to module headers, e.g. "gnuradio/modulename" |
| 28 | +
|
| 29 | + Keyword arguments: |
| 30 | + output_dir -- path where bindings will be placed |
| 31 | + addl_includes -- comma separated list of additional include directories (default "") |
| 32 | + match_include_structure -- |
| 33 | + If set to False, a bindings/ dir will be placed directly under the specified output_dir |
| 34 | + If set to True, the directory structure under include/ will be mirrored |
| 35 | + """ |
| 36 | + |
| 37 | + self.header_extensions = ['.h', '.hh', '.hpp'] |
| 38 | + self.addl_include = addl_includes |
| 39 | + self.prefix = prefix |
| 40 | + self.namespace = namespace |
| 41 | + self.module_name = namespace[-1] |
| 42 | + self.prefix_include_root = prefix_include_root |
| 43 | + self.output_dir = output_dir |
| 44 | + self.match_include_structure = match_include_structure |
| 45 | + |
| 46 | + pass |
| 47 | + |
| 48 | + def gen_pydoc_h(self, header_info, base_name): |
| 49 | + current_path = os.path.dirname(pathlib.Path(__file__).absolute()) |
| 50 | + tpl = Template(filename=os.path.join( |
| 51 | + current_path, '..', 'templates', 'license.mako')) |
| 52 | + license = tpl.render(year=datetime.now().year) |
| 53 | + |
| 54 | + tpl = Template(filename=os.path.join(current_path, '..', |
| 55 | + 'templates', 'pydoc_h.mako')) |
| 56 | + return tpl.render( |
| 57 | + license=license, |
| 58 | + header_info=header_info, |
| 59 | + basename=base_name, |
| 60 | + prefix_include_root=self.prefix_include_root, |
| 61 | + ) |
| 62 | + |
| 63 | + |
| 64 | + def gen_pybind_cc(self, header_info, base_name): |
| 65 | + current_path = os.path.dirname(pathlib.Path(__file__).absolute()) |
| 66 | + tpl = Template(filename=os.path.join( |
| 67 | + current_path, '..', 'templates', 'license.mako')) |
| 68 | + license = tpl.render(year=datetime.now().year) |
| 69 | + |
| 70 | + tpl = Template(filename=os.path.join(current_path, '..', |
| 71 | + 'templates', 'generic_python_cc.mako')) |
| 72 | + return tpl.render( |
| 73 | + license=license, |
| 74 | + header_info=header_info, |
| 75 | + basename=base_name, |
| 76 | + prefix_include_root=self.prefix_include_root, |
| 77 | + ) |
| 78 | + |
| 79 | + def write_pydoc_h(self, header_info, base_name, output_dir): |
| 80 | + |
| 81 | + doc_pathname = os.path.join( |
| 82 | + output_dir, 'docstrings', '{}_pydoc_template.h'.format(base_name)) |
| 83 | + |
| 84 | + try: |
| 85 | + pybind_code = self.gen_pydoc_h( |
| 86 | + header_info, base_name) |
| 87 | + with open(doc_pathname, 'w+') as outfile: |
| 88 | + print("Writing binding code to {}".format(doc_pathname)) |
| 89 | + outfile.write(pybind_code) |
| 90 | + return doc_pathname |
| 91 | + except Exception as e: |
| 92 | + print(e) |
| 93 | + return None |
| 94 | + |
| 95 | + |
| 96 | + def write_pybind_cc(self, header_info, base_name, output_dir): |
| 97 | + |
| 98 | + binding_pathname_cc = os.path.join( |
| 99 | + output_dir, '{}_python.cc'.format(base_name)) |
| 100 | + |
| 101 | + try: |
| 102 | + pybind_code = self.gen_pybind_cc( |
| 103 | + header_info, base_name) |
| 104 | + with open(binding_pathname_cc, 'w+') as outfile: |
| 105 | + print("Writing binding code to {}".format(binding_pathname_cc)) |
| 106 | + outfile.write(pybind_code) |
| 107 | + return binding_pathname_cc |
| 108 | + except Exception as e: |
| 109 | + print(e) |
| 110 | + return None |
| 111 | + |
| 112 | + def write_json(self, header_info, base_name, output_dir): |
| 113 | + json_pathname = os.path.join(output_dir, '{}.json'.format(base_name)) |
| 114 | + with open(json_pathname, 'w') as outfile: |
| 115 | + json.dump(header_info, outfile) |
| 116 | + |
| 117 | + def read_json(self, pathname): |
| 118 | + with open(pathname, 'r') as fp: |
| 119 | + header_info = json.load(fp) |
| 120 | + return header_info |
| 121 | + |
| 122 | + def gen_file_binding(self, file_to_process): |
| 123 | + """Produce the blockname_python.cc python bindings""" |
| 124 | + output_dir = self.get_output_dir(file_to_process) |
| 125 | + binding_pathname = None |
| 126 | + base_name = os.path.splitext(os.path.basename(file_to_process))[0] |
| 127 | + module_include_path = os.path.abspath(os.path.dirname(file_to_process)) |
| 128 | + top_include_path = os.path.join( |
| 129 | + module_include_path.split('include'+os.path.sep)[0], 'include') |
| 130 | + |
| 131 | + include_paths = ','.join( |
| 132 | + (module_include_path, top_include_path)) |
| 133 | + if self.prefix: |
| 134 | + prefix_include_path = os.path.abspath( |
| 135 | + os.path.join(self.prefix, 'include')) |
| 136 | + include_paths = ','.join( |
| 137 | + (include_paths, prefix_include_path) |
| 138 | + ) |
| 139 | + if self.addl_include: |
| 140 | + include_paths = ','.join((include_paths, self.addl_include)) |
| 141 | + |
| 142 | + parser = GenericHeaderParser( |
| 143 | + include_paths=include_paths, file_path=file_to_process) |
| 144 | + try: |
| 145 | + header_info = parser.get_header_info(self.namespace) |
| 146 | + # TODO: Scrape the docstrings |
| 147 | + self.write_json(header_info, base_name, output_dir) |
| 148 | + self.write_pybind_cc(header_info, base_name, output_dir) |
| 149 | + self.write_pydoc_h(header_info, base_name, output_dir) |
| 150 | + |
| 151 | + except Exception as e: |
| 152 | + print(e) |
| 153 | + failure_pathname = os.path.join( |
| 154 | + output_dir, 'failed_conversions.txt') |
| 155 | + with open(failure_pathname, 'a+') as outfile: |
| 156 | + outfile.write(file_to_process) |
| 157 | + outfile.write(str(e)) |
| 158 | + outfile.write('\n') |
| 159 | + |
| 160 | + return binding_pathname |
| 161 | + |
| 162 | + def get_output_dir(self, filename): |
| 163 | + """Get the output directory for a given file""" |
| 164 | + output_dir = self.output_dir |
| 165 | + rel_path_after_include = "" |
| 166 | + if self.match_include_structure: |
| 167 | + if 'include'+os.path.sep in filename: |
| 168 | + rel_path_after_include = os.path.split( |
| 169 | + filename.split('include'+os.path.sep)[-1])[0] |
| 170 | + |
| 171 | + output_dir = os.path.join( |
| 172 | + self.output_dir, rel_path_after_include, 'bindings') |
| 173 | + doc_dir = os.path.join(output_dir,'docstrings') |
| 174 | + if output_dir and not os.path.exists(output_dir) and not os.path.exists(doc_dir): |
| 175 | + output_dir = os.path.abspath(output_dir) |
| 176 | + print('creating output directory {}'.format(output_dir)) |
| 177 | + os.makedirs(output_dir) |
| 178 | + |
| 179 | + if doc_dir and not os.path.exists(doc_dir): |
| 180 | + doc_dir = os.path.abspath(doc_dir) |
| 181 | + print('creating docstrings directory {}'.format(doc_dir)) |
| 182 | + os.makedirs(doc_dir) |
| 183 | + |
| 184 | + return output_dir |
| 185 | + |
| 186 | + def gen_top_level_cpp(self, file_list): |
| 187 | + """Produce the python_bindings.cc for the bindings""" |
| 188 | + current_path = os.path.dirname(pathlib.Path(__file__).absolute()) |
| 189 | + file = file_list[0] |
| 190 | + output_dir = self.get_output_dir(file) |
| 191 | + |
| 192 | + tpl = Template(filename=os.path.join( |
| 193 | + current_path, '..', 'templates', 'license.mako')) |
| 194 | + license = tpl.render(year=datetime.now().year) |
| 195 | + |
| 196 | + binding_pathname = os.path.join(output_dir, 'python_bindings.cc') |
| 197 | + file_list = [os.path.split(f)[-1] for f in file_list] |
| 198 | + tpl = Template(filename=os.path.join(current_path, '..', |
| 199 | + 'templates', 'python_bindings_cc.mako')) |
| 200 | + pybind_code = tpl.render( |
| 201 | + license=license, |
| 202 | + files=file_list, |
| 203 | + module_name=self.module_name |
| 204 | + ) |
| 205 | + |
| 206 | + # print(pybind_code) |
| 207 | + try: |
| 208 | + with open(binding_pathname, 'w+') as outfile: |
| 209 | + outfile.write(pybind_code) |
| 210 | + return binding_pathname |
| 211 | + except: |
| 212 | + return None |
| 213 | + |
| 214 | + def gen_cmake_lists(self, file_list): |
| 215 | + """Produce the CMakeLists.txt for the bindings""" |
| 216 | + current_path = os.path.dirname(pathlib.Path(__file__).absolute()) |
| 217 | + file = file_list[0] |
| 218 | + output_dir = self.get_output_dir(file) |
| 219 | + |
| 220 | + tpl = Template(filename=os.path.join( |
| 221 | + current_path, '..', 'templates', 'license.mako')) |
| 222 | + license = tpl.render(year=datetime.now().year) |
| 223 | + |
| 224 | + binding_pathname = os.path.join(output_dir, 'CMakeLists.txt') |
| 225 | + file_list = [os.path.split(f)[-1] for f in file_list] |
| 226 | + tpl = Template(filename=os.path.join(current_path, '..', |
| 227 | + 'templates', 'CMakeLists.txt.mako')) |
| 228 | + pybind_code = tpl.render( |
| 229 | + license=license, |
| 230 | + files=file_list, |
| 231 | + module_name=self.module_name |
| 232 | + ) |
| 233 | + |
| 234 | + # print(pybind_code) |
| 235 | + try: |
| 236 | + with open(binding_pathname, 'w+') as outfile: |
| 237 | + outfile.write(pybind_code) |
| 238 | + return binding_pathname |
| 239 | + except: |
| 240 | + return None |
| 241 | + |
| 242 | + def get_file_list(self, include_path): |
| 243 | + """Recursively get sorted list of files in path""" |
| 244 | + file_list = [] |
| 245 | + for root, _, files in os.walk(include_path): |
| 246 | + for file in files: |
| 247 | + _, file_extension = os.path.splitext(file) |
| 248 | + if (file_extension in self.header_extensions): |
| 249 | + pathname = os.path.abspath(os.path.join(root, file)) |
| 250 | + file_list.append(pathname) |
| 251 | + return sorted(file_list) |
| 252 | + |
| 253 | + def gen_bindings(self, module_dir): |
| 254 | + """Generate bindings for an entire GR module |
| 255 | +
|
| 256 | + Produces CMakeLists.txt, python_bindings.cc, and blockname_python.cc |
| 257 | + for each block in the module |
| 258 | +
|
| 259 | + module_dir -- path to the include directory where the public headers live |
| 260 | + """ |
| 261 | + file_list = self.get_file_list(module_dir) |
| 262 | + api_pathnames = [s for s in file_list if 'api.h' in s] |
| 263 | + for f in api_pathnames: |
| 264 | + file_list.remove(f) |
| 265 | + self.gen_top_level_cpp(file_list) |
| 266 | + self.gen_cmake_lists(file_list) |
| 267 | + for fn in file_list: |
| 268 | + self.gen_file_binding(fn) |
0 commit comments