blob: adc4d82330180b9a53cd18de95e10896f90d5bfd [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/.*",
66 # ignore minified js and jquery
67 r".*\.min\.js",
68 r".*jquery.*\.js",
Mike Frysinger33a458d2014-03-03 17:00:51 -050069
70 # Ignore license files as the content is often taken verbatim.
71 r'.*/licenses/.*',
Ryan Cuiec4d6332011-05-02 14:15:25 -070072]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070073
Ryan Cui1562fb82011-05-09 11:01:31 -070074
Ryan Cui9b651632011-05-11 11:38:58 -070075_CONFIG_FILE = 'PRESUBMIT.cfg'
76
77
Doug Anderson44a644f2011-11-02 10:37:37 -070078# Exceptions
79
80
81class BadInvocation(Exception):
82 """An Exception indicating a bad invocation of the program."""
83 pass
84
85
Ryan Cui1562fb82011-05-09 11:01:31 -070086# General Helpers
87
Sean Paulba01d402011-05-05 11:36:23 -040088
Doug Anderson44a644f2011-11-02 10:37:37 -070089def _run_command(cmd, cwd=None, stderr=None):
90 """Executes the passed in command and returns raw stdout output.
91
92 Args:
93 cmd: The command to run; should be a list of strings.
94 cwd: The directory to switch to for running the command.
95 stderr: Can be one of None (print stderr to console), subprocess.STDOUT
96 (combine stderr with stdout), or subprocess.PIPE (ignore stderr).
97
98 Returns:
99 The standard out from the process.
100 """
101 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr, cwd=cwd)
102 return p.communicate()[0]
Ryan Cui72834d12011-05-05 14:51:33 -0700103
Ryan Cui1562fb82011-05-09 11:01:31 -0700104
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700105def _get_hooks_dir():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700106 """Returns the absolute path to the repohooks directory."""
Doug Anderson44a644f2011-11-02 10:37:37 -0700107 if __name__ == '__main__':
108 # Works when file is run on its own (__file__ is defined)...
109 return os.path.abspath(os.path.dirname(__file__))
110 else:
111 # We need to do this when we're run through repo. Since repo executes
112 # us with execfile(), we don't get __file__ defined.
113 cmd = ['repo', 'forall', 'chromiumos/repohooks', '-c', 'pwd']
114 return _run_command(cmd).strip()
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700115
Ryan Cui1562fb82011-05-09 11:01:31 -0700116
Ryan Cuiec4d6332011-05-02 14:15:25 -0700117def _match_regex_list(subject, expressions):
118 """Try to match a list of regular expressions to a string.
119
120 Args:
121 subject: The string to match regexes on
122 expressions: A list of regular expressions to check for matches with.
123
124 Returns:
125 Whether the passed in subject matches any of the passed in regexes.
126 """
127 for expr in expressions:
Mike Frysingerae409522014-02-01 03:16:11 -0500128 if re.search(expr, subject):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700129 return True
130 return False
131
Ryan Cui1562fb82011-05-09 11:01:31 -0700132
Mike Frysingerae409522014-02-01 03:16:11 -0500133def _filter_files(files, include_list, exclude_list=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700134 """Filter out files based on the conditions passed in.
135
136 Args:
137 files: list of filepaths to filter
138 include_list: list of regex that when matched with a file path will cause it
139 to be added to the output list unless the file is also matched with a
140 regex in the exclude_list.
141 exclude_list: list of regex that when matched with a file will prevent it
142 from being added to the output list, even if it is also matched with a
143 regex in the include_list.
144
145 Returns:
146 A list of filepaths that contain files matched in the include_list and not
147 in the exclude_list.
148 """
149 filtered = []
150 for f in files:
151 if (_match_regex_list(f, include_list) and
152 not _match_regex_list(f, exclude_list)):
153 filtered.append(f)
154 return filtered
155
Ryan Cuiec4d6332011-05-02 14:15:25 -0700156
David Hendricks35030d02013-02-04 17:49:16 -0800157def _verify_header_content(commit, content, fail_msg):
158 """Verify that file headers contain specified content.
159
160 Args:
161 commit: the affected commit.
162 content: the content of the header to be verified.
163 fail_msg: the first message to display in case of failure.
164
Mike Frysinger33a458d2014-03-03 17:00:51 -0500165 Returns:
166 The return value of HookFailure().
David Hendricks35030d02013-02-04 17:49:16 -0800167 """
168 license_re = re.compile(content, re.MULTILINE)
169 bad_files = []
170 files = _filter_files(_get_affected_files(commit),
171 COMMON_INCLUDED_PATHS,
172 COMMON_EXCLUDED_PATHS)
173
174 for f in files:
Tom Wai-Hong Tam667db5d2014-02-27 06:28:14 +0800175 # Ignore non-existant files and symlinks
176 if os.path.exists(f) and not os.path.islink(f):
Gabe Blackcf3c32c2013-02-27 00:26:13 -0800177 contents = open(f).read()
Mike Frysingerae409522014-02-01 03:16:11 -0500178 if not contents:
179 # Ignore empty files
180 continue
Gabe Blackcf3c32c2013-02-27 00:26:13 -0800181 if not license_re.search(contents):
182 bad_files.append(f)
David Hendricks35030d02013-02-04 17:49:16 -0800183 if bad_files:
Mike Frysingerae409522014-02-01 03:16:11 -0500184 msg = "%s:\n%s\n%s" % (fail_msg, license_re.pattern,
185 "Found a bad header in these files:")
186 return HookFailure(msg, bad_files)
David Hendricks35030d02013-02-04 17:49:16 -0800187
188
Ryan Cuiec4d6332011-05-02 14:15:25 -0700189# Git Helpers
Ryan Cui1562fb82011-05-09 11:01:31 -0700190
191
Ryan Cui4725d952011-05-05 15:41:19 -0700192def _get_upstream_branch():
193 """Returns the upstream tracking branch of the current branch.
194
195 Raises:
196 Error if there is no tracking branch
197 """
198 current_branch = _run_command(['git', 'symbolic-ref', 'HEAD']).strip()
199 current_branch = current_branch.replace('refs/heads/', '')
200 if not current_branch:
Ryan Cui1562fb82011-05-09 11:01:31 -0700201 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700202
203 cfg_option = 'branch.' + current_branch + '.%s'
204 full_upstream = _run_command(['git', 'config', cfg_option % 'merge']).strip()
205 remote = _run_command(['git', 'config', cfg_option % 'remote']).strip()
206 if not remote or not full_upstream:
Ryan Cui1562fb82011-05-09 11:01:31 -0700207 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700208
209 return full_upstream.replace('heads', 'remotes/' + remote)
210
Ryan Cui1562fb82011-05-09 11:01:31 -0700211
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700212def _get_patch(commit):
213 """Returns the patch for this commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700214 if commit == PRE_SUBMIT:
215 return _run_command(['git', 'diff', '--cached', 'HEAD'])
216 else:
217 return _run_command(['git', 'format-patch', '--stdout', '-1', commit])
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700218
Ryan Cui1562fb82011-05-09 11:01:31 -0700219
Jon Salz98255932012-08-18 14:48:02 +0800220def _try_utf8_decode(data):
221 """Attempts to decode a string as UTF-8.
222
223 Returns:
224 The decoded Unicode object, or the original string if parsing fails.
225 """
226 try:
227 return unicode(data, 'utf-8', 'strict')
228 except UnicodeDecodeError:
229 return data
230
231
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500232def _get_file_content(path, commit):
233 """Returns the content of a file at a specific commit.
234
235 We can't rely on the file as it exists in the filesystem as people might be
236 uploading a series of changes which modifies the file multiple times.
237
238 Note: The "content" of a symlink is just the target. So if you're expecting
239 a full file, you should check that first. One way to detect is that the
240 content will not have any newlines.
241 """
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700242 if commit == PRE_SUBMIT:
243 return _run_command(['git', 'diff', 'HEAD', path])
244 else:
245 return _run_command(['git', 'show', '%s:%s' % (commit, path)])
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500246
247
Mike Frysingerae409522014-02-01 03:16:11 -0500248def _get_file_diff(path, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700249 """Returns a list of (linenum, lines) tuples that the commit touched."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700250 command = ['git', 'diff', '-p', '--pretty=format:', '--no-ext-diff']
251 if commit == PRE_SUBMIT:
252 command += ['HEAD', path]
253 else:
254 command += [commit, path]
255 output = _run_command(command)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700256
257 new_lines = []
258 line_num = 0
259 for line in output.splitlines():
260 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
261 if m:
262 line_num = int(m.groups(1)[0])
263 continue
264 if line.startswith('+') and not line.startswith('++'):
Jon Salz98255932012-08-18 14:48:02 +0800265 new_lines.append((line_num, _try_utf8_decode(line[1:])))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700266 if not line.startswith('-'):
267 line_num += 1
268 return new_lines
269
Ryan Cui1562fb82011-05-09 11:01:31 -0700270
Peter Ammon811f6702014-06-12 15:45:38 -0700271def _parse_affected_files(output, include_deletes=False, relative=False):
272 """Parses git diff's 'raw' format, returning a list of modified file paths.
273
274 This excludes directories and symlinks, and optionally includes files that
275 were deleted.
Doug Anderson42b8a052013-06-26 10:45:36 -0700276
277 Args:
Peter Ammon811f6702014-06-12 15:45:38 -0700278 output: The result of the 'git diff --raw' command
279 include_deletes: If true, we'll include deleted files in the result
280 relative: Whether to return relative or full paths to files
Doug Anderson42b8a052013-06-26 10:45:36 -0700281
282 Returns:
283 A list of modified/added (and perhaps deleted) files
284 """
Ryan Cuiec4d6332011-05-02 14:15:25 -0700285 files = []
Peter Ammon811f6702014-06-12 15:45:38 -0700286 # See the documentation for 'git diff --raw' for the relevant format.
Ryan Cuiec4d6332011-05-02 14:15:25 -0700287 for statusline in output.splitlines():
Peter Ammon811f6702014-06-12 15:45:38 -0700288 attributes, paths = statusline.split('\t', 1)
289 _, mode, _, _, status = attributes.split(' ')
290
291 # Ignore symlinks and directories.
292 imode = int(mode, 8)
293 if stat.S_ISDIR(imode) or stat.S_ISLNK(imode):
294 continue
295
296 # Ignore deleted files, and optionally return absolute paths of files.
297 if include_deletes or status != 'D':
298 # If a file was merely modified, we will have a single file path.
299 # If it was moved, we will have two paths (source and destination).
300 # In either case, we want the last path.
301 f = paths.split('\t')[-1]
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500302 if not relative:
303 pwd = os.getcwd()
304 f = os.path.join(pwd, f)
305 files.append(f)
Peter Ammon811f6702014-06-12 15:45:38 -0700306
Ryan Cuiec4d6332011-05-02 14:15:25 -0700307 return files
308
Ryan Cui1562fb82011-05-09 11:01:31 -0700309
Peter Ammon811f6702014-06-12 15:45:38 -0700310def _get_affected_files(commit, include_deletes=False, relative=False):
311 """Returns list of file paths that were modified/added, excluding symlinks.
312
313 Args:
314 commit: The commit
315 include_deletes: If true, we'll include deleted files in the result
316 relative: Whether to return relative or full paths to files
317
318 Returns:
319 A list of modified/added (and perhaps deleted) files
320 """
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700321 if commit == PRE_SUBMIT:
322 return _run_command(['git', 'diff-index', '--cached',
323 '--name-only', 'HEAD']).split()
Peter Ammon811f6702014-06-12 15:45:38 -0700324 output = _run_command(['git', 'diff', '--raw', commit + '^!'])
325 return _parse_affected_files(output, include_deletes, relative)
326
327
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700328def _get_commits():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700329 """Returns a list of commits for this review."""
Ryan Cui4725d952011-05-05 15:41:19 -0700330 cmd = ['git', 'log', '%s..' % _get_upstream_branch(), '--format=%H']
Ryan Cui72834d12011-05-05 14:51:33 -0700331 return _run_command(cmd).split()
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700332
Ryan Cui1562fb82011-05-09 11:01:31 -0700333
Ryan Cuiec4d6332011-05-02 14:15:25 -0700334def _get_commit_desc(commit):
335 """Returns the full commit message of a commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700336 if commit == PRE_SUBMIT:
337 return ''
Sean Paul23a2c582011-05-06 13:10:44 -0400338 return _run_command(['git', 'log', '--format=%s%n%n%b', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700339
340
341# Common Hooks
342
Ryan Cui1562fb82011-05-09 11:01:31 -0700343
Mike Frysingerae409522014-02-01 03:16:11 -0500344def _check_no_long_lines(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700345 """Checks that there aren't any lines longer than maxlen characters in any of
346 the text files to be submitted.
347 """
348 MAX_LEN = 80
Jon Salz98255932012-08-18 14:48:02 +0800349 SKIP_REGEXP = re.compile('|'.join([
350 r'https?://',
351 r'^#\s*(define|include|import|pragma|if|endif)\b']))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700352
353 errors = []
354 files = _filter_files(_get_affected_files(commit),
355 COMMON_INCLUDED_PATHS,
356 COMMON_EXCLUDED_PATHS)
357
358 for afile in files:
359 for line_num, line in _get_file_diff(afile, commit):
360 # Allow certain lines to exceed the maxlen rule.
Mike Frysingerae409522014-02-01 03:16:11 -0500361 if len(line) <= MAX_LEN or SKIP_REGEXP.search(line):
Jon Salz98255932012-08-18 14:48:02 +0800362 continue
363
364 errors.append('%s, line %s, %s chars' % (afile, line_num, len(line)))
365 if len(errors) == 5: # Just show the first 5 errors.
366 break
Ryan Cuiec4d6332011-05-02 14:15:25 -0700367
368 if errors:
369 msg = 'Found lines longer than %s characters (first 5 shown):' % MAX_LEN
Ryan Cui1562fb82011-05-09 11:01:31 -0700370 return HookFailure(msg, errors)
371
Ryan Cuiec4d6332011-05-02 14:15:25 -0700372
Mike Frysingerae409522014-02-01 03:16:11 -0500373def _check_no_stray_whitespace(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700374 """Checks that there is no stray whitespace at source lines end."""
375 errors = []
376 files = _filter_files(_get_affected_files(commit),
Mike Frysingerae409522014-02-01 03:16:11 -0500377 COMMON_INCLUDED_PATHS,
378 COMMON_EXCLUDED_PATHS)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700379 for afile in files:
380 for line_num, line in _get_file_diff(afile, commit):
381 if line.rstrip() != line:
382 errors.append('%s, line %s' % (afile, line_num))
383 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700384 return HookFailure('Found line ending with white space in:', errors)
385
Ryan Cuiec4d6332011-05-02 14:15:25 -0700386
Mike Frysingerae409522014-02-01 03:16:11 -0500387def _check_no_tabs(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700388 """Checks there are no unexpanded tabs."""
389 TAB_OK_PATHS = [
Ryan Cui31e0c172011-05-04 21:00:45 -0700390 r"/src/third_party/u-boot/",
Ryan Cuiec4d6332011-05-02 14:15:25 -0700391 r".*\.ebuild$",
392 r".*\.eclass$",
Elly Jones5ab34192011-11-15 14:57:06 -0500393 r".*/[M|m]akefile$",
394 r".*\.mk$"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700395 ]
396
397 errors = []
398 files = _filter_files(_get_affected_files(commit),
399 COMMON_INCLUDED_PATHS,
400 COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
401
402 for afile in files:
403 for line_num, line in _get_file_diff(afile, commit):
404 if '\t' in line:
Mike Frysingerae409522014-02-01 03:16:11 -0500405 errors.append('%s, line %s' % (afile, line_num))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700406 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700407 return HookFailure('Found a tab character in:', errors)
408
Ryan Cuiec4d6332011-05-02 14:15:25 -0700409
Mike Frysingerae409522014-02-01 03:16:11 -0500410def _check_change_has_test_field(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700411 """Check for a non-empty 'TEST=' field in the commit message."""
David McMahon8f6553e2011-06-10 15:46:36 -0700412 TEST_RE = r'\nTEST=\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700413
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700414 if not re.search(TEST_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700415 msg = 'Changelist description needs TEST field (after first line)'
416 return HookFailure(msg)
417
Ryan Cuiec4d6332011-05-02 14:15:25 -0700418
Mike Frysingerae409522014-02-01 03:16:11 -0500419def _check_change_has_valid_cq_depend(_project, commit):
David Jamesc3b68b32013-04-03 09:17:03 -0700420 """Check for a correctly formatted CQ-DEPEND field in the commit message."""
421 msg = 'Changelist has invalid CQ-DEPEND target.'
422 example = 'Example: CQ-DEPEND=CL:1234, CL:2345'
423 try:
424 patch.GetPaladinDeps(_get_commit_desc(commit))
425 except ValueError as ex:
426 return HookFailure(msg, [example, str(ex)])
427
428
Mike Frysingerae409522014-02-01 03:16:11 -0500429def _check_change_has_bug_field(_project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700430 """Check for a correctly formatted 'BUG=' field in the commit message."""
David James5c0073d2013-04-03 08:48:52 -0700431 OLD_BUG_RE = r'\nBUG=.*chromium-os'
432 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
433 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
434 'the chromium tracker in your BUG= line now.')
435 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700436
David James5c0073d2013-04-03 08:48:52 -0700437 BUG_RE = r'\nBUG=([Nn]one|(chrome-os-partner|chromium):\d+)'
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700438 if not re.search(BUG_RE, _get_commit_desc(commit)):
David McMahon8f6553e2011-06-10 15:46:36 -0700439 msg = ('Changelist description needs BUG field (after first line):\n'
David James5c0073d2013-04-03 08:48:52 -0700440 'BUG=chromium:9999 (for public tracker)\n'
David McMahon8f6553e2011-06-10 15:46:36 -0700441 'BUG=chrome-os-partner:9999 (for partner tracker)\n'
442 'BUG=None')
Ryan Cui1562fb82011-05-09 11:01:31 -0700443 return HookFailure(msg)
444
Ryan Cuiec4d6332011-05-02 14:15:25 -0700445
Doug Anderson42b8a052013-06-26 10:45:36 -0700446def _check_for_uprev(project, commit):
447 """Check that we're not missing a revbump of an ebuild in the given commit.
448
449 If the given commit touches files in a directory that has ebuilds somewhere
450 up the directory hierarchy, it's very likely that we need an ebuild revbump
451 in order for those changes to take effect.
452
453 It's not totally trivial to detect a revbump, so at least detect that an
454 ebuild with a revision number in it was touched. This should handle the
455 common case where we use a symlink to do the revbump.
456
457 TODO: it would be nice to enhance this hook to:
458 * Handle cases where people revbump with a slightly different syntax. I see
459 one ebuild (puppy) that revbumps with _pN. This is a false positive.
460 * Catches cases where people aren't using symlinks for revbumps. If they
461 edit a revisioned file directly (and are expected to rename it for revbump)
462 we'll miss that. Perhaps we could detect that the file touched is a
463 symlink?
464
465 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
466 still better off than without this check.
467
468 Args:
469 project: The project to look at
470 commit: The commit to look at
471
472 Returns:
473 A HookFailure or None.
474 """
Mike Frysinger011af942014-01-17 16:12:22 -0500475 # If this is the portage-stable overlay, then ignore the check. It's rare
476 # that we're doing anything other than importing files from upstream, so
477 # forcing a rev bump makes no sense.
478 whitelist = (
479 'chromiumos/overlays/portage-stable',
480 )
481 if project in whitelist:
482 return None
483
Doug Anderson42b8a052013-06-26 10:45:36 -0700484 affected_paths = _get_affected_files(commit, include_deletes=True)
485
486 # Don't yell about changes to whitelisted files...
Mike Frysinger011af942014-01-17 16:12:22 -0500487 whitelist = ('ChangeLog', 'Manifest', 'metadata.xml')
Doug Anderson42b8a052013-06-26 10:45:36 -0700488 affected_paths = [path for path in affected_paths
489 if os.path.basename(path) not in whitelist]
490 if not affected_paths:
491 return None
492
493 # If we've touched any file named with a -rN.ebuild then we'll say we're
494 # OK right away. See TODO above about enhancing this.
495 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', path)
496 for path in affected_paths)
497 if touched_revved_ebuild:
498 return None
499
500 # We want to examine the current contents of all directories that are parents
501 # of files that were touched (up to the top of the project).
502 #
503 # ...note: we use the current directory contents even though it may have
504 # changed since the commit we're looking at. This is just a heuristic after
505 # all. Worst case we don't flag a missing revbump.
506 project_top = os.getcwd()
507 dirs_to_check = set([project_top])
508 for path in affected_paths:
509 path = os.path.dirname(path)
510 while os.path.exists(path) and not os.path.samefile(path, project_top):
511 dirs_to_check.add(path)
512 path = os.path.dirname(path)
513
514 # Look through each directory. If it's got an ebuild in it then we'll
515 # consider this as a case when we need a revbump.
516 for dir_path in dirs_to_check:
517 contents = os.listdir(dir_path)
518 ebuilds = [os.path.join(dir_path, path)
519 for path in contents if path.endswith('.ebuild')]
520 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
521
522 # If the -9999.ebuild file was touched the bot will uprev for us.
523 # ...we'll use a simple intersection here as a heuristic...
524 if set(ebuilds_9999) & set(affected_paths):
525 continue
526
527 if ebuilds:
528 return HookFailure('Changelist probably needs a revbump of an ebuild\n'
529 'or a -r1.ebuild symlink if this is a new ebuild')
530
531 return None
532
533
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500534def _check_ebuild_eapi(project, commit):
535 """Make sure we have people use EAPI=4 or newer with custom ebuilds.
536
537 We want to get away from older EAPI's as it makes life confusing and they
538 have less builtin error checking.
539
540 Args:
541 project: The project to look at
542 commit: The commit to look at
543
544 Returns:
545 A HookFailure or None.
546 """
547 # If this is the portage-stable overlay, then ignore the check. It's rare
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500548 # that we're doing anything other than importing files from upstream, and
549 # we shouldn't be rewriting things fundamentally anyways.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500550 whitelist = (
551 'chromiumos/overlays/portage-stable',
552 )
553 if project in whitelist:
554 return None
555
556 BAD_EAPIS = ('0', '1', '2', '3')
557
558 get_eapi = re.compile(r'^\s*EAPI=[\'"]?([^\'"]+)')
559
560 ebuilds_re = [r'\.ebuild$']
561 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
562 ebuilds_re)
563 bad_ebuilds = []
564
565 for ebuild in ebuilds:
566 # If the ebuild does not specify an EAPI, it defaults to 0.
567 eapi = '0'
568
569 lines = _get_file_content(ebuild, commit).splitlines()
570 if len(lines) == 1:
571 # This is most likely a symlink, so skip it entirely.
572 continue
573
574 for line in lines:
575 m = get_eapi.match(line)
576 if m:
577 # Once we hit the first EAPI line in this ebuild, stop processing.
578 # The spec requires that there only be one and it be first, so
579 # checking all possible values is pointless. We also assume that
580 # it's "the" EAPI line and not something in the middle of a heredoc.
581 eapi = m.group(1)
582 break
583
584 if eapi in BAD_EAPIS:
585 bad_ebuilds.append((ebuild, eapi))
586
587 if bad_ebuilds:
588 # pylint: disable=C0301
589 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/upgrade-ebuild-eapis'
590 # pylint: enable=C0301
591 return HookFailure(
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500592 'These ebuilds are using old EAPIs. If these are imported from\n'
593 'Gentoo, then you may ignore and upload once with the --no-verify\n'
594 'flag. Otherwise, please update to 4 or newer.\n'
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500595 '\t%s\n'
596 'See this guide for more details:\n%s\n' %
597 ('\n\t'.join(['%s: EAPI=%s' % x for x in bad_ebuilds]), url))
598
599
Mike Frysinger89bdb852014-02-01 05:26:26 -0500600def _check_ebuild_keywords(_project, commit):
Mike Frysingerc51ece72014-01-17 16:23:40 -0500601 """Make sure we use the new style KEYWORDS when possible in ebuilds.
602
603 If an ebuild generally does not care about the arch it is running on, then
604 ebuilds should flag it with one of:
605 KEYWORDS="*" # A stable ebuild.
606 KEYWORDS="~*" # An unstable ebuild.
607 KEYWORDS="-* ..." # Is known to only work on specific arches.
608
609 Args:
610 project: The project to look at
611 commit: The commit to look at
612
613 Returns:
614 A HookFailure or None.
615 """
616 WHITELIST = set(('*', '-*', '~*'))
617
618 get_keywords = re.compile(r'^\s*KEYWORDS="(.*)"')
619
Mike Frysinger89bdb852014-02-01 05:26:26 -0500620 ebuilds_re = [r'\.ebuild$']
621 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
622 ebuilds_re)
623
Mike Frysingerc51ece72014-01-17 16:23:40 -0500624 for ebuild in ebuilds:
625 for _, line in _get_file_diff(ebuild, commit):
626 m = get_keywords.match(line)
627 if m:
628 keywords = set(m.group(1).split())
629 if not keywords or WHITELIST - keywords != WHITELIST:
630 continue
631
632 return HookFailure(
633 'Please update KEYWORDS to use a glob:\n'
634 'If the ebuild should be marked stable (normal for non-9999 '
635 'ebuilds):\n'
636 ' KEYWORDS="*"\n'
637 'If the ebuild should be marked unstable (normal for '
638 'cros-workon / 9999 ebuilds):\n'
639 ' KEYWORDS="~*"\n'
640 'If the ebuild needs to be marked for only specific arches,'
641 'then use -* like so:\n'
642 ' KEYWORDS="-* arm ..."\n')
643
644
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800645def _check_ebuild_licenses(_project, commit):
646 """Check if the LICENSE field in the ebuild is correct."""
647 affected_paths = _get_affected_files(commit)
648 touched_ebuilds = [x for x in affected_paths if x.endswith('.ebuild')]
649
650 # A list of licenses to ignore for now.
Yu-Ju Hongc0963fa2014-03-03 12:36:52 -0800651 LICENSES_IGNORE = ['||', '(', ')']
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800652
653 for ebuild in touched_ebuilds:
654 # Skip virutal packages.
655 if ebuild.split('/')[-3] == 'virtual':
656 continue
657
658 try:
Mike Frysinger2ec70ed2014-08-17 19:28:34 -0400659 license_types = licenses_lib.GetLicenseTypesFromEbuild(ebuild)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800660 except ValueError as e:
661 return HookFailure(e.message, [ebuild])
662
663 # Also ignore licenses ending with '?'
664 for license_type in [x for x in license_types
665 if x not in LICENSES_IGNORE and not x.endswith('?')]:
666 try:
Mike Frysinger2ec70ed2014-08-17 19:28:34 -0400667 licenses_lib.Licensing.FindLicenseType(license_type)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800668 except AssertionError as e:
669 return HookFailure(e.message, [ebuild])
670
671
Mike Frysingercd363c82014-02-01 05:20:18 -0500672def _check_ebuild_virtual_pv(project, commit):
673 """Enforce the virtual PV policies."""
674 # If this is the portage-stable overlay, then ignore the check.
675 # We want to import virtuals as-is from upstream Gentoo.
676 whitelist = (
677 'chromiumos/overlays/portage-stable',
678 )
679 if project in whitelist:
680 return None
681
682 # We assume the repo name is the same as the dir name on disk.
683 # It would be dumb to not have them match though.
684 project = os.path.basename(project)
685
686 is_variant = lambda x: x.startswith('overlay-variant-')
687 is_board = lambda x: x.startswith('overlay-')
688 is_private = lambda x: x.endswith('-private')
689
690 get_pv = re.compile(r'(.*?)virtual/([^/]+)/\2-([^/]*)\.ebuild$')
691
692 ebuilds_re = [r'\.ebuild$']
693 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
694 ebuilds_re)
695 bad_ebuilds = []
696
697 for ebuild in ebuilds:
698 m = get_pv.match(ebuild)
699 if m:
700 overlay = m.group(1)
701 if not overlay or not is_board(overlay):
702 overlay = project
703
704 pv = m.group(3).split('-', 1)[0]
705
706 if is_private(overlay):
707 want_pv = '3.5' if is_variant(overlay) else '3'
708 elif is_board(overlay):
709 want_pv = '2.5' if is_variant(overlay) else '2'
710 else:
711 want_pv = '1'
712
713 if pv != want_pv:
714 bad_ebuilds.append((ebuild, pv, want_pv))
715
716 if bad_ebuilds:
717 # pylint: disable=C0301
718 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/portage-build-faq#TOC-Virtuals-and-central-management'
719 # pylint: enable=C0301
720 return HookFailure(
721 'These virtuals have incorrect package versions (PVs). Please adjust:\n'
722 '\t%s\n'
723 'If this is an upstream Gentoo virtual, then you may ignore this\n'
724 'check (and re-run w/--no-verify). Otherwise, please see this\n'
725 'page for more details:\n%s\n' %
726 ('\n\t'.join(['%s:\n\t\tPV is %s but should be %s' % x
727 for x in bad_ebuilds]), url))
728
729
Mike Frysingerae409522014-02-01 03:16:11 -0500730def _check_change_has_proper_changeid(_project, commit):
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700731 """Verify that Change-ID is present in last paragraph of commit message."""
732 desc = _get_commit_desc(commit)
733 loc = desc.rfind('\nChange-Id:')
734 if loc == -1 or re.search('\n\s*\n\s*\S+', desc[loc:]):
Ryan Cui1562fb82011-05-09 11:01:31 -0700735 return HookFailure('Change-Id must be in last paragraph of description.')
736
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700737
Mike Frysingerae409522014-02-01 03:16:11 -0500738def _check_license(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700739 """Verifies the license header."""
740 LICENSE_HEADER = (
Chris Sosaed7a3fa2014-02-26 12:18:31 -0800741 r".* Copyright( \(c\))? 20[-0-9]{2,7} The Chromium OS Authors\. "
742 "All rights reserved\." "\n"
743 r".* Use of this source code is governed by a BSD-style license that can "
744 "be\n"
745 r".* found in the LICENSE file\."
746 "\n"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700747 )
David Hendricks35030d02013-02-04 17:49:16 -0800748 FAIL_MSG = "License must match"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700749
David Hendricks35030d02013-02-04 17:49:16 -0800750 return _verify_header_content(commit, LICENSE_HEADER, FAIL_MSG)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700751
752
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400753def _check_layout_conf(_project, commit):
754 """Verifies the metadata/layout.conf file."""
755 repo_name = 'profiles/repo_name'
756 layout_path = 'metadata/layout.conf'
757
758 files = _get_affected_files(commit, relative=True)
759
760 # Disallow new repos with the repo_name file.
761 if repo_name in files:
762 return HookFailure('%s: use "repo-name" in %s instead' %
763 (repo_name, layout_path))
764
765 # If the layout.conf file doesn't exist, nothing else to do.
766 if layout_path not in files:
767 return
768
769 errors = []
770
771 # Make sure the config file is sorted.
772 data = [x for x in _get_file_content(layout_path, commit).splitlines()
773 if x and x[0] != '#']
774 if sorted(data) != data:
775 errors += ['keep lines sorted']
776
777 # Require people to set specific values all the time.
778 settings = (
779 # TODO: Enable this for everyone. http://crbug.com/408038
780 #('fast caching', 'cache-format = md5-dict'),
781 ('fast manifests', 'thin-manifests = true'),
782 ('extra features', 'profile-formats = portage-2'),
783 )
784 for reason, line in settings:
785 if line not in data:
786 errors += ['enable %s with: %s' % (reason, line)]
787
788 # Require one of these settings.
789 if ('use-manifests = true' not in data and
790 'use-manifests = strict' not in data):
791 errors += ['enable file checking with: use-manifests = true']
792
793 if errors:
794 lines = [('%s: error(s) detected '
795 '(see the portage(5) man page for more details)') %
796 layout_path] + errors
797 return HookFailure('\n\t- '.join(lines))
798
799
Ryan Cuiec4d6332011-05-02 14:15:25 -0700800# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700801
Ryan Cui1562fb82011-05-09 11:01:31 -0700802
Mike Frysingerae409522014-02-01 03:16:11 -0500803def _run_checkpatch(_project, commit, options=()):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700804 """Runs checkpatch.pl on the given project"""
805 hooks_dir = _get_hooks_dir()
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700806 options = list(options)
807 if commit == PRE_SUBMIT:
808 # The --ignore option must be present and include 'MISSING_SIGN_OFF' in
809 # this case.
810 options.append('--ignore=MISSING_SIGN_OFF')
811 cmd = ['%s/checkpatch.pl' % hooks_dir] + options + ['-']
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700812 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700813 output = p.communicate(_get_patch(commit))[0]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700814 if p.returncode:
Ryan Cui1562fb82011-05-09 11:01:31 -0700815 return HookFailure('checkpatch.pl errors/warnings\n\n' + output)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700816
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700817
Anton Staaf815d6852011-08-22 10:08:45 -0700818def _run_checkpatch_no_tree(project, commit):
819 return _run_checkpatch(project, commit, ['--no-tree'])
820
Mike Frysingerae409522014-02-01 03:16:11 -0500821
Randall Spangler7318fd62013-11-21 12:16:58 -0800822def _run_checkpatch_ec(project, commit):
823 """Runs checkpatch with options for Chromium EC projects."""
824 return _run_checkpatch(project, commit, ['--no-tree',
825 '--ignore=MSLEEP,VOLATILE'])
826
Mike Frysingerae409522014-02-01 03:16:11 -0500827
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700828def _run_checkpatch_depthcharge(project, commit):
829 """Runs checkpatch with options for depthcharge."""
830 return _run_checkpatch(project, commit, [
831 '--no-tree',
Julius Werner83a35bb2014-06-23 12:51:48 -0700832 '--ignore=CAMELCASE,C99_COMMENTS,NEW_TYPEDEFS,CONFIG_DESCRIPTION,'
833 'SPACING,PREFER_PACKED,PREFER_PRINTF,PREFER_ALIGNED,GLOBAL_INITIALISERS,'
834 'INITIALISED_STATIC,OPEN_BRACE,TRAILING_STATEMENTS'])
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700835
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -0700836def _run_checkpatch_coreboot(project, commit):
837 """Runs checkpatch with options for coreboot."""
838 return _run_checkpatch(project, commit, [
839 '--no-tree',
840 '--ignore=NEW_TYPEDEFS,PREFER_PACKED,PREFER_PRINTF,PREFER_ALIGNED,'
841 'GLOBAL_INITIALISERS,INITIALISED_STATIC'])
842
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700843
Mike Frysingerae409522014-02-01 03:16:11 -0500844def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -0700845 """Makes sure kernel config changes are not mixed with code changes"""
846 files = _get_affected_files(commit)
847 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
848 return HookFailure('Changes to chromeos/config/ and regular files must '
849 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -0700850
Mike Frysingerae409522014-02-01 03:16:11 -0500851
852def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -0700853 """Checks that all JSON files are syntactically valid."""
Dale Curtisa039cfd2011-05-04 12:01:05 -0700854 for f in _filter_files(_get_affected_files(commit), [r'.*\.json']):
Dale Curtis2975c432011-05-03 17:25:20 -0700855 try:
856 json.load(open(f))
857 except Exception, e:
Ryan Cui1562fb82011-05-09 11:01:31 -0700858 return HookFailure('Invalid JSON in %s: %s' % (f, e))
Dale Curtis2975c432011-05-03 17:25:20 -0700859
860
Mike Frysingerae409522014-02-01 03:16:11 -0500861def _check_manifests(_project, commit):
Mike Frysinger52b537e2013-08-22 22:59:53 -0400862 """Make sure Manifest files only have DIST lines"""
863 paths = []
864
865 for path in _get_affected_files(commit):
866 if os.path.basename(path) != 'Manifest':
867 continue
868 if not os.path.exists(path):
869 continue
870
871 with open(path, 'r') as f:
872 for line in f.readlines():
873 if not line.startswith('DIST '):
874 paths.append(path)
875 break
876
877 if paths:
878 return HookFailure('Please remove lines that do not start with DIST:\n%s' %
879 ('\n'.join(paths),))
880
881
Mike Frysingerae409522014-02-01 03:16:11 -0500882def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700883 """Check for a non-empty 'BRANCH=' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700884 if commit == PRE_SUBMIT:
885 return
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700886 BRANCH_RE = r'\nBRANCH=\S+'
887
888 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
889 msg = ('Changelist description needs BRANCH field (after first line)\n'
890 'E.g. BRANCH=none or BRANCH=link,snow')
891 return HookFailure(msg)
892
893
Mike Frysingerae409522014-02-01 03:16:11 -0500894def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800895 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700896 if commit == PRE_SUBMIT:
897 return
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800898 SIGNOFF_RE = r'\nSigned-off-by: \S+'
899
900 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
901 msg = ('Changelist description needs Signed-off-by: field\n'
902 'E.g. Signed-off-by: My Name <me@chromium.org>')
903 return HookFailure(msg)
904
905
Jon Salz3ee59de2012-08-18 13:54:22 +0800906def _run_project_hook_script(script, project, commit):
907 """Runs a project hook script.
908
909 The script is run with the following environment variables set:
910 PRESUBMIT_PROJECT: The affected project
911 PRESUBMIT_COMMIT: The affected commit
912 PRESUBMIT_FILES: A newline-separated list of affected files
913
914 The script is considered to fail if the exit code is non-zero. It should
915 write an error message to stdout.
916 """
917 env = dict(os.environ)
918 env['PRESUBMIT_PROJECT'] = project
919 env['PRESUBMIT_COMMIT'] = commit
920
921 # Put affected files in an environment variable
922 files = _get_affected_files(commit)
923 env['PRESUBMIT_FILES'] = '\n'.join(files)
924
925 process = subprocess.Popen(script, env=env, shell=True,
926 stdin=open(os.devnull),
Jon Salz7b618af2012-08-31 06:03:16 +0800927 stdout=subprocess.PIPE,
928 stderr=subprocess.STDOUT)
Jon Salz3ee59de2012-08-18 13:54:22 +0800929 stdout, _ = process.communicate()
930 if process.wait():
Jon Salz7b618af2012-08-31 06:03:16 +0800931 if stdout:
932 stdout = re.sub('(?m)^', ' ', stdout)
933 return HookFailure('Hook script "%s" failed with code %d%s' %
Jon Salz3ee59de2012-08-18 13:54:22 +0800934 (script, process.returncode,
935 ':\n' + stdout if stdout else ''))
936
937
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -0700938def _moved_to_platform2(project, _commit):
939 """Forbids commits to legacy repo in src/platform."""
940 return HookFailure('%s has been moved to platform2. This change should be '
941 'made there.' % project)
942
943
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -0700944def _check_project_prefix(_project, commit):
945 """Fails if the change is project specific and the commit message is not
946 prefixed by the project_name.
947 """
948
949 files = _get_affected_files(commit, relative=True)
950 prefix = os.path.commonprefix(files)
951 prefix = os.path.dirname(prefix)
952
953 # If there is no common prefix, the CL span multiple projects.
954 if prefix == '':
955 return
956
957 project_name = prefix.split('/')[0]
958 alias_file = os.path.join(prefix, '.project_alias')
959 # If an alias exists, use it.
960 if os.path.isfile(alias_file):
961 with open(alias_file, 'r') as f:
962 project_name = f.read().strip()
963
964 if not _get_commit_desc(commit).startswith(project_name + ': '):
965 return HookFailure('The commit title for changes affecting only %s'
966 ' should start with \"%s: \"'
967 % (project_name, project_name))
968
969
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700970# Base
971
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700972# A list of hooks which are not project specific and check patch description
973# (as opposed to patch body).
974_PATCH_DESCRIPTION_HOOKS = [
Ryan Cui9b651632011-05-11 11:38:58 -0700975 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -0700976 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -0700977 _check_change_has_test_field,
978 _check_change_has_proper_changeid,
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700979]
980
981
982# A list of hooks that are not project-specific
983_COMMON_HOOKS = [
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500984 _check_ebuild_eapi,
Mike Frysinger89bdb852014-02-01 05:26:26 -0500985 _check_ebuild_keywords,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800986 _check_ebuild_licenses,
Mike Frysingercd363c82014-02-01 05:20:18 -0500987 _check_ebuild_virtual_pv,
Ryan Cui9b651632011-05-11 11:38:58 -0700988 _check_no_stray_whitespace,
989 _check_no_long_lines,
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400990 _check_layout_conf,
Ryan Cui9b651632011-05-11 11:38:58 -0700991 _check_license,
992 _check_no_tabs,
Doug Anderson42b8a052013-06-26 10:45:36 -0700993 _check_for_uprev,
Ryan Cui9b651632011-05-11 11:38:58 -0700994]
Ryan Cuiec4d6332011-05-02 14:15:25 -0700995
Ryan Cui1562fb82011-05-09 11:01:31 -0700996
Ryan Cui9b651632011-05-11 11:38:58 -0700997# A dictionary of project-specific hooks(callbacks), indexed by project name.
998# dict[project] = [callback1, callback2]
999_PROJECT_SPECIFIC_HOOKS = {
Mike Frysingerdf980702013-08-22 22:25:22 -04001000 "chromeos/autotest-tools": [_run_json_check],
Mike Frysinger52b537e2013-08-22 22:59:53 -04001001 "chromeos/overlays/chromeos-overlay": [_check_manifests],
1002 "chromeos/overlays/chromeos-partner-overlay": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -08001003 "chromeos/platform/ec-private": [_run_checkpatch_ec,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001004 _check_change_has_branch_field],
Puneet Kumar57b9c092012-08-14 18:58:29 -07001005 "chromeos/third_party/intel-framework": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001006 "chromeos/vendor/kernel-exynos-staging": [_run_checkpatch,
1007 _kernel_configcheck],
Mike Frysinger52b537e2013-08-22 22:59:53 -04001008 "chromiumos/overlays/board-overlays": [_check_manifests],
1009 "chromiumos/overlays/chromiumos-overlay": [_check_manifests],
1010 "chromiumos/overlays/portage-stable": [_check_manifests],
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001011 "chromiumos/platform2": [_check_project_prefix],
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -07001012 "chromiumos/platform/depthcharge": [_check_change_has_signoff_field,
1013 _run_checkpatch_depthcharge],
Randall Spangler7318fd62013-11-21 12:16:58 -08001014 "chromiumos/platform/ec": [_run_checkpatch_ec,
Mike Frysingerdf980702013-08-22 22:25:22 -04001015 _check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001016 "chromiumos/platform/mosys": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001017 "chromiumos/platform/vboot_reference": [_check_change_has_branch_field],
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -07001018 "chromiumos/third_party/coreboot": [_check_change_has_signoff_field,
1019 _run_checkpatch_coreboot],
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001020 "chromiumos/third_party/flashrom": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001021 "chromiumos/third_party/kernel": [_run_checkpatch, _kernel_configcheck],
1022 "chromiumos/third_party/kernel-next": [_run_checkpatch,
1023 _kernel_configcheck],
Vadim Bendebury203fbc22014-04-22 11:58:14 -07001024 "chromiumos/third_party/u-boot": [_run_checkpatch_no_tree],
Ryan Cui9b651632011-05-11 11:38:58 -07001025}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001026
Ryan Cui1562fb82011-05-09 11:01:31 -07001027
Ryan Cui9b651632011-05-11 11:38:58 -07001028# A dictionary of flags (keys) that can appear in the config file, and the hook
1029# that the flag disables (value)
1030_DISABLE_FLAGS = {
1031 'stray_whitespace_check': _check_no_stray_whitespace,
1032 'long_line_check': _check_no_long_lines,
1033 'cros_license_check': _check_license,
1034 'tab_check': _check_no_tabs,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001035 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001036 'signoff_check': _check_change_has_signoff_field,
Josh Triplett0e8fc7f2014-04-23 16:00:00 -07001037 'bug_field_check': _check_change_has_bug_field,
1038 'test_field_check': _check_change_has_test_field,
Ryan Cui9b651632011-05-11 11:38:58 -07001039}
1040
1041
Jon Salz3ee59de2012-08-18 13:54:22 +08001042def _get_disabled_hooks(config):
Ryan Cui9b651632011-05-11 11:38:58 -07001043 """Returns a set of hooks disabled by the current project's config file.
1044
1045 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +08001046
1047 Args:
1048 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07001049 """
1050 SECTION = 'Hook Overrides'
Jon Salz3ee59de2012-08-18 13:54:22 +08001051 if not config.has_section(SECTION):
1052 return set()
Ryan Cui9b651632011-05-11 11:38:58 -07001053
1054 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +08001055 for flag in config.options(SECTION):
Ryan Cui9b651632011-05-11 11:38:58 -07001056 try:
Mike Frysingerae409522014-02-01 03:16:11 -05001057 if not config.getboolean(SECTION, flag):
1058 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -07001059 except ValueError as e:
1060 msg = "Error parsing flag \'%s\' in %s file - " % (flag, _CONFIG_FILE)
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001061 print(msg + str(e))
Ryan Cui9b651632011-05-11 11:38:58 -07001062
1063 disabled_keys = set(_DISABLE_FLAGS.iterkeys()).intersection(disable_flags)
1064 return set([_DISABLE_FLAGS[key] for key in disabled_keys])
1065
1066
Jon Salz3ee59de2012-08-18 13:54:22 +08001067def _get_project_hook_scripts(config):
1068 """Returns a list of project-specific hook scripts.
1069
1070 Args:
1071 config: A ConfigParser for the project's config file.
1072 """
1073 SECTION = 'Hook Scripts'
1074 if not config.has_section(SECTION):
1075 return []
1076
1077 hook_names_values = config.items(SECTION)
1078 hook_names_values.sort(key=lambda x: x[0])
1079 return [x[1] for x in hook_names_values]
1080
1081
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001082def _get_project_hooks(project, presubmit):
Ryan Cui9b651632011-05-11 11:38:58 -07001083 """Returns a list of hooks that need to be run for a project.
1084
1085 Expects to be called from within the project root.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001086
1087 Args:
1088 project: A string, name of the project.
1089 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui9b651632011-05-11 11:38:58 -07001090 """
Jon Salz3ee59de2012-08-18 13:54:22 +08001091 config = ConfigParser.RawConfigParser()
1092 try:
1093 config.read(_CONFIG_FILE)
1094 except ConfigParser.Error:
1095 # Just use an empty config file
1096 config = ConfigParser.RawConfigParser()
1097
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001098 if presubmit:
1099 hook_list = _COMMON_HOOKS
1100 else:
1101 hook_list = _PATCH_DESCRIPTION_HOOKS + _COMMON_HOOKS
1102
Jon Salz3ee59de2012-08-18 13:54:22 +08001103 disabled_hooks = _get_disabled_hooks(config)
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001104 hooks = [hook for hook in hook_list if hook not in disabled_hooks]
Ryan Cui9b651632011-05-11 11:38:58 -07001105
1106 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001107 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
1108 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -07001109
Jon Salz3ee59de2012-08-18 13:54:22 +08001110 for script in _get_project_hook_scripts(config):
1111 hooks.append(functools.partial(_run_project_hook_script, script))
1112
Ryan Cui9b651632011-05-11 11:38:58 -07001113 return hooks
1114
1115
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001116def _run_project_hooks(project, proj_dir=None,
1117 commit_list=None, presubmit=False):
Ryan Cui1562fb82011-05-09 11:01:31 -07001118 """For each project run its project specific hook from the hooks dictionary.
1119
1120 Args:
Doug Anderson44a644f2011-11-02 10:37:37 -07001121 project: The name of project to run hooks for.
1122 proj_dir: If non-None, this is the directory the project is in. If None,
1123 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -07001124 commit_list: A list of commits to run hooks against. If None or empty list
1125 then we'll automatically get the list of commits that would be uploaded.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001126 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui1562fb82011-05-09 11:01:31 -07001127
1128 Returns:
1129 Boolean value of whether any errors were ecountered while running the hooks.
1130 """
Doug Anderson44a644f2011-11-02 10:37:37 -07001131 if proj_dir is None:
David James2edd9002013-10-11 14:09:19 -07001132 proj_dirs = _run_command(['repo', 'forall', project, '-c', 'pwd']).split()
1133 if len(proj_dirs) == 0:
1134 print('%s cannot be found.' % project, file=sys.stderr)
1135 print('Please specify a valid project.', file=sys.stderr)
1136 return True
1137 if len(proj_dirs) > 1:
1138 print('%s is associated with multiple directories.' % project,
1139 file=sys.stderr)
1140 print('Please specify a directory to help disambiguate.', file=sys.stderr)
1141 return True
1142 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -07001143
Ryan Cuiec4d6332011-05-02 14:15:25 -07001144 pwd = os.getcwd()
1145 # hooks assume they are run from the root of the project
1146 os.chdir(proj_dir)
1147
Doug Anderson14749562013-06-26 13:38:29 -07001148 if not commit_list:
1149 try:
1150 commit_list = _get_commits()
1151 except VerifyException as e:
1152 PrintErrorForProject(project, HookFailure(str(e)))
1153 os.chdir(pwd)
1154 return True
Ryan Cuifa55df52011-05-06 11:16:55 -07001155
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001156 hooks = _get_project_hooks(project, presubmit)
Ryan Cui1562fb82011-05-09 11:01:31 -07001157 error_found = False
Ryan Cuifa55df52011-05-06 11:16:55 -07001158 for commit in commit_list:
Ryan Cui1562fb82011-05-09 11:01:31 -07001159 error_list = []
Ryan Cui9b651632011-05-11 11:38:58 -07001160 for hook in hooks:
Ryan Cui1562fb82011-05-09 11:01:31 -07001161 hook_error = hook(project, commit)
1162 if hook_error:
1163 error_list.append(hook_error)
1164 error_found = True
1165 if error_list:
1166 PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
1167 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -07001168
Ryan Cuiec4d6332011-05-02 14:15:25 -07001169 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -07001170 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001171
Mike Frysingerae409522014-02-01 03:16:11 -05001172
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001173# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001174
Ryan Cui1562fb82011-05-09 11:01:31 -07001175
Mike Frysingerae409522014-02-01 03:16:11 -05001176def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -08001177 """Main function invoked directly by repo.
1178
1179 This function will exit directly upon error so that repo doesn't print some
1180 obscure error message.
1181
1182 Args:
1183 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -07001184 worktree_list: A list of directories. It should be the same length as
1185 project_list, so that each entry in project_list matches with a directory
1186 in worktree_list. If None, we will attempt to calculate the directories
1187 automatically.
Doug Anderson06456632012-01-05 11:02:14 -08001188 kwargs: Leave this here for forward-compatibility.
1189 """
Ryan Cui1562fb82011-05-09 11:01:31 -07001190 found_error = False
David James2edd9002013-10-11 14:09:19 -07001191 if not worktree_list:
1192 worktree_list = [None] * len(project_list)
1193 for project, worktree in zip(project_list, worktree_list):
1194 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -07001195 found_error = True
1196
Mike Frysingerae409522014-02-01 03:16:11 -05001197 if found_error:
Ryan Cui1562fb82011-05-09 11:01:31 -07001198 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -07001199 '- To disable some source style checks, and for other hints, see '
1200 '<checkout_dir>/src/repohooks/README\n'
1201 '- To upload only current project, run \'repo upload .\'')
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001202 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -07001203 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -07001204
Ryan Cui1562fb82011-05-09 11:01:31 -07001205
Doug Anderson44a644f2011-11-02 10:37:37 -07001206def _identify_project(path):
1207 """Identify the repo project associated with the given path.
1208
1209 Returns:
1210 A string indicating what project is associated with the path passed in or
1211 a blank string upon failure.
1212 """
1213 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
1214 stderr=subprocess.PIPE, cwd=path).strip()
1215
1216
1217def direct_main(args, verbose=False):
1218 """Run hooks directly (outside of the context of repo).
1219
1220 # Setup for doctests below.
1221 # ...note that some tests assume that running pre-upload on this CWD is fine.
1222 # TODO: Use mock and actually mock out _run_project_hooks() for tests.
1223 >>> mydir = os.path.dirname(os.path.abspath(__file__))
1224 >>> olddir = os.getcwd()
1225
1226 # OK to run w/ no arugments; will run with CWD.
1227 >>> os.chdir(mydir)
1228 >>> direct_main(['prog_name'], verbose=True)
1229 Running hooks on chromiumos/repohooks
1230 0
1231 >>> os.chdir(olddir)
1232
1233 # Run specifying a dir
1234 >>> direct_main(['prog_name', '--dir=%s' % mydir], verbose=True)
1235 Running hooks on chromiumos/repohooks
1236 0
1237
1238 # Not a problem to use a bogus project; we'll just get default settings.
1239 >>> direct_main(['prog_name', '--dir=%s' % mydir, '--project=X'],verbose=True)
1240 Running hooks on X
1241 0
1242
1243 # Run with project but no dir
1244 >>> os.chdir(mydir)
1245 >>> direct_main(['prog_name', '--project=X'], verbose=True)
1246 Running hooks on X
1247 0
1248 >>> os.chdir(olddir)
1249
1250 # Try with a non-git CWD
1251 >>> os.chdir('/tmp')
1252 >>> direct_main(['prog_name'])
1253 Traceback (most recent call last):
1254 ...
1255 BadInvocation: The current directory is not part of a git project.
1256
1257 # Check various bad arguments...
1258 >>> direct_main(['prog_name', 'bogus'])
1259 Traceback (most recent call last):
1260 ...
1261 BadInvocation: Unexpected arguments: bogus
1262 >>> direct_main(['prog_name', '--project=bogus', '--dir=bogusdir'])
1263 Traceback (most recent call last):
1264 ...
1265 BadInvocation: Invalid dir: bogusdir
1266 >>> direct_main(['prog_name', '--project=bogus', '--dir=/tmp'])
1267 Traceback (most recent call last):
1268 ...
1269 BadInvocation: Not a git directory: /tmp
1270
1271 Args:
1272 args: The value of sys.argv
Mike Frysingerae409522014-02-01 03:16:11 -05001273 verbose: Log verbose info while running
Doug Anderson44a644f2011-11-02 10:37:37 -07001274
1275 Returns:
1276 0 if no pre-upload failures, 1 if failures.
1277
1278 Raises:
1279 BadInvocation: On some types of invocation errors.
1280 """
1281 desc = 'Run Chromium OS pre-upload hooks on changes compared to upstream.'
1282 parser = optparse.OptionParser(description=desc)
1283
1284 parser.add_option('--dir', default=None,
1285 help='The directory that the project lives in. If not '
1286 'specified, use the git project root based on the cwd.')
1287 parser.add_option('--project', default=None,
1288 help='The project repo path; this can affect how the hooks '
1289 'get run, since some hooks are project-specific. For '
1290 'chromite this is chromiumos/chromite. If not specified, '
1291 'the repo tool will be used to figure this out based on '
1292 'the dir.')
Doug Anderson14749562013-06-26 13:38:29 -07001293 parser.add_option('--rerun-since', default=None,
1294 help='Rerun hooks on old commits since the given date. '
1295 'The date should match git log\'s concept of a date. '
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001296 'e.g. 2012-06-20. This option is mutually exclusive '
1297 'with --pre-submit.')
1298 parser.add_option('--pre-submit', action="store_true",
1299 help='Run the check against the pending commit. '
1300 'This option should be used at the \'git commit\' '
1301 'phase as opposed to \'repo upload\'. This option '
1302 'is mutually exclusive with --rerun-since.')
Doug Anderson14749562013-06-26 13:38:29 -07001303
1304 parser.usage = "pre-upload.py [options] [commits]"
Doug Anderson44a644f2011-11-02 10:37:37 -07001305
1306 opts, args = parser.parse_args(args[1:])
1307
Doug Anderson14749562013-06-26 13:38:29 -07001308 if opts.rerun_since:
1309 if args:
1310 raise BadInvocation('Can\'t pass commits and use rerun-since: %s' %
1311 ' '.join(args))
1312
1313 cmd = ['git', 'log', '--since="%s"' % opts.rerun_since, '--pretty=%H']
1314 all_commits = _run_command(cmd).splitlines()
1315 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
1316
1317 # Eliminate chrome-bot commits but keep ordering the same...
1318 bot_commits = set(bot_commits)
1319 args = [c for c in all_commits if c not in bot_commits]
1320
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001321 if opts.pre_submit:
1322 raise BadInvocation('rerun-since and pre-submit can not be '
1323 'used together')
1324 if opts.pre_submit:
1325 if args:
1326 raise BadInvocation('Can\'t pass commits and use pre-submit: %s' %
1327 ' '.join(args))
1328 args = [PRE_SUBMIT,]
Doug Anderson44a644f2011-11-02 10:37:37 -07001329
1330 # Check/normlaize git dir; if unspecified, we'll use the root of the git
1331 # project from CWD
1332 if opts.dir is None:
1333 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
1334 stderr=subprocess.PIPE).strip()
1335 if not git_dir:
1336 raise BadInvocation('The current directory is not part of a git project.')
1337 opts.dir = os.path.dirname(os.path.abspath(git_dir))
1338 elif not os.path.isdir(opts.dir):
1339 raise BadInvocation('Invalid dir: %s' % opts.dir)
1340 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
1341 raise BadInvocation('Not a git directory: %s' % opts.dir)
1342
1343 # Identify the project if it wasn't specified; this _requires_ the repo
1344 # tool to be installed and for the project to be part of a repo checkout.
1345 if not opts.project:
1346 opts.project = _identify_project(opts.dir)
1347 if not opts.project:
1348 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
1349
1350 if verbose:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001351 print("Running hooks on %s" % (opts.project))
Doug Anderson44a644f2011-11-02 10:37:37 -07001352
Doug Anderson14749562013-06-26 13:38:29 -07001353 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001354 commit_list=args,
1355 presubmit=opts.pre_submit)
Doug Anderson44a644f2011-11-02 10:37:37 -07001356 if found_error:
1357 return 1
1358 return 0
1359
1360
1361def _test():
1362 """Run any built-in tests."""
1363 import doctest
1364 doctest.testmod()
1365
1366
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001367if __name__ == '__main__':
Doug Anderson44a644f2011-11-02 10:37:37 -07001368 if sys.argv[1:2] == ["--test"]:
1369 _test()
1370 exit_code = 0
1371 else:
1372 prog_name = os.path.basename(sys.argv[0])
1373 try:
1374 exit_code = direct_main(sys.argv)
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -07001375 except BadInvocation, err:
1376 print("%s: %s" % (prog_name, str(err)), file=sys.stderr)
Doug Anderson44a644f2011-11-02 10:37:37 -07001377 exit_code = 1
1378 sys.exit(exit_code)