xref: /aosp_15_r20/external/openscreen/tools/licenses.py (revision 3f982cf4871df8771c9d4abe6e9a6f8d829b2736)
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