| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Makes sure files have the right permissions. | 6 """Makes sure files have the right permissions. |
| 7 | 7 |
| 8 Some developers have broken SCM configurations that flip the svn:executable | 8 Some developers have broken SCM configurations that flip the svn:executable |
| 9 permission on for no good reason. Unix developers who run ls --color will then | 9 permission on for no good reason. Unix developers who run ls --color will then |
| 10 see .cc files in green and get confused. | 10 see .cc files in green and get confused. |
| 11 | 11 |
| 12 To ignore a particular file, add it to WHITELIST_FILES. | 12 To ignore a particular file, add it to WHITELIST_FILES. |
| 13 To ignore a particular extension, add it to WHITELIST_EXTENSIONS. | 13 To ignore a particular extension, add it to WHITELIST_EXTENSIONS. |
| 14 To ignore whatever regexps your heart desires, add it WHITELIST_REGEX. | 14 To ignore a particular path, add it WHITELIST_PATHS. |
| 15 | 15 |
| 16 Note that all directory separators must be slashes (Unix-style) and not | 16 Note that all directory separators must be slashes (Unix-style) and not |
| 17 backslashes. All directories should be relative to the source root and all | 17 backslashes. All directories should be relative to the source root and all |
| 18 file paths should be only lowercase. | 18 file paths should be only lowercase. |
| 19 """ | 19 """ |
| 20 | 20 |
| 21 import logging |
| 21 import optparse | 22 import optparse |
| 22 import os | 23 import os |
| 23 import pipes | |
| 24 import re | |
| 25 import stat | 24 import stat |
| 25 import subprocess |
| 26 import sys | 26 import sys |
| 27 | 27 |
| 28 #### USER EDITABLE SECTION STARTS HERE #### | 28 #### USER EDITABLE SECTION STARTS HERE #### |
| 29 | 29 |
| 30 # Files with these extensions are allowed to have executable permissions. | 30 # Files with these extensions are allowed to have executable permissions. |
| 31 WHITELIST_EXTENSIONS = [ | 31 WHITELIST_EXTENSIONS = ( |
| 32 'bash', | 32 'bat', |
| 33 'bat', | 33 'dll', |
| 34 'dll', | 34 'dylib', |
| 35 'dylib', | 35 'exe', |
| 36 'exe', | 36 ) |
| 37 'pl', | |
| 38 'py', | |
| 39 'rb', | |
| 40 'sed', | |
| 41 'sh', | |
| 42 ] | |
| 43 | |
| 44 # Files that end the following paths are whitelisted too. | |
| 45 WHITELIST_FILES = [ | |
| 46 '/build/gyp_chromium', | |
| 47 '/build/linux/dump_app_syms', | |
| 48 '/build/linux/pkg-config-wrapper', | |
| 49 '/build/mac/strip_from_xcode', | |
| 50 '/build/mac/strip_save_dsym', | |
| 51 '/chrome/installer/mac/pkg-dmg', | |
| 52 '/chrome/tools/build/linux/chrome-wrapper', | |
| 53 '/chrome/tools/build/mac/build_app_dmg', | |
| 54 '/chrome/tools/build/mac/clean_up_old_versions', | |
| 55 '/chrome/tools/build/mac/dump_product_syms', | |
| 56 '/chrome/tools/build/mac/generate_localizer', | |
| 57 '/chrome/tools/build/mac/make_sign_sh', | |
| 58 '/chrome/tools/build/mac/verify_order', | |
| 59 '/o3d/build/gyp_o3d', | |
| 60 '/o3d/gypbuild', | |
| 61 '/o3d/installer/linux/debian.in/rules', | |
| 62 '/third_party/icu/source/runconfigureicu', | |
| 63 '/third_party/gold/gold32', | |
| 64 '/third_party/gold/gold64', | |
| 65 '/third_party/gold/ld', | |
| 66 '/third_party/gold/ld.bfd', | |
| 67 '/third_party/lcov/bin/gendesc', | |
| 68 '/third_party/lcov/bin/genhtml', | |
| 69 '/third_party/lcov/bin/geninfo', | |
| 70 '/third_party/lcov/bin/genpng', | |
| 71 '/third_party/lcov/bin/lcov', | |
| 72 '/third_party/lcov/bin/mcov', | |
| 73 '/third_party/lcov-1.9/bin/gendesc', | |
| 74 '/third_party/lcov-1.9/bin/genhtml', | |
| 75 '/third_party/lcov-1.9/bin/geninfo', | |
| 76 '/third_party/lcov-1.9/bin/genpng', | |
| 77 '/third_party/lcov-1.9/bin/lcov', | |
| 78 '/third_party/libxml/linux/xml2-config', | |
| 79 '/third_party/lzma_sdk/executable/7za.exe', | |
| 80 '/third_party/swig/linux/swig', | |
| 81 '/third_party/tcmalloc/chromium/src/pprof', | |
| 82 '/tools/deep_memory_profiler/dmprof', | |
| 83 '/tools/git/post-checkout', | |
| 84 '/tools/git/post-merge', | |
| 85 '/tools/ld_bfd/ld', | |
| 86 ] | |
| 87 | 37 |
| 88 # File names that are always whitelisted. (These are all autoconf spew.) | 38 # File names that are always whitelisted. (These are all autoconf spew.) |
| 89 WHITELIST_FILENAMES = set(( | 39 WHITELIST_FILENAMES = set(( |
| 90 'config.guess', | 40 'config.guess', |
| 91 'config.sub', | 41 'config.sub', |
| 92 'configure', | 42 'configure', |
| 93 'depcomp', | 43 'depcomp', |
| 94 'install-sh', | 44 'install-sh', |
| 95 'missing', | 45 'missing', |
| 96 'mkinstalldirs', | 46 'mkinstalldirs', |
| 47 'naclsdk', |
| 97 'scons', | 48 'scons', |
| 98 'naclsdk', | |
| 99 )) | 49 )) |
| 100 | 50 |
| 101 # File paths that contain these regexps will be whitelisted as well. | 51 # File paths that contain these regexps will be whitelisted as well. |
| 102 WHITELIST_REGEX = [ | 52 WHITELIST_PATHS = ( |
| 103 re.compile('/third_party/openssl/'), | 53 '/third_party/openssl/', |
| 104 re.compile('/third_party/sqlite/'), | 54 '/third_party/sqlite/', |
| 105 re.compile('/third_party/xdg-utils/'), | 55 '/third_party/xdg-utils/', |
| 106 re.compile('/third_party/yasm/source/patched-yasm/config'), | 56 '/third_party/yasm/source/patched-yasm/config', |
| 107 re.compile('/third_party/ffmpeg/tools'), | 57 '/third_party/ffmpeg/tools', |
| 108 ] | 58 ) |
| 109 | 59 |
| 110 #### USER EDITABLE SECTION ENDS HERE #### | 60 #### USER EDITABLE SECTION ENDS HERE #### |
| 111 | 61 |
| 112 WHITELIST_EXTENSIONS_REGEX = re.compile(r'\.(%s)' % | |
| 113 '|'.join(WHITELIST_EXTENSIONS)) | |
| 114 | 62 |
| 115 WHITELIST_FILES_REGEX = re.compile(r'(%s)$' % '|'.join(WHITELIST_FILES)) | 63 def is_verbose(): |
| 116 | 64 return logging.getLogger().isEnabledFor(logging.DEBUG) |
| 117 # Set to true for more output. This is set by the command line options. | |
| 118 VERBOSE = False | |
| 119 | |
| 120 # Using forward slashes as directory separators, ending in a forward slash. | |
| 121 # Set by the command line options. | |
| 122 BASE_DIRECTORY = '' | |
| 123 | |
| 124 # The default if BASE_DIRECTORY is not set on the command line. | |
| 125 DEFAULT_BASE_DIRECTORY = '../../..' | |
| 126 | |
| 127 # The directories which contain the sources managed by git. | |
| 128 GIT_SOURCE_DIRECTORY = set() | |
| 129 | |
| 130 # The SVN repository url. | |
| 131 SVN_REPO_URL = '' | |
| 132 | |
| 133 # Whether we are using SVN or GIT. | |
| 134 IS_SVN = True | |
| 135 | |
| 136 # Executable permission mask | |
| 137 EXECUTABLE_PERMISSION = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH | |
| 138 | 65 |
| 139 | 66 |
| 140 def IsWhiteListed(file_path): | 67 def capture(cmd, cwd): |
| 141 """Returns True if file_path is in our whitelist of files to ignore.""" | 68 """Returns the output of a command. |
| 142 if WHITELIST_EXTENSIONS_REGEX.match(os.path.splitext(file_path)[1]): | 69 |
| 143 return True | 70 Ignores the error code or stderr. |
| 144 if WHITELIST_FILES_REGEX.search(file_path): | 71 """ |
| 145 return True | 72 env = os.environ.copy() |
| 146 if os.path.basename(file_path) in WHITELIST_FILENAMES: | 73 env['LANG'] = 'en_us.UTF-8' |
| 147 return True | 74 p = subprocess.Popen( |
| 148 for regex in WHITELIST_REGEX: | 75 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd, env=env) |
| 149 if regex.search(file_path): | 76 return p.communicate()[0] |
| 150 return True | |
| 151 return False | |
| 152 | 77 |
| 153 | 78 |
| 154 def CheckFile(file_path): | 79 class ApiBase(object): |
| 155 """Checks file_path's permissions. | 80 def __init__(self, root_dir): |
| 81 self.root_dir = root_dir |
| 156 | 82 |
| 157 Args: | 83 @staticmethod |
| 158 file_path: The file path to check. | 84 def is_whitelisted(file_path): |
| 85 """Returns True if file_path is in our whitelist of files to ignore. |
| 159 | 86 |
| 160 Returns: | 87 file_path is a relative directory to the checkout root. |
| 161 Either a string describing the error if there was one, or None if the file | 88 """ |
| 162 checked out OK. | 89 file_path = file_path.lower() |
| 163 """ | 90 return ( |
| 164 if VERBOSE: | 91 os.path.splitext(file_path)[1][1:] in WHITELIST_EXTENSIONS or |
| 165 print 'Checking file: ' + file_path | 92 os.path.basename(file_path) in WHITELIST_FILENAMES or |
| 93 file_path.startswith(WHITELIST_PATHS)) |
| 166 | 94 |
| 167 file_path_lower = file_path.lower() | 95 @staticmethod |
| 168 if IsWhiteListed(file_path_lower): | 96 def has_executable_bit(file_path): |
| 169 return None | 97 """Returns if any executable bit is set. |
| 170 | 98 |
| 171 # Not whitelisted, stat the file and check permissions. | 99 file_path is the absolute path to the file. |
| 172 try: | 100 """ |
| 173 st_mode = os.stat(file_path).st_mode | 101 permission = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH |
| 174 except IOError, e: | 102 return bool(permission & os.stat(file_path).st_mode) |
| 175 return 'Failed to stat file: %s' % e | |
| 176 except OSError, e: | |
| 177 return 'Failed to stat file: %s' % e | |
| 178 | 103 |
| 179 if EXECUTABLE_PERMISSION & st_mode: | 104 @staticmethod |
| 180 # Look if the file starts with #!/ | 105 def has_shebang(file_path): |
| 106 """Returns if the file starts with #!/. |
| 107 |
| 108 file_path is the absolute path to the file. |
| 109 """ |
| 181 with open(file_path, 'rb') as f: | 110 with open(file_path, 'rb') as f: |
| 182 if f.read(3) == '#!/': | 111 return f.read(3) == '#!/' |
| 183 # That's fine. | 112 |
| 184 return None | 113 def check_file(self, file_path): |
| 185 # TODO(maruel): Check that non-executable file do not start with a shebang. | 114 """Checks file_path's permissions and returns an error if it is |
| 186 error = 'Contains executable permission' | 115 inconsistent. |
| 187 if VERBOSE: | 116 """ |
| 188 return '%s: %06o' % (error, st_mode) | 117 if is_verbose(): |
| 189 return error | 118 print 'Checking file: %s' % file_path |
| 190 return None | 119 |
| 120 if self.is_whitelisted(file_path): |
| 121 return None |
| 122 |
| 123 full_path = os.path.join(self.root_dir, file_path) |
| 124 bit = self.has_executable_bit(full_path) |
| 125 shebang = self.has_shebang(full_path) |
| 126 if bit != shebang: |
| 127 if bit: |
| 128 return '%s: Has executable bit but not shebang' % file_path |
| 129 else: |
| 130 return '%s: Has shebang but not executable bit' % file_path |
| 131 |
| 132 def check(self, start_dir): |
| 133 """Check the files in start_dir, recursively check its subdirectories.""" |
| 134 errors = [] |
| 135 for root, dirs, files in os.walk(start_dir): |
| 136 for f in files: |
| 137 error = check_file(root_dir, os.path.join(root, f)) |
| 138 if error: |
| 139 errors.append(error) |
| 140 return errors |
| 191 | 141 |
| 192 | 142 |
| 193 def ShouldCheckDirectory(dir_path): | 143 class ApiSvn(ApiBase): |
| 194 """Determine if we should check the content of dir_path.""" | 144 @staticmethod |
| 195 if not IS_SVN: | 145 def get_info(dir_path): |
| 196 return dir_path in GIT_SOURCE_DIRECTORY | 146 """Returns svn meta-data for a svn checkout.""" |
| 197 repo_url = GetSvnRepositoryRoot(dir_path) | 147 if not os.path.isdir(dir_path): |
| 198 if not repo_url: | 148 return {} |
| 199 return False | 149 out = capture(['svn', 'info', '.'], dir_path) |
| 200 return repo_url == SVN_REPO_URL | 150 return dict(l.split(': ', 1) for l in out.splitlines() if l) |
| 151 |
| 152 @staticmethod |
| 153 def get_root(dir_path): |
| 154 """Returns the svn checkout root or None.""" |
| 155 svn_url = get_svn_info(dir_path).get('Repository Root:') |
| 156 if not svn_url: |
| 157 return None |
| 158 while True: |
| 159 parent = os.path.dirname(dir_path) |
| 160 if parent == dir_path: |
| 161 return None |
| 162 if svn_url != get_svn_info(parent).get('Repository Root:'): |
| 163 return dir_path |
| 164 dir_path = parent |
| 165 |
| 166 def check(self, start_dir): |
| 167 """Like ApiBase.check() excepts that it skips non-versioned directories.""" |
| 168 return super(ApiSvn, self).check(start_dir) |
| 201 | 169 |
| 202 | 170 |
| 203 def CheckDirectory(dir_path): | 171 class ApiGit(ApiBase): |
| 204 """Check the files in dir_path; recursively check its subdirectories.""" | 172 @staticmethod |
| 205 # Collect a list of all files and directories to check. | 173 def get_root(dir_path): |
| 206 files_to_check = [] | 174 """Returns the git checkout root or None.""" |
| 207 dirs_to_check = [] | 175 root = capture(['git', 'rev-parse', '--show-toplevel'], dir_path).strip() |
| 208 success = True | 176 if root: |
| 209 contents = os.listdir(dir_path) | 177 return root |
| 210 for cur in contents: | |
| 211 full_path = os.path.join(dir_path, cur) | |
| 212 if os.path.isdir(full_path) and ShouldCheckDirectory(full_path): | |
| 213 dirs_to_check.append(full_path) | |
| 214 elif os.path.isfile(full_path): | |
| 215 files_to_check.append(full_path) | |
| 216 | 178 |
| 217 # First check all files in this directory. | 179 def check(self, start_dir): |
| 218 for cur_file in files_to_check: | 180 """Like ApiBase.check() excepts that it skips non-versioned directories.""" |
| 219 file_status = CheckFile(cur_file) | 181 return super(ApiSvn, self).check(start_dir) |
| 220 if file_status is not None: | |
| 221 print 'ERROR in %s\n%s' % (cur_file, file_status) | |
| 222 success = False | |
| 223 | |
| 224 # Next recurse into the subdirectories. | |
| 225 for cur_dir in dirs_to_check: | |
| 226 if not CheckDirectory(cur_dir): | |
| 227 success = False | |
| 228 return success | |
| 229 | 182 |
| 230 | 183 |
| 231 def GetGitSourceDirectory(root): | 184 def get_scm(dir_path): |
| 232 """Returns a set of the directories to be checked. | 185 """Returns a properly configured ApiBase instance.""" |
| 186 root = ApiSvn.get_root(dir_path) |
| 187 if root: |
| 188 return ApiSvn(root) |
| 189 root = ApiGit.get_root(dir_path) |
| 190 if root: |
| 191 return ApiGit(root) |
| 233 | 192 |
| 234 Args: | 193 # Returns a non-scm aware checker. |
| 235 root: The repository root where a .git directory must exist. | 194 logging.warn('Failed to determine the SCM for %s' % dir_path) |
| 236 | 195 return ApiBase(root) |
| 237 Returns: | |
| 238 A set of directories which contain sources managed by git. | |
| 239 """ | |
| 240 git_source_directory = set() | |
| 241 popen_out = os.popen('cd %s && git ls-files --full-name .' % | |
| 242 pipes.quote(root)) | |
| 243 for line in popen_out: | |
| 244 dir_path = os.path.join(root, os.path.dirname(line)) | |
| 245 git_source_directory.add(dir_path) | |
| 246 git_source_directory.add(root) | |
| 247 return git_source_directory | |
| 248 | 196 |
| 249 | 197 |
| 250 def GetSvnRepositoryRoot(dir_path): | 198 def main(): |
| 251 """Returns the repository root for a directory. | |
| 252 | |
| 253 Args: | |
| 254 dir_path: A directory where a .svn subdirectory should exist. | |
| 255 | |
| 256 Returns: | |
| 257 The svn repository that contains dir_path or None. | |
| 258 """ | |
| 259 svn_dir = os.path.join(dir_path, '.svn') | |
| 260 if not os.path.isdir(svn_dir): | |
| 261 return None | |
| 262 popen_out = os.popen('cd %s && svn info' % pipes.quote(dir_path)) | |
| 263 for line in popen_out: | |
| 264 if line.startswith('Repository Root: '): | |
| 265 return line[len('Repository Root: '):].rstrip() | |
| 266 return None | |
| 267 | |
| 268 | |
| 269 def main(argv): | |
| 270 usage = """Usage: python %prog [--root <root>] [tocheck] | 199 usage = """Usage: python %prog [--root <root>] [tocheck] |
| 271 tocheck Specifies the directory, relative to root, to check. This defaults | 200 tocheck Specifies the directory, relative to root, to check. This defaults |
| 272 to "." so it checks everything. | 201 to "." so it checks everything. |
| 273 | 202 |
| 274 Examples: | 203 Examples: |
| 275 python checkperms.py | 204 python %prog |
| 276 python checkperms.py --root /path/to/source chrome""" | 205 python %prog --root /path/to/source chrome""" |
| 277 | 206 |
| 278 option_parser = optparse.OptionParser(usage=usage) | 207 parser = optparse.OptionParser(usage=usage) |
| 279 option_parser.add_option('--root', dest='base_directory', | 208 parser.add_option( |
| 280 default=DEFAULT_BASE_DIRECTORY, | 209 '--root', |
| 281 help='Specifies the repository root. This defaults ' | 210 default=get_scm('.')[0], |
| 282 'to %default relative to the script file, which ' | 211 help='Specifies the repository root. This defaults ' |
| 283 'will normally be the repository root.') | 212 'to %default relative to the script file, which ' |
| 284 option_parser.add_option('-v', '--verbose', action='store_true', | 213 'will normally be the repository root.') |
| 285 help='Print debug logging') | 214 parser.add_option( |
| 286 options, args = option_parser.parse_args() | 215 '-v', '--verbose', action='store_true', help='Print debug logging') |
| 216 options, args = parser.parse_args() |
| 287 | 217 |
| 288 global VERBOSE | 218 logging.basicConfig( |
| 289 if options.verbose: | 219 level=(logging.DEBUG if options.verbose else logging.ERROR)) |
| 290 VERBOSE = True | |
| 291 | 220 |
| 292 # Optional base directory of the repository. | 221 if len(args) > 1: |
| 293 global BASE_DIRECTORY | 222 parser.error('Too many arguments used') |
| 294 if (not options.base_directory or | 223 if args: |
| 295 options.base_directory == DEFAULT_BASE_DIRECTORY): | 224 start_dir = args[0] |
| 296 BASE_DIRECTORY = os.path.abspath( | |
| 297 os.path.join(os.path.abspath(argv[0]), DEFAULT_BASE_DIRECTORY)) | |
| 298 else: | |
| 299 BASE_DIRECTORY = os.path.abspath(argv[2]) | |
| 300 | 225 |
| 301 # Figure out which directory we have to check. | 226 if not options.root: |
| 302 if not args: | 227 parser.error('Must specify --root') |
| 303 # No directory to check specified, use the repository root. | 228 options.root = os.path.abspath(options.root) |
| 304 start_dir = BASE_DIRECTORY | |
| 305 elif len(args) == 1: | |
| 306 # Directory specified. Start here. It's supposed to be relative to the | |
| 307 # base directory. | |
| 308 start_dir = os.path.abspath(os.path.join(BASE_DIRECTORY, args[0])) | |
| 309 else: | |
| 310 # More than one argument, we don't handle this. | |
| 311 option_parser.print_help() | |
| 312 return 1 | |
| 313 | 229 |
| 314 print 'Using base directory:', BASE_DIRECTORY | 230 # Guess again the SCM used. |
| 315 print 'Checking directory:', start_dir | 231 api = get_scm(options.root) |
| 316 | 232 |
| 317 BASE_DIRECTORY = BASE_DIRECTORY.replace('\\', '/') | 233 if not api.check(start_dir): |
| 318 start_dir = start_dir.replace('\\', '/') | |
| 319 | |
| 320 success = True | |
| 321 if os.path.exists(os.path.join(BASE_DIRECTORY, '.svn')): | |
| 322 global SVN_REPO_URL | |
| 323 SVN_REPO_URL = GetSvnRepositoryRoot(BASE_DIRECTORY) | |
| 324 if not SVN_REPO_URL: | |
| 325 print 'Cannot determine the SVN repo URL' | |
| 326 success = False | |
| 327 elif os.path.exists(os.path.join(BASE_DIRECTORY, '.git')): | |
| 328 global IS_SVN | |
| 329 IS_SVN = False | |
| 330 global GIT_SOURCE_DIRECTORY | |
| 331 GIT_SOURCE_DIRECTORY = GetGitSourceDirectory(BASE_DIRECTORY) | |
| 332 if not GIT_SOURCE_DIRECTORY: | |
| 333 print 'Cannot determine the list of GIT directories' | |
| 334 success = False | |
| 335 else: | |
| 336 print 'Cannot determine the SCM used in %s' % BASE_DIRECTORY | |
| 337 success = False | |
| 338 | |
| 339 if success: | |
| 340 success = CheckDirectory(start_dir) | |
| 341 if not success: | |
| 342 print '\nFAILED\n' | 234 print '\nFAILED\n' |
| 343 return 1 | 235 return 1 |
| 344 print '\nSUCCESS\n' | 236 print '\nSUCCESS\n' |
| 345 return 0 | 237 return 0 |
| 346 | 238 |
| 347 | 239 |
| 348 if '__main__' == __name__: | 240 if '__main__' == __name__: |
| 349 sys.exit(main(sys.argv)) | 241 sys.exit(main()) |
| OLD | NEW |