blob: 9e6214c3d7373796ca73ca0ae5a19edb2fe88e52 [file] [log] [blame]
Doug Anderson44a644f2011-11-02 10:37:37 -07001#!/usr/bin/env python
Jon Salz98255932012-08-18 14:48:02 +08002# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Mike Frysingerae409522014-02-01 03:16:11 -05006"""Presubmit checks to run when doing `repo upload`.
7
8You can add new checks by adding a functions to the HOOKS constants.
9"""
10
Mike Frysinger09d6a3d2013-10-08 22:21:03 -040011from __future__ import print_function
12
Ryan Cui9b651632011-05-11 11:38:58 -070013import ConfigParser
Jon Salz3ee59de2012-08-18 13:54:22 +080014import functools
Dale Curtis2975c432011-05-03 17:25:20 -070015import json
Doug Anderson44a644f2011-11-02 10:37:37 -070016import optparse
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070017import os
Ryan Cuiec4d6332011-05-02 14:15:25 -070018import re
Mandeep Singh Bainesa7ffa4b2011-05-03 11:37:02 -070019import sys
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070020import subprocess
Peter Ammon811f6702014-06-12 15:45:38 -070021import stat
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070022
Ryan Cui1562fb82011-05-09 11:01:31 -070023from errors import (VerifyException, HookFailure, PrintErrorForProject,
24 PrintErrorsForCommit)
Ryan Cuiec4d6332011-05-02 14:15:25 -070025
David Jamesc3b68b32013-04-03 09:17:03 -070026# If repo imports us, the __name__ will be __builtin__, and the wrapper will
27# be in $CHROMEOS_CHECKOUT/.repo/repo/main.py, so we need to go two directories
28# up. The same logic also happens to work if we're executed directly.
29if __name__ in ('__builtin__', '__main__'):
30 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..', '..'))
31
32from chromite.lib import patch
Mike Frysinger2ec70ed2014-08-17 19:28:34 -040033from chromite.licensing import licenses_lib
David Jamesc3b68b32013-04-03 09:17:03 -070034
Vadim Bendebury2b62d742014-06-22 13:14:51 -070035PRE_SUBMIT = 'pre-submit'
Ryan Cuiec4d6332011-05-02 14:15:25 -070036
37COMMON_INCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050038 # C++ and friends
39 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
40 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$",
41 # Scripts
42 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
43 # No extension at all, note that ALL CAPS files are black listed in
44 # COMMON_EXCLUDED_LIST below.
45 r"(^|.*[\\\/])[^.]+$",
46 # Other
47 r".*\.java$", r".*\.mk$", r".*\.am$",
Ryan Cuiec4d6332011-05-02 14:15:25 -070048]
49
Ryan Cui1562fb82011-05-09 11:01:31 -070050
Ryan Cuiec4d6332011-05-02 14:15:25 -070051COMMON_EXCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050052 # avoid doing source file checks for kernel
53 r"/src/third_party/kernel/",
54 r"/src/third_party/kernel-next/",
55 r"/src/third_party/ktop/",
56 r"/src/third_party/punybench/",
57 r".*\bexperimental[\\\/].*",
58 r".*\b[A-Z0-9_]{2,}$",
59 r".*[\\\/]debian[\\\/]rules$",
60 # for ebuild trees, ignore any caches and manifest data
61 r".*/Manifest$",
62 r".*/metadata/[^/]*cache[^/]*/[^/]+/[^/]+$",
Doug Anderson5bfb6792011-10-25 16:45:41 -070063
Mike Frysingerae409522014-02-01 03:16:11 -050064 # ignore profiles data (like overlay-tegra2/profiles)
65 r".*/overlay-.*/profiles/.*",
Mike Frysinger98638102014-08-28 00:15:08 -040066 r"^profiles/.*$",
67
Mike Frysingerae409522014-02-01 03:16:11 -050068 # ignore minified js and jquery
69 r".*\.min\.js",
70 r".*jquery.*\.js",
Mike Frysinger33a458d2014-03-03 17:00:51 -050071
72 # Ignore license files as the content is often taken verbatim.
73 r'.*/licenses/.*',
Ryan Cuiec4d6332011-05-02 14:15:25 -070074]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070075
Ryan Cui1562fb82011-05-09 11:01:31 -070076
Ryan Cui9b651632011-05-11 11:38:58 -070077_CONFIG_FILE = 'PRESUBMIT.cfg'
78
79
Doug Anderson44a644f2011-11-02 10:37:37 -070080# Exceptions
81
82
83class BadInvocation(Exception):
84 """An Exception indicating a bad invocation of the program."""
85 pass
86
87
Ryan Cui1562fb82011-05-09 11:01:31 -070088# General Helpers
89
Sean Paulba01d402011-05-05 11:36:23 -040090
Doug Anderson44a644f2011-11-02 10:37:37 -070091def _run_command(cmd, cwd=None, stderr=None):
92 """Executes the passed in command and returns raw stdout output.
93
94 Args:
95 cmd: The command to run; should be a list of strings.
96 cwd: The directory to switch to for running the command.
97 stderr: Can be one of None (print stderr to console), subprocess.STDOUT
98 (combine stderr with stdout), or subprocess.PIPE (ignore stderr).
99
100 Returns:
101 The standard out from the process.
102 """
103 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr, cwd=cwd)
104 return p.communicate()[0]
Ryan Cui72834d12011-05-05 14:51:33 -0700105
Ryan Cui1562fb82011-05-09 11:01:31 -0700106
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700107def _get_hooks_dir():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700108 """Returns the absolute path to the repohooks directory."""
Doug Anderson44a644f2011-11-02 10:37:37 -0700109 if __name__ == '__main__':
110 # Works when file is run on its own (__file__ is defined)...
111 return os.path.abspath(os.path.dirname(__file__))
112 else:
113 # We need to do this when we're run through repo. Since repo executes
114 # us with execfile(), we don't get __file__ defined.
115 cmd = ['repo', 'forall', 'chromiumos/repohooks', '-c', 'pwd']
116 return _run_command(cmd).strip()
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700117
Ryan Cui1562fb82011-05-09 11:01:31 -0700118
Ryan Cuiec4d6332011-05-02 14:15:25 -0700119def _match_regex_list(subject, expressions):
120 """Try to match a list of regular expressions to a string.
121
122 Args:
123 subject: The string to match regexes on
124 expressions: A list of regular expressions to check for matches with.
125
126 Returns:
127 Whether the passed in subject matches any of the passed in regexes.
128 """
129 for expr in expressions:
Mike Frysingerae409522014-02-01 03:16:11 -0500130 if re.search(expr, subject):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700131 return True
132 return False
133
Ryan Cui1562fb82011-05-09 11:01:31 -0700134
Mike Frysingerae409522014-02-01 03:16:11 -0500135def _filter_files(files, include_list, exclude_list=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700136 """Filter out files based on the conditions passed in.
137
138 Args:
139 files: list of filepaths to filter
140 include_list: list of regex that when matched with a file path will cause it
141 to be added to the output list unless the file is also matched with a
142 regex in the exclude_list.
143 exclude_list: list of regex that when matched with a file will prevent it
144 from being added to the output list, even if it is also matched with a
145 regex in the include_list.
146
147 Returns:
148 A list of filepaths that contain files matched in the include_list and not
149 in the exclude_list.
150 """
151 filtered = []
152 for f in files:
153 if (_match_regex_list(f, include_list) and
154 not _match_regex_list(f, exclude_list)):
155 filtered.append(f)
156 return filtered
157
Ryan Cuiec4d6332011-05-02 14:15:25 -0700158
159# Git Helpers
Ryan Cui1562fb82011-05-09 11:01:31 -0700160
161
Ryan Cui4725d952011-05-05 15:41:19 -0700162def _get_upstream_branch():
163 """Returns the upstream tracking branch of the current branch.
164
165 Raises:
166 Error if there is no tracking branch
167 """
168 current_branch = _run_command(['git', 'symbolic-ref', 'HEAD']).strip()
169 current_branch = current_branch.replace('refs/heads/', '')
170 if not current_branch:
Ryan Cui1562fb82011-05-09 11:01:31 -0700171 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700172
173 cfg_option = 'branch.' + current_branch + '.%s'
174 full_upstream = _run_command(['git', 'config', cfg_option % 'merge']).strip()
175 remote = _run_command(['git', 'config', cfg_option % 'remote']).strip()
176 if not remote or not full_upstream:
Ryan Cui1562fb82011-05-09 11:01:31 -0700177 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700178
179 return full_upstream.replace('heads', 'remotes/' + remote)
180
Ryan Cui1562fb82011-05-09 11:01:31 -0700181
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700182def _get_patch(commit):
183 """Returns the patch for this commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700184 if commit == PRE_SUBMIT:
185 return _run_command(['git', 'diff', '--cached', 'HEAD'])
186 else:
187 return _run_command(['git', 'format-patch', '--stdout', '-1', commit])
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700188
Ryan Cui1562fb82011-05-09 11:01:31 -0700189
Jon Salz98255932012-08-18 14:48:02 +0800190def _try_utf8_decode(data):
191 """Attempts to decode a string as UTF-8.
192
193 Returns:
194 The decoded Unicode object, or the original string if parsing fails.
195 """
196 try:
197 return unicode(data, 'utf-8', 'strict')
198 except UnicodeDecodeError:
199 return data
200
201
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500202def _get_file_content(path, commit):
203 """Returns the content of a file at a specific commit.
204
205 We can't rely on the file as it exists in the filesystem as people might be
206 uploading a series of changes which modifies the file multiple times.
207
208 Note: The "content" of a symlink is just the target. So if you're expecting
209 a full file, you should check that first. One way to detect is that the
210 content will not have any newlines.
211 """
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700212 if commit == PRE_SUBMIT:
213 return _run_command(['git', 'diff', 'HEAD', path])
214 else:
215 return _run_command(['git', 'show', '%s:%s' % (commit, path)])
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500216
217
Mike Frysingerae409522014-02-01 03:16:11 -0500218def _get_file_diff(path, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700219 """Returns a list of (linenum, lines) tuples that the commit touched."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700220 command = ['git', 'diff', '-p', '--pretty=format:', '--no-ext-diff']
221 if commit == PRE_SUBMIT:
222 command += ['HEAD', path]
223 else:
224 command += [commit, path]
225 output = _run_command(command)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700226
227 new_lines = []
228 line_num = 0
229 for line in output.splitlines():
230 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
231 if m:
232 line_num = int(m.groups(1)[0])
233 continue
234 if line.startswith('+') and not line.startswith('++'):
Jon Salz98255932012-08-18 14:48:02 +0800235 new_lines.append((line_num, _try_utf8_decode(line[1:])))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700236 if not line.startswith('-'):
237 line_num += 1
238 return new_lines
239
Ryan Cui1562fb82011-05-09 11:01:31 -0700240
Peter Ammon811f6702014-06-12 15:45:38 -0700241def _parse_affected_files(output, include_deletes=False, relative=False):
242 """Parses git diff's 'raw' format, returning a list of modified file paths.
243
244 This excludes directories and symlinks, and optionally includes files that
245 were deleted.
Doug Anderson42b8a052013-06-26 10:45:36 -0700246
247 Args:
Peter Ammon811f6702014-06-12 15:45:38 -0700248 output: The result of the 'git diff --raw' command
249 include_deletes: If true, we'll include deleted files in the result
250 relative: Whether to return relative or full paths to files
Doug Anderson42b8a052013-06-26 10:45:36 -0700251
252 Returns:
253 A list of modified/added (and perhaps deleted) files
254 """
Ryan Cuiec4d6332011-05-02 14:15:25 -0700255 files = []
Peter Ammon811f6702014-06-12 15:45:38 -0700256 # See the documentation for 'git diff --raw' for the relevant format.
Ryan Cuiec4d6332011-05-02 14:15:25 -0700257 for statusline in output.splitlines():
Peter Ammon811f6702014-06-12 15:45:38 -0700258 attributes, paths = statusline.split('\t', 1)
259 _, mode, _, _, status = attributes.split(' ')
260
261 # Ignore symlinks and directories.
262 imode = int(mode, 8)
263 if stat.S_ISDIR(imode) or stat.S_ISLNK(imode):
264 continue
265
266 # Ignore deleted files, and optionally return absolute paths of files.
267 if include_deletes or status != 'D':
268 # If a file was merely modified, we will have a single file path.
269 # If it was moved, we will have two paths (source and destination).
270 # In either case, we want the last path.
271 f = paths.split('\t')[-1]
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500272 if not relative:
273 pwd = os.getcwd()
274 f = os.path.join(pwd, f)
275 files.append(f)
Peter Ammon811f6702014-06-12 15:45:38 -0700276
Ryan Cuiec4d6332011-05-02 14:15:25 -0700277 return files
278
Ryan Cui1562fb82011-05-09 11:01:31 -0700279
Peter Ammon811f6702014-06-12 15:45:38 -0700280def _get_affected_files(commit, include_deletes=False, relative=False):
281 """Returns list of file paths that were modified/added, excluding symlinks.
282
283 Args:
284 commit: The commit
285 include_deletes: If true, we'll include deleted files in the result
286 relative: Whether to return relative or full paths to files
287
288 Returns:
289 A list of modified/added (and perhaps deleted) files
290 """
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700291 if commit == PRE_SUBMIT:
292 return _run_command(['git', 'diff-index', '--cached',
293 '--name-only', 'HEAD']).split()
Peter Ammon811f6702014-06-12 15:45:38 -0700294 output = _run_command(['git', 'diff', '--raw', commit + '^!'])
295 return _parse_affected_files(output, include_deletes, relative)
296
297
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700298def _get_commits():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700299 """Returns a list of commits for this review."""
Ryan Cui4725d952011-05-05 15:41:19 -0700300 cmd = ['git', 'log', '%s..' % _get_upstream_branch(), '--format=%H']
Ryan Cui72834d12011-05-05 14:51:33 -0700301 return _run_command(cmd).split()
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700302
Ryan Cui1562fb82011-05-09 11:01:31 -0700303
Ryan Cuiec4d6332011-05-02 14:15:25 -0700304def _get_commit_desc(commit):
305 """Returns the full commit message of a commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700306 if commit == PRE_SUBMIT:
307 return ''
Sean Paul23a2c582011-05-06 13:10:44 -0400308 return _run_command(['git', 'log', '--format=%s%n%n%b', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700309
310
311# Common Hooks
312
Ryan Cui1562fb82011-05-09 11:01:31 -0700313
Mike Frysingerae409522014-02-01 03:16:11 -0500314def _check_no_long_lines(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700315 """Checks that there aren't any lines longer than maxlen characters in any of
316 the text files to be submitted.
317 """
318 MAX_LEN = 80
Jon Salz98255932012-08-18 14:48:02 +0800319 SKIP_REGEXP = re.compile('|'.join([
320 r'https?://',
321 r'^#\s*(define|include|import|pragma|if|endif)\b']))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700322
323 errors = []
324 files = _filter_files(_get_affected_files(commit),
325 COMMON_INCLUDED_PATHS,
326 COMMON_EXCLUDED_PATHS)
327
328 for afile in files:
329 for line_num, line in _get_file_diff(afile, commit):
330 # Allow certain lines to exceed the maxlen rule.
Mike Frysingerae409522014-02-01 03:16:11 -0500331 if len(line) <= MAX_LEN or SKIP_REGEXP.search(line):
Jon Salz98255932012-08-18 14:48:02 +0800332 continue
333
334 errors.append('%s, line %s, %s chars' % (afile, line_num, len(line)))
335 if len(errors) == 5: # Just show the first 5 errors.
336 break
Ryan Cuiec4d6332011-05-02 14:15:25 -0700337
338 if errors:
339 msg = 'Found lines longer than %s characters (first 5 shown):' % MAX_LEN
Ryan Cui1562fb82011-05-09 11:01:31 -0700340 return HookFailure(msg, errors)
341
Ryan Cuiec4d6332011-05-02 14:15:25 -0700342
Mike Frysingerae409522014-02-01 03:16:11 -0500343def _check_no_stray_whitespace(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700344 """Checks that there is no stray whitespace at source lines end."""
345 errors = []
346 files = _filter_files(_get_affected_files(commit),
Mike Frysingerae409522014-02-01 03:16:11 -0500347 COMMON_INCLUDED_PATHS,
348 COMMON_EXCLUDED_PATHS)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700349 for afile in files:
350 for line_num, line in _get_file_diff(afile, commit):
351 if line.rstrip() != line:
352 errors.append('%s, line %s' % (afile, line_num))
353 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700354 return HookFailure('Found line ending with white space in:', errors)
355
Ryan Cuiec4d6332011-05-02 14:15:25 -0700356
Mike Frysingerae409522014-02-01 03:16:11 -0500357def _check_no_tabs(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700358 """Checks there are no unexpanded tabs."""
359 TAB_OK_PATHS = [
Ryan Cui31e0c172011-05-04 21:00:45 -0700360 r"/src/third_party/u-boot/",
Ryan Cuiec4d6332011-05-02 14:15:25 -0700361 r".*\.ebuild$",
362 r".*\.eclass$",
Elly Jones5ab34192011-11-15 14:57:06 -0500363 r".*/[M|m]akefile$",
364 r".*\.mk$"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700365 ]
366
367 errors = []
368 files = _filter_files(_get_affected_files(commit),
369 COMMON_INCLUDED_PATHS,
370 COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
371
372 for afile in files:
373 for line_num, line in _get_file_diff(afile, commit):
374 if '\t' in line:
Mike Frysingerae409522014-02-01 03:16:11 -0500375 errors.append('%s, line %s' % (afile, line_num))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700376 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700377 return HookFailure('Found a tab character in:', errors)
378
Ryan Cuiec4d6332011-05-02 14:15:25 -0700379
Mike Frysingerae409522014-02-01 03:16:11 -0500380def _check_change_has_test_field(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700381 """Check for a non-empty 'TEST=' field in the commit message."""
David McMahon8f6553e2011-06-10 15:46:36 -0700382 TEST_RE = r'\nTEST=\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700383
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700384 if not re.search(TEST_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700385 msg = 'Changelist description needs TEST field (after first line)'
386 return HookFailure(msg)
387
Ryan Cuiec4d6332011-05-02 14:15:25 -0700388
Mike Frysingerae409522014-02-01 03:16:11 -0500389def _check_change_has_valid_cq_depend(_project, commit):
David Jamesc3b68b32013-04-03 09:17:03 -0700390 """Check for a correctly formatted CQ-DEPEND field in the commit message."""
391 msg = 'Changelist has invalid CQ-DEPEND target.'
392 example = 'Example: CQ-DEPEND=CL:1234, CL:2345'
393 try:
394 patch.GetPaladinDeps(_get_commit_desc(commit))
395 except ValueError as ex:
396 return HookFailure(msg, [example, str(ex)])
397
398
Mike Frysingerae409522014-02-01 03:16:11 -0500399def _check_change_has_bug_field(_project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700400 """Check for a correctly formatted 'BUG=' field in the commit message."""
David James5c0073d2013-04-03 08:48:52 -0700401 OLD_BUG_RE = r'\nBUG=.*chromium-os'
402 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
403 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
404 'the chromium tracker in your BUG= line now.')
405 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700406
David James5c0073d2013-04-03 08:48:52 -0700407 BUG_RE = r'\nBUG=([Nn]one|(chrome-os-partner|chromium):\d+)'
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700408 if not re.search(BUG_RE, _get_commit_desc(commit)):
David McMahon8f6553e2011-06-10 15:46:36 -0700409 msg = ('Changelist description needs BUG field (after first line):\n'
David James5c0073d2013-04-03 08:48:52 -0700410 'BUG=chromium:9999 (for public tracker)\n'
David McMahon8f6553e2011-06-10 15:46:36 -0700411 'BUG=chrome-os-partner:9999 (for partner tracker)\n'
412 'BUG=None')
Ryan Cui1562fb82011-05-09 11:01:31 -0700413 return HookFailure(msg)
414
Ryan Cuiec4d6332011-05-02 14:15:25 -0700415
Doug Anderson42b8a052013-06-26 10:45:36 -0700416def _check_for_uprev(project, commit):
417 """Check that we're not missing a revbump of an ebuild in the given commit.
418
419 If the given commit touches files in a directory that has ebuilds somewhere
420 up the directory hierarchy, it's very likely that we need an ebuild revbump
421 in order for those changes to take effect.
422
423 It's not totally trivial to detect a revbump, so at least detect that an
424 ebuild with a revision number in it was touched. This should handle the
425 common case where we use a symlink to do the revbump.
426
427 TODO: it would be nice to enhance this hook to:
428 * Handle cases where people revbump with a slightly different syntax. I see
429 one ebuild (puppy) that revbumps with _pN. This is a false positive.
430 * Catches cases where people aren't using symlinks for revbumps. If they
431 edit a revisioned file directly (and are expected to rename it for revbump)
432 we'll miss that. Perhaps we could detect that the file touched is a
433 symlink?
434
435 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
436 still better off than without this check.
437
438 Args:
439 project: The project to look at
440 commit: The commit to look at
441
442 Returns:
443 A HookFailure or None.
444 """
Mike Frysinger011af942014-01-17 16:12:22 -0500445 # If this is the portage-stable overlay, then ignore the check. It's rare
446 # that we're doing anything other than importing files from upstream, so
447 # forcing a rev bump makes no sense.
448 whitelist = (
449 'chromiumos/overlays/portage-stable',
450 )
451 if project in whitelist:
452 return None
453
Doug Anderson42b8a052013-06-26 10:45:36 -0700454 affected_paths = _get_affected_files(commit, include_deletes=True)
455
456 # Don't yell about changes to whitelisted files...
Mike Frysinger011af942014-01-17 16:12:22 -0500457 whitelist = ('ChangeLog', 'Manifest', 'metadata.xml')
Doug Anderson42b8a052013-06-26 10:45:36 -0700458 affected_paths = [path for path in affected_paths
459 if os.path.basename(path) not in whitelist]
460 if not affected_paths:
461 return None
462
463 # If we've touched any file named with a -rN.ebuild then we'll say we're
464 # OK right away. See TODO above about enhancing this.
465 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', path)
466 for path in affected_paths)
467 if touched_revved_ebuild:
468 return None
469
470 # We want to examine the current contents of all directories that are parents
471 # of files that were touched (up to the top of the project).
472 #
473 # ...note: we use the current directory contents even though it may have
474 # changed since the commit we're looking at. This is just a heuristic after
475 # all. Worst case we don't flag a missing revbump.
476 project_top = os.getcwd()
477 dirs_to_check = set([project_top])
478 for path in affected_paths:
479 path = os.path.dirname(path)
480 while os.path.exists(path) and not os.path.samefile(path, project_top):
481 dirs_to_check.add(path)
482 path = os.path.dirname(path)
483
484 # Look through each directory. If it's got an ebuild in it then we'll
485 # consider this as a case when we need a revbump.
486 for dir_path in dirs_to_check:
487 contents = os.listdir(dir_path)
488 ebuilds = [os.path.join(dir_path, path)
489 for path in contents if path.endswith('.ebuild')]
490 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
491
492 # If the -9999.ebuild file was touched the bot will uprev for us.
493 # ...we'll use a simple intersection here as a heuristic...
494 if set(ebuilds_9999) & set(affected_paths):
495 continue
496
497 if ebuilds:
498 return HookFailure('Changelist probably needs a revbump of an ebuild\n'
499 'or a -r1.ebuild symlink if this is a new ebuild')
500
501 return None
502
503
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500504def _check_ebuild_eapi(project, commit):
505 """Make sure we have people use EAPI=4 or newer with custom ebuilds.
506
507 We want to get away from older EAPI's as it makes life confusing and they
508 have less builtin error checking.
509
510 Args:
511 project: The project to look at
512 commit: The commit to look at
513
514 Returns:
515 A HookFailure or None.
516 """
517 # If this is the portage-stable overlay, then ignore the check. It's rare
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500518 # that we're doing anything other than importing files from upstream, and
519 # we shouldn't be rewriting things fundamentally anyways.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500520 whitelist = (
521 'chromiumos/overlays/portage-stable',
522 )
523 if project in whitelist:
524 return None
525
526 BAD_EAPIS = ('0', '1', '2', '3')
527
528 get_eapi = re.compile(r'^\s*EAPI=[\'"]?([^\'"]+)')
529
530 ebuilds_re = [r'\.ebuild$']
531 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
532 ebuilds_re)
533 bad_ebuilds = []
534
535 for ebuild in ebuilds:
536 # If the ebuild does not specify an EAPI, it defaults to 0.
537 eapi = '0'
538
539 lines = _get_file_content(ebuild, commit).splitlines()
540 if len(lines) == 1:
541 # This is most likely a symlink, so skip it entirely.
542 continue
543
544 for line in lines:
545 m = get_eapi.match(line)
546 if m:
547 # Once we hit the first EAPI line in this ebuild, stop processing.
548 # The spec requires that there only be one and it be first, so
549 # checking all possible values is pointless. We also assume that
550 # it's "the" EAPI line and not something in the middle of a heredoc.
551 eapi = m.group(1)
552 break
553
554 if eapi in BAD_EAPIS:
555 bad_ebuilds.append((ebuild, eapi))
556
557 if bad_ebuilds:
558 # pylint: disable=C0301
559 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/upgrade-ebuild-eapis'
560 # pylint: enable=C0301
561 return HookFailure(
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500562 'These ebuilds are using old EAPIs. If these are imported from\n'
563 'Gentoo, then you may ignore and upload once with the --no-verify\n'
564 'flag. Otherwise, please update to 4 or newer.\n'
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500565 '\t%s\n'
566 'See this guide for more details:\n%s\n' %
567 ('\n\t'.join(['%s: EAPI=%s' % x for x in bad_ebuilds]), url))
568
569
Mike Frysinger89bdb852014-02-01 05:26:26 -0500570def _check_ebuild_keywords(_project, commit):
Mike Frysingerc51ece72014-01-17 16:23:40 -0500571 """Make sure we use the new style KEYWORDS when possible in ebuilds.
572
573 If an ebuild generally does not care about the arch it is running on, then
574 ebuilds should flag it with one of:
575 KEYWORDS="*" # A stable ebuild.
576 KEYWORDS="~*" # An unstable ebuild.
577 KEYWORDS="-* ..." # Is known to only work on specific arches.
578
579 Args:
580 project: The project to look at
581 commit: The commit to look at
582
583 Returns:
584 A HookFailure or None.
585 """
586 WHITELIST = set(('*', '-*', '~*'))
587
588 get_keywords = re.compile(r'^\s*KEYWORDS="(.*)"')
589
Mike Frysinger89bdb852014-02-01 05:26:26 -0500590 ebuilds_re = [r'\.ebuild$']
591 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
592 ebuilds_re)
593
Mike Frysingerc51ece72014-01-17 16:23:40 -0500594 for ebuild in ebuilds:
595 for _, line in _get_file_diff(ebuild, commit):
596 m = get_keywords.match(line)
597 if m:
598 keywords = set(m.group(1).split())
599 if not keywords or WHITELIST - keywords != WHITELIST:
600 continue
601
602 return HookFailure(
603 'Please update KEYWORDS to use a glob:\n'
604 'If the ebuild should be marked stable (normal for non-9999 '
605 'ebuilds):\n'
606 ' KEYWORDS="*"\n'
607 'If the ebuild should be marked unstable (normal for '
608 'cros-workon / 9999 ebuilds):\n'
609 ' KEYWORDS="~*"\n'
610 'If the ebuild needs to be marked for only specific arches,'
611 'then use -* like so:\n'
612 ' KEYWORDS="-* arm ..."\n')
613
614
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800615def _check_ebuild_licenses(_project, commit):
616 """Check if the LICENSE field in the ebuild is correct."""
617 affected_paths = _get_affected_files(commit)
618 touched_ebuilds = [x for x in affected_paths if x.endswith('.ebuild')]
619
620 # A list of licenses to ignore for now.
Yu-Ju Hongc0963fa2014-03-03 12:36:52 -0800621 LICENSES_IGNORE = ['||', '(', ')']
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800622
623 for ebuild in touched_ebuilds:
624 # Skip virutal packages.
625 if ebuild.split('/')[-3] == 'virtual':
626 continue
627
628 try:
Mike Frysinger2ec70ed2014-08-17 19:28:34 -0400629 license_types = licenses_lib.GetLicenseTypesFromEbuild(ebuild)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800630 except ValueError as e:
631 return HookFailure(e.message, [ebuild])
632
633 # Also ignore licenses ending with '?'
634 for license_type in [x for x in license_types
635 if x not in LICENSES_IGNORE and not x.endswith('?')]:
636 try:
Mike Frysinger2ec70ed2014-08-17 19:28:34 -0400637 licenses_lib.Licensing.FindLicenseType(license_type)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800638 except AssertionError as e:
639 return HookFailure(e.message, [ebuild])
640
641
Mike Frysingercd363c82014-02-01 05:20:18 -0500642def _check_ebuild_virtual_pv(project, commit):
643 """Enforce the virtual PV policies."""
644 # If this is the portage-stable overlay, then ignore the check.
645 # We want to import virtuals as-is from upstream Gentoo.
646 whitelist = (
647 'chromiumos/overlays/portage-stable',
648 )
649 if project in whitelist:
650 return None
651
652 # We assume the repo name is the same as the dir name on disk.
653 # It would be dumb to not have them match though.
654 project = os.path.basename(project)
655
656 is_variant = lambda x: x.startswith('overlay-variant-')
657 is_board = lambda x: x.startswith('overlay-')
658 is_private = lambda x: x.endswith('-private')
659
660 get_pv = re.compile(r'(.*?)virtual/([^/]+)/\2-([^/]*)\.ebuild$')
661
662 ebuilds_re = [r'\.ebuild$']
663 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
664 ebuilds_re)
665 bad_ebuilds = []
666
667 for ebuild in ebuilds:
668 m = get_pv.match(ebuild)
669 if m:
670 overlay = m.group(1)
671 if not overlay or not is_board(overlay):
672 overlay = project
673
674 pv = m.group(3).split('-', 1)[0]
675
676 if is_private(overlay):
677 want_pv = '3.5' if is_variant(overlay) else '3'
678 elif is_board(overlay):
679 want_pv = '2.5' if is_variant(overlay) else '2'
680 else:
681 want_pv = '1'
682
683 if pv != want_pv:
684 bad_ebuilds.append((ebuild, pv, want_pv))
685
686 if bad_ebuilds:
687 # pylint: disable=C0301
688 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/portage-build-faq#TOC-Virtuals-and-central-management'
689 # pylint: enable=C0301
690 return HookFailure(
691 'These virtuals have incorrect package versions (PVs). Please adjust:\n'
692 '\t%s\n'
693 'If this is an upstream Gentoo virtual, then you may ignore this\n'
694 'check (and re-run w/--no-verify). Otherwise, please see this\n'
695 'page for more details:\n%s\n' %
696 ('\n\t'.join(['%s:\n\t\tPV is %s but should be %s' % x
697 for x in bad_ebuilds]), url))
698
699
Mike Frysingerae409522014-02-01 03:16:11 -0500700def _check_change_has_proper_changeid(_project, commit):
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700701 """Verify that Change-ID is present in last paragraph of commit message."""
702 desc = _get_commit_desc(commit)
703 loc = desc.rfind('\nChange-Id:')
704 if loc == -1 or re.search('\n\s*\n\s*\S+', desc[loc:]):
Ryan Cui1562fb82011-05-09 11:01:31 -0700705 return HookFailure('Change-Id must be in last paragraph of description.')
706
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700707
Mike Frysingerae409522014-02-01 03:16:11 -0500708def _check_license(_project, commit):
Mike Frysinger98638102014-08-28 00:15:08 -0400709 """Verifies the license/copyright header.
Ryan Cuiec4d6332011-05-02 14:15:25 -0700710
Mike Frysinger98638102014-08-28 00:15:08 -0400711 Should be following the spec:
712 http://dev.chromium.org/developers/coding-style#TOC-File-headers
713 """
714 # For older years, be a bit more flexible as our policy says leave them be.
715 LICENSE_HEADER = (
716 r'.* Copyright( \(c\))? 20[-0-9]{2,7} The Chromium OS Authors\. '
717 'All rights reserved\.' '\n'
718 r'.* Use of this source code is governed by a BSD-style license that can '
719 'be\n'
720 r'.* found in the LICENSE file\.'
721 '\n'
722 )
723 license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
724
725 # For newer years, be stricter.
726 COPYRIGHT_LINE = (
727 r'.* Copyright \(c\) 20(1[5-9]|[2-9][0-9]) The Chromium OS Authors\. '
728 'All rights reserved\.' '\n'
729 )
730 copyright_re = re.compile(COPYRIGHT_LINE)
731
732 bad_files = []
733 bad_copyright_files = []
734 files = _filter_files(_get_affected_files(commit, relative=True),
735 COMMON_INCLUDED_PATHS,
736 COMMON_EXCLUDED_PATHS)
737
738 for f in files:
739 contents = _get_file_content(f, commit)
740 if not contents:
741 # Ignore empty files.
742 continue
743
744 if not license_re.search(contents):
745 bad_files.append(f)
746 elif copyright_re.search(contents):
747 bad_copyright_files.append(f)
748
749 if bad_files:
750 msg = '%s:\n%s\n%s' % (
751 'License must match', license_re.pattern,
752 'Found a bad header in these files:')
753 return HookFailure(msg, bad_files)
754
755 if bad_copyright_files:
756 msg = 'Do not use (c) in copyright headers in new files:'
757 return HookFailure(msg, bad_copyright_files)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700758
759
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400760def _check_layout_conf(_project, commit):
761 """Verifies the metadata/layout.conf file."""
762 repo_name = 'profiles/repo_name'
763 layout_path = 'metadata/layout.conf'
764
765 files = _get_affected_files(commit, relative=True)
766
767 # Disallow new repos with the repo_name file.
768 if repo_name in files:
769 return HookFailure('%s: use "repo-name" in %s instead' %
770 (repo_name, layout_path))
771
772 # If the layout.conf file doesn't exist, nothing else to do.
773 if layout_path not in files:
774 return
775
776 errors = []
777
778 # Make sure the config file is sorted.
779 data = [x for x in _get_file_content(layout_path, commit).splitlines()
780 if x and x[0] != '#']
781 if sorted(data) != data:
782 errors += ['keep lines sorted']
783
784 # Require people to set specific values all the time.
785 settings = (
786 # TODO: Enable this for everyone. http://crbug.com/408038
787 #('fast caching', 'cache-format = md5-dict'),
788 ('fast manifests', 'thin-manifests = true'),
789 ('extra features', 'profile-formats = portage-2'),
790 )
791 for reason, line in settings:
792 if line not in data:
793 errors += ['enable %s with: %s' % (reason, line)]
794
795 # Require one of these settings.
796 if ('use-manifests = true' not in data and
797 'use-manifests = strict' not in data):
798 errors += ['enable file checking with: use-manifests = true']
799
800 if errors:
801 lines = [('%s: error(s) detected '
802 '(see the portage(5) man page for more details)') %
803 layout_path] + errors
804 return HookFailure('\n\t- '.join(lines))
805
806
Ryan Cuiec4d6332011-05-02 14:15:25 -0700807# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700808
Ryan Cui1562fb82011-05-09 11:01:31 -0700809
Mike Frysingerae409522014-02-01 03:16:11 -0500810def _run_checkpatch(_project, commit, options=()):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700811 """Runs checkpatch.pl on the given project"""
812 hooks_dir = _get_hooks_dir()
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700813 options = list(options)
814 if commit == PRE_SUBMIT:
815 # The --ignore option must be present and include 'MISSING_SIGN_OFF' in
816 # this case.
817 options.append('--ignore=MISSING_SIGN_OFF')
818 cmd = ['%s/checkpatch.pl' % hooks_dir] + options + ['-']
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700819 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700820 output = p.communicate(_get_patch(commit))[0]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700821 if p.returncode:
Ryan Cui1562fb82011-05-09 11:01:31 -0700822 return HookFailure('checkpatch.pl errors/warnings\n\n' + output)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700823
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700824
Anton Staaf815d6852011-08-22 10:08:45 -0700825def _run_checkpatch_no_tree(project, commit):
826 return _run_checkpatch(project, commit, ['--no-tree'])
827
Mike Frysingerae409522014-02-01 03:16:11 -0500828
Randall Spangler7318fd62013-11-21 12:16:58 -0800829def _run_checkpatch_ec(project, commit):
830 """Runs checkpatch with options for Chromium EC projects."""
831 return _run_checkpatch(project, commit, ['--no-tree',
832 '--ignore=MSLEEP,VOLATILE'])
833
Mike Frysingerae409522014-02-01 03:16:11 -0500834
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700835def _run_checkpatch_depthcharge(project, commit):
836 """Runs checkpatch with options for depthcharge."""
837 return _run_checkpatch(project, commit, [
838 '--no-tree',
Julius Werner83a35bb2014-06-23 12:51:48 -0700839 '--ignore=CAMELCASE,C99_COMMENTS,NEW_TYPEDEFS,CONFIG_DESCRIPTION,'
840 'SPACING,PREFER_PACKED,PREFER_PRINTF,PREFER_ALIGNED,GLOBAL_INITIALISERS,'
841 'INITIALISED_STATIC,OPEN_BRACE,TRAILING_STATEMENTS'])
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700842
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -0700843def _run_checkpatch_coreboot(project, commit):
844 """Runs checkpatch with options for coreboot."""
845 return _run_checkpatch(project, commit, [
846 '--no-tree',
847 '--ignore=NEW_TYPEDEFS,PREFER_PACKED,PREFER_PRINTF,PREFER_ALIGNED,'
848 'GLOBAL_INITIALISERS,INITIALISED_STATIC'])
849
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700850
Mike Frysingerae409522014-02-01 03:16:11 -0500851def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -0700852 """Makes sure kernel config changes are not mixed with code changes"""
853 files = _get_affected_files(commit)
854 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
855 return HookFailure('Changes to chromeos/config/ and regular files must '
856 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -0700857
Mike Frysingerae409522014-02-01 03:16:11 -0500858
859def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -0700860 """Checks that all JSON files are syntactically valid."""
Dale Curtisa039cfd2011-05-04 12:01:05 -0700861 for f in _filter_files(_get_affected_files(commit), [r'.*\.json']):
Dale Curtis2975c432011-05-03 17:25:20 -0700862 try:
863 json.load(open(f))
864 except Exception, e:
Ryan Cui1562fb82011-05-09 11:01:31 -0700865 return HookFailure('Invalid JSON in %s: %s' % (f, e))
Dale Curtis2975c432011-05-03 17:25:20 -0700866
867
Mike Frysingerae409522014-02-01 03:16:11 -0500868def _check_manifests(_project, commit):
Mike Frysinger52b537e2013-08-22 22:59:53 -0400869 """Make sure Manifest files only have DIST lines"""
870 paths = []
871
872 for path in _get_affected_files(commit):
873 if os.path.basename(path) != 'Manifest':
874 continue
875 if not os.path.exists(path):
876 continue
877
878 with open(path, 'r') as f:
879 for line in f.readlines():
880 if not line.startswith('DIST '):
881 paths.append(path)
882 break
883
884 if paths:
885 return HookFailure('Please remove lines that do not start with DIST:\n%s' %
886 ('\n'.join(paths),))
887
888
Mike Frysingerae409522014-02-01 03:16:11 -0500889def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700890 """Check for a non-empty 'BRANCH=' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700891 if commit == PRE_SUBMIT:
892 return
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700893 BRANCH_RE = r'\nBRANCH=\S+'
894
895 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
896 msg = ('Changelist description needs BRANCH field (after first line)\n'
897 'E.g. BRANCH=none or BRANCH=link,snow')
898 return HookFailure(msg)
899
900
Mike Frysingerae409522014-02-01 03:16:11 -0500901def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800902 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700903 if commit == PRE_SUBMIT:
904 return
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800905 SIGNOFF_RE = r'\nSigned-off-by: \S+'
906
907 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
908 msg = ('Changelist description needs Signed-off-by: field\n'
909 'E.g. Signed-off-by: My Name <me@chromium.org>')
910 return HookFailure(msg)
911
912
Jon Salz3ee59de2012-08-18 13:54:22 +0800913def _run_project_hook_script(script, project, commit):
914 """Runs a project hook script.
915
916 The script is run with the following environment variables set:
917 PRESUBMIT_PROJECT: The affected project
918 PRESUBMIT_COMMIT: The affected commit
919 PRESUBMIT_FILES: A newline-separated list of affected files
920
921 The script is considered to fail if the exit code is non-zero. It should
922 write an error message to stdout.
923 """
924 env = dict(os.environ)
925 env['PRESUBMIT_PROJECT'] = project
926 env['PRESUBMIT_COMMIT'] = commit
927
928 # Put affected files in an environment variable
929 files = _get_affected_files(commit)
930 env['PRESUBMIT_FILES'] = '\n'.join(files)
931
932 process = subprocess.Popen(script, env=env, shell=True,
933 stdin=open(os.devnull),
Jon Salz7b618af2012-08-31 06:03:16 +0800934 stdout=subprocess.PIPE,
935 stderr=subprocess.STDOUT)
Jon Salz3ee59de2012-08-18 13:54:22 +0800936 stdout, _ = process.communicate()
937 if process.wait():
Jon Salz7b618af2012-08-31 06:03:16 +0800938 if stdout:
939 stdout = re.sub('(?m)^', ' ', stdout)
940 return HookFailure('Hook script "%s" failed with code %d%s' %
Jon Salz3ee59de2012-08-18 13:54:22 +0800941 (script, process.returncode,
942 ':\n' + stdout if stdout else ''))
943
944
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -0700945def _moved_to_platform2(project, _commit):
946 """Forbids commits to legacy repo in src/platform."""
947 return HookFailure('%s has been moved to platform2. This change should be '
948 'made there.' % project)
949
950
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -0700951def _check_project_prefix(_project, commit):
952 """Fails if the change is project specific and the commit message is not
953 prefixed by the project_name.
954 """
955
956 files = _get_affected_files(commit, relative=True)
957 prefix = os.path.commonprefix(files)
958 prefix = os.path.dirname(prefix)
959
960 # If there is no common prefix, the CL span multiple projects.
961 if prefix == '':
962 return
963
964 project_name = prefix.split('/')[0]
965 alias_file = os.path.join(prefix, '.project_alias')
966 # If an alias exists, use it.
967 if os.path.isfile(alias_file):
968 with open(alias_file, 'r') as f:
969 project_name = f.read().strip()
970
971 if not _get_commit_desc(commit).startswith(project_name + ': '):
972 return HookFailure('The commit title for changes affecting only %s'
973 ' should start with \"%s: \"'
974 % (project_name, project_name))
975
976
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700977# Base
978
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700979# A list of hooks which are not project specific and check patch description
980# (as opposed to patch body).
981_PATCH_DESCRIPTION_HOOKS = [
Ryan Cui9b651632011-05-11 11:38:58 -0700982 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -0700983 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -0700984 _check_change_has_test_field,
985 _check_change_has_proper_changeid,
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700986]
987
988
989# A list of hooks that are not project-specific
990_COMMON_HOOKS = [
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500991 _check_ebuild_eapi,
Mike Frysinger89bdb852014-02-01 05:26:26 -0500992 _check_ebuild_keywords,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800993 _check_ebuild_licenses,
Mike Frysingercd363c82014-02-01 05:20:18 -0500994 _check_ebuild_virtual_pv,
Ryan Cui9b651632011-05-11 11:38:58 -0700995 _check_no_stray_whitespace,
996 _check_no_long_lines,
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400997 _check_layout_conf,
Ryan Cui9b651632011-05-11 11:38:58 -0700998 _check_license,
999 _check_no_tabs,
Doug Anderson42b8a052013-06-26 10:45:36 -07001000 _check_for_uprev,
Ryan Cui9b651632011-05-11 11:38:58 -07001001]
Ryan Cuiec4d6332011-05-02 14:15:25 -07001002
Ryan Cui1562fb82011-05-09 11:01:31 -07001003
Ryan Cui9b651632011-05-11 11:38:58 -07001004# A dictionary of project-specific hooks(callbacks), indexed by project name.
1005# dict[project] = [callback1, callback2]
1006_PROJECT_SPECIFIC_HOOKS = {
Mike Frysingerdf980702013-08-22 22:25:22 -04001007 "chromeos/autotest-tools": [_run_json_check],
Mike Frysinger52b537e2013-08-22 22:59:53 -04001008 "chromeos/overlays/chromeos-overlay": [_check_manifests],
1009 "chromeos/overlays/chromeos-partner-overlay": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -08001010 "chromeos/platform/ec-private": [_run_checkpatch_ec,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001011 _check_change_has_branch_field],
Puneet Kumar57b9c092012-08-14 18:58:29 -07001012 "chromeos/third_party/intel-framework": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001013 "chromeos/vendor/kernel-exynos-staging": [_run_checkpatch,
1014 _kernel_configcheck],
Mike Frysinger52b537e2013-08-22 22:59:53 -04001015 "chromiumos/overlays/board-overlays": [_check_manifests],
1016 "chromiumos/overlays/chromiumos-overlay": [_check_manifests],
1017 "chromiumos/overlays/portage-stable": [_check_manifests],
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001018 "chromiumos/platform2": [_check_project_prefix],
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -07001019 "chromiumos/platform/depthcharge": [_check_change_has_signoff_field,
1020 _run_checkpatch_depthcharge],
Randall Spangler7318fd62013-11-21 12:16:58 -08001021 "chromiumos/platform/ec": [_run_checkpatch_ec,
Mike Frysingerdf980702013-08-22 22:25:22 -04001022 _check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001023 "chromiumos/platform/mosys": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001024 "chromiumos/platform/vboot_reference": [_check_change_has_branch_field],
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -07001025 "chromiumos/third_party/coreboot": [_check_change_has_signoff_field,
1026 _run_checkpatch_coreboot],
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001027 "chromiumos/third_party/flashrom": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001028 "chromiumos/third_party/kernel": [_run_checkpatch, _kernel_configcheck],
1029 "chromiumos/third_party/kernel-next": [_run_checkpatch,
1030 _kernel_configcheck],
Vadim Bendebury203fbc22014-04-22 11:58:14 -07001031 "chromiumos/third_party/u-boot": [_run_checkpatch_no_tree],
Ryan Cui9b651632011-05-11 11:38:58 -07001032}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001033
Ryan Cui1562fb82011-05-09 11:01:31 -07001034
Ryan Cui9b651632011-05-11 11:38:58 -07001035# A dictionary of flags (keys) that can appear in the config file, and the hook
1036# that the flag disables (value)
1037_DISABLE_FLAGS = {
1038 'stray_whitespace_check': _check_no_stray_whitespace,
1039 'long_line_check': _check_no_long_lines,
1040 'cros_license_check': _check_license,
1041 'tab_check': _check_no_tabs,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001042 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001043 'signoff_check': _check_change_has_signoff_field,
Josh Triplett0e8fc7f2014-04-23 16:00:00 -07001044 'bug_field_check': _check_change_has_bug_field,
1045 'test_field_check': _check_change_has_test_field,
Ryan Cui9b651632011-05-11 11:38:58 -07001046}
1047
1048
Jon Salz3ee59de2012-08-18 13:54:22 +08001049def _get_disabled_hooks(config):
Ryan Cui9b651632011-05-11 11:38:58 -07001050 """Returns a set of hooks disabled by the current project's config file.
1051
1052 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +08001053
1054 Args:
1055 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07001056 """
1057 SECTION = 'Hook Overrides'
Jon Salz3ee59de2012-08-18 13:54:22 +08001058 if not config.has_section(SECTION):
1059 return set()
Ryan Cui9b651632011-05-11 11:38:58 -07001060
1061 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +08001062 for flag in config.options(SECTION):
Ryan Cui9b651632011-05-11 11:38:58 -07001063 try:
Mike Frysingerae409522014-02-01 03:16:11 -05001064 if not config.getboolean(SECTION, flag):
1065 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -07001066 except ValueError as e:
1067 msg = "Error parsing flag \'%s\' in %s file - " % (flag, _CONFIG_FILE)
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001068 print(msg + str(e))
Ryan Cui9b651632011-05-11 11:38:58 -07001069
1070 disabled_keys = set(_DISABLE_FLAGS.iterkeys()).intersection(disable_flags)
1071 return set([_DISABLE_FLAGS[key] for key in disabled_keys])
1072
1073
Jon Salz3ee59de2012-08-18 13:54:22 +08001074def _get_project_hook_scripts(config):
1075 """Returns a list of project-specific hook scripts.
1076
1077 Args:
1078 config: A ConfigParser for the project's config file.
1079 """
1080 SECTION = 'Hook Scripts'
1081 if not config.has_section(SECTION):
1082 return []
1083
1084 hook_names_values = config.items(SECTION)
1085 hook_names_values.sort(key=lambda x: x[0])
1086 return [x[1] for x in hook_names_values]
1087
1088
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001089def _get_project_hooks(project, presubmit):
Ryan Cui9b651632011-05-11 11:38:58 -07001090 """Returns a list of hooks that need to be run for a project.
1091
1092 Expects to be called from within the project root.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001093
1094 Args:
1095 project: A string, name of the project.
1096 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui9b651632011-05-11 11:38:58 -07001097 """
Jon Salz3ee59de2012-08-18 13:54:22 +08001098 config = ConfigParser.RawConfigParser()
1099 try:
1100 config.read(_CONFIG_FILE)
1101 except ConfigParser.Error:
1102 # Just use an empty config file
1103 config = ConfigParser.RawConfigParser()
1104
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001105 if presubmit:
1106 hook_list = _COMMON_HOOKS
1107 else:
1108 hook_list = _PATCH_DESCRIPTION_HOOKS + _COMMON_HOOKS
1109
Jon Salz3ee59de2012-08-18 13:54:22 +08001110 disabled_hooks = _get_disabled_hooks(config)
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001111 hooks = [hook for hook in hook_list if hook not in disabled_hooks]
Ryan Cui9b651632011-05-11 11:38:58 -07001112
1113 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001114 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
1115 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -07001116
Jon Salz3ee59de2012-08-18 13:54:22 +08001117 for script in _get_project_hook_scripts(config):
1118 hooks.append(functools.partial(_run_project_hook_script, script))
1119
Ryan Cui9b651632011-05-11 11:38:58 -07001120 return hooks
1121
1122
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001123def _run_project_hooks(project, proj_dir=None,
1124 commit_list=None, presubmit=False):
Ryan Cui1562fb82011-05-09 11:01:31 -07001125 """For each project run its project specific hook from the hooks dictionary.
1126
1127 Args:
Doug Anderson44a644f2011-11-02 10:37:37 -07001128 project: The name of project to run hooks for.
1129 proj_dir: If non-None, this is the directory the project is in. If None,
1130 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -07001131 commit_list: A list of commits to run hooks against. If None or empty list
1132 then we'll automatically get the list of commits that would be uploaded.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001133 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui1562fb82011-05-09 11:01:31 -07001134
1135 Returns:
1136 Boolean value of whether any errors were ecountered while running the hooks.
1137 """
Doug Anderson44a644f2011-11-02 10:37:37 -07001138 if proj_dir is None:
David James2edd9002013-10-11 14:09:19 -07001139 proj_dirs = _run_command(['repo', 'forall', project, '-c', 'pwd']).split()
1140 if len(proj_dirs) == 0:
1141 print('%s cannot be found.' % project, file=sys.stderr)
1142 print('Please specify a valid project.', file=sys.stderr)
1143 return True
1144 if len(proj_dirs) > 1:
1145 print('%s is associated with multiple directories.' % project,
1146 file=sys.stderr)
1147 print('Please specify a directory to help disambiguate.', file=sys.stderr)
1148 return True
1149 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -07001150
Ryan Cuiec4d6332011-05-02 14:15:25 -07001151 pwd = os.getcwd()
1152 # hooks assume they are run from the root of the project
1153 os.chdir(proj_dir)
1154
Doug Anderson14749562013-06-26 13:38:29 -07001155 if not commit_list:
1156 try:
1157 commit_list = _get_commits()
1158 except VerifyException as e:
1159 PrintErrorForProject(project, HookFailure(str(e)))
1160 os.chdir(pwd)
1161 return True
Ryan Cuifa55df52011-05-06 11:16:55 -07001162
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001163 hooks = _get_project_hooks(project, presubmit)
Ryan Cui1562fb82011-05-09 11:01:31 -07001164 error_found = False
Ryan Cuifa55df52011-05-06 11:16:55 -07001165 for commit in commit_list:
Ryan Cui1562fb82011-05-09 11:01:31 -07001166 error_list = []
Ryan Cui9b651632011-05-11 11:38:58 -07001167 for hook in hooks:
Ryan Cui1562fb82011-05-09 11:01:31 -07001168 hook_error = hook(project, commit)
1169 if hook_error:
1170 error_list.append(hook_error)
1171 error_found = True
1172 if error_list:
1173 PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
1174 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -07001175
Ryan Cuiec4d6332011-05-02 14:15:25 -07001176 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -07001177 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001178
Mike Frysingerae409522014-02-01 03:16:11 -05001179
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001180# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001181
Ryan Cui1562fb82011-05-09 11:01:31 -07001182
Mike Frysingerae409522014-02-01 03:16:11 -05001183def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -08001184 """Main function invoked directly by repo.
1185
1186 This function will exit directly upon error so that repo doesn't print some
1187 obscure error message.
1188
1189 Args:
1190 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -07001191 worktree_list: A list of directories. It should be the same length as
1192 project_list, so that each entry in project_list matches with a directory
1193 in worktree_list. If None, we will attempt to calculate the directories
1194 automatically.
Doug Anderson06456632012-01-05 11:02:14 -08001195 kwargs: Leave this here for forward-compatibility.
1196 """
Ryan Cui1562fb82011-05-09 11:01:31 -07001197 found_error = False
David James2edd9002013-10-11 14:09:19 -07001198 if not worktree_list:
1199 worktree_list = [None] * len(project_list)
1200 for project, worktree in zip(project_list, worktree_list):
1201 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -07001202 found_error = True
1203
Mike Frysingerae409522014-02-01 03:16:11 -05001204 if found_error:
Ryan Cui1562fb82011-05-09 11:01:31 -07001205 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -07001206 '- To disable some source style checks, and for other hints, see '
1207 '<checkout_dir>/src/repohooks/README\n'
1208 '- To upload only current project, run \'repo upload .\'')
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001209 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -07001210 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -07001211
Ryan Cui1562fb82011-05-09 11:01:31 -07001212
Doug Anderson44a644f2011-11-02 10:37:37 -07001213def _identify_project(path):
1214 """Identify the repo project associated with the given path.
1215
1216 Returns:
1217 A string indicating what project is associated with the path passed in or
1218 a blank string upon failure.
1219 """
1220 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
1221 stderr=subprocess.PIPE, cwd=path).strip()
1222
1223
1224def direct_main(args, verbose=False):
1225 """Run hooks directly (outside of the context of repo).
1226
1227 # Setup for doctests below.
1228 # ...note that some tests assume that running pre-upload on this CWD is fine.
1229 # TODO: Use mock and actually mock out _run_project_hooks() for tests.
1230 >>> mydir = os.path.dirname(os.path.abspath(__file__))
1231 >>> olddir = os.getcwd()
1232
1233 # OK to run w/ no arugments; will run with CWD.
1234 >>> os.chdir(mydir)
1235 >>> direct_main(['prog_name'], verbose=True)
1236 Running hooks on chromiumos/repohooks
1237 0
1238 >>> os.chdir(olddir)
1239
1240 # Run specifying a dir
1241 >>> direct_main(['prog_name', '--dir=%s' % mydir], verbose=True)
1242 Running hooks on chromiumos/repohooks
1243 0
1244
1245 # Not a problem to use a bogus project; we'll just get default settings.
1246 >>> direct_main(['prog_name', '--dir=%s' % mydir, '--project=X'],verbose=True)
1247 Running hooks on X
1248 0
1249
1250 # Run with project but no dir
1251 >>> os.chdir(mydir)
1252 >>> direct_main(['prog_name', '--project=X'], verbose=True)
1253 Running hooks on X
1254 0
1255 >>> os.chdir(olddir)
1256
1257 # Try with a non-git CWD
1258 >>> os.chdir('/tmp')
1259 >>> direct_main(['prog_name'])
1260 Traceback (most recent call last):
1261 ...
1262 BadInvocation: The current directory is not part of a git project.
1263
1264 # Check various bad arguments...
1265 >>> direct_main(['prog_name', 'bogus'])
1266 Traceback (most recent call last):
1267 ...
1268 BadInvocation: Unexpected arguments: bogus
1269 >>> direct_main(['prog_name', '--project=bogus', '--dir=bogusdir'])
1270 Traceback (most recent call last):
1271 ...
1272 BadInvocation: Invalid dir: bogusdir
1273 >>> direct_main(['prog_name', '--project=bogus', '--dir=/tmp'])
1274 Traceback (most recent call last):
1275 ...
1276 BadInvocation: Not a git directory: /tmp
1277
1278 Args:
1279 args: The value of sys.argv
Mike Frysingerae409522014-02-01 03:16:11 -05001280 verbose: Log verbose info while running
Doug Anderson44a644f2011-11-02 10:37:37 -07001281
1282 Returns:
1283 0 if no pre-upload failures, 1 if failures.
1284
1285 Raises:
1286 BadInvocation: On some types of invocation errors.
1287 """
1288 desc = 'Run Chromium OS pre-upload hooks on changes compared to upstream.'
1289 parser = optparse.OptionParser(description=desc)
1290
1291 parser.add_option('--dir', default=None,
1292 help='The directory that the project lives in. If not '
1293 'specified, use the git project root based on the cwd.')
1294 parser.add_option('--project', default=None,
1295 help='The project repo path; this can affect how the hooks '
1296 'get run, since some hooks are project-specific. For '
1297 'chromite this is chromiumos/chromite. If not specified, '
1298 'the repo tool will be used to figure this out based on '
1299 'the dir.')
Doug Anderson14749562013-06-26 13:38:29 -07001300 parser.add_option('--rerun-since', default=None,
1301 help='Rerun hooks on old commits since the given date. '
1302 'The date should match git log\'s concept of a date. '
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001303 'e.g. 2012-06-20. This option is mutually exclusive '
1304 'with --pre-submit.')
1305 parser.add_option('--pre-submit', action="store_true",
1306 help='Run the check against the pending commit. '
1307 'This option should be used at the \'git commit\' '
1308 'phase as opposed to \'repo upload\'. This option '
1309 'is mutually exclusive with --rerun-since.')
Doug Anderson14749562013-06-26 13:38:29 -07001310
1311 parser.usage = "pre-upload.py [options] [commits]"
Doug Anderson44a644f2011-11-02 10:37:37 -07001312
1313 opts, args = parser.parse_args(args[1:])
1314
Doug Anderson14749562013-06-26 13:38:29 -07001315 if opts.rerun_since:
1316 if args:
1317 raise BadInvocation('Can\'t pass commits and use rerun-since: %s' %
1318 ' '.join(args))
1319
1320 cmd = ['git', 'log', '--since="%s"' % opts.rerun_since, '--pretty=%H']
1321 all_commits = _run_command(cmd).splitlines()
1322 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
1323
1324 # Eliminate chrome-bot commits but keep ordering the same...
1325 bot_commits = set(bot_commits)
1326 args = [c for c in all_commits if c not in bot_commits]
1327
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001328 if opts.pre_submit:
1329 raise BadInvocation('rerun-since and pre-submit can not be '
1330 'used together')
1331 if opts.pre_submit:
1332 if args:
1333 raise BadInvocation('Can\'t pass commits and use pre-submit: %s' %
1334 ' '.join(args))
1335 args = [PRE_SUBMIT,]
Doug Anderson44a644f2011-11-02 10:37:37 -07001336
1337 # Check/normlaize git dir; if unspecified, we'll use the root of the git
1338 # project from CWD
1339 if opts.dir is None:
1340 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
1341 stderr=subprocess.PIPE).strip()
1342 if not git_dir:
1343 raise BadInvocation('The current directory is not part of a git project.')
1344 opts.dir = os.path.dirname(os.path.abspath(git_dir))
1345 elif not os.path.isdir(opts.dir):
1346 raise BadInvocation('Invalid dir: %s' % opts.dir)
1347 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
1348 raise BadInvocation('Not a git directory: %s' % opts.dir)
1349
1350 # Identify the project if it wasn't specified; this _requires_ the repo
1351 # tool to be installed and for the project to be part of a repo checkout.
1352 if not opts.project:
1353 opts.project = _identify_project(opts.dir)
1354 if not opts.project:
1355 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
1356
1357 if verbose:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001358 print("Running hooks on %s" % (opts.project))
Doug Anderson44a644f2011-11-02 10:37:37 -07001359
Doug Anderson14749562013-06-26 13:38:29 -07001360 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001361 commit_list=args,
1362 presubmit=opts.pre_submit)
Doug Anderson44a644f2011-11-02 10:37:37 -07001363 if found_error:
1364 return 1
1365 return 0
1366
1367
1368def _test():
1369 """Run any built-in tests."""
1370 import doctest
1371 doctest.testmod()
1372
1373
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001374if __name__ == '__main__':
Doug Anderson44a644f2011-11-02 10:37:37 -07001375 if sys.argv[1:2] == ["--test"]:
1376 _test()
1377 exit_code = 0
1378 else:
1379 prog_name = os.path.basename(sys.argv[0])
1380 try:
1381 exit_code = direct_main(sys.argv)
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -07001382 except BadInvocation, err:
1383 print("%s: %s" % (prog_name, str(err)), file=sys.stderr)
Doug Anderson44a644f2011-11-02 10:37:37 -07001384 exit_code = 1
1385 sys.exit(exit_code)