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','pywebsocket'),
53 os
.path
.join('third_party','syzygy'),
54 os
.path
.join('tools','gn'),
56 # Chromium code in third_party.
57 os
.path
.join('third_party','fuzzymatch'),
58 os
.path
.join('tools', 'swarming_client'),
60 # Stuff pulled in from chrome-internal for official builds/tools.
61 os
.path
.join('third_party', 'clear_cache'),
62 os
.path
.join('third_party', 'gnu'),
63 os
.path
.join('third_party', 'googlemac'),
64 os
.path
.join('third_party', 'pcre'),
65 os
.path
.join('third_party', 'psutils'),
66 os
.path
.join('third_party', 'sawbuck'),
68 # Redistribution does not require attribution in documentation.
69 os
.path
.join('third_party','directxsdk'),
70 os
.path
.join('third_party','platformsdk_win2008_6_1'),
71 os
.path
.join('third_party','platformsdk_win7'),
74 # Directories we don't scan through.
75 VCS_METADATA_DIRS
= ('.svn', '.git')
76 PRUNE_DIRS
= (VCS_METADATA_DIRS
+
77 ('out', 'Debug', 'Release', # build files
78 'layout_tests')) # lots of subdirs
81 os
.path
.join('breakpad'),
82 os
.path
.join('chrome', 'common', 'extensions', 'docs', 'examples'),
83 os
.path
.join('chrome', 'test', 'chromeos', 'autotest'),
84 os
.path
.join('chrome', 'test', 'data'),
85 os
.path
.join('native_client'),
86 os
.path
.join('net', 'tools', 'spdyshark'),
87 os
.path
.join('sdch', 'open-vcdiff'),
88 os
.path
.join('testing', 'gmock'),
89 os
.path
.join('testing', 'gtest'),
90 # The directory with the word list for Chinese and Japanese segmentation
91 # with different license terms than ICU.
92 os
.path
.join('third_party','icu','source','data','brkitr'),
93 os
.path
.join('tools', 'grit'),
94 os
.path
.join('tools', 'gyp'),
95 os
.path
.join('tools', 'page_cycler', 'acid3'),
96 os
.path
.join('url', 'third_party', 'mozilla'),
98 # Fake directory so we can include the strongtalk license.
99 os
.path
.join('v8', 'strongtalk'),
103 # Directories where we check out directly from upstream, and therefore
104 # can't provide a README.chromium. Please prefer a README.chromium
107 os
.path
.join('native_client'): {
108 "Name": "native client",
109 "URL": "http://code.google.com/p/nativeclient",
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",
122 "License File": "NOT_SHIPPED",
124 os
.path
.join('testing', 'gtest'): {
126 "URL": "http://code.google.com/p/googletest",
128 "License File": "NOT_SHIPPED",
130 os
.path
.join('third_party', 'angle'): {
131 "Name": "Almost Native Graphics Layer Engine",
132 "URL": "http://code.google.com/p/angleproject/",
135 os
.path
.join('third_party', 'cros_system_api'): {
136 "Name": "Chromium OS system API",
137 "URL": "http://www.chromium.org/chromium-os",
139 # Absolute path here is resolved as relative to the source root.
140 "License File": "/LICENSE.chromium_os",
142 os
.path
.join('third_party', 'lss'): {
143 "Name": "linux-syscall-support",
144 "URL": "http://code.google.com/p/linux-syscall-support/",
146 "License File": "/LICENSE",
148 os
.path
.join('third_party', 'ots'): {
149 "Name": "OTS (OpenType Sanitizer)",
150 "URL": "http://code.google.com/p/ots/",
153 os
.path
.join('third_party', 'pdfium'): {
155 "URL": "http://code.google.com/p/pdfium/",
158 os
.path
.join('third_party', 'pdfsqueeze'): {
159 "Name": "pdfsqueeze",
160 "URL": "http://code.google.com/p/pdfsqueeze/",
161 "License": "Apache 2.0",
162 "License File": "COPYING",
164 os
.path
.join('third_party', 'ppapi'): {
166 "URL": "http://code.google.com/p/ppapi/",
168 os
.path
.join('third_party', 'scons-2.0.1'): {
169 "Name": "scons-2.0.1",
170 "URL": "http://www.scons.org",
172 "License File": "NOT_SHIPPED",
174 os
.path
.join('third_party', 'trace-viewer'): {
175 "Name": "trace-viewer",
176 "URL": "http://code.google.com/p/trace-viewer",
178 "License File": "NOT_SHIPPED",
180 os
.path
.join('third_party', 'v8-i18n'): {
181 "Name": "Internationalization Library for v8",
182 "URL": "http://code.google.com/p/v8-i18n/",
183 "License": "Apache 2.0",
185 os
.path
.join('third_party', 'WebKit'): {
187 "URL": "http://webkit.org/",
188 "License": "BSD and GPL v2",
189 # Absolute path here is resolved as relative to the source root.
190 "License File": "/webkit/LICENSE",
192 os
.path
.join('third_party', 'webpagereplay'): {
193 "Name": "webpagereplay",
194 "URL": "http://code.google.com/p/web-page-replay",
195 "License": "Apache 2.0",
196 "License File": "NOT_SHIPPED",
198 os
.path
.join('tools', 'grit'): {
200 "URL": "http://code.google.com/p/grit-i18n",
202 "License File": "NOT_SHIPPED",
204 os
.path
.join('tools', 'gyp'): {
206 "URL": "http://code.google.com/p/gyp",
208 "License File": "NOT_SHIPPED",
210 os
.path
.join('v8'): {
211 "Name": "V8 JavaScript Engine",
212 "URL": "http://code.google.com/p/v8",
215 os
.path
.join('v8', 'strongtalk'): {
216 "Name": "Strongtalk",
217 "URL": "http://www.strongtalk.org/",
219 # Absolute path here is resolved as relative to the source root.
220 "License File": "/v8/LICENSE.strongtalk",
224 # Special value for 'License File' field used to indicate that the license file
225 # should not be used in about:credits.
226 NOT_SHIPPED
= "NOT_SHIPPED"
229 class LicenseError(Exception):
230 """We raise this exception when a directory's licensing info isn't
234 def AbsolutePath(path
, filename
, root
):
235 """Convert a path in README.chromium to be absolute based on the source
237 if filename
.startswith('/'):
238 # Absolute-looking paths are relative to the source root
239 # (which is the directory we're run from).
240 absolute_path
= os
.path
.join(root
, filename
[1:])
242 absolute_path
= os
.path
.join(root
, path
, filename
)
243 if os
.path
.exists(absolute_path
):
247 def ParseDir(path
, root
, require_license_file
=True):
248 """Examine a third_party/foo component and extract its metadata."""
250 # Parse metadata fields out of README.chromium.
251 # We examine "LICENSE" for the license file by default.
253 "License File": "LICENSE", # Relative path to license text.
254 "Name": None, # Short name (for header on about:credits).
255 "URL": None, # Project home page.
256 "License": None, # Software license.
259 # Relative path to a file containing some html we're required to place in
261 optional_keys
= ["Required Text", "License Android Compatible"]
263 if path
in SPECIAL_CASES
:
264 metadata
.update(SPECIAL_CASES
[path
])
266 # Try to find README.chromium.
267 readme_path
= os
.path
.join(root
, path
, 'README.chromium')
268 if not os
.path
.exists(readme_path
):
269 raise LicenseError("missing README.chromium or licenses.py "
270 "SPECIAL_CASES entry")
272 for line
in open(readme_path
):
276 for key
in metadata
.keys() + optional_keys
:
278 if line
.startswith(field
):
279 metadata
[key
] = line
[len(field
):]
281 # Check that all expected metadata is present.
282 for key
, value
in metadata
.iteritems():
284 raise LicenseError("couldn't find '" + key
+ "' line "
285 "in README.chromium or licences.py "
288 # Special-case modules that aren't in the shipping product, so don't need
289 # their license in about:credits.
290 if metadata
["License File"] != NOT_SHIPPED
:
291 # Check that the license file exists.
292 for filename
in (metadata
["License File"], "COPYING"):
293 license_path
= AbsolutePath(path
, filename
, root
)
294 if license_path
is not None:
297 if require_license_file
and not license_path
:
298 raise LicenseError("License file not found. "
299 "Either add a file named LICENSE, "
300 "import upstream's COPYING if available, "
301 "or add a 'License File:' line to "
302 "README.chromium with the appropriate path.")
303 metadata
["License File"] = license_path
305 if "Required Text" in metadata
:
306 required_path
= AbsolutePath(path
, metadata
["Required Text"], root
)
307 if required_path
is not None:
308 metadata
["Required Text"] = required_path
310 raise LicenseError("Required text file listed but not found.")
315 def ContainsFiles(path
, root
):
316 """Determines whether any files exist in a directory or in any of its
318 for _
, dirs
, files
in os
.walk(os
.path
.join(root
, path
)):
321 for vcs_metadata
in VCS_METADATA_DIRS
:
322 if vcs_metadata
in dirs
:
323 dirs
.remove(vcs_metadata
)
327 def FilterDirsWithFiles(dirs_list
, root
):
328 # If a directory contains no files, assume it's a DEPS directory for a
329 # project not used by our current configuration and skip it.
330 return [x
for x
in dirs_list
if ContainsFiles(x
, root
)]
333 def FindThirdPartyDirs(prune_paths
, root
):
334 """Find all third_party directories underneath the source root."""
335 third_party_dirs
= set()
336 for path
, dirs
, files
in os
.walk(root
):
337 path
= path
[len(root
)+1:] # Pretty up the path.
339 if path
in prune_paths
:
343 # Prune out directories we want to skip.
344 # (Note that we loop over PRUNE_DIRS so we're not iterating over a
345 # list that we're simultaneously mutating.)
346 for skip
in PRUNE_DIRS
:
350 if os
.path
.basename(path
) == 'third_party':
351 # Add all subdirectories that are not marked for skipping.
353 dirpath
= os
.path
.join(path
, dir)
354 if dirpath
not in prune_paths
:
355 third_party_dirs
.add(dirpath
)
357 # Don't recurse into any subdirs from here.
361 # Don't recurse into paths in ADDITIONAL_PATHS, like we do with regular
362 # third_party/foo paths.
363 if path
in ADDITIONAL_PATHS
:
366 for dir in ADDITIONAL_PATHS
:
367 if dir not in prune_paths
:
368 third_party_dirs
.add(dir)
370 return third_party_dirs
373 def ScanThirdPartyDirs(root
=None):
374 """Scan a list of directories and report on any problems we find."""
377 third_party_dirs
= FindThirdPartyDirs(PRUNE_PATHS
, root
)
378 third_party_dirs
= FilterDirsWithFiles(third_party_dirs
, root
)
381 for path
in sorted(third_party_dirs
):
383 metadata
= ParseDir(path
, root
)
384 except LicenseError
, e
:
385 errors
.append((path
, e
.args
[0]))
388 for path
, error
in sorted(errors
):
389 print path
+ ": " + error
391 return len(errors
) == 0
394 def GenerateCredits():
395 """Generate about:credits."""
397 if len(sys
.argv
) not in (2, 3):
398 print 'usage: licenses.py credits [output_file]'
401 def EvaluateTemplate(template
, env
, escape
=True):
402 """Expand a template with variables like {{foo}} using a
403 dictionary of expansions."""
404 for key
, val
in env
.items():
405 if escape
and not key
.endswith("_unescaped"):
406 val
= cgi
.escape(val
)
407 template
= template
.replace('{{%s}}' % key
, val
)
410 root
= os
.path
.join(os
.path
.dirname(__file__
), '..')
411 third_party_dirs
= FindThirdPartyDirs(PRUNE_PATHS
, root
)
413 entry_template
= open(os
.path
.join(root
, 'chrome', 'browser', 'resources',
414 'about_credits_entry.tmpl'), 'rb').read()
416 for path
in sorted(third_party_dirs
):
418 metadata
= ParseDir(path
, root
)
420 # TODO(phajdan.jr): Convert to fatal error (http://crbug.com/39240).
422 if metadata
['License File'] == NOT_SHIPPED
:
425 'name': metadata
['Name'],
426 'url': metadata
['URL'],
427 'license': open(metadata
['License File'], 'rb').read(),
428 'license_unescaped': '',
430 if 'Required Text' in metadata
:
431 required_text
= open(metadata
['Required Text'], 'rb').read()
432 env
["license_unescaped"] = required_text
433 entries
.append(EvaluateTemplate(entry_template
, env
))
435 file_template
= open(os
.path
.join(root
, 'chrome', 'browser', 'resources',
436 'about_credits.tmpl'), 'rb').read()
437 template_contents
= "<!-- Generated by licenses.py; do not edit. -->"
438 template_contents
+= EvaluateTemplate(file_template
,
439 {'entries': '\n'.join(entries
)},
442 if len(sys
.argv
) == 3:
443 with
open(sys
.argv
[2], 'w') as output_file
:
444 output_file
.write(template_contents
)
445 elif len(sys
.argv
) == 2:
446 print template_contents
453 if len(sys
.argv
) > 1:
454 command
= sys
.argv
[1]
456 if command
== 'scan':
457 if not ScanThirdPartyDirs():
459 elif command
== 'credits':
460 if not GenerateCredits():
467 if __name__
== '__main__':