blob: ffefd090306ec5ddd8de0d53c238f34fc94ef9bb [file] [log] [blame]
Mike Frysinger2e65c542016-03-08 16:17:00 -05001#!/usr/bin/python
2# -*- coding:utf-8 -*-
3# Copyright 2016 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Repo pre-upload hook.
18
19Normally this is loaded indirectly by repo itself, but it can be run directly
20when developing.
21"""
22
23from __future__ import print_function
24
25import argparse
Mike Frysinger579111e2019-12-04 21:36:01 -050026import datetime
Mike Frysinger2e65c542016-03-08 16:17:00 -050027import os
28import sys
29
Mike Frysinger7b30f4d2019-10-15 01:18:38 -040030
31# Assert some minimum Python versions as we don't test or support any others.
32# We only support Python 2.7, and require 2.7.5+/3.4+ to include signal fix:
33# https://bugs.python.org/issue14173
34if sys.version_info < (2, 7, 5):
35 print('repohooks: error: Python-2.7.5+ is required', file=sys.stderr)
36 sys.exit(1)
Mike Frysinger07107e62020-07-15 00:40:47 -040037elif sys.version_info.major == 3 and sys.version_info < (3, 5):
38 print('repohooks: error: Python-3.5+ is required', file=sys.stderr)
Mike Frysinger7b30f4d2019-10-15 01:18:38 -040039 sys.exit(1)
Mike Frysingere67d4802020-07-15 00:38:22 -040040elif sys.version_info < (3, 6):
Mike Frysingerd89347c2020-02-14 13:01:44 -050041 # We want to get people off of old versions of Python.
42 print('repohooks: warning: Python-3.6+ is going to be required; '
43 'please upgrade soon to maintain support.', file=sys.stderr)
Mike Frysinger7b30f4d2019-10-15 01:18:38 -040044
45
Mike Frysinger2e65c542016-03-08 16:17:00 -050046_path = os.path.dirname(os.path.realpath(__file__))
47if sys.path[0] != _path:
48 sys.path.insert(0, _path)
49del _path
50
Mike Frysinger2ef213c2017-11-10 15:41:56 -050051# We have to import our local modules after the sys.path tweak. We can't use
52# relative imports because this is an executable program, not a module.
53# pylint: disable=wrong-import-position
Mike Frysingerb9608182016-10-20 20:45:04 -040054import rh
Mike Frysinger2e65c542016-03-08 16:17:00 -050055import rh.results
56import rh.config
57import rh.git
58import rh.hooks
Mike Frysingerce3ab292019-08-09 17:58:50 -040059import rh.sixish
Mike Frysinger2e65c542016-03-08 16:17:00 -050060import rh.terminal
61import rh.utils
62
63
64# Repohooks homepage.
65REPOHOOKS_URL = 'https://android.googlesource.com/platform/tools/repohooks/'
66
67
Josh Gao25abf4b2016-09-23 18:36:27 -070068class Output(object):
69 """Class for reporting hook status."""
70
71 COLOR = rh.terminal.Color()
72 COMMIT = COLOR.color(COLOR.CYAN, 'COMMIT')
73 RUNNING = COLOR.color(COLOR.YELLOW, 'RUNNING')
74 PASSED = COLOR.color(COLOR.GREEN, 'PASSED')
75 FAILED = COLOR.color(COLOR.RED, 'FAILED')
Jason Monk0886c912017-11-10 13:17:17 -050076 WARNING = COLOR.color(COLOR.YELLOW, 'WARNING')
Josh Gao25abf4b2016-09-23 18:36:27 -070077
Mike Frysinger42234b72019-02-15 16:21:41 -050078 def __init__(self, project_name):
Josh Gao25abf4b2016-09-23 18:36:27 -070079 """Create a new Output object for a specified project.
80
81 Args:
82 project_name: name of project.
Josh Gao25abf4b2016-09-23 18:36:27 -070083 """
84 self.project_name = project_name
Mike Frysinger42234b72019-02-15 16:21:41 -050085 self.num_hooks = None
Josh Gao25abf4b2016-09-23 18:36:27 -070086 self.hook_index = 0
87 self.success = True
Mike Frysinger579111e2019-12-04 21:36:01 -050088 self.start_time = datetime.datetime.now()
Josh Gao25abf4b2016-09-23 18:36:27 -070089
Mike Frysinger42234b72019-02-15 16:21:41 -050090 def set_num_hooks(self, num_hooks):
91 """Keep track of how many hooks we'll be running.
92
93 Args:
94 num_hooks: number of hooks to be run.
95 """
96 self.num_hooks = num_hooks
97
Josh Gao25abf4b2016-09-23 18:36:27 -070098 def commit_start(self, commit, commit_summary):
99 """Emit status for new commit.
100
101 Args:
102 commit: commit hash.
103 commit_summary: commit summary.
104 """
105 status_line = '[%s %s] %s' % (self.COMMIT, commit[0:12], commit_summary)
106 rh.terminal.print_status_line(status_line, print_newline=True)
107 self.hook_index = 1
108
109 def hook_start(self, hook_name):
110 """Emit status before the start of a hook.
111
112 Args:
113 hook_name: name of the hook.
114 """
115 status_line = '[%s %d/%d] %s' % (self.RUNNING, self.hook_index,
116 self.num_hooks, hook_name)
117 self.hook_index += 1
118 rh.terminal.print_status_line(status_line)
119
120 def hook_error(self, hook_name, error):
Mike Frysingera18d5f12019-02-15 16:27:35 -0500121 """Print an error for a single hook.
Josh Gao25abf4b2016-09-23 18:36:27 -0700122
123 Args:
124 hook_name: name of the hook.
125 error: error string.
126 """
Mike Frysingera18d5f12019-02-15 16:27:35 -0500127 self.error(hook_name, error)
Josh Gao25abf4b2016-09-23 18:36:27 -0700128
Jason Monk0886c912017-11-10 13:17:17 -0500129 def hook_warning(self, hook_name, warning):
Mike Frysingera18d5f12019-02-15 16:27:35 -0500130 """Print a warning for a single hook.
Jason Monk0886c912017-11-10 13:17:17 -0500131
132 Args:
133 hook_name: name of the hook.
134 warning: warning string.
135 """
136 status_line = '[%s] %s' % (self.WARNING, hook_name)
137 rh.terminal.print_status_line(status_line, print_newline=True)
138 print(warning, file=sys.stderr)
139
Mike Frysingera18d5f12019-02-15 16:27:35 -0500140 def error(self, header, error):
141 """Print a general error.
142
143 Args:
144 header: A unique identifier for the source of this error.
145 error: error string.
146 """
147 status_line = '[%s] %s' % (self.FAILED, header)
148 rh.terminal.print_status_line(status_line, print_newline=True)
149 print(error, file=sys.stderr)
150 self.success = False
151
Josh Gao25abf4b2016-09-23 18:36:27 -0700152 def finish(self):
Mike Frysingera18d5f12019-02-15 16:27:35 -0500153 """Print summary for all the hooks."""
Mike Frysinger579111e2019-12-04 21:36:01 -0500154 status_line = '[%s] repohooks for %s %s in %s' % (
Josh Gao25abf4b2016-09-23 18:36:27 -0700155 self.PASSED if self.success else self.FAILED,
156 self.project_name,
Mike Frysinger579111e2019-12-04 21:36:01 -0500157 'passed' if self.success else 'failed',
158 rh.utils.timedelta_str(datetime.datetime.now() - self.start_time))
Josh Gao25abf4b2016-09-23 18:36:27 -0700159 rh.terminal.print_status_line(status_line, print_newline=True)
160
161
162def _process_hook_results(results):
163 """Returns an error string if an error occurred.
Mike Frysinger2e65c542016-03-08 16:17:00 -0500164
165 Args:
Josh Gao25abf4b2016-09-23 18:36:27 -0700166 results: A list of HookResult objects, or None.
Mike Frysinger2e65c542016-03-08 16:17:00 -0500167
168 Returns:
Josh Gao25abf4b2016-09-23 18:36:27 -0700169 error output if an error occurred, otherwise None
Jason Monk0886c912017-11-10 13:17:17 -0500170 warning output if an error occurred, otherwise None
Mike Frysinger2e65c542016-03-08 16:17:00 -0500171 """
Josh Gao25abf4b2016-09-23 18:36:27 -0700172 if not results:
Jason Monk0886c912017-11-10 13:17:17 -0500173 return (None, None)
Mike Frysinger2e65c542016-03-08 16:17:00 -0500174
Mike Frysingere5ad9af2020-03-27 22:29:01 -0400175 # We track these as dedicated fields in case a hook doesn't output anything.
176 # We want to treat silent non-zero exits as failures too.
177 has_error = False
178 has_warning = False
179
Jason Monk0886c912017-11-10 13:17:17 -0500180 error_ret = ''
181 warning_ret = ''
Mike Frysinger2e65c542016-03-08 16:17:00 -0500182 for result in results:
183 if result:
Jason Monk0886c912017-11-10 13:17:17 -0500184 ret = ''
Mike Frysinger2e65c542016-03-08 16:17:00 -0500185 if result.files:
Josh Gao25abf4b2016-09-23 18:36:27 -0700186 ret += ' FILES: %s' % (result.files,)
Mike Frysinger2e65c542016-03-08 16:17:00 -0500187 lines = result.error.splitlines()
Josh Gao25abf4b2016-09-23 18:36:27 -0700188 ret += '\n'.join(' %s' % (x,) for x in lines)
Jason Monk0886c912017-11-10 13:17:17 -0500189 if result.is_warning():
Mike Frysingere5ad9af2020-03-27 22:29:01 -0400190 has_warning = True
Jason Monk0886c912017-11-10 13:17:17 -0500191 warning_ret += ret
192 else:
Mike Frysingere5ad9af2020-03-27 22:29:01 -0400193 has_error = True
Jason Monk0886c912017-11-10 13:17:17 -0500194 error_ret += ret
Mike Frysinger2e65c542016-03-08 16:17:00 -0500195
Mike Frysingere5ad9af2020-03-27 22:29:01 -0400196 return (error_ret if has_error else None,
197 warning_ret if has_warning else None)
Mike Frysinger2e65c542016-03-08 16:17:00 -0500198
199
Luis Hector Chavez5c4c2932016-10-16 21:56:58 -0700200def _get_project_config():
201 """Returns the configuration for a project.
Mike Frysinger2e65c542016-03-08 16:17:00 -0500202
203 Expects to be called from within the project root.
204 """
Mike Frysingerca797702016-09-03 02:00:55 -0400205 global_paths = (
206 # Load the global config found in the manifest repo.
207 os.path.join(rh.git.find_repo_root(), '.repo', 'manifests'),
208 # Load the global config found in the root of the repo checkout.
209 rh.git.find_repo_root(),
210 )
211 paths = (
212 # Load the config for this git repo.
213 '.',
214 )
Mike Frysinger1baec122020-08-25 00:27:52 -0400215 return rh.config.PreUploadSettings(paths=paths, global_paths=global_paths)
Mike Frysinger2e65c542016-03-08 16:17:00 -0500216
217
Luis Hector Chavezdab680c2016-12-21 13:57:09 -0800218def _attempt_fixes(fixup_func_list, commit_list):
219 """Attempts to run |fixup_func_list| given |commit_list|."""
220 if len(fixup_func_list) != 1:
221 # Only single fixes will be attempted, since various fixes might
222 # interact with each other.
223 return
224
225 hook_name, commit, fixup_func = fixup_func_list[0]
226
227 if commit != commit_list[0]:
228 # If the commit is not at the top of the stack, git operations might be
229 # needed and might leave the working directory in a tricky state if the
230 # fix is attempted to run automatically (e.g. it might require manual
231 # merge conflict resolution). Refuse to run the fix in those cases.
232 return
233
234 prompt = ('An automatic fix can be attempted for the "%s" hook. '
235 'Do you want to run it?' % hook_name)
236 if not rh.terminal.boolean_prompt(prompt):
237 return
238
239 result = fixup_func()
240 if result:
241 print('Attempt to fix "%s" for commit "%s" failed: %s' %
242 (hook_name, commit, result),
243 file=sys.stderr)
244 else:
245 print('Fix successfully applied. Amend the current commit before '
246 'attempting to upload again.\n', file=sys.stderr)
247
248
Mike Frysinger42234b72019-02-15 16:21:41 -0500249def _run_project_hooks_in_cwd(project_name, proj_dir, output, commit_list=None):
250 """Run the project-specific hooks in the cwd.
Mike Frysinger2e65c542016-03-08 16:17:00 -0500251
252 Args:
Mike Frysinger42234b72019-02-15 16:21:41 -0500253 project_name: The name of this project.
254 proj_dir: The directory for this project (for passing on in metadata).
255 output: Helper for summarizing output/errors to the user.
Mike Frysinger2e65c542016-03-08 16:17:00 -0500256 commit_list: A list of commits to run hooks against. If None or empty
257 list then we'll automatically get the list of commits that would be
258 uploaded.
259
260 Returns:
261 False if any errors were found, else True.
262 """
Mike Frysingera18d5f12019-02-15 16:27:35 -0500263 try:
264 config = _get_project_config()
265 except rh.config.ValidationError as e:
266 output.error('Loading config files', str(e))
Mike Frysingera65ecb92019-02-15 15:58:31 -0500267 return False
Mike Frysingera18d5f12019-02-15 16:27:35 -0500268
269 # If the repo has no pre-upload hooks enabled, then just return.
Luis Hector Chavez5c4c2932016-10-16 21:56:58 -0700270 hooks = list(config.callable_hooks())
Mike Frysinger558aff42016-04-04 16:02:55 -0400271 if not hooks:
272 return True
273
Mike Frysinger42234b72019-02-15 16:21:41 -0500274 output.set_num_hooks(len(hooks))
275
Mike Frysinger2e65c542016-03-08 16:17:00 -0500276 # Set up the environment like repo would with the forall command.
Luis Hector Chavezd8f36752016-09-15 13:25:03 -0700277 try:
278 remote = rh.git.get_upstream_remote()
Luis Hector Chaveze9db4eb2016-11-29 16:20:12 -0800279 upstream_branch = rh.git.get_upstream_branch()
Mike Frysinger36d2ce62019-12-04 22:17:07 -0500280 except rh.utils.CalledProcessError as e:
Mike Frysingera18d5f12019-02-15 16:27:35 -0500281 output.error('Upstream remote/tracking branch lookup',
282 '%s\nDid you run repo start? Is your HEAD detached?' %
283 (e,))
Mike Frysingera65ecb92019-02-15 15:58:31 -0500284 return False
Mike Frysingera18d5f12019-02-15 16:27:35 -0500285
Mike Frysinger2e65c542016-03-08 16:17:00 -0500286 os.environ.update({
Luis Hector Chaveze9db4eb2016-11-29 16:20:12 -0800287 'REPO_LREV': rh.git.get_commit_for_ref(upstream_branch),
Mike Frysingerfdd1a842019-08-09 13:45:57 -0400288 'REPO_PATH': os.path.relpath(proj_dir, rh.git.find_repo_root()),
Luis Hector Chaveze9db4eb2016-11-29 16:20:12 -0800289 'REPO_PROJECT': project_name,
Mike Frysinger2e65c542016-03-08 16:17:00 -0500290 'REPO_REMOTE': remote,
Luis Hector Chaveze9db4eb2016-11-29 16:20:12 -0800291 'REPO_RREV': rh.git.get_remote_revision(upstream_branch, remote),
Mike Frysinger2e65c542016-03-08 16:17:00 -0500292 })
293
Mike Frysingerb9608182016-10-20 20:45:04 -0400294 project = rh.Project(name=project_name, dir=proj_dir, remote=remote)
Mike Frysinger2e65c542016-03-08 16:17:00 -0500295
296 if not commit_list:
Luis Hector Chavez5c4c2932016-10-16 21:56:58 -0700297 commit_list = rh.git.get_commits(
298 ignore_merged_commits=config.ignore_merged_commits)
Mike Frysinger2e65c542016-03-08 16:17:00 -0500299
Mike Frysinger2e65c542016-03-08 16:17:00 -0500300 ret = True
Luis Hector Chavezdab680c2016-12-21 13:57:09 -0800301 fixup_func_list = []
Josh Gao25abf4b2016-09-23 18:36:27 -0700302
Mike Frysinger2e65c542016-03-08 16:17:00 -0500303 for commit in commit_list:
304 # Mix in some settings for our hooks.
Mike Frysingerdc253622016-04-04 15:43:02 -0400305 os.environ['PREUPLOAD_COMMIT'] = commit
Mike Frysinger2e65c542016-03-08 16:17:00 -0500306 diff = rh.git.get_affected_files(commit)
Mike Frysinger76b1bc72016-04-20 16:50:16 -0400307 desc = rh.git.get_commit_desc(commit)
Mike Frysingerce3ab292019-08-09 17:58:50 -0400308 rh.sixish.setenv('PREUPLOAD_COMMIT_MESSAGE', desc)
Mike Frysinger2e65c542016-03-08 16:17:00 -0500309
Josh Gao25abf4b2016-09-23 18:36:27 -0700310 commit_summary = desc.split('\n', 1)[0]
311 output.commit_start(commit=commit, commit_summary=commit_summary)
Mike Frysinger2e65c542016-03-08 16:17:00 -0500312
Josh Gao25abf4b2016-09-23 18:36:27 -0700313 for name, hook in hooks:
314 output.hook_start(name)
315 hook_results = hook(project, commit, desc, diff)
Jason Monk0886c912017-11-10 13:17:17 -0500316 (error, warning) = _process_hook_results(hook_results)
Mike Frysingere5ad9af2020-03-27 22:29:01 -0400317 if error is not None or warning is not None:
318 if warning is not None:
Jason Monk0886c912017-11-10 13:17:17 -0500319 output.hook_warning(name, warning)
Mike Frysingere5ad9af2020-03-27 22:29:01 -0400320 if error is not None:
Jason Monk0886c912017-11-10 13:17:17 -0500321 ret = False
322 output.hook_error(name, error)
Luis Hector Chavezdab680c2016-12-21 13:57:09 -0800323 for result in hook_results:
324 if result.fixup_func:
325 fixup_func_list.append((name, commit,
326 result.fixup_func))
327
328 if fixup_func_list:
329 _attempt_fixes(fixup_func_list, commit_list)
Josh Gao25abf4b2016-09-23 18:36:27 -0700330
Mike Frysinger2e65c542016-03-08 16:17:00 -0500331 return ret
332
333
Mike Frysinger42234b72019-02-15 16:21:41 -0500334def _run_project_hooks(project_name, proj_dir=None, commit_list=None):
335 """Run the project-specific hooks in |proj_dir|.
336
337 Args:
338 project_name: The name of project to run hooks for.
339 proj_dir: If non-None, this is the directory the project is in. If None,
340 we'll ask repo.
341 commit_list: A list of commits to run hooks against. If None or empty
342 list then we'll automatically get the list of commits that would be
343 uploaded.
344
345 Returns:
346 False if any errors were found, else True.
347 """
348 output = Output(project_name)
349
350 if proj_dir is None:
351 cmd = ['repo', 'forall', project_name, '-c', 'pwd']
Mike Frysinger70b78f02019-12-04 21:42:39 -0500352 result = rh.utils.run(cmd, capture_output=True)
Mike Frysinger36d2ce62019-12-04 22:17:07 -0500353 proj_dirs = result.stdout.split()
Mike Frysinger5ac20862019-06-05 22:50:49 -0400354 if not proj_dirs:
Mike Frysinger42234b72019-02-15 16:21:41 -0500355 print('%s cannot be found.' % project_name, file=sys.stderr)
356 print('Please specify a valid project.', file=sys.stderr)
357 return False
358 if len(proj_dirs) > 1:
359 print('%s is associated with multiple directories.' % project_name,
360 file=sys.stderr)
361 print('Please specify a directory to help disambiguate.',
362 file=sys.stderr)
363 return False
364 proj_dir = proj_dirs[0]
365
366 pwd = os.getcwd()
367 try:
368 # Hooks assume they are run from the root of the project.
369 os.chdir(proj_dir)
370 return _run_project_hooks_in_cwd(project_name, proj_dir, output,
371 commit_list=commit_list)
372 finally:
373 output.finish()
374 os.chdir(pwd)
375
376
Mike Frysinger2e65c542016-03-08 16:17:00 -0500377def main(project_list, worktree_list=None, **_kwargs):
378 """Main function invoked directly by repo.
379
380 We must use the name "main" as that is what repo requires.
381
382 This function will exit directly upon error so that repo doesn't print some
383 obscure error message.
384
385 Args:
386 project_list: List of projects to run on.
387 worktree_list: A list of directories. It should be the same length as
388 project_list, so that each entry in project_list matches with a
389 directory in worktree_list. If None, we will attempt to calculate
390 the directories automatically.
391 kwargs: Leave this here for forward-compatibility.
392 """
393 found_error = False
394 if not worktree_list:
395 worktree_list = [None] * len(project_list)
396 for project, worktree in zip(project_list, worktree_list):
397 if not _run_project_hooks(project, proj_dir=worktree):
398 found_error = True
Mike Frysingera18d5f12019-02-15 16:27:35 -0500399 # If a repo had failures, add a blank line to help break up the
400 # output. If there were no failures, then the output should be
401 # very minimal, so we don't add it then.
402 print('', file=sys.stderr)
Mike Frysinger2e65c542016-03-08 16:17:00 -0500403
404 if found_error:
405 color = rh.terminal.Color()
406 print('%s: Preupload failed due to above error(s).\n'
407 'For more info, please see:\n%s' %
408 (color.color(color.RED, 'FATAL'), REPOHOOKS_URL),
409 file=sys.stderr)
410 sys.exit(1)
411
412
413def _identify_project(path):
414 """Identify the repo project associated with the given path.
415
416 Returns:
417 A string indicating what project is associated with the path passed in or
418 a blank string upon failure.
419 """
420 cmd = ['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}']
Mike Frysinger7bd2a9a2020-04-08 18:23:57 -0400421 return rh.utils.run(cmd, capture_output=True, cwd=path).stdout.strip()
Mike Frysinger2e65c542016-03-08 16:17:00 -0500422
423
424def direct_main(argv):
425 """Run hooks directly (outside of the context of repo).
426
427 Args:
428 argv: The command line args to process.
429
430 Returns:
431 0 if no pre-upload failures, 1 if failures.
432
433 Raises:
434 BadInvocation: On some types of invocation errors.
435 """
436 parser = argparse.ArgumentParser(description=__doc__)
437 parser.add_argument('--dir', default=None,
438 help='The directory that the project lives in. If not '
439 'specified, use the git project root based on the cwd.')
440 parser.add_argument('--project', default=None,
441 help='The project repo path; this can affect how the '
442 'hooks get run, since some hooks are project-specific.'
443 'If not specified, `repo` will be used to figure this '
444 'out based on the dir.')
445 parser.add_argument('commits', nargs='*',
446 help='Check specific commits')
447 opts = parser.parse_args(argv)
448
449 # Check/normalize git dir; if unspecified, we'll use the root of the git
450 # project from CWD.
451 if opts.dir is None:
452 cmd = ['git', 'rev-parse', '--git-dir']
Mike Frysinger7bd2a9a2020-04-08 18:23:57 -0400453 git_dir = rh.utils.run(cmd, capture_output=True).stdout.strip()
Mike Frysinger2e65c542016-03-08 16:17:00 -0500454 if not git_dir:
455 parser.error('The current directory is not part of a git project.')
456 opts.dir = os.path.dirname(os.path.abspath(git_dir))
457 elif not os.path.isdir(opts.dir):
458 parser.error('Invalid dir: %s' % opts.dir)
Adrian Roos8ac865f2018-04-13 12:08:52 +0100459 elif not rh.git.is_git_repository(opts.dir):
460 parser.error('Not a git repository: %s' % opts.dir)
Mike Frysinger2e65c542016-03-08 16:17:00 -0500461
462 # Identify the project if it wasn't specified; this _requires_ the repo
463 # tool to be installed and for the project to be part of a repo checkout.
464 if not opts.project:
465 opts.project = _identify_project(opts.dir)
466 if not opts.project:
467 parser.error("Repo couldn't identify the project of %s" % opts.dir)
468
469 if _run_project_hooks(opts.project, proj_dir=opts.dir,
470 commit_list=opts.commits):
471 return 0
Mike Frysinger5ac20862019-06-05 22:50:49 -0400472 return 1
Mike Frysinger2e65c542016-03-08 16:17:00 -0500473
474
475if __name__ == '__main__':
476 sys.exit(direct_main(sys.argv[1:]))