Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(364)

Side by Side Diff: android_webview/tools/webview_licenses.py

Issue 10816041: Add a tool to check license compatibility with Android (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Update regex Created 8 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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())
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698