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', 'pdfsqueeze'): {
154 "Name": "pdfsqueeze",
155 "URL": "http://code.google.com/p/pdfsqueeze/",
156 "License": "Apache 2.0",
157 "License File": "COPYING",
159 os
.path
.join('third_party', 'ppapi'): {
161 "URL": "http://code.google.com/p/ppapi/",
163 os
.path
.join('third_party', 'scons-2.0.1'): {
164 "Name": "scons-2.0.1",
165 "URL": "http://www.scons.org",
167 "License File": "NOT_SHIPPED",
169 os
.path
.join('third_party', 'trace-viewer'): {
170 "Name": "trace-viewer",
171 "URL": "http://code.google.com/p/trace-viewer",
173 "License File": "NOT_SHIPPED",
175 os
.path
.join('third_party', 'v8-i18n'): {
176 "Name": "Internationalization Library for v8",
177 "URL": "http://code.google.com/p/v8-i18n/",
178 "License": "Apache 2.0",
180 os
.path
.join('third_party', 'WebKit'): {
182 "URL": "http://webkit.org/",
183 "License": "BSD and GPL v2",
184 # Absolute path here is resolved as relative to the source root.
185 "License File": "/webkit/LICENSE",
187 os
.path
.join('third_party', 'webpagereplay'): {
188 "Name": "webpagereplay",
189 "URL": "http://code.google.com/p/web-page-replay",
190 "License": "Apache 2.0",
191 "License File": "NOT_SHIPPED",
193 os
.path
.join('tools', 'grit'): {
195 "URL": "http://code.google.com/p/grit-i18n",
197 "License File": "NOT_SHIPPED",
199 os
.path
.join('tools', 'gyp'): {
201 "URL": "http://code.google.com/p/gyp",
203 "License File": "NOT_SHIPPED",
205 os
.path
.join('v8'): {
206 "Name": "V8 JavaScript Engine",
207 "URL": "http://code.google.com/p/v8",
210 os
.path
.join('v8', 'strongtalk'): {
211 "Name": "Strongtalk",
212 "URL": "http://www.strongtalk.org/",
214 # Absolute path here is resolved as relative to the source root.
215 "License File": "/v8/LICENSE.strongtalk",
219 # Special value for 'License File' field used to indicate that the license file
220 # should not be used in about:credits.
221 NOT_SHIPPED
= "NOT_SHIPPED"
224 class LicenseError(Exception):
225 """We raise this exception when a directory's licensing info isn't
229 def AbsolutePath(path
, filename
, root
):
230 """Convert a path in README.chromium to be absolute based on the source
232 if filename
.startswith('/'):
233 # Absolute-looking paths are relative to the source root
234 # (which is the directory we're run from).
235 absolute_path
= os
.path
.join(root
, filename
[1:])
237 absolute_path
= os
.path
.join(root
, path
, filename
)
238 if os
.path
.exists(absolute_path
):
242 def ParseDir(path
, root
, require_license_file
=True):
243 """Examine a third_party/foo component and extract its metadata."""
245 # Parse metadata fields out of README.chromium.
246 # We examine "LICENSE" for the license file by default.
248 "License File": "LICENSE", # Relative path to license text.
249 "Name": None, # Short name (for header on about:credits).
250 "URL": None, # Project home page.
251 "License": None, # Software license.
254 # Relative path to a file containing some html we're required to place in
256 optional_keys
= ["Required Text", "License Android Compatible"]
258 if path
in SPECIAL_CASES
:
259 metadata
.update(SPECIAL_CASES
[path
])
261 # Try to find README.chromium.
262 readme_path
= os
.path
.join(root
, path
, 'README.chromium')
263 if not os
.path
.exists(readme_path
):
264 raise LicenseError("missing README.chromium or licenses.py "
265 "SPECIAL_CASES entry")
267 for line
in open(readme_path
):
271 for key
in metadata
.keys() + optional_keys
:
273 if line
.startswith(field
):
274 metadata
[key
] = line
[len(field
):]
276 # Check that all expected metadata is present.
277 for key
, value
in metadata
.iteritems():
279 raise LicenseError("couldn't find '" + key
+ "' line "
280 "in README.chromium or licences.py "
283 # Special-case modules that aren't in the shipping product, so don't need
284 # their license in about:credits.
285 if metadata
["License File"] != NOT_SHIPPED
:
286 # Check that the license file exists.
287 for filename
in (metadata
["License File"], "COPYING"):
288 license_path
= AbsolutePath(path
, filename
, root
)
289 if license_path
is not None:
292 if require_license_file
and not license_path
:
293 raise LicenseError("License file not found. "
294 "Either add a file named LICENSE, "
295 "import upstream's COPYING if available, "
296 "or add a 'License File:' line to "
297 "README.chromium with the appropriate path.")
298 metadata
["License File"] = license_path
300 if "Required Text" in metadata
:
301 required_path
= AbsolutePath(path
, metadata
["Required Text"], root
)
302 if required_path
is not None:
303 metadata
["Required Text"] = required_path
305 raise LicenseError("Required text file listed but not found.")
310 def ContainsFiles(path
, root
):
311 """Determines whether any files exist in a directory or in any of its
313 for _
, dirs
, files
in os
.walk(os
.path
.join(root
, path
)):
316 for vcs_metadata
in VCS_METADATA_DIRS
:
317 if vcs_metadata
in dirs
:
318 dirs
.remove(vcs_metadata
)
322 def FilterDirsWithFiles(dirs_list
, root
):
323 # If a directory contains no files, assume it's a DEPS directory for a
324 # project not used by our current configuration and skip it.
325 return [x
for x
in dirs_list
if ContainsFiles(x
, root
)]
328 def FindThirdPartyDirs(prune_paths
, root
):
329 """Find all third_party directories underneath the source root."""
330 third_party_dirs
= set()
331 for path
, dirs
, files
in os
.walk(root
):
332 path
= path
[len(root
)+1:] # Pretty up the path.
334 if path
in prune_paths
:
338 # Prune out directories we want to skip.
339 # (Note that we loop over PRUNE_DIRS so we're not iterating over a
340 # list that we're simultaneously mutating.)
341 for skip
in PRUNE_DIRS
:
345 if os
.path
.basename(path
) == 'third_party':
346 # Add all subdirectories that are not marked for skipping.
348 dirpath
= os
.path
.join(path
, dir)
349 if dirpath
not in prune_paths
:
350 third_party_dirs
.add(dirpath
)
352 # Don't recurse into any subdirs from here.
356 # Don't recurse into paths in ADDITIONAL_PATHS, like we do with regular
357 # third_party/foo paths.
358 if path
in ADDITIONAL_PATHS
:
361 for dir in ADDITIONAL_PATHS
:
362 if dir not in prune_paths
:
363 third_party_dirs
.add(dir)
365 return third_party_dirs
368 def ScanThirdPartyDirs(root
=None):
369 """Scan a list of directories and report on any problems we find."""
372 third_party_dirs
= FindThirdPartyDirs(PRUNE_PATHS
, root
)
373 third_party_dirs
= FilterDirsWithFiles(third_party_dirs
, root
)
376 for path
in sorted(third_party_dirs
):
378 metadata
= ParseDir(path
, root
)
379 except LicenseError
, e
:
380 errors
.append((path
, e
.args
[0]))
383 for path
, error
in sorted(errors
):
384 print path
+ ": " + error
386 return len(errors
) == 0
389 def GenerateCredits():
390 """Generate about:credits."""
392 if len(sys
.argv
) not in (2, 3):
393 print 'usage: licenses.py credits [output_file]'
396 def EvaluateTemplate(template
, env
, escape
=True):
397 """Expand a template with variables like {{foo}} using a
398 dictionary of expansions."""
399 for key
, val
in env
.items():
400 if escape
and not key
.endswith("_unescaped"):
401 val
= cgi
.escape(val
)
402 template
= template
.replace('{{%s}}' % key
, val
)
405 root
= os
.path
.join(os
.path
.dirname(__file__
), '..')
406 third_party_dirs
= FindThirdPartyDirs(PRUNE_PATHS
, root
)
408 entry_template
= open(os
.path
.join(root
, 'chrome', 'browser', 'resources',
409 'about_credits_entry.tmpl'), 'rb').read()
411 for path
in sorted(third_party_dirs
):
413 metadata
= ParseDir(path
, root
)
415 # TODO(phajdan.jr): Convert to fatal error (http://crbug.com/39240).
417 if metadata
['License File'] == NOT_SHIPPED
:
420 'name': metadata
['Name'],
421 'url': metadata
['URL'],
422 'license': open(metadata
['License File'], 'rb').read(),
423 'license_unescaped': '',
425 if 'Required Text' in metadata
:
426 required_text
= open(metadata
['Required Text'], 'rb').read()
427 env
["license_unescaped"] = required_text
428 entries
.append(EvaluateTemplate(entry_template
, env
))
430 file_template
= open(os
.path
.join(root
, 'chrome', 'browser', 'resources',
431 'about_credits.tmpl'), 'rb').read()
432 template_contents
= "<!-- Generated by licenses.py; do not edit. -->"
433 template_contents
+= EvaluateTemplate(file_template
,
434 {'entries': '\n'.join(entries
)},
437 if len(sys
.argv
) == 3:
438 with
open(sys
.argv
[2], 'w') as output_file
:
439 output_file
.write(template_contents
)
440 elif len(sys
.argv
) == 2:
441 print template_contents
448 if len(sys
.argv
) > 1:
449 command
= sys
.argv
[1]
451 if command
== 'scan':
452 if not ScanThirdPartyDirs():
454 elif command
== 'credits':
455 if not GenerateCredits():
462 if __name__
== '__main__':