licensing: add "tainted" signal

The check for tainted tag in singing will be made in a separate CL.

BUG=chromium:1059363
TEST=unit and manual

Change-Id: I309ad5947b00e405e7e746f71121061eb2e20d02
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/2560225
Tested-by: Sergey Frolov <sfrolov@google.com>
Commit-Queue: Sergey Frolov <sfrolov@google.com>
Reviewed-by: Mike Frysinger <vapier@chromium.org>
Reviewed-by: Stephane Belmon <sbelmon@google.com>
diff --git a/licensing/about_credits.tmpl b/licensing/about_credits.tmpl
index 67e93ba..3d8bb5b 100644
--- a/licensing/about_credits.tmpl
+++ b/licensing/about_credits.tmpl
@@ -5,6 +5,7 @@
 <html>
 <head>
 <meta charset="UTF-8">
+{{tainted_warning_if_any}}
 <title>Credits</title>
 <link rel="stylesheet" href="chrome://resources/css/text_defaults.css">
 <style>
diff --git a/licensing/about_credits_entry.tmpl b/licensing/about_credits_entry.tmpl
index 490b209..1230011 100644
--- a/licensing/about_credits_entry.tmpl
+++ b/licensing/about_credits_entry.tmpl
@@ -1,3 +1,4 @@
+{{comments}}
 <div class="product">
 <span class="title" id="{{name}}">{{namerev}}</span>
 <a class="show" href="#" onclick="return toggle(this);">show license text</a>
diff --git a/licensing/licenses.py b/licensing/licenses.py
index 19455ab..a50f011 100644
--- a/licensing/licenses.py
+++ b/licensing/licenses.py
@@ -45,8 +45,8 @@
 This gets automatically installed in
 /build/x86-alex/var/db/pkg/dev-util/libc-bench-0.0.1-r8/license.yaml
 
-Unless you run with --generate, the script will now gather those license
-bits and generate a license file from there.
+Unless you run with --generate-licenses, the script will now gather those
+license bits and generate a license file from there.
 License bits for each package are generated by default from
 src/scripts/hooks/install/gen-package-licenses.sh which gets run automatically
 by emerge as part of a package build (by running this script with
@@ -62,8 +62,8 @@
   %(prog)s --package "dev-libs/libatomic_ops-7.2d" --package \
   "net-misc/wget-1.14" --board $BOARD -o out.html
 
-Note that you'll want to use --generate to force regeneration of the licensing
-bits from a package source you may have just modified but not rebuilt.
+Note that you'll want to use --generate-licenses to force regeneration of the
+licensing bits from a package source you may have just modified but not rebuilt.
 
 If you want to check licensing against all ChromeOS packages, you should
 run ./build_packages --board=$BOARD to build everything and then run
diff --git a/licensing/licenses_lib.py b/licensing/licenses_lib.py
index e4eff85..a77c984 100644
--- a/licensing/licenses_lib.py
+++ b/licensing/licenses_lib.py
@@ -60,16 +60,19 @@
     'virtual',
 ]
 
+# If you have an early package for which license terms have yet to be decided,
+# use this. It will cause licensing for the package to be mostly ignored.
+# Tainted builds will fail signing with official keys.
+TAINTED = 'TAINTED'
+
+# HTML outputs will include this tag if tainted.
+TAINTED_COMMENT_TAG = '<!-- tainted -->'
+
 SKIPPED_LICENSES = [
     # Some of our packages contain binary blobs for which we have special
     # negotiated licenses, and no need to display anything publicly. Strongly
     # consider using Google-TOS instead, if possible.
     'Proprietary-Binary',
-
-    # If you have an early repo for which license terms have yet to be decided
-    # use this. It will cause licensing for the package to be mostly ignored.
-    # Official should error for any package with this license.
-    'TAINTED', # TODO(dgarrett): Error on official builds with this license.
 ]
 
 LICENSE_NAMES_REGEX = [
@@ -325,9 +328,12 @@
     # one to skip in licensing.
     self.skip = False
 
-    # Intellegently populate initial skip information.
+    # Intelligently populate initial skip information.
     self.LookForSkip()
 
+    # Set to something by GetLicenses().
+    self.tainted = None
+
   @property
   def fullnamerev(self):
     """e.g. libnl/libnl-3.2.24-r12"""
@@ -596,7 +602,7 @@
     """Populate the license related fields.
 
     Fields populated:
-      license_names, license_text_scanned, homepages, skip
+      license_names, license_text_scanned, homepages, skip, tainted
 
     Some packages have static license mappings applied to them that get
     retrieved from the ebuild.
@@ -625,6 +631,11 @@
     self.homepages = _BuildInfo(build_info_dir, 'HOMEPAGE').split()
     ebuild_license_names = _BuildInfo(build_info_dir, 'LICENSE').split()
 
+    # Is this tainted?
+    self.tainted = TAINTED in ebuild_license_names
+    if self.tainted:
+      logging.warning('Package %s is tainted', self.fullnamerev)
+
     # If this ebuild only uses skipped licenses, skip it.
     if (ebuild_license_names and
         all(l in SKIPPED_LICENSES for l in ebuild_license_names)):
@@ -1026,6 +1037,9 @@
     self.packages = {}
     self._package_fullnames = package_fullnames
 
+    # Set by ProcessPackageLicenses().
+    self.tainted_pkgs = []
+
   @property
   def sorted_licenses(self):
     return sorted(self.licenses, key=lambda x: x.lower())
@@ -1081,6 +1095,11 @@
         logging.debug('loading dump for %s', pkg.fullnamerev)
         self._LoadLicenseDump(pkg)
 
+      # Store all tainted packages to print at the top of generated license.
+      # If any package is tainted, the whole thing is tainted.
+      if pkg.tainted:
+        self.tainted_pkgs.append(package_name)
+
   def AddExtraPkg(self, fullnamerev, homepages, license_names, license_texts):
     """Allow adding pre-created virtual packages.
 
@@ -1105,7 +1124,7 @@
   @staticmethod
   def FindLicenseType(license_name, board=None, sysroot=None, overlay_path=None,
                       buildroot=constants.SOURCE_ROOT):
-    """Says if a license is stock Gentoo, custom, or doesn't exist.
+    """Says if a license is stock Gentoo, custom, tainted, or doesn't exist.
 
     Will check the old, static locations by default, but supplying either the
     overlay directory or sysroot allows searching in the overlay hierarchy.
@@ -1125,6 +1144,9 @@
     Raises:
       AssertionError when the license couldn't be found
     """
+    if license_name == TAINTED:
+      return TAINTED
+
     # Check the stock licenses first since those may appear in the generated
     # list of overlay directories for a board
     stock = _GetLicenseDirectories(dir_set=_STOCK_DIRS, buildroot=buildroot)
@@ -1221,6 +1243,7 @@
                            pkg.fullnamerev)
 
     env = {
+        'comments': TAINTED_COMMENT_TAG if pkg.tainted else '',
         'name': pkg.name,
         'namerev': '%s-%s' % (pkg.name, pkg.version),
         'url': html.escape(pkg.homepages[0]) if pkg.homepages else '',
@@ -1250,6 +1273,9 @@
     license_txts = {}
     # Keep track of which licenses are used by which packages.
     for pkg in self.packages.values():
+      if pkg.tainted:
+        license_txts[pkg] = TAINTED
+        continue
       if pkg.skip:
         continue
       for sln in pkg.license_names:
@@ -1318,7 +1344,19 @@
       licenses_txt += [self.EvaluateTemplate(license_template, env)]
 
     file_template = ReadUnknownEncodedFile(output_template)
+    tainted_warning = ''
+    if self.tainted_pkgs:
+      tainted_warning = (TAINTED_COMMENT_TAG + '\n' +
+            '<h1>Image is TAINTED due to the following packages:</h1>\n' +
+            '<ul style="font-size:large">\n' +
+            '\n'.join(f'  <li>{x}</li>' for x in self.tainted_pkgs) +
+            '\n</ul>\n')
+      for tainted_pkg in self.tainted_pkgs:
+        logging.warning('Package %s is tainted', tainted_pkg)
+      logging.warning('Image is tainted. See licensing docs to fix this: '
+                      'https://dev.chromium.org/chromium-os/licensing')
     env = {
+        'tainted_warning_if_any': tainted_warning,
         'entries': '\n'.join(sorted_license_txt),
         'licenses': '\n'.join(licenses_txt),
     }
diff --git a/licensing/licenses_lib_unittest.py b/licensing/licenses_lib_unittest.py
index 1b19d39..8181b5b 100644
--- a/licensing/licenses_lib_unittest.py
+++ b/licensing/licenses_lib_unittest.py
@@ -13,7 +13,6 @@
 from chromite.lib import osutils
 from chromite.licensing import licenses_lib
 
-
 # pylint: disable=protected-access
 
 
@@ -70,6 +69,12 @@
             'dir': 'src/overlays/overlay-bar/licenses/OOR',
             'skip_test': True,
         },
+        licenses_lib.TAINTED: {
+            'contents': licenses_lib.TAINTED,
+            'dir': 'src/overlays/overlay-foo/licenses/TAINTED',
+            'board': 'foo',
+            'type': licenses_lib.TAINTED,
+        },
     }
 
     # Overlays for testing.
@@ -139,6 +144,11 @@
             'board': 'foo',
             'license': 'FTL',
         },
+        'ttl-pkg': {
+            'dir': os.path.join(foo_eb_dir, 'ttl-pkg/ttl-pkg-1.ebuild'),
+            'board': 'foo',
+            'license': licenses_lib.TAINTED,
+        },
         'pub-pkg': {
             'dir': os.path.join(foo_eb_dir, 'pub-pkg/pub-pkg-1.ebuild'),
             'board': 'foo',
@@ -175,6 +185,7 @@
                         D('ftl-pkg', ('ftl-pkg-1.ebuild',)),
                         D('pub-pkg', ('pub-pkg-1.ebuild',)),
                         D('tpl-pkg', ('tpl-pkg-1.ebuild',)),
+                        D('ttl-pkg', ('ttl-pkg-1.ebuild',)),
                     )),
                     D('eclass', ('pub-cls.eclass',)),
                     D('licenses', ('FTL',)),
@@ -242,6 +253,17 @@
     expected = ['FTL']
     self.assertEqual(expected, sorted(result))
 
+  def testGetLicenseTypeFromEbuildTainted(self):
+    """Tests the fetched tainted license from ebuilds is correct."""
+    ebuild_content = self.ebuilds['ttl-pkg']['content']
+    overlay_path = os.sep.join(
+        self.ebuilds['ttl-pkg']['dir'].split(os.sep)[:-3])
+
+    result = licenses_lib.GetLicenseTypesFromEbuild(ebuild_content,
+                                                    overlay_path, self.tempdir)
+    expected = [licenses_lib.TAINTED]
+    self.assertEqual(expected, sorted(result))
+
   def testFindLicenseType(self):
     """Tests the type (gentoo/custom) for licenses are correctly identified."""
     # Doesn't exist anywhere.