parse_dsc.py revision 0529e5d033099cbfc42635f6f6183833b09dff6e
1#!/usr/bin/env python
2# Copyright (c) 2013 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import collections
7import fnmatch
8import optparse
9import os
10import sys
11
12VALID_TOOLCHAINS = [
13  'bionic',
14  'newlib',
15  'glibc',
16  'pnacl',
17  'win',
18  'linux',
19  'mac',
20]
21
22# 'KEY' : ( <TYPE>, [Accepted Values], <Required?>)
23DSC_FORMAT = {
24    'DISABLE': (bool, [True, False], False),
25    'SEL_LDR': (bool, [True, False], False),
26    # Disable this project from being included in the NaCl packaged app.
27    'DISABLE_PACKAGE': (bool, [True, False], False),
28    # Don't generate the additional files to allow this project to run as a
29    # packaged app (i.e. manifest.json, background.js, etc.).
30    'NO_PACKAGE_FILES': (bool, [True, False], False),
31    'TOOLS' : (list, VALID_TOOLCHAINS, True),
32    'CONFIGS' : (list, ['Debug', 'Release'], False),
33    'PREREQ' : (list, '', False),
34    'TARGETS' : (list, {
35        'NAME': (str, '', True),
36        # main = nexe target
37        # lib = library target
38        # so = shared object target, automatically added to NMF
39        # so-standalone =  shared object target, not put into NMF
40        'TYPE': (str, ['main', 'lib', 'static-lib', 'so', 'so-standalone'],
41                 True),
42        'SOURCES': (list, '', True),
43        'CFLAGS': (list, '', False),
44        'CFLAGS_GCC': (list, '', False),
45        'CXXFLAGS': (list, '', False),
46        'DEFINES': (list, '', False),
47        'LDFLAGS': (list, '', False),
48        'INCLUDES': (list, '', False),
49        'LIBS' : (dict, VALID_TOOLCHAINS, False),
50        'DEPS' : (list, '', False)
51    }, False),
52    'HEADERS': (list, {
53        'FILES': (list, '', True),
54        'DEST': (str, '', True),
55    }, False),
56    'SEARCH': (list, '', False),
57    'POST': (str, '', False),
58    'PRE': (str, '', False),
59    'DEST': (str, ['getting_started', 'examples/api',
60                   'examples/demo', 'examples/tutorial',
61                   'src', 'tests'], True),
62    'NAME': (str, '', False),
63    'DATA': (list, '', False),
64    'TITLE': (str, '', False),
65    'GROUP': (str, '', False),
66    'EXPERIMENTAL': (bool, [True, False], False),
67    'PERMISSIONS': (list, '', False),
68    'SOCKET_PERMISSIONS': (list, '', False),
69    'MULTI_PLATFORM': (bool, [True, False], False),
70}
71
72
73class ValidationError(Exception):
74  pass
75
76
77def ValidateFormat(src, dsc_format):
78  # Verify all required keys are there
79  for key in dsc_format:
80    exp_type, exp_value, required = dsc_format[key]
81    if required and key not in src:
82      raise ValidationError('Missing required key %s.' % key)
83
84  # For each provided key, verify it's valid
85  for key in src:
86    # Verify the key is known
87    if key not in dsc_format:
88      raise ValidationError('Unexpected key %s.' % key)
89
90    exp_type, exp_value, required = dsc_format[key]
91    value = src[key]
92
93    # Verify the value is non-empty if required
94    if required and not value:
95      raise ValidationError('Expected non-empty value for %s.' % key)
96
97    # If the expected type is a dict, but the provided type is a list
98    # then the list applies to all keys of the dictionary, so we reset
99    # the expected type and value.
100    if exp_type is dict:
101      if type(value) is list:
102        exp_type = list
103        exp_value = ''
104
105    # Verify the key is of the expected type
106    if exp_type != type(value):
107      raise ValidationError('Key %s expects %s not %s.' % (
108          key, exp_type.__name__.upper(), type(value).__name__.upper()))
109
110    # If it's a bool, the expected values are always True or False.
111    if exp_type is bool:
112      continue
113
114    # If it's a string and there are expected values, make sure it matches
115    if exp_type is str:
116      if type(exp_value) is list and exp_value:
117        if value not in exp_value:
118          raise ValidationError("Value '%s' not expected for %s." %
119                                (value, key))
120      continue
121
122    # if it's a list, then we need to validate the values
123    if exp_type is list:
124      # If we expect a dictionary, then call this recursively
125      if type(exp_value) is dict:
126        for val in value:
127          ValidateFormat(val, exp_value)
128        continue
129      # If we expect a list of strings
130      if type(exp_value) is str:
131        for val in value:
132          if type(val) is not str:
133            raise ValidationError('Value %s in %s is not a string.' %
134                                  (val, key))
135        continue
136      # if we expect a particular string
137      if type(exp_value) is list:
138        for val in value:
139          if val not in exp_value:
140            raise ValidationError('Value %s not expected in %s.' %
141                                  (val, key))
142        continue
143
144    # if we are expecting a dict, verify the keys are allowed
145    if exp_type is dict:
146      print "Expecting dict\n"
147      for sub in value:
148        if sub not in exp_value:
149          raise ValidationError('Sub key %s not expected in %s.' %
150                                (sub, key))
151      continue
152
153    # If we got this far, it's an unexpected type
154    raise ValidationError('Unexpected type %s for key %s.' %
155                          (str(type(src[key])), key))
156
157
158def LoadProject(filename):
159  with open(filename, 'r') as descfile:
160    try:
161      desc = eval(descfile.read(), {}, {})
162    except Exception as e:
163      raise ValidationError(e)
164  if desc.get('DISABLE', False):
165    return None
166  ValidateFormat(desc, DSC_FORMAT)
167  desc['FILEPATH'] = os.path.abspath(filename)
168  return desc
169
170
171def LoadProjectTreeUnfiltered(srcpath):
172  # Build the tree
173  out = collections.defaultdict(list)
174  for root, _, files in os.walk(srcpath):
175    for filename in files:
176      if fnmatch.fnmatch(filename, '*.dsc'):
177        filepath = os.path.join(root, filename)
178        try:
179          desc = LoadProject(filepath)
180        except ValidationError as e:
181          raise ValidationError("Failed to validate: %s: %s" % (filepath, e))
182        if desc:
183          key = desc['DEST']
184          out[key].append(desc)
185  return out
186
187
188def LoadProjectTree(srcpath, include, exclude=None):
189  out = LoadProjectTreeUnfiltered(srcpath)
190  return FilterTree(out, MakeDefaultFilterFn(include, exclude))
191
192
193def GenerateProjects(tree):
194  for key in tree:
195    for val in tree[key]:
196      yield key, val
197
198
199def FilterTree(tree, filter_fn):
200  out = collections.defaultdict(list)
201  for branch, desc in GenerateProjects(tree):
202    if filter_fn(desc):
203      out[branch].append(desc)
204  return out
205
206
207def MakeDefaultFilterFn(include, exclude):
208  def DefaultFilterFn(desc):
209    matches_include = not include or DescMatchesFilter(desc, include)
210    matches_exclude = exclude and DescMatchesFilter(desc, exclude)
211
212    # Exclude list overrides include list.
213    if matches_exclude:
214      return False
215    return matches_include
216
217  return DefaultFilterFn
218
219
220def DescMatchesFilter(desc, filters):
221  for key, expected in filters.iteritems():
222    # For any filtered key which is unspecified, assumed False
223    value = desc.get(key, False)
224
225    # If we provide an expected list, match at least one
226    if type(expected) != list:
227      expected = set([expected])
228    if type(value) != list:
229      value = set([value])
230
231    if not set(expected) & set(value):
232      return False
233
234  # If we fall through, then we matched the filters
235  return True
236
237
238def PrintProjectTree(tree):
239  for key in tree:
240    print key + ':'
241    for val in tree[key]:
242      print '\t' + val['NAME']
243
244
245def main(argv):
246  parser = optparse.OptionParser(usage='%prog [options] <dir>')
247  parser.add_option('-e', '--experimental',
248      help='build experimental examples and libraries', action='store_true')
249  parser.add_option('-t', '--toolchain',
250      help='Build using toolchain. Can be passed more than once.',
251      action='append')
252
253  options, args = parser.parse_args(argv[1:])
254  filters = {}
255
256  load_from_dir = '.'
257  if len(args) > 1:
258    parser.error('Expected 0 or 1 args, got %d.' % len(args))
259
260  if args:
261    load_from_dir = args[0]
262
263  if options.toolchain:
264    filters['TOOLS'] = options.toolchain
265
266  if not options.experimental:
267    filters['EXPERIMENTAL'] = False
268
269  try:
270    tree = LoadProjectTree(load_from_dir, include=filters)
271  except ValidationError as e:
272    sys.stderr.write(str(e) + '\n')
273    return 1
274
275  PrintProjectTree(tree)
276  return 0
277
278
279if __name__ == '__main__':
280  sys.exit(main(sys.argv))
281