1#!/usr/bin/env python
2# Copyright (c) 2013 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import optparse
7import os
8import re
9import sys
10
11import build_version
12from build_paths import SDK_SRC_DIR, SCRIPT_DIR, OUT_DIR
13
14# Add SDK make tools scripts to the python path.
15sys.path.append(os.path.join(SDK_SRC_DIR, 'tools'))
16
17import getos
18
19VALID_PLATFORMS = ['linux', 'mac', 'win']
20PLATFORM_PREFIX_RE = re.compile(r'^\[([^\]]*)\](.*)$')
21
22class ParseException(Exception):
23  def __init__(self, filename, line, message):
24    Exception.__init__(self)
25    self.filename = filename
26    self.line = line
27    self.message = message
28
29  def __str__(self):
30    return '%s:%d: %s' % (self.filename, self.line, self.message)
31
32
33def SplitPattern(pattern):
34  match = PLATFORM_PREFIX_RE.match(pattern)
35  if not match:
36    return pattern, []
37
38  # platform-specific line
39  platforms = match.group(1).split(',')
40
41  # If this platform is included, strip the [...] part.
42  pattern = match.group(2)
43  return pattern, platforms
44
45
46class VerifyException(Exception):
47  pass
48
49class Rules(object):
50  def __init__(self, filename, platform=None, contents=None):
51    self.glob_prefixes = []
52    self.exact_filenames = set()
53    self.filename = filename
54    self.platform = platform or getos.GetPlatform()
55    self.exe_ext = '.exe' if self.platform == 'win' else ''
56
57    if self.platform not in VALID_PLATFORMS:
58      raise ParseException(self.filename, 1,
59                           'Unknown platform %s' % self.platform)
60
61    if not contents:
62      with open(filename) as f:
63        contents = f.read()
64
65    for line_no, rule in enumerate(contents.split('\n')):
66      rule = rule.strip()
67      if rule:
68        self.ParsePattern(line_no + 1, rule)
69
70  def ParsePattern(self, line_no, pattern):
71    pattern, platforms = SplitPattern(pattern)
72    if platforms:
73      unknown_platforms = set(platforms) - set(VALID_PLATFORMS)
74      if unknown_platforms:
75        msg = 'Unknown platform(s) %s.' % (
76            ', '.join('"%s"' % platform for platform in unknown_platforms))
77        raise ParseException(self.filename, line_no, msg)
78      if self.platform not in platforms:
79        return
80
81    pattern = pattern.replace('${PLATFORM}', self.platform)
82    pattern = pattern.replace('${EXE_EXT}', self.exe_ext)
83
84    if '*' in pattern:
85      # glob pattern
86      # We only support * at the end.
87      if pattern.find('*') != len(pattern) - 1:
88        msg = '* is only allowed at the end of the line.'
89        raise ParseException(self.filename, line_no, msg)
90
91      # Remove the *
92      pattern = pattern[:-1]
93      self.glob_prefixes.append(pattern)
94      # Sort by longest prefix first; otherwise the rules:
95      #
96      # foo/*
97      # foo/bar/*
98      #
99      # Won't work properly. A file "foo/bar/baz" will match the first rule,
100      # not the second.
101      self.glob_prefixes.sort(cmp=lambda x, y: cmp(len(y), len(x)))
102    else:
103      self.exact_filenames.add(pattern)
104
105  def VerifyDirectoryList(self, directory_list):
106    exact_filenames_used = set()
107    glob_prefixes_used = set()
108    expected_globs = set()
109    expected_filenames = set()
110    unexpected_filenames = set()
111
112    for filename in directory_list:
113      if os.path.sep != '/':
114        filename = filename.replace(os.path.sep, '/')
115      if filename in self.exact_filenames:
116        exact_filenames_used.add(filename)
117        continue
118
119      # glob pattern
120      found_prefix = False
121      for prefix in self.glob_prefixes:
122        if filename.startswith(prefix):
123          glob_prefixes_used.add(prefix)
124          found_prefix = True
125          break
126
127      if not found_prefix:
128        unexpected_filenames.add(filename)
129
130    if len(exact_filenames_used) != len(self.exact_filenames):
131      # We looped through the directory list, so if the lengths are unequal, it
132      # must be that we expected something that isn't there.
133      expected_filenames = self.exact_filenames - exact_filenames_used
134
135    if len(glob_prefixes_used) != self.glob_prefixes:
136      expected_globs = set(self.glob_prefixes) - glob_prefixes_used
137
138    if expected_filenames or unexpected_filenames or expected_globs:
139      msg = ''
140      if unexpected_filenames:
141        msg += '>>> Unexpected filenames: <<<\n%s\n' % (
142            '\n'.join(sorted(unexpected_filenames)))
143      if expected_filenames:
144        msg += '>>> Expected filenames: <<<\n%s\n' % (
145            '\n'.join(sorted(expected_filenames)))
146      if expected_globs:
147        msg += '>>> Expected 1+ files in these directories: <<< \n%s\n' % (
148            '\n'.join(sorted(expected_globs)))
149      raise VerifyException(msg)
150
151
152def GetDirectoryList(directory_path):
153  result = []
154  for root, _, files in os.walk(directory_path):
155    rel_root = os.path.relpath(root, directory_path)
156    if rel_root == '.':
157      rel_root = ''
158    for base_name in files:
159      result.append(os.path.join(rel_root, base_name))
160  return result
161
162
163def Verify(rule_path, directory_path, platform=None):
164  rules = Rules(rule_path, platform=platform)
165  directory_list = GetDirectoryList(directory_path)
166  rules.VerifyDirectoryList(directory_list)
167
168
169def SortFile(rule_path):
170  with open(rule_path) as infile:
171    lines = infile.readlines()
172
173  def compare(line1, line2):
174    line1 = SplitPattern(line1)[0].lower()
175    line2 = SplitPattern(line2)[0].lower()
176    return cmp(line1, line2)
177
178  lines.sort(compare)
179  with open(rule_path, 'w') as output:
180    for line in lines:
181      output.write(line)
182
183
184def main(args):
185  parser = optparse.OptionParser(usage='%prog <rule file> <directory>')
186  parser.add_option('-p', '--platform',
187      help='Test with this platform, instead of the system\'s platform')
188  parser.add_option('-s', '--sort', action='store_true',
189      help='Sort the file list in place, rather than verifying the contents.')
190  options, args = parser.parse_args(args)
191
192  if not args:
193    args = [os.path.join(SCRIPT_DIR, 'sdk_files.list')]
194
195  if options.sort:
196    if not args:
197      parser.error('Expected rule file.')
198    SortFile(args[0])
199    return 0
200
201  if len(args) < 2:
202    version = build_version.ChromeMajorVersion()
203    args.append(os.path.join(OUT_DIR, 'pepper_%s' % version))
204
205  rule_path, directory_path = args
206  if options.platform:
207    if options.platform not in VALID_PLATFORMS:
208      parser.error('Unknown platform: %s' % options.platform)
209    platform = options.platform
210  else:
211    platform = getos.GetPlatform()
212
213  try:
214    return Verify(rule_path, directory_path, platform)
215  except ParseException, e:
216    print >> sys.stderr, 'Error parsing rules:\n', e
217    return 1
218  except VerifyException, e:
219    print >> sys.stderr, 'Error verifying file list:\n', e
220    return 1
221  return 0
222
223
224if __name__ == '__main__':
225  sys.exit(main(sys.argv[1:]))
226