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', 'browser', 'resources', 'settings', 'routing',
89 os
.path
.join('chrome', 'test', 'chromeos', 'autotest'),
90 os
.path
.join('chrome', 'test', 'data'),
91 os
.path
.join('native_client'),
92 os
.path
.join('net', 'tools', 'spdyshark'),
93 os
.path
.join('sdch', 'open-vcdiff'),
94 os
.path
.join('testing', 'gmock'),
95 os
.path
.join('testing', 'gtest'),
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'),
103 os
.path
.join('v8', 'third_party', 'fdlibm'),
107 # Directories where we check out directly from upstream, and therefore
108 # can't provide a README.chromium. Please prefer a README.chromium
111 os
.path
.join('native_client'): {
112 "Name": "native client",
113 "URL": "http://code.google.com/p/nativeclient",
116 os
.path
.join('sdch', 'open-vcdiff'): {
117 "Name": "open-vcdiff",
118 "URL": "http://code.google.com/p/open-vcdiff",
119 "License": "Apache 2.0, MIT, GPL v2 and custom licenses",
120 "License Android Compatible": "yes",
122 os
.path
.join('testing', 'gmock'): {
124 "URL": "http://code.google.com/p/googlemock",
126 "License File": "NOT_SHIPPED",
128 os
.path
.join('testing', 'gtest'): {
130 "URL": "http://code.google.com/p/googletest",
132 "License File": "NOT_SHIPPED",
134 os
.path
.join('third_party', 'angle'): {
135 "Name": "Almost Native Graphics Layer Engine",
136 "URL": "http://code.google.com/p/angleproject/",
139 os
.path
.join('third_party', 'cros_system_api'): {
140 "Name": "Chromium OS system API",
141 "URL": "http://www.chromium.org/chromium-os",
143 # Absolute path here is resolved as relative to the source root.
144 "License File": "/LICENSE.chromium_os",
146 os
.path
.join('third_party', 'lss'): {
147 "Name": "linux-syscall-support",
148 "URL": "http://code.google.com/p/linux-syscall-support/",
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', 'pdfium'): {
159 "URL": "http://code.google.com/p/pdfium/",
162 os
.path
.join('third_party', 'pdfsqueeze'): {
163 "Name": "pdfsqueeze",
164 "URL": "http://code.google.com/p/pdfsqueeze/",
165 "License": "Apache 2.0",
166 "License File": "COPYING",
168 os
.path
.join('third_party', 'ppapi'): {
170 "URL": "http://code.google.com/p/ppapi/",
172 os
.path
.join('third_party', 'scons-2.0.1'): {
173 "Name": "scons-2.0.1",
174 "URL": "http://www.scons.org",
176 "License File": "NOT_SHIPPED",
178 os
.path
.join('third_party', 'trace-viewer'): {
179 "Name": "trace-viewer",
180 "URL": "http://code.google.com/p/trace-viewer",
182 "License File": "NOT_SHIPPED",
184 os
.path
.join('third_party', 'v8-i18n'): {
185 "Name": "Internationalization Library for v8",
186 "URL": "http://code.google.com/p/v8-i18n/",
187 "License": "Apache 2.0",
189 os
.path
.join('third_party', 'WebKit'): {
191 "URL": "http://webkit.org/",
192 "License": "BSD and GPL v2",
193 # Absolute path here is resolved as relative to the source root.
194 "License File": "/webkit/LICENSE",
196 os
.path
.join('third_party', 'webpagereplay'): {
197 "Name": "webpagereplay",
198 "URL": "http://code.google.com/p/web-page-replay",
199 "License": "Apache 2.0",
200 "License File": "NOT_SHIPPED",
202 os
.path
.join('tools', 'grit'): {
204 "URL": "http://code.google.com/p/grit-i18n",
206 "License File": "NOT_SHIPPED",
208 os
.path
.join('tools', 'gyp'): {
210 "URL": "http://code.google.com/p/gyp",
212 "License File": "NOT_SHIPPED",
214 os
.path
.join('v8'): {
215 "Name": "V8 JavaScript Engine",
216 "URL": "http://code.google.com/p/v8",
219 os
.path
.join('v8', 'strongtalk'): {
220 "Name": "Strongtalk",
221 "URL": "http://www.strongtalk.org/",
223 # Absolute path here is resolved as relative to the source root.
224 "License File": "/v8/LICENSE.strongtalk",
226 os
.path
.join('v8', 'third_party', 'fdlibm'): {
228 "URL": "http://www.netlib.org/fdlibm/",
229 "License": "Freely Distributable",
230 # Absolute path here is resolved as relative to the source root.
231 "License File" : "/v8/third_party/fdlibm/LICENSE",
232 "License Android Compatible" : "yes",
234 os
.path
.join('third_party', 'khronos_glcts'): {
235 # These sources are not shipped, are not public, and it isn't
236 # clear why they're tripping the license check.
237 "Name": "khronos_glcts",
238 "URL": "http://no-public-url",
239 "License": "Khronos",
240 "License File": "NOT_SHIPPED",
242 os
.path
.join('tools', 'telemetry', 'third_party', 'gsutil'): {
244 "URL": "https://cloud.google.com/storage/docs/gsutil",
245 "License": "Apache 2.0",
246 "License File": "NOT_SHIPPED",
250 # Special value for 'License File' field used to indicate that the license file
251 # should not be used in about:credits.
252 NOT_SHIPPED
= "NOT_SHIPPED"
255 class LicenseError(Exception):
256 """We raise this exception when a directory's licensing info isn't
260 def AbsolutePath(path
, filename
, root
):
261 """Convert a path in README.chromium to be absolute based on the source
263 if filename
.startswith('/'):
264 # Absolute-looking paths are relative to the source root
265 # (which is the directory we're run from).
266 absolute_path
= os
.path
.join(root
, filename
[1:])
268 absolute_path
= os
.path
.join(root
, path
, filename
)
269 if os
.path
.exists(absolute_path
):
273 def ParseDir(path
, root
, require_license_file
=True, optional_keys
=None):
274 """Examine a third_party/foo component and extract its metadata."""
276 # Parse metadata fields out of README.chromium.
277 # We examine "LICENSE" for the license file by default.
279 "License File": "LICENSE", # Relative path to license text.
280 "Name": None, # Short name (for header on about:credits).
281 "URL": None, # Project home page.
282 "License": None, # Software license.
285 if optional_keys
is None:
288 if path
in SPECIAL_CASES
:
289 metadata
.update(SPECIAL_CASES
[path
])
291 # Try to find README.chromium.
292 readme_path
= os
.path
.join(root
, path
, 'README.chromium')
293 if not os
.path
.exists(readme_path
):
294 raise LicenseError("missing README.chromium or licenses.py "
295 "SPECIAL_CASES entry")
297 for line
in open(readme_path
):
301 for key
in metadata
.keys() + optional_keys
:
303 if line
.startswith(field
):
304 metadata
[key
] = line
[len(field
):]
306 # Check that all expected metadata is present.
307 for key
, value
in metadata
.iteritems():
309 raise LicenseError("couldn't find '" + key
+ "' line "
310 "in README.chromium or licences.py "
313 # Special-case modules that aren't in the shipping product, so don't need
314 # their license in about:credits.
315 if metadata
["License File"] != NOT_SHIPPED
:
316 # Check that the license file exists.
317 for filename
in (metadata
["License File"], "COPYING"):
318 license_path
= AbsolutePath(path
, filename
, root
)
319 if license_path
is not None:
322 if require_license_file
and not license_path
:
323 raise LicenseError("License file not found. "
324 "Either add a file named LICENSE, "
325 "import upstream's COPYING if available, "
326 "or add a 'License File:' line to "
327 "README.chromium with the appropriate path.")
328 metadata
["License File"] = license_path
333 def ContainsFiles(path
, root
):
334 """Determines whether any files exist in a directory or in any of its
336 for _
, dirs
, files
in os
.walk(os
.path
.join(root
, path
)):
339 for vcs_metadata
in VCS_METADATA_DIRS
:
340 if vcs_metadata
in dirs
:
341 dirs
.remove(vcs_metadata
)
345 def FilterDirsWithFiles(dirs_list
, root
):
346 # If a directory contains no files, assume it's a DEPS directory for a
347 # project not used by our current configuration and skip it.
348 return [x
for x
in dirs_list
if ContainsFiles(x
, root
)]
351 def FindThirdPartyDirs(prune_paths
, root
):
352 """Find all third_party directories underneath the source root."""
353 third_party_dirs
= set()
354 for path
, dirs
, files
in os
.walk(root
):
355 path
= path
[len(root
)+1:] # Pretty up the path.
357 if path
in prune_paths
:
361 # Prune out directories we want to skip.
362 # (Note that we loop over PRUNE_DIRS so we're not iterating over a
363 # list that we're simultaneously mutating.)
364 for skip
in PRUNE_DIRS
:
368 if os
.path
.basename(path
) == 'third_party':
369 # Add all subdirectories that are not marked for skipping.
371 dirpath
= os
.path
.join(path
, dir)
372 if dirpath
not in prune_paths
:
373 third_party_dirs
.add(dirpath
)
375 # Don't recurse into any subdirs from here.
379 # Don't recurse into paths in ADDITIONAL_PATHS, like we do with regular
380 # third_party/foo paths.
381 if path
in ADDITIONAL_PATHS
:
384 for dir in ADDITIONAL_PATHS
:
385 if dir not in prune_paths
:
386 third_party_dirs
.add(dir)
388 return third_party_dirs
391 def ScanThirdPartyDirs(root
=None):
392 """Scan a list of directories and report on any problems we find."""
395 third_party_dirs
= FindThirdPartyDirs(PRUNE_PATHS
, root
)
396 third_party_dirs
= FilterDirsWithFiles(third_party_dirs
, root
)
399 for path
in sorted(third_party_dirs
):
401 metadata
= ParseDir(path
, root
)
402 except LicenseError
, e
:
403 errors
.append((path
, e
.args
[0]))
406 for path
, error
in sorted(errors
):
407 print path
+ ": " + error
409 return len(errors
) == 0
412 def GenerateCredits():
413 """Generate about:credits."""
415 if len(sys
.argv
) not in (2, 3):
416 print 'usage: licenses.py credits [output_file]'
419 def EvaluateTemplate(template
, env
, escape
=True):
420 """Expand a template with variables like {{foo}} using a
421 dictionary of expansions."""
422 for key
, val
in env
.items():
424 val
= cgi
.escape(val
)
425 template
= template
.replace('{{%s}}' % key
, val
)
428 root
= os
.path
.join(os
.path
.dirname(__file__
), '..')
429 third_party_dirs
= FindThirdPartyDirs(PRUNE_PATHS
, root
)
431 entry_template
= open(os
.path
.join(root
, 'chrome', 'browser', 'resources',
432 'about_credits_entry.tmpl'), 'rb').read()
434 for path
in sorted(third_party_dirs
):
436 metadata
= ParseDir(path
, root
)
438 # TODO(phajdan.jr): Convert to fatal error (http://crbug.com/39240).
440 if metadata
['License File'] == NOT_SHIPPED
:
443 'name': metadata
['Name'],
444 'url': metadata
['URL'],
445 'license': open(metadata
['License File'], 'rb').read(),
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__':