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'),
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('native_client'): {
109 "Name": "native client",
110 "URL": "http://code.google.com/p/nativeclient",
113 os
.path
.join('sdch', 'open-vcdiff'): {
114 "Name": "open-vcdiff",
115 "URL": "http://code.google.com/p/open-vcdiff",
116 "License": "Apache 2.0, MIT, GPL v2 and custom licenses",
117 "License Android Compatible": "yes",
119 os
.path
.join('testing', 'gmock'): {
121 "URL": "http://code.google.com/p/googlemock",
123 "License File": "NOT_SHIPPED",
125 os
.path
.join('testing', 'gtest'): {
127 "URL": "http://code.google.com/p/googletest",
129 "License File": "NOT_SHIPPED",
131 os
.path
.join('third_party', 'angle'): {
132 "Name": "Almost Native Graphics Layer Engine",
133 "URL": "http://code.google.com/p/angleproject/",
136 os
.path
.join('third_party', 'cros_system_api'): {
137 "Name": "Chromium OS system API",
138 "URL": "http://www.chromium.org/chromium-os",
140 # Absolute path here is resolved as relative to the source root.
141 "License File": "/LICENSE.chromium_os",
143 os
.path
.join('third_party', 'lss'): {
144 "Name": "linux-syscall-support",
145 "URL": "http://code.google.com/p/linux-syscall-support/",
147 "License File": "/LICENSE",
149 os
.path
.join('third_party', 'ots'): {
150 "Name": "OTS (OpenType Sanitizer)",
151 "URL": "http://code.google.com/p/ots/",
154 os
.path
.join('third_party', 'pdfium'): {
156 "URL": "http://code.google.com/p/pdfium/",
159 os
.path
.join('third_party', 'pdfsqueeze'): {
160 "Name": "pdfsqueeze",
161 "URL": "http://code.google.com/p/pdfsqueeze/",
162 "License": "Apache 2.0",
163 "License File": "COPYING",
165 os
.path
.join('third_party', 'ppapi'): {
167 "URL": "http://code.google.com/p/ppapi/",
169 os
.path
.join('third_party', 'scons-2.0.1'): {
170 "Name": "scons-2.0.1",
171 "URL": "http://www.scons.org",
173 "License File": "NOT_SHIPPED",
175 os
.path
.join('third_party', 'trace-viewer'): {
176 "Name": "trace-viewer",
177 "URL": "http://code.google.com/p/trace-viewer",
179 "License File": "NOT_SHIPPED",
181 os
.path
.join('third_party', 'v8-i18n'): {
182 "Name": "Internationalization Library for v8",
183 "URL": "http://code.google.com/p/v8-i18n/",
184 "License": "Apache 2.0",
186 os
.path
.join('third_party', 'WebKit'): {
188 "URL": "http://webkit.org/",
189 "License": "BSD and GPL v2",
190 # Absolute path here is resolved as relative to the source root.
191 "License File": "/webkit/LICENSE",
193 os
.path
.join('third_party', 'webpagereplay'): {
194 "Name": "webpagereplay",
195 "URL": "http://code.google.com/p/web-page-replay",
196 "License": "Apache 2.0",
197 "License File": "NOT_SHIPPED",
199 os
.path
.join('tools', 'grit'): {
201 "URL": "http://code.google.com/p/grit-i18n",
203 "License File": "NOT_SHIPPED",
205 os
.path
.join('tools', 'gyp'): {
207 "URL": "http://code.google.com/p/gyp",
209 "License File": "NOT_SHIPPED",
211 os
.path
.join('v8'): {
212 "Name": "V8 JavaScript Engine",
213 "URL": "http://code.google.com/p/v8",
216 os
.path
.join('v8', 'strongtalk'): {
217 "Name": "Strongtalk",
218 "URL": "http://www.strongtalk.org/",
220 # Absolute path here is resolved as relative to the source root.
221 "License File": "/v8/LICENSE.strongtalk",
225 # Special value for 'License File' field used to indicate that the license file
226 # should not be used in about:credits.
227 NOT_SHIPPED
= "NOT_SHIPPED"
230 class LicenseError(Exception):
231 """We raise this exception when a directory's licensing info isn't
235 def AbsolutePath(path
, filename
, root
):
236 """Convert a path in README.chromium to be absolute based on the source
238 if filename
.startswith('/'):
239 # Absolute-looking paths are relative to the source root
240 # (which is the directory we're run from).
241 absolute_path
= os
.path
.join(root
, filename
[1:])
243 absolute_path
= os
.path
.join(root
, path
, filename
)
244 if os
.path
.exists(absolute_path
):
248 def ParseDir(path
, root
, require_license_file
=True):
249 """Examine a third_party/foo component and extract its metadata."""
251 # Parse metadata fields out of README.chromium.
252 # We examine "LICENSE" for the license file by default.
254 "License File": "LICENSE", # Relative path to license text.
255 "Name": None, # Short name (for header on about:credits).
256 "URL": None, # Project home page.
257 "License": None, # Software license.
260 # Relative path to a file containing some html we're required to place in
262 optional_keys
= ["Required Text", "License Android Compatible"]
264 if path
in SPECIAL_CASES
:
265 metadata
.update(SPECIAL_CASES
[path
])
267 # Try to find README.chromium.
268 readme_path
= os
.path
.join(root
, path
, 'README.chromium')
269 if not os
.path
.exists(readme_path
):
270 raise LicenseError("missing README.chromium or licenses.py "
271 "SPECIAL_CASES entry")
273 for line
in open(readme_path
):
277 for key
in metadata
.keys() + optional_keys
:
279 if line
.startswith(field
):
280 metadata
[key
] = line
[len(field
):]
282 # Check that all expected metadata is present.
283 for key
, value
in metadata
.iteritems():
285 raise LicenseError("couldn't find '" + key
+ "' line "
286 "in README.chromium or licences.py "
289 # Special-case modules that aren't in the shipping product, so don't need
290 # their license in about:credits.
291 if metadata
["License File"] != NOT_SHIPPED
:
292 # Check that the license file exists.
293 for filename
in (metadata
["License File"], "COPYING"):
294 license_path
= AbsolutePath(path
, filename
, root
)
295 if license_path
is not None:
298 if require_license_file
and not license_path
:
299 raise LicenseError("License file not found. "
300 "Either add a file named LICENSE, "
301 "import upstream's COPYING if available, "
302 "or add a 'License File:' line to "
303 "README.chromium with the appropriate path.")
304 metadata
["License File"] = license_path
306 if "Required Text" in metadata
:
307 required_path
= AbsolutePath(path
, metadata
["Required Text"], root
)
308 if required_path
is not None:
309 metadata
["Required Text"] = required_path
311 raise LicenseError("Required text file listed but not found.")
316 def ContainsFiles(path
, root
):
317 """Determines whether any files exist in a directory or in any of its
319 for _
, dirs
, files
in os
.walk(os
.path
.join(root
, path
)):
322 for vcs_metadata
in VCS_METADATA_DIRS
:
323 if vcs_metadata
in dirs
:
324 dirs
.remove(vcs_metadata
)
328 def FilterDirsWithFiles(dirs_list
, root
):
329 # If a directory contains no files, assume it's a DEPS directory for a
330 # project not used by our current configuration and skip it.
331 return [x
for x
in dirs_list
if ContainsFiles(x
, root
)]
334 def FindThirdPartyDirs(prune_paths
, root
):
335 """Find all third_party directories underneath the source root."""
336 third_party_dirs
= set()
337 for path
, dirs
, files
in os
.walk(root
):
338 path
= path
[len(root
)+1:] # Pretty up the path.
340 if path
in prune_paths
:
344 # Prune out directories we want to skip.
345 # (Note that we loop over PRUNE_DIRS so we're not iterating over a
346 # list that we're simultaneously mutating.)
347 for skip
in PRUNE_DIRS
:
351 if os
.path
.basename(path
) == 'third_party':
352 # Add all subdirectories that are not marked for skipping.
354 dirpath
= os
.path
.join(path
, dir)
355 if dirpath
not in prune_paths
:
356 third_party_dirs
.add(dirpath
)
358 # Don't recurse into any subdirs from here.
362 # Don't recurse into paths in ADDITIONAL_PATHS, like we do with regular
363 # third_party/foo paths.
364 if path
in ADDITIONAL_PATHS
:
367 for dir in ADDITIONAL_PATHS
:
368 if dir not in prune_paths
:
369 third_party_dirs
.add(dir)
371 return third_party_dirs
374 def ScanThirdPartyDirs(root
=None):
375 """Scan a list of directories and report on any problems we find."""
378 third_party_dirs
= FindThirdPartyDirs(PRUNE_PATHS
, root
)
379 third_party_dirs
= FilterDirsWithFiles(third_party_dirs
, root
)
382 for path
in sorted(third_party_dirs
):
384 metadata
= ParseDir(path
, root
)
385 except LicenseError
, e
:
386 errors
.append((path
, e
.args
[0]))
389 for path
, error
in sorted(errors
):
390 print path
+ ": " + error
392 return len(errors
) == 0
395 def GenerateCredits():
396 """Generate about:credits."""
398 if len(sys
.argv
) not in (2, 3):
399 print 'usage: licenses.py credits [output_file]'
402 def EvaluateTemplate(template
, env
, escape
=True):
403 """Expand a template with variables like {{foo}} using a
404 dictionary of expansions."""
405 for key
, val
in env
.items():
406 if escape
and not key
.endswith("_unescaped"):
407 val
= cgi
.escape(val
)
408 template
= template
.replace('{{%s}}' % key
, val
)
411 root
= os
.path
.join(os
.path
.dirname(__file__
), '..')
412 third_party_dirs
= FindThirdPartyDirs(PRUNE_PATHS
, root
)
414 entry_template
= open(os
.path
.join(root
, 'chrome', 'browser', 'resources',
415 'about_credits_entry.tmpl'), 'rb').read()
417 for path
in sorted(third_party_dirs
):
419 metadata
= ParseDir(path
, root
)
421 # TODO(phajdan.jr): Convert to fatal error (http://crbug.com/39240).
423 if metadata
['License File'] == NOT_SHIPPED
:
426 'name': metadata
['Name'],
427 'url': metadata
['URL'],
428 'license': open(metadata
['License File'], 'rb').read(),
429 'license_unescaped': '',
431 if 'Required Text' in metadata
:
432 required_text
= open(metadata
['Required Text'], 'rb').read()
433 env
["license_unescaped"] = required_text
434 entries
.append(EvaluateTemplate(entry_template
, env
))
436 file_template
= open(os
.path
.join(root
, 'chrome', 'browser', 'resources',
437 'about_credits.tmpl'), 'rb').read()
438 template_contents
= "<!-- Generated by licenses.py; do not edit. -->"
439 template_contents
+= EvaluateTemplate(file_template
,
440 {'entries': '\n'.join(entries
)},
443 if len(sys
.argv
) == 3:
444 with
open(sys
.argv
[2], 'w') as output_file
:
445 output_file
.write(template_contents
)
446 elif len(sys
.argv
) == 2:
447 print template_contents
454 if len(sys
.argv
) > 1:
455 command
= sys
.argv
[1]
457 if command
== 'scan':
458 if not ScanThirdPartyDirs():
460 elif command
== 'credits':
461 if not GenerateCredits():
468 if __name__
== '__main__':