1#!/usr/bin/python
2##
3## license:BSD-3-Clause
4## copyright-holders:Vas Crabb
5
6import argparse
7import io
8import os.path
9import sys
10
11
12class ParserBase(object):
13    def process_lines(self, f):
14        self.input_line = 1
15        for line in f:
16            pos = 0
17            start = 0
18            if line.endswith('\n'):
19                line = line[:-1]
20            used = 0
21            while used is not None:
22                start += used
23                used = self.processors[self.parse_state](line[start:])
24            self.input_line += 1
25
26
27class CppParser(ParserBase):
28    TOKEN_LEAD = frozenset(
29            [chr(x) for x in range(ord('A'), ord('Z') + 1)] +
30            [chr(x) for x in range(ord('a'), ord('z') + 1)] +
31            ['_'])
32    TOKEN_CONTINUATION = frozenset(
33            [chr(x) for x in range(ord('0'), ord('9') + 1)] +
34            [chr(x) for x in range(ord('A'), ord('Z') + 1)] +
35            [chr(x) for x in range(ord('a'), ord('z') + 1)] +
36            ['_'])
37    HEXADECIMAL_DIGIT = frozenset(
38            [chr(x) for x in range(ord('0'), ord('9') + 1)] +
39            [chr(x) for x in range(ord('A'), ord('F') + 1)] +
40            [chr(x) for x in range(ord('a'), ord('f') + 1)])
41
42    class Handler(object):
43        def line(self, text):
44            pass
45
46        def comment(self, text):
47            pass
48
49        def line_comment(self, text):
50            pass
51
52    class ParseState(object):
53        DEFAULT = 0
54        COMMENT = 1
55        LINE_COMMENT = 2
56        TOKEN = 3
57        STRING_CONSTANT = 4
58        CHARACTER_CONSTANT = 5
59        NUMERIC_CONSTANT = 6
60
61    def __init__(self, handler, **kwargs):
62        super(CppParser, self).__init__(**kwargs)
63        self.handler = handler
64        self.processors = {
65                self.ParseState.DEFAULT: self.process_default,
66                self.ParseState.COMMENT: self.process_comment,
67                self.ParseState.LINE_COMMENT: self.process_line_comment,
68                self.ParseState.TOKEN: self.process_token,
69                self.ParseState.STRING_CONSTANT: self.process_text,
70                self.ParseState.CHARACTER_CONSTANT: self.process_text,
71                self.ParseState.NUMERIC_CONSTANT: self.process_numeric }
72
73    def parse(self, f):
74        self.parse_state = self.ParseState.DEFAULT
75        self.comment_line = None
76        self.lead_digit = None
77        self.radix = None
78        self.line_buffer = ''
79        self.comment_buffer = ''
80        self.process_lines(f)
81        if self.parse_state == self.ParseState.COMMENT:
82            raise Exception('unterminated multi-line comment beginning on line %d' % (self.comment_line, ))
83        elif self.parse_state == self.ParseState.CHARACTER_CONSTANT:
84            raise Exception('unterminated character literal on line %d' % (self.input_line, ))
85        elif self.parse_state == self.ParseState.STRING_CONSTANT:
86            raise Exception('unterminated string literal on line %d' % (self.input_line, ))
87
88    def process_default(self, line):
89        escape = False
90        pos = 0
91        length = len(line)
92        while pos < length:
93            ch = line[pos]
94            if (ch == '"') or (ch == "'"):
95                self.parse_state = self.ParseState.STRING_CONSTANT if ch == '"' else self.ParseState.CHARACTER_CONSTANT
96                self.line_buffer += line[:pos + 1]
97                return pos + 1
98            elif ch == '*':
99                if escape:
100                    self.parse_state = self.ParseState.COMMENT
101                    self.comment_line = self.input_line
102                    self.line_buffer += line[:pos - 1] + ' '
103                    return pos + 1
104            elif ch == '/':
105                if escape:
106                    self.parse_state = self.ParseState.LINE_COMMENT
107                    self.handler.line(self.line_buffer + line[:pos - 1] + ' ')
108                    self.line_buffer = ''
109                    return pos + 1
110            elif ch in self.TOKEN_LEAD:
111                self.parse_state = self.ParseState.TOKEN
112                self.line_buffer += line[:pos]
113                return pos
114            elif (ch >= '0') and (ch <= '9'):
115                self.parse_state = self.ParseState.NUMERIC_CONSTANT
116                self.line_buffer += line[:pos]
117                return pos
118            escape = ch == '/'
119            pos += 1
120        if line.endswith('\\'):
121            self.line_buffer += line[:-1]
122        else:
123            self.handler.line(self.line_buffer + line)
124            self.line_buffer = ''
125
126    def process_comment(self, line):
127        escape = False
128        pos = 0
129        length = len(line)
130        while pos < length:
131            ch = line[pos]
132            if escape and (ch == '/'):
133                self.parse_state = self.ParseState.DEFAULT
134                self.comment_line = None
135                self.handler.comment(self.comment_buffer + line[:pos - 1])
136                self.comment_buffer = ''
137                return pos + 1
138            escape = ch == '*'
139            pos += 1
140        if line.endswith('\\'):
141            self.comment_buffer += line[:-1]
142        else:
143            self.comment_buffer += line + '\n'
144
145    def process_line_comment(self, line):
146        self.parse_state = self.ParseState.DEFAULT
147        self.handler.line_comment(self.comment_buffer + line)
148        self.comment_buffer = ''
149
150    def process_token(self, line):
151        pos = 0
152        length = len(line)
153        while pos < length:
154            ch = line[pos]
155            if ch not in self.TOKEN_CONTINUATION:
156                self.parse_state = self.ParseState.DEFAULT
157                self.line_buffer += line[:pos]
158                return pos
159            pos += 1
160        self.parse_state = self.ParseState.DEFAULT
161        self.handler.line(self.line_buffer + line)
162        self.line_buffer = ''
163
164    def process_text(self, line):
165        quote = '"' if self.parse_state == self.ParseState.STRING_CONSTANT else "'"
166        escape = False
167        pos = 0
168        length = len(line)
169        while pos < length:
170            ch = line[pos]
171            if (ch == quote) and not escape:
172                self.parse_state = self.ParseState.DEFAULT
173                self.line_buffer += line[:pos + 1]
174                return pos + 1
175            escape = (ch == '\\') and not escape
176            pos += 1
177        if line.endswith('\\'):
178            self.line_buffer += line[:-1]
179        else:
180            t = 'string' if self.ParseState == self.ParseState.STRING_CONSTANT else 'character'
181            raise Exception('unterminated %s literal on line %d' % (t, self.input_line))
182
183    def process_numeric(self, line):
184        escape = False
185        pos = 0
186        length = len(line)
187        while pos < length:
188            ch = line[pos]
189            if self.lead_digit is None:
190                self.lead_digit = ch
191                if ch != '0':
192                    self.radix = 10
193            elif self.radix is None:
194                if ch == "'":
195                    if escape:
196                        raise Exception('adjacent digit separators on line %d' % (self.input_line, ))
197                    else:
198                        escape = True
199                elif (ch == 'B') or (ch == 'b'):
200                    self.radix = 2
201                elif (ch == 'X') or (ch == 'x'):
202                    self.radix = 16
203                elif (ch >= '0') and (ch <= '7'):
204                    self.radix = 8
205                else:
206                    self.parse_state = self.ParseState.DEFAULT # probably an argument to a token-pasting or stringifying macro
207            else:
208                if ch == "'":
209                    if escape:
210                        raise Exception('adjacent digit separators on line %d' % (self.input_line, ))
211                    else:
212                        escape = True
213                else:
214                    escape = False
215                    if self.radix == 2:
216                        if (ch < '0') or (ch > '1'):
217                            self.parse_state = self.ParseState.DEFAULT
218                    elif self.radix == 8:
219                        if (ch < '0') or (ch > '7'):
220                            self.parse_state = self.ParseState.DEFAULT
221                    elif self.radix == 10:
222                        if (ch < '0') or (ch > '9'):
223                            self.parse_state = self.ParseState.DEFAULT
224                    elif self.radix == 16:
225                        if ch not in self.HEXADECIMAL_DIGIT:
226                            self.parse_state = self.ParseState.DEFAULT
227            if self.parse_state == self.ParseState.DEFAULT:
228                self.lead_digit = None
229                self.radix = None
230                self.line_buffer += line[:pos]
231                return pos
232            pos += 1
233        self.parse_state = self.ParseState.DEFAULT
234        self.lead_digit = None
235        self.radix = None
236        self.handler.line(self.line_buffer + line)
237        self.line_buffer = ''
238
239
240class LuaParser(ParserBase):
241    class Handler(object):
242        def short_comment(self, text):
243            pass
244
245        def long_comment_start(self, level):
246            pass
247
248        def long_comment_line(self, text):
249            pass
250
251        def long_comment_end(self):
252            pass
253
254    class ParseState(object):
255        DEFAULT = 0
256        SHORT_COMMENT = 1
257        LONG_COMMENT = 2
258        STRING_CONSTANT = 3
259        LONG_STRING_CONSTANT = 4
260
261    def __init__(self, handler, **kwargs):
262        super(LuaParser, self).__init__(**kwargs)
263        self.handler = handler
264        self.processors = {
265                self.ParseState.DEFAULT: self.process_default,
266                self.ParseState.SHORT_COMMENT: self.process_short_comment,
267                self.ParseState.LONG_COMMENT: self.process_long_comment,
268                self.ParseState.STRING_CONSTANT: self.process_string_constant,
269                self.ParseState.LONG_STRING_CONSTANT: self.process_long_string_constant }
270
271    def parse(self, f):
272        self.parse_state = self.ParseState.DEFAULT
273        self.long_bracket_level = None
274        self.escape = False
275        self.block_line = None
276        self.block_level = None
277        self.string_quote = None
278        self.process_lines(f)
279        if self.parse_state == self.ParseState.LONG_COMMENT:
280            raise Exception('unterminated long comment beginning on line %d' % (self.block_line, ));
281        elif self.parse_state == self.ParseState.STRING_CONSTANT:
282            raise Exception('unterminated string literal on line %d' % (self.input_line, ));
283        elif self.parse_state == self.ParseState.LONG_STRING_CONSTANT:
284            raise Exception('unterminated long string literal beginning on line %d' % (self.block_line, ));
285
286    def process_default(self, line):
287        pos = 0
288        length = len(line)
289        while pos < length:
290            ch = line[pos]
291            if (ch == '"') or (ch == "'"):
292                self.string_quote = ch
293                self.parse_state = self.ParseState.STRING_CONSTANT
294                self.long_bracket_level = None
295                self.escape = False
296                return pos + 1;
297            elif (ch == '-') and self.escape:
298                self.parse_state = self.ParseState.SHORT_COMMENT
299                self.long_bracket_level = None
300                self.escape = False
301                return pos + 1
302            elif self.long_bracket_level is not None:
303                if ch == '=':
304                    self.long_bracket_level += 1
305                elif ch == '[':
306                    self.block_line = self.input_line
307                    self.block_level = self.long_bracket_level
308                    self.parse_state = self.ParseState.LONG_STRING_CONSTANT
309                    self.long_bracket_level = None
310                    self.escape = False
311                    return pos + 1
312                else:
313                    self.long_bracket_level = None
314            elif ch == '[':
315                self.long_bracket_level = 0
316            self.escape = ch == '-'
317            pos += 1
318        self.escape = False
319
320    def process_short_comment(self, line):
321        pos = 0
322        length = len(line)
323        while pos < length:
324            ch = line[pos]
325            if self.long_bracket_level is not None:
326                if ch == '=':
327                    self.long_bracket_level += 1
328                elif ch == '[':
329                    self.block_line = self.input_line
330                    self.block_level = self.long_bracket_level
331                    self.parse_state = self.ParseState.LONG_COMMENT
332                    self.long_bracket_level = None
333                    self.handler.long_comment_start(self.block_level)
334                    return pos + 1
335                else:
336                    self.long_bracket_level = None
337            elif ch == '[':
338                self.long_bracket_level = 0
339            if self.long_bracket_level is None:
340                self.handler.short_comment(line[pos:])
341                self.parse_state = self.ParseState.DEFAULT
342                return None
343            pos += 1
344        self.handler.short_comment(line)
345        self.parse_state = self.ParseState.DEFAULT
346
347    def process_long_comment(self, line):
348        pos = 0
349        length = len(line)
350        while pos < length:
351            ch = line[pos]
352            if self.long_bracket_level is not None:
353                if ch == '=':
354                    self.long_bracket_level += 1
355                elif ch == ']':
356                    if self.long_bracket_level == self.block_level:
357                        if self.parse_state == self.ParseState.LONG_COMMENT:
358                            self.handler.long_comment_line(line[:endpos])
359                            self.handler.long_comment_end()
360                        self.parse_state = self.ParseState.DEFAULT
361                        return pos + 1
362                    else:
363                        self.long_bracket_level = 0
364                else:
365                    self.long_bracket_level = None
366            elif ch == ']':
367                endpos = pos
368                self.long_bracket_level = 0
369            pos += 1
370        self.long_bracket_level = None
371        self.handler.long_comment_line(line)
372
373    def process_string_constant(self, line):
374        pos = 0
375        length = len(line)
376        while pos < length:
377            ch = line[pos]
378            if (ch == self.string_quote) and not self.escape:
379                self.parse_state = self.ParseState.DEFAULT
380                return pos + 1
381            self.escape = (ch == '\\') and not self.escape
382            pos += 1
383        if not self.escape:
384            raise Exception('unterminated string literal on line %d' % (self.input_line, ));
385
386    def process_long_string_constant(self, line):
387        self.process_long_comment(line) # this works because they're both closed by a matching long bracket
388
389
390class DriverFilter(object):
391    DRIVER_CHARS = frozenset(
392            [chr(x) for x in range(ord('0'), ord('9') + 1)] +
393            [chr(x) for x in range(ord('a'), ord('z') + 1)] +
394            ['_'])
395
396    def __init__(self, options, **kwargs):
397        super(DriverFilter, self).__init__(**kwargs)
398        self.parse_filter(options.filter)
399        self.parse_list(options.list)
400
401    def write_source(self, f):
402        f.write(
403                '#include "emu.h"\n' \
404                '\n' \
405                '#include "drivenum.h"\n' \
406                '\n')
407        for driver in self.drivers:
408            f.write('GAME_EXTERN(%s);\n' % driver)
409        f.write(
410                '\n' \
411                'game_driver const *const driver_list::s_drivers_sorted[%d] =\n' \
412                '{\n' % (len(self.drivers), ))
413        for driver in self.drivers:
414            f.write('\t&GAME_NAME(%s),\n' % driver)
415        f.write(
416                '};\n' \
417                '\n' \
418                'std::size_t const driver_list::s_driver_count = %d;\n' % (len(self.drivers), ))
419
420    def parse_filter(self, path):
421        def do_parse(p):
422            def line_hook(text):
423                text = text.strip()
424                if text.startswith('#'):
425                    do_parse(os.path.join(os.path.dirname(n), text[1:].lstrip()))
426                elif text.startswith('+'):
427                    text = text[1:].lstrip()
428                    if not text:
429                        sys.stderr.write('%s:%s: Empty driver name\n' % (p, parser.input_line, text))
430                        sys.exit(1)
431                    elif not all(x in self.DRIVER_CHARS for x in text):
432                        sys.stderr.write('%s:%s: Invalid character in driver name "%s"\n' % (p, parser.input_line, text))
433                        sys.exit(1)
434                    includes.add(text)
435                    excludes.discard(text)
436                elif text.startswith('-'):
437                    text = text[1:].lstrip()
438                    if not text:
439                        sys.stderr.write('%s:%s: Empty driver name\n' % (p, parser.input_line, text))
440                        sys.exit(1)
441                    elif not all(x in self.DRIVER_CHARS for x in text):
442                        sys.stderr.write('%s:%s: Invalid character in driver name "%s"\n' % (p, parser.input_line, text))
443                        sys.exit(1)
444                    includes.discard(text)
445                    excludes.add(text)
446                elif text:
447                    sources.add(text)
448
449            n = os.path.normpath(p)
450            if n not in filters:
451                filters.add(n)
452                try:
453                    f = io.open(n, 'r', encoding='utf-8')
454                except IOError:
455                    sys.stderr.write('Unable to open filter file "%s"\n' % (p, ))
456                    sys.exit(1)
457                with f:
458                    handler = CppParser.Handler()
459                    handler.line = line_hook
460                    parser = CppParser(handler)
461                    try:
462                        parser.parse(f)
463                    except IOError:
464                        sys.stderr.write('Error reading filter file "%s"\n' % (p, ))
465                        sys.exit(1)
466                    except Exception as e:
467                        sys.stderr.write('Error parsing filter file "%s": %s\n' % (p, e))
468                        sys.exit(1)
469
470        sources = set()
471        includes = set()
472        excludes = set()
473        filters = set()
474        if path is not None:
475            do_parse(path)
476            sys.stderr.write('%d source file(s) found\n' % (len(sources), ))
477        self.sources = frozenset(sources)
478        self.includes = frozenset(includes)
479        self.excludes = frozenset(excludes)
480
481    def parse_list(self, path):
482        def do_parse(p):
483            def line_hook(text):
484                text = text.strip()
485                if text.startswith('#'):
486                    do_parse(os.path.join(os.path.dirname(n), text[1:].lstrip()))
487                elif text.startswith('@'):
488                    parts = text[1:].lstrip().split(':', 1)
489                    parts[0] = parts[0].strip()
490                    if (parts[0] == 'source') and (len(parts) == 2):
491                        parts[1] = parts[1].strip()
492                        if not parts[1]:
493                            sys.stderr.write('%s:%s: Empty source file name "%s"\n' % (p, parser.input_line, text))
494                            sys.exit(1)
495                        elif self.sources:
496                            state['includesrc'] = parts[1] in self.sources
497                    else:
498                        sys.stderr.write('%s:%s: Unsupported directive "%s"\n' % (p, parser.input_line, text))
499                        sys.exit(1)
500                elif text:
501                    if not all(x in self.DRIVER_CHARS for x in text):
502                        sys.stderr.write('%s:%s: Invalid character in driver name "%s"\n' % (p, parser.input_line, text))
503                        sys.exit(1)
504                    elif state['includesrc'] and (text not in self.excludes):
505                        drivers.add(text)
506
507            n = os.path.normpath(p)
508            if n not in lists:
509                lists.add(n)
510                try:
511                    f = io.open(n, 'r', encoding='utf-8')
512                except IOError:
513                    sys.stderr.write('Unable to open list file "%s"\n' % (p, ))
514                    sys.exit(1)
515                with f:
516                    handler = CppParser.Handler()
517                    handler.line = line_hook
518                    parser = CppParser(handler)
519                    try:
520                        parser.parse(f)
521                    except IOError:
522                        sys.stderr.write('Error reading list file "%s"\n' % (p, ))
523                        sys.exit(1)
524                    except Exception as e:
525                        sys.stderr.write('Error parsing list file "%s": %s\n' % (p, e))
526                        sys.exit(1)
527
528        lists = set()
529        drivers = set()
530        state = object()
531        state = { 'includesrc': True }
532        do_parse(path)
533        for driver in self.includes:
534            drivers.add(driver)
535        sys.stderr.write('%d driver(s) found\n' % (len(drivers), ))
536        drivers.add('___empty')
537        self.drivers = sorted(drivers)
538
539
540def split_path(path):
541    path = os.path.normpath(path)
542    result = [ ]
543    while True:
544        dirname, basename = os.path.split(path)
545        if dirname == path:
546            result.insert(0, dirname)
547            return result
548        elif basename == path:
549            result.insert(0, basename)
550            return result
551        else:
552            result.insert(0, basename)
553            path = dirname
554
555
556def parse_command_line():
557    parser = argparse.ArgumentParser()
558    subparsers = parser.add_subparsers(title='commands', dest='command', metavar='<command>')
559
560    subparser = subparsers.add_parser('sourcesproject', help='generate project directives for source files')
561    subparser.add_argument('-r', '--root', metavar='<srcroot>', default='.', help='path to emulator source root (defaults to working directory)')
562    subparser.add_argument('-t', '--target', metavar='<target>', required=True, help='generated emulator target name')
563    subparser.add_argument('sources', metavar='<srcfile>', nargs='+', help='source files to include')
564
565    subparser = subparsers.add_parser('sourcesfilter', help='generate driver filter for source files')
566    subparser.add_argument('sources', metavar='<srcfile>', nargs='+', help='source files to include')
567
568    subparser = subparsers.add_parser('driverlist', help='generate driver list source')
569    subparser.add_argument('-f', '--filter', metavar='<fltfile>', help='input filter file')
570    subparser.add_argument('list', metavar='<lstfile>', help='input list file')
571
572    return parser.parse_args()
573
574
575def collect_lua_directives(options):
576    def short_comment_hook(text):
577        if text.startswith('@'):
578            name, action = text[1:].rstrip().rsplit(',', 1)
579            if name not in result:
580                result[name] = [ ]
581            result[name].append(action)
582
583    base = os.path.join(options.root, 'scripts', 'src')
584    result = { }
585    handler = LuaParser.Handler()
586    handler.short_comment = short_comment_hook
587    parser = LuaParser(handler)
588    for name in ('bus', 'cpu', 'machine', 'sound', 'video', 'formats'):
589        path = os.path.join(base, name + '.lua')
590        try:
591            f = io.open(path, 'r', encoding='utf-8')
592        except IOError:
593            sys.stderr.write('Unable to open source file "%s"\n' % (path, ))
594            sys.exit(1)
595        try:
596            with f:
597                parser.parse(f)
598        except IOError:
599            sys.stderr.write('Error reading source file "%s"\n' % (path, ))
600            sys.exit(1)
601        except Exception as e:
602            sys.stderr.write('Error parsing source file "%s": %s\n' % (path, e))
603            sys.exit(1)
604    return result
605
606
607def scan_source_dependencies(options):
608    def locate_include(path):
609        split = [ ]
610        forward = 0
611        reverse = 0
612        for part in path.split('/'):
613            if part and (part != '.'):
614                if part != '..':
615                    forward += 1
616                    split.append(part)
617                elif forward:
618                    split.pop()
619                    forward -= 1
620                else:
621                    split.append(part)
622                    reverse += 1
623        split = tuple(split)
624        for incdir, depth in roots:
625            if (not depth) or (not reverse):
626                components = incdir + split
627                depth = depth + forward - 1
628            elif depth >= reverse:
629                components = incdir[:-reverse] + split[reverse:]
630                depth = depth + forward - reverse - 1
631            else:
632                components = incdir[:-depth] + split[depth:]
633                depth = forward - 1
634            if os.path.isfile(os.path.join(options.root, *components)):
635                return components, depth
636        return None, 0
637
638    def test_siblings(relative, basename, depth):
639        pathbase = '/'.join(relative) + '/'
640        dirname = os.path.join(options.root, *relative)
641        for ext in ('.cpp', '.ipp', '.hxx'):
642            path = pathbase + basename + ext
643            if (path not in seen) and os.path.isfile(os.path.join(dirname, basename + ext)):
644                remaining.append((path, depth))
645                seen.add(path)
646
647    def line_hook(text):
648        text = text.lstrip()
649        if text.startswith('#'):
650            text = text[1:].lstrip()
651            if text.startswith('include'):
652                text = text[7:]
653                if text[:1].isspace():
654                    text = text.strip()
655                    if (len(text) > 2) and (text[0] == '"') and (text[-1] == '"'):
656                        components, depth = locate_include(text[1:-1])
657                        if components:
658                            path = '/'.join(components)
659                            if path not in seen:
660                                remaining.append((path, depth))
661                                seen.add(path)
662                                base, ext = os.path.splitext(components[-1])
663                                if ext.lower().startswith('.h'):
664                                    components = components[:-1]
665                                    test_siblings(components, base, depth)
666                                    if components == ('src', 'mame', 'includes'):
667                                        for aspect in ('audio', 'drivers', 'video', 'machine'):
668                                            test_siblings(('src', 'mame', aspect), base, depth)
669
670    handler = CppParser.Handler()
671    handler.line = line_hook
672    parser = CppParser(handler)
673    seen = set('/'.join(x for x in split_path(source) if x) for source in options.sources)
674    remaining = list([(x, 0) for x in seen])
675    default_roots = ((('src', 'devices'), 0), (('src', 'mame'), 0), (('src', 'lib'), 0))
676    while remaining:
677        source, depth = remaining.pop()
678        components = tuple(source.split('/'))
679        roots = ((components[:-1], depth), ) + default_roots
680        try:
681            f = io.open(os.path.join(options.root, *components), 'r', encoding='utf-8')
682        except IOError:
683            sys.stderr.write('Unable to open source file "%s"\n' % (source, ))
684            sys.exit(1)
685        try:
686            with f:
687                parser.parse(f)
688        except IOError:
689            sys.stderr.write('Error reading source file "%s"\n' % (source, ))
690            sys.exit(1)
691        except Exception as e:
692            sys.stderr.write('Error parsing source file "%s": %s\n' % (source, e))
693            sys.exit(1)
694    return seen
695
696
697def write_project(options, f, mappings, sources):
698    targetsrc = ''
699    for source in sorted(sources):
700        action = mappings.get(source)
701        if action:
702            for line in action:
703                f.write(line + '\n')
704        if source.startswith('src/mame/'):
705            targetsrc += '        MAME_DIR .. "%s",\n' % (source, )
706    f.write(
707            '\n' \
708            'function createProjects_mame_%s(_target, _subtarget)\n' \
709            '    project ("mame_%s")\n' \
710            '    targetsubdir(_target .."_" .. _subtarget)\n' \
711            '    kind (LIBTYPE)\n' \
712            '    uuid (os.uuid("drv-mame-%s"))\n' \
713            '    addprojectflags()\n' \
714            '    \n' \
715            '    includedirs {\n' \
716            '        MAME_DIR .. "src/osd",\n' \
717            '        MAME_DIR .. "src/emu",\n' \
718            '        MAME_DIR .. "src/devices",\n' \
719            '        MAME_DIR .. "src/mame",\n' \
720            '        MAME_DIR .. "src/lib",\n' \
721            '        MAME_DIR .. "src/lib/util",\n' \
722            '        MAME_DIR .. "src/lib/netlist",\n' \
723            '        MAME_DIR .. "3rdparty",\n' \
724            '        GEN_DIR  .. "mame/layout",\n' \
725            '        ext_includedir("flac"),\n' \
726            '        ext_includedir("glm"),\n' \
727            '        ext_includedir("jpeg"),\n' \
728            '        ext_includedir("rapidjson"),\n' \
729            '        ext_includedir("zlib"),\n' \
730            '    }\n' \
731            '\n' \
732            '    files{\n%s' \
733            '    }\n' \
734            'end\n' \
735            '\n' \
736            'function linkProjects_mame_%s(_target, _subtarget)\n' \
737            '    links {\n' \
738            '        "mame_%s",\n' \
739            '    }\n' \
740            'end\n' % (options.target, options.target, options.target, targetsrc, options.target, options.target))
741
742
743def write_filter(options, f):
744    drivers = set()
745    for source in options.sources:
746        components = tuple(x for x in split_path(source) if x)
747        if (len(components) > 3) and (components[:3] == ('src', 'mame', 'drivers')):
748            ext = os.path.splitext(components[-1])[1].lower()
749            if ext.startswith('.c'):
750                drivers.add('/'.join(components[3:]))
751    for driver in sorted(drivers):
752        f.write(driver + '\n')
753
754
755if __name__ == '__main__':
756    options = parse_command_line()
757    if options.command == 'sourcesproject':
758        header_to_optional = collect_lua_directives(options)
759        source_dependencies = scan_source_dependencies(options)
760        write_project(options, sys.stdout, header_to_optional, source_dependencies)
761    elif options.command == 'sourcesfilter':
762        write_filter(options, sys.stdout)
763    elif options.command == 'driverlist':
764        DriverFilter(options).write_source(sys.stdout)
765