libuavcan_dsdl_compiler/__init__.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 #
3 # UAVCAN DSDL compiler for libuavcan
4 #
5 # Copyright (C) 2014 Pavel Kirienko <pavel.kirienko@gmail.com>
6 #
7 
8 '''
9 This module implements the core functionality of the UAVCAN DSDL compiler for libuavcan.
10 Supported Python versions: 3.2+, 2.7.
11 It accepts a list of root namespaces and produces the set of C++ header files for libuavcan.
12 It is based on the DSDL parsing package from pyuavcan.
13 '''
14 
15 from __future__ import division, absolute_import, print_function, unicode_literals
16 import sys, os, logging, errno, re
17 from .pyratemp import Template
18 from pyuavcan_v0 import dsdl
19 
20 # Python 2.7 compatibility
21 try:
22  str = unicode
23 except NameError:
24  pass
25 
26 OUTPUT_FILE_EXTENSION = 'hpp'
27 OUTPUT_FILE_PERMISSIONS = 0o444 # Read only for all
28 TEMPLATE_FILENAME = os.path.join(os.path.dirname(__file__), 'data_type_template.tmpl')
29 
30 __all__ = ['run', 'logger', 'DsdlCompilerException']
31 
32 class DsdlCompilerException(Exception):
33  pass
34 
35 logger = logging.getLogger(__name__)
36 
37 def run(source_dirs, include_dirs, output_dir):
38  '''
39  This function takes a list of root namespace directories (containing DSDL definition files to parse), a
40  possibly empty list of search directories (containing DSDL definition files that can be referenced from the types
41  that are going to be parsed), and the output directory path (possibly nonexistent) where the generated C++
42  header files will be stored.
43 
44  Note that this module features lazy write, i.e. if an output file does already exist and its content is not going
45  to change, it will not be overwritten. This feature allows to avoid unnecessary recompilation of dependent object
46  files.
47 
48  Args:
49  source_dirs List of root namespace directories to parse.
50  include_dirs List of root namespace directories with referenced types (possibly empty). This list is
51  automaitcally extended with source_dirs.
52  output_dir Output directory path. Will be created if doesn't exist.
53  '''
54  assert isinstance(source_dirs, list)
55  assert isinstance(include_dirs, list)
56  output_dir = str(output_dir)
57 
58  types = run_parser(source_dirs, include_dirs + source_dirs)
59  if not types:
60  die('No type definitions were found')
61 
62  logger.info('%d types total', len(types))
63  run_generator(types, output_dir)
64 
65 # -----------------
66 
67 def pretty_filename(filename):
68  try:
69  a = os.path.abspath(filename)
70  r = os.path.relpath(filename)
71  return a if '..' in r else r
72  except ValueError:
73  return filename
74 
76  assert t.category == t.CATEGORY_COMPOUND
77  return t.full_name.replace('.', os.path.sep) + '.' + OUTPUT_FILE_EXTENSION
78 
79 def makedirs(path):
80  try:
81  try:
82  os.makedirs(path, exist_ok=True) # May throw "File exists" when executed as root, which is wrong
83  except TypeError:
84  os.makedirs(path) # Python 2.7 compatibility
85  except OSError as ex:
86  if ex.errno != errno.EEXIST: # http://stackoverflow.com/questions/12468022
87  raise
88 
89 def die(text):
90  raise DsdlCompilerException(str(text))
91 
92 def run_parser(source_dirs, search_dirs):
93  try:
94  types = dsdl.parse_namespaces(source_dirs, search_dirs)
95  except dsdl.DsdlException as ex:
96  logger.info('Parser failure', exc_info=True)
97  die(ex)
98  return types
99 
100 def run_generator(types, dest_dir):
101  try:
102  template_expander = make_template_expander(TEMPLATE_FILENAME)
103  dest_dir = os.path.abspath(dest_dir) # Removing '..'
104  makedirs(dest_dir)
105  for t in types:
106  logger.info('Generating type %s', t.full_name)
107  filename = os.path.join(dest_dir, type_output_filename(t))
108  text = generate_one_type(template_expander, t)
109  write_generated_data(filename, text)
110  except Exception as ex:
111  logger.info('Generator failure', exc_info=True)
112  die(ex)
113 
114 def write_generated_data(filename, data):
115  dirname = os.path.dirname(filename)
116  makedirs(dirname)
117 
118  # Lazy update - file will not be rewritten if its content is not going to change
119  if os.path.exists(filename):
120  with open(filename) as f:
121  existing_data = f.read()
122  if data == existing_data:
123  logger.info('Up to date [%s]', pretty_filename(filename))
124  return
125  logger.info('Rewriting [%s]', pretty_filename(filename))
126  os.remove(filename)
127  else:
128  logger.info('Creating [%s]', pretty_filename(filename))
129 
130  # Full rewrite
131  with open(filename, 'w') as f:
132  f.write(data)
133  try:
134  os.chmod(filename, OUTPUT_FILE_PERMISSIONS)
135  except (OSError, IOError) as ex:
136  logger.warning('Failed to set permissions for %s: %s', pretty_filename(filename), ex)
137 
139  if t.category == t.CATEGORY_PRIMITIVE:
140  cast_mode = {
141  t.CAST_MODE_SATURATED: '::uavcan::CastModeSaturate',
142  t.CAST_MODE_TRUNCATED: '::uavcan::CastModeTruncate',
143  }[t.cast_mode]
144  if t.kind == t.KIND_FLOAT:
145  return '::uavcan::FloatSpec< %d, %s >' % (t.bitlen, cast_mode)
146  else:
147  signedness = {
148  t.KIND_BOOLEAN: '::uavcan::SignednessUnsigned',
149  t.KIND_UNSIGNED_INT: '::uavcan::SignednessUnsigned',
150  t.KIND_SIGNED_INT: '::uavcan::SignednessSigned',
151  }[t.kind]
152  return '::uavcan::IntegerSpec< %d, %s, %s >' % (t.bitlen, signedness, cast_mode)
153  elif t.category == t.CATEGORY_ARRAY:
154  value_type = type_to_cpp_type(t.value_type)
155  mode = {
156  t.MODE_STATIC: '::uavcan::ArrayModeStatic',
157  t.MODE_DYNAMIC: '::uavcan::ArrayModeDynamic',
158  }[t.mode]
159  return '::uavcan::Array< %s, %s, %d >' % (value_type, mode, t.max_size)
160  elif t.category == t.CATEGORY_COMPOUND:
161  return '::' + t.full_name.replace('.', '::')
162  elif t.category == t.CATEGORY_VOID:
163  return '::uavcan::IntegerSpec< %d, ::uavcan::SignednessUnsigned, ::uavcan::CastModeSaturate >' % t.bitlen
164  else:
165  raise DsdlCompilerException('Unknown type category: %s' % t.category)
166 
167 def generate_one_type(template_expander, t):
168  t.short_name = t.full_name.split('.')[-1]
169  t.cpp_type_name = t.short_name + '_'
170  t.cpp_full_type_name = '::' + t.full_name.replace('.', '::')
171  t.include_guard = t.full_name.replace('.', '_').upper() + '_HPP_INCLUDED'
172 
173  # Dependencies (no duplicates)
174  def fields_includes(fields):
175  def detect_include(t):
176  if t.category == t.CATEGORY_COMPOUND:
177  return type_output_filename(t)
178  if t.category == t.CATEGORY_ARRAY:
179  return detect_include(t.value_type)
180  return list(sorted(set(filter(None, [detect_include(x.type) for x in fields]))))
181 
182  if t.kind == t.KIND_MESSAGE:
183  t.cpp_includes = fields_includes(t.fields)
184  else:
185  t.cpp_includes = fields_includes(t.request_fields + t.response_fields)
186 
187  t.cpp_namespace_components = t.full_name.split('.')[:-1]
188  t.has_default_dtid = t.default_dtid is not None
189 
190  # Attribute types
191  def inject_cpp_types(attributes):
192  void_index = 0
193  for a in attributes:
194  a.cpp_type = type_to_cpp_type(a.type)
195  a.void = a.type.category == a.type.CATEGORY_VOID
196  if a.void:
197  assert not a.name
198  a.name = '_void_%d' % void_index
199  void_index += 1
200 
201  if t.kind == t.KIND_MESSAGE:
202  inject_cpp_types(t.fields)
203  inject_cpp_types(t.constants)
204  t.all_attributes = t.fields + t.constants
205  t.union = t.union and len(t.fields)
206  else:
207  inject_cpp_types(t.request_fields)
208  inject_cpp_types(t.request_constants)
209  inject_cpp_types(t.response_fields)
210  inject_cpp_types(t.response_constants)
211  t.all_attributes = t.request_fields + t.request_constants + t.response_fields + t.response_constants
212  t.request_union = t.request_union and len(t.request_fields)
213  t.response_union = t.response_union and len(t.response_fields)
214 
215  # Constant properties
216  def inject_constant_info(constants):
217  for c in constants:
218  if c.type.kind == c.type.KIND_FLOAT:
219  float(c.string_value) # Making sure that this is a valid float literal
220  c.cpp_value = c.string_value
221  else:
222  int(c.string_value) # Making sure that this is a valid integer literal
223  c.cpp_value = c.string_value
224  if c.type.kind == c.type.KIND_UNSIGNED_INT:
225  c.cpp_value += 'U'
226 
227  if t.kind == t.KIND_MESSAGE:
228  inject_constant_info(t.constants)
229  else:
230  inject_constant_info(t.request_constants)
231  inject_constant_info(t.response_constants)
232 
233  # Data type kind
234  t.cpp_kind = {
235  t.KIND_MESSAGE: '::uavcan::DataTypeKindMessage',
236  t.KIND_SERVICE: '::uavcan::DataTypeKindService',
237  }[t.kind]
238 
239  # Generation
240  text = template_expander(t=t) # t for Type
241  text = '\n'.join(x.rstrip() for x in text.splitlines())
242  text = text.replace('\n\n\n\n\n', '\n\n').replace('\n\n\n\n', '\n\n').replace('\n\n\n', '\n\n')
243  text = text.replace('{\n\n ', '{\n ')
244  return text
245 
247  '''
248  Templating is based on pyratemp (http://www.simple-is-better.org/template/pyratemp.html).
249  The pyratemp's syntax is rather verbose and not so human friendly, so we define some
250  custom extensions to make it easier to read and write.
251  The resulting syntax somewhat resembles Mako (which was used earlier instead of pyratemp):
252  Substitution:
253  ${expression}
254  Line joining through backslash (replaced with a single space):
255  ${foo(bar(very_long_arument=42, \
256  second_line=72))}
257  Blocks:
258  % for a in range(10):
259  % if a == 5:
260  ${foo()}
261  % endif
262  % endfor
263  The extended syntax is converted into pyratemp's through regexp substitution.
264  '''
265  with open(filename) as f:
266  template_text = f.read()
267 
268  # Backslash-newline elimination
269  template_text = re.sub(r'\\\r{0,1}\n\ *', r' ', template_text)
270 
271  # Substitution syntax transformation: ${foo} ==> $!foo!$
272  template_text = re.sub(r'([^\$]{0,1})\$\{([^\}]+)\}', r'\1$!\2!$', template_text)
273 
274  # Flow control expression transformation: % foo: ==> <!--(foo)-->
275  template_text = re.sub(r'(?m)^(\ *)\%\ *(.+?):{0,1}$', r'\1<!--(\2)-->', template_text)
276 
277  # Block termination transformation: <!--(endfoo)--> ==> <!--(end)-->
278  template_text = re.sub(r'<\!--\(end[a-z]+\)-->', r'<!--(end)-->', template_text)
279 
280  # Pyratemp workaround.
281  # The problem is that if there's no empty line after a macro declaration, first line will be doubly indented.
282  # Workaround:
283  # 1. Remove trailing comments
284  # 2. Add a newline after each macro declaration
285  template_text = re.sub(r'\ *\#\!.*', '', template_text)
286  template_text = re.sub(r'(<\!--\(macro\ [a-zA-Z0-9_]+\)-->.*?)', r'\1\n', template_text)
287 
288  # Preprocessed text output for debugging
289 # with open(filename + '.d', 'w') as f:
290 # f.write(template_text)
291 
292  template = Template(template_text)
293 
294  def expand(**args):
295  # This function adds one indentation level (4 spaces); it will be used from the template
296  args['indent'] = lambda text, idnt = ' ': idnt + text.replace('\n', '\n' + idnt)
297  # This function works like enumerate(), telling you whether the current item is the last one
298  def enum_last_value(iterable, start=0):
299  it = iter(iterable)
300  count = start
301  try:
302  last = next(it)
303  except StopIteration:
304  return
305  for val in it:
306  yield count, False, last
307  last = val
308  count += 1
309  yield count, True, last
310  args['enum_last_value'] = enum_last_value
311  return template(**args)
312 
313  return expand
libuavcan_dsdl_compiler.pyratemp.Template
Definition: pyratemp.py:1149
libuavcan_dsdl_compiler.str
str
Definition: libuavcan_dsdl_compiler/__init__.py:22
libuavcan_dsdl_compiler.DsdlCompilerException
Definition: libuavcan_dsdl_compiler/__init__.py:32
libuavcan_dsdl_compiler.run
def run(source_dirs, include_dirs, output_dir)
Definition: libuavcan_dsdl_compiler/__init__.py:37
libuavcan_dsdl_compiler.type_output_filename
def type_output_filename(t)
Definition: libuavcan_dsdl_compiler/__init__.py:75
libuavcan_dsdl_compiler.die
def die(text)
Definition: libuavcan_dsdl_compiler/__init__.py:89
libuavcan_dsdl_compiler.write_generated_data
def write_generated_data(filename, data)
Definition: libuavcan_dsdl_compiler/__init__.py:114
libuavcan_dsdl_compiler.makedirs
def makedirs(path)
Definition: libuavcan_dsdl_compiler/__init__.py:79
libuavcan_dsdl_compiler.run_parser
def run_parser(source_dirs, search_dirs)
Definition: libuavcan_dsdl_compiler/__init__.py:92
int
int
Definition: libstubs.cpp:120
libuavcan_dsdl_compiler.type_to_cpp_type
def type_to_cpp_type(t)
Definition: libuavcan_dsdl_compiler/__init__.py:138
libuavcan_dsdl_compiler.run_generator
def run_generator(types, dest_dir)
Definition: libuavcan_dsdl_compiler/__init__.py:100
libuavcan_dsdl_compiler.pretty_filename
def pretty_filename(filename)
Definition: libuavcan_dsdl_compiler/__init__.py:67
libuavcan_dsdl_compiler.make_template_expander
def make_template_expander(filename)
Definition: libuavcan_dsdl_compiler/__init__.py:246
libuavcan_dsdl_compiler.generate_one_type
def generate_one_type(template_expander, t)
Definition: libuavcan_dsdl_compiler/__init__.py:167


uavcan_communicator
Author(s):
autogenerated on Fri Dec 13 2024 03:10:02