diff options
Diffstat (limited to 'grc')
-rw-r--r-- | grc/blocks/options.block.yml | 28 | ||||
-rw-r--r-- | grc/blocks/stream.domain.yml | 3 | ||||
-rw-r--r-- | grc/blocks/variable.block.yml | 6 | ||||
-rw-r--r-- | grc/core/FlowGraph.py | 4 | ||||
-rw-r--r-- | grc/core/blocks/_build.py | 14 | ||||
-rw-r--r-- | grc/core/blocks/_flags.py | 2 | ||||
-rw-r--r-- | grc/core/blocks/block.py | 98 | ||||
-rw-r--r-- | grc/core/generator/FlowGraphProxy.py | 30 | ||||
-rw-r--r-- | grc/core/generator/Generator.py | 26 | ||||
-rw-r--r-- | grc/core/generator/cpp_hier_block.py | 196 | ||||
-rw-r--r-- | grc/core/generator/cpp_templates/CMakeLists.txt.mako | 64 | ||||
-rw-r--r-- | grc/core/generator/cpp_templates/flow_graph.cpp.mako | 155 | ||||
-rw-r--r-- | grc/core/generator/cpp_templates/flow_graph.hpp.mako | 89 | ||||
-rw-r--r-- | grc/core/generator/cpp_top_block.py | 344 | ||||
-rw-r--r-- | grc/core/generator/top_block.py | 31 | ||||
-rw-r--r-- | grc/core/platform.py | 17 | ||||
-rw-r--r-- | grc/core/schema_checker/block.py | 9 | ||||
-rw-r--r-- | grc/gui/Executor.py | 74 | ||||
-rw-r--r-- | grc/tests/test_cpp.py | 18 |
19 files changed, 1189 insertions, 19 deletions
diff --git a/grc/blocks/options.block.yml b/grc/blocks/options.block.yml index 269351ddc0..dec77a3f84 100644 --- a/grc/blocks/options.block.yml +++ b/grc/blocks/options.block.yml @@ -1,5 +1,6 @@ id: options label: Options +flags: has_python, has_cpp parameters: - id: title @@ -22,12 +23,36 @@ parameters: label: Canvas Size dtype: int_vector hide: part +- id: output_language + label: Output Language + dtype: enum + default: python + options: [python, cpp] + option_labels: [Python, C++] - id: generate_options label: Generate Options dtype: enum default: qt_gui options: [qt_gui, bokeh_gui, no_gui, hb, hb_qt_gui] option_labels: [QT GUI, Bokeh GUI, No GUI, Hier Block, Hier Block (QT GUI)] +- id: gen_linking + label: Linking + dtype: enum + default: dynamic + options: [dynamic, static] + option_labels: [ Dynamic, Static ] + hide: ${ ('part' if output_language == 'cpp' else 'all') } +- id: gen_cmake + label: Generate CMakeLists.txt + dtype: enum + default: 'On' + options: ['On', 'Off'] + hide: ${ ('part' if output_language == 'cpp' else 'all') } +- id: cmake_opt + label: CMake options + dtype: string + default: '' + hide: ${ ('part' if output_language == 'cpp' else 'all') } - id: category label: Category dtype: string @@ -130,6 +155,9 @@ templates: else: self.stop(); self.wait()' +cpp_templates: + includes: '#include <gnuradio/top_block.h>' + documentation: |- The options block sets special parameters for the flow graph. Only one option block is allowed per flow graph. diff --git a/grc/blocks/stream.domain.yml b/grc/blocks/stream.domain.yml index a4d786f8b4..f97695b828 100644 --- a/grc/blocks/stream.domain.yml +++ b/grc/blocks/stream.domain.yml @@ -7,4 +7,5 @@ multiple_connections_per_output: true templates: - type: [stream, stream] - connect: self.connect(${ make_port_sig(source) }, ${ make_port_sig(sink) }) + connect: connect(${ make_port_sig(source) }, ${ make_port_sig(sink) }) + cpp_connect: hier_block2::connect(${ make_port_sig(source) }, ${ make_port_sig(sink) }) diff --git a/grc/blocks/variable.block.yml b/grc/blocks/variable.block.yml index fa62dabe87..045511afb0 100644 --- a/grc/blocks/variable.block.yml +++ b/grc/blocks/variable.block.yml @@ -1,5 +1,6 @@ id: variable label: Variable +flags: has_python, has_cpp parameters: - id: value @@ -13,6 +14,11 @@ templates: callbacks: - self.set_${id}(${value}) +cpp_templates: + var_make: ${id} = ${value}; + callbacks: + - this->set_${id}(${value}) + documentation: |- This block maps a value to a unique variable. This variable block has no graphical representation. diff --git a/grc/core/FlowGraph.py b/grc/core/FlowGraph.py index a47513d314..6786bbee30 100644 --- a/grc/core/FlowGraph.py +++ b/grc/core/FlowGraph.py @@ -64,7 +64,7 @@ class FlowGraph(Element): def imports(self): """ - Get a set of all import statements in this flow graph namespace. + Get a set of all import statements (Python) in this flow graph namespace. Returns: a list of import statements @@ -73,7 +73,7 @@ class FlowGraph(Element): def get_variables(self): """ - Get a list of all variables in this flow graph namespace. + Get a list of all variables (Python) in this flow graph namespace. Exclude parameterized variables. Returns: diff --git a/grc/core/blocks/_build.py b/grc/core/blocks/_build.py index 0c41d1ae70..bed0f6934a 100644 --- a/grc/core/blocks/_build.py +++ b/grc/core/blocks/_build.py @@ -33,7 +33,7 @@ from ._templates import MakoTemplates def build(id, label='', category='', flags='', documentation='', value=None, asserts=None, - parameters=None, inputs=None, outputs=None, templates=None, **kwargs): + parameters=None, inputs=None, outputs=None, templates=None, cpp_templates=None, **kwargs): block_id = id cls = type(str(block_id), (Block,), {}) @@ -63,6 +63,17 @@ def build(id, label='', category='', flags='', documentation='', callbacks=templates.get('callbacks', []), var_make=templates.get('var_make', ''), ) + + cpp_templates = cpp_templates or {} + cls.cpp_templates = MakoTemplates( + includes=cpp_templates.get('includes', ''), + make=cpp_templates.get('make', ''), + callbacks=cpp_templates.get('callbacks', []), + var_make=cpp_templates.get('var_make', ''), + link=cpp_templates.get('link', []), + translations=cpp_templates.get('translations', []), + declarations=cpp_templates.get('declarations', ''), + ) # todo: MakoTemplates.compile() to check for errors cls.value = _single_mako_expr(value, block_id) @@ -149,4 +160,3 @@ def _validate_option_attributes(param_data, block_id): if key in dir(str): del param_data['option_attributes'][key] send_warning('{} - option_attribute "{}" overrides str, ignoring'.format(block_id, key)) - diff --git a/grc/core/blocks/_flags.py b/grc/core/blocks/_flags.py index bbedd6a2d7..ec590ebbb5 100644 --- a/grc/core/blocks/_flags.py +++ b/grc/core/blocks/_flags.py @@ -25,6 +25,8 @@ class Flags(object): NEED_QT_GUI = 'need_qt_gui' DEPRECATED = 'deprecated' NOT_DSP = 'not_dsp' + HAS_PYTHON = 'has_python' + HAS_CPP = 'has_cpp' def __init__(self, flags): self.data = set(flags) diff --git a/grc/core/blocks/block.py b/grc/core/blocks/block.py index c3502397ec..4df3ec9663 100644 --- a/grc/core/blocks/block.py +++ b/grc/core/blocks/block.py @@ -21,9 +21,11 @@ from __future__ import absolute_import import collections import itertools +import copy import six from six.moves import range +import re from ._templates import MakoTemplates from ._flags import Flags @@ -31,7 +33,6 @@ from ._flags import Flags from ..base import Element from ..utils.descriptors import lazy_property - def _get_elem(iterable, key): items = list(iterable) for item in items: @@ -49,6 +50,7 @@ class Block(Element): key = '' label = '' category = '' + vtype = '' # This is only used for variables when we want C++ output flags = Flags('') documentation = {'': ''} @@ -83,6 +85,7 @@ class Block(Element): self.active_sinks = [] # on rewrite self.states = {'state': True} + self.orig_cpp_templates = self.cpp_templates # The original template, in case we have to edit it when transpiling to C++ # region Rewrite_and_Validation def rewrite(self): @@ -136,6 +139,7 @@ class Block(Element): Element.validate(self) self._run_asserts() self._validate_generate_mode_compat() + self._validate_output_language_compat() self._validate_var_value() def _run_asserts(self): @@ -161,6 +165,13 @@ class Block(Element): check_generate_mode('QT GUI', Flags.NEED_QT_GUI, ('qt_gui', 'hb_qt_gui')) + def _validate_output_language_compat(self): + """check if this block supports the selected output language""" + current_output_language = self.parent.get_option('output_language') + + if current_output_language == 'cpp' and 'has_cpp' not in self.flags: + self.add_error_message("This block does not support C++ output.") + def _validate_var_value(self): """or variables check the value (only if var_value is used)""" if self.is_variable and self.value != 'value': @@ -227,6 +238,9 @@ class Block(Element): def get_var_make(self): return self.templates.render('var_make') + def get_cpp_var_make(self): + return self.cpp_templates.render('var_make') + def get_var_value(self): return self.templates.render('var_value') @@ -241,8 +255,88 @@ class Block(Element): if 'self.' in callback: return callback return 'self.{}.{}'.format(self.name, callback) + return [make_callback(c) for c in self.templates.render('callbacks')] + def get_cpp_callbacks(self): + """ + Get a list of C++ function callbacks for this block. + + Returns: + a list of strings + """ + def make_callback(callback): + if 'this->' in callback: + return callback + return 'this->{}->{}'.format(self.name, callback) + + return [make_callback(c) for c in self.cpp_templates.render('callbacks')] + + def decide_type(self): + """ + Evaluate the value of the variable block and decide its type. + + Returns: + None + """ + value = self.params['value'].value + self.cpp_templates = copy.copy(self.orig_cpp_templates) + + def get_type(element): + try: + evaluated = ast.literal_eval(element) + + except ValueError or SyntaxError: + if re.match(r'^(numpy|np|scipy|sp)\.pi$', value): + return 'pi' + else: + return 'std::string' + + else: + _vtype = type(evaluated) + if _vtype in [int, float, bool, list]: + if _vtype == (int or long): + return 'int' + + if _vtype == float: + return 'double' + + if _vtype == bool: + return 'bool' + + if _vtype == list: + try: + first_element_type = type(evaluated[0]) + if first_element_type != str: + list_type = get_type(str(evaluated[0])) + else: + list_type = get_type(evaluated[0]) + + except IndexError: # empty list + return 'std::vector<std::string>' + + else: + return 'std::vector<' + list_type + '>' + + else: + return 'std::string' + + self.vtype = get_type(value) + if self.vtype == 'bool': + self.cpp_templates['var_make'] = self.cpp_templates['var_make'].replace('${value}', (value[0].lower() + value[1:])) + + elif self.vtype == 'pi': + self.vtype = 'double' + self.cpp_templates['var_make'] = self.cpp_templates['var_make'].replace('${value}', 'boost::math::constants::pi<double>()') + self.cpp_templates['includes'].append('#include <boost/math/constants/constants.hpp>') + + elif 'std::vector' in self.vtype: + self.cpp_templates['includes'].append('#include <vector>') + self.cpp_templates['var_make'] = self.cpp_templates['var_make'].replace('${value}', '{' + value[1:-1] + '}') + + if 'string' in self.vtype: + self.cpp_templates['includes'].append('#include <string>') + def is_virtual_sink(self): return self.key == 'virtual_sink' @@ -261,7 +355,7 @@ class Block(Element): Bypass the block Returns: - True if block chagnes state + True if block changes state """ if self.state != 'bypassed' and self.can_bypass(): self.state = 'bypassed' diff --git a/grc/core/generator/FlowGraphProxy.py b/grc/core/generator/FlowGraphProxy.py index f438fa0d39..8068156c0d 100644 --- a/grc/core/generator/FlowGraphProxy.py +++ b/grc/core/generator/FlowGraphProxy.py @@ -19,6 +19,8 @@ from __future__ import absolute_import from six.moves import range +from ..utils import expr_utils +from operator import methodcaller, attrgetter class FlowGraphProxy(object): # TODO: move this in a refactored Generator @@ -130,6 +132,34 @@ class FlowGraphProxy(object): # TODO: move this in a refactored Generator key_offset += len(pad.sinks) + len(pad.sources) return -1 + def get_cpp_variables(self): + """ + Get a list of all variables (C++) in this flow graph namespace. + Exclude parameterized variables. + + Returns: + a sorted list of variable blocks in order of dependency (indep -> dep) + """ + variables = [block for block in self.iter_enabled_blocks() if block.is_variable] + return expr_utils.sort_objects(variables, attrgetter('name'), methodcaller('get_cpp_var_make')) + + def includes(self): + """ + Get a set of all include statements (C++) in this flow graph namespace. + + Returns: + a list of #include statements + """ + return [block.cpp_templates.render('includes') for block in self.iter_enabled_blocks()] + + def links(self): + """ + Get a set of all libraries to link against (C++) in this flow graph namespace. + + Returns: + a list of GNU Radio modules + """ + return [block.cpp_templates.render('link') for block in self.iter_enabled_blocks()] def get_hier_block_io(flow_graph, direction, domain=None): """ diff --git a/grc/core/generator/Generator.py b/grc/core/generator/Generator.py index 62dc26b8a8..91526d8714 100644 --- a/grc/core/generator/Generator.py +++ b/grc/core/generator/Generator.py @@ -24,6 +24,7 @@ from mako.template import Template from .hier_block import HierBlockGenerator, QtHierBlockGenerator from .top_block import TopBlockGenerator +from .cpp_top_block import CppTopBlockGenerator DATA_DIR = os.path.dirname(__file__) FLOW_GRAPH_TEMPLATE = os.path.join(DATA_DIR, 'flow_graph.py.mako') @@ -43,12 +44,25 @@ class Generator(object): file_path: the path to the grc file """ self.generate_options = flow_graph.get_option('generate_options') - if self.generate_options == 'hb': - generator_cls = HierBlockGenerator - elif self.generate_options == 'hb_qt_gui': - generator_cls = QtHierBlockGenerator - else: - generator_cls = TopBlockGenerator + self.output_language = flow_graph.get_option('output_language') + + if self.output_language == 'python': + + if self.generate_options == 'hb': + generator_cls = HierBlockGenerator + elif self.generate_options == 'hb_qt_gui': + generator_cls = QtHierBlockGenerator + else: + generator_cls = TopBlockGenerator + + elif self.output_language == 'cpp': + + if self.generate_options == 'hb': + pass + elif self.generate_options == 'hb_qt_gui': + pass + else: + generator_cls = CppTopBlockGenerator self._generator = generator_cls(flow_graph, file_path) diff --git a/grc/core/generator/cpp_hier_block.py b/grc/core/generator/cpp_hier_block.py new file mode 100644 index 0000000000..e81d62c549 --- /dev/null +++ b/grc/core/generator/cpp_hier_block.py @@ -0,0 +1,196 @@ +import collections +import os + +import six + +from .top_block import TopBlockGenerator + +from .. import ParseXML, Constants + +class CppHierBlockGenerator(CppTopBlockGenerator): + """Extends the top block generator to also generate a block XML file""" + + def __init__(self, flow_graph, file_path): + """ + Initialize the hier block generator object. + + Args: + flow_graph: the flow graph object + file_path: where to write the py file (the xml goes into HIER_BLOCK_LIB_DIR) + """ + CppTopBlockGenerator.__init__(self, flow_graph, file_path) + platform = flow_graph.parent + + hier_block_lib_dir = platform.config.hier_block_lib_dir + if not os.path.exists(hier_block_lib_dir): + os.mkdir(hier_block_lib_dir) + + self._mode = Constants.HIER_BLOCK_FILE_MODE + self.file_path = os.path.join(hier_block_lib_dir, self._flow_graph.get_option('id')) + self.file_path_xml = self.file_path + '.xml' # TODO: yml + + def write(self): + """generate output and write it to files""" + TopBlockGenerator.write(self) + ParseXML.to_file(self._build_block_n_from_flow_graph_io(), self.file_path_xml) + ParseXML.validate_dtd(self.file_path_xml, Constants.BLOCK_DTD) + try: + os.chmod(self.file_path_xml, self._mode) + except: + pass + + def _build_block_n_from_flow_graph_io(self): + """ + Generate a block XML nested data from the flow graph IO + + Returns: + a xml node tree + """ + # Extract info from the flow graph + block_id = self._flow_graph.get_option('id') + parameters = self._flow_graph.get_parameters() + + def var_or_value(name): + if name in (p.name for p in parameters): + return "${" + name + " }" + return name + + # Build the nested data + data = collections.OrderedDict() + data['id'] = block_id + data['label'] = ( + self._flow_graph.get_option('title') or + self._flow_graph.get_option('id').replace('_', ' ').title() + ) + data['category'] = self._flow_graph.get_option('category') + + # Parameters + data['parameters'] = [] + for param_block in parameters: + p = collections.OrderedDict() + p['id'] = param_block.name + p['label'] = param_block.params['label'].get_value() or param_block.name + p['dtype'] = 'raw' + p['value'] = param_block.params['value'].get_value() + p['hide'] = param_block.params['hide'].get_value() + data['param'].append(p) + + # Ports + for direction in ('inputs', 'outputs'): + data[direction] = [] + for port in get_hier_block_io(self._flow_graph, direction): + p = collections.OrderedDict() + if port.domain == Constants.GR_MESSAGE_DOMAIN: + p['id'] = port.id + #p['label'] = port.label + if port.domain != Constants.DEFAULT_DOMAIN: + p['domain'] = port.domain + p['dtype'] = port.dtype + if port.domain != Constants.GR_MESSAGE_DOMAIN: + p['vlen'] = var_or_value(port.vlen) + if port.optional: + p['optional'] = True + data[direction].append(p) + + t = data['templates'] = collections.OrderedDict() + + t['import'] = "from {0} import {0} # grc-generated hier_block".format( + self._flow_graph.get_option('id')) + # Make data + if parameters: + t['make'] = '{cls}(\n {kwargs},\n)'.format( + cls=block_id, + kwargs=',\n '.join( + '{key}=${key}'.format(key=param.name) for param in parameters + ), + ) + else: + t['make'] = '{cls}()'.format(cls=block_id) + # Callback data + t['callback'] = [ + 'set_{key}(${key})'.format(key=param_block.name) for param_block in parameters + ] + + + # Documentation + data['doc'] = "\n".join(field for field in ( + self._flow_graph.get_option('author'), + self._flow_graph.get_option('description'), + self.file_path + ) if field) + data['grc_source'] = str(self._flow_graph.grc_file_path) + + n = {'block': data} + return n + + +class CppQtHierBlockGenerator(CppHierBlockGenerator): + + def _build_block_n_from_flow_graph_io(self): + n = CppHierBlockGenerator._build_block_n_from_flow_graph_io(self) + block_n = collections.OrderedDict() + + # insert flags after category + for key, value in six.iteritems(n['block']): + block_n[key] = value + if key == 'category': + block_n['flags'] = 'need_qt_gui' + + if not block_n['name'].upper().startswith('QT GUI'): + block_n['name'] = 'QT GUI ' + block_n['name'] + + gui_hint_param = collections.OrderedDict() + gui_hint_param['name'] = 'GUI Hint' + gui_hint_param['key'] = 'gui_hint' + gui_hint_param['value'] = '' + gui_hint_param['type'] = 'gui_hint' + gui_hint_param['hide'] = 'part' + block_n['param'].append(gui_hint_param) + + block_n['make'] += ( + "\n#set $win = 'self.%s' % $id" + "\n${gui_hint()($win)}" + ) + + return {'block': block_n} + + +def get_hier_block_io(flow_graph, direction, domain=None): + """ + Get a list of io ports for this flow graph. + + Returns a list of dicts with: type, label, vlen, size, optional + """ + pads = flow_graph.get_pad_sources() if direction == 'inputs' else flow_graph.get_pad_sinks() + + ports = [] + for pad in pads: + for port in (pad.sources if direction == 'inputs' else pad.sinks): + if domain and port.domain != domain: + continue + yield port + + type_param = pad.params['type'] + master = { + 'label': str(pad.params['label'].get_evaluated()), + 'type': str(pad.params['type'].get_evaluated()), + 'vlen': str(pad.params['vlen'].get_value()), + 'size': type_param.options.attributes[type_param.get_value()]['size'], + 'optional': bool(pad.params['optional'].get_evaluated()), + } + + if domain and pad: + num_ports = pad.params['num_streams'].get_evaluated() + if num_ports <= 1: + yield master + else: + for i in range(num_ports): + clone = master.copy() + clone['label'] += str(i) + ports.append(clone) + else: + ports.append(master) + + if domain is not None: + ports = [p for p in ports if p.domain == domain] + yield ports # TODO: Not sure this fix is correct diff --git a/grc/core/generator/cpp_templates/CMakeLists.txt.mako b/grc/core/generator/cpp_templates/CMakeLists.txt.mako new file mode 100644 index 0000000000..56f39c6b22 --- /dev/null +++ b/grc/core/generator/cpp_templates/CMakeLists.txt.mako @@ -0,0 +1,64 @@ +##################### +# GNU Radio C++ Flow Graph CMakeLists.txt +# +# Title: ${title} +# % if flow_graph.get_option('author'): +# Author: ${flow_graph.get_option('author')} +# % endif +# % if flow_graph.get_option('description'): +# Description: ${flow_graph.get_option('description')} +# % endif +# Generated: ${generated_time} +##################### + +<% +class_name = flow_graph.get_option('id') +cmake_opt_list = flow_graph.get_option('cmake_opt').split(";") +%>\ + +# cmake_minimum_required(VERSION 3.8) Which version? + +% if generate_options == 'qt_gui': +find_package(Qt5Widgets REQUIRED) +% endif + +include_directories( + ${'$'}{GNURADIO_ALL_INCLUDE_DIRS} + ${'$'}{Boost_INCLUDE_DIRS} + % if generate_options == 'qt_gui': + ${'$'}{Qt5Widgets_INCLUDES} + % endif +) + +% if generate_options == 'qt_gui': +add_definitions(${'$'}{Qt5Widgets_DEFINITIONS}) + +set(CMAKE_AUTOMOC TRUE) +% endif + +% if cmake_opt_list != ['']: +% for opt in cmake_opt_list: +set(${opt.split("=")[0].strip()} ${opt.split("=")[1].strip()}) +% endfor +% endif + +% if flow_graph.get_option('gen_linking') == 'static': +set(BUILD_SHARED_LIBS false) +set(CMAKE_EXE_LINKER_FLAGS " -static") +set(CMAKE_FIND_LIBRARY_SUFFIXES ".a") +% endif + +set(GR_LIBRARIES + boost_system + gnuradio-blocks + gnuradio-runtime + gnuradio-pmt + % for link in links: + % if link != '': + ${link} + % endif + % endfor +) + +add_executable(${class_name} ${class_name}.cpp) +target_link_libraries(${class_name} ${'$'}{GR_LIBRARIES}) diff --git a/grc/core/generator/cpp_templates/flow_graph.cpp.mako b/grc/core/generator/cpp_templates/flow_graph.cpp.mako new file mode 100644 index 0000000000..b919015f86 --- /dev/null +++ b/grc/core/generator/cpp_templates/flow_graph.cpp.mako @@ -0,0 +1,155 @@ +/******************** +GNU Radio C++ Flow Graph Source File + +Title: ${title} +% if flow_graph.get_option('author'): +Author: ${flow_graph.get_option('author')} +% endif +% if flow_graph.get_option('description'): +Description: ${flow_graph.get_option('description')} +% endif +Generated: ${generated_time} +********************/ + +#include "${flow_graph.get_option('id')}.hpp" + +using namespace gr; + +<% + class_name = flow_graph.get_option('id') +## TODO: param_str +%>\ +<%def name="indent(code)">${ '\n '.join(str(code).splitlines()) }</%def> + +## TODO: param_str +% if generate_options == 'no_gui': +${class_name}::${class_name} () : top_block("${title}") { +% elif generate_options.startswith('hb'): +## TODO: make_io_sig +${class_name}::${class_name} () : hier_block2("${title}") { +% for pad in flow_graph.get_hier_block_message_io('in'): + message_port_register_hier_in("${pad['label']}") +% endfor +% for pad in flow_graph.get_hier_block_message_io('out'): + message_port_register_hier_out("${pad['label']}") +% endfor +% elif generate_options == 'qt_gui': +${class_name}::${class_name} () : QWidget(), top_block("display_qt") { + this->setWindowTitle("${title}"); + // check_set_qss + // set icon + this->top_scroll_layout = new QVBoxLayout(); + this->setLayout(this->top_scroll_layout); + this->top_scroll = new QScrollArea(); + this->top_scroll->setFrameStyle(QFrame::NoFrame); + this->top_scroll_layout->addWidget(this->top_scroll); + this->top_scroll->setWidgetResizable(true); + this->top_widget = new QWidget(); + this->top_scroll->setWidget(this->top_widget); + this->top_layout = new QVBoxLayout(this->top_widget); + this->top_grid_layout = new QGridLayout(); + this->top_layout->addLayout(this->top_grid_layout); + + this->settings = new QSettings("GNU Radio", "${class_name}"); + +% endif + +% if flow_graph.get_option('thread_safe_setters'): +## self._lock = threading.RLock() +% endif + +% if blocks: +// Blocks: +% for blk, blk_make, declarations in blocks: + { + ${indent(blk_make)} +## % if 'alias' in blk.params and blk.params['alias'].get_evaluated(): +## ${blk.name}.set_block_alias("${blk.params['alias'].get_evaluated()}") +## % endif +## % if 'affinity' in blk.params and blk.params['affinity'].get_evaluated(): +## ${blk.name}.set_processor_affinity("${blk.params['affinity'].get_evaluated()}") +## % endif +## % if len(blk.sources) > 0 and 'minoutbuf' in blk.params and int(blk.params['minoutbuf'].get_evaluated()) > 0: +## ${blk.name}.set_min_output_buffer(${blk.params['minoutbuf'].get_evaluated()}) +## % endif +## % if len(blk.sources) > 0 and 'maxoutbuf' in blk.params and int(blk.params['maxoutbuf'].get_evaluated()) > 0: +## ${blk.name}.set_max_output_buffer(${blk.params['maxoutbuf'].get_evaluated()}) +## % endif + } +% endfor +% endif + +% if connections: +// Connections: +% for connection in connections: + ${connection.rstrip()}; +% endfor +% endif +} + +${class_name}::~${class_name} () { +} + +// Callbacks: +% for var in parameters + variables: +${var.vtype} ${class_name}::get_${var.name} () const { + return this->${var.name}; +} + +void ${class_name}::set_${var.name} (${var.vtype} ${var.name}) { +% if flow_graph.get_option('thread_safe_setters'): + ## with self._lock: + return; +% else: + this->${var.name} = ${var.name}; + % for callback in callbacks[var.name]: + ${callback}; + % endfor +% endif +} +% endfor + +int main (int argc, char **argv) { + % if parameters: + ## parse args + % endif + % if flow_graph.get_option('realtime_scheduling'): + if (enable_realtime_scheduling() != RT_OK) { + std::cout << "Error: failed to enable real-time scheduling." << std::endl; + } + % endif + + % if generate_options == 'no_gui': + ${class_name}* top_block = new ${class_name}(); + ## TODO: params + % if flow_graph.get_option('run_options') == 'prompt': + top_block->start(); + % for m in monitors: + (top_block->${m.name}).start(); + % endfor + std::cout << "Press Enter to quit: "; + std::cin.ignore(); + top_block->stop(); + % elif flow_graph.get_option('run_options') == 'run': + top_block->start(); + % endif + % for m in monitors: + (top_block->${m.name}).start(); + % endfor + top_block->wait(); + % elif generate_options == 'qt_gui': + QApplication app(argc, argv); + + ${class_name} *top_block = new ${class_name}(); + + top_block->start(); + top_block->show(); + app.exec(); + + % endif + + return 0; +} +% if generate_options == 'qt_gui': +#include "moc_${class_name}.cpp" +% endif diff --git a/grc/core/generator/cpp_templates/flow_graph.hpp.mako b/grc/core/generator/cpp_templates/flow_graph.hpp.mako new file mode 100644 index 0000000000..0acd90734a --- /dev/null +++ b/grc/core/generator/cpp_templates/flow_graph.hpp.mako @@ -0,0 +1,89 @@ +#ifndef ${flow_graph.get_option('id').upper()}_HPP +#define ${flow_graph.get_option('id').upper()}_HPP +/******************** +GNU Radio C++ Flow Graph Header File + +Title: ${title} +% if flow_graph.get_option('author'): +Author: ${flow_graph.get_option('author')} +% endif +% if flow_graph.get_option('description'): +Description: ${flow_graph.get_option('description')} +% endif +Generated: ${generated_time} +********************/ + +/******************** +** Create includes +********************/ +% for inc in includes: +${inc} +% endfor + +% if generate_options == 'qt_gui': +#include <QVBoxLayout> +#include <QScrollArea> +#include <QWidget> +#include <QGridLayout> +#include <QSettings> +% endif + +using namespace gr; + +<% + class_name = flow_graph.get_option('id') +## TODO: param_str +%>\ + +% if generate_options == 'no_gui': +class ${class_name} : public top_block { +% elif generate_options.startswith('hb'): +class ${class_name} : public hier_block2 { +% elif generate_options == 'qt_gui': +class ${class_name} : public QWidget, public top_block { + Q_OBJECT +% endif + +private: +% if generate_options == 'qt_gui': + QVBoxLayout *top_scroll_layout; + QScrollArea *top_scroll; + QWidget *top_widget; + QVBoxLayout *top_layout; + QGridLayout *top_grid_layout; + QSettings *settings; +% endif + + +% for block, make, declarations in blocks: + % if declarations: + ${declarations} + % endif +% endfor + +% if parameters: +// Parameters: +% for param in parameters: + ${param.get_var_make()} +% endfor +% endif + +% if variables: +// Variables: +% for var in variables: + ${var.vtype} ${var.cpp_templates.render('var_make')} +% endfor +% endif + +public: + ${class_name}(); + ## TODO: param_str + ~${class_name}(); + +% for var in parameters + variables: + ${var.vtype} get_${var.name} () const; + void set_${var.name}(${var.vtype} ${var.name}); +% endfor + +}; +#endif diff --git a/grc/core/generator/cpp_top_block.py b/grc/core/generator/cpp_top_block.py new file mode 100644 index 0000000000..6991f22bf2 --- /dev/null +++ b/grc/core/generator/cpp_top_block.py @@ -0,0 +1,344 @@ +import codecs +import operator +import os +import tempfile +import textwrap +import time +import re + +from mako.template import Template + +from .. import Messages, blocks +from ..Constants import TOP_BLOCK_FILE_MODE +from .FlowGraphProxy import FlowGraphProxy +from ..utils import expr_utils +from .top_block import TopBlockGenerator + +DATA_DIR = os.path.dirname(__file__) + +HEADER_TEMPLATE = os.path.join(DATA_DIR, 'cpp_templates/flow_graph.hpp.mako') +SOURCE_TEMPLATE = os.path.join(DATA_DIR, 'cpp_templates/flow_graph.cpp.mako') +CMAKE_TEMPLATE = os.path.join(DATA_DIR, 'cpp_templates/CMakeLists.txt.mako') + +header_template = Template(filename=HEADER_TEMPLATE) +source_template = Template(filename=SOURCE_TEMPLATE) +cmake_template = Template(filename=CMAKE_TEMPLATE) + + +class CppTopBlockGenerator(TopBlockGenerator): + + def __init__(self, flow_graph, file_path): + """ + Initialize the top block generator object. + + Args: + flow_graph: the flow graph object + file_path: the path to write the file to + """ + + self._flow_graph = FlowGraphProxy(flow_graph) + self._generate_options = self._flow_graph.get_option('generate_options') + + self._mode = TOP_BLOCK_FILE_MODE + dirname = os.path.dirname(file_path) + # Handle the case where the directory is read-only + # In this case, use the system's temp directory + if not os.access(dirname, os.W_OK): + dirname = tempfile.gettempdir() + + filename = self._flow_graph.get_option('id') + self.file_path = os.path.join(dirname, filename) + self._dirname = dirname + + def write(self): + """generate output and write it to files""" + self._warnings() + + fg = self._flow_graph + self.title = fg.get_option('title') or fg.get_option('id').replace('_', ' ').title() + variables = fg.get_cpp_variables() + parameters = fg.get_parameters() + monitors = fg.get_monitors() + self._variable_types() + + self.namespace = { + 'flow_graph': fg, + 'variables': variables, + 'parameters': parameters, + 'monitors': monitors, + 'generate_options': self._generate_options, + 'generated_time': time.ctime(), + } + + if not os.path.exists(os.path.join(self.file_path, 'build')): + os.makedirs(os.path.join(self.file_path, 'build')) + + for filename, data in self._build_cpp_header_code_from_template(): + with codecs.open(filename, 'w', encoding='utf-8') as fp: + fp.write(data) + + for filename, data in self._build_cpp_source_code_from_template(): + with codecs.open(filename, 'w', encoding='utf-8') as fp: + fp.write(data) + + if fg.get_option('gen_cmake') == 'On': + for filename, data in self._build_cmake_code_from_template(): + with codecs.open(filename, 'w', encoding='utf-8') as fp: + fp.write(data) + + def _build_cpp_source_code_from_template(self): + """ + Convert the flow graph to a C++ source file. + + Returns: + a string of C++ code + """ + file_path = self.file_path + '/' + self._flow_graph.get_option('id') + '.cpp' + + output = [] + + flow_graph_code = source_template.render( + title=self.title, + includes=self._includes(), + blocks=self._blocks(), + callbacks=self._callbacks(), + connections=self._connections(), + **self.namespace + ) + # strip trailing white-space + flow_graph_code = "\n".join(line.rstrip() for line in flow_graph_code.split("\n")) + output.append((file_path, flow_graph_code)) + + return output + + + def _build_cpp_header_code_from_template(self): + """ + Convert the flow graph to a C++ header file. + + Returns: + a string of C++ code + """ + file_path = self.file_path + '/' + self._flow_graph.get_option('id') + '.hpp' + + output = [] + + flow_graph_code = header_template.render( + title=self.title, + includes=self._includes(), + blocks=self._blocks(), + callbacks=self._callbacks(), + connections=self._connections(), + **self.namespace + ) + # strip trailing white-space + flow_graph_code = "\n".join(line.rstrip() for line in flow_graph_code.split("\n")) + output.append((file_path, flow_graph_code)) + + return output + + def _build_cmake_code_from_template(self): + """ + Convert the flow graph to a CMakeLists.txt file. + + Returns: + a string of CMake code + """ + filename = 'CMakeLists.txt' + file_path = os.path.join(self.file_path, filename) + + output = [] + + flow_graph_code = cmake_template.render( + title=self.title, + includes=self._includes(), + blocks=self._blocks(), + callbacks=self._callbacks(), + connections=self._connections(), + links=self._links(), + **self.namespace + ) + # strip trailing white-space + flow_graph_code = "\n".join(line.rstrip() for line in flow_graph_code.split("\n")) + output.append((file_path, flow_graph_code)) + + return output + + def _links(self): + fg = self._flow_graph + links = fg.links() + seen = set() + output = [] + + for link_list in links: + if link_list: + for link in link_list: + seen.add(link) + + return list(seen) + + def _includes(self): + fg = self._flow_graph + includes = fg.includes() + seen = set() + output = [] + + def is_duplicate(l): + if l.startswith('#include') and l in seen: + return True + seen.add(line) + return False + + for block_ in includes: + for include_ in block_: + if not include_: + continue + line = include_.rstrip() + if not is_duplicate(line): + output.append(line) + + return output + + def _blocks(self): + fg = self._flow_graph + parameters = fg.get_parameters() + + # List of blocks not including variables and imports and parameters and disabled + def _get_block_sort_text(block): + code = block.cpp_templates.render('make').replace(block.name, ' ') + try: + code += block.params['gui_hint'].get_value() # Newer gui markup w/ qtgui + except: + pass + return code + + blocks = [ + b for b in fg.blocks + if b.enabled and not (b.get_bypassed() or b.is_import or b in parameters or b.key == 'options') + ] + + blocks = expr_utils.sort_objects(blocks, operator.attrgetter('name'), _get_block_sort_text) + blocks_make = [] + for block in blocks: + translations = block.cpp_templates.render('translations') + make = block.cpp_templates.render('make') + declarations = block.cpp_templates.render('declarations') + + if translations: + # TODO: Would be nice to avoid using eval() here + translations_dict = eval(translations) + for key in translations_dict: + make = make.replace(key, translations_dict[key]) + declarations = declarations.replace(key, translations_dict[key]) + if make: + blocks_make.append((block, make, declarations)) + elif 'qt' in block.key: + # The QT Widget blocks are technically variables, + # but they contain some code we don't want to miss + blocks_make.append(('', make, declarations)) + return blocks_make + + def _variable_types(self): + fg = self._flow_graph + variables = fg.get_cpp_variables() + + for var in variables: + var.decide_type() + + def _callbacks(self): + fg = self._flow_graph + variables = fg.get_cpp_variables() + parameters = fg.get_parameters() + + # List of variable names + var_ids = [var.name for var in parameters + variables] + + replace_dict = dict((var_id, 'this->' + var_id) for var_id in var_ids) + + callbacks_all = [] + for block in fg.iter_enabled_blocks(): + callbacks_all.extend(expr_utils.expr_replace(cb, replace_dict) for cb in block.get_cpp_callbacks()) + + # Map var id to callbacks + def uses_var_id(callback): + used = expr_utils.get_variable_dependencies(callback, [var_id]) + return used and ('this->' + var_id in callback) # callback might contain var_id itself + + callbacks = {} + for var_id in var_ids: + callbacks[var_id] = [callback for callback in callbacks_all if uses_var_id(callback)] + + return callbacks + + def _connections(self): + fg = self._flow_graph + templates = {key: Template(text) + for key, text in fg.parent_platform.cpp_connection_templates.items()} + + def make_port_sig(port): + # TODO: make sense of this + if port.parent.key in ('pad_source', 'pad_sink'): + block = 'self' + key = fg.get_pad_port_global_key(port) + else: + block = 'this->' + port.parent_block.name + key = port.key + + if not key.isdigit(): + key = re.findall(r'\d+', key)[0] + + return '{block}, {key}'.format(block=block, key=key) + + connections = fg.get_enabled_connections() + + # Get the virtual blocks and resolve their connections + connection_factory = fg.parent_platform.Connection + virtual = [c for c in connections if isinstance(c.source_block, blocks.VirtualSource)] + for connection in virtual: + sink = connection.sink_port + for source in connection.source_port.resolve_virtual_source(): + resolved = connection_factory(fg.orignal_flowgraph, source, sink) + connections.append(resolved) + # Remove the virtual connection + connections.remove(connection) + + # Bypassing blocks: Need to find all the enabled connections for the block using + # the *connections* object rather than get_connections(). Create new connections + # that bypass the selected block and remove the existing ones. This allows adjacent + # bypassed blocks to see the newly created connections to downstream blocks, + # allowing them to correctly construct bypass connections. + bypassed_blocks = fg.get_bypassed_blocks() + for block in bypassed_blocks: + # Get the upstream connection (off of the sink ports) + # Use *connections* not get_connections() + source_connection = [c for c in connections if c.sink_port == block.sinks[0]] + # The source connection should never have more than one element. + assert (len(source_connection) == 1) + + # Get the source of the connection. + source_port = source_connection[0].source_port + + # Loop through all the downstream connections + for sink in (c for c in connections if c.source_port == block.sources[0]): + if not sink.enabled: + # Ignore disabled connections + continue + connection = connection_factory(fg.orignal_flowgraph, source_port, sink.sink_port) + connections.append(connection) + # Remove this sink connection + connections.remove(sink) + # Remove the source connection + connections.remove(source_connection[0]) + + # List of connections where each endpoint is enabled (sorted by domains, block names) + def by_domain_and_blocks(c): + return c.type, c.source_block.name, c.sink_block.name + + rendered = [] + for con in sorted(connections, key=by_domain_and_blocks): + template = templates[con.type] + code = template.render(make_port_sig=make_port_sig, source=con.source_port, sink=con.sink_port) + code = 'this->' + code + rendered.append(code) + + return rendered diff --git a/grc/core/generator/top_block.py b/grc/core/generator/top_block.py index b5661762c0..cab1bf71cb 100644 --- a/grc/core/generator/top_block.py +++ b/grc/core/generator/top_block.py @@ -13,8 +13,10 @@ from .FlowGraphProxy import FlowGraphProxy from ..utils import expr_utils DATA_DIR = os.path.dirname(__file__) -FLOW_GRAPH_TEMPLATE = os.path.join(DATA_DIR, 'flow_graph.py.mako') -flow_graph_template = Template(filename=FLOW_GRAPH_TEMPLATE) + +PYTHON_TEMPLATE = os.path.join(DATA_DIR, 'flow_graph.py.mako') + +python_template = Template(filename=PYTHON_TEMPLATE) class TopBlockGenerator(object): @@ -65,6 +67,21 @@ class TopBlockGenerator(object): """generate output and write it to files""" self._warnings() + fg = self._flow_graph + self.title = fg.get_option('title') or fg.get_option('id').replace('_', ' ').title() + variables = fg.get_variables() + parameters = fg.get_parameters() + monitors = fg.get_monitors() + + self.namespace = { + 'flow_graph': fg, + 'variables': variables, + 'parameters': parameters, + 'monitors': monitors, + 'generate_options': self._generate_options, + 'generated_time': time.ctime(), + } + for filename, data in self._build_python_code_from_template(): with codecs.open(filename, 'w', encoding='utf-8') as fp: fp.write(data) @@ -106,13 +123,13 @@ class TopBlockGenerator(object): 'generated_time': time.ctime(), 'version': platform.config.version } - flow_graph_code = flow_graph_template.render( + flow_graph_code = python_template.render( title=title, imports=self._imports(), blocks=self._blocks(), callbacks=self._callbacks(), connections=self._connections(), - **namespace + **self.namespace ) # strip trailing white-space flow_graph_code = "\n".join(line.rstrip() for line in flow_graph_code.split("\n")) @@ -196,7 +213,9 @@ class TopBlockGenerator(object): # List of variable names var_ids = [var.name for var in parameters + variables] + replace_dict = dict((var_id, 'self.' + var_id) for var_id in var_ids) + callbacks_all = [] for block in fg.iter_enabled_blocks(): callbacks_all.extend(expr_utils.expr_replace(cb, replace_dict) for cb in block.get_callbacks()) @@ -204,7 +223,7 @@ class TopBlockGenerator(object): # Map var id to callbacks def uses_var_id(callback): used = expr_utils.get_variable_dependencies(callback, [var_id]) - return used and 'self.' + var_id in callback # callback might contain var_id itself + return used and (('self.' + var_id in callback) or ('this->' + var_id in callback)) # callback might contain var_id itself callbacks = {} for var_id in var_ids: @@ -218,6 +237,7 @@ class TopBlockGenerator(object): for key, text in fg.parent_platform.connection_templates.items()} def make_port_sig(port): + # TODO: make sense of this if port.parent.key in ('pad_source', 'pad_sink'): block = 'self' key = fg.get_pad_port_global_key(port) @@ -282,6 +302,7 @@ class TopBlockGenerator(object): for con in sorted(connections, key=by_domain_and_blocks): template = templates[con.type] code = template.render(make_port_sig=make_port_sig, source=con.source_port, sink=con.sink_port) + code = 'self.' + code rendered.append(code) return rendered diff --git a/grc/core/platform.py b/grc/core/platform.py index fc86be927d..5449a117ad 100644 --- a/grc/core/platform.py +++ b/grc/core/platform.py @@ -22,6 +22,7 @@ from collections import namedtuple import os import logging from itertools import chain +import re import six from six.moves import range @@ -61,6 +62,7 @@ class Platform(Element): self.blocks = self.block_classes self.domains = {} self.connection_templates = {} + self.cpp_connection_templates = {} self._block_categories = {} self._auto_hier_block_generate_chain = set() @@ -138,6 +140,7 @@ class Platform(Element): self.blocks.clear() self.domains.clear() self.connection_templates.clear() + self.cpp_connection_templates.clear() self._block_categories.clear() # # FIXME: remove this as soon as converter is stable @@ -277,6 +280,7 @@ class Platform(Element): continue connection_id = str(source_id), str(sink_id) self.connection_templates[connection_id] = connection.get('connect', '') + self.cpp_connection_templates[connection_id] = connection.get('cpp_connect', '') def load_category_tree_description(self, data, file_path): """Parse category tree file and add it to list""" @@ -362,7 +366,10 @@ class Platform(Element): ('metadata:\n', '\nmetadata:\n'), ] for r in replace: + # the purpose of using re.sub() here is to avoid corrupting the .grc file + out = re.sub(r'(nconnections)(?<!\\)', 'NCONNECTIONS', out) out = out.replace(*r) + out = re.sub(r'(NCONNECTIONS)(?<!\\)', 'nconnections', out) with open(filename, 'w', encoding='utf-8') as fp: fp.write(out) @@ -377,6 +384,16 @@ class Platform(Element): return [(value, name, value == generate_mode_default) for value, name in zip(param['options'], param['option_labels'])] + def get_output_language(self): + for param in self.block_classes['options'].parameters_data: + if param.get('id') == 'output_language': + break + else: + return [] + output_language_default = param.get('default') + return [(value, name, value == output_language_default) + for value, name in zip(param['options'], param['option_labels'])] + ############################################## # Factories ############################################## diff --git a/grc/core/schema_checker/block.py b/grc/core/schema_checker/block.py index 92ed48758c..91ce906df2 100644 --- a/grc/core/schema_checker/block.py +++ b/grc/core/schema_checker/block.py @@ -35,6 +35,14 @@ TEMPLATES_SCHEME = expand( make=str_, callbacks=list, ) +CPP_TEMPLATES_SCHEME = expand( + includes=list, + declarations=str_, + make=str_, + callbacks=list, + link=list, + translations=dict, +) BLOCK_SCHEME = expand( id=Spec(types=str_, required=True, item_scheme=None), label=str_, @@ -49,6 +57,7 @@ BLOCK_SCHEME = expand( value=str_, templates=Spec(types=dict, required=False, item_scheme=TEMPLATES_SCHEME), + cpp_templates=Spec(types=dict, required=False, item_scheme=CPP_TEMPLATES_SCHEME), documentation=str_, grc_source=str_, diff --git a/grc/gui/Executor.py b/grc/gui/Executor.py index efed54b042..79bb29f101 100644 --- a/grc/gui/Executor.py +++ b/grc/gui/Executor.py @@ -44,7 +44,11 @@ class ExecFlowGraphThread(threading.Thread): self.update_callback = callback try: - self.process = self.page.process = self._popen() + if self.flow_graph.get_option('output_language') == 'python': + self.process = self.page.process = self._popen() + elif self.flow_graph.get_option('output_language') == 'cpp': + self.process = self.page.process = self._cpp_popen() + self.update_callback() self.start() except Exception as e: @@ -75,6 +79,74 @@ class ExecFlowGraphThread(threading.Thread): shell=False, universal_newlines=True ) + def _cpp_run_cmake(self): + """ + Generate and compile this C++ flow graph. + """ + generator = self.page.get_generator() + + xterm_executable = find_executable(self.xterm_executable) + + dirname = generator.file_path + builddir = os.path.join(dirname, 'build') + + run_command_args = [ 'cmake', '..' ] + Messages.send_start_exec(' '.join(run_command_args)) + + return subprocess.Popen( + args=run_command_args, + cwd=builddir, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + shell=False, universal_newlines=True + ) + + def _cpp_compile(self): + """ + Compile this C++ flow graph. + """ + generator = self.page.get_generator() + + xterm_executable = find_executable(self.xterm_executable) + + dirname = generator.file_path + builddir = os.path.join(dirname, 'build') + + run_command_args = [ 'make' ] + Messages.send_start_exec(' '.join(run_command_args)) + + return subprocess.Popen( + args=run_command_args, + cwd=builddir, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + shell=False, universal_newlines=True + ) + + + def _cpp_popen(self): + """ + Execute this C++ flow graph after generating and compiling it. + """ + generator = self.page.get_generator() + run_command = generator.file_path + '/build/' + self.flow_graph.get_option('id') + + if os.path.isfile(run_command): + os.remove(run_command) + + xterm_executable = find_executable(self.xterm_executable) + process = self._cpp_run_cmake() + process.wait() + process = self._cpp_compile() + process.wait() + + run_command_args = [xterm_executable, '-e', run_command] + Messages.send_start_exec(' '.join(run_command_args)) + + return subprocess.Popen( + args=run_command_args, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + shell=False, universal_newlines=True + ) + def run(self): """ Wait on the executing process by reading from its stdout. diff --git a/grc/tests/test_cpp.py b/grc/tests/test_cpp.py new file mode 100644 index 0000000000..fc52218477 --- /dev/null +++ b/grc/tests/test_cpp.py @@ -0,0 +1,18 @@ + + +from argparse import Namespace +from os import path +import tempfile +import subprocess + +from .test_compiler import * + +def test_cpp(): + test_compiler('test_cpp.grc') + if not os.path.exists('test_cpp/build'): + os.makedirs('directory') + return_code = subprocess.Popen('cmake ..', cwd='./build/') + assert(return_code == 0) + return_code = subprocess.Popen('make', cwd='./build/') + assert(return_code == 0) + |