Add LICENSE file checking

This patch adds license file checking in Open Screen, by adding a new
licenses.py that iterates through third_party dependencies and returns
a list of licensing errors. This runs as part of PRESUBMIT.

This patch also includes README.chromium and LICENSE fixes for
third_party dependencies.

Finally, as part of refactoring I fixed the DEPS checker--before this
patch it fails to load modules so never runs on pre-submission.

Bug: b/173625891

Change-Id: I417e61f878dab809cf959d69480be749f9229128
Reviewed-on: https://chromium-review.googlesource.com/c/openscreen/+/2547807
Reviewed-by: Jordan Bayles <jophba@chromium.org>
Reviewed-by: mark a. foltz <mfoltz@chromium.org>
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index dafbc5e..07d567c 100755
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -2,7 +2,21 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import os
 import re
+import sys
+
+_REPO_PATH = os.path.dirname(os.path.realpath('__file__'))
+_IMPORT_SUBFOLDERS = ['tools', os.path.join('buildtools', 'checkdeps')]
+
+# git-cl upload is not compatible with __init__.py based subfolder imports, so
+# we extend the system path instead.
+sys.path.extend(os.path.join(_REPO_PATH, p) for p in _IMPORT_SUBFOLDERS)
+
+import licenses
+from checkdeps import DepsChecker
+from cpp_checker import CppChecker
+from rules import Rule
 
 # Rather than pass this to all of the checks, we override the global excluded
 # list with this one.
@@ -22,25 +36,19 @@
 )
 
 
-def _CheckDeps(input_api, output_api):
-  results = []
-  import sys
-  original_sys_path = sys.path
-  try:
-    sys.path = sys.path + [input_api.os_path.join(
-        input_api.PresubmitLocalPath(), 'buildtools', 'checkdeps')]
-    import checkdeps
-    from cpp_checker import CppChecker
-    from rules import Rule
-  finally:
-    sys.path = original_sys_path
+def _CheckLicenses(input_api, output_api):
+    """Checks third party licenses and returns a list of violations."""
+    return [
+        output_api.PresubmitError(v) for v in licenses.ScanThirdPartyDirs()
+    ]
 
-  deps_checker = checkdeps.DepsChecker(input_api.PresubmitLocalPath())
-  deps_checker.CheckDirectory(input_api.PresubmitLocalPath())
-  deps_results = deps_checker.results_formatter.GetResults()
-  for violation in deps_results:
-    results.append(output_api.PresubmitError(violation))
-  return results
+
+def _CheckDeps(input_api, output_api):
+    """Checks DEPS rules and returns a list of violations."""
+    deps_checker = DepsChecker(input_api.PresubmitLocalPath())
+    deps_checker.CheckDirectory(input_api.PresubmitLocalPath())
+    deps_results = deps_checker.results_formatter.GetResults()
+    return [output_api.PresubmitError(v) for v in deps_results]
 
 
 # Matches Foo(Foo&&) when not followed by noexcept.
@@ -49,7 +57,7 @@
 
 
 def _CheckNoexceptOnMove(filename, clean_lines, linenum, error):
-  """Checks that move constructors are declared with 'noexcept'.
+    """Checks that move constructors are declared with 'noexcept'.
 
   Args:
     filename: The name of the current file.
@@ -57,17 +65,19 @@
     linenum: The number of the line to check.
     error: The function to call with any errors found.
   """
-  # We only check headers as noexcept is meaningful on declarations, not
-  # definitions.  This may skip some definitions in .cc files though.
-  if not filename.endswith('.h'):
-    return
+    # We only check headers as noexcept is meaningful on declarations, not
+    # definitions.  This may skip some definitions in .cc files though.
+    if not filename.endswith('.h'):
+        return
 
-  line = clean_lines.elided[linenum]
-  matched = _RE_PATTERN_MOVE_WITHOUT_NOEXCEPT.match(line)
-  if matched:
-    error(filename, linenum, 'runtime/noexcept', 4,
-          'Move constructor of %s not declared \'noexcept\' in %s' %
-          (matched.group('classname'), matched.group(0).strip()))
+    line = clean_lines.elided[linenum]
+    matched = _RE_PATTERN_MOVE_WITHOUT_NOEXCEPT.match(line)
+    if matched:
+        error(
+            filename, linenum, 'runtime/noexcept', 4,
+            'Move constructor of {} not declared \'noexcept\' in {}'.format(
+                matched.group('classname'),
+                matched.group(0).strip()))
 
 # - We disable c++11 header checks since Open Screen allows them.
 # - We disable whitespace/braces because of various false positives.
@@ -75,83 +85,89 @@
 #   enough to keep.
 # - We add a custom check for 'noexcept' usage.
 def _CheckChangeLintsClean(input_api, output_api):
-  """Checks that all '.cc' and '.h' files pass cpplint.py."""
-  result = []
+    """Checks that all '.cc' and '.h' files pass cpplint.py."""
+    cpplint = input_api.cpplint
+    # Access to a protected member _XX of a client class
+    # pylint: disable=protected-access
+    cpplint._cpplint_state.ResetErrorCounts()
 
-  cpplint = input_api.cpplint
-  # Access to a protected member _XX of a client class
-  # pylint: disable=protected-access
-  cpplint._cpplint_state.ResetErrorCounts()
+    cpplint._SetFilters('-build/c++11,-whitespace/braces')
+    files = [
+        f.AbsoluteLocalPath() for f in input_api.AffectedSourceFiles(None)
+    ]
+    CPPLINT_VERBOSE_LEVEL = 4
+    for file_name in files:
+        cpplint.ProcessFile(file_name, CPPLINT_VERBOSE_LEVEL,
+                            [_CheckNoexceptOnMove])
 
-  cpplint._SetFilters('-build/c++11,-whitespace/braces')
-  files = [f.AbsoluteLocalPath() for f in input_api.AffectedSourceFiles(None)]
-  for file_name in files:
-    # 4 = verbose_level
-    cpplint.ProcessFile(file_name, 4, [_CheckNoexceptOnMove])
+    if cpplint._cpplint_state.error_count:
+        if input_api.is_committing:
+            res_type = output_api.PresubmitError
+        else:
+            res_type = output_api.PresubmitPromptWarning
+        return [res_type('Changelist failed cpplint.py check.')]
 
-  if cpplint._cpplint_state.error_count > 0:
-    if input_api.is_committing:
-      res_type = output_api.PresubmitError
-    else:
-      res_type = output_api.PresubmitPromptWarning
-    result = [res_type('Changelist failed cpplint.py check.')]
-
-  return result
+    return []
 
 
 def _CommonChecks(input_api, output_api):
-  results = []
-  # PanProjectChecks include:
-  #   CheckLongLines (@ 80 cols)
-  #   CheckChangeHasNoTabs
-  #   CheckChangeHasNoStrayWhitespace
-  #   CheckLicense
-  #   CheckChangeWasUploaded (if committing)
-  #   CheckChangeHasDescription
-  #   CheckDoNotSubmitInDescription
-  #   CheckDoNotSubmitInFiles
-  results.extend(input_api.canned_checks.PanProjectChecks(
-    input_api, output_api, owners_check=False));
+    # PanProjectChecks include:
+    #   CheckLongLines (@ 80 cols)
+    #   CheckChangeHasNoTabs
+    #   CheckChangeHasNoStrayWhitespace
+    #   CheckLicense
+    #   CheckChangeWasUploaded (if committing)
+    #   CheckChangeHasDescription
+    #   CheckDoNotSubmitInDescription
+    #   CheckDoNotSubmitInFiles
+    results = input_api.canned_checks.PanProjectChecks(input_api,
+                                                       output_api,
+                                                       owners_check=False)
 
-  # No carriage return characters, files end with one EOL (\n).
-  results.extend(input_api.canned_checks.CheckChangeHasNoCrAndHasOnlyOneEol(
-    input_api, output_api));
+    # No carriage return characters, files end with one EOL (\n).
+    results.extend(
+        input_api.canned_checks.CheckChangeHasNoCrAndHasOnlyOneEol(
+            input_api, output_api))
 
-  # Gender inclusivity
-  results.extend(input_api.canned_checks.CheckGenderNeutral(
-    input_api, output_api))
+    # Gender inclusivity
+    results.extend(
+        input_api.canned_checks.CheckGenderNeutral(input_api, output_api))
 
-  # TODO(bug) format required
-  results.extend(input_api.canned_checks.CheckChangeTodoHasOwner(
-    input_api, output_api))
+    # TODO(bug) format required
+    results.extend(
+        input_api.canned_checks.CheckChangeTodoHasOwner(input_api, output_api))
 
-  # Linter.
-  results.extend(_CheckChangeLintsClean(input_api, output_api))
+    # Linter.
+    results.extend(_CheckChangeLintsClean(input_api, output_api))
 
-  # clang-format
-  results.extend(input_api.canned_checks.CheckPatchFormatted(
-    input_api, output_api, bypass_warnings=False))
+    # clang-format
+    results.extend(
+        input_api.canned_checks.CheckPatchFormatted(input_api,
+                                                    output_api,
+                                                    bypass_warnings=False))
 
-  # GN formatting
-  results.extend(input_api.canned_checks.CheckGNFormatted(
-    input_api, output_api))
+    # GN formatting
+    results.extend(
+        input_api.canned_checks.CheckGNFormatted(input_api, output_api))
 
-  # buildtools/checkdeps
-  results.extend(_CheckDeps(input_api, output_api))
-  return results
+    # buildtools/checkdeps
+    results.extend(_CheckDeps(input_api, output_api))
+
+    # tools/licenses
+    results.extend(_CheckLicenses(input_api, output_api))
+
+    return results
 
 
 def CheckChangeOnUpload(input_api, output_api):
-  input_api.DEFAULT_FILES_TO_SKIP = _EXCLUDED_PATHS;
-  results = []
-  results.extend(_CommonChecks(input_api, output_api))
-  results.extend(
-      input_api.canned_checks.CheckChangedLUCIConfigs(input_api, output_api))
-  return results
+    input_api.DEFAULT_FILES_TO_SKIP = _EXCLUDED_PATHS
+    # We always run the OnCommit checks, as well as some additional checks.
+    results = CheckChangeOnCommit(input_api, output_api)
+    results.extend(
+        input_api.canned_checks.CheckChangedLUCIConfigs(input_api, output_api))
+    return results
 
 
 def CheckChangeOnCommit(input_api, output_api):
-  input_api.DEFAULT_FILES_TO_SKIP = _EXCLUDED_PATHS;
-  results = []
-  results.extend(_CommonChecks(input_api, output_api))
-  return results
+    input_api.DEFAULT_FILES_TO_SKIP = _EXCLUDED_PATHS
+    return _CommonChecks(input_api, output_api)
diff --git a/third_party/abseil/README.chromium b/third_party/abseil/README.chromium
new file mode 100644
index 0000000..fb55ac0
--- /dev/null
+++ b/third_party/abseil/README.chromium
@@ -0,0 +1,13 @@
+Name: Abseil
+Short Name: absl
+URL: https://github.com/abseil/abseil-cpp
+License: Apache 2.0
+License File: src/LICENSE
+Version: 0
+Security Critical: yes
+
+Description:
+This directory contains the source code of Abseil for C++. Open Screen primarily
+uses this library to essentially backport C++17 and C++20 features. The set of
+files included from Abseil has been selected judiciously, and care should be
+taken when adding dependencies on new Abseil files.
diff --git a/third_party/boringssl/README.chromium b/third_party/boringssl/README.chromium
new file mode 100644
index 0000000..adecfa8
--- /dev/null
+++ b/third_party/boringssl/README.chromium
@@ -0,0 +1,11 @@
+Name: BoringSSL
+URL: https://boringssl.googlesource.com/boringssl
+Version: git
+License: BSDish
+License File: src/LICENSE
+License Android Compatible: yes
+Security Critical: yes
+
+Description:
+This is BoringSSL, a fork of OpenSSL. See
+https://www.imperialviolet.org/2014/06/20/boringssl.html
diff --git a/third_party/chromium_quic/README.chromium b/third_party/chromium_quic/README.chromium
new file mode 100644
index 0000000..2570cec
--- /dev/null
+++ b/third_party/chromium_quic/README.chromium
@@ -0,0 +1,13 @@
+Name: Chromium QUIC
+Short Name: chromium_quic
+URL: https://chromium.googlesource.com/openscreen/quic.git
+License: ISC
+License File: src/LICENSE
+Version: 0
+Security Critical: yes
+
+Description:
+This directory contains the source code of Abseil for C++. Open Screen primarily
+uses this library to essentially backport C++17 and C++20 features. The set of
+files included from Abseil has been selected judiciously, and care should be
+taken when adding dependencies on new Abseil files.
diff --git a/third_party/jsoncpp/README.chromium b/third_party/jsoncpp/README.chromium
new file mode 100644
index 0000000..3acbb5b
--- /dev/null
+++ b/third_party/jsoncpp/README.chromium
@@ -0,0 +1,12 @@
+Name: jsoncpp
+URL: https://github.com/open-source-parsers/jsoncpp
+Version: 1.9.4
+License: MIT
+License File: src/LICENSE
+Security Critical: yes
+
+Description:
+JsonCpp is used for parsing and generating JSON data. This
+project is mirrored for Chrome from the public GitHub project, with a custom BUILD.gn
+to allow for building with our Ninja + GN configuration. The main project uses
+Meson or CMake for building.
\ No newline at end of file
diff --git a/third_party/libfuzzer/LICENSE.txt b/third_party/libfuzzer/LICENSE.txt
new file mode 100644
index 0000000..0fcf3f8
--- /dev/null
+++ b/third_party/libfuzzer/LICENSE.txt
@@ -0,0 +1,70 @@
+==============================================================================
+LLVM Release License
+==============================================================================
+University of Illinois/NCSA
+Open Source License
+
+Copyright (c) 2003-2015 University of Illinois at Urbana-Champaign.
+All rights reserved.
+
+Developed by:
+
+    LLVM Team
+
+    University of Illinois at Urbana-Champaign
+
+    http://llvm.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal with
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimers.
+
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimers in the
+      documentation and/or other materials provided with the distribution.
+
+    * Neither the names of the LLVM Team, University of Illinois at
+      Urbana-Champaign, nor the names of its contributors may be used to
+      endorse or promote products derived from this Software without specific
+      prior written permission.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
+SOFTWARE.
+
+==============================================================================
+Copyrights and Licenses for Third Party Software Distributed with LLVM:
+==============================================================================
+The LLVM software contains code written by third parties.  Such software will
+have its own individual LICENSE.TXT file in the directory in which it appears.
+This file will describe the copyrights, license, and restrictions which apply
+to that code.
+
+The disclaimer of warranty in the University of Illinois Open Source License
+applies to all code in the LLVM Distribution, and nothing in any of the
+other licenses gives permission to use the names of the LLVM Team or the
+University of Illinois to endorse or promote products derived from this
+Software.
+
+The following pieces of software have additional or alternate copyrights,
+licenses, and/or restrictions:
+
+Program             Directory
+-------             ---------
+Autoconf            llvm/autoconf
+                    llvm/projects/ModuleMaker/autoconf
+Google Test         llvm/utils/unittest/googletest
+OpenBSD regex       llvm/lib/Support/{reg*, COPYRIGHT.regex}
+pyyaml tests        llvm/test/YAMLParser/{*.data, LICENSE.TXT}
+ARM contributions   llvm/lib/Target/ARM/LICENSE.TXT
+md5 contributions   llvm/lib/Support/MD5.cpp llvm/include/llvm/Support/MD5.h
\ No newline at end of file
diff --git a/third_party/libfuzzer/README.chromium b/third_party/libfuzzer/README.chromium
new file mode 100644
index 0000000..3cb06fe
--- /dev/null
+++ b/third_party/libfuzzer/README.chromium
@@ -0,0 +1,9 @@
+Name: libFuzzer
+URL: http://llvm.org/docs/LibFuzzer.html
+License: University of Illinois/NCSA Open Source
+License File: LICENSE.txt
+Security Critical: no
+
+Description:
+Library for in-process coverage-guided fuzz testing (fuzzing) of other
+libraries.
diff --git a/third_party/libprotobuf-mutator/README.chromium b/third_party/libprotobuf-mutator/README.chromium
new file mode 100644
index 0000000..7a3fccc
--- /dev/null
+++ b/third_party/libprotobuf-mutator/README.chromium
@@ -0,0 +1,10 @@
+Name: libprotobuf-mutator
+URL: https://github.com/google/libprotobuf-mutator
+Version: 0
+License: Apache 2.0
+License File: src/LICENSE
+Security Critical: no
+
+Description:
+Library for protocol buffer mutation. Assistance library to in-process
+coverage-guided fuzz testing (fuzzing).
diff --git a/third_party/mDNSResponder/README.chromium b/third_party/mDNSResponder/README.chromium
new file mode 100644
index 0000000..7746918
--- /dev/null
+++ b/third_party/mDNSResponder/README.chromium
@@ -0,0 +1,10 @@
+Name: mDNSResponder
+URL: https://github.com/jevinskie/mDNSResponder
+License: Apache License, Version 2.0
+License File: src/LICENSE
+Security Critical: no
+
+Description:
+
+Pull from Apple Bonjour's MDNS/DNS-SD implementation. Will eventually be
+replaced with our custom implementation, currently only used in osp.
diff --git a/third_party/mozilla/README.chromium b/third_party/mozilla/README.chromium
new file mode 100644
index 0000000..cb0eead
--- /dev/null
+++ b/third_party/mozilla/README.chromium
@@ -0,0 +1,7 @@
+Name: url_parse
+URL: http://mxr.mozilla.org/comm-central/source/mozilla/netwerk/base/src/nsURLParsers.cpp
+Security Critical: yes
+License: BSD and MPL 1.1/GPL 2.0/LGPL 2.1
+License File: LICENSE.txt
+Description:
+The file url_parse.cc is based on nsURLParsers.cc from Mozilla.
diff --git a/third_party/protobuf/README.chromium b/third_party/protobuf/README.chromium
new file mode 100644
index 0000000..3ee3064
--- /dev/null
+++ b/third_party/protobuf/README.chromium
@@ -0,0 +1,12 @@
+Name: Protocol Buffers
+Short Name: protobuf
+URL: https://github.com/google/protobuf
+License: BSD
+License File: src/LICENSE
+Security Critical: yes
+
+Description:
+
+Protocol buffers are Google's language-neutral, platform-neutral, extensible
+mechanism for serializing structured data. Open Screen uses protobufs primary
+for message serialization.
\ No newline at end of file
diff --git a/third_party/tinycbor/README.chromium b/third_party/tinycbor/README.chromium
new file mode 100644
index 0000000..1532f3f
--- /dev/null
+++ b/third_party/tinycbor/README.chromium
@@ -0,0 +1,10 @@
+Name: Tiny CBOR
+Short Name: tinycbor
+URL: https://chromium.googlesource.com/external/github.com/intel/tinycbor.git
+License: MIT
+License File: src/LICENSE
+Security Critical: yes
+
+Description:
+This directory contains the source code of TinyCGBOR, which is a binary object
+representation library.
diff --git a/third_party/zlib/LICENSE b/third_party/zlib/LICENSE
new file mode 100644
index 0000000..91dc1c4
--- /dev/null
+++ b/third_party/zlib/LICENSE
@@ -0,0 +1,25 @@
+/* zlib.h -- interface of the 'zlib' general purpose compression library
+  version 1.2.11, January 15th, 2017
+
+  Copyright (C) 1995-2017 Jean-loup Gailly and Mark Adler
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+
+  Jean-loup Gailly        Mark Adler
+  jloup@gzip.org          madler@alumni.caltech.edu
+
+*/
diff --git a/third_party/zlib/README.chromium b/third_party/zlib/README.chromium
new file mode 100644
index 0000000..3082682
--- /dev/null
+++ b/third_party/zlib/README.chromium
@@ -0,0 +1,15 @@
+Name: zlib
+Short Name: zlib
+URL: http://zlib.net/
+Security Critical: yes
+License: Custom license
+License File: LICENSE
+License Android Compatible: yes
+
+Description:
+"A massively spiffy yet delicately unobtrusive compression library."
+
+zlib is a free, general-purpose, legally unencumbered lossless data-compression
+library. zlib implements the "deflate" compression algorithm described by RFC
+1951, which combines the LZ77 (Lempel-Ziv) algorithm with Huffman coding. zlib
+also implements the zlib (RFC 1950) and gzip (RFC 1952) wrapper formats.
diff --git a/tools/licenses.py b/tools/licenses.py
new file mode 100755
index 0000000..9873455
--- /dev/null
+++ b/tools/licenses.py
@@ -0,0 +1,509 @@
+#!/usr/bin/env python3
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Utility for checking and processing licensing information in third_party
+directories. Copied from Chrome's tools/licenses.py.
+
+Usage: licenses.py <command>
+
+Commands:
+  scan     scan third_party directories, verifying that we have licensing info
+  credits  generate about:credits on stdout
+
+(You can also import this as a module.)
+"""
+from __future__ import print_function
+
+import argparse
+import codecs
+import json
+import os
+import shutil
+import re
+import subprocess
+import sys
+import tempfile
+
+# TODO(issuetracker.google.com/173766869): Remove Python2 checks/compatibility.
+if sys.version_info.major == 2:
+    from cgi import escape
+else:
+    from html import escape
+
+_REPOSITORY_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
+
+# Paths from the root of the tree to directories to skip.
+PRUNE_PATHS = set([
+    # Used for development and test, not in the shipping product.
+    os.path.join('third_party', 'llvm-build'),
+])
+
+# Directories we don't scan through.
+PRUNE_DIRS = ('.git')
+
+# Directories where we check out directly from upstream, and therefore
+# can't provide a README.chromium.  Please prefer a README.chromium
+# wherever possible.
+SPECIAL_CASES = {
+    os.path.join('third_party', 'googletest'): {
+        "Name": "gtest",
+        "URL": "http://code.google.com/p/googletest",
+        "License": "BSD",
+        "License File": "NOT_SHIPPED",
+    }
+}
+
+# Special value for 'License File' field used to indicate that the license file
+# should not be used in about:credits.
+NOT_SHIPPED = "NOT_SHIPPED"
+
+
+def MakeDirectory(dir_path):
+    try:
+        os.makedirs(dir_path)
+    except OSError:
+        pass
+
+
+def WriteDepfile(depfile_path, first_gn_output, inputs=None):
+    assert depfile_path != first_gn_output  # http://crbug.com/646165
+    assert not isinstance(inputs, string_types)  # Easy mistake to make
+    inputs = inputs or []
+    MakeDirectory(os.path.dirname(depfile_path))
+    # Ninja does not support multiple outputs in depfiles.
+    with open(depfile_path, 'w') as depfile:
+        depfile.write(first_gn_output.replace(' ', '\\ '))
+        depfile.write(': ')
+        depfile.write(' '.join(i.replace(' ', '\\ ') for i in inputs))
+        depfile.write('\n')
+
+
+class LicenseError(Exception):
+    """We raise this exception when a directory's licensing info isn't
+    fully filled out."""
+    pass
+
+
+def AbsolutePath(path, filename, root):
+    """Convert a path in README.chromium to be absolute based on the source
+    root."""
+    if filename.startswith('/'):
+        # Absolute-looking paths are relative to the source root
+        # (which is the directory we're run from).
+        absolute_path = os.path.join(root, filename[1:])
+    else:
+        absolute_path = os.path.join(root, path, filename)
+    if os.path.exists(absolute_path):
+        return absolute_path
+    return None
+
+
+def ParseDir(path, root, require_license_file=True, optional_keys=None):
+    """Examine a third_party/foo component and extract its metadata."""
+    # Parse metadata fields out of README.chromium.
+    # We examine "LICENSE" for the license file by default.
+    metadata = {
+        "License File": "LICENSE",  # Relative path to license text.
+        "Name": None,  # Short name (for header on about:credits).
+        "URL": None,  # Project home page.
+        "License": None,  # Software license.
+    }
+
+    if optional_keys is None:
+        optional_keys = []
+
+    if path in SPECIAL_CASES:
+        metadata.update(SPECIAL_CASES[path])
+    else:
+        # Try to find README.chromium.
+        readme_path = os.path.join(root, path, 'README.chromium')
+        if not os.path.exists(readme_path):
+            raise LicenseError("missing README.chromium or licenses.py "
+                               "SPECIAL_CASES entry in %s\n" % path)
+
+        for line in open(readme_path):
+            line = line.strip()
+            if not line:
+                break
+            for key in list(metadata.keys()) + optional_keys:
+                field = key + ": "
+                if line.startswith(field):
+                    metadata[key] = line[len(field):]
+
+    # Check that all expected metadata is present.
+    errors = []
+    for key, value in metadata.items():
+        if not value:
+            errors.append("couldn't find '" + key + "' line "
+                          "in README.chromium or licences.py "
+                          "SPECIAL_CASES")
+
+    # Special-case modules that aren't in the shipping product, so don't need
+    # their license in about:credits.
+    if metadata["License File"] != NOT_SHIPPED:
+        # Check that the license file exists.
+        for filename in (metadata["License File"], "COPYING"):
+            license_path = AbsolutePath(path, filename, root)
+            if license_path is not None:
+                break
+
+        if require_license_file and not license_path:
+            errors.append("License file not found. "
+                          "Either add a file named LICENSE, "
+                          "import upstream's COPYING if available, "
+                          "or add a 'License File:' line to "
+                          "README.chromium with the appropriate path.")
+        metadata["License File"] = license_path
+
+    if errors:
+        raise LicenseError("Errors in %s:\n %s\n" %
+                           (path, ";\n ".join(errors)))
+    return metadata
+
+
+def ContainsFiles(path, root):
+    """Determines whether any files exist in a directory or in any of its
+    subdirectories."""
+    for _, dirs, files in os.walk(os.path.join(root, path)):
+        if files:
+            return True
+        for prune_dir in PRUNE_DIRS:
+            if prune_dir in dirs:
+                dirs.remove(prune_dir)
+    return False
+
+
+def FilterDirsWithFiles(dirs_list, root):
+    # If a directory contains no files, assume it's a DEPS directory for a
+    # project not used by our current configuration and skip it.
+    return [x for x in dirs_list if ContainsFiles(x, root)]
+
+
+def FindThirdPartyDirs(prune_paths, root):
+    """Find all third_party directories underneath the source root."""
+    third_party_dirs = set()
+    for path, dirs, files in os.walk(root):
+        path = path[len(root) + 1:]  # Pretty up the path.
+
+        # .gitignore ignores /out*/, so do the same here.
+        if path in prune_paths or path.startswith('out'):
+            dirs[:] = []
+            continue
+
+        # Prune out directories we want to skip.
+        # (Note that we loop over PRUNE_DIRS so we're not iterating over a
+        # list that we're simultaneously mutating.)
+        for skip in PRUNE_DIRS:
+            if skip in dirs:
+                dirs.remove(skip)
+
+        if os.path.basename(path) == 'third_party':
+            # Add all subdirectories that are not marked for skipping.
+            for dir in dirs:
+                dirpath = os.path.join(path, dir)
+                if dirpath not in prune_paths:
+                    third_party_dirs.add(dirpath)
+
+            # Don't recurse into any subdirs from here.
+            dirs[:] = []
+            continue
+
+    return third_party_dirs
+
+
+def FindThirdPartyDirsWithFiles(root):
+    third_party_dirs = FindThirdPartyDirs(PRUNE_PATHS, root)
+    return FilterDirsWithFiles(third_party_dirs, root)
+
+
+# Many builders do not contain 'gn' in their PATH, so use the GN binary from
+# //buildtools.
+def _GnBinary():
+    exe = 'gn'
+    if sys.platform.startswith('linux'):
+        subdir = 'linux64'
+    elif sys.platform == 'darwin':
+        subdir = 'mac'
+    elif sys.platform == 'win32':
+        subdir, exe = 'win', 'gn.exe'
+    else:
+        raise RuntimeError("Unsupported platform '%s'." % sys.platform)
+
+    return os.path.join(_REPOSITORY_ROOT, 'buildtools', subdir, exe)
+
+
+def GetThirdPartyDepsFromGNDepsOutput(gn_deps, target_os):
+    """Returns third_party/foo directories given the output of "gn desc deps".
+
+    Note that it always returns the direct sub-directory of third_party
+    where README.chromium and LICENSE files are, so that it can be passed to
+    ParseDir(). e.g.:
+        third_party/cld_3/src/src/BUILD.gn -> third_party/cld_3
+
+    It returns relative paths from _REPOSITORY_ROOT, not absolute paths.
+    """
+    third_party_deps = set()
+    for absolute_build_dep in gn_deps.split():
+        relative_build_dep = os.path.relpath(absolute_build_dep,
+                                             _REPOSITORY_ROOT)
+        m = re.search(
+            r'^((.+[/\\])?third_party[/\\][^/\\]+[/\\])(.+[/\\])?BUILD\.gn$',
+            relative_build_dep)
+        if not m:
+            continue
+        third_party_path = m.group(1)
+        if any(third_party_path.startswith(p + os.sep) for p in PRUNE_PATHS):
+            continue
+        third_party_deps.add(third_party_path[:-1])
+    return third_party_deps
+
+
+def FindThirdPartyDeps(gn_out_dir, gn_target, target_os):
+    if not gn_out_dir:
+        raise RuntimeError("--gn-out-dir is required if --gn-target is used.")
+
+    # Generate gn project in temp directory and use it to find dependencies.
+    # Current gn directory cannot be used when we run this script in a gn action
+    # rule, because gn doesn't allow recursive invocations due to potential side
+    # effects.
+    tmp_dir = None
+    try:
+        tmp_dir = tempfile.mkdtemp(dir=gn_out_dir)
+        shutil.copy(os.path.join(gn_out_dir, "args.gn"), tmp_dir)
+        subprocess.check_output([_GnBinary(), "gen", tmp_dir])
+        gn_deps = subprocess.check_output([
+            _GnBinary(), "desc", tmp_dir, gn_target, "deps", "--as=buildfile",
+            "--all"
+        ])
+        if isinstance(gn_deps, bytes):
+            gn_deps = gn_deps.decode("utf-8")
+    finally:
+        if tmp_dir and os.path.exists(tmp_dir):
+            shutil.rmtree(tmp_dir)
+
+    return GetThirdPartyDepsFromGNDepsOutput(gn_deps, target_os)
+
+
+def ScanThirdPartyDirs(root=None):
+    """Scan a list of directories and report on any problems we find."""
+    if root is None:
+        root = os.getcwd()
+    third_party_dirs = FindThirdPartyDirsWithFiles(root)
+
+    errors = []
+    for path in sorted(third_party_dirs):
+        try:
+            metadata = ParseDir(path, root)
+        except LicenseError as e:
+            errors.append((path, e.args[0]))
+            continue
+
+    return ['{}: {}'.format(path, error) for path, error in sorted(errors)]
+
+
+def GenerateCredits(file_template_file,
+                    entry_template_file,
+                    output_file,
+                    target_os,
+                    gn_out_dir,
+                    gn_target,
+                    depfile=None):
+    """Generate about:credits."""
+
+    def EvaluateTemplate(template, env, escape=True):
+        """Expand a template with variables like {{foo}} using a
+        dictionary of expansions."""
+        for key, val in env.items():
+            if escape:
+                val = escape(val)
+            template = template.replace('{{%s}}' % key, val)
+        return template
+
+    def MetadataToTemplateEntry(metadata, entry_template):
+        env = {
+            'name': metadata['Name'],
+            'url': metadata['URL'],
+            'license': open(metadata['License File']).read(),
+        }
+        return {
+            'name': metadata['Name'],
+            'content': EvaluateTemplate(entry_template, env),
+            'license_file': metadata['License File'],
+        }
+
+    if gn_target:
+        third_party_dirs = FindThirdPartyDeps(gn_out_dir, gn_target, target_os)
+
+        # Sanity-check to raise a build error if invalid gn_... settings are
+        # somehow passed to this script.
+        if not third_party_dirs:
+            raise RuntimeError("No deps found.")
+    else:
+        third_party_dirs = FindThirdPartyDirs(PRUNE_PATHS, _REPOSITORY_ROOT)
+
+    if not file_template_file:
+        file_template_file = os.path.join(_REPOSITORY_ROOT, 'components',
+                                          'about_ui', 'resources',
+                                          'about_credits.tmpl')
+    if not entry_template_file:
+        entry_template_file = os.path.join(_REPOSITORY_ROOT, 'components',
+                                           'about_ui', 'resources',
+                                           'about_credits_entry.tmpl')
+
+    entry_template = open(entry_template_file).read()
+    entries = []
+    # Start from Chromium's LICENSE file
+    chromium_license_metadata = {
+        'Name': 'The Chromium Project',
+        'URL': 'http://www.chromium.org',
+        'License File': os.path.join(_REPOSITORY_ROOT, 'LICENSE')
+    }
+    entries.append(
+        MetadataToTemplateEntry(chromium_license_metadata, entry_template))
+
+    entries_by_name = {}
+    for path in third_party_dirs:
+        try:
+            metadata = ParseDir(path, _REPOSITORY_ROOT)
+        except LicenseError:
+            # TODO(phajdan.jr): Convert to fatal error (http://crbug.com/39240).
+            continue
+        if metadata['License File'] == NOT_SHIPPED:
+            continue
+
+        new_entry = MetadataToTemplateEntry(metadata, entry_template)
+        # Skip entries that we've already seen.
+        prev_entry = entries_by_name.setdefault(new_entry['name'], new_entry)
+        if prev_entry is not new_entry and (
+                prev_entry['content'] == new_entry['content']):
+            continue
+
+        entries.append(new_entry)
+
+    entries.sort(key=lambda entry: (entry['name'].lower(), entry['content']))
+    for entry_id, entry in enumerate(entries):
+        entry['content'] = entry['content'].replace('{{id}}', str(entry_id))
+
+    entries_contents = '\n'.join([entry['content'] for entry in entries])
+    file_template = open(file_template_file).read()
+    template_contents = "<!-- Generated by licenses.py; do not edit. -->"
+    template_contents += EvaluateTemplate(file_template,
+                                          {'entries': entries_contents},
+                                          escape=False)
+
+    if output_file:
+        changed = True
+        try:
+            old_output = open(output_file, 'r').read()
+            if old_output == template_contents:
+                changed = False
+        except:
+            pass
+        if changed:
+            with open(output_file, 'w') as output:
+                output.write(template_contents)
+    else:
+        print(template_contents)
+
+    if depfile:
+        assert output_file
+        # Add in build.ninja so that the target will be considered dirty when
+        # gn gen is run. Otherwise, it will fail to notice new files being
+        # added. This is still not perfect, as it will fail if no build files
+        # are changed, but a new README.chromium / LICENSE is added. This
+        # shouldn't happen in practice however.
+        license_file_list = (entry['license_file'] for entry in entries)
+        license_file_list = (os.path.relpath(p) for p in license_file_list)
+        license_file_list = sorted(set(license_file_list))
+        WriteDepfile(depfile, output_file, license_file_list + ['build.ninja'])
+
+    return True
+
+
+def _ReadFile(path):
+    """Reads a file from disk.
+    Args:
+      path: The path of the file to read, relative to the root of the
+      repository.
+    Returns:
+      The contents of the file as a string.
+    """
+    with codecs.open(os.path.join(_REPOSITORY_ROOT, path), 'r', 'utf-8') as f:
+        return f.read()
+
+
+def GenerateLicenseFile(output_file, gn_out_dir, gn_target, target_os):
+    """Generate a plain-text LICENSE file which can be used when you ship a part
+    of Chromium code (specified by gn_target) as a stand-alone library
+    (e.g., //ios/web_view).
+
+    The LICENSE file contains licenses of both Chromium and third-party
+    libraries which gn_target depends on. """
+
+    third_party_dirs = FindThirdPartyDeps(gn_out_dir, gn_target, target_os)
+
+    # Start with Chromium's LICENSE file.
+    content = [_ReadFile('LICENSE')]
+
+    # Add necessary third_party.
+    for directory in sorted(third_party_dirs):
+        metadata = ParseDir(directory,
+                            _REPOSITORY_ROOT,
+                            require_license_file=True)
+        license_file = metadata['License File']
+        if license_file and license_file != NOT_SHIPPED:
+            content.append('-' * 20)
+            content.append(directory.split(os.sep)[-1])
+            content.append('-' * 20)
+            content.append(_ReadFile(license_file))
+
+    content_text = '\n'.join(content)
+
+    if output_file:
+        with codecs.open(output_file, 'w', 'utf-8') as output:
+            output.write(content_text)
+    else:
+        print(content_text)
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument('--file-template',
+                        help='Template HTML to use for the license page.')
+    parser.add_argument('--entry-template',
+                        help='Template HTML to use for each license.')
+    parser.add_argument('--target-os', help='OS that this build is targeting.')
+    parser.add_argument('--gn-out-dir',
+                        help='GN output directory for scanning dependencies.')
+    parser.add_argument('--gn-target',
+                        help='GN target to scan for dependencies.')
+    parser.add_argument('command',
+                        choices=['help', 'scan', 'credits', 'license_file'])
+    parser.add_argument('output_file', nargs='?')
+    parser.add_argument('--depfile',
+                        help='Path to depfile (refer to `gn help depfile`)')
+    args = parser.parse_args()
+
+    if args.command == 'scan':
+        if not ScanThirdPartyDirs():
+            return 1
+    elif args.command == 'credits':
+        if not GenerateCredits(args.file_template, args.entry_template,
+                               args.output_file, args.target_os,
+                               args.gn_out_dir, args.gn_target, args.depfile):
+            return 1
+    elif args.command == 'license_file':
+        try:
+            GenerateLicenseFile(args.output_file, args.gn_out_dir,
+                                args.gn_target, args.target_os)
+        except LicenseError as e:
+            print("Failed to parse README.chromium: {}".format(e))
+            return 1
+    else:
+        print(__doc__)
+        return 1
+
+
+if __name__ == '__main__':
+    sys.exit(main())