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 VCS_METADATA_DIRS
= ('.svn', '.git')
75 PRUNE_DIRS
= (VCS_METADATA_DIRS
+
76 ('out', 'Debug', 'Release', # build files
77 'layout_tests')) # lots of subdirs
80 os
.path
.join('breakpad'),
81 os
.path
.join('chrome', 'common', 'extensions', 'docs', 'examples'),
82 os
.path
.join('chrome', 'test', 'chromeos', 'autotest'),
83 os
.path
.join('chrome', 'test', 'data'),
84 os
.path
.join('googleurl'),
85 os
.path
.join('native_client'),
86 os
.path
.join('native_client_sdk'),
87 os
.path
.join('net', 'tools', 'spdyshark'),
88 os
.path
.join('ppapi'),
89 os
.path
.join('sandbox', 'linux', 'seccomp-legacy'),
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'),
100 # Fake directory so we can include the strongtalk license.
101 os
.path
.join('v8', 'strongtalk'),
105 # Directories where we check out directly from upstream, and therefore
106 # can't provide a README.chromium. Please prefer a README.chromium
109 os
.path
.join('googleurl'): {
110 "Name": "google-url",
111 "URL": "http://code.google.com/p/google-url/",
112 "License": "BSD and MPL 1.1/GPL 2.0/LGPL 2.1",
113 "License File": "LICENSE.txt",
115 os
.path
.join('native_client'): {
116 "Name": "native client",
117 "URL": "http://code.google.com/p/nativeclient",
120 os
.path
.join('sandbox', 'linux', 'seccomp-legacy'): {
121 "Name": "seccompsandbox",
122 "URL": "http://code.google.com/p/seccompsandbox",
125 os
.path
.join('sdch', 'open-vcdiff'): {
126 "Name": "open-vcdiff",
127 "URL": "http://code.google.com/p/open-vcdiff",
128 "License": "Apache 2.0, MIT, GPL v2 and custom licenses",
129 "License Android Compatible": "yes",
131 os
.path
.join('testing', 'gmock'): {
133 "URL": "http://code.google.com/p/googlemock",
135 "License File": "NOT_SHIPPED",
137 os
.path
.join('testing', 'gtest'): {
139 "URL": "http://code.google.com/p/googletest",
141 "License File": "NOT_SHIPPED",
143 os
.path
.join('third_party', 'angle'): {
144 "Name": "Almost Native Graphics Layer Engine",
145 "URL": "http://code.google.com/p/angleproject/",
148 os
.path
.join('third_party', 'cros_system_api'): {
149 "Name": "Chromium OS system API",
150 "URL": "http://www.chromium.org/chromium-os",
152 # Absolute path here is resolved as relative to the source root.
153 "License File": "/LICENSE.chromium_os",
155 os
.path
.join('third_party', 'GTM'): {
156 "Name": "Google Toolbox for Mac",
157 "URL": "http://code.google.com/p/google-toolbox-for-mac/",
158 "License": "Apache 2.0",
159 "License File": "COPYING",
161 os
.path
.join('third_party', 'lss'): {
162 "Name": "linux-syscall-support",
163 "URL": "http://code.google.com/p/linux-syscall-support/",
165 "License File": "/LICENSE",
167 os
.path
.join('third_party', 'ots'): {
168 "Name": "OTS (OpenType Sanitizer)",
169 "URL": "http://code.google.com/p/ots/",
172 os
.path
.join('third_party', 'pdfsqueeze'): {
173 "Name": "pdfsqueeze",
174 "URL": "http://code.google.com/p/pdfsqueeze/",
175 "License": "Apache 2.0",
176 "License File": "COPYING",
178 os
.path
.join('third_party', 'ppapi'): {
180 "URL": "http://code.google.com/p/ppapi/",
182 os
.path
.join('third_party', 'scons-2.0.1'): {
183 "Name": "scons-2.0.1",
184 "URL": "http://www.scons.org",
186 "License File": "NOT_SHIPPED",
188 os
.path
.join('third_party', 'trace-viewer'): {
189 "Name": "trace-viewer",
190 "URL": "http://code.google.com/p/trace-viewer",
192 "License File": "NOT_SHIPPED",
194 os
.path
.join('third_party', 'v8-i18n'): {
195 "Name": "Internationalization Library for v8",
196 "URL": "http://code.google.com/p/v8-i18n/",
197 "License": "Apache 2.0",
199 os
.path
.join('third_party', 'WebKit'): {
201 "URL": "http://webkit.org/",
202 "License": "BSD and GPL v2",
203 # Absolute path here is resolved as relative to the source root.
204 "License File": "/webkit/LICENSE",
206 os
.path
.join('third_party', 'webpagereplay'): {
207 "Name": "webpagereplay",
208 "URL": "http://code.google.com/p/web-page-replay",
209 "License": "Apache 2.0",
210 "License File": "NOT_SHIPPED",
212 os
.path
.join('tools', 'grit'): {
214 "URL": "http://code.google.com/p/grit-i18n",
216 "License File": "NOT_SHIPPED",
218 os
.path
.join('tools', 'gyp'): {
220 "URL": "http://code.google.com/p/gyp",
222 "License File": "NOT_SHIPPED",
224 os
.path
.join('v8'): {
225 "Name": "V8 JavaScript Engine",
226 "URL": "http://code.google.com/p/v8",
229 os
.path
.join('v8', 'strongtalk'): {
230 "Name": "Strongtalk",
231 "URL": "http://www.strongtalk.org/",
233 # Absolute path here is resolved as relative to the source root.
234 "License File": "/v8/LICENSE.strongtalk",
238 # Special value for 'License File' field used to indicate that the license file
239 # should not be used in about:credits.
240 NOT_SHIPPED
= "NOT_SHIPPED"
243 class LicenseError(Exception):
244 """We raise this exception when a directory's licensing info isn't
248 def AbsolutePath(path
, filename
, root
):
249 """Convert a path in README.chromium to be absolute based on the source
251 if filename
.startswith('/'):
252 # Absolute-looking paths are relative to the source root
253 # (which is the directory we're run from).
254 absolute_path
= os
.path
.join(root
, filename
[1:])
256 absolute_path
= os
.path
.join(root
, path
, filename
)
257 if os
.path
.exists(absolute_path
):
261 def ParseDir(path
, root
, require_license_file
=True):
262 """Examine a third_party/foo component and extract its metadata."""
264 # Parse metadata fields out of README.chromium.
265 # We examine "LICENSE" for the license file by default.
267 "License File": "LICENSE", # Relative path to license text.
268 "Name": None, # Short name (for header on about:credits).
269 "URL": None, # Project home page.
270 "License": None, # Software license.
273 # Relative path to a file containing some html we're required to place in
275 optional_keys
= ["Required Text", "License Android Compatible"]
277 if path
in SPECIAL_CASES
:
278 metadata
.update(SPECIAL_CASES
[path
])
280 # Try to find README.chromium.
281 readme_path
= os
.path
.join(root
, path
, 'README.chromium')
282 if not os
.path
.exists(readme_path
):
283 raise LicenseError("missing README.chromium or licenses.py "
284 "SPECIAL_CASES entry")
286 for line
in open(readme_path
):
290 for key
in metadata
.keys() + optional_keys
:
292 if line
.startswith(field
):
293 metadata
[key
] = line
[len(field
):]
295 # Check that all expected metadata is present.
296 for key
, value
in metadata
.iteritems():
298 raise LicenseError("couldn't find '" + key
+ "' line "
299 "in README.chromium or licences.py "
302 # Special-case modules that aren't in the shipping product, so don't need
303 # their license in about:credits.
304 if metadata
["License File"] != NOT_SHIPPED
:
305 # Check that the license file exists.
306 for filename
in (metadata
["License File"], "COPYING"):
307 license_path
= AbsolutePath(path
, filename
, root
)
308 if license_path
is not None:
311 if require_license_file
and not license_path
:
312 raise LicenseError("License file not found. "
313 "Either add a file named LICENSE, "
314 "import upstream's COPYING if available, "
315 "or add a 'License File:' line to "
316 "README.chromium with the appropriate path.")
317 metadata
["License File"] = license_path
319 if "Required Text" in metadata
:
320 required_path
= AbsolutePath(path
, metadata
["Required Text"], root
)
321 if required_path
is not None:
322 metadata
["Required Text"] = required_path
324 raise LicenseError("Required text file listed but not found.")
329 def ContainsFiles(path
, root
):
330 """Determines whether any files exist in a directory or in any of its
332 for _
, dirs
, files
in os
.walk(os
.path
.join(root
, path
)):
335 for vcs_metadata
in VCS_METADATA_DIRS
:
336 if vcs_metadata
in dirs
:
337 dirs
.remove(vcs_metadata
)
341 def FilterDirsWithFiles(dirs_list
, root
):
342 # If a directory contains no files, assume it's a DEPS directory for a
343 # project not used by our current configuration and skip it.
344 return [x
for x
in dirs_list
if ContainsFiles(x
, root
)]
347 def FindThirdPartyDirs(prune_paths
, root
):
348 """Find all third_party directories underneath the source root."""
349 third_party_dirs
= []
350 for path
, dirs
, files
in os
.walk(root
):
351 path
= path
[len(root
)+1:] # Pretty up the path.
353 if path
in prune_paths
:
357 # Prune out directories we want to skip.
358 # (Note that we loop over PRUNE_DIRS so we're not iterating over a
359 # list that we're simultaneously mutating.)
360 for skip
in PRUNE_DIRS
:
364 if os
.path
.basename(path
) == 'third_party':
365 # Add all subdirectories that are not marked for skipping.
367 dirpath
= os
.path
.join(path
, dir)
368 if dirpath
not in prune_paths
:
369 third_party_dirs
.append(dirpath
)
371 # Don't recurse into any subdirs from here.
375 # Don't recurse into paths in ADDITIONAL_PATHS, like we do with regular
376 # third_party/foo paths.
377 if path
in ADDITIONAL_PATHS
:
380 for dir in ADDITIONAL_PATHS
:
381 if dir not in prune_paths
:
382 third_party_dirs
.append(dir)
384 return third_party_dirs
387 def ScanThirdPartyDirs(root
=None):
388 """Scan a list of directories and report on any problems we find."""
391 third_party_dirs
= FindThirdPartyDirs(PRUNE_PATHS
, root
)
392 third_party_dirs
= FilterDirsWithFiles(third_party_dirs
, root
)
395 for path
in sorted(third_party_dirs
):
397 metadata
= ParseDir(path
, root
)
398 except LicenseError
, e
:
399 errors
.append((path
, e
.args
[0]))
402 for path
, error
in sorted(errors
):
403 print path
+ ": " + error
405 return len(errors
) == 0
408 def GenerateCredits():
409 """Generate about:credits."""
411 if len(sys
.argv
) not in (2, 3):
412 print 'usage: licenses.py credits [output_file]'
415 def EvaluateTemplate(template
, env
, escape
=True):
416 """Expand a template with variables like {{foo}} using a
417 dictionary of expansions."""
418 for key
, val
in env
.items():
419 if escape
and not key
.endswith("_unescaped"):
420 val
= cgi
.escape(val
)
421 template
= template
.replace('{{%s}}' % key
, val
)
424 root
= os
.path
.join(os
.path
.dirname(__file__
), '..')
425 third_party_dirs
= FindThirdPartyDirs(PRUNE_PATHS
, root
)
427 entry_template
= open(os
.path
.join(root
, 'chrome', 'browser', 'resources',
428 'about_credits_entry.tmpl'), 'rb').read()
430 for path
in sorted(third_party_dirs
):
432 metadata
= ParseDir(path
, root
)
434 # TODO(phajdan.jr): Convert to fatal error (http://crbug.com/39240).
436 if metadata
['License File'] == NOT_SHIPPED
:
439 'name': metadata
['Name'],
440 'url': metadata
['URL'],
441 'license': open(metadata
['License File'], 'rb').read(),
442 'license_unescaped': '',
444 if 'Required Text' in metadata
:
445 required_text
= open(metadata
['Required Text'], 'rb').read()
446 env
["license_unescaped"] = required_text
447 entries
.append(EvaluateTemplate(entry_template
, env
))
449 file_template
= open(os
.path
.join(root
, 'chrome', 'browser', 'resources',
450 'about_credits.tmpl'), 'rb').read()
451 template_contents
= "<!-- Generated by licenses.py; do not edit. -->"
452 template_contents
+= EvaluateTemplate(file_template
,
453 {'entries': '\n'.join(entries
)},
456 if len(sys
.argv
) == 3:
457 with
open(sys
.argv
[2], 'w') as output_file
:
458 output_file
.write(template_contents
)
459 elif len(sys
.argv
) == 2:
460 print template_contents
467 if len(sys
.argv
) > 1:
468 command
= sys
.argv
[1]
470 if command
== 'scan':
471 if not ScanThirdPartyDirs():
473 elif command
== 'credits':
474 if not GenerateCredits():
481 if __name__
== '__main__':