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'),
58 os
.path
.join('tools', 'profile_chrome', 'third_party'),
60 # Chromium code in third_party.
61 os
.path
.join('third_party','fuzzymatch'),
62 os
.path
.join('tools', 'swarming_client'),
64 # Stuff pulled in from chrome-internal for official builds/tools.
65 os
.path
.join('third_party', 'clear_cache'),
66 os
.path
.join('third_party', 'gnu'),
67 os
.path
.join('third_party', 'googlemac'),
68 os
.path
.join('third_party', 'pcre'),
69 os
.path
.join('third_party', 'psutils'),
70 os
.path
.join('third_party', 'sawbuck'),
72 # Redistribution does not require attribution in documentation.
73 os
.path
.join('third_party','directxsdk'),
74 os
.path
.join('third_party','platformsdk_win2008_6_1'),
75 os
.path
.join('third_party','platformsdk_win7'),
78 # Directories we don't scan through.
79 VCS_METADATA_DIRS
= ('.svn', '.git')
80 PRUNE_DIRS
= (VCS_METADATA_DIRS
+
81 ('out', 'Debug', 'Release', # build files
82 'layout_tests')) # lots of subdirs
85 os
.path
.join('breakpad'),
86 os
.path
.join('chrome', 'common', 'extensions', 'docs', 'examples'),
87 os
.path
.join('chrome', 'test', 'chromeos', 'autotest'),
88 os
.path
.join('chrome', 'test', 'data'),
89 os
.path
.join('native_client'),
90 os
.path
.join('net', 'tools', 'spdyshark'),
91 os
.path
.join('sdch', 'open-vcdiff'),
92 os
.path
.join('testing', 'gmock'),
93 os
.path
.join('testing', 'gtest'),
94 os
.path
.join('tools', 'grit'),
95 os
.path
.join('tools', 'gyp'),
96 os
.path
.join('tools', 'page_cycler', 'acid3'),
97 os
.path
.join('url', 'third_party', 'mozilla'),
99 # Fake directory so we can include the strongtalk license.
100 os
.path
.join('v8', 'strongtalk'),
101 os
.path
.join('v8', 'third_party', 'fdlibm'),
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('native_client'): {
110 "Name": "native client",
111 "URL": "http://code.google.com/p/nativeclient",
114 os
.path
.join('sdch', 'open-vcdiff'): {
115 "Name": "open-vcdiff",
116 "URL": "http://code.google.com/p/open-vcdiff",
117 "License": "Apache 2.0, MIT, GPL v2 and custom licenses",
118 "License Android Compatible": "yes",
120 os
.path
.join('testing', 'gmock'): {
122 "URL": "http://code.google.com/p/googlemock",
124 "License File": "NOT_SHIPPED",
126 os
.path
.join('testing', 'gtest'): {
128 "URL": "http://code.google.com/p/googletest",
130 "License File": "NOT_SHIPPED",
132 os
.path
.join('third_party', 'angle'): {
133 "Name": "Almost Native Graphics Layer Engine",
134 "URL": "http://code.google.com/p/angleproject/",
137 os
.path
.join('third_party', 'cros_system_api'): {
138 "Name": "Chromium OS system API",
139 "URL": "http://www.chromium.org/chromium-os",
141 # Absolute path here is resolved as relative to the source root.
142 "License File": "/LICENSE.chromium_os",
144 os
.path
.join('third_party', 'lss'): {
145 "Name": "linux-syscall-support",
146 "URL": "http://code.google.com/p/linux-syscall-support/",
148 "License File": "/LICENSE",
150 os
.path
.join('third_party', 'ots'): {
151 "Name": "OTS (OpenType Sanitizer)",
152 "URL": "http://code.google.com/p/ots/",
155 os
.path
.join('third_party', 'pdfium'): {
157 "URL": "http://code.google.com/p/pdfium/",
160 os
.path
.join('third_party', 'pdfsqueeze'): {
161 "Name": "pdfsqueeze",
162 "URL": "http://code.google.com/p/pdfsqueeze/",
163 "License": "Apache 2.0",
164 "License File": "COPYING",
166 os
.path
.join('third_party', 'ppapi'): {
168 "URL": "http://code.google.com/p/ppapi/",
170 os
.path
.join('third_party', 'scons-2.0.1'): {
171 "Name": "scons-2.0.1",
172 "URL": "http://www.scons.org",
174 "License File": "NOT_SHIPPED",
176 os
.path
.join('third_party', 'trace-viewer'): {
177 "Name": "trace-viewer",
178 "URL": "http://code.google.com/p/trace-viewer",
180 "License File": "NOT_SHIPPED",
182 os
.path
.join('third_party', 'v8-i18n'): {
183 "Name": "Internationalization Library for v8",
184 "URL": "http://code.google.com/p/v8-i18n/",
185 "License": "Apache 2.0",
187 os
.path
.join('third_party', 'WebKit'): {
189 "URL": "http://webkit.org/",
190 "License": "BSD and GPL v2",
191 # Absolute path here is resolved as relative to the source root.
192 "License File": "/webkit/LICENSE",
194 os
.path
.join('third_party', 'webpagereplay'): {
195 "Name": "webpagereplay",
196 "URL": "http://code.google.com/p/web-page-replay",
197 "License": "Apache 2.0",
198 "License File": "NOT_SHIPPED",
200 os
.path
.join('tools', 'grit'): {
202 "URL": "http://code.google.com/p/grit-i18n",
204 "License File": "NOT_SHIPPED",
206 os
.path
.join('tools', 'gyp'): {
208 "URL": "http://code.google.com/p/gyp",
210 "License File": "NOT_SHIPPED",
212 os
.path
.join('v8'): {
213 "Name": "V8 JavaScript Engine",
214 "URL": "http://code.google.com/p/v8",
217 os
.path
.join('v8', 'strongtalk'): {
218 "Name": "Strongtalk",
219 "URL": "http://www.strongtalk.org/",
221 # Absolute path here is resolved as relative to the source root.
222 "License File": "/v8/LICENSE.strongtalk",
224 os
.path
.join('v8', 'third_party', 'fdlibm'): {
226 "URL": "http://www.netlib.org/fdlibm/",
227 "License": "Freely Distributable",
228 # Absolute path here is resolved as relative to the source root.
229 "License File" : "/v8/third_party/fdlibm/LICENSE",
230 "License Android Compatible" : "yes",
232 os
.path
.join('third_party', 'khronos_glcts'): {
233 # These sources are not shipped, are not public, and it isn't
234 # clear why they're tripping the license check.
235 "Name": "khronos_glcts",
236 "URL": "http://no-public-url",
237 "License": "Khronos",
238 "License File": "NOT_SHIPPED",
240 os
.path
.join('tools', 'telemetry', 'third_party', 'gsutil'): {
242 "URL": "https://cloud.google.com/storage/docs/gsutil",
243 "License": "Apache 2.0",
244 "License File": "NOT_SHIPPED",
248 # Special value for 'License File' field used to indicate that the license file
249 # should not be used in about:credits.
250 NOT_SHIPPED
= "NOT_SHIPPED"
253 class LicenseError(Exception):
254 """We raise this exception when a directory's licensing info isn't
258 def AbsolutePath(path
, filename
, root
):
259 """Convert a path in README.chromium to be absolute based on the source
261 if filename
.startswith('/'):
262 # Absolute-looking paths are relative to the source root
263 # (which is the directory we're run from).
264 absolute_path
= os
.path
.join(root
, filename
[1:])
266 absolute_path
= os
.path
.join(root
, path
, filename
)
267 if os
.path
.exists(absolute_path
):
271 def ParseDir(path
, root
, require_license_file
=True, optional_keys
=None):
272 """Examine a third_party/foo component and extract its metadata."""
274 # Parse metadata fields out of README.chromium.
275 # We examine "LICENSE" for the license file by default.
277 "License File": "LICENSE", # Relative path to license text.
278 "Name": None, # Short name (for header on about:credits).
279 "URL": None, # Project home page.
280 "License": None, # Software license.
283 if optional_keys
is None:
286 if path
in SPECIAL_CASES
:
287 metadata
.update(SPECIAL_CASES
[path
])
289 # Try to find README.chromium.
290 readme_path
= os
.path
.join(root
, path
, 'README.chromium')
291 if not os
.path
.exists(readme_path
):
292 raise LicenseError("missing README.chromium or licenses.py "
293 "SPECIAL_CASES entry")
295 for line
in open(readme_path
):
299 for key
in metadata
.keys() + optional_keys
:
301 if line
.startswith(field
):
302 metadata
[key
] = line
[len(field
):]
304 # Check that all expected metadata is present.
305 for key
, value
in metadata
.iteritems():
307 raise LicenseError("couldn't find '" + key
+ "' line "
308 "in README.chromium or licences.py "
311 # Special-case modules that aren't in the shipping product, so don't need
312 # their license in about:credits.
313 if metadata
["License File"] != NOT_SHIPPED
:
314 # Check that the license file exists.
315 for filename
in (metadata
["License File"], "COPYING"):
316 license_path
= AbsolutePath(path
, filename
, root
)
317 if license_path
is not None:
320 if require_license_file
and not license_path
:
321 raise LicenseError("License file not found. "
322 "Either add a file named LICENSE, "
323 "import upstream's COPYING if available, "
324 "or add a 'License File:' line to "
325 "README.chromium with the appropriate path.")
326 metadata
["License File"] = license_path
331 def ContainsFiles(path
, root
):
332 """Determines whether any files exist in a directory or in any of its
334 for _
, dirs
, files
in os
.walk(os
.path
.join(root
, path
)):
337 for vcs_metadata
in VCS_METADATA_DIRS
:
338 if vcs_metadata
in dirs
:
339 dirs
.remove(vcs_metadata
)
343 def FilterDirsWithFiles(dirs_list
, root
):
344 # If a directory contains no files, assume it's a DEPS directory for a
345 # project not used by our current configuration and skip it.
346 return [x
for x
in dirs_list
if ContainsFiles(x
, root
)]
349 def FindThirdPartyDirs(prune_paths
, root
):
350 """Find all third_party directories underneath the source root."""
351 third_party_dirs
= set()
352 for path
, dirs
, files
in os
.walk(root
):
353 path
= path
[len(root
)+1:] # Pretty up the path.
355 if path
in prune_paths
:
359 # Prune out directories we want to skip.
360 # (Note that we loop over PRUNE_DIRS so we're not iterating over a
361 # list that we're simultaneously mutating.)
362 for skip
in PRUNE_DIRS
:
366 if os
.path
.basename(path
) == 'third_party':
367 # Add all subdirectories that are not marked for skipping.
369 dirpath
= os
.path
.join(path
, dir)
370 if dirpath
not in prune_paths
:
371 third_party_dirs
.add(dirpath
)
373 # Don't recurse into any subdirs from here.
377 # Don't recurse into paths in ADDITIONAL_PATHS, like we do with regular
378 # third_party/foo paths.
379 if path
in ADDITIONAL_PATHS
:
382 for dir in ADDITIONAL_PATHS
:
383 if dir not in prune_paths
:
384 third_party_dirs
.add(dir)
386 return third_party_dirs
389 def ScanThirdPartyDirs(root
=None):
390 """Scan a list of directories and report on any problems we find."""
393 third_party_dirs
= FindThirdPartyDirs(PRUNE_PATHS
, root
)
394 third_party_dirs
= FilterDirsWithFiles(third_party_dirs
, root
)
397 for path
in sorted(third_party_dirs
):
399 metadata
= ParseDir(path
, root
)
400 except LicenseError
, e
:
401 errors
.append((path
, e
.args
[0]))
404 for path
, error
in sorted(errors
):
405 print path
+ ": " + error
407 return len(errors
) == 0
410 def GenerateCredits():
411 """Generate about:credits."""
413 if len(sys
.argv
) not in (2, 3):
414 print 'usage: licenses.py credits [output_file]'
417 def EvaluateTemplate(template
, env
, escape
=True):
418 """Expand a template with variables like {{foo}} using a
419 dictionary of expansions."""
420 for key
, val
in env
.items():
422 val
= cgi
.escape(val
)
423 template
= template
.replace('{{%s}}' % key
, val
)
426 root
= os
.path
.join(os
.path
.dirname(__file__
), '..')
427 third_party_dirs
= FindThirdPartyDirs(PRUNE_PATHS
, root
)
429 entry_template
= open(os
.path
.join(root
, 'chrome', 'browser', 'resources',
430 'about_credits_entry.tmpl'), 'rb').read()
432 for path
in sorted(third_party_dirs
):
434 metadata
= ParseDir(path
, root
)
436 # TODO(phajdan.jr): Convert to fatal error (http://crbug.com/39240).
438 if metadata
['License File'] == NOT_SHIPPED
:
441 'name': metadata
['Name'],
442 'url': metadata
['URL'],
443 'license': open(metadata
['License File'], 'rb').read(),
445 entries
.append(EvaluateTemplate(entry_template
, env
))
447 file_template
= open(os
.path
.join(root
, 'chrome', 'browser', 'resources',
448 'about_credits.tmpl'), 'rb').read()
449 template_contents
= "<!-- Generated by licenses.py; do not edit. -->"
450 template_contents
+= EvaluateTemplate(file_template
,
451 {'entries': '\n'.join(entries
)},
454 if len(sys
.argv
) == 3:
455 with
open(sys
.argv
[2], 'w') as output_file
:
456 output_file
.write(template_contents
)
457 elif len(sys
.argv
) == 2:
458 print template_contents
465 if len(sys
.argv
) > 1:
466 command
= sys
.argv
[1]
468 if command
== 'scan':
469 if not ScanThirdPartyDirs():
471 elif command
== 'credits':
472 if not GenerateCredits():
479 if __name__
== '__main__':