OLD | NEW |
---|---|
(Empty) | |
1 #!/usr/bin/python | |
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 | |
4 # found in the LICENSE file. | |
5 | |
6 # This tool checks third-party licenses for the purposes of the Android WebView | |
7 # build. See the output of '--help' for details. | |
8 | |
9 | |
10 import optparse | |
11 import os | |
12 import re | |
13 import subprocess | |
14 import sys | |
15 import textwrap | |
16 | |
17 | |
18 REPOSITORY_ROOT = os.path.abspath(os.path.join( | |
19 os.path.dirname(__file__), '..', '..')) | |
20 | |
21 sys.path.append(os.path.join(REPOSITORY_ROOT, 'tools')) | |
22 import licenses | |
23 | |
24 | |
25 def _CheckDirectories(directory_list): | |
26 """Checks that all top-level directories under directories named 'third_party' | |
27 are listed. | |
28 Args: | |
29 directory_list: The list of directories. | |
30 Returns: | |
31 True if all directories are listed and the list contains no stale entries, | |
32 otherwise false. | |
33 """ | |
34 | |
35 cwd = os.getcwd() | |
36 os.chdir(REPOSITORY_ROOT) | |
37 unlisted_directories = [] | |
38 parent_listed_directory = None | |
39 for root, _, _ in os.walk('.'): | |
40 root = os.path.normpath(root) | |
41 if root in directory_list: | |
42 parent_listed_directory = root | |
43 is_listed = (parent_listed_directory and | |
44 root.startswith(parent_listed_directory)) | |
45 if (not is_listed | |
46 and not root.startswith('out/') | |
47 and os.path.dirname(root).endswith('third_party')): | |
48 unlisted_directories += [root] | |
49 stale = [x for x in directory_list if not os.path.exists(x)] | |
50 os.chdir(cwd) | |
51 | |
52 if unlisted_directories: | |
53 print 'Some third-party directories are not listed. You must add the ' \ | |
54 'following directories to the list.\n%s' % \ | |
55 '\n'.join(unlisted_directories) | |
56 return False | |
57 | |
58 if stale: | |
59 print 'Some third-party directories are listed but not present. You must ' \ | |
60 'remove the following directories from the list.\n%s' % \ | |
61 '\n'.join(stale) | |
62 return False | |
63 | |
64 return True | |
65 | |
66 | |
67 def _GetCmdOutput(args): | |
68 p = subprocess.Popen(args=args, cwd=REPOSITORY_ROOT, stdout=subprocess.PIPE) | |
69 ret = p.communicate()[0] | |
70 return ret | |
71 | |
72 | |
73 def _CheckLicenseHeaders(directory_list, file_list): | |
74 """Checks that all files which are not in a listed third-party directory, | |
75 and which do not use the standard Chromium license, are listed. | |
76 Args: | |
77 directory_list: The list of directories. | |
78 file_list: The list of files. | |
79 Returns: | |
80 True if all files with non-standard license headers are listed and the | |
81 file list contains no stale entries, otherwise false. | |
82 """ | |
83 # Matches one of ... | |
84 # - '[Cc]opyright' but not when followed by | |
85 # ' 20[0-9][0-9] [Tt]he Chromium Authors' or | |
86 # ' 20[0-9][0-9]-20[0-9][0-9] [Tt]he [Cc]hromium [Aa]uthors', with an | |
87 # optional '([Cc])' | |
88 # - '([Cc]) 20[0-9][0-9] but not when preceeded by '[Cc]opyright' or | |
89 # 'opyright ' | |
90 regex = '[Cc]opyright(?!( \([Cc]\))? 20[0-9][0-9](-20[0-9][0-9])? ' \ | |
91 '[Tt]he [Cc]hromium [Aa]uthors)' \ | |
92 '|' \ | |
93 '(?<!([Cc]opyright|opyright ))\([Cc]\) (19|20)[0-9][0-9]' | |
Evan Martin
2012/07/24 19:27:55
Why not fix the case on the code rather than this
| |
94 | |
95 args = ['grep', | |
96 '-rPlI', | |
97 '--exclude-dir', 'third_party', | |
98 '--exclude-dir', 'out', | |
99 '--exclude-dir', '.git', | |
100 regex, | |
101 '.'] | |
102 files = _GetCmdOutput(args).splitlines() | |
103 | |
104 # Exclude files under listed directories and some known offendors. | |
105 offending_files = [] | |
106 for x in files: | |
107 x = os.path.normpath(x) | |
108 is_in_listed_directory = False | |
109 for y in directory_list: | |
110 if x.startswith(y): | |
111 is_in_listed_directory = True | |
112 break | |
113 if (not is_in_listed_directory | |
114 # Exists in Android tree. | |
115 and not x == 'ThirdPartyProject.prop' | |
116 # Ignore these tools. | |
117 and not x.startswith('android_webview/tools/') | |
118 # This is a build intermediate directory. | |
119 and not x.startswith('chrome/app/theme/google_chrome/') | |
120 # This is a test output directory. | |
121 and not x.startswith('data/page_cycler/') | |
122 # 'Copyright' appears in strings. | |
123 and not x.startswith('chrome/app/resources/')): | |
124 offending_files += [x] | |
125 | |
126 unknown = set(offending_files) - set(file_list) | |
127 if unknown: | |
128 print 'The following files contain a third-party license but are not in ' \ | |
129 'a listed third-party directory and are not themselves listed. You ' \ | |
130 'must add the following files to the list.\n%s' % '\n'.join(unknown) | |
131 return False | |
132 | |
133 stale = set(file_list) - set(offending_files) | |
134 if stale: | |
135 print 'The following third-party files are listed unnecessarily. You ' \ | |
136 'must remove the following files from the list.\n%s' % \ | |
137 '\n'.join(stale) | |
138 return False | |
139 | |
140 return True | |
141 | |
142 | |
143 def _GetEntriesWithAnnotation(entries, annotation): | |
144 """Gets a list of all entries with the specified annotation. | |
145 Args: | |
146 entries: The list of entries. | |
147 annotation: The annotation. | |
148 Returns: | |
149 A list of entries. | |
150 """ | |
151 | |
152 result = [] | |
153 for line in entries.splitlines(): | |
154 match = re.match(r'([^#\s]*)\s+' + annotation + r'\s+', line) | |
155 if match: | |
156 result += [match.group(1)] | |
157 return result | |
158 | |
159 | |
160 def _GetLicenseFile(directory): | |
161 """Gets the path to the license file for the specified directory. Uses the | |
162 licenses tool from scripts/'. | |
163 Args: | |
164 directory: The directory to consider, relative to the root of the | |
165 repository. | |
166 Returns: | |
167 The absolute path to the license file. | |
168 """ | |
169 | |
170 return licenses.ParseDir(directory, False)['License File'] | |
171 | |
172 | |
173 def _CheckLicenseFiles(directories): | |
174 """Checks that all directories annotated with REQUIRES_ATTRIBUTION have a | |
175 license file. | |
176 Args: | |
177 directories: The list of directories. | |
178 Returns: | |
179 Whether the check succeeded. | |
180 """ | |
181 | |
182 offending_directories = [] | |
183 for directory in directories: | |
184 if not os.path.exists(_GetLicenseFile(directory)): | |
185 offending_directories += [license_file] | |
186 | |
187 if offending_directories: | |
188 print 'Some license files are missing. You must provide license files in ' \ | |
189 'the following directories.\n%s' % '\n'.join(offending_directories) | |
190 return False | |
191 | |
192 return True | |
193 | |
194 | |
195 def _ReadFile(path): | |
196 """Reads a file from disk. | |
197 Args: | |
198 path: The path of the file to read, relative to the root of the repository. | |
199 Returns: | |
200 The contents of the file as a string. | |
201 """ | |
202 | |
203 with file(os.path.join(REPOSITORY_ROOT, path), 'r') as f: | |
204 lines = f.read() | |
205 return lines | |
Evan Martin
2012/07/24 19:41:28
"lines" is a confusing name, as it is a single str
| |
206 | |
207 | |
208 def _GetEntriesWithoutAnnotation(entries, annotation): | |
209 """Gets a list of all entries without the specified annotation. | |
210 Args: | |
211 entries: The list of entries. | |
212 annotation: The annotation. | |
213 Returns: | |
214 A list of entries. | |
215 """ | |
216 | |
217 result = [] | |
218 for line in entries.splitlines(): | |
219 match = re.match(r'([^#\s]*)((?!' + annotation + r').)*$', line) | |
220 if match and not len(match.group(1)) == 0: | |
221 result += [match.group(1)] | |
222 return result | |
223 | |
224 | |
225 def _Check(directories_data, files_data): | |
226 """Checks that all third-party code in projects used by the WebView either | |
227 uses a license compatible with Android or is exlcuded from the snapshot. Also | |
228 checks that license text is present for third-party code requiring | |
229 attribution. | |
230 Args: | |
231 directories_data: The contents of the directories data file. | |
232 files_data: The contents of the files data file. | |
233 Returns: | |
234 Whether the check succeeded. | |
235 """ | |
236 | |
237 | |
238 # We use two signals to find third-party code. First, directories named | |
239 # 'third-party' and second, non-standard license text. | |
240 directories = _GetEntriesWithoutAnnotation(directories_data, | |
241 'INCOMPATIBLE_AND_UNUSED') | |
242 files = _GetEntriesWithoutAnnotation(files_data, 'INCOMPATIBLE_AND_UNUSED') | |
243 result = _CheckDirectories(directories) | |
244 result = _CheckLicenseHeaders(directories, files) and result | |
245 | |
246 # Also check that all directories annotated with REQUIRES_ATTRIBUTION have a | |
247 # license file. | |
248 directories = _GetEntriesWithAnnotation(directories_data, | |
249 'REQUIRES_ATTRIBUTION') | |
250 return _CheckLicenseFiles(directories) and result | |
251 | |
252 | |
253 def _GenerateNoticeFile(directories_data, print_warnings): | |
254 """Generates the contents of an Android NOTICE file for the third-party code. | |
255 Args: | |
256 directories_data: The contents of the directories data file. | |
257 print_warnings: Whether to print warnings. | |
258 Returns: | |
259 The contents of the NOTICE file. | |
260 """ | |
261 | |
262 # Don't forget Chromium's LICENSE file | |
263 content = [_ReadFile('LICENSE')] | |
264 | |
265 for directory in _GetEntriesWithAnnotation(directories_data, | |
266 'REQUIRES_ATTRIBUTION'): | |
267 content += [_ReadFile(_GetLicenseFile(directory))] | |
268 | |
269 return '\n'.join(content) | |
270 | |
271 | |
272 def main(): | |
273 class IndentedHelpFormatterWithNL(optparse.IndentedHelpFormatter): | |
274 def format_description(self, description): | |
275 if not description: return "" | |
276 desc_width = self.width - self.current_indent | |
277 indent = " "*self.current_indent | |
278 bits = description.split('\n') | |
279 formatted_bits = [ | |
280 textwrap.fill(bit, | |
281 desc_width, | |
282 initial_indent=indent, | |
283 subsequent_indent=indent) | |
284 for bit in bits] | |
285 result = '\n'.join(formatted_bits) + '\n' | |
286 return result | |
287 | |
288 parser = optparse.OptionParser(formatter=IndentedHelpFormatterWithNL(), | |
289 usage='%prog [options]') | |
290 parser.description = 'Checks third-party licenses for the purposes of the ' \ | |
291 'Android WebView build.\n\n' \ | |
292 'The Android tree includes a snapshot of Chromium in ' \ | |
293 'order to power the system WebView. The snapshot '\ | |
294 'includes only the third-party DEPS projects required ' \ | |
295 'for the WebView. This tool is intended to be run in ' \ | |
296 'the snapshot and checks that all code uses ' \ | |
297 'open-source licenses compatible with Android, and ' \ | |
298 'that we meet the requirements of those licenses. It ' \ | |
299 'can also be used to generate an Android NOTICE file ' \ | |
300 'for the third-party code.\n\n' \ | |
301 'It makes use of two data files, ' \ | |
302 'third_party_files.txt and ' \ | |
303 'third_party_directories.txt. These record the ' \ | |
304 'license status of all third-party code in the main ' \ | |
305 'Chromium repository and in the third-party DEPS ' \ | |
306 'projects used in the snapshot. This status includes ' \ | |
307 'why the code\'s license is compatible with Android, ' \ | |
308 'or why the code must be excluded from the ' \ | |
309 'snapshot.\n\n' \ | |
Evan Martin
2012/07/24 19:27:55
I think this long string should be the docstring o
| |
310 'Commands:\n' \ | |
311 ' check Check licenses.\n' \ | |
312 ' notice Generate Android NOTICE file on stdout' | |
313 (options, args) = parser.parse_args() | |
314 if len(args) != 1: | |
315 parser.print_help() | |
316 return 1 | |
317 | |
318 tools_directory = os.path.join('android_webview', 'tools') | |
319 directories_data = _ReadFile(os.path.join(tools_directory, | |
320 'third_party_directories.txt')) | |
321 files_data = _ReadFile(os.path.join(tools_directory, 'third_party_files.txt')) | |
322 | |
323 if args[0] == 'check': | |
324 if _Check(directories_data, files_data): | |
325 print 'OK!' | |
326 return 0 | |
327 else: | |
328 return 1 | |
329 elif args[0] == 'notice': | |
330 print _GenerateNoticeFile(directories_data, False) | |
331 return 0 | |
332 | |
333 parser.print_help() | |
334 return 1 | |
335 | |
336 if __name__ == '__main__': | |
337 sys.exit(main()) | |
OLD | NEW |