1# Copyright 2017 Steven Watanabe
2#
3# Distributed under the Boost Software License, Version 1.0.
4# (See accompanying file LICENSE_1_0.txt or copy at
5# http://www.boost.org/LICENSE_1_0.txt)
6
7from __future__ import print_function
8
9import sys
10import os
11import re
12
13# Represents a sequence of arguments that must appear
14# in a fixed order.
15class ordered:
16    def __init__(self, *args):
17        self.args = args
18    def match(self, command_line, pos, outputs):
19        for p in self.args:
20            res = try_match(command_line, pos, p, outputs)
21            if res is None:
22                return
23            pos = res
24        return pos
25
26# Represents a sequence of arguments that can appear
27# in any order.
28class unordered:
29    def __init__(self, *args):
30        self.args = list(args)
31    def match(self, command_line, pos, outputs):
32        unmatched = self.args[:]
33        while len(unmatched) > 0:
34            res = try_match_one(command_line, pos, unmatched, outputs)
35            if res is None:
36                return
37            pos = res
38        return pos
39
40# Represents a single input file.
41# If id is set, then the file must have been created
42# by a prior use of output_file.
43# If source is set, then the file must be that source file.
44class input_file:
45    def __init__(self, id=None, source=None):
46        assert((id is None) ^ (source is None))
47        self.id = id
48        self.source = source
49    def check(self, path):
50        if path.startswith("-"):
51            return
52        if self.id is not None:
53            try:
54                with open(path, "r") as f:
55                    data = f.read()
56                    if data == make_file_contents(self.id):
57                        return True
58                    else:
59                        return
60            except:
61                return
62        elif self.source is not None:
63            if self.source == path:
64                return True
65            else:
66                return
67        assert(False)
68    def match(self, command_line, pos, outputs):
69        if self.check(command_line[pos]):
70            return pos + 1
71
72# Matches an output file.
73# If the full pattern is matched, The
74# file will be created.
75class output_file:
76    def __init__(self, id):
77        self.id = id
78    def match(self, command_line, pos, outputs):
79        if command_line[pos].startswith("-"):
80            return
81        outputs.append((command_line[pos], self.id))
82        return pos + 1
83
84# Matches the directory containing an input_file
85class target_path(object):
86    def __init__(self, id):
87        self.tester = input_file(id=id)
88    def match(self, command_line, pos, outputs):
89        arg = command_line[pos]
90        if arg.startswith("-"):
91            return
92        try:
93            for path in os.listdir(arg):
94                if self.tester.check(os.path.join(arg, path)):
95                    return pos + 1
96        except:
97            return
98
99# Matches a single argument, which is composed of a prefix and a path
100# for example arguments of the form -ofilename.
101class arg(object):
102    def __init__(self, prefix, a):
103        # The prefix should be a string, a should be target_path or input_file.
104        self.prefix = prefix
105        self.a = a
106    def match(self, command_line, pos, outputs):
107        s = command_line[pos]
108        if s.startswith(self.prefix) and try_match([s[len(self.prefix):]], 0, self.a, outputs) == 1:
109            return pos + 1
110
111# Given a file id, returns a string that will be
112# written to the file to allow it to be recognized.
113def make_file_contents(id):
114    return id
115
116# Matches a single pattern from a list.
117# If it succeeds, the matching pattern
118# is removed from the list.
119# Returns the index after the end of the match
120def try_match_one(command_line, pos, patterns, outputs):
121    for p in patterns:
122        tmp = outputs[:]
123        res = try_match(command_line, pos, p, tmp)
124        if res is not None:
125            outputs[:] = tmp
126            patterns.remove(p)
127            return res
128
129# returns the end of the match if any
130def try_match(command_line, pos, pattern, outputs):
131    if pos == len(command_line):
132        return
133    elif type(pattern) is str:
134        if pattern == command_line[pos]:
135            return pos + 1
136    else:
137        return pattern.match(command_line, pos, outputs)
138
139known_patterns = []
140program_name = None
141
142# Registers a command
143# The arguments should be a sequence of:
144# str, ordered, unordered, arg, input_file, output_file, target_path
145# kwarg: stdout is text that will be printed on success.
146def command(*args, **kwargs):
147    global known_patterns
148    global program_name
149    stdout = kwargs.get("stdout", None)
150    pattern = ordered(*args)
151    known_patterns += [(pattern, stdout)]
152    if program_name is None:
153        program_name = args[0]
154    else:
155        assert(program_name == args[0])
156
157# Use this to filter the recognized commands, based on the properties
158# passed to b2.
159def allow_properties(*args):
160    try:
161        return all(a in os.environ["B2_PROPERTIES"].split(" ") for a in args)
162    except KeyError:
163        return True
164
165# Use this in the stdout argument of command to print the command
166# for running another script.
167def script(name):
168    return os.path.join(os.path.dirname(__file__), "bin", re.sub('\.py$', '', name))
169
170def match(command_line):
171    for (p, stdout) in known_patterns:
172        outputs = []
173        if try_match(command_line, 0, p, outputs) == len(command_line):
174            return (stdout, outputs)
175
176# Every mock program should call this after setting up all the commands.
177def main():
178    command_line = [program_name] + sys.argv[1:]
179    result = match(command_line)
180    if result is not None:
181        (stdout, outputs) = result
182        if stdout is not None:
183            print(stdout)
184        for (file,id) in outputs:
185            with open(file, "w") as f:
186                f.write(make_file_contents(id))
187        exit(0)
188    else:
189        print(command_line)
190        exit(1)
191
192# file should be the name of a file in the same directory
193# as this.  Must be called after verify_setup
194def verify_file(filename):
195    global known_files
196    if filename not in known_files:
197        known_files.add(filename)
198        srcdir = os.path.dirname(__file__)
199        execfile(os.path.join(srcdir, filename), {})
200
201def verify_setup():
202    """Override the behavior of most module components
203    in order to detect whether they are being used correctly."""
204    global main
205    global allow_properties
206    global output_file
207    global input_file
208    global target_path
209    global script
210    global command
211    global verify_errors
212    global output_ids
213    global input_ids
214    global known_files
215    def allow_properties(*args):
216        return True
217    def main():
218        pass
219    def output_file(id):
220        global output_ids
221        global verify_error
222        if id in output_ids:
223            verify_error("duplicate output_file: %s" % id)
224        output_ids.add(id)
225    def input_file(id=None, source=None):
226        if id is not None:
227            input_ids.add(id)
228    def target_path(id):
229        input_ids.add(id)
230    def script(filename):
231        verify_file(filename)
232    def command(*args, **kwargs):
233        pass
234    verify_errors = []
235    output_ids = set()
236    input_ids = set()
237    known_files = set()
238
239def verify_error(message):
240    global verify_errors
241    verify_errors += [message]
242
243def verify_finalize():
244    for id in input_ids:
245        if not id in output_ids:
246            verify_error("Input file does not exist: %s" % id)
247    for error in verify_errors:
248        print("error: %s" % error)
249    if len(verify_errors) != 0:
250        return 1
251    else:
252        return 0
253
254def verify():
255    srcdir = os.path.dirname(__file__)
256    if srcdir == '':
257        srcdir = '.'
258    verify_setup()
259    for f in os.listdir(srcdir):
260        if re.match(r"(gcc|clang|darwin|intel)-.*\.py", f):
261            verify_file(f)
262    exit(verify_finalize())
263