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','bidichecker'),
39 os
.path
.join('third_party','cygwin'),
40 os
.path
.join('third_party','gold'),
41 os
.path
.join('third_party','lighttpd'),
42 os
.path
.join('third_party','mingw-w64'),
43 os
.path
.join('third_party','pefile'),
44 os
.path
.join('third_party','python_26'),
45 os
.path
.join('third_party','pywebsocket'),
47 # Stuff pulled in from chrome-internal for official builds/tools.
48 os
.path
.join('third_party', 'clear_cache'),
49 os
.path
.join('third_party', 'gnu'),
50 os
.path
.join('third_party', 'googlemac'),
51 os
.path
.join('third_party', 'pcre'),
52 os
.path
.join('third_party', 'psutils'),
53 os
.path
.join('third_party', 'sawbuck'),
55 # Redistribution does not require attribution in documentation.
56 os
.path
.join('third_party','directxsdk'),
57 os
.path
.join('third_party','platformsdk_win2008_6_1'),
58 os
.path
.join('third_party','platformsdk_win7'),
61 # Directories we don't scan through.
62 PRUNE_DIRS
= ('.svn', '.git', # VCS metadata
63 'out', 'Debug', 'Release', # build files
64 'layout_tests') # lots of subdirs
67 os
.path
.join('breakpad'),
68 os
.path
.join('chrome', 'common', 'extensions', 'docs', 'examples'),
69 os
.path
.join('chrome', 'test', 'chromeos', 'autotest'),
70 os
.path
.join('chrome', 'test', 'data'),
71 os
.path
.join('googleurl'),
72 os
.path
.join('native_client'),
73 os
.path
.join('native_client_sdk'),
74 os
.path
.join('net', 'tools', 'spdyshark'),
75 os
.path
.join('ppapi'),
76 os
.path
.join('sandbox', 'linux', 'seccomp-legacy'),
77 os
.path
.join('sdch', 'open-vcdiff'),
78 os
.path
.join('testing', 'gmock'),
79 os
.path
.join('testing', 'gtest'),
80 # The directory with the word list for Chinese and Japanese segmentation
81 # with different license terms than ICU.
82 os
.path
.join('third_party','icu','source','data','brkitr'),
83 os
.path
.join('tools', 'grit'),
84 os
.path
.join('tools', 'gyp'),
85 os
.path
.join('tools', 'page_cycler', 'acid3'),
87 # Fake directory so we can include the strongtalk license.
88 os
.path
.join('v8', 'strongtalk'),
92 # Directories where we check out directly from upstream, and therefore
93 # can't provide a README.chromium. Please prefer a README.chromium
96 os
.path
.join('googleurl'): {
98 "URL": "http://code.google.com/p/google-url/",
99 "License": "BSD and MPL 1.1/GPL 2.0/LGPL 2.1",
100 "License File": "LICENSE.txt",
102 os
.path
.join('native_client'): {
103 "Name": "native client",
104 "URL": "http://code.google.com/p/nativeclient",
107 os
.path
.join('sandbox', 'linux', 'seccomp-legacy'): {
108 "Name": "seccompsandbox",
109 "URL": "http://code.google.com/p/seccompsandbox",
112 os
.path
.join('sdch', 'open-vcdiff'): {
113 "Name": "open-vcdiff",
114 "URL": "http://code.google.com/p/open-vcdiff",
115 "License": "Apache 2.0, MIT, GPL v2 and custom licenses",
116 "License Android Compatible": "yes",
118 os
.path
.join('testing', 'gmock'): {
120 "URL": "http://code.google.com/p/googlemock",
123 os
.path
.join('testing', 'gtest'): {
125 "URL": "http://code.google.com/p/googletest",
128 os
.path
.join('third_party', 'angle'): {
129 "Name": "Almost Native Graphics Layer Engine",
130 "URL": "http://code.google.com/p/angleproject/",
133 os
.path
.join('third_party', 'cros_system_api'): {
134 "Name": "Chromium OS system API",
135 "URL": "http://www.chromium.org/chromium-os",
137 # Absolute path here is resolved as relative to the source root.
138 "License File": "/LICENSE.chromium_os",
140 os
.path
.join('third_party', 'GTM'): {
141 "Name": "Google Toolbox for Mac",
142 "URL": "http://code.google.com/p/google-toolbox-for-mac/",
143 "License": "Apache 2.0",
144 "License File": "COPYING",
146 os
.path
.join('third_party', 'lss'): {
147 "Name": "linux-syscall-support",
148 "URL": "http://code.google.com/p/lss/",
150 "License File": "/LICENSE",
152 os
.path
.join('third_party', 'ots'): {
153 "Name": "OTS (OpenType Sanitizer)",
154 "URL": "http://code.google.com/p/ots/",
157 os
.path
.join('third_party', 'pdfsqueeze'): {
158 "Name": "pdfsqueeze",
159 "URL": "http://code.google.com/p/pdfsqueeze/",
160 "License": "Apache 2.0",
161 "License File": "COPYING",
163 os
.path
.join('third_party', 'ppapi'): {
165 "URL": "http://code.google.com/p/ppapi/",
167 os
.path
.join('third_party', 'scons-2.0.1'): {
168 "Name": "scons-2.0.1",
169 "URL": "http://www.scons.org",
172 os
.path
.join('third_party', 'trace-viewer'): {
173 "Name": "trace-viewer",
174 "URL": "http://code.google.com/p/trace-viewer",
177 os
.path
.join('third_party', 'v8-i18n'): {
178 "Name": "Internationalization Library for v8",
179 "URL": "http://code.google.com/p/v8-i18n/",
180 "License": "Apache 2.0, BSD and others",
182 os
.path
.join('third_party', 'WebKit'): {
184 "URL": "http://webkit.org/",
185 "License": "BSD and GPL v2",
186 # Absolute path here is resolved as relative to the source root.
187 "License File": "/webkit/LICENSE",
189 os
.path
.join('third_party', 'webpagereplay'): {
190 "Name": "webpagereplay",
191 "URL": "http://code.google.com/p/web-page-replay",
192 "License": "Apache 2.0",
194 os
.path
.join('tools', 'grit'): {
196 "URL": "http://code.google.com/p/grit-i18n",
199 os
.path
.join('tools', 'gyp'): {
201 "URL": "http://code.google.com/p/gyp",
204 os
.path
.join('v8'): {
205 "Name": "V8 JavaScript Engine",
206 "URL": "http://code.google.com/p/v8",
209 os
.path
.join('v8', 'strongtalk'): {
210 "Name": "Strongtalk",
211 "URL": "http://www.strongtalk.org/",
213 # Absolute path here is resolved as relative to the source root.
214 "License File": "/v8/LICENSE.strongtalk",
218 # Special value for 'License File' field used to indicate that the license file
219 # should not be used in about:credits.
220 NOT_SHIPPED
= "NOT_SHIPPED"
223 class LicenseError(Exception):
224 """We raise this exception when a directory's licensing info isn't
228 def AbsolutePath(path
, filename
, root
):
229 """Convert a path in README.chromium to be absolute based on the source
231 if filename
.startswith('/'):
232 # Absolute-looking paths are relative to the source root
233 # (which is the directory we're run from).
234 absolute_path
= os
.path
.join(root
, filename
[1:])
236 absolute_path
= os
.path
.join(root
, path
, filename
)
237 if os
.path
.exists(absolute_path
):
241 def ParseDir(path
, root
, require_license_file
=True):
242 """Examine a third_party/foo component and extract its metadata."""
244 # Parse metadata fields out of README.chromium.
245 # We examine "LICENSE" for the license file by default.
247 "License File": "LICENSE", # Relative path to license text.
248 "Name": None, # Short name (for header on about:credits).
249 "URL": None, # Project home page.
250 "License": None, # Software license.
253 # Relative path to a file containing some html we're required to place in
255 optional_keys
= ["Required Text", "License Android Compatible"]
257 if path
in SPECIAL_CASES
:
258 metadata
.update(SPECIAL_CASES
[path
])
260 # Try to find README.chromium.
261 readme_path
= os
.path
.join(root
, path
, 'README.chromium')
262 if not os
.path
.exists(readme_path
):
263 raise LicenseError("missing README.chromium or licenses.py "
264 "SPECIAL_CASES entry")
266 for line
in open(readme_path
):
270 for key
in metadata
.keys() + optional_keys
:
272 if line
.startswith(field
):
273 metadata
[key
] = line
[len(field
):]
275 # Check that all expected metadata is present.
276 for key
, value
in metadata
.iteritems():
278 raise LicenseError("couldn't find '" + key
+ "' line "
279 "in README.chromium or licences.py "
282 # Special-case modules that aren't in the shipping product, so don't need
283 # their license in about:credits.
284 if metadata
["License File"] != NOT_SHIPPED
:
285 # Check that the license file exists.
286 for filename
in (metadata
["License File"], "COPYING"):
287 license_path
= AbsolutePath(path
, filename
, root
)
288 if license_path
is not None:
291 if require_license_file
and not license_path
:
292 raise LicenseError("License file not found. "
293 "Either add a file named LICENSE, "
294 "import upstream's COPYING if available, "
295 "or add a 'License File:' line to "
296 "README.chromium with the appropriate path.")
297 metadata
["License File"] = license_path
299 if "Required Text" in metadata
:
300 required_path
= AbsolutePath(path
, metadata
["Required Text"], root
)
301 if required_path
is not None:
302 metadata
["Required Text"] = required_path
304 raise LicenseError("Required text file listed but not found.")
309 def ContainsFiles(path
, root
):
310 """Determines whether any files exist in a directory or in any of its
312 for _
, _
, files
in os
.walk(os
.path
.join(root
, path
)):
318 def FindThirdPartyDirs(prune_paths
, root
):
319 """Find all third_party directories underneath the source root."""
320 third_party_dirs
= []
321 for path
, dirs
, files
in os
.walk(root
):
322 path
= path
[len(root
)+1:] # Pretty up the path.
324 if path
in prune_paths
:
328 # Prune out directories we want to skip.
329 # (Note that we loop over PRUNE_DIRS so we're not iterating over a
330 # list that we're simultaneously mutating.)
331 for skip
in PRUNE_DIRS
:
335 if os
.path
.basename(path
) == 'third_party':
336 # Add all subdirectories that are not marked for skipping.
338 dirpath
= os
.path
.join(path
, dir)
339 if dirpath
not in prune_paths
:
340 third_party_dirs
.append(dirpath
)
342 # Don't recurse into any subdirs from here.
346 # Don't recurse into paths in ADDITIONAL_PATHS, like we do with regular
347 # third_party/foo paths.
348 if path
in ADDITIONAL_PATHS
:
351 for dir in ADDITIONAL_PATHS
:
352 third_party_dirs
.append(dir)
354 # If a directory contains no files, assume it's a DEPS directory for a
355 # project not used by our current configuration and skip it.
356 return [x
for x
in third_party_dirs
if ContainsFiles(x
, root
)]
359 def ScanThirdPartyDirs(root
=None):
360 """Scan a list of directories and report on any problems we find."""
363 third_party_dirs
= FindThirdPartyDirs(PRUNE_PATHS
, root
)
366 for path
in sorted(third_party_dirs
):
368 metadata
= ParseDir(path
, root
)
369 except LicenseError
, e
:
370 errors
.append((path
, e
.args
[0]))
373 for path
, error
in sorted(errors
):
374 print path
+ ": " + error
376 return len(errors
) == 0
379 def GenerateCredits(root
=None):
380 """Generate about:credits, dumping the result to stdout."""
382 def EvaluateTemplate(template
, env
, escape
=True):
383 """Expand a template with variables like {{foo}} using a
384 dictionary of expansions."""
385 for key
, val
in env
.items():
386 if escape
and not key
.endswith("_unescaped"):
387 val
= cgi
.escape(val
)
388 template
= template
.replace('{{%s}}' % key
, val
)
393 third_party_dirs
= FindThirdPartyDirs(PRUNE_PATHS
, root
)
395 entry_template
= open(os
.path
.join(root
, 'chrome', 'browser', 'resources',
396 'about_credits_entry.tmpl'), 'rb').read()
398 for path
in sorted(third_party_dirs
):
400 metadata
= ParseDir(path
, root
)
402 print >>sys
.stderr
, ("WARNING: licensing info for " + path
+
403 " is incomplete, skipping.")
405 if metadata
['License File'] == NOT_SHIPPED
:
406 print >>sys
.stderr
, ("Path " + path
+ " marked as " + NOT_SHIPPED
+
410 'name': metadata
['Name'],
411 'url': metadata
['URL'],
412 'license': open(metadata
['License File'], 'rb').read(),
413 'license_unescaped': '',
415 if 'Required Text' in metadata
:
416 required_text
= open(metadata
['Required Text'], 'rb').read()
417 env
["license_unescaped"] = required_text
418 entries
.append(EvaluateTemplate(entry_template
, env
))
420 file_template
= open(os
.path
.join(root
, 'chrome', 'browser', 'resources',
421 'about_credits.tmpl'), 'rb').read()
422 print "<!-- Generated by licenses.py; do not edit. -->"
423 print EvaluateTemplate(file_template
, {'entries': '\n'.join(entries
)},
429 if len(sys
.argv
) > 1:
430 command
= sys
.argv
[1]
432 if command
== 'scan':
433 if not ScanThirdPartyDirs():
435 elif command
== 'credits':
436 if not GenerateCredits():
443 if __name__
== '__main__':