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