Build notice files from license metadata.

Switch from generate-notice-files.py and mergenotice.py to htmlnotice.

Bug: 151177513
Bug: 213388645
Bug: 210912771

Test: m droid dist reportmissinglicenses
Change-Id: I6cac049d24f35ec358c6f341a04f4ba6161703bf
diff --git a/android/notices.go b/android/notices.go
index 194a734..2a4c17c 100644
--- a/android/notices.go
+++ b/android/notices.go
@@ -15,93 +15,9 @@
 package android
 
 import (
-	"path/filepath"
 	"strings"
-
-	"github.com/google/blueprint"
 )
 
-func init() {
-	pctx.SourcePathVariable("merge_notices", "build/soong/scripts/mergenotice.py")
-	pctx.SourcePathVariable("generate_notice", "build/soong/scripts/generate-notice-files.py")
-
-	pctx.HostBinToolVariable("minigzip", "minigzip")
-}
-
-type NoticeOutputs struct {
-	Merged       OptionalPath
-	TxtOutput    OptionalPath
-	HtmlOutput   OptionalPath
-	HtmlGzOutput OptionalPath
-}
-
-var (
-	mergeNoticesRule = pctx.AndroidStaticRule("mergeNoticesRule", blueprint.RuleParams{
-		Command:     `${merge_notices} --output $out $in`,
-		CommandDeps: []string{"${merge_notices}"},
-		Description: "merge notice files into $out",
-	})
-
-	generateNoticeRule = pctx.AndroidStaticRule("generateNoticeRule", blueprint.RuleParams{
-		Command: `rm -rf $$(dirname $txtOut) $$(dirname $htmlOut) $$(dirname $out) && ` +
-			`mkdir -p $$(dirname $txtOut) $$(dirname $htmlOut)  $$(dirname $out) && ` +
-			`${generate_notice} --text-output $txtOut --html-output $htmlOut -t "$title" -s $inputDir && ` +
-			`${minigzip} -c $htmlOut > $out`,
-		CommandDeps: []string{"${generate_notice}", "${minigzip}"},
-		Description: "produce notice file $out",
-	}, "txtOut", "htmlOut", "title", "inputDir")
-)
-
-func MergeNotices(ctx ModuleContext, mergedNotice WritablePath, noticePaths []Path) {
-	ctx.Build(pctx, BuildParams{
-		Rule:        mergeNoticesRule,
-		Description: "merge notices",
-		Inputs:      noticePaths,
-		Output:      mergedNotice,
-	})
-}
-
-func BuildNoticeOutput(ctx ModuleContext, installPath InstallPath, installFilename string,
-	noticePaths []Path) NoticeOutputs {
-	// Merge all NOTICE files into one.
-	// TODO(jungjw): We should just produce a well-formatted NOTICE.html file in a single pass.
-	//
-	// generate-notice-files.py, which processes the merged NOTICE file, has somewhat strict rules
-	// about input NOTICE file paths.
-	// 1. Their relative paths to the src root become their NOTICE index titles. We want to use
-	// on-device paths as titles, and so output the merged NOTICE file the corresponding location.
-	// 2. They must end with .txt extension. Otherwise, they're ignored.
-	noticeRelPath := InstallPathToOnDevicePath(ctx, installPath.Join(ctx, installFilename+".txt"))
-	mergedNotice := PathForModuleOut(ctx, filepath.Join("NOTICE_FILES/src", noticeRelPath))
-	MergeNotices(ctx, mergedNotice, noticePaths)
-
-	// Transform the merged NOTICE file into a gzipped HTML file.
-	txtOuptut := PathForModuleOut(ctx, "NOTICE_txt", "NOTICE.txt")
-	htmlOutput := PathForModuleOut(ctx, "NOTICE_html", "NOTICE.html")
-	htmlGzOutput := PathForModuleOut(ctx, "NOTICE", "NOTICE.html.gz")
-	title := "Notices for " + ctx.ModuleName()
-	ctx.Build(pctx, BuildParams{
-		Rule:            generateNoticeRule,
-		Description:     "generate notice output",
-		Input:           mergedNotice,
-		Output:          htmlGzOutput,
-		ImplicitOutputs: WritablePaths{txtOuptut, htmlOutput},
-		Args: map[string]string{
-			"txtOut":   txtOuptut.String(),
-			"htmlOut":  htmlOutput.String(),
-			"title":    title,
-			"inputDir": PathForModuleOut(ctx, "NOTICE_FILES/src").String(),
-		},
-	})
-
-	return NoticeOutputs{
-		Merged:       OptionalPathForPath(mergedNotice),
-		TxtOutput:    OptionalPathForPath(txtOuptut),
-		HtmlOutput:   OptionalPathForPath(htmlOutput),
-		HtmlGzOutput: OptionalPathForPath(htmlGzOutput),
-	}
-}
-
 // BuildNoticeTextOutputFromLicenseMetadata writes out a notice text file based on the module's
 // generated license metadata file.
 func BuildNoticeTextOutputFromLicenseMetadata(ctx ModuleContext, outputFile WritablePath) {
@@ -112,5 +28,18 @@
 		FlagWithOutput("-o ", outputFile).
 		FlagWithDepFile("-d ", depsFile).
 		Input(ctx.Module().base().licenseMetadataFile)
-	rule.Build("container_notice", "container notice file")
+	rule.Build("text_notice", "container notice file")
+}
+
+// BuildNoticeHtmlOutputFromLicenseMetadata writes out a notice text file based on the module's
+// generated license metadata file.
+func BuildNoticeHtmlOutputFromLicenseMetadata(ctx ModuleContext, outputFile WritablePath) {
+	depsFile := outputFile.ReplaceExtension(ctx, strings.TrimPrefix(outputFile.Ext()+".d", "."))
+	rule := NewRuleBuilder(pctx, ctx)
+	rule.Command().
+		BuiltTool("htmlnotice").
+		FlagWithOutput("-o ", outputFile).
+		FlagWithDepFile("-d ", depsFile).
+		Input(ctx.Module().base().licenseMetadataFile)
+	rule.Build("html_notice", "container notice file")
 }
diff --git a/apex/androidmk.go b/apex/androidmk.go
index 059b4d7..e094a12 100644
--- a/apex/androidmk.go
+++ b/apex/androidmk.go
@@ -396,10 +396,6 @@
 				}
 				a.writeRequiredModules(w, moduleNames)
 
-				if a.mergedNotices.Merged.Valid() {
-					fmt.Fprintln(w, "LOCAL_NOTICE_FILE :=", a.mergedNotices.Merged.Path().String())
-				}
-
 				fmt.Fprintln(w, "include $(BUILD_PREBUILT)")
 
 				if apexType == imageApex {
diff --git a/apex/apex.go b/apex/apex.go
index 6d8a67a..cb88f02 100644
--- a/apex/apex.go
+++ b/apex/apex.go
@@ -414,8 +414,8 @@
 	// Processed file_contexts files
 	fileContexts android.WritablePath
 
-	// Struct holding the merged notice file paths in different formats
-	mergedNotices android.NoticeOutputs
+	// Path to notice file in html.gz format.
+	htmlGzNotice android.WritablePath
 
 	// The built APEX file. This is the main product.
 	// Could be .apex or .capex
@@ -487,11 +487,10 @@
 // for each of the files in case when the APEX is flattened.
 type apexFile struct {
 	// buildFile is put in the installDir inside the APEX.
-	builtFile   android.Path
-	noticeFiles android.Paths
-	installDir  string
-	customStem  string
-	symlinks    []string // additional symlinks
+	builtFile  android.Path
+	installDir string
+	customStem string
+	symlinks   []string // additional symlinks
 
 	// Info for Android.mk Module name of `module` in AndroidMk. Note the generated AndroidMk
 	// module for apexFile is named something like <AndroidMk module name>.<apex name>[<apex
@@ -528,7 +527,6 @@
 		module:              module,
 	}
 	if module != nil {
-		ret.noticeFiles = module.NoticeFiles()
 		ret.moduleDir = ctx.OtherModuleDir(module)
 		ret.requiredModuleNames = module.RequiredModuleNames()
 		ret.targetRequiredModuleNames = module.TargetRequiredModuleNames()
diff --git a/apex/apex_test.go b/apex/apex_test.go
index 85bd595..ec815c4 100644
--- a/apex/apex_test.go
+++ b/apex/apex_test.go
@@ -591,15 +591,6 @@
 		t.Errorf("Could not find all expected symlinks! foo: %t, foo_link_64: %t. Command was %s", found_foo, found_foo_link_64, copyCmds)
 	}
 
-	mergeNoticesRule := ctx.ModuleForTests("myapex", "android_common_myapex_image").Rule("mergeNoticesRule")
-	noticeInputs := mergeNoticesRule.Inputs.Strings()
-	if len(noticeInputs) != 3 {
-		t.Errorf("number of input notice files: expected = 3, actual = %q", len(noticeInputs))
-	}
-	ensureListContains(t, noticeInputs, "NOTICE")
-	ensureListContains(t, noticeInputs, "custom_notice")
-	ensureListContains(t, noticeInputs, "custom_notice_for_static_lib")
-
 	fullDepsInfo := strings.Split(ctx.ModuleForTests("myapex", "android_common_myapex_image").Output("depsinfo/fulllist.txt").Args["content"], "\\n")
 	ensureListContains(t, fullDepsInfo, "  myjar(minSdkVersion:(no version)) <- myapex")
 	ensureListContains(t, fullDepsInfo, "  mylib2(minSdkVersion:(no version)) <- mylib")
diff --git a/apex/builder.go b/apex/builder.go
index 8c5f99b..5d5d1ab 100644
--- a/apex/builder.go
+++ b/apex/builder.go
@@ -305,32 +305,6 @@
 	return output.OutputPath
 }
 
-// buildNoticeFiles creates a buile rule for aggregating notice files from the modules that
-// contributes to this APEX. The notice files are merged into a big notice file.
-func (a *apexBundle) buildNoticeFiles(ctx android.ModuleContext, apexFileName string) android.NoticeOutputs {
-	var noticeFiles android.Paths
-
-	a.WalkPayloadDeps(ctx, func(ctx android.ModuleContext, from blueprint.Module, to android.ApexModule, externalDep bool) bool {
-		if externalDep {
-			// As soon as the dependency graph crosses the APEX boundary, don't go further.
-			return false
-		}
-		noticeFiles = append(noticeFiles, to.NoticeFiles()...)
-		return true
-	})
-
-	// TODO(jiyong): why do we need this? WalkPayloadDeps should have already covered this.
-	for _, fi := range a.filesInfo {
-		noticeFiles = append(noticeFiles, fi.noticeFiles...)
-	}
-
-	if len(noticeFiles) == 0 {
-		return android.NoticeOutputs{}
-	}
-
-	return android.BuildNoticeOutput(ctx, a.installDir, apexFileName, android.SortedUniquePaths(noticeFiles))
-}
-
 // buildInstalledFilesFile creates a build rule for the installed-files.txt file where the list of
 // files included in this APEX is shown. The text file is dist'ed so that people can see what's
 // included in the APEX without actually downloading and extracting it.
@@ -642,12 +616,11 @@
 			optFlags = append(optFlags, "--logging_parent ", a.overridableProperties.Logging_parent)
 		}
 
-		a.mergedNotices = a.buildNoticeFiles(ctx, a.Name()+suffix)
-		if a.mergedNotices.HtmlGzOutput.Valid() {
-			// If there's a NOTICE file, embed it as an asset file in the APEX.
-			implicitInputs = append(implicitInputs, a.mergedNotices.HtmlGzOutput.Path())
-			optFlags = append(optFlags, "--assets_dir "+filepath.Dir(a.mergedNotices.HtmlGzOutput.String()))
-		}
+		// Create a NOTICE file, and embed it as an asset file in the APEX.
+		a.htmlGzNotice = android.PathForModuleOut(ctx, "NOTICE/NOTICES.html.gz")
+		android.BuildNoticeHtmlOutputFromLicenseMetadata(ctx, a.htmlGzNotice)
+		implicitInputs = append(implicitInputs, a.htmlGzNotice)
+		optFlags = append(optFlags, "--assets_dir "+filepath.Dir(a.htmlGzNotice.String()))
 
 		if (moduleMinSdkVersion.GreaterThan(android.SdkVersion_Android10) && !a.shouldGenerateHashtree()) && !compressionEnabled {
 			// Apexes which are supposed to be installed in builtin dirs(/system, etc)
diff --git a/java/androidmk.go b/java/androidmk.go
index b930441..80b828d 100644
--- a/java/androidmk.go
+++ b/java/androidmk.go
@@ -409,22 +409,6 @@
 				entries.SetOptionalPaths("LOCAL_SOONG_LINT_REPORTS", app.linter.reports)
 			},
 		},
-		ExtraFooters: []android.AndroidMkExtraFootersFunc{
-			func(w io.Writer, name, prefix, moduleDir string) {
-				if app.noticeOutputs.Merged.Valid() {
-					fmt.Fprintf(w, "$(call dist-for-goals,%s,%s:%s)\n",
-						app.installApkName, app.noticeOutputs.Merged.String(), app.installApkName+"_NOTICE")
-				}
-				if app.noticeOutputs.TxtOutput.Valid() {
-					fmt.Fprintf(w, "$(call dist-for-goals,%s,%s:%s)\n",
-						app.installApkName, app.noticeOutputs.TxtOutput.String(), app.installApkName+"_NOTICE.txt")
-				}
-				if app.noticeOutputs.HtmlOutput.Valid() {
-					fmt.Fprintf(w, "$(call dist-for-goals,%s,%s:%s)\n",
-						app.installApkName, app.noticeOutputs.HtmlOutput.String(), app.installApkName+"_NOTICE.html")
-				}
-			},
-		},
 	}}
 }
 
diff --git a/java/app.go b/java/app.go
index 8728df6..573b9ec 100755
--- a/java/app.go
+++ b/java/app.go
@@ -19,7 +19,6 @@
 
 import (
 	"path/filepath"
-	"sort"
 	"strings"
 
 	"github.com/google/blueprint"
@@ -164,8 +163,6 @@
 
 	additionalAaptFlags []string
 
-	noticeOutputs android.NoticeOutputs
-
 	overriddenManifestPackageName string
 
 	android.ApexBundleDepsInfo
@@ -523,53 +520,6 @@
 	return jniSymbols
 }
 
-func (a *AndroidApp) noticeBuildActions(ctx android.ModuleContext) {
-	// Collect NOTICE files from all dependencies.
-	seenModules := make(map[android.Module]bool)
-	noticePathSet := make(map[android.Path]bool)
-
-	ctx.WalkDeps(func(child android.Module, parent android.Module) bool {
-		// Have we already seen this?
-		if _, ok := seenModules[child]; ok {
-			return false
-		}
-		seenModules[child] = true
-
-		// Skip host modules.
-		if child.Target().Os.Class == android.Host {
-			return false
-		}
-
-		paths := child.(android.Module).NoticeFiles()
-		if len(paths) > 0 {
-			for _, path := range paths {
-				noticePathSet[path] = true
-			}
-		}
-		return true
-	})
-
-	// If the app has one, add it too.
-	if len(a.NoticeFiles()) > 0 {
-		for _, path := range a.NoticeFiles() {
-			noticePathSet[path] = true
-		}
-	}
-
-	if len(noticePathSet) == 0 {
-		return
-	}
-	var noticePaths []android.Path
-	for path := range noticePathSet {
-		noticePaths = append(noticePaths, path)
-	}
-	sort.Slice(noticePaths, func(i, j int) bool {
-		return noticePaths[i].String() < noticePaths[j].String()
-	})
-
-	a.noticeOutputs = android.BuildNoticeOutput(ctx, a.installDir, a.installApkName+".apk", noticePaths)
-}
-
 // Reads and prepends a main cert from the default cert dir if it hasn't been set already, i.e. it
 // isn't a cert module reference. Also checks and enforces system cert restriction if applicable.
 func processMainCert(m android.ModuleBase, certPropValue string, certificates []Certificate, ctx android.ModuleContext) []Certificate {
@@ -636,9 +586,10 @@
 	}
 	a.onDeviceDir = android.InstallPathToOnDevicePath(ctx, a.installDir)
 
-	a.noticeBuildActions(ctx)
+	noticeFile := android.PathForModuleOut(ctx, "NOTICES.html.gz")
+	android.BuildNoticeHtmlOutputFromLicenseMetadata(ctx, noticeFile)
 	if Bool(a.appProperties.Embed_notices) || ctx.Config().IsEnvTrue("ALWAYS_EMBED_NOTICES") {
-		a.aapt.noticeFile = a.noticeOutputs.HtmlGzOutput
+		a.aapt.noticeFile = android.OptionalPathForPath(noticeFile)
 	}
 
 	a.classLoaderContexts = a.usesLibrary.classLoaderContextForUsesLibDeps(ctx)
diff --git a/java/app_test.go b/java/app_test.go
index 16bbec1..08baf54 100644
--- a/java/app_test.go
+++ b/java/app_test.go
@@ -27,7 +27,6 @@
 	"android/soong/android"
 	"android/soong/cc"
 	"android/soong/dexpreopt"
-	"android/soong/genrule"
 )
 
 // testApp runs tests using the prepareForJavaTest
@@ -2722,116 +2721,6 @@
 	}
 }
 
-func TestEmbedNotice(t *testing.T) {
-	result := android.GroupFixturePreparers(
-		PrepareForTestWithJavaDefaultModules,
-		cc.PrepareForTestWithCcDefaultModules,
-		genrule.PrepareForTestWithGenRuleBuildComponents,
-		android.MockFS{
-			"APP_NOTICE":     nil,
-			"GENRULE_NOTICE": nil,
-			"LIB_NOTICE":     nil,
-			"TOOL_NOTICE":    nil,
-		}.AddToFixture(),
-	).RunTestWithBp(t, `
-		android_app {
-			name: "foo",
-			srcs: ["a.java"],
-			static_libs: ["javalib"],
-			jni_libs: ["libjni"],
-			notice: "APP_NOTICE",
-			embed_notices: true,
-			sdk_version: "current",
-		}
-
-		// No embed_notice flag
-		android_app {
-			name: "bar",
-			srcs: ["a.java"],
-			jni_libs: ["libjni"],
-			notice: "APP_NOTICE",
-			sdk_version: "current",
-		}
-
-		// No NOTICE files
-		android_app {
-			name: "baz",
-			srcs: ["a.java"],
-			embed_notices: true,
-			sdk_version: "current",
-		}
-
-		cc_library {
-			name: "libjni",
-			system_shared_libs: [],
-			stl: "none",
-			notice: "LIB_NOTICE",
-			sdk_version: "current",
-		}
-
-		java_library {
-			name: "javalib",
-			srcs: [
-				":gen",
-			],
-			sdk_version: "current",
-		}
-
-		genrule {
-			name: "gen",
-			tools: ["gentool"],
-			out: ["gen.java"],
-			notice: "GENRULE_NOTICE",
-		}
-
-		java_binary_host {
-			name: "gentool",
-			srcs: ["b.java"],
-			notice: "TOOL_NOTICE",
-		}
-	`)
-
-	// foo has NOTICE files to process, and embed_notices is true.
-	foo := result.ModuleForTests("foo", "android_common")
-	// verify merge notices rule.
-	mergeNotices := foo.Rule("mergeNoticesRule")
-	noticeInputs := mergeNotices.Inputs.Strings()
-	// TOOL_NOTICE should be excluded as it's a host module.
-	if len(mergeNotices.Inputs) != 3 {
-		t.Errorf("number of input notice files: expected = 3, actual = %q", noticeInputs)
-	}
-	if !inList("APP_NOTICE", noticeInputs) {
-		t.Errorf("APP_NOTICE is missing from notice files, %q", noticeInputs)
-	}
-	if !inList("LIB_NOTICE", noticeInputs) {
-		t.Errorf("LIB_NOTICE is missing from notice files, %q", noticeInputs)
-	}
-	if !inList("GENRULE_NOTICE", noticeInputs) {
-		t.Errorf("GENRULE_NOTICE is missing from notice files, %q", noticeInputs)
-	}
-	// aapt2 flags should include -A <NOTICE dir> so that its contents are put in the APK's /assets.
-	res := foo.Output("package-res.apk")
-	aapt2Flags := res.Args["flags"]
-	e := "-A out/soong/.intermediates/foo/android_common/NOTICE"
-	android.AssertStringDoesContain(t, "expected.apkPath", aapt2Flags, e)
-
-	// bar has NOTICE files to process, but embed_notices is not set.
-	bar := result.ModuleForTests("bar", "android_common")
-	res = bar.Output("package-res.apk")
-	aapt2Flags = res.Args["flags"]
-	e = "-A out/soong/.intermediates/bar/android_common/NOTICE"
-	android.AssertStringDoesNotContain(t, "bar shouldn't have the asset dir flag for NOTICE", aapt2Flags, e)
-
-	// baz's embed_notice is true, but it doesn't have any NOTICE files.
-	baz := result.ModuleForTests("baz", "android_common")
-	res = baz.Output("package-res.apk")
-	aapt2Flags = res.Args["flags"]
-	e = "-A out/soong/.intermediates/baz/android_common/NOTICE"
-	if strings.Contains(aapt2Flags, e) {
-		t.Errorf("baz shouldn't have the asset dir flag for NOTICE: %q", e)
-	}
-}
-
 func TestUncompressDex(t *testing.T) {
 	testCases := []struct {
 		name string
diff --git a/scripts/generate-notice-files.py b/scripts/generate-notice-files.py
deleted file mode 100755
index 1b4acfa..0000000
--- a/scripts/generate-notice-files.py
+++ /dev/null
@@ -1,272 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2012 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""
-Usage: generate-notice-files --text-output [plain text output file] \
-               --html-output [html output file] \
-               --xml-output [xml output file] \
-               -t [file title] -s [directory of notices]
-
-Generate the Android notice files, including both text and html files.
-
--h to display this usage message and exit.
-"""
-from collections import defaultdict
-import argparse
-import hashlib
-import itertools
-import os
-import os.path
-import re
-import struct
-import sys
-
-MD5_BLOCKSIZE = 1024 * 1024
-HTML_ESCAPE_TABLE = {
-    b"&": b"&amp;",
-    b'"': b"&quot;",
-    b"'": b"&apos;",
-    b">": b"&gt;",
-    b"<": b"&lt;",
-    }
-
-def md5sum(filename):
-    """Calculate an MD5 of the file given by FILENAME,
-    and return hex digest as a string.
-    Output should be compatible with md5sum command"""
-
-    f = open(filename, "rb")
-    sum = hashlib.md5()
-    while 1:
-        block = f.read(MD5_BLOCKSIZE)
-        if not block:
-            break
-        sum.update(block)
-    f.close()
-    return sum.hexdigest()
-
-
-def html_escape(text):
-    """Produce entities within text."""
-    # Using for i in text doesn't work since i will be an int, not a byte.
-    # There are multiple ways to solve this, but the most performant way
-    # to iterate over a byte array is to use unpack. Using the
-    # for i in range(len(text)) and using that to get a byte using array
-    # slices is twice as slow as this method.
-    return b"".join(HTML_ESCAPE_TABLE.get(i,i) for i in struct.unpack(str(len(text)) + 'c', text))
-
-HTML_OUTPUT_CSS=b"""
-<style type="text/css">
-body { padding: 0; font-family: sans-serif; }
-.same-license { background-color: #eeeeee; border-top: 20px solid white; padding: 10px; }
-.label { font-weight: bold; }
-.file-list { margin-left: 1em; color: blue; }
-</style>
-
-"""
-
-def combine_notice_files_html(file_hash, input_dir, output_filename):
-    """Combine notice files in FILE_HASH and output a HTML version to OUTPUT_FILENAME."""
-
-    SRC_DIR_STRIP_RE = re.compile(input_dir + "(/.*).txt")
-
-    # Set up a filename to row id table (anchors inside tables don't work in
-    # most browsers, but href's to table row ids do)
-    id_table = {}
-    id_count = 0
-    for value in file_hash:
-        for filename in value:
-             id_table[filename] = id_count
-        id_count += 1
-
-    # Open the output file, and output the header pieces
-    output_file = open(output_filename, "wb")
-
-    output_file.write(b"<html><head>\n")
-    output_file.write(HTML_OUTPUT_CSS)
-    output_file.write(b'</head><body topmargin="0" leftmargin="0" rightmargin="0" bottommargin="0">\n')
-
-    # Output our table of contents
-    output_file.write(b'<div class="toc">\n')
-    output_file.write(b"<ul>\n")
-
-    # Flatten the list of lists into a single list of filenames
-    sorted_filenames = sorted(itertools.chain.from_iterable(file_hash))
-
-    # Print out a nice table of contents
-    for filename in sorted_filenames:
-        stripped_filename = SRC_DIR_STRIP_RE.sub(r"\1", filename)
-        output_file.write(('<li><a href="#id%d">%s</a></li>\n' % (id_table.get(filename), stripped_filename)).encode())
-
-    output_file.write(b"</ul>\n")
-    output_file.write(b"</div><!-- table of contents -->\n")
-    # Output the individual notice file lists
-    output_file.write(b'<table cellpadding="0" cellspacing="0" border="0">\n')
-    for value in file_hash:
-        output_file.write(('<tr id="id%d"><td class="same-license">\n' % id_table.get(value[0])).encode())
-        output_file.write(b'<div class="label">Notices for file(s):</div>\n')
-        output_file.write(b'<div class="file-list">\n')
-        for filename in value:
-            output_file.write(("%s <br/>\n" % (SRC_DIR_STRIP_RE.sub(r"\1", filename))).encode())
-        output_file.write(b"</div><!-- file-list -->\n\n")
-        output_file.write(b'<pre class="license-text">\n')
-        with open(value[0], "rb") as notice_file:
-            output_file.write(html_escape(notice_file.read()))
-        output_file.write(b"\n</pre><!-- license-text -->\n")
-        output_file.write(b"</td></tr><!-- same-license -->\n\n\n\n")
-
-    # Finish off the file output
-    output_file.write(b"</table>\n")
-    output_file.write(b"</body></html>\n")
-    output_file.close()
-
-def combine_notice_files_text(file_hash, input_dir, output_filename, file_title):
-    """Combine notice files in FILE_HASH and output a text version to OUTPUT_FILENAME."""
-
-    SRC_DIR_STRIP_RE = re.compile(input_dir + "(/.*).txt")
-    output_file = open(output_filename, "wb")
-    output_file.write(file_title.encode())
-    output_file.write(b"\n")
-    for value in file_hash:
-        output_file.write(b"============================================================\n")
-        output_file.write(b"Notices for file(s):\n")
-        for filename in value:
-            output_file.write(SRC_DIR_STRIP_RE.sub(r"\1", filename).encode())
-            output_file.write(b"\n")
-        output_file.write(b"------------------------------------------------------------\n")
-        with open(value[0], "rb") as notice_file:
-            output_file.write(notice_file.read())
-            output_file.write(b"\n")
-    output_file.close()
-
-def combine_notice_files_xml(files_with_same_hash, input_dir, output_filename):
-    """Combine notice files in FILE_HASH and output a XML version to OUTPUT_FILENAME."""
-
-    SRC_DIR_STRIP_RE = re.compile(input_dir + "(/.*).txt")
-
-    # Set up a filename to row id table (anchors inside tables don't work in
-    # most browsers, but href's to table row ids do)
-    id_table = {}
-    for file_key, files in files_with_same_hash.items():
-        for filename in files:
-             id_table[filename] = file_key
-
-    # Open the output file, and output the header pieces
-    output_file = open(output_filename, "wb")
-
-    output_file.write(b'<?xml version="1.0" encoding="utf-8"?>\n')
-    output_file.write(b"<licenses>\n")
-
-    # Flatten the list of lists into a single list of filenames
-    sorted_filenames = sorted(list(id_table))
-
-    # Print out a nice table of contents
-    for filename in sorted_filenames:
-        stripped_filename = SRC_DIR_STRIP_RE.sub(r"\1", filename)
-        output_file.write(('<file-name contentId="%s">%s</file-name>\n' % (id_table.get(filename), stripped_filename)).encode())
-    output_file.write(b"\n\n")
-
-    processed_file_keys = []
-    # Output the individual notice file lists
-    for filename in sorted_filenames:
-        file_key = id_table.get(filename)
-        if file_key in processed_file_keys:
-            continue
-        processed_file_keys.append(file_key)
-
-        output_file.write(('<file-content contentId="%s"><![CDATA[' % file_key).encode())
-        with open(filename, "rb") as notice_file:
-            output_file.write(html_escape(notice_file.read()))
-        output_file.write(b"]]></file-content>\n\n")
-
-    # Finish off the file output
-    output_file.write(b"</licenses>\n")
-    output_file.close()
-
-def get_args():
-    parser = argparse.ArgumentParser()
-    parser.add_argument(
-        '--text-output', required=True,
-        help='The text output file path.')
-    parser.add_argument(
-        '--html-output',
-        help='The html output file path.')
-    parser.add_argument(
-        '--xml-output',
-        help='The xml output file path.')
-    parser.add_argument(
-        '-t', '--title', required=True,
-        help='The file title.')
-    parser.add_argument(
-        '-s', '--source-dir', required=True,
-        help='The directory containing notices.')
-    parser.add_argument(
-        '-i', '--included-subdirs', action='append',
-        help='The sub directories which should be included.')
-    parser.add_argument(
-        '-e', '--excluded-subdirs', action='append',
-        help='The sub directories which should be excluded.')
-    return parser.parse_args()
-
-def main(argv):
-    args = get_args()
-
-    txt_output_file = args.text_output
-    html_output_file = args.html_output
-    xml_output_file = args.xml_output
-    file_title = args.title
-    included_subdirs = []
-    excluded_subdirs = []
-    if args.included_subdirs is not None:
-        included_subdirs = args.included_subdirs
-    if args.excluded_subdirs is not None:
-        excluded_subdirs = args.excluded_subdirs
-
-    # Find all the notice files and md5 them
-    input_dir = os.path.normpath(args.source_dir)
-    files_with_same_hash = defaultdict(list)
-    for root, dir, files in os.walk(input_dir):
-        for file in files:
-            matched = True
-            if len(included_subdirs) > 0:
-                matched = False
-                for subdir in included_subdirs:
-                    if (root == (input_dir + '/' + subdir) or
-                        root.startswith(input_dir + '/' + subdir + '/')):
-                        matched = True
-                        break
-            elif len(excluded_subdirs) > 0:
-                for subdir in excluded_subdirs:
-                    if (root == (input_dir + '/' + subdir) or
-                        root.startswith(input_dir + '/' + subdir + '/')):
-                        matched = False
-                        break
-            if matched and file.endswith(".txt"):
-                filename = os.path.join(root, file)
-                file_md5sum = md5sum(filename)
-                files_with_same_hash[file_md5sum].append(filename)
-
-    filesets = [sorted(files_with_same_hash[md5]) for md5 in sorted(list(files_with_same_hash))]
-
-    combine_notice_files_text(filesets, input_dir, txt_output_file, file_title)
-
-    if html_output_file is not None:
-        combine_notice_files_html(filesets, input_dir, html_output_file)
-
-    if xml_output_file is not None:
-        combine_notice_files_xml(files_with_same_hash, input_dir, xml_output_file)
-
-if __name__ == "__main__":
-    main(sys.argv)
diff --git a/scripts/mergenotice.py b/scripts/mergenotice.py
deleted file mode 100755
index fe99073..0000000
--- a/scripts/mergenotice.py
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/usr/bin/env python
-#
-# Copyright (C) 2019 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-"""
-Merges input notice files to the output file while ignoring duplicated files
-This script shouldn't be confused with build/soong/scripts/generate-notice-files.py
-which is responsible for creating the final notice file for all artifacts
-installed. This script has rather limited scope; it is meant to create a merged
-notice file for a set of modules that are packaged together, e.g. in an APEX.
-The merged notice file does not reveal the individual files in the package.
-"""
-
-import sys
-import argparse
-
-def get_args():
-  parser = argparse.ArgumentParser(description='Merge notice files.')
-  parser.add_argument('--output', help='output file path.')
-  parser.add_argument('inputs', metavar='INPUT', nargs='+',
-                      help='input notice file')
-  return parser.parse_args()
-
-def main(argv):
-  args = get_args()
-
-  processed = set()
-  with open(args.output, 'w+') as output:
-    for input in args.inputs:
-      with open(input, 'r') as f:
-        data = f.read().strip()
-        if data not in processed:
-          processed.add(data)
-          output.write('%s\n\n' % data)
-
-if __name__ == '__main__':
-  main(sys.argv)