Rewrite generate-xcfilelists in Python
[WebKit-https.git] / Tools / Scripts / webkitpy / generate_xcfilelists_lib / util.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # Copyright (C) 2019 Apple Inc.  All rights reserved.
5 #
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions
8 # are met:
9 #
10 # 1. Redistributions of source code must retain the above copyright
11 #    notice, this list of conditions and the following disclaimer.
12 # 2. Redistributions in binary form must reproduce the above copyright
13 #    notice, this list of conditions and the following disclaimer in the
14 #    documentation and/or other materials provided with the distribution.
15 #
16 # THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
17 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
21 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
22 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
23 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
24 # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
28 from __future__ import print_function
29
30 import argparse
31 import os
32 import subprocess
33 import sys
34 import traceback
35
36 # Gather information about our debugging environment right now. Do this before
37 # executing any "main" code so that our debugging preferences are in place by
38 # the time we get there and we can debug from main() on down as opposed to
39 # parse_args() on down.
40
41 SHOW_DEBUG_LOGGING = "-d" in sys.argv or "--debug" in sys.argv
42
43 if SHOW_DEBUG_LOGGING:
44
45     DEBUG_LOGGING_FILE = None
46     for index, arg in enumerate(sys.argv):
47         if arg == "--debug-file":
48             if index + 1 < len(sys.argv):
49                 DEBUG_LOGGING_FILE = sys.argv[index + 1]
50
51     # Bottleneck function for printing debugging lines to either the console or
52     # the screen, as appropriate.
53
54     if DEBUG_LOGGING_FILE:
55
56         def debug_log(msg):
57             with open(DEBUG_LOGGING_FILE, "a") as f:
58                 print(msg, file=f)
59     else:
60
61         def debug_log(msg):
62             print(msg)
63
64     # Context Manager class for logging information about function entry/exit.
65     # On entry, the function name is logged along with its parameters. On exit,
66     # the function name is logged with its result. If an exception occurs, the
67     # function name is logged with the exception. In all cases, the logging is
68     # indented according to the level of the function in the backtrace.
69     #
70     # Versions of these classes exist for instance methods, class methods, and
71     # global functions so that instance and class information can be extracted
72     # and displayed.
73
74     class LogEntryHelper(object):
75         __slots__ = ["indent", "class_name", "function_name", "type"]
76
77         def __init__(self, func, type):
78             tb = traceback.extract_stack()
79             self.indent = " " * 2 * (len(tb) - 3)
80             self.class_name = None
81             self.function_name = func.__name__
82             self.type = type
83
84         def log_entry(self, args, kwargs):
85             if self.type == "instance":
86                 self.class_name = args[0].__class__.__name__ + "."
87                 args = args[1:]
88             elif self.type == "class":
89                 self.class_name = args[0].__name__ + "."
90                 args = args[1:]
91             else:
92                 self.class_name = ""
93
94             self._print("args={}, kwargs={}".format(args, kwargs))
95
96         def log_result(self, result):
97             if hasattr(result, '__iter__') and not isinstance(result, str):
98                 for line in result:
99                     self._print("result={}".format(line))
100             else:
101                 self._print("result={}".format(result))
102
103         def log_exception(self, exc):
104             self._print("exception={}".format(exc))
105
106         def _print(self, msg):
107             debug_log("{}{}{}: {}".format(self.indent, self.class_name, self.function_name, msg))
108
109     def LogEntryExit(func):
110         def _show_debug_logging(*args, **kwargs):
111             helper = LogEntryHelper(func, "instance")
112             helper.log_entry(args, kwargs)
113             try:
114                 result = func(*args, **kwargs)
115                 helper.log_result(result)
116                 return result
117             except BaseException as e:
118                 helper.log_exception(e)
119                 raise
120         return _show_debug_logging
121
122     def LogEntryExitClass(func):
123         def _show_debug_logging(*args, **kwargs):
124             helper = LogEntryHelper(func, "class")
125             helper.log_entry(args, kwargs)
126             try:
127                 result = func(*args, **kwargs)
128                 helper.log_result(result)
129                 return result
130             except BaseException as e:
131                 helper.log_exception(e)
132                 raise
133         return _show_debug_logging
134
135     def LogEntryExitGlobal(func):
136         def _show_debug_logging(*args, **kwargs):
137             helper = LogEntryHelper(func, None)
138             helper.log_entry(args, kwargs)
139             try:
140                 result = func(*args, **kwargs)
141                 helper.log_result(result)
142                 return result
143             except BaseException as e:
144                 helper.log_exception(e)
145                 raise
146         return _show_debug_logging
147
148 else:
149
150     def debug_log(msg):
151         pass
152
153     def LogEntryExit(func):
154         return func
155
156     def LogEntryExitClass(func):
157         return func
158
159     def LogEntryExitGlobal(func):
160         return func
161
162
163 # Utility function for operating similar to subprocess.run() in Python 3. One
164 # difference is that the result is a 2-tuple with stdout and stderr, rather
165 # than a 3-tuple that includes returncode. For our purposes, if returncode is
166 # non-zero, we raise an exception.
167
168 @LogEntryExitGlobal
169 def subprocess_run(args, **kwargs):
170     kwargs["stdout"] = subprocess.PIPE
171     kwargs["stderr"] = subprocess.PIPE
172     input = None
173     if "input" in kwargs:
174         input = kwargs["input"]
175         del kwargs["input"]
176         kwargs["stdin"] = subprocess.PIPE
177     process = subprocess.Popen(args, **kwargs)
178     (stdout, stderr) = process.communicate(input=input)
179     stdout = stdout.decode() if isinstance(stdout, bytes) else stdout
180     stderr = stderr.decode() if isinstance(stderr, bytes) else stderr
181     if process.returncode:
182         raise CalledProcessError(process.returncode, args[0], stdout, stderr)
183     return (stdout, stderr)
184
185
186 # Utility function to allow us to verify that we're running under Xcode or not.
187 # For example, if we are not, then we need to make sure that we don't try to
188 # access Xcode-specific environment variables.
189
190 @LogEntryExitGlobal
191 def is_running_under_xcode():
192     return os.environ.get("XCODE_INSTALL_PATH")
193
194
195 # An argparse.Action subclass that validates the user-provided value against a
196 # list of valid values. Aliasing is supported; that is, the user can provide a
197 # value that can get mapped to a corresponding canonical value, and that
198 # resulting value is compared to the list of valid values.
199 #
200 # On error, calls parser.error().
201
202 class CheckValidItemAction(argparse.Action):
203     @LogEntryExit
204     def __init__(self, *args, **kwargs):
205         self.item_type = kwargs.get("item_type", None)
206         self.valid_items = kwargs.get("valid_items", None)
207         self.aliases = kwargs.get("aliases", None)
208
209         self.lowered_valid_items = [item.lower() for item in self.valid_items]
210
211         kwargs.pop("item_type", None)
212         kwargs.pop("valid_items", None)
213         kwargs.pop("aliases", None)
214
215         super(CheckValidItemAction, self).__init__(*args, **kwargs)
216
217     @LogEntryExit
218     def __call__(self, parser, namespace, values, option_string=None):
219         try:
220             validated = self.validate_item(values)
221         except:
222             parser.error("The {} \"{}\" is not supported.".format(self.item_type, values))
223         items = getattr(namespace, self.dest, None)
224         items = items[:] if items else []
225         items.append(validated)
226         setattr(namespace, self.dest, items)
227
228     @LogEntryExit
229     def validate_item(self, item):
230         item = item.lower()
231         try:
232             validated_index = self.lowered_valid_items.index(item)
233         except:
234             if not self.aliases:
235                 raise
236             item = self.aliases.get(item, None)
237             validated_index = self.lowered_valid_items.index(item)
238         return self.valid_items[validated_index]
239
240
241 # An argparse.Action subclass that validates the user-provided script command
242 # (generate, check, etc.)
243 #
244 # On error, calls parser.error().
245
246 class CheckCommandAction(argparse.Action):
247     @LogEntryExit
248     def __init__(self, *args, **kwargs):
249         self.valid_commands = kwargs.get("valid_commands", None)
250         kwargs.pop("valid_commands", None)
251         super(CheckCommandAction, self).__init__(*args, **kwargs)
252
253     @LogEntryExit
254     def __call__(self, parser, namespace, value, option_string=None):
255         if not value in self.valid_commands:
256             parser.error('"{}" is not a valid command'.format(value))
257         setattr(namespace, self.dest, value)
258
259
260 # Some Exceptions
261
262 class InvalidCommandError(Exception):
263     pass
264
265
266 class InvalidArgumentError(Exception):
267     pass
268
269
270 class InvalidConfigurationError(Exception):
271     pass
272
273
274 # subprocess.CalledProcessError has problems with being pickled, which is
275 # something that we do to it. In particular, when unpickled, it throws an
276 # exception, and so CalledProcessError get's turned into an exception saying
277 # "__init__() takes at least 3 arguments (1 given)". Address this by creating
278 # our own CalledProcessError that's a little more generic.
279
280 class CalledProcessError(Exception):
281     def __str__(self):
282         returncode = self.args[0] if len(self.args) > 0 else None
283         command = self.args[1] if len(self.args) > 1 else None
284         stdout = self.args[2] if len(self.args) > 2 else None
285         stderr = self.args[3] if len(self.args) > 3 else None
286
287         if stderr:
288             return "Command '{}' returned non-zero exit status {}: {}".format(command, returncode, stderr)
289         elif stdout:
290             return "Command '{}' returned non-zero exit status {}: {}".format(command, returncode, stdout)
291         else:
292             return "Command '{}' returned non-zero exit status {}".format(command, returncode)