1*3f982cf4SFabien Sanglard#!/usr/bin/env python3 2*3f982cf4SFabien Sanglard# Copyright 2020 The Chromium Authors. All rights reserved. 3*3f982cf4SFabien Sanglard# Use of this source code is governed by a BSD-style license that can be 4*3f982cf4SFabien Sanglard# found in the LICENSE file. 5*3f982cf4SFabien Sanglard"""Utility for checking and processing licensing information in third_party 6*3f982cf4SFabien Sanglarddirectories. Copied from Chrome's tools/licenses.py. 7*3f982cf4SFabien Sanglard 8*3f982cf4SFabien SanglardUsage: licenses.py <command> 9*3f982cf4SFabien Sanglard 10*3f982cf4SFabien SanglardCommands: 11*3f982cf4SFabien Sanglard scan scan third_party directories, verifying that we have licensing info 12*3f982cf4SFabien Sanglard credits generate about:credits on stdout 13*3f982cf4SFabien Sanglard 14*3f982cf4SFabien Sanglard(You can also import this as a module.) 15*3f982cf4SFabien Sanglard""" 16*3f982cf4SFabien Sanglardfrom __future__ import print_function 17*3f982cf4SFabien Sanglard 18*3f982cf4SFabien Sanglardimport argparse 19*3f982cf4SFabien Sanglardimport codecs 20*3f982cf4SFabien Sanglardimport json 21*3f982cf4SFabien Sanglardimport os 22*3f982cf4SFabien Sanglardimport shutil 23*3f982cf4SFabien Sanglardimport re 24*3f982cf4SFabien Sanglardimport subprocess 25*3f982cf4SFabien Sanglardimport sys 26*3f982cf4SFabien Sanglardimport tempfile 27*3f982cf4SFabien Sanglard 28*3f982cf4SFabien Sanglard# TODO(issuetracker.google.com/173766869): Remove Python2 checks/compatibility. 29*3f982cf4SFabien Sanglardif sys.version_info.major == 2: 30*3f982cf4SFabien Sanglard from cgi import escape 31*3f982cf4SFabien Sanglardelse: 32*3f982cf4SFabien Sanglard from html import escape 33*3f982cf4SFabien Sanglard 34*3f982cf4SFabien Sanglard_REPOSITORY_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) 35*3f982cf4SFabien Sanglard 36*3f982cf4SFabien Sanglard# Paths from the root of the tree to directories to skip. 37*3f982cf4SFabien SanglardPRUNE_PATHS = set([ 38*3f982cf4SFabien Sanglard # Used for development and test, not in the shipping product. 39*3f982cf4SFabien Sanglard os.path.join('third_party', 'llvm-build'), 40*3f982cf4SFabien Sanglard]) 41*3f982cf4SFabien Sanglard 42*3f982cf4SFabien Sanglard# Directories we don't scan through. 43*3f982cf4SFabien SanglardPRUNE_DIRS = ('.git') 44*3f982cf4SFabien Sanglard 45*3f982cf4SFabien Sanglard# Directories where we check out directly from upstream, and therefore 46*3f982cf4SFabien Sanglard# can't provide a README.chromium. Please prefer a README.chromium 47*3f982cf4SFabien Sanglard# wherever possible. 48*3f982cf4SFabien SanglardSPECIAL_CASES = { 49*3f982cf4SFabien Sanglard os.path.join('third_party', 'googletest'): { 50*3f982cf4SFabien Sanglard "Name": "gtest", 51*3f982cf4SFabien Sanglard "URL": "http://code.google.com/p/googletest", 52*3f982cf4SFabien Sanglard "License": "BSD", 53*3f982cf4SFabien Sanglard "License File": "NOT_SHIPPED", 54*3f982cf4SFabien Sanglard } 55*3f982cf4SFabien Sanglard} 56*3f982cf4SFabien Sanglard 57*3f982cf4SFabien Sanglard# Special value for 'License File' field used to indicate that the license file 58*3f982cf4SFabien Sanglard# should not be used in about:credits. 59*3f982cf4SFabien SanglardNOT_SHIPPED = "NOT_SHIPPED" 60*3f982cf4SFabien Sanglard 61*3f982cf4SFabien Sanglard 62*3f982cf4SFabien Sanglarddef MakeDirectory(dir_path): 63*3f982cf4SFabien Sanglard try: 64*3f982cf4SFabien Sanglard os.makedirs(dir_path) 65*3f982cf4SFabien Sanglard except OSError: 66*3f982cf4SFabien Sanglard pass 67*3f982cf4SFabien Sanglard 68*3f982cf4SFabien Sanglard 69*3f982cf4SFabien Sanglarddef WriteDepfile(depfile_path, first_gn_output, inputs=None): 70*3f982cf4SFabien Sanglard assert depfile_path != first_gn_output # http://crbug.com/646165 71*3f982cf4SFabien Sanglard assert not isinstance(inputs, string_types) # Easy mistake to make 72*3f982cf4SFabien Sanglard inputs = inputs or [] 73*3f982cf4SFabien Sanglard MakeDirectory(os.path.dirname(depfile_path)) 74*3f982cf4SFabien Sanglard # Ninja does not support multiple outputs in depfiles. 75*3f982cf4SFabien Sanglard with open(depfile_path, 'w') as depfile: 76*3f982cf4SFabien Sanglard depfile.write(first_gn_output.replace(' ', '\\ ')) 77*3f982cf4SFabien Sanglard depfile.write(': ') 78*3f982cf4SFabien Sanglard depfile.write(' '.join(i.replace(' ', '\\ ') for i in inputs)) 79*3f982cf4SFabien Sanglard depfile.write('\n') 80*3f982cf4SFabien Sanglard 81*3f982cf4SFabien Sanglard 82*3f982cf4SFabien Sanglardclass LicenseError(Exception): 83*3f982cf4SFabien Sanglard """We raise this exception when a directory's licensing info isn't 84*3f982cf4SFabien Sanglard fully filled out.""" 85*3f982cf4SFabien Sanglard pass 86*3f982cf4SFabien Sanglard 87*3f982cf4SFabien Sanglard 88*3f982cf4SFabien Sanglarddef AbsolutePath(path, filename, root): 89*3f982cf4SFabien Sanglard """Convert a path in README.chromium to be absolute based on the source 90*3f982cf4SFabien Sanglard root.""" 91*3f982cf4SFabien Sanglard if filename.startswith('/'): 92*3f982cf4SFabien Sanglard # Absolute-looking paths are relative to the source root 93*3f982cf4SFabien Sanglard # (which is the directory we're run from). 94*3f982cf4SFabien Sanglard absolute_path = os.path.join(root, filename[1:]) 95*3f982cf4SFabien Sanglard else: 96*3f982cf4SFabien Sanglard absolute_path = os.path.join(root, path, filename) 97*3f982cf4SFabien Sanglard if os.path.exists(absolute_path): 98*3f982cf4SFabien Sanglard return absolute_path 99*3f982cf4SFabien Sanglard return None 100*3f982cf4SFabien Sanglard 101*3f982cf4SFabien Sanglard 102*3f982cf4SFabien Sanglarddef ParseDir(path, root, require_license_file=True, optional_keys=None): 103*3f982cf4SFabien Sanglard """Examine a third_party/foo component and extract its metadata.""" 104*3f982cf4SFabien Sanglard # Parse metadata fields out of README.chromium. 105*3f982cf4SFabien Sanglard # We examine "LICENSE" for the license file by default. 106*3f982cf4SFabien Sanglard metadata = { 107*3f982cf4SFabien Sanglard "License File": "LICENSE", # Relative path to license text. 108*3f982cf4SFabien Sanglard "Name": None, # Short name (for header on about:credits). 109*3f982cf4SFabien Sanglard "URL": None, # Project home page. 110*3f982cf4SFabien Sanglard "License": None, # Software license. 111*3f982cf4SFabien Sanglard } 112*3f982cf4SFabien Sanglard 113*3f982cf4SFabien Sanglard if optional_keys is None: 114*3f982cf4SFabien Sanglard optional_keys = [] 115*3f982cf4SFabien Sanglard 116*3f982cf4SFabien Sanglard if path in SPECIAL_CASES: 117*3f982cf4SFabien Sanglard metadata.update(SPECIAL_CASES[path]) 118*3f982cf4SFabien Sanglard else: 119*3f982cf4SFabien Sanglard # Try to find README.chromium. 120*3f982cf4SFabien Sanglard readme_path = os.path.join(root, path, 'README.chromium') 121*3f982cf4SFabien Sanglard if not os.path.exists(readme_path): 122*3f982cf4SFabien Sanglard raise LicenseError("missing README.chromium or licenses.py " 123*3f982cf4SFabien Sanglard "SPECIAL_CASES entry in %s\n" % path) 124*3f982cf4SFabien Sanglard 125*3f982cf4SFabien Sanglard for line in open(readme_path): 126*3f982cf4SFabien Sanglard line = line.strip() 127*3f982cf4SFabien Sanglard if not line: 128*3f982cf4SFabien Sanglard break 129*3f982cf4SFabien Sanglard for key in list(metadata.keys()) + optional_keys: 130*3f982cf4SFabien Sanglard field = key + ": " 131*3f982cf4SFabien Sanglard if line.startswith(field): 132*3f982cf4SFabien Sanglard metadata[key] = line[len(field):] 133*3f982cf4SFabien Sanglard 134*3f982cf4SFabien Sanglard # Check that all expected metadata is present. 135*3f982cf4SFabien Sanglard errors = [] 136*3f982cf4SFabien Sanglard for key, value in metadata.items(): 137*3f982cf4SFabien Sanglard if not value: 138*3f982cf4SFabien Sanglard errors.append("couldn't find '" + key + "' line " 139*3f982cf4SFabien Sanglard "in README.chromium or licences.py " 140*3f982cf4SFabien Sanglard "SPECIAL_CASES") 141*3f982cf4SFabien Sanglard 142*3f982cf4SFabien Sanglard # Special-case modules that aren't in the shipping product, so don't need 143*3f982cf4SFabien Sanglard # their license in about:credits. 144*3f982cf4SFabien Sanglard if metadata["License File"] != NOT_SHIPPED: 145*3f982cf4SFabien Sanglard # Check that the license file exists. 146*3f982cf4SFabien Sanglard for filename in (metadata["License File"], "COPYING"): 147*3f982cf4SFabien Sanglard license_path = AbsolutePath(path, filename, root) 148*3f982cf4SFabien Sanglard if license_path is not None: 149*3f982cf4SFabien Sanglard break 150*3f982cf4SFabien Sanglard 151*3f982cf4SFabien Sanglard if require_license_file and not license_path: 152*3f982cf4SFabien Sanglard errors.append("License file not found. " 153*3f982cf4SFabien Sanglard "Either add a file named LICENSE, " 154*3f982cf4SFabien Sanglard "import upstream's COPYING if available, " 155*3f982cf4SFabien Sanglard "or add a 'License File:' line to " 156*3f982cf4SFabien Sanglard "README.chromium with the appropriate path.") 157*3f982cf4SFabien Sanglard metadata["License File"] = license_path 158*3f982cf4SFabien Sanglard 159*3f982cf4SFabien Sanglard if errors: 160*3f982cf4SFabien Sanglard raise LicenseError("Errors in %s:\n %s\n" % 161*3f982cf4SFabien Sanglard (path, ";\n ".join(errors))) 162*3f982cf4SFabien Sanglard return metadata 163*3f982cf4SFabien Sanglard 164*3f982cf4SFabien Sanglard 165*3f982cf4SFabien Sanglarddef ContainsFiles(path, root): 166*3f982cf4SFabien Sanglard """Determines whether any files exist in a directory or in any of its 167*3f982cf4SFabien Sanglard subdirectories.""" 168*3f982cf4SFabien Sanglard for _, dirs, files in os.walk(os.path.join(root, path)): 169*3f982cf4SFabien Sanglard if files: 170*3f982cf4SFabien Sanglard return True 171*3f982cf4SFabien Sanglard for prune_dir in PRUNE_DIRS: 172*3f982cf4SFabien Sanglard if prune_dir in dirs: 173*3f982cf4SFabien Sanglard dirs.remove(prune_dir) 174*3f982cf4SFabien Sanglard return False 175*3f982cf4SFabien Sanglard 176*3f982cf4SFabien Sanglard 177*3f982cf4SFabien Sanglarddef FilterDirsWithFiles(dirs_list, root): 178*3f982cf4SFabien Sanglard # If a directory contains no files, assume it's a DEPS directory for a 179*3f982cf4SFabien Sanglard # project not used by our current configuration and skip it. 180*3f982cf4SFabien Sanglard return [x for x in dirs_list if ContainsFiles(x, root)] 181*3f982cf4SFabien Sanglard 182*3f982cf4SFabien Sanglard 183*3f982cf4SFabien Sanglarddef FindThirdPartyDirs(prune_paths, root): 184*3f982cf4SFabien Sanglard """Find all third_party directories underneath the source root.""" 185*3f982cf4SFabien Sanglard third_party_dirs = set() 186*3f982cf4SFabien Sanglard for path, dirs, files in os.walk(root): 187*3f982cf4SFabien Sanglard path = path[len(root) + 1:] # Pretty up the path. 188*3f982cf4SFabien Sanglard 189*3f982cf4SFabien Sanglard # .gitignore ignores /out*/, so do the same here. 190*3f982cf4SFabien Sanglard if path in prune_paths or path.startswith('out'): 191*3f982cf4SFabien Sanglard dirs[:] = [] 192*3f982cf4SFabien Sanglard continue 193*3f982cf4SFabien Sanglard 194*3f982cf4SFabien Sanglard # Prune out directories we want to skip. 195*3f982cf4SFabien Sanglard # (Note that we loop over PRUNE_DIRS so we're not iterating over a 196*3f982cf4SFabien Sanglard # list that we're simultaneously mutating.) 197*3f982cf4SFabien Sanglard for skip in PRUNE_DIRS: 198*3f982cf4SFabien Sanglard if skip in dirs: 199*3f982cf4SFabien Sanglard dirs.remove(skip) 200*3f982cf4SFabien Sanglard 201*3f982cf4SFabien Sanglard if os.path.basename(path) == 'third_party': 202*3f982cf4SFabien Sanglard # Add all subdirectories that are not marked for skipping. 203*3f982cf4SFabien Sanglard for dir in dirs: 204*3f982cf4SFabien Sanglard dirpath = os.path.join(path, dir) 205*3f982cf4SFabien Sanglard if dirpath not in prune_paths: 206*3f982cf4SFabien Sanglard third_party_dirs.add(dirpath) 207*3f982cf4SFabien Sanglard 208*3f982cf4SFabien Sanglard # Don't recurse into any subdirs from here. 209*3f982cf4SFabien Sanglard dirs[:] = [] 210*3f982cf4SFabien Sanglard continue 211*3f982cf4SFabien Sanglard 212*3f982cf4SFabien Sanglard return third_party_dirs 213*3f982cf4SFabien Sanglard 214*3f982cf4SFabien Sanglard 215*3f982cf4SFabien Sanglarddef FindThirdPartyDirsWithFiles(root): 216*3f982cf4SFabien Sanglard third_party_dirs = FindThirdPartyDirs(PRUNE_PATHS, root) 217*3f982cf4SFabien Sanglard return FilterDirsWithFiles(third_party_dirs, root) 218*3f982cf4SFabien Sanglard 219*3f982cf4SFabien Sanglard 220*3f982cf4SFabien Sanglard# Many builders do not contain 'gn' in their PATH, so use the GN binary from 221*3f982cf4SFabien Sanglard# //buildtools. 222*3f982cf4SFabien Sanglarddef _GnBinary(): 223*3f982cf4SFabien Sanglard exe = 'gn' 224*3f982cf4SFabien Sanglard if sys.platform.startswith('linux'): 225*3f982cf4SFabien Sanglard subdir = 'linux64' 226*3f982cf4SFabien Sanglard elif sys.platform == 'darwin': 227*3f982cf4SFabien Sanglard subdir = 'mac' 228*3f982cf4SFabien Sanglard elif sys.platform == 'win32': 229*3f982cf4SFabien Sanglard subdir, exe = 'win', 'gn.exe' 230*3f982cf4SFabien Sanglard else: 231*3f982cf4SFabien Sanglard raise RuntimeError("Unsupported platform '%s'." % sys.platform) 232*3f982cf4SFabien Sanglard 233*3f982cf4SFabien Sanglard return os.path.join(_REPOSITORY_ROOT, 'buildtools', subdir, exe) 234*3f982cf4SFabien Sanglard 235*3f982cf4SFabien Sanglard 236*3f982cf4SFabien Sanglarddef GetThirdPartyDepsFromGNDepsOutput(gn_deps, target_os): 237*3f982cf4SFabien Sanglard """Returns third_party/foo directories given the output of "gn desc deps". 238*3f982cf4SFabien Sanglard 239*3f982cf4SFabien Sanglard Note that it always returns the direct sub-directory of third_party 240*3f982cf4SFabien Sanglard where README.chromium and LICENSE files are, so that it can be passed to 241*3f982cf4SFabien Sanglard ParseDir(). e.g.: 242*3f982cf4SFabien Sanglard third_party/cld_3/src/src/BUILD.gn -> third_party/cld_3 243*3f982cf4SFabien Sanglard 244*3f982cf4SFabien Sanglard It returns relative paths from _REPOSITORY_ROOT, not absolute paths. 245*3f982cf4SFabien Sanglard """ 246*3f982cf4SFabien Sanglard third_party_deps = set() 247*3f982cf4SFabien Sanglard for absolute_build_dep in gn_deps.split(): 248*3f982cf4SFabien Sanglard relative_build_dep = os.path.relpath(absolute_build_dep, 249*3f982cf4SFabien Sanglard _REPOSITORY_ROOT) 250*3f982cf4SFabien Sanglard m = re.search( 251*3f982cf4SFabien Sanglard r'^((.+[/\\])?third_party[/\\][^/\\]+[/\\])(.+[/\\])?BUILD\.gn$', 252*3f982cf4SFabien Sanglard relative_build_dep) 253*3f982cf4SFabien Sanglard if not m: 254*3f982cf4SFabien Sanglard continue 255*3f982cf4SFabien Sanglard third_party_path = m.group(1) 256*3f982cf4SFabien Sanglard if any(third_party_path.startswith(p + os.sep) for p in PRUNE_PATHS): 257*3f982cf4SFabien Sanglard continue 258*3f982cf4SFabien Sanglard third_party_deps.add(third_party_path[:-1]) 259*3f982cf4SFabien Sanglard return third_party_deps 260*3f982cf4SFabien Sanglard 261*3f982cf4SFabien Sanglard 262*3f982cf4SFabien Sanglarddef FindThirdPartyDeps(gn_out_dir, gn_target, target_os): 263*3f982cf4SFabien Sanglard if not gn_out_dir: 264*3f982cf4SFabien Sanglard raise RuntimeError("--gn-out-dir is required if --gn-target is used.") 265*3f982cf4SFabien Sanglard 266*3f982cf4SFabien Sanglard # Generate gn project in temp directory and use it to find dependencies. 267*3f982cf4SFabien Sanglard # Current gn directory cannot be used when we run this script in a gn action 268*3f982cf4SFabien Sanglard # rule, because gn doesn't allow recursive invocations due to potential side 269*3f982cf4SFabien Sanglard # effects. 270*3f982cf4SFabien Sanglard tmp_dir = None 271*3f982cf4SFabien Sanglard try: 272*3f982cf4SFabien Sanglard tmp_dir = tempfile.mkdtemp(dir=gn_out_dir) 273*3f982cf4SFabien Sanglard shutil.copy(os.path.join(gn_out_dir, "args.gn"), tmp_dir) 274*3f982cf4SFabien Sanglard subprocess.check_output([_GnBinary(), "gen", tmp_dir]) 275*3f982cf4SFabien Sanglard gn_deps = subprocess.check_output([ 276*3f982cf4SFabien Sanglard _GnBinary(), "desc", tmp_dir, gn_target, "deps", "--as=buildfile", 277*3f982cf4SFabien Sanglard "--all" 278*3f982cf4SFabien Sanglard ]) 279*3f982cf4SFabien Sanglard if isinstance(gn_deps, bytes): 280*3f982cf4SFabien Sanglard gn_deps = gn_deps.decode("utf-8") 281*3f982cf4SFabien Sanglard finally: 282*3f982cf4SFabien Sanglard if tmp_dir and os.path.exists(tmp_dir): 283*3f982cf4SFabien Sanglard shutil.rmtree(tmp_dir) 284*3f982cf4SFabien Sanglard 285*3f982cf4SFabien Sanglard return GetThirdPartyDepsFromGNDepsOutput(gn_deps, target_os) 286*3f982cf4SFabien Sanglard 287*3f982cf4SFabien Sanglard 288*3f982cf4SFabien Sanglarddef ScanThirdPartyDirs(root=None): 289*3f982cf4SFabien Sanglard """Scan a list of directories and report on any problems we find.""" 290*3f982cf4SFabien Sanglard if root is None: 291*3f982cf4SFabien Sanglard root = os.getcwd() 292*3f982cf4SFabien Sanglard third_party_dirs = FindThirdPartyDirsWithFiles(root) 293*3f982cf4SFabien Sanglard 294*3f982cf4SFabien Sanglard errors = [] 295*3f982cf4SFabien Sanglard for path in sorted(third_party_dirs): 296*3f982cf4SFabien Sanglard try: 297*3f982cf4SFabien Sanglard metadata = ParseDir(path, root) 298*3f982cf4SFabien Sanglard except LicenseError as e: 299*3f982cf4SFabien Sanglard errors.append((path, e.args[0])) 300*3f982cf4SFabien Sanglard continue 301*3f982cf4SFabien Sanglard 302*3f982cf4SFabien Sanglard return ['{}: {}'.format(path, error) for path, error in sorted(errors)] 303*3f982cf4SFabien Sanglard 304*3f982cf4SFabien Sanglard 305*3f982cf4SFabien Sanglarddef GenerateCredits(file_template_file, 306*3f982cf4SFabien Sanglard entry_template_file, 307*3f982cf4SFabien Sanglard output_file, 308*3f982cf4SFabien Sanglard target_os, 309*3f982cf4SFabien Sanglard gn_out_dir, 310*3f982cf4SFabien Sanglard gn_target, 311*3f982cf4SFabien Sanglard depfile=None): 312*3f982cf4SFabien Sanglard """Generate about:credits.""" 313*3f982cf4SFabien Sanglard 314*3f982cf4SFabien Sanglard def EvaluateTemplate(template, env, escape=True): 315*3f982cf4SFabien Sanglard """Expand a template with variables like {{foo}} using a 316*3f982cf4SFabien Sanglard dictionary of expansions.""" 317*3f982cf4SFabien Sanglard for key, val in env.items(): 318*3f982cf4SFabien Sanglard if escape: 319*3f982cf4SFabien Sanglard val = escape(val) 320*3f982cf4SFabien Sanglard template = template.replace('{{%s}}' % key, val) 321*3f982cf4SFabien Sanglard return template 322*3f982cf4SFabien Sanglard 323*3f982cf4SFabien Sanglard def MetadataToTemplateEntry(metadata, entry_template): 324*3f982cf4SFabien Sanglard env = { 325*3f982cf4SFabien Sanglard 'name': metadata['Name'], 326*3f982cf4SFabien Sanglard 'url': metadata['URL'], 327*3f982cf4SFabien Sanglard 'license': open(metadata['License File']).read(), 328*3f982cf4SFabien Sanglard } 329*3f982cf4SFabien Sanglard return { 330*3f982cf4SFabien Sanglard 'name': metadata['Name'], 331*3f982cf4SFabien Sanglard 'content': EvaluateTemplate(entry_template, env), 332*3f982cf4SFabien Sanglard 'license_file': metadata['License File'], 333*3f982cf4SFabien Sanglard } 334*3f982cf4SFabien Sanglard 335*3f982cf4SFabien Sanglard if gn_target: 336*3f982cf4SFabien Sanglard third_party_dirs = FindThirdPartyDeps(gn_out_dir, gn_target, target_os) 337*3f982cf4SFabien Sanglard 338*3f982cf4SFabien Sanglard # Sanity-check to raise a build error if invalid gn_... settings are 339*3f982cf4SFabien Sanglard # somehow passed to this script. 340*3f982cf4SFabien Sanglard if not third_party_dirs: 341*3f982cf4SFabien Sanglard raise RuntimeError("No deps found.") 342*3f982cf4SFabien Sanglard else: 343*3f982cf4SFabien Sanglard third_party_dirs = FindThirdPartyDirs(PRUNE_PATHS, _REPOSITORY_ROOT) 344*3f982cf4SFabien Sanglard 345*3f982cf4SFabien Sanglard if not file_template_file: 346*3f982cf4SFabien Sanglard file_template_file = os.path.join(_REPOSITORY_ROOT, 'components', 347*3f982cf4SFabien Sanglard 'about_ui', 'resources', 348*3f982cf4SFabien Sanglard 'about_credits.tmpl') 349*3f982cf4SFabien Sanglard if not entry_template_file: 350*3f982cf4SFabien Sanglard entry_template_file = os.path.join(_REPOSITORY_ROOT, 'components', 351*3f982cf4SFabien Sanglard 'about_ui', 'resources', 352*3f982cf4SFabien Sanglard 'about_credits_entry.tmpl') 353*3f982cf4SFabien Sanglard 354*3f982cf4SFabien Sanglard entry_template = open(entry_template_file).read() 355*3f982cf4SFabien Sanglard entries = [] 356*3f982cf4SFabien Sanglard # Start from Chromium's LICENSE file 357*3f982cf4SFabien Sanglard chromium_license_metadata = { 358*3f982cf4SFabien Sanglard 'Name': 'The Chromium Project', 359*3f982cf4SFabien Sanglard 'URL': 'http://www.chromium.org', 360*3f982cf4SFabien Sanglard 'License File': os.path.join(_REPOSITORY_ROOT, 'LICENSE') 361*3f982cf4SFabien Sanglard } 362*3f982cf4SFabien Sanglard entries.append( 363*3f982cf4SFabien Sanglard MetadataToTemplateEntry(chromium_license_metadata, entry_template)) 364*3f982cf4SFabien Sanglard 365*3f982cf4SFabien Sanglard entries_by_name = {} 366*3f982cf4SFabien Sanglard for path in third_party_dirs: 367*3f982cf4SFabien Sanglard try: 368*3f982cf4SFabien Sanglard metadata = ParseDir(path, _REPOSITORY_ROOT) 369*3f982cf4SFabien Sanglard except LicenseError: 370*3f982cf4SFabien Sanglard # TODO(phajdan.jr): Convert to fatal error (http://crbug.com/39240). 371*3f982cf4SFabien Sanglard continue 372*3f982cf4SFabien Sanglard if metadata['License File'] == NOT_SHIPPED: 373*3f982cf4SFabien Sanglard continue 374*3f982cf4SFabien Sanglard 375*3f982cf4SFabien Sanglard new_entry = MetadataToTemplateEntry(metadata, entry_template) 376*3f982cf4SFabien Sanglard # Skip entries that we've already seen. 377*3f982cf4SFabien Sanglard prev_entry = entries_by_name.setdefault(new_entry['name'], new_entry) 378*3f982cf4SFabien Sanglard if prev_entry is not new_entry and ( 379*3f982cf4SFabien Sanglard prev_entry['content'] == new_entry['content']): 380*3f982cf4SFabien Sanglard continue 381*3f982cf4SFabien Sanglard 382*3f982cf4SFabien Sanglard entries.append(new_entry) 383*3f982cf4SFabien Sanglard 384*3f982cf4SFabien Sanglard entries.sort(key=lambda entry: (entry['name'].lower(), entry['content'])) 385*3f982cf4SFabien Sanglard for entry_id, entry in enumerate(entries): 386*3f982cf4SFabien Sanglard entry['content'] = entry['content'].replace('{{id}}', str(entry_id)) 387*3f982cf4SFabien Sanglard 388*3f982cf4SFabien Sanglard entries_contents = '\n'.join([entry['content'] for entry in entries]) 389*3f982cf4SFabien Sanglard file_template = open(file_template_file).read() 390*3f982cf4SFabien Sanglard template_contents = "<!-- Generated by licenses.py; do not edit. -->" 391*3f982cf4SFabien Sanglard template_contents += EvaluateTemplate(file_template, 392*3f982cf4SFabien Sanglard {'entries': entries_contents}, 393*3f982cf4SFabien Sanglard escape=False) 394*3f982cf4SFabien Sanglard 395*3f982cf4SFabien Sanglard if output_file: 396*3f982cf4SFabien Sanglard changed = True 397*3f982cf4SFabien Sanglard try: 398*3f982cf4SFabien Sanglard old_output = open(output_file, 'r').read() 399*3f982cf4SFabien Sanglard if old_output == template_contents: 400*3f982cf4SFabien Sanglard changed = False 401*3f982cf4SFabien Sanglard except: 402*3f982cf4SFabien Sanglard pass 403*3f982cf4SFabien Sanglard if changed: 404*3f982cf4SFabien Sanglard with open(output_file, 'w') as output: 405*3f982cf4SFabien Sanglard output.write(template_contents) 406*3f982cf4SFabien Sanglard else: 407*3f982cf4SFabien Sanglard print(template_contents) 408*3f982cf4SFabien Sanglard 409*3f982cf4SFabien Sanglard if depfile: 410*3f982cf4SFabien Sanglard assert output_file 411*3f982cf4SFabien Sanglard # Add in build.ninja so that the target will be considered dirty when 412*3f982cf4SFabien Sanglard # gn gen is run. Otherwise, it will fail to notice new files being 413*3f982cf4SFabien Sanglard # added. This is still not perfect, as it will fail if no build files 414*3f982cf4SFabien Sanglard # are changed, but a new README.chromium / LICENSE is added. This 415*3f982cf4SFabien Sanglard # shouldn't happen in practice however. 416*3f982cf4SFabien Sanglard license_file_list = (entry['license_file'] for entry in entries) 417*3f982cf4SFabien Sanglard license_file_list = (os.path.relpath(p) for p in license_file_list) 418*3f982cf4SFabien Sanglard license_file_list = sorted(set(license_file_list)) 419*3f982cf4SFabien Sanglard WriteDepfile(depfile, output_file, license_file_list + ['build.ninja']) 420*3f982cf4SFabien Sanglard 421*3f982cf4SFabien Sanglard return True 422*3f982cf4SFabien Sanglard 423*3f982cf4SFabien Sanglard 424*3f982cf4SFabien Sanglarddef _ReadFile(path): 425*3f982cf4SFabien Sanglard """Reads a file from disk. 426*3f982cf4SFabien Sanglard Args: 427*3f982cf4SFabien Sanglard path: The path of the file to read, relative to the root of the 428*3f982cf4SFabien Sanglard repository. 429*3f982cf4SFabien Sanglard Returns: 430*3f982cf4SFabien Sanglard The contents of the file as a string. 431*3f982cf4SFabien Sanglard """ 432*3f982cf4SFabien Sanglard with codecs.open(os.path.join(_REPOSITORY_ROOT, path), 'r', 'utf-8') as f: 433*3f982cf4SFabien Sanglard return f.read() 434*3f982cf4SFabien Sanglard 435*3f982cf4SFabien Sanglard 436*3f982cf4SFabien Sanglarddef GenerateLicenseFile(output_file, gn_out_dir, gn_target, target_os): 437*3f982cf4SFabien Sanglard """Generate a plain-text LICENSE file which can be used when you ship a part 438*3f982cf4SFabien Sanglard of Chromium code (specified by gn_target) as a stand-alone library 439*3f982cf4SFabien Sanglard (e.g., //ios/web_view). 440*3f982cf4SFabien Sanglard 441*3f982cf4SFabien Sanglard The LICENSE file contains licenses of both Chromium and third-party 442*3f982cf4SFabien Sanglard libraries which gn_target depends on. """ 443*3f982cf4SFabien Sanglard 444*3f982cf4SFabien Sanglard third_party_dirs = FindThirdPartyDeps(gn_out_dir, gn_target, target_os) 445*3f982cf4SFabien Sanglard 446*3f982cf4SFabien Sanglard # Start with Chromium's LICENSE file. 447*3f982cf4SFabien Sanglard content = [_ReadFile('LICENSE')] 448*3f982cf4SFabien Sanglard 449*3f982cf4SFabien Sanglard # Add necessary third_party. 450*3f982cf4SFabien Sanglard for directory in sorted(third_party_dirs): 451*3f982cf4SFabien Sanglard metadata = ParseDir(directory, 452*3f982cf4SFabien Sanglard _REPOSITORY_ROOT, 453*3f982cf4SFabien Sanglard require_license_file=True) 454*3f982cf4SFabien Sanglard license_file = metadata['License File'] 455*3f982cf4SFabien Sanglard if license_file and license_file != NOT_SHIPPED: 456*3f982cf4SFabien Sanglard content.append('-' * 20) 457*3f982cf4SFabien Sanglard content.append(directory.split(os.sep)[-1]) 458*3f982cf4SFabien Sanglard content.append('-' * 20) 459*3f982cf4SFabien Sanglard content.append(_ReadFile(license_file)) 460*3f982cf4SFabien Sanglard 461*3f982cf4SFabien Sanglard content_text = '\n'.join(content) 462*3f982cf4SFabien Sanglard 463*3f982cf4SFabien Sanglard if output_file: 464*3f982cf4SFabien Sanglard with codecs.open(output_file, 'w', 'utf-8') as output: 465*3f982cf4SFabien Sanglard output.write(content_text) 466*3f982cf4SFabien Sanglard else: 467*3f982cf4SFabien Sanglard print(content_text) 468*3f982cf4SFabien Sanglard 469*3f982cf4SFabien Sanglard 470*3f982cf4SFabien Sanglarddef main(): 471*3f982cf4SFabien Sanglard parser = argparse.ArgumentParser() 472*3f982cf4SFabien Sanglard parser.add_argument('--file-template', 473*3f982cf4SFabien Sanglard help='Template HTML to use for the license page.') 474*3f982cf4SFabien Sanglard parser.add_argument('--entry-template', 475*3f982cf4SFabien Sanglard help='Template HTML to use for each license.') 476*3f982cf4SFabien Sanglard parser.add_argument('--target-os', help='OS that this build is targeting.') 477*3f982cf4SFabien Sanglard parser.add_argument('--gn-out-dir', 478*3f982cf4SFabien Sanglard help='GN output directory for scanning dependencies.') 479*3f982cf4SFabien Sanglard parser.add_argument('--gn-target', 480*3f982cf4SFabien Sanglard help='GN target to scan for dependencies.') 481*3f982cf4SFabien Sanglard parser.add_argument('command', 482*3f982cf4SFabien Sanglard choices=['help', 'scan', 'credits', 'license_file']) 483*3f982cf4SFabien Sanglard parser.add_argument('output_file', nargs='?') 484*3f982cf4SFabien Sanglard parser.add_argument('--depfile', 485*3f982cf4SFabien Sanglard help='Path to depfile (refer to `gn help depfile`)') 486*3f982cf4SFabien Sanglard args = parser.parse_args() 487*3f982cf4SFabien Sanglard 488*3f982cf4SFabien Sanglard if args.command == 'scan': 489*3f982cf4SFabien Sanglard if not ScanThirdPartyDirs(): 490*3f982cf4SFabien Sanglard return 1 491*3f982cf4SFabien Sanglard elif args.command == 'credits': 492*3f982cf4SFabien Sanglard if not GenerateCredits(args.file_template, args.entry_template, 493*3f982cf4SFabien Sanglard args.output_file, args.target_os, 494*3f982cf4SFabien Sanglard args.gn_out_dir, args.gn_target, args.depfile): 495*3f982cf4SFabien Sanglard return 1 496*3f982cf4SFabien Sanglard elif args.command == 'license_file': 497*3f982cf4SFabien Sanglard try: 498*3f982cf4SFabien Sanglard GenerateLicenseFile(args.output_file, args.gn_out_dir, 499*3f982cf4SFabien Sanglard args.gn_target, args.target_os) 500*3f982cf4SFabien Sanglard except LicenseError as e: 501*3f982cf4SFabien Sanglard print("Failed to parse README.chromium: {}".format(e)) 502*3f982cf4SFabien Sanglard return 1 503*3f982cf4SFabien Sanglard else: 504*3f982cf4SFabien Sanglard print(__doc__) 505*3f982cf4SFabien Sanglard return 1 506*3f982cf4SFabien Sanglard 507*3f982cf4SFabien Sanglard 508*3f982cf4SFabien Sanglardif __name__ == '__main__': 509*3f982cf4SFabien Sanglard sys.exit(main()) 510