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.
6 """Utility for checking and processing licensing information in third_party
9 Usage: licenses.py <command>
12 scan scan third_party directories, verifying that we have licensing info
13 credits generate about:credits on stdout
15 (You can also import this as a module.)
22 # Paths from the root of the tree to directories to skip.
24 # Same module occurs in crypto/third_party/nss and net/third_party/nss, so
26 os
.path
.join('third_party','nss'),
28 # Placeholder directory only, not third-party code.
29 os
.path
.join('third_party','adobe'),
31 # Build files only, not third-party code.
32 os
.path
.join('third_party','widevine'),
34 # Only binaries, used during development.
35 os
.path
.join('third_party','valgrind'),
37 # Used for development and test, not in the shipping product.
38 os
.path
.join('build','secondary'),
39 os
.path
.join('third_party','bison'),
40 os
.path
.join('third_party','blanketjs'),
41 os
.path
.join('third_party','cygwin'),
42 os
.path
.join('third_party','gnu_binutils'),
43 os
.path
.join('third_party','gold'),
44 os
.path
.join('third_party','gperf'),
45 os
.path
.join('third_party','lighttpd'),
46 os
.path
.join('third_party','llvm'),
47 os
.path
.join('third_party','llvm-build'),
48 os
.path
.join('third_party','mingw-w64'),
49 os
.path
.join('third_party','nacl_sdk_binaries'),
50 os
.path
.join('third_party','pefile'),
51 os
.path
.join('third_party','perl'),
52 os
.path
.join('third_party','psyco_win32'),
53 os
.path
.join('third_party','pylib'),
54 os
.path
.join('third_party','pywebsocket'),
55 os
.path
.join('third_party','qunit'),
56 os
.path
.join('third_party','sinonjs'),
57 os
.path
.join('third_party','syzygy'),
59 # Chromium code in third_party.
60 os
.path
.join('third_party','fuzzymatch'),
61 os
.path
.join('tools', 'swarming_client'),
63 # Stuff pulled in from chrome-internal for official builds/tools.
64 os
.path
.join('third_party', 'clear_cache'),
65 os
.path
.join('third_party', 'gnu'),
66 os
.path
.join('third_party', 'googlemac'),
67 os
.path
.join('third_party', 'pcre'),
68 os
.path
.join('third_party', 'psutils'),
69 os
.path
.join('third_party', 'sawbuck'),
71 # Redistribution does not require attribution in documentation.
72 os
.path
.join('third_party','directxsdk'),
73 os
.path
.join('third_party','platformsdk_win2008_6_1'),
74 os
.path
.join('third_party','platformsdk_win7'),
77 # Directories we don't scan through.
78 VCS_METADATA_DIRS
= ('.svn', '.git')
79 PRUNE_DIRS
= (VCS_METADATA_DIRS
+
80 ('out', 'Debug', 'Release', # build files
81 'layout_tests')) # lots of subdirs
84 os
.path
.join('breakpad'),
85 os
.path
.join('chrome', 'common', 'extensions', 'docs', 'examples'),
86 os
.path
.join('chrome', 'test', 'chromeos', 'autotest'),
87 os
.path
.join('chrome', 'test', 'data'),
88 os
.path
.join('native_client'),
89 os
.path
.join('net', 'tools', 'spdyshark'),
90 os
.path
.join('sdch', 'open-vcdiff'),
91 os
.path
.join('testing', 'gmock'),
92 os
.path
.join('testing', 'gtest'),
93 # The directory with the word list for Chinese and Japanese segmentation
94 # with different license terms than ICU.
95 os
.path
.join('third_party','icu','source','data','brkitr'),
96 os
.path
.join('tools', 'grit'),
97 os
.path
.join('tools', 'gyp'),
98 os
.path
.join('tools', 'page_cycler', 'acid3'),
99 os
.path
.join('url', 'third_party', 'mozilla'),
101 # Fake directory so we can include the strongtalk license.
102 os
.path
.join('v8', 'strongtalk'),
106 # Directories where we check out directly from upstream, and therefore
107 # can't provide a README.chromium. Please prefer a README.chromium
110 os
.path
.join('native_client'): {
111 "Name": "native client",
112 "URL": "http://code.google.com/p/nativeclient",
115 os
.path
.join('sdch', 'open-vcdiff'): {
116 "Name": "open-vcdiff",
117 "URL": "http://code.google.com/p/open-vcdiff",
118 "License": "Apache 2.0, MIT, GPL v2 and custom licenses",
119 "License Android Compatible": "yes",
121 os
.path
.join('testing', 'gmock'): {
123 "URL": "http://code.google.com/p/googlemock",
125 "License File": "NOT_SHIPPED",
127 os
.path
.join('testing', 'gtest'): {
129 "URL": "http://code.google.com/p/googletest",
131 "License File": "NOT_SHIPPED",
133 os
.path
.join('third_party', 'angle'): {
134 "Name": "Almost Native Graphics Layer Engine",
135 "URL": "http://code.google.com/p/angleproject/",
138 os
.path
.join('third_party', 'cros_system_api'): {
139 "Name": "Chromium OS system API",
140 "URL": "http://www.chromium.org/chromium-os",
142 # Absolute path here is resolved as relative to the source root.
143 "License File": "/LICENSE.chromium_os",
145 os
.path
.join('third_party', 'lss'): {
146 "Name": "linux-syscall-support",
147 "URL": "http://code.google.com/p/linux-syscall-support/",
149 "License File": "/LICENSE",
151 os
.path
.join('third_party', 'ots'): {
152 "Name": "OTS (OpenType Sanitizer)",
153 "URL": "http://code.google.com/p/ots/",
156 os
.path
.join('third_party', 'pdfium'): {
158 "URL": "http://code.google.com/p/pdfium/",
161 os
.path
.join('third_party', 'pdfsqueeze'): {
162 "Name": "pdfsqueeze",
163 "URL": "http://code.google.com/p/pdfsqueeze/",
164 "License": "Apache 2.0",
165 "License File": "COPYING",
167 os
.path
.join('third_party', 'ppapi'): {
169 "URL": "http://code.google.com/p/ppapi/",
171 os
.path
.join('third_party', 'scons-2.0.1'): {
172 "Name": "scons-2.0.1",
173 "URL": "http://www.scons.org",
175 "License File": "NOT_SHIPPED",
177 os
.path
.join('third_party', 'trace-viewer'): {
178 "Name": "trace-viewer",
179 "URL": "http://code.google.com/p/trace-viewer",
181 "License File": "NOT_SHIPPED",
183 os
.path
.join('third_party', 'v8-i18n'): {
184 "Name": "Internationalization Library for v8",
185 "URL": "http://code.google.com/p/v8-i18n/",
186 "License": "Apache 2.0",
188 os
.path
.join('third_party', 'WebKit'): {
190 "URL": "http://webkit.org/",
191 "License": "BSD and GPL v2",
192 # Absolute path here is resolved as relative to the source root.
193 "License File": "/webkit/LICENSE",
195 os
.path
.join('third_party', 'webpagereplay'): {
196 "Name": "webpagereplay",
197 "URL": "http://code.google.com/p/web-page-replay",
198 "License": "Apache 2.0",
199 "License File": "NOT_SHIPPED",
201 os
.path
.join('tools', 'grit'): {
203 "URL": "http://code.google.com/p/grit-i18n",
205 "License File": "NOT_SHIPPED",
207 os
.path
.join('tools', 'gyp'): {
209 "URL": "http://code.google.com/p/gyp",
211 "License File": "NOT_SHIPPED",
213 os
.path
.join('v8'): {
214 "Name": "V8 JavaScript Engine",
215 "URL": "http://code.google.com/p/v8",
218 os
.path
.join('v8', 'strongtalk'): {
219 "Name": "Strongtalk",
220 "URL": "http://www.strongtalk.org/",
222 # Absolute path here is resolved as relative to the source root.
223 "License File": "/v8/LICENSE.strongtalk",
227 # Special value for 'License File' field used to indicate that the license file
228 # should not be used in about:credits.
229 NOT_SHIPPED
= "NOT_SHIPPED"
232 class LicenseError(Exception):
233 """We raise this exception when a directory's licensing info isn't
237 def AbsolutePath(path
, filename
, root
):
238 """Convert a path in README.chromium to be absolute based on the source
240 if filename
.startswith('/'):
241 # Absolute-looking paths are relative to the source root
242 # (which is the directory we're run from).
243 absolute_path
= os
.path
.join(root
, filename
[1:])
245 absolute_path
= os
.path
.join(root
, path
, filename
)
246 if os
.path
.exists(absolute_path
):
250 def ParseDir(path
, root
, require_license_file
=True):
251 """Examine a third_party/foo component and extract its metadata."""
253 # Parse metadata fields out of README.chromium.
254 # We examine "LICENSE" for the license file by default.
256 "License File": "LICENSE", # Relative path to license text.
257 "Name": None, # Short name (for header on about:credits).
258 "URL": None, # Project home page.
259 "License": None, # Software license.
262 # Relative path to a file containing some html we're required to place in
264 optional_keys
= ["Required Text", "License Android Compatible"]
266 if path
in SPECIAL_CASES
:
267 metadata
.update(SPECIAL_CASES
[path
])
269 # Try to find README.chromium.
270 readme_path
= os
.path
.join(root
, path
, 'README.chromium')
271 if not os
.path
.exists(readme_path
):
272 raise LicenseError("missing README.chromium or licenses.py "
273 "SPECIAL_CASES entry")
275 for line
in open(readme_path
):
279 for key
in metadata
.keys() + optional_keys
:
281 if line
.startswith(field
):
282 metadata
[key
] = line
[len(field
):]
284 # Check that all expected metadata is present.
285 for key
, value
in metadata
.iteritems():
287 raise LicenseError("couldn't find '" + key
+ "' line "
288 "in README.chromium or licences.py "
291 # Special-case modules that aren't in the shipping product, so don't need
292 # their license in about:credits.
293 if metadata
["License File"] != NOT_SHIPPED
:
294 # Check that the license file exists.
295 for filename
in (metadata
["License File"], "COPYING"):
296 license_path
= AbsolutePath(path
, filename
, root
)
297 if license_path
is not None:
300 if require_license_file
and not license_path
:
301 raise LicenseError("License file not found. "
302 "Either add a file named LICENSE, "
303 "import upstream's COPYING if available, "
304 "or add a 'License File:' line to "
305 "README.chromium with the appropriate path.")
306 metadata
["License File"] = license_path
308 if "Required Text" in metadata
:
309 required_path
= AbsolutePath(path
, metadata
["Required Text"], root
)
310 if required_path
is not None:
311 metadata
["Required Text"] = required_path
313 raise LicenseError("Required text file listed but not found.")
318 def ContainsFiles(path
, root
):
319 """Determines whether any files exist in a directory or in any of its
321 for _
, dirs
, files
in os
.walk(os
.path
.join(root
, path
)):
324 for vcs_metadata
in VCS_METADATA_DIRS
:
325 if vcs_metadata
in dirs
:
326 dirs
.remove(vcs_metadata
)
330 def FilterDirsWithFiles(dirs_list
, root
):
331 # If a directory contains no files, assume it's a DEPS directory for a
332 # project not used by our current configuration and skip it.
333 return [x
for x
in dirs_list
if ContainsFiles(x
, root
)]
336 def FindThirdPartyDirs(prune_paths
, root
):
337 """Find all third_party directories underneath the source root."""
338 third_party_dirs
= set()
339 for path
, dirs
, files
in os
.walk(root
):
340 path
= path
[len(root
)+1:] # Pretty up the path.
342 if path
in prune_paths
:
346 # Prune out directories we want to skip.
347 # (Note that we loop over PRUNE_DIRS so we're not iterating over a
348 # list that we're simultaneously mutating.)
349 for skip
in PRUNE_DIRS
:
353 if os
.path
.basename(path
) == 'third_party':
354 # Add all subdirectories that are not marked for skipping.
356 dirpath
= os
.path
.join(path
, dir)
357 if dirpath
not in prune_paths
:
358 third_party_dirs
.add(dirpath
)
360 # Don't recurse into any subdirs from here.
364 # Don't recurse into paths in ADDITIONAL_PATHS, like we do with regular
365 # third_party/foo paths.
366 if path
in ADDITIONAL_PATHS
:
369 for dir in ADDITIONAL_PATHS
:
370 if dir not in prune_paths
:
371 third_party_dirs
.add(dir)
373 return third_party_dirs
376 def ScanThirdPartyDirs(root
=None):
377 """Scan a list of directories and report on any problems we find."""
380 third_party_dirs
= FindThirdPartyDirs(PRUNE_PATHS
, root
)
381 third_party_dirs
= FilterDirsWithFiles(third_party_dirs
, root
)
384 for path
in sorted(third_party_dirs
):
386 metadata
= ParseDir(path
, root
)
387 except LicenseError
, e
:
388 errors
.append((path
, e
.args
[0]))
391 for path
, error
in sorted(errors
):
392 print path
+ ": " + error
394 return len(errors
) == 0
397 def GenerateCredits():
398 """Generate about:credits."""
400 if len(sys
.argv
) not in (2, 3):
401 print 'usage: licenses.py credits [output_file]'
404 def EvaluateTemplate(template
, env
, escape
=True):
405 """Expand a template with variables like {{foo}} using a
406 dictionary of expansions."""
407 for key
, val
in env
.items():
408 if escape
and not key
.endswith("_unescaped"):
409 val
= cgi
.escape(val
)
410 template
= template
.replace('{{%s}}' % key
, val
)
413 root
= os
.path
.join(os
.path
.dirname(__file__
), '..')
414 third_party_dirs
= FindThirdPartyDirs(PRUNE_PATHS
, root
)
416 entry_template
= open(os
.path
.join(root
, 'chrome', 'browser', 'resources',
417 'about_credits_entry.tmpl'), 'rb').read()
419 for path
in sorted(third_party_dirs
):
421 metadata
= ParseDir(path
, root
)
423 # TODO(phajdan.jr): Convert to fatal error (http://crbug.com/39240).
425 if metadata
['License File'] == NOT_SHIPPED
:
428 'name': metadata
['Name'],
429 'url': metadata
['URL'],
430 'license': open(metadata
['License File'], 'rb').read(),
431 'license_unescaped': '',
433 if 'Required Text' in metadata
:
434 required_text
= open(metadata
['Required Text'], 'rb').read()
435 env
["license_unescaped"] = required_text
436 entries
.append(EvaluateTemplate(entry_template
, env
))
438 file_template
= open(os
.path
.join(root
, 'chrome', 'browser', 'resources',
439 'about_credits.tmpl'), 'rb').read()
440 template_contents
= "<!-- Generated by licenses.py; do not edit. -->"
441 template_contents
+= EvaluateTemplate(file_template
,
442 {'entries': '\n'.join(entries
)},
445 if len(sys
.argv
) == 3:
446 with
open(sys
.argv
[2], 'w') as output_file
:
447 output_file
.write(template_contents
)
448 elif len(sys
.argv
) == 2:
449 print template_contents
456 if len(sys
.argv
) > 1:
457 command
= sys
.argv
[1]
459 if command
== 'scan':
460 if not ScanThirdPartyDirs():
462 elif command
== 'credits':
463 if not GenerateCredits():
470 if __name__
== '__main__':