Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 1 | #!/usr/bin/python3 |
| 2 | |
| 3 | # This script converts simple Gyp files to their Bazel equivalents. |
| 4 | # It is not intended to be particularly extensible; it is only going to be used |
| 5 | # once over all of our code and then forgotten about. |
| 6 | # This "only has to work on exactly the code we have now" property also means |
| 7 | # it's pretty picky about many things and doesn't try to handle anything |
| 8 | # beyond what we actually have. |
| 9 | # |
| 10 | # It takes a list of folders to deal with. |
| 11 | # |
| 12 | # Running this script requires PyYAML <pyyaml.org> to be installed. |
| 13 | |
| 14 | import sys |
| 15 | import os |
| 16 | import yaml |
| 17 | import collections |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 18 | import re |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 19 | |
| 20 | '''Converts a Gyp filename to its Bazel equivalent. |
| 21 | |
| 22 | Args: |
| 23 | gyp_file_name: The name of the gyp file this is contained in. |
| 24 | This is important for resolving relative paths etc. |
| 25 | file: The name of the file to deal with. |
| 26 | ''' |
| 27 | def gyp_file_to_bazel(gyp_file_name, file): |
| 28 | if file.startswith('<(AOS)/'): |
| 29 | return '//aos' + file[6:] |
| 30 | elif file.startswith('<(DEPTH)/'): |
| 31 | return '//' + file[9:] |
| 32 | else: |
| 33 | if '(' in file: |
| 34 | raise RuntimeError('Bad variable in file "%s"' % file) |
| 35 | split_gyp = os.path.dirname(gyp_file_name).split('/') |
| 36 | rest = [] |
| 37 | dotdots = 0 |
| 38 | doing_dotdots = True |
| 39 | for component in file.split('/'): |
| 40 | if component == '..': |
| 41 | dotdots += 1 |
| 42 | if not doing_dotdots: |
| 43 | raise RuntimeError('Bad use of .. in file "%s"' % file) |
| 44 | else: |
| 45 | doing_dotdots = False |
| 46 | rest.append(component) |
| 47 | return '/'.join(['/'] + split_gyp[:-dotdots] + rest) |
| 48 | |
| 49 | '''Converts a Gyp target to its Bazel equivalent. |
| 50 | |
| 51 | Args: |
| 52 | gyp_file_name: The name of the gyp file this is contained in. |
| 53 | This is important for resolving relative paths etc. |
| 54 | target: The name of the target to deal with. |
| 55 | ''' |
| 56 | def gyp_target_to_bazel(gyp_file_name, target): |
| 57 | if not ':' in target: |
| 58 | if '.' in target: |
| 59 | raise RuntimeError('Have a filename instead of a target: "%s"' % target) |
| 60 | return ':' + target |
| 61 | if target[0] == ':': |
| 62 | return target |
| 63 | |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 64 | # These thin wrappers won't be copied. |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 65 | if target == '<(AOS)/build/aos.gyp:logging': |
| 66 | return '//aos/common/logging' |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 67 | if target == '<(AOS)/build/aos.gyp:logging_interface': |
| 68 | return '//aos/common/logging:logging_interface' |
| 69 | |
| 70 | # These are getting moved to the right place manually. |
| 71 | if target == '<(AOS)/common/common.gyp:condition': |
| 72 | return '//aos/linux_code/ipc_lib:condition' |
| 73 | if target == '<(AOS)/common/common.gyp:mutex': |
| 74 | return '//aos/linux_code/ipc_lib:mutex' |
| 75 | if target == '<(AOS)/common/common.gyp:event': |
| 76 | return '//aos/linux_code/ipc_lib:event' |
| 77 | |
| 78 | # By building ..., we can mostly ignore these. |
| 79 | if (target == '<(AOS)/build/aos_all.gyp:Prime' or |
| 80 | target == '../../frc971/frc971.gyp:All'): |
| 81 | return '//aos:prime_binaries' |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 82 | |
| 83 | split = target.split(':') |
| 84 | if len(split) != 2: |
| 85 | raise RuntimeError('Not sure how to parse target "%s"' % target) |
| 86 | |
| 87 | if split[0] == '<(EXTERNALS)': |
| 88 | return '//third_party/' + split[1] |
| 89 | |
| 90 | split_path = split[0].rsplit('/', 1) |
| 91 | if len(split_path) != 2: |
| 92 | raise RuntimeError('TODO(Brian): Handle referring to this .gyp file as %s!' % split[0]) |
| 93 | if not split_path[1].endswith('.gyp'): |
| 94 | raise RuntimeError('Not sure how to deal with gyp filename "%s"' % split[0]) |
| 95 | |
| 96 | folder = gyp_file_to_bazel(gyp_file_name, split_path[0]) |
| 97 | |
| 98 | if not folder.endswith(split_path[1][:-4]): |
| 99 | raise RuntimeError('Not sure how to deal with non-matching gyp file "%s"' % target) |
| 100 | |
| 101 | return '%s:%s' % (folder, split[1]) |
| 102 | |
| 103 | '''Represents a Bazel build target. |
| 104 | |
| 105 | Subclasses represent actual concrete things which are emitted into a BUILD |
| 106 | file.''' |
| 107 | class BuildTarget(object): |
| 108 | def __init__(self, type, name): |
| 109 | self.__type = type |
| 110 | self.__name = name |
| 111 | |
| 112 | def add_dep(self, bazel_dep): |
| 113 | self.__deps.append(bazel_dep) |
| 114 | |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 115 | def _type(self): |
| 116 | return self.__type |
| 117 | |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 118 | '''Returns a collections.OrderedDict with all of the attributes on the |
| 119 | Bazel rule this represents. |
| 120 | |
| 121 | Subclasses are expected to override this and add their own attributes |
| 122 | in the appropriate order.''' |
| 123 | def attrs(self): |
| 124 | r = collections.OrderedDict() |
| 125 | r['name'] = self.__name |
| 126 | return r |
| 127 | |
| 128 | '''Returns a set of load statements. |
| 129 | |
| 130 | Subclasses are expected to override this and add their own loads. |
| 131 | |
| 132 | Each element of the result is the arguments to a single load call.''' |
| 133 | def loads(self): |
| 134 | return set() |
| 135 | |
| 136 | '''Returns the Bazel representation of a given object as an attribute |
| 137 | value.''' |
| 138 | def __to_bazel_string(o): |
| 139 | if isinstance(o, str): |
| 140 | return repr(o) |
| 141 | if hasattr(o, '__iter__'): |
| 142 | r = ['['] |
| 143 | for c in o: |
| 144 | r.append(' %s,' % BuildTarget.__to_bazel_string(c)) |
| 145 | r.append(' ]') |
| 146 | return '\n'.join(r) |
| 147 | else: |
| 148 | return str(o) |
| 149 | |
| 150 | def __str__(self): |
| 151 | r = [self.__type + '('] |
| 152 | for name, value in self.attrs().items(): |
| 153 | if value: |
| 154 | r.append(' %s = %s,' % (name, BuildTarget.__to_bazel_string(value))) |
| 155 | r.append(')') |
| 156 | return '\n'.join(r) |
| 157 | |
| 158 | '''Represents a cc_* target.''' |
| 159 | class CcBuildTarget(BuildTarget): |
| 160 | def __init__(self, type, name): |
| 161 | if not type.startswith('cc_'): |
| 162 | raise |
| 163 | |
| 164 | super(CcBuildTarget, self).__init__(type, name) |
| 165 | |
| 166 | self.__srcs = [] |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 167 | self.__hdrs = [] |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 168 | self.__deps = [] |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 169 | self.__tags = [] |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 170 | |
| 171 | def add_src(self, src): |
| 172 | self.__srcs.append(src) |
| 173 | |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 174 | def add_hdr(self, hdr): |
| 175 | self.__hdrs.append(hdr) |
| 176 | |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 177 | def add_dep(self, dep): |
| 178 | self.__deps.append(dep) |
| 179 | |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 180 | def add_tag(self, tag): |
| 181 | if self._type() != 'cc_test': |
| 182 | raise RuntimeError( |
| 183 | 'Trying to add tag %s to non-test type %s' % (tag, self._type())) |
| 184 | self.__tags.append(tag) |
| 185 | |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 186 | def attrs(self): |
Brian Silverman | f480a61 | 2015-09-13 02:22:01 -0400 | [diff] [blame] | 187 | unique_deps = [] |
| 188 | for dep in self.__deps: |
| 189 | if dep not in unique_deps: |
| 190 | unique_deps.append(dep) |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 191 | r = super(CcBuildTarget, self).attrs(); |
| 192 | r['srcs'] = self.__srcs |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 193 | r['hdrs'] = self.__hdrs |
| 194 | r['tags'] = self.__tags |
Brian Silverman | f480a61 | 2015-09-13 02:22:01 -0400 | [diff] [blame] | 195 | r['deps'] = unique_deps |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 196 | return r |
| 197 | |
| 198 | '''Represents a filegroup target.''' |
| 199 | class FilegroupTarget(BuildTarget): |
| 200 | def __init__(self, name): |
| 201 | super(FilegroupTarget, self).__init__('filegroup', name) |
| 202 | |
| 203 | self.__srcs = [] |
| 204 | |
| 205 | def add_src(self, src): |
| 206 | self.__srcs.append(src) |
| 207 | |
| 208 | def attrs(self): |
| 209 | r = super(FilegroupTarget, self).attrs(); |
| 210 | r['srcs'] = self.__srcs |
| 211 | return r |
| 212 | |
| 213 | '''Represents a queue_library target.''' |
| 214 | class QueueTarget(BuildTarget): |
| 215 | def __init__(self, name): |
| 216 | super(QueueTarget, self).__init__('queue_library', name) |
| 217 | |
| 218 | self.__srcs = [] |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 219 | self.__deps = [] |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 220 | |
| 221 | def add_src(self, src): |
| 222 | self.__srcs.append(src) |
| 223 | |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 224 | def add_dep(self, dep): |
| 225 | self.__deps.append(dep) |
| 226 | |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 227 | def loads(self): |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 228 | return set((('/aos/build/queues', 'queue_library'),)) |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 229 | |
| 230 | def attrs(self): |
| 231 | r = super(QueueTarget, self).attrs(); |
| 232 | r['srcs'] = self.__srcs |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 233 | r['deps'] = self.__deps |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 234 | return r |
| 235 | |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 236 | def _warn_attr(keys_to_handle, name, gyp_file_name, attr): |
| 237 | if attr in keys_to_handle: |
| 238 | print('Target %s in %s has %s' % (name, gyp_file_name, attr), |
| 239 | file=sys.stderr) |
| 240 | keys_to_handle.remove(attr) |
| 241 | |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 242 | def main(argv): |
| 243 | for d in argv: |
| 244 | build_targets = [] |
| 245 | |
| 246 | d = d.rstrip('/') |
| 247 | gyp_file_name = os.path.join(d, os.path.split(d)[-1] + '.gyp') |
| 248 | with open(gyp_file_name, 'r') as gyp_file: |
| 249 | gyp = yaml.load(gyp_file) |
| 250 | if 'targets' not in gyp: |
| 251 | print('No targets entry found in %s!' % gyp_file_name, file=sys.stderr) |
| 252 | return 1 |
| 253 | if list(gyp.keys()) != ['targets']: |
| 254 | print('Unknown keys of %s from %s' % (gyp.keys(), gyp_file_name), |
| 255 | file=sys.stderr) |
| 256 | targets = gyp['targets'] |
| 257 | for gyp_target in targets: |
| 258 | target = None |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 259 | keys_to_handle = set(gyp_target.keys()) |
| 260 | if 'export_dependent_settings' in gyp_target: |
| 261 | keys_to_handle.remove('export_dependent_settings') |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 262 | name = gyp_target['target_name'] |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 263 | keys_to_handle.remove('target_name') |
| 264 | _warn_attr(keys_to_handle, name, gyp_file_name, 'actions') |
| 265 | _warn_attr(keys_to_handle, name, gyp_file_name, 'conditions') |
| 266 | _warn_attr(keys_to_handle, name, gyp_file_name, 'copies') |
| 267 | _warn_attr(keys_to_handle, name, gyp_file_name, 'hard_dependency') |
| 268 | _warn_attr(keys_to_handle, name, gyp_file_name, |
| 269 | 'direct_dependent_settings') |
| 270 | |
| 271 | # These are getting moved to the right place manually. |
| 272 | if gyp_file_name == 'aos/common/common.gyp': |
| 273 | if name == 'condition' or name == 'mutex' or name == 'event': |
| 274 | continue |
| 275 | # By building ..., this becomes irrelevant. |
| 276 | if gyp_file_name == 'frc971/frc971.gyp': |
| 277 | if name == 'All': |
| 278 | continue |
| 279 | |
| 280 | if 'variables' in gyp_target: |
| 281 | if 'no_rsync' in gyp_target['variables']: |
| 282 | del gyp_target['variables']['no_rsync'] |
| 283 | |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 284 | type = gyp_target['type'] |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 285 | keys_to_handle.remove('type') |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 286 | if (type in ['static_library', 'executable'] and |
| 287 | not 'includes' in gyp_target): |
| 288 | cc_type = { |
| 289 | 'static_library': 'cc_library', |
| 290 | 'executable': 'cc_binary', |
| 291 | }[type] |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 292 | if re.match('.*_test$', name) and cc_type == 'cc_binary': |
| 293 | cc_type = 'cc_test' |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 294 | target = CcBuildTarget(cc_type, name) |
| 295 | |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 296 | if 'dependencies' in gyp_target: |
| 297 | for dep in gyp_target['dependencies']: |
| 298 | target.add_dep(gyp_target_to_bazel(gyp_file_name, dep)) |
| 299 | keys_to_handle.remove('dependencies') |
| 300 | if 'sources' in gyp_target: |
| 301 | for src in gyp_target['sources']: |
| 302 | # In //aos/common:queue_types, this will get dealt with manually |
| 303 | # along with the actions. |
| 304 | if src == '<(print_field_cc)': |
| 305 | continue |
| 306 | |
| 307 | if '/' in src: |
| 308 | raise RuntimeError( |
| 309 | 'Bad folder for %s in target %s in %s' % (src, name, |
| 310 | gyp_file_name)) |
| 311 | |
| 312 | target.add_src(src) |
| 313 | |
| 314 | # This is sort of a heuristic: if there's a header file matching |
| 315 | # the source file, add it as an hdr. This is going to require some |
| 316 | # manual cleanup, but it'll be close. |
| 317 | src_filename = os.path.join(os.path.dirname(gyp_file_name), src) |
| 318 | if not os.path.exists(src_filename): |
| 319 | raise RuntimeError( |
| 320 | 'Can not find source %s in target %s' % (src_filename, |
| 321 | name)) |
| 322 | header = src_filename.rsplit('.', 2)[0] + '.h' |
| 323 | if os.path.exists(header): |
| 324 | target.add_hdr(src.rsplit('.', 2)[0] + '.h') |
| 325 | keys_to_handle.remove('sources') |
| 326 | if 'variables' in gyp_target: |
| 327 | vars = gyp_target['variables'] |
| 328 | if 'is_special_test' in vars: |
| 329 | if vars['is_special_test'] != 1: |
| 330 | raise RuntimeError( |
| 331 | 'Unexpected is_special_test value in target %s' % name) |
| 332 | target.add_tag('manual') |
| 333 | del vars['is_special_test'] |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 334 | elif type == 'none': |
| 335 | target = FilegroupTarget(name) |
| 336 | for dep in gyp_target['dependencies']: |
| 337 | target.add_src(gyp_target_to_bazel(gyp_file_name, dep)) |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 338 | keys_to_handle.remove('dependencies') |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 339 | elif 'includes' in gyp_target: |
| 340 | includes = gyp_target['includes'] |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 341 | keys_to_handle.remove('includes') |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 342 | if len(includes) != 1: |
| 343 | raise RuntimeError( |
| 344 | 'Not sure how to handle multiple includes in %s' % gyp_target) |
| 345 | include = gyp_file_to_bazel(gyp_file_name, includes[0]) |
| 346 | if include == '//aos/build/queues.gypi': |
| 347 | vars = gyp_target['variables'] |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 348 | keys_to_handle.remove('variables') |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 349 | if 'header_path' not in vars: |
| 350 | raise RuntimeError( |
| 351 | 'No header_path for target %s in %s' % (name, gyp_file_name)) |
| 352 | if list(vars.keys()) != ['header_path']: |
| 353 | raise RuntimeError( |
| 354 | 'Extra variables for target %s in %s' % (name, gyp_file_name)) |
| 355 | if vars['header_path'] != os.path.dirname(gyp_file_name): |
| 356 | raise RuntimeError( |
| 357 | 'Incorrect header_path for target %s in %s' % (name, |
| 358 | gyp_file_name)) |
| 359 | |
| 360 | target = QueueTarget(name) |
| 361 | for src in gyp_target['sources']: |
| 362 | if '/' in src: |
| 363 | raise RuntimeError( |
| 364 | '.q src %s in bad dir for target %s in %s' % (src, |
| 365 | name, |
| 366 | gyp_file_name)) |
| 367 | target.add_src(src) |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 368 | keys_to_handle.remove('sources') |
| 369 | if 'dependencies' in gyp_target: |
| 370 | for dep in gyp_target['dependencies']: |
| 371 | target.add_dep(gyp_target_to_bazel(gyp_file_name, dep)) |
| 372 | keys_to_handle.remove('dependencies') |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 373 | else: |
| 374 | raise RuntimeError( |
| 375 | 'Unknown include %s for target %s in %s' % (include, name, |
| 376 | gyp_file_name)) |
| 377 | else: |
| 378 | raise RuntimeError( |
| 379 | 'Unknown type %s for target %s in %s' % (type, name, gyp_file_name)) |
| 380 | |
| 381 | if not target: |
| 382 | raise |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 383 | |
| 384 | if (gyp_file_name == 'y2015/http_status/http_status.gyp' and |
| 385 | name == 'http_status'): |
| 386 | # We'll handle these manually. |
| 387 | keys_to_handle.remove('include_dirs') |
| 388 | if (gyp_file_name == 'aos/common/common.gyp' and |
| 389 | name == 'queue_types'): |
| 390 | # These will get handled manually as part of dealing with the |
| 391 | # actions. |
| 392 | keys_to_handle.remove('variables') |
| 393 | |
| 394 | # If there were variables but they all got deleted, then we don't |
| 395 | # actually have any more to handle. |
| 396 | if 'variables' in keys_to_handle and not gyp_target['variables']: |
| 397 | keys_to_handle.remove('variables') |
| 398 | if keys_to_handle: |
| 399 | raise RuntimeError( |
| 400 | 'Unhandled keys for target %s in %s: %s' % (name, gyp_file_name, |
| 401 | keys_to_handle)) |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 402 | build_targets.append(target) |
| 403 | |
Brian Silverman | 100534c | 2015-09-07 15:51:23 -0400 | [diff] [blame] | 404 | if not build_targets: |
| 405 | print('No output targets for %s' % d, file=sys.stderr) |
| 406 | continue |
| 407 | |
Brian Silverman | 8c374e0 | 2015-09-06 23:02:21 -0400 | [diff] [blame] | 408 | with open(os.path.join(d, 'BUILD'), 'w') as build_file: |
| 409 | build_file.write( |
| 410 | 'package(default_visibility = [\'//visibility:public\'])\n') |
| 411 | loads = set() |
| 412 | for t in build_targets: |
| 413 | loads |= t.loads() |
| 414 | if loads: |
| 415 | build_file.write('\n') |
| 416 | for load in sorted(loads): |
| 417 | build_file.write('load(%s)\n' % (', '.join([repr(part) for part |
| 418 | in load]))) |
| 419 | for t in build_targets: |
| 420 | build_file.write('\n') |
| 421 | build_file.write(str(t)) |
| 422 | build_file.write('\n') |
| 423 | |
| 424 | if __name__ == '__main__': |
| 425 | sys.exit(main(sys.argv[1:])) |