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('third_party','bison'),
39 os
.path
.join('third_party','cygwin'),
40 os
.path
.join('third_party','gnu_binutils'),
41 os
.path
.join('third_party','gold'),
42 os
.path
.join('third_party','gperf'),
43 os
.path
.join('third_party','lighttpd'),
44 os
.path
.join('third_party','llvm'),
45 os
.path
.join('third_party','llvm-build'),
46 os
.path
.join('third_party','mingw-w64'),
47 os
.path
.join('third_party','nacl_sdk_binaries'),
48 os
.path
.join('third_party','pefile'),
49 os
.path
.join('third_party','perl'),
50 os
.path
.join('third_party','psyco_win32'),
51 os
.path
.join('third_party','pylib'),
52 os
.path
.join('third_party','python_26'),
53 os
.path
.join('third_party','pywebsocket'),
54 os
.path
.join('third_party','syzygy'),
56 # Chromium code in third_party.
57 os
.path
.join('third_party','fuzzymatch'),
59 # Stuff pulled in from chrome-internal for official builds/tools.
60 os
.path
.join('third_party', 'clear_cache'),
61 os
.path
.join('third_party', 'gnu'),
62 os
.path
.join('third_party', 'googlemac'),
63 os
.path
.join('third_party', 'pcre'),
64 os
.path
.join('third_party', 'psutils'),
65 os
.path
.join('third_party', 'sawbuck'),
67 # Redistribution does not require attribution in documentation.
68 os
.path
.join('third_party','directxsdk'),
69 os
.path
.join('third_party','platformsdk_win2008_6_1'),
70 os
.path
.join('third_party','platformsdk_win7'),
73 # Directories we don't scan through.
74 PRUNE_DIRS
= ('.svn', '.git', # VCS metadata
75 'out', 'Debug', 'Release', # build files
76 'layout_tests') # lots of subdirs
79 os
.path
.join('breakpad'),
80 os
.path
.join('chrome', 'common', 'extensions', 'docs', 'examples'),
81 os
.path
.join('chrome', 'test', 'chromeos', 'autotest'),
82 os
.path
.join('chrome', 'test', 'data'),
83 os
.path
.join('googleurl'),
84 os
.path
.join('native_client'),
85 os
.path
.join('native_client_sdk'),
86 os
.path
.join('net', 'tools', 'spdyshark'),
87 os
.path
.join('ppapi'),
88 os
.path
.join('sandbox', 'linux', 'seccomp-legacy'),
89 os
.path
.join('sdch', 'open-vcdiff'),
90 os
.path
.join('testing', 'gmock'),
91 os
.path
.join('testing', 'gtest'),
92 # The directory with the word list for Chinese and Japanese segmentation
93 # with different license terms than ICU.
94 os
.path
.join('third_party','icu','source','data','brkitr'),
95 os
.path
.join('tools', 'grit'),
96 os
.path
.join('tools', 'gyp'),
97 os
.path
.join('tools', 'page_cycler', 'acid3'),
99 # Fake directory so we can include the strongtalk license.
100 os
.path
.join('v8', 'strongtalk'),
104 # Directories where we check out directly from upstream, and therefore
105 # can't provide a README.chromium. Please prefer a README.chromium
108 os
.path
.join('googleurl'): {
109 "Name": "google-url",
110 "URL": "http://code.google.com/p/google-url/",
111 "License": "BSD and MPL 1.1/GPL 2.0/LGPL 2.1",
112 "License File": "LICENSE.txt",
114 os
.path
.join('native_client'): {
115 "Name": "native client",
116 "URL": "http://code.google.com/p/nativeclient",
119 os
.path
.join('sandbox', 'linux', 'seccomp-legacy'): {
120 "Name": "seccompsandbox",
121 "URL": "http://code.google.com/p/seccompsandbox",
124 os
.path
.join('sdch', 'open-vcdiff'): {
125 "Name": "open-vcdiff",
126 "URL": "http://code.google.com/p/open-vcdiff",
127 "License": "Apache 2.0, MIT, GPL v2 and custom licenses",
128 "License Android Compatible": "yes",
130 os
.path
.join('testing', 'gmock'): {
132 "URL": "http://code.google.com/p/googlemock",
134 "License File": "NOT_SHIPPED",
136 os
.path
.join('testing', 'gtest'): {
138 "URL": "http://code.google.com/p/googletest",
140 "License File": "NOT_SHIPPED",
142 os
.path
.join('third_party', 'angle'): {
143 "Name": "Almost Native Graphics Layer Engine",
144 "URL": "http://code.google.com/p/angleproject/",
147 os
.path
.join('third_party', 'cros_system_api'): {
148 "Name": "Chromium OS system API",
149 "URL": "http://www.chromium.org/chromium-os",
151 # Absolute path here is resolved as relative to the source root.
152 "License File": "/LICENSE.chromium_os",
154 os
.path
.join('third_party', 'GTM'): {
155 "Name": "Google Toolbox for Mac",
156 "URL": "http://code.google.com/p/google-toolbox-for-mac/",
157 "License": "Apache 2.0",
158 "License File": "COPYING",
160 os
.path
.join('third_party', 'lss'): {
161 "Name": "linux-syscall-support",
162 "URL": "http://code.google.com/p/linux-syscall-support/",
164 "License File": "/LICENSE",
166 os
.path
.join('third_party', 'ots'): {
167 "Name": "OTS (OpenType Sanitizer)",
168 "URL": "http://code.google.com/p/ots/",
171 os
.path
.join('third_party', 'pdfsqueeze'): {
172 "Name": "pdfsqueeze",
173 "URL": "http://code.google.com/p/pdfsqueeze/",
174 "License": "Apache 2.0",
175 "License File": "COPYING",
177 os
.path
.join('third_party', 'ppapi'): {
179 "URL": "http://code.google.com/p/ppapi/",
181 os
.path
.join('third_party', 'scons-2.0.1'): {
182 "Name": "scons-2.0.1",
183 "URL": "http://www.scons.org",
185 "License File": "NOT_SHIPPED",
187 os
.path
.join('third_party', 'trace-viewer'): {
188 "Name": "trace-viewer",
189 "URL": "http://code.google.com/p/trace-viewer",
191 "License File": "NOT_SHIPPED",
193 os
.path
.join('third_party', 'v8-i18n'): {
194 "Name": "Internationalization Library for v8",
195 "URL": "http://code.google.com/p/v8-i18n/",
196 "License": "Apache 2.0",
198 os
.path
.join('third_party', 'WebKit'): {
200 "URL": "http://webkit.org/",
201 "License": "BSD and GPL v2",
202 # Absolute path here is resolved as relative to the source root.
203 "License File": "/webkit/LICENSE",
205 os
.path
.join('third_party', 'webpagereplay'): {
206 "Name": "webpagereplay",
207 "URL": "http://code.google.com/p/web-page-replay",
208 "License": "Apache 2.0",
209 "License File": "NOT_SHIPPED",
211 os
.path
.join('tools', 'grit'): {
213 "URL": "http://code.google.com/p/grit-i18n",
215 "License File": "NOT_SHIPPED",
217 os
.path
.join('tools', 'gyp'): {
219 "URL": "http://code.google.com/p/gyp",
221 "License File": "NOT_SHIPPED",
223 os
.path
.join('v8'): {
224 "Name": "V8 JavaScript Engine",
225 "URL": "http://code.google.com/p/v8",
228 os
.path
.join('v8', 'strongtalk'): {
229 "Name": "Strongtalk",
230 "URL": "http://www.strongtalk.org/",
232 # Absolute path here is resolved as relative to the source root.
233 "License File": "/v8/LICENSE.strongtalk",
237 # Special value for 'License File' field used to indicate that the license file
238 # should not be used in about:credits.
239 NOT_SHIPPED
= "NOT_SHIPPED"
242 class LicenseError(Exception):
243 """We raise this exception when a directory's licensing info isn't
247 def AbsolutePath(path
, filename
, root
):
248 """Convert a path in README.chromium to be absolute based on the source
250 if filename
.startswith('/'):
251 # Absolute-looking paths are relative to the source root
252 # (which is the directory we're run from).
253 absolute_path
= os
.path
.join(root
, filename
[1:])
255 absolute_path
= os
.path
.join(root
, path
, filename
)
256 if os
.path
.exists(absolute_path
):
260 def ParseDir(path
, root
, require_license_file
=True):
261 """Examine a third_party/foo component and extract its metadata."""
263 # Parse metadata fields out of README.chromium.
264 # We examine "LICENSE" for the license file by default.
266 "License File": "LICENSE", # Relative path to license text.
267 "Name": None, # Short name (for header on about:credits).
268 "URL": None, # Project home page.
269 "License": None, # Software license.
272 # Relative path to a file containing some html we're required to place in
274 optional_keys
= ["Required Text", "License Android Compatible"]
276 if path
in SPECIAL_CASES
:
277 metadata
.update(SPECIAL_CASES
[path
])
279 # Try to find README.chromium.
280 readme_path
= os
.path
.join(root
, path
, 'README.chromium')
281 if not os
.path
.exists(readme_path
):
282 raise LicenseError("missing README.chromium or licenses.py "
283 "SPECIAL_CASES entry")
285 for line
in open(readme_path
):
289 for key
in metadata
.keys() + optional_keys
:
291 if line
.startswith(field
):
292 metadata
[key
] = line
[len(field
):]
294 # Check that all expected metadata is present.
295 for key
, value
in metadata
.iteritems():
297 raise LicenseError("couldn't find '" + key
+ "' line "
298 "in README.chromium or licences.py "
301 # Special-case modules that aren't in the shipping product, so don't need
302 # their license in about:credits.
303 if metadata
["License File"] != NOT_SHIPPED
:
304 # Check that the license file exists.
305 for filename
in (metadata
["License File"], "COPYING"):
306 license_path
= AbsolutePath(path
, filename
, root
)
307 if license_path
is not None:
310 if require_license_file
and not license_path
:
311 raise LicenseError("License file not found. "
312 "Either add a file named LICENSE, "
313 "import upstream's COPYING if available, "
314 "or add a 'License File:' line to "
315 "README.chromium with the appropriate path.")
316 metadata
["License File"] = license_path
318 if "Required Text" in metadata
:
319 required_path
= AbsolutePath(path
, metadata
["Required Text"], root
)
320 if required_path
is not None:
321 metadata
["Required Text"] = required_path
323 raise LicenseError("Required text file listed but not found.")
328 def ContainsFiles(path
, root
):
329 """Determines whether any files exist in a directory or in any of its
331 for _
, _
, files
in os
.walk(os
.path
.join(root
, path
)):
337 def FilterDirsWithFiles(dirs_list
, root
):
338 # If a directory contains no files, assume it's a DEPS directory for a
339 # project not used by our current configuration and skip it.
340 return [x
for x
in dirs_list
if ContainsFiles(x
, root
)]
343 def FindThirdPartyDirs(prune_paths
, root
):
344 """Find all third_party directories underneath the source root."""
345 third_party_dirs
= []
346 for path
, dirs
, files
in os
.walk(root
):
347 path
= path
[len(root
)+1:] # Pretty up the path.
349 if path
in prune_paths
:
353 # Prune out directories we want to skip.
354 # (Note that we loop over PRUNE_DIRS so we're not iterating over a
355 # list that we're simultaneously mutating.)
356 for skip
in PRUNE_DIRS
:
360 if os
.path
.basename(path
) == 'third_party':
361 # Add all subdirectories that are not marked for skipping.
363 dirpath
= os
.path
.join(path
, dir)
364 if dirpath
not in prune_paths
:
365 third_party_dirs
.append(dirpath
)
367 # Don't recurse into any subdirs from here.
371 # Don't recurse into paths in ADDITIONAL_PATHS, like we do with regular
372 # third_party/foo paths.
373 if path
in ADDITIONAL_PATHS
:
376 for dir in ADDITIONAL_PATHS
:
377 if dir not in prune_paths
:
378 third_party_dirs
.append(dir)
380 return third_party_dirs
383 def ScanThirdPartyDirs(root
=None):
384 """Scan a list of directories and report on any problems we find."""
387 third_party_dirs
= FindThirdPartyDirs(PRUNE_PATHS
, root
)
388 third_party_dirs
= FilterDirsWithFiles(third_party_dirs
, root
)
391 for path
in sorted(third_party_dirs
):
393 metadata
= ParseDir(path
, root
)
394 except LicenseError
, e
:
395 errors
.append((path
, e
.args
[0]))
398 for path
, error
in sorted(errors
):
399 print path
+ ": " + error
401 return len(errors
) == 0
404 def GenerateCredits():
405 """Generate about:credits."""
407 if len(sys
.argv
) not in (2, 3):
408 print 'usage: licenses.py credits [output_file]'
411 def EvaluateTemplate(template
, env
, escape
=True):
412 """Expand a template with variables like {{foo}} using a
413 dictionary of expansions."""
414 for key
, val
in env
.items():
415 if escape
and not key
.endswith("_unescaped"):
416 val
= cgi
.escape(val
)
417 template
= template
.replace('{{%s}}' % key
, val
)
420 root
= os
.path
.join(os
.path
.dirname(__file__
), '..')
421 third_party_dirs
= FindThirdPartyDirs(PRUNE_PATHS
, root
)
423 entry_template
= open(os
.path
.join(root
, 'chrome', 'browser', 'resources',
424 'about_credits_entry.tmpl'), 'rb').read()
426 for path
in sorted(third_party_dirs
):
428 metadata
= ParseDir(path
, root
)
430 # TODO(phajdan.jr): Convert to fatal error (http://crbug.com/39240).
432 if metadata
['License File'] == NOT_SHIPPED
:
435 'name': metadata
['Name'],
436 'url': metadata
['URL'],
437 'license': open(metadata
['License File'], 'rb').read(),
438 'license_unescaped': '',
440 if 'Required Text' in metadata
:
441 required_text
= open(metadata
['Required Text'], 'rb').read()
442 env
["license_unescaped"] = required_text
443 entries
.append(EvaluateTemplate(entry_template
, env
))
445 file_template
= open(os
.path
.join(root
, 'chrome', 'browser', 'resources',
446 'about_credits.tmpl'), 'rb').read()
447 template_contents
= "<!-- Generated by licenses.py; do not edit. -->"
448 template_contents
+= EvaluateTemplate(file_template
,
449 {'entries': '\n'.join(entries
)},
452 if len(sys
.argv
) == 3:
453 with
open(sys
.argv
[2], 'w') as output_file
:
454 output_file
.write(template_contents
)
455 elif len(sys
.argv
) == 2:
456 print template_contents
463 if len(sys
.argv
) > 1:
464 command
= sys
.argv
[1]
466 if command
== 'scan':
467 if not ScanThirdPartyDirs():
469 elif command
== 'credits':
470 if not GenerateCredits():
477 if __name__
== '__main__':