blob: f6ba150db2a2e194b62a00c6e9c4c359ce944532 [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, [
Vadim Bendeburyac4bc6c2014-08-29 19:56:43 -0700846 '--min-conf-desc-length=2',
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -0700847 '--no-tree',
848 '--ignore=NEW_TYPEDEFS,PREFER_PACKED,PREFER_PRINTF,PREFER_ALIGNED,'
849 'GLOBAL_INITIALISERS,INITIALISED_STATIC'])
850
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700851
Mike Frysingerae409522014-02-01 03:16:11 -0500852def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -0700853 """Makes sure kernel config changes are not mixed with code changes"""
854 files = _get_affected_files(commit)
855 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
856 return HookFailure('Changes to chromeos/config/ and regular files must '
857 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -0700858
Mike Frysingerae409522014-02-01 03:16:11 -0500859
860def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -0700861 """Checks that all JSON files are syntactically valid."""
Dale Curtisa039cfd2011-05-04 12:01:05 -0700862 for f in _filter_files(_get_affected_files(commit), [r'.*\.json']):
Dale Curtis2975c432011-05-03 17:25:20 -0700863 try:
864 json.load(open(f))
865 except Exception, e:
Ryan Cui1562fb82011-05-09 11:01:31 -0700866 return HookFailure('Invalid JSON in %s: %s' % (f, e))
Dale Curtis2975c432011-05-03 17:25:20 -0700867
868
Mike Frysingerae409522014-02-01 03:16:11 -0500869def _check_manifests(_project, commit):
Mike Frysinger52b537e2013-08-22 22:59:53 -0400870 """Make sure Manifest files only have DIST lines"""
871 paths = []
872
873 for path in _get_affected_files(commit):
874 if os.path.basename(path) != 'Manifest':
875 continue
876 if not os.path.exists(path):
877 continue
878
879 with open(path, 'r') as f:
880 for line in f.readlines():
881 if not line.startswith('DIST '):
882 paths.append(path)
883 break
884
885 if paths:
886 return HookFailure('Please remove lines that do not start with DIST:\n%s' %
887 ('\n'.join(paths),))
888
889
Mike Frysingerae409522014-02-01 03:16:11 -0500890def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700891 """Check for a non-empty 'BRANCH=' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700892 if commit == PRE_SUBMIT:
893 return
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700894 BRANCH_RE = r'\nBRANCH=\S+'
895
896 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
897 msg = ('Changelist description needs BRANCH field (after first line)\n'
898 'E.g. BRANCH=none or BRANCH=link,snow')
899 return HookFailure(msg)
900
901
Mike Frysingerae409522014-02-01 03:16:11 -0500902def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800903 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700904 if commit == PRE_SUBMIT:
905 return
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800906 SIGNOFF_RE = r'\nSigned-off-by: \S+'
907
908 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
909 msg = ('Changelist description needs Signed-off-by: field\n'
910 'E.g. Signed-off-by: My Name <me@chromium.org>')
911 return HookFailure(msg)
912
913
Jon Salz3ee59de2012-08-18 13:54:22 +0800914def _run_project_hook_script(script, project, commit):
915 """Runs a project hook script.
916
917 The script is run with the following environment variables set:
918 PRESUBMIT_PROJECT: The affected project
919 PRESUBMIT_COMMIT: The affected commit
920 PRESUBMIT_FILES: A newline-separated list of affected files
921
922 The script is considered to fail if the exit code is non-zero. It should
923 write an error message to stdout.
924 """
925 env = dict(os.environ)
926 env['PRESUBMIT_PROJECT'] = project
927 env['PRESUBMIT_COMMIT'] = commit
928
929 # Put affected files in an environment variable
930 files = _get_affected_files(commit)
931 env['PRESUBMIT_FILES'] = '\n'.join(files)
932
933 process = subprocess.Popen(script, env=env, shell=True,
934 stdin=open(os.devnull),
Jon Salz7b618af2012-08-31 06:03:16 +0800935 stdout=subprocess.PIPE,
936 stderr=subprocess.STDOUT)
Jon Salz3ee59de2012-08-18 13:54:22 +0800937 stdout, _ = process.communicate()
938 if process.wait():
Jon Salz7b618af2012-08-31 06:03:16 +0800939 if stdout:
940 stdout = re.sub('(?m)^', ' ', stdout)
941 return HookFailure('Hook script "%s" failed with code %d%s' %
Jon Salz3ee59de2012-08-18 13:54:22 +0800942 (script, process.returncode,
943 ':\n' + stdout if stdout else ''))
944
945
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -0700946def _moved_to_platform2(project, _commit):
947 """Forbids commits to legacy repo in src/platform."""
948 return HookFailure('%s has been moved to platform2. This change should be '
949 'made there.' % project)
950
951
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -0700952def _check_project_prefix(_project, commit):
953 """Fails if the change is project specific and the commit message is not
954 prefixed by the project_name.
955 """
956
957 files = _get_affected_files(commit, relative=True)
958 prefix = os.path.commonprefix(files)
959 prefix = os.path.dirname(prefix)
960
961 # If there is no common prefix, the CL span multiple projects.
962 if prefix == '':
963 return
964
965 project_name = prefix.split('/')[0]
966 alias_file = os.path.join(prefix, '.project_alias')
967 # If an alias exists, use it.
968 if os.path.isfile(alias_file):
969 with open(alias_file, 'r') as f:
970 project_name = f.read().strip()
971
972 if not _get_commit_desc(commit).startswith(project_name + ': '):
973 return HookFailure('The commit title for changes affecting only %s'
974 ' should start with \"%s: \"'
975 % (project_name, project_name))
976
977
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700978# Base
979
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700980# A list of hooks which are not project specific and check patch description
981# (as opposed to patch body).
982_PATCH_DESCRIPTION_HOOKS = [
Ryan Cui9b651632011-05-11 11:38:58 -0700983 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -0700984 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -0700985 _check_change_has_test_field,
986 _check_change_has_proper_changeid,
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700987]
988
989
990# A list of hooks that are not project-specific
991_COMMON_HOOKS = [
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500992 _check_ebuild_eapi,
Mike Frysinger89bdb852014-02-01 05:26:26 -0500993 _check_ebuild_keywords,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800994 _check_ebuild_licenses,
Mike Frysingercd363c82014-02-01 05:20:18 -0500995 _check_ebuild_virtual_pv,
Ryan Cui9b651632011-05-11 11:38:58 -0700996 _check_no_stray_whitespace,
997 _check_no_long_lines,
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400998 _check_layout_conf,
Ryan Cui9b651632011-05-11 11:38:58 -0700999 _check_license,
1000 _check_no_tabs,
Doug Anderson42b8a052013-06-26 10:45:36 -07001001 _check_for_uprev,
Ryan Cui9b651632011-05-11 11:38:58 -07001002]
Ryan Cuiec4d6332011-05-02 14:15:25 -07001003
Ryan Cui1562fb82011-05-09 11:01:31 -07001004
Ryan Cui9b651632011-05-11 11:38:58 -07001005# A dictionary of project-specific hooks(callbacks), indexed by project name.
1006# dict[project] = [callback1, callback2]
1007_PROJECT_SPECIFIC_HOOKS = {
Mike Frysingerdf980702013-08-22 22:25:22 -04001008 "chromeos/autotest-tools": [_run_json_check],
Mike Frysinger52b537e2013-08-22 22:59:53 -04001009 "chromeos/overlays/chromeos-overlay": [_check_manifests],
1010 "chromeos/overlays/chromeos-partner-overlay": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -08001011 "chromeos/platform/ec-private": [_run_checkpatch_ec,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001012 _check_change_has_branch_field],
Puneet Kumar57b9c092012-08-14 18:58:29 -07001013 "chromeos/third_party/intel-framework": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001014 "chromeos/vendor/kernel-exynos-staging": [_run_checkpatch,
1015 _kernel_configcheck],
Mike Frysinger52b537e2013-08-22 22:59:53 -04001016 "chromiumos/overlays/board-overlays": [_check_manifests],
1017 "chromiumos/overlays/chromiumos-overlay": [_check_manifests],
1018 "chromiumos/overlays/portage-stable": [_check_manifests],
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001019 "chromiumos/platform2": [_check_project_prefix],
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -07001020 "chromiumos/platform/depthcharge": [_check_change_has_signoff_field,
1021 _run_checkpatch_depthcharge],
Randall Spangler7318fd62013-11-21 12:16:58 -08001022 "chromiumos/platform/ec": [_run_checkpatch_ec,
Mike Frysingerdf980702013-08-22 22:25:22 -04001023 _check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001024 "chromiumos/platform/mosys": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001025 "chromiumos/platform/vboot_reference": [_check_change_has_branch_field],
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -07001026 "chromiumos/third_party/coreboot": [_check_change_has_signoff_field,
1027 _run_checkpatch_coreboot],
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001028 "chromiumos/third_party/flashrom": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001029 "chromiumos/third_party/kernel": [_run_checkpatch, _kernel_configcheck],
1030 "chromiumos/third_party/kernel-next": [_run_checkpatch,
1031 _kernel_configcheck],
Vadim Bendebury203fbc22014-04-22 11:58:14 -07001032 "chromiumos/third_party/u-boot": [_run_checkpatch_no_tree],
Ryan Cui9b651632011-05-11 11:38:58 -07001033}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001034
Ryan Cui1562fb82011-05-09 11:01:31 -07001035
Ryan Cui9b651632011-05-11 11:38:58 -07001036# A dictionary of flags (keys) that can appear in the config file, and the hook
1037# that the flag disables (value)
1038_DISABLE_FLAGS = {
1039 'stray_whitespace_check': _check_no_stray_whitespace,
1040 'long_line_check': _check_no_long_lines,
1041 'cros_license_check': _check_license,
1042 'tab_check': _check_no_tabs,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001043 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001044 'signoff_check': _check_change_has_signoff_field,
Josh Triplett0e8fc7f2014-04-23 16:00:00 -07001045 'bug_field_check': _check_change_has_bug_field,
1046 'test_field_check': _check_change_has_test_field,
Ryan Cui9b651632011-05-11 11:38:58 -07001047}
1048
1049
Jon Salz3ee59de2012-08-18 13:54:22 +08001050def _get_disabled_hooks(config):
Ryan Cui9b651632011-05-11 11:38:58 -07001051 """Returns a set of hooks disabled by the current project's config file.
1052
1053 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +08001054
1055 Args:
1056 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07001057 """
1058 SECTION = 'Hook Overrides'
Jon Salz3ee59de2012-08-18 13:54:22 +08001059 if not config.has_section(SECTION):
1060 return set()
Ryan Cui9b651632011-05-11 11:38:58 -07001061
1062 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +08001063 for flag in config.options(SECTION):
Ryan Cui9b651632011-05-11 11:38:58 -07001064 try:
Mike Frysingerae409522014-02-01 03:16:11 -05001065 if not config.getboolean(SECTION, flag):
1066 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -07001067 except ValueError as e:
1068 msg = "Error parsing flag \'%s\' in %s file - " % (flag, _CONFIG_FILE)
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001069 print(msg + str(e))
Ryan Cui9b651632011-05-11 11:38:58 -07001070
1071 disabled_keys = set(_DISABLE_FLAGS.iterkeys()).intersection(disable_flags)
1072 return set([_DISABLE_FLAGS[key] for key in disabled_keys])
1073
1074
Jon Salz3ee59de2012-08-18 13:54:22 +08001075def _get_project_hook_scripts(config):
1076 """Returns a list of project-specific hook scripts.
1077
1078 Args:
1079 config: A ConfigParser for the project's config file.
1080 """
1081 SECTION = 'Hook Scripts'
1082 if not config.has_section(SECTION):
1083 return []
1084
1085 hook_names_values = config.items(SECTION)
1086 hook_names_values.sort(key=lambda x: x[0])
1087 return [x[1] for x in hook_names_values]
1088
1089
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001090def _get_project_hooks(project, presubmit):
Ryan Cui9b651632011-05-11 11:38:58 -07001091 """Returns a list of hooks that need to be run for a project.
1092
1093 Expects to be called from within the project root.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001094
1095 Args:
1096 project: A string, name of the project.
1097 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui9b651632011-05-11 11:38:58 -07001098 """
Jon Salz3ee59de2012-08-18 13:54:22 +08001099 config = ConfigParser.RawConfigParser()
1100 try:
1101 config.read(_CONFIG_FILE)
1102 except ConfigParser.Error:
1103 # Just use an empty config file
1104 config = ConfigParser.RawConfigParser()
1105
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001106 if presubmit:
1107 hook_list = _COMMON_HOOKS
1108 else:
1109 hook_list = _PATCH_DESCRIPTION_HOOKS + _COMMON_HOOKS
1110
Jon Salz3ee59de2012-08-18 13:54:22 +08001111 disabled_hooks = _get_disabled_hooks(config)
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001112 hooks = [hook for hook in hook_list if hook not in disabled_hooks]
Ryan Cui9b651632011-05-11 11:38:58 -07001113
1114 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001115 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
1116 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -07001117
Jon Salz3ee59de2012-08-18 13:54:22 +08001118 for script in _get_project_hook_scripts(config):
1119 hooks.append(functools.partial(_run_project_hook_script, script))
1120
Ryan Cui9b651632011-05-11 11:38:58 -07001121 return hooks
1122
1123
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001124def _run_project_hooks(project, proj_dir=None,
1125 commit_list=None, presubmit=False):
Ryan Cui1562fb82011-05-09 11:01:31 -07001126 """For each project run its project specific hook from the hooks dictionary.
1127
1128 Args:
Doug Anderson44a644f2011-11-02 10:37:37 -07001129 project: The name of project to run hooks for.
1130 proj_dir: If non-None, this is the directory the project is in. If None,
1131 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -07001132 commit_list: A list of commits to run hooks against. If None or empty list
1133 then we'll automatically get the list of commits that would be uploaded.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001134 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui1562fb82011-05-09 11:01:31 -07001135
1136 Returns:
1137 Boolean value of whether any errors were ecountered while running the hooks.
1138 """
Doug Anderson44a644f2011-11-02 10:37:37 -07001139 if proj_dir is None:
David James2edd9002013-10-11 14:09:19 -07001140 proj_dirs = _run_command(['repo', 'forall', project, '-c', 'pwd']).split()
1141 if len(proj_dirs) == 0:
1142 print('%s cannot be found.' % project, file=sys.stderr)
1143 print('Please specify a valid project.', file=sys.stderr)
1144 return True
1145 if len(proj_dirs) > 1:
1146 print('%s is associated with multiple directories.' % project,
1147 file=sys.stderr)
1148 print('Please specify a directory to help disambiguate.', file=sys.stderr)
1149 return True
1150 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -07001151
Ryan Cuiec4d6332011-05-02 14:15:25 -07001152 pwd = os.getcwd()
1153 # hooks assume they are run from the root of the project
1154 os.chdir(proj_dir)
1155
Doug Anderson14749562013-06-26 13:38:29 -07001156 if not commit_list:
1157 try:
1158 commit_list = _get_commits()
1159 except VerifyException as e:
1160 PrintErrorForProject(project, HookFailure(str(e)))
1161 os.chdir(pwd)
1162 return True
Ryan Cuifa55df52011-05-06 11:16:55 -07001163
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001164 hooks = _get_project_hooks(project, presubmit)
Ryan Cui1562fb82011-05-09 11:01:31 -07001165 error_found = False
Ryan Cuifa55df52011-05-06 11:16:55 -07001166 for commit in commit_list:
Ryan Cui1562fb82011-05-09 11:01:31 -07001167 error_list = []
Ryan Cui9b651632011-05-11 11:38:58 -07001168 for hook in hooks:
Ryan Cui1562fb82011-05-09 11:01:31 -07001169 hook_error = hook(project, commit)
1170 if hook_error:
1171 error_list.append(hook_error)
1172 error_found = True
1173 if error_list:
1174 PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
1175 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -07001176
Ryan Cuiec4d6332011-05-02 14:15:25 -07001177 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -07001178 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001179
Mike Frysingerae409522014-02-01 03:16:11 -05001180
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001181# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001182
Ryan Cui1562fb82011-05-09 11:01:31 -07001183
Mike Frysingerae409522014-02-01 03:16:11 -05001184def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -08001185 """Main function invoked directly by repo.
1186
1187 This function will exit directly upon error so that repo doesn't print some
1188 obscure error message.
1189
1190 Args:
1191 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -07001192 worktree_list: A list of directories. It should be the same length as
1193 project_list, so that each entry in project_list matches with a directory
1194 in worktree_list. If None, we will attempt to calculate the directories
1195 automatically.
Doug Anderson06456632012-01-05 11:02:14 -08001196 kwargs: Leave this here for forward-compatibility.
1197 """
Ryan Cui1562fb82011-05-09 11:01:31 -07001198 found_error = False
David James2edd9002013-10-11 14:09:19 -07001199 if not worktree_list:
1200 worktree_list = [None] * len(project_list)
1201 for project, worktree in zip(project_list, worktree_list):
1202 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -07001203 found_error = True
1204
Mike Frysingerae409522014-02-01 03:16:11 -05001205 if found_error:
Ryan Cui1562fb82011-05-09 11:01:31 -07001206 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -07001207 '- To disable some source style checks, and for other hints, see '
1208 '<checkout_dir>/src/repohooks/README\n'
1209 '- To upload only current project, run \'repo upload .\'')
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001210 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -07001211 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -07001212
Ryan Cui1562fb82011-05-09 11:01:31 -07001213
Doug Anderson44a644f2011-11-02 10:37:37 -07001214def _identify_project(path):
1215 """Identify the repo project associated with the given path.
1216
1217 Returns:
1218 A string indicating what project is associated with the path passed in or
1219 a blank string upon failure.
1220 """
1221 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
1222 stderr=subprocess.PIPE, cwd=path).strip()
1223
1224
1225def direct_main(args, verbose=False):
1226 """Run hooks directly (outside of the context of repo).
1227
1228 # Setup for doctests below.
1229 # ...note that some tests assume that running pre-upload on this CWD is fine.
1230 # TODO: Use mock and actually mock out _run_project_hooks() for tests.
1231 >>> mydir = os.path.dirname(os.path.abspath(__file__))
1232 >>> olddir = os.getcwd()
1233
1234 # OK to run w/ no arugments; will run with CWD.
1235 >>> os.chdir(mydir)
1236 >>> direct_main(['prog_name'], verbose=True)
1237 Running hooks on chromiumos/repohooks
1238 0
1239 >>> os.chdir(olddir)
1240
1241 # Run specifying a dir
1242 >>> direct_main(['prog_name', '--dir=%s' % mydir], verbose=True)
1243 Running hooks on chromiumos/repohooks
1244 0
1245
1246 # Not a problem to use a bogus project; we'll just get default settings.
1247 >>> direct_main(['prog_name', '--dir=%s' % mydir, '--project=X'],verbose=True)
1248 Running hooks on X
1249 0
1250
1251 # Run with project but no dir
1252 >>> os.chdir(mydir)
1253 >>> direct_main(['prog_name', '--project=X'], verbose=True)
1254 Running hooks on X
1255 0
1256 >>> os.chdir(olddir)
1257
1258 # Try with a non-git CWD
1259 >>> os.chdir('/tmp')
1260 >>> direct_main(['prog_name'])
1261 Traceback (most recent call last):
1262 ...
1263 BadInvocation: The current directory is not part of a git project.
1264
1265 # Check various bad arguments...
1266 >>> direct_main(['prog_name', 'bogus'])
1267 Traceback (most recent call last):
1268 ...
1269 BadInvocation: Unexpected arguments: bogus
1270 >>> direct_main(['prog_name', '--project=bogus', '--dir=bogusdir'])
1271 Traceback (most recent call last):
1272 ...
1273 BadInvocation: Invalid dir: bogusdir
1274 >>> direct_main(['prog_name', '--project=bogus', '--dir=/tmp'])
1275 Traceback (most recent call last):
1276 ...
1277 BadInvocation: Not a git directory: /tmp
1278
1279 Args:
1280 args: The value of sys.argv
Mike Frysingerae409522014-02-01 03:16:11 -05001281 verbose: Log verbose info while running
Doug Anderson44a644f2011-11-02 10:37:37 -07001282
1283 Returns:
1284 0 if no pre-upload failures, 1 if failures.
1285
1286 Raises:
1287 BadInvocation: On some types of invocation errors.
1288 """
1289 desc = 'Run Chromium OS pre-upload hooks on changes compared to upstream.'
1290 parser = optparse.OptionParser(description=desc)
1291
1292 parser.add_option('--dir', default=None,
1293 help='The directory that the project lives in. If not '
1294 'specified, use the git project root based on the cwd.')
1295 parser.add_option('--project', default=None,
1296 help='The project repo path; this can affect how the hooks '
1297 'get run, since some hooks are project-specific. For '
1298 'chromite this is chromiumos/chromite. If not specified, '
1299 'the repo tool will be used to figure this out based on '
1300 'the dir.')
Doug Anderson14749562013-06-26 13:38:29 -07001301 parser.add_option('--rerun-since', default=None,
1302 help='Rerun hooks on old commits since the given date. '
1303 'The date should match git log\'s concept of a date. '
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001304 'e.g. 2012-06-20. This option is mutually exclusive '
1305 'with --pre-submit.')
1306 parser.add_option('--pre-submit', action="store_true",
1307 help='Run the check against the pending commit. '
1308 'This option should be used at the \'git commit\' '
1309 'phase as opposed to \'repo upload\'. This option '
1310 'is mutually exclusive with --rerun-since.')
Doug Anderson14749562013-06-26 13:38:29 -07001311
1312 parser.usage = "pre-upload.py [options] [commits]"
Doug Anderson44a644f2011-11-02 10:37:37 -07001313
1314 opts, args = parser.parse_args(args[1:])
1315
Doug Anderson14749562013-06-26 13:38:29 -07001316 if opts.rerun_since:
1317 if args:
1318 raise BadInvocation('Can\'t pass commits and use rerun-since: %s' %
1319 ' '.join(args))
1320
1321 cmd = ['git', 'log', '--since="%s"' % opts.rerun_since, '--pretty=%H']
1322 all_commits = _run_command(cmd).splitlines()
1323 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
1324
1325 # Eliminate chrome-bot commits but keep ordering the same...
1326 bot_commits = set(bot_commits)
1327 args = [c for c in all_commits if c not in bot_commits]
1328
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001329 if opts.pre_submit:
1330 raise BadInvocation('rerun-since and pre-submit can not be '
1331 'used together')
1332 if opts.pre_submit:
1333 if args:
1334 raise BadInvocation('Can\'t pass commits and use pre-submit: %s' %
1335 ' '.join(args))
1336 args = [PRE_SUBMIT,]
Doug Anderson44a644f2011-11-02 10:37:37 -07001337
1338 # Check/normlaize git dir; if unspecified, we'll use the root of the git
1339 # project from CWD
1340 if opts.dir is None:
1341 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
1342 stderr=subprocess.PIPE).strip()
1343 if not git_dir:
1344 raise BadInvocation('The current directory is not part of a git project.')
1345 opts.dir = os.path.dirname(os.path.abspath(git_dir))
1346 elif not os.path.isdir(opts.dir):
1347 raise BadInvocation('Invalid dir: %s' % opts.dir)
1348 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
1349 raise BadInvocation('Not a git directory: %s' % opts.dir)
1350
1351 # Identify the project if it wasn't specified; this _requires_ the repo
1352 # tool to be installed and for the project to be part of a repo checkout.
1353 if not opts.project:
1354 opts.project = _identify_project(opts.dir)
1355 if not opts.project:
1356 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
1357
1358 if verbose:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001359 print("Running hooks on %s" % (opts.project))
Doug Anderson44a644f2011-11-02 10:37:37 -07001360
Doug Anderson14749562013-06-26 13:38:29 -07001361 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001362 commit_list=args,
1363 presubmit=opts.pre_submit)
Doug Anderson44a644f2011-11-02 10:37:37 -07001364 if found_error:
1365 return 1
1366 return 0
1367
1368
1369def _test():
1370 """Run any built-in tests."""
1371 import doctest
1372 doctest.testmod()
1373
1374
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001375if __name__ == '__main__':
Doug Anderson44a644f2011-11-02 10:37:37 -07001376 if sys.argv[1:2] == ["--test"]:
1377 _test()
1378 exit_code = 0
1379 else:
1380 prog_name = os.path.basename(sys.argv[0])
1381 try:
1382 exit_code = direct_main(sys.argv)
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -07001383 except BadInvocation, err:
1384 print("%s: %s" % (prog_name, str(err)), file=sys.stderr)
Doug Anderson44a644f2011-11-02 10:37:37 -07001385 exit_code = 1
1386 sys.exit(exit_code)