Merge "core & tools: system_dlkm: add dynamic partition"
diff --git a/core/Makefile b/core/Makefile
index 530e32d..b9103ab 100644
--- a/core/Makefile
+++ b/core/Makefile
@@ -3697,21 +3697,21 @@
 INTERNAL_PREBUILT_PVMFWIMAGE := packages/modules/Virtualization/pvmfw/pvmfw.img
 
 ifdef BOARD_PREBUILT_PVMFWIMAGE
-BUILT_PVMFWIMAGE_TARGET := $(BOARD_PREBUILT_PVMFWIMAGE)
-else ifeq ($(BUILDING_PVMFW_IMAGE),true)
-BUILT_PVMFWIMAGE_TARGET := $(INTERNAL_PREBUILT_PVMFWIMAGE)
+PREBUILT_PVMFWIMAGE_TARGET := $(BOARD_PREBUILT_PVMFWIMAGE)
+else
+PREBUILT_PVMFWIMAGE_TARGET := $(INTERNAL_PREBUILT_PVMFWIMAGE)
 endif
 
 ifeq ($(BOARD_AVB_ENABLE),true)
-$(INSTALLED_PVMFWIMAGE_TARGET): $(BUILT_PVMFWIMAGE_TARGET) $(AVBTOOL) $(BOARD_AVB_PVMFW_KEY_PATH)
-	cp $(BUILT_PVMFWIMAGE_TARGET) $@
+$(INSTALLED_PVMFWIMAGE_TARGET): $(PREBUILT_PVMFWIMAGE_TARGET) $(AVBTOOL) $(BOARD_AVB_PVMFW_KEY_PATH)
+	cp $< $@
 	$(AVBTOOL) add_hash_footer \
 	    --image $@ \
 	    --partition_size $(BOARD_PVMFWIMAGE_PARTITION_SIZE) \
 	    --partition_name pvmfw $(INTERNAL_AVB_PVMFW_SIGNING_ARGS) \
 	    $(BOARD_AVB_PVMFW_ADD_HASH_FOOTER_ARGS)
 else
-$(eval $(call copy-one-file,$(BUILT_PVMFWIMAGE_TARGET),$(INSTALLED_PVMFWIMAGE_TARGET)))
+$(eval $(call copy-one-file,$(PREBUILT_PVMFWIMAGE_TARGET),$(INSTALLED_PVMFWIMAGE_TARGET)))
 endif
 
 endif # BOARD_USES_PVMFWIMAGE
@@ -5622,12 +5622,9 @@
 	$(hide) mkdir -p $(zip_root)/PREBUILT_IMAGES
 	$(hide) cp $(INSTALLED_DTBOIMAGE_TARGET) $(zip_root)/PREBUILT_IMAGES/
 endif # BOARD_PREBUILT_DTBOIMAGE
-ifdef BOARD_PREBUILT_PVMFWIMAGE
+ifeq ($(BOARD_USES_PVMFWIMAGE),true)
 	$(hide) mkdir -p $(zip_root)/PREBUILT_IMAGES
 	$(hide) cp $(INSTALLED_PVMFWIMAGE_TARGET) $(zip_root)/PREBUILT_IMAGES/
-else ifeq ($(BUILDING_PVMFW_IMAGE),true)
-	$(hide) mkdir -p $(zip_root)/IMAGES
-	$(hide) cp $(INSTALLED_PVMFWIMAGE_TARGET) $(zip_root)/IMAGES/
 endif
 ifdef BOARD_PREBUILT_BOOTLOADER
 	$(hide) mkdir -p $(zip_root)/IMAGES
diff --git a/tools/compliance/Android.bp b/tools/compliance/Android.bp
index 7037670..e74986d 100644
--- a/tools/compliance/Android.bp
+++ b/tools/compliance/Android.bp
@@ -55,11 +55,21 @@
 blueprint_go_binary {
     name: "htmlnotice",
     srcs: ["cmd/htmlnotice/htmlnotice.go"],
-    deps: ["compliance-module"],
+    deps: [
+        "compliance-module",
+        "blueprint-deptools",
+    ],
     testSrcs: ["cmd/htmlnotice/htmlnotice_test.go"],
 }
 
 blueprint_go_binary {
+    name: "rtrace",
+    srcs: ["cmd/rtrace/rtrace.go"],
+    deps: ["compliance-module"],
+    testSrcs: ["cmd/rtrace/rtrace_test.go"],
+}
+
+blueprint_go_binary {
     name: "shippedlibs",
     srcs: ["cmd/shippedlibs/shippedlibs.go"],
     deps: ["compliance-module"],
@@ -69,7 +79,10 @@
 blueprint_go_binary {
     name: "textnotice",
     srcs: ["cmd/textnotice/textnotice.go"],
-    deps: ["compliance-module"],
+    deps: [
+        "compliance-module",
+        "blueprint-deptools",
+    ],
     testSrcs: ["cmd/textnotice/textnotice_test.go"],
 }
 
diff --git a/tools/compliance/cmd/dumpresolutions/dumpresolutions_test.go b/tools/compliance/cmd/dumpresolutions/dumpresolutions_test.go
index b698bf2..b7ccdc5 100644
--- a/tools/compliance/cmd/dumpresolutions/dumpresolutions_test.go
+++ b/tools/compliance/cmd/dumpresolutions/dumpresolutions_test.go
@@ -919,7 +919,7 @@
 				for len(outList) > startLine && len(expectedList) > startLine && outList[startLine] == expectedList[startLine] {
 					startLine++
 				}
-				t.Errorf("listshare: gotStdout = %v, want %v, somewhere near line %d Stdout = %v, want %v",
+				t.Errorf("dumpresoliutions: gotStdout = %v, want %v, somewhere near line %d Stdout = %v, want %v",
 					out, expected, startLine+1, outList[startLine], expectedList[startLine])
 			}
 		})
diff --git a/tools/compliance/cmd/htmlnotice/htmlnotice.go b/tools/compliance/cmd/htmlnotice/htmlnotice.go
index 2f59ee0..6363acf 100644
--- a/tools/compliance/cmd/htmlnotice/htmlnotice.go
+++ b/tools/compliance/cmd/htmlnotice/htmlnotice.go
@@ -16,6 +16,7 @@
 
 import (
 	"bytes"
+	"compress/gzip"
 	"flag"
 	"fmt"
 	"html"
@@ -26,10 +27,13 @@
 	"strings"
 
 	"android/soong/tools/compliance"
+
+	"github.com/google/blueprint/deptools"
 )
 
 var (
 	outputFile  = flag.String("o", "-", "Where to write the NOTICE text file. (default stdout)")
+	depsFile    = flag.String("d", "", "Where to write the deps file")
 	includeTOC  = flag.Bool("toc", true, "Whether to include a table of contents.")
 	stripPrefix = flag.String("strip_prefix", "", "Prefix to remove from paths. i.e. path to root")
 	title       = flag.String("title", "", "The title of the notice file.")
@@ -45,13 +49,15 @@
 	includeTOC  bool
 	stripPrefix string
 	title       string
+	deps        *[]string
 }
 
 func init() {
 	flag.Usage = func() {
 		fmt.Fprintf(os.Stderr, `Usage: %s {options} file.meta_lic {file.meta_lic...}
 
-Outputs an html NOTICE.html file.
+Outputs an html NOTICE.html or gzipped NOTICE.html.gz file if the -o filename
+ends with ".gz".
 
 Options:
 `, filepath.Base(os.Args[0]))
@@ -90,12 +96,21 @@
 	}
 
 	var ofile io.Writer
+	var closer io.Closer
 	ofile = os.Stdout
+	var obuf *bytes.Buffer
 	if *outputFile != "-" {
-		ofile = &bytes.Buffer{}
+		obuf = &bytes.Buffer{}
+		ofile = obuf
+	}
+	if strings.HasSuffix(*outputFile, ".gz") {
+		ofile, _ = gzip.NewWriterLevel(obuf, gzip.BestCompression)
+		closer = ofile.(io.Closer)
 	}
 
-	ctx := &context{ofile, os.Stderr, os.DirFS("."), *includeTOC, *stripPrefix, *title}
+	var deps []string
+
+	ctx := &context{ofile, os.Stderr, os.DirFS("."), *includeTOC, *stripPrefix, *title, &deps}
 
 	err := htmlNotice(ctx, flag.Args()...)
 	if err != nil {
@@ -105,13 +120,24 @@
 		fmt.Fprintf(os.Stderr, "%s\n", err.Error())
 		os.Exit(1)
 	}
+	if closer != nil {
+		closer.Close()
+	}
+
 	if *outputFile != "-" {
-		err := os.WriteFile(*outputFile, ofile.(*bytes.Buffer).Bytes(), 0666)
+		err := os.WriteFile(*outputFile, obuf.Bytes(), 0666)
 		if err != nil {
 			fmt.Fprintf(os.Stderr, "could not write output to %q: %s\n", *outputFile, err)
 			os.Exit(1)
 		}
 	}
+	if *depsFile != "" {
+		err := deptools.WriteDepFile(*depsFile, *outputFile, deps)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "could not write deps to %q: %s\n", *depsFile, err)
+			os.Exit(1)
+		}
+	}
 	os.Exit(0)
 }
 
@@ -213,5 +239,7 @@
 	}
 	fmt.Fprintln(ctx.stdout, "</body></html>")
 
+	*ctx.deps = ni.InputNoticeFiles()
+
 	return nil
 }
diff --git a/tools/compliance/cmd/htmlnotice/htmlnotice_test.go b/tools/compliance/cmd/htmlnotice/htmlnotice_test.go
index fc935c1..4e9c43c 100644
--- a/tools/compliance/cmd/htmlnotice/htmlnotice_test.go
+++ b/tools/compliance/cmd/htmlnotice/htmlnotice_test.go
@@ -20,6 +20,7 @@
 	"fmt"
 	"html"
 	"os"
+	"reflect"
 	"regexp"
 	"strings"
 	"testing"
@@ -51,13 +52,14 @@
 
 func Test(t *testing.T) {
 	tests := []struct {
-		condition   string
-		name        string
-		roots       []string
-		includeTOC  bool
-		stripPrefix string
-		title       string
-		expectedOut []matcher
+		condition    string
+		name         string
+		roots        []string
+		includeTOC   bool
+		stripPrefix  string
+		title        string
+		expectedOut  []matcher
+		expectedDeps []string
 	}{
 		{
 			condition: "firstparty",
@@ -73,6 +75,7 @@
 				usedBy{"highest.apex/lib/libb.so"},
 				firstParty{},
 			},
+			expectedDeps: []string{"testdata/firstparty/FIRST_PARTY_LICENSE"},
 		},
 		{
 			condition:  "firstparty",
@@ -100,6 +103,7 @@
 				usedBy{"highest.apex/lib/libb.so"},
 				firstParty{},
 			},
+			expectedDeps: []string{"testdata/firstparty/FIRST_PARTY_LICENSE"},
 		},
 		{
 			condition: "firstparty",
@@ -117,6 +121,7 @@
 				usedBy{"highest.apex/lib/libb.so"},
 				firstParty{},
 			},
+			expectedDeps: []string{"testdata/firstparty/FIRST_PARTY_LICENSE"},
 		},
 		{
 			condition:  "firstparty",
@@ -146,6 +151,7 @@
 				usedBy{"highest.apex/lib/libb.so"},
 				firstParty{},
 			},
+			expectedDeps: []string{"testdata/firstparty/FIRST_PARTY_LICENSE"},
 		},
 		{
 			condition: "firstparty",
@@ -161,6 +167,7 @@
 				usedBy{"container.zip/libb.so"},
 				firstParty{},
 			},
+			expectedDeps: []string{"testdata/firstparty/FIRST_PARTY_LICENSE"},
 		},
 		{
 			condition: "firstparty",
@@ -172,6 +179,7 @@
 				usedBy{"application"},
 				firstParty{},
 			},
+			expectedDeps: []string{"testdata/firstparty/FIRST_PARTY_LICENSE"},
 		},
 		{
 			condition: "firstparty",
@@ -183,6 +191,7 @@
 				usedBy{"bin/bin1"},
 				firstParty{},
 			},
+			expectedDeps: []string{"testdata/firstparty/FIRST_PARTY_LICENSE"},
 		},
 		{
 			condition: "firstparty",
@@ -194,6 +203,7 @@
 				usedBy{"lib/libd.so"},
 				firstParty{},
 			},
+			expectedDeps: []string{"testdata/firstparty/FIRST_PARTY_LICENSE"},
 		},
 		{
 			condition: "notice",
@@ -215,6 +225,10 @@
 				usedBy{"highest.apex/bin/bin1"},
 				notice{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/notice/NOTICE_LICENSE",
+			},
 		},
 		{
 			condition: "notice",
@@ -236,6 +250,10 @@
 				usedBy{"container.zip/bin1"},
 				notice{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/notice/NOTICE_LICENSE",
+			},
 		},
 		{
 			condition: "notice",
@@ -251,6 +269,10 @@
 				usedBy{"application"},
 				notice{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/notice/NOTICE_LICENSE",
+			},
 		},
 		{
 			condition: "notice",
@@ -268,6 +290,10 @@
 				usedBy{"bin/bin1"},
 				notice{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/notice/NOTICE_LICENSE",
+			},
 		},
 		{
 			condition: "notice",
@@ -279,6 +305,7 @@
 				usedBy{"lib/libd.so"},
 				notice{},
 			},
+			expectedDeps: []string{"testdata/notice/NOTICE_LICENSE"},
 		},
 		{
 			condition: "reciprocal",
@@ -300,6 +327,10 @@
 				usedBy{"highest.apex/bin/bin1"},
 				reciprocal{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/reciprocal/RECIPROCAL_LICENSE",
+			},
 		},
 		{
 			condition: "reciprocal",
@@ -321,6 +352,10 @@
 				usedBy{"container.zip/bin1"},
 				reciprocal{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/reciprocal/RECIPROCAL_LICENSE",
+			},
 		},
 		{
 			condition: "reciprocal",
@@ -336,6 +371,10 @@
 				usedBy{"application"},
 				reciprocal{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/reciprocal/RECIPROCAL_LICENSE",
+			},
 		},
 		{
 			condition: "reciprocal",
@@ -353,6 +392,10 @@
 				usedBy{"bin/bin1"},
 				reciprocal{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/reciprocal/RECIPROCAL_LICENSE",
+			},
 		},
 		{
 			condition: "reciprocal",
@@ -364,6 +407,7 @@
 				usedBy{"lib/libd.so"},
 				notice{},
 			},
+			expectedDeps: []string{"testdata/notice/NOTICE_LICENSE"},
 		},
 		{
 			condition: "restricted",
@@ -389,6 +433,11 @@
 				usedBy{"highest.apex/bin/bin1"},
 				reciprocal{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/reciprocal/RECIPROCAL_LICENSE",
+				"testdata/restricted/RESTRICTED_LICENSE",
+			},
 		},
 		{
 			condition: "restricted",
@@ -414,6 +463,11 @@
 				usedBy{"container.zip/bin1"},
 				reciprocal{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/reciprocal/RECIPROCAL_LICENSE",
+				"testdata/restricted/RESTRICTED_LICENSE",
+			},
 		},
 		{
 			condition: "restricted",
@@ -429,6 +483,10 @@
 				usedBy{"application"},
 				restricted{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/restricted/RESTRICTED_LICENSE",
+			},
 		},
 		{
 			condition: "restricted",
@@ -448,6 +506,11 @@
 				usedBy{"bin/bin1"},
 				reciprocal{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/reciprocal/RECIPROCAL_LICENSE",
+				"testdata/restricted/RESTRICTED_LICENSE",
+			},
 		},
 		{
 			condition: "restricted",
@@ -459,6 +522,7 @@
 				usedBy{"lib/libd.so"},
 				notice{},
 			},
+			expectedDeps: []string{"testdata/notice/NOTICE_LICENSE"},
 		},
 		{
 			condition: "proprietary",
@@ -485,6 +549,11 @@
 				usedBy{"highest.apex/bin/bin1"},
 				proprietary{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/proprietary/PROPRIETARY_LICENSE",
+				"testdata/restricted/RESTRICTED_LICENSE",
+			},
 		},
 		{
 			condition: "proprietary",
@@ -511,6 +580,11 @@
 				usedBy{"container.zip/bin1"},
 				proprietary{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/proprietary/PROPRIETARY_LICENSE",
+				"testdata/restricted/RESTRICTED_LICENSE",
+			},
 		},
 		{
 			condition: "proprietary",
@@ -526,6 +600,10 @@
 				usedBy{"application"},
 				proprietary{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/proprietary/PROPRIETARY_LICENSE",
+			},
 		},
 		{
 			condition: "proprietary",
@@ -543,6 +621,10 @@
 				usedBy{"bin/bin1"},
 				proprietary{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/proprietary/PROPRIETARY_LICENSE",
+			},
 		},
 		{
 			condition: "proprietary",
@@ -554,6 +636,7 @@
 				usedBy{"lib/libd.so"},
 				notice{},
 			},
+			expectedDeps: []string{"testdata/notice/NOTICE_LICENSE"},
 		},
 	}
 	for _, tt := range tests {
@@ -566,7 +649,9 @@
 				rootFiles = append(rootFiles, "testdata/"+tt.condition+"/"+r)
 			}
 
-			ctx := context{stdout, stderr, os.DirFS("."), tt.includeTOC, tt.stripPrefix, tt.title}
+			var deps []string
+
+			ctx := context{stdout, stderr, os.DirFS("."), tt.includeTOC, tt.stripPrefix, tt.title, &deps}
 
 			err := htmlNotice(&ctx, rootFiles...)
 			if err != nil {
@@ -625,6 +710,15 @@
 			for ; lineno < len(tt.expectedOut); lineno++ {
 				t.Errorf("htmlnotice: missing output line %d: ended early, want %q", lineno+1, tt.expectedOut[lineno].String())
 			}
+
+			t.Logf("got deps: %q", deps)
+
+			t.Logf("want deps: %q", tt.expectedDeps)
+
+			if g, w := deps, tt.expectedDeps; !reflect.DeepEqual(g, w) {
+				t.Errorf("unexpected deps, wanted:\n%s\ngot:\n%s\n",
+					strings.Join(w, "\n"), strings.Join(g, "\n"))
+			}
 		})
 	}
 }
diff --git a/tools/compliance/cmd/rtrace/rtrace.go b/tools/compliance/cmd/rtrace/rtrace.go
new file mode 100644
index 0000000..a2ff594
--- /dev/null
+++ b/tools/compliance/cmd/rtrace/rtrace.go
@@ -0,0 +1,185 @@
+// Copyright 2021 Google LLC
+//
+// 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.
+
+package main
+
+import (
+	"flag"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+
+	"android/soong/tools/compliance"
+)
+
+var (
+	sources         = newMultiString("rtrace", "Projects or metadata files to trace back from. (required; multiple allowed)")
+	stripPrefix     = flag.String("strip_prefix", "", "Prefix to remove from paths. i.e. path to root")
+
+	failNoneRequested = fmt.Errorf("\nNo license metadata files requested")
+	failNoSources     = fmt.Errorf("\nNo projects or metadata files to trace back from")
+	failNoLicenses    = fmt.Errorf("No licenses found")
+)
+
+type context struct {
+	sources         []string
+	stripPrefix     string
+}
+
+func init() {
+	flag.Usage = func() {
+		fmt.Fprintf(os.Stderr, `Usage: %s {options} file.meta_lic {file.meta_lic...}
+
+Outputs a space-separated Target ActsOn Origin Condition tuple for each
+resolution in the graph. When -dot flag given, outputs nodes and edges
+in graphviz directed graph format.
+
+If one or more '-c condition' conditions are given, outputs the
+resolution for the union of the conditions. Otherwise, outputs the
+resolution for all conditions.
+
+In plain text mode, when '-label_conditions' is requested, the Target
+and Origin have colon-separated license conditions appended:
+i.e. target:condition1:condition2 etc.
+
+Options:
+`, filepath.Base(os.Args[0]))
+		flag.PrintDefaults()
+	}
+}
+
+// newMultiString creates a flag that allows multiple values in an array.
+func newMultiString(name, usage string) *multiString {
+	var f multiString
+	flag.Var(&f, name, usage)
+	return &f
+}
+
+// multiString implements the flag `Value` interface for multiple strings.
+type multiString []string
+
+func (ms *multiString) String() string     { return strings.Join(*ms, ", ") }
+func (ms *multiString) Set(s string) error { *ms = append(*ms, s); return nil }
+
+func main() {
+	flag.Parse()
+
+	// Must specify at least one root target.
+	if flag.NArg() == 0 {
+		flag.Usage()
+		os.Exit(2)
+	}
+
+	if len(*sources) == 0 {
+		flag.Usage()
+		fmt.Fprintf(os.Stderr, "\nMust specify at least 1 --rtrace source.\n")
+		os.Exit(2)
+	}
+
+	ctx := &context{
+		sources:         *sources,
+		stripPrefix:     *stripPrefix,
+	}
+	_, err := traceRestricted(ctx, os.Stdout, os.Stderr, flag.Args()...)
+	if err != nil {
+		if err == failNoneRequested {
+			flag.Usage()
+		}
+		fmt.Fprintf(os.Stderr, "%s\n", err.Error())
+		os.Exit(1)
+	}
+	os.Exit(0)
+}
+
+// traceRestricted implements the rtrace utility.
+func traceRestricted(ctx *context, stdout, stderr io.Writer, files ...string) (*compliance.LicenseGraph, error) {
+	if len(files) < 1 {
+		return nil, failNoneRequested
+	}
+
+	if len(ctx.sources) < 1 {
+		return nil, failNoSources
+	}
+
+	// Read the license graph from the license metadata files (*.meta_lic).
+	licenseGraph, err := compliance.ReadLicenseGraph(os.DirFS("."), stderr, files)
+	if err != nil {
+		return nil, fmt.Errorf("Unable to read license metadata file(s) %q: %v\n", files, err)
+	}
+	if licenseGraph == nil {
+		return nil, failNoLicenses
+	}
+
+	sourceMap := make(map[string]struct{})
+	for _, source := range ctx.sources {
+		sourceMap[source] = struct{}{}
+	}
+
+	compliance.TraceTopDownConditions(licenseGraph, func(tn *compliance.TargetNode) compliance.LicenseConditionSet {
+		if _, isPresent := sourceMap[tn.Name()]; isPresent {
+			return compliance.ImpliesRestricted
+		}
+		for _, project := range tn.Projects() {
+			if _, isPresent := sourceMap[project]; isPresent {
+				return compliance.ImpliesRestricted
+			}
+		}
+		return compliance.NewLicenseConditionSet()
+	})
+
+	// targetOut calculates the string to output for `target` adding `sep`-separated conditions as needed.
+	targetOut := func(target *compliance.TargetNode, sep string) string {
+		tOut := strings.TrimPrefix(target.Name(), ctx.stripPrefix)
+		return tOut
+	}
+
+	// outputResolution prints a resolution in the requested format to `stdout`, where one can read
+	// a resolution as `tname` resolves conditions named in `cnames`.
+	// `tname` is the name of the target the resolution traces back to.
+	// `cnames` is the list of conditions to resolve.
+	outputResolution := func(tname string, cnames []string) {
+		// ... one edge per line with names in a colon-separated tuple.
+		fmt.Fprintf(stdout, "%s %s\n", tname, strings.Join(cnames, ":"))
+	}
+
+	// Sort the resolutions by targetname for repeatability/stability.
+	actions := compliance.WalkResolutionsForCondition(licenseGraph, compliance.ImpliesShared).AllActions()
+	targets := make(compliance.TargetNodeList, 0, len(actions))
+	for tn := range actions {
+		if tn.LicenseConditions().MatchesAnySet(compliance.ImpliesRestricted) {
+			targets = append(targets, tn)
+		}
+	}
+	sort.Sort(targets)
+
+	// Output the sorted targets.
+	for _, target := range targets {
+		var tname string
+		tname = targetOut(target, ":")
+
+		// cnames accumulates the list of condition names originating at a single origin that apply to `target`.
+		cnames := target.LicenseConditions().Names()
+
+		// Output 1 line for each attachesTo+actsOn combination.
+		outputResolution(tname, cnames)
+	}
+	fmt.Fprintf(stdout, "restricted conditions trace to %d targets\n", len(targets))
+	if 0 == len(targets) {
+		fmt.Fprintln(stdout, "  (check for typos in project names or metadata files)")
+	}
+	return licenseGraph, nil
+}
diff --git a/tools/compliance/cmd/rtrace/rtrace_test.go b/tools/compliance/cmd/rtrace/rtrace_test.go
new file mode 100644
index 0000000..658aa96
--- /dev/null
+++ b/tools/compliance/cmd/rtrace/rtrace_test.go
@@ -0,0 +1,316 @@
+// Copyright 2021 Google LLC
+//
+// 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.
+
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"os"
+	"strings"
+	"testing"
+)
+
+func TestMain(m *testing.M) {
+	// Change into the parent directory before running the tests
+	// so they can find the testdata directory.
+	if err := os.Chdir(".."); err != nil {
+		fmt.Printf("failed to change to testdata directory: %s\n", err)
+		os.Exit(1)
+	}
+	os.Exit(m.Run())
+}
+
+func Test_plaintext(t *testing.T) {
+	tests := []struct {
+		condition   string
+		name        string
+		roots       []string
+		ctx         context
+		expectedOut []string
+	}{
+		{
+			condition: "firstparty",
+			name:      "apex",
+			roots:     []string{"highest.apex.meta_lic"},
+			expectedOut: []string{},
+		},
+		{
+			condition: "firstparty",
+			name:      "apex_trimmed",
+			roots:     []string{"highest.apex.meta_lic"},
+			ctx: context{
+				sources:     []string{"testdata/firstparty/bin/bin1.meta_lic"},
+				stripPrefix: "testdata/firstparty/",
+			},
+			expectedOut: []string{},
+		},
+		{
+			condition: "firstparty",
+			name:      "container",
+			roots:     []string{"container.zip.meta_lic"},
+			expectedOut: []string{},
+		},
+		{
+			condition: "firstparty",
+			name:      "application",
+			roots:     []string{"application.meta_lic"},
+			expectedOut: []string{},
+		},
+		{
+			condition: "firstparty",
+			name:      "binary",
+			roots:     []string{"bin/bin1.meta_lic"},
+			expectedOut: []string{},
+		},
+		{
+			condition: "firstparty",
+			name:      "library",
+			roots:     []string{"lib/libd.so.meta_lic"},
+			expectedOut: []string{},
+		},
+		{
+			condition: "notice",
+			name:      "apex",
+			roots:     []string{"highest.apex.meta_lic"},
+			expectedOut: []string{},
+		},
+		{
+			condition: "notice",
+			name:      "apex_trimmed",
+			roots:     []string{"highest.apex.meta_lic"},
+			ctx: context{
+				sources:     []string{"testdata/notice/bin/bin1.meta_lic"},
+				stripPrefix: "testdata/notice/",
+			},
+			expectedOut: []string{},
+		},
+		{
+			condition: "notice",
+			name:      "container",
+			roots:     []string{"container.zip.meta_lic"},
+			expectedOut: []string{},
+		},
+		{
+			condition: "notice",
+			name:      "application",
+			roots:     []string{"application.meta_lic"},
+			expectedOut: []string{},
+		},
+		{
+			condition: "notice",
+			name:      "binary",
+			roots:     []string{"bin/bin1.meta_lic"},
+			expectedOut: []string{},
+		},
+		{
+			condition: "notice",
+			name:      "library",
+			roots:     []string{"lib/libd.so.meta_lic"},
+			expectedOut: []string{},
+		},
+		{
+			condition: "reciprocal",
+			name:      "apex",
+			roots:     []string{"highest.apex.meta_lic"},
+			expectedOut: []string{},
+		},
+		{
+			condition: "reciprocal",
+			name:      "apex_trimmed",
+			roots:     []string{"highest.apex.meta_lic"},
+			ctx: context{
+				sources:     []string{"testdata/reciprocal/bin/bin1.meta_lic"},
+				stripPrefix: "testdata/reciprocal/",
+			},
+			expectedOut: []string{},
+		},
+		{
+			condition: "reciprocal",
+			name:      "container",
+			roots:     []string{"container.zip.meta_lic"},
+			expectedOut: []string{},
+		},
+		{
+			condition: "reciprocal",
+			name:      "application",
+			roots:     []string{"application.meta_lic"},
+			expectedOut: []string{},
+		},
+		{
+			condition: "reciprocal",
+			name:      "binary",
+			roots:     []string{"bin/bin1.meta_lic"},
+			expectedOut: []string{},
+		},
+		{
+			condition: "reciprocal",
+			name:      "library",
+			roots:     []string{"lib/libd.so.meta_lic"},
+			expectedOut: []string{},
+		},
+		{
+			condition: "restricted",
+			name:      "apex",
+			roots:     []string{"highest.apex.meta_lic"},
+			expectedOut: []string{
+				"testdata/restricted/lib/liba.so.meta_lic restricted_allows_dynamic_linking",
+				"testdata/restricted/lib/libb.so.meta_lic restricted",
+			},
+		},
+		{
+			condition: "restricted",
+			name:      "apex_trimmed_bin1",
+			roots:     []string{"highest.apex.meta_lic"},
+			ctx: context{
+				sources:     []string{"testdata/restricted/bin/bin1.meta_lic"},
+				stripPrefix: "testdata/restricted/",
+			},
+			expectedOut: []string{"lib/liba.so.meta_lic restricted_allows_dynamic_linking"},
+		},
+		{
+			condition: "restricted",
+			name:      "apex_trimmed_bin2",
+			roots:     []string{"highest.apex.meta_lic"},
+			ctx: context{
+				sources:     []string{"testdata/restricted/bin/bin2.meta_lic"},
+				stripPrefix: "testdata/restricted/",
+			},
+			expectedOut: []string{"lib/libb.so.meta_lic restricted"},
+		},
+		{
+			condition: "restricted",
+			name:      "container",
+			roots:     []string{"container.zip.meta_lic"},
+			expectedOut: []string{
+				"testdata/restricted/lib/liba.so.meta_lic restricted_allows_dynamic_linking",
+				"testdata/restricted/lib/libb.so.meta_lic restricted",
+			},
+		},
+		{
+			condition: "restricted",
+			name:      "application",
+			roots:     []string{"application.meta_lic"},
+			expectedOut: []string{"testdata/restricted/lib/liba.so.meta_lic restricted_allows_dynamic_linking"},
+		},
+		{
+			condition: "restricted",
+			name:      "binary",
+			roots:     []string{"bin/bin1.meta_lic"},
+			expectedOut: []string{"testdata/restricted/lib/liba.so.meta_lic restricted_allows_dynamic_linking"},
+		},
+		{
+			condition: "restricted",
+			name:      "library",
+			roots:     []string{"lib/libd.so.meta_lic"},
+			expectedOut: []string{},
+		},
+		{
+			condition: "proprietary",
+			name:      "apex",
+			roots:     []string{"highest.apex.meta_lic"},
+			expectedOut: []string{"testdata/proprietary/lib/libb.so.meta_lic restricted"},
+		},
+		{
+			condition: "proprietary",
+			name:      "apex_trimmed_bin1",
+			roots:     []string{"highest.apex.meta_lic"},
+			ctx: context{
+				sources:     []string{"testdata/proprietary/bin/bin1.meta_lic"},
+				stripPrefix: "testdata/proprietary/",
+			},
+			expectedOut: []string{},
+		},
+		{
+			condition: "proprietary",
+			name:      "apex_trimmed_bin2",
+			roots:     []string{"highest.apex.meta_lic"},
+			ctx: context{
+				sources:     []string{"testdata/proprietary/bin/bin2.meta_lic"},
+				stripPrefix: "testdata/proprietary/",
+			},
+			expectedOut: []string{"lib/libb.so.meta_lic restricted"},
+		},
+		{
+			condition: "proprietary",
+			name:      "container",
+			roots:     []string{"container.zip.meta_lic"},
+			expectedOut: []string{"testdata/proprietary/lib/libb.so.meta_lic restricted"},
+		},
+		{
+			condition: "proprietary",
+			name:      "application",
+			roots:     []string{"application.meta_lic"},
+			expectedOut: []string{},
+		},
+		{
+			condition: "proprietary",
+			name:      "binary",
+			roots:     []string{"bin/bin1.meta_lic"},
+			expectedOut: []string{},
+		},
+		{
+			condition: "proprietary",
+			name:      "library",
+			roots:     []string{"lib/libd.so.meta_lic"},
+			expectedOut: []string{},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.condition+" "+tt.name, func(t *testing.T) {
+			expectedOut := &bytes.Buffer{}
+			for _, eo := range tt.expectedOut {
+				expectedOut.WriteString(eo)
+				expectedOut.WriteString("\n")
+			}
+			fmt.Fprintf(expectedOut, "restricted conditions trace to %d targets\n", len(tt.expectedOut))
+			if 0 == len(tt.expectedOut) {
+				fmt.Fprintln(expectedOut, "  (check for typos in project names or metadata files)")
+			}
+
+			stdout := &bytes.Buffer{}
+			stderr := &bytes.Buffer{}
+
+			rootFiles := make([]string, 0, len(tt.roots))
+			for _, r := range tt.roots {
+				rootFiles = append(rootFiles, "testdata/"+tt.condition+"/"+r)
+			}
+			if len(tt.ctx.sources) < 1 {
+				tt.ctx.sources = rootFiles
+			}
+			_, err := traceRestricted(&tt.ctx, stdout, stderr, rootFiles...)
+			t.Logf("rtrace: stderr = %v", stderr)
+			t.Logf("rtrace: stdout = %v", stdout)
+			if err != nil {
+				t.Fatalf("rtrace: error = %v", err)
+				return
+			}
+			if stderr.Len() > 0 {
+				t.Errorf("rtrace: gotStderr = %v, want none", stderr)
+			}
+			out := stdout.String()
+			expected := expectedOut.String()
+			if out != expected {
+				outList := strings.Split(out, "\n")
+				expectedList := strings.Split(expected, "\n")
+				startLine := 0
+				for startLine < len(outList) && startLine < len(expectedList) && outList[startLine] == expectedList[startLine] {
+					startLine++
+				}
+				t.Errorf("rtrace: gotStdout = %v, want %v, somewhere near line %d Stdout = %v, want %v",
+					out, expected, startLine+1, outList[startLine], expectedList[startLine])
+			}
+		})
+	}
+}
diff --git a/tools/compliance/cmd/textnotice/textnotice.go b/tools/compliance/cmd/textnotice/textnotice.go
index b89aff1..eebb13d 100644
--- a/tools/compliance/cmd/textnotice/textnotice.go
+++ b/tools/compliance/cmd/textnotice/textnotice.go
@@ -25,10 +25,13 @@
 	"strings"
 
 	"android/soong/tools/compliance"
+
+	"github.com/google/blueprint/deptools"
 )
 
 var (
 	outputFile  = flag.String("o", "-", "Where to write the NOTICE text file. (default stdout)")
+	depsFile    = flag.String("d", "", "Where to write the deps file")
 	stripPrefix = flag.String("strip_prefix", "", "Prefix to remove from paths. i.e. path to root")
 
 	failNoneRequested = fmt.Errorf("\nNo license metadata files requested")
@@ -40,6 +43,7 @@
 	stderr      io.Writer
 	rootFS      fs.FS
 	stripPrefix string
+	deps        *[]string
 }
 
 func init() {
@@ -90,7 +94,9 @@
 		ofile = &bytes.Buffer{}
 	}
 
-	ctx := &context{ofile, os.Stderr, os.DirFS("."), *stripPrefix}
+	var deps []string
+
+	ctx := &context{ofile, os.Stderr, os.DirFS("."), *stripPrefix, &deps}
 
 	err := textNotice(ctx, flag.Args()...)
 	if err != nil {
@@ -107,6 +113,13 @@
 			os.Exit(1)
 		}
 	}
+	if *depsFile != "" {
+		err := deptools.WriteDepFile(*depsFile, *outputFile, deps)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "could not write deps to %q: %s\n", *depsFile, err)
+			os.Exit(1)
+		}
+	}
 	os.Exit(0)
 }
 
@@ -150,5 +163,8 @@
 		ctx.stdout.Write(ni.HashText(h))
 		fmt.Fprintln(ctx.stdout)
 	}
+
+	*ctx.deps = ni.InputNoticeFiles()
+
 	return nil
 }
diff --git a/tools/compliance/cmd/textnotice/textnotice_test.go b/tools/compliance/cmd/textnotice/textnotice_test.go
index 7993532..39f711d 100644
--- a/tools/compliance/cmd/textnotice/textnotice_test.go
+++ b/tools/compliance/cmd/textnotice/textnotice_test.go
@@ -19,6 +19,7 @@
 	"bytes"
 	"fmt"
 	"os"
+	"reflect"
 	"regexp"
 	"strings"
 	"testing"
@@ -40,11 +41,12 @@
 
 func Test(t *testing.T) {
 	tests := []struct {
-		condition   string
-		name        string
-		roots       []string
-		stripPrefix string
-		expectedOut []matcher
+		condition    string
+		name         string
+		roots        []string
+		stripPrefix  string
+		expectedOut  []matcher
+		expectedDeps []string
 	}{
 		{
 			condition: "firstparty",
@@ -60,6 +62,7 @@
 				usedBy{"highest.apex/lib/libb.so"},
 				firstParty{},
 			},
+			expectedDeps: []string{"testdata/firstparty/FIRST_PARTY_LICENSE"},
 		},
 		{
 			condition: "firstparty",
@@ -75,6 +78,7 @@
 				usedBy{"container.zip/libb.so"},
 				firstParty{},
 			},
+			expectedDeps: []string{"testdata/firstparty/FIRST_PARTY_LICENSE"},
 		},
 		{
 			condition: "firstparty",
@@ -86,6 +90,7 @@
 				usedBy{"application"},
 				firstParty{},
 			},
+			expectedDeps: []string{"testdata/firstparty/FIRST_PARTY_LICENSE"},
 		},
 		{
 			condition: "firstparty",
@@ -97,6 +102,7 @@
 				usedBy{"bin/bin1"},
 				firstParty{},
 			},
+			expectedDeps: []string{"testdata/firstparty/FIRST_PARTY_LICENSE"},
 		},
 		{
 			condition: "firstparty",
@@ -108,6 +114,7 @@
 				usedBy{"lib/libd.so"},
 				firstParty{},
 			},
+			expectedDeps: []string{"testdata/firstparty/FIRST_PARTY_LICENSE"},
 		},
 		{
 			condition: "notice",
@@ -129,6 +136,10 @@
 				usedBy{"highest.apex/bin/bin1"},
 				notice{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/notice/NOTICE_LICENSE",
+			},
 		},
 		{
 			condition: "notice",
@@ -150,6 +161,10 @@
 				usedBy{"container.zip/bin1"},
 				notice{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/notice/NOTICE_LICENSE",
+			},
 		},
 		{
 			condition: "notice",
@@ -165,6 +180,10 @@
 				usedBy{"application"},
 				notice{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/notice/NOTICE_LICENSE",
+			},
 		},
 		{
 			condition: "notice",
@@ -182,6 +201,10 @@
 				usedBy{"bin/bin1"},
 				notice{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/notice/NOTICE_LICENSE",
+			},
 		},
 		{
 			condition: "notice",
@@ -193,6 +216,7 @@
 				usedBy{"lib/libd.so"},
 				notice{},
 			},
+			expectedDeps: []string{"testdata/notice/NOTICE_LICENSE"},
 		},
 		{
 			condition: "reciprocal",
@@ -214,6 +238,10 @@
 				usedBy{"highest.apex/bin/bin1"},
 				reciprocal{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/reciprocal/RECIPROCAL_LICENSE",
+			},
 		},
 		{
 			condition: "reciprocal",
@@ -235,6 +263,10 @@
 				usedBy{"container.zip/bin1"},
 				reciprocal{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/reciprocal/RECIPROCAL_LICENSE",
+			},
 		},
 		{
 			condition: "reciprocal",
@@ -250,6 +282,10 @@
 				usedBy{"application"},
 				reciprocal{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/reciprocal/RECIPROCAL_LICENSE",
+			},
 		},
 		{
 			condition: "reciprocal",
@@ -267,6 +303,10 @@
 				usedBy{"bin/bin1"},
 				reciprocal{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/reciprocal/RECIPROCAL_LICENSE",
+			},
 		},
 		{
 			condition: "reciprocal",
@@ -278,6 +318,9 @@
 				usedBy{"lib/libd.so"},
 				notice{},
 			},
+			expectedDeps: []string{
+				"testdata/notice/NOTICE_LICENSE",
+			},
 		},
 		{
 			condition: "restricted",
@@ -303,6 +346,11 @@
 				usedBy{"highest.apex/bin/bin1"},
 				reciprocal{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/reciprocal/RECIPROCAL_LICENSE",
+				"testdata/restricted/RESTRICTED_LICENSE",
+			},
 		},
 		{
 			condition: "restricted",
@@ -328,6 +376,11 @@
 				usedBy{"container.zip/bin1"},
 				reciprocal{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/reciprocal/RECIPROCAL_LICENSE",
+				"testdata/restricted/RESTRICTED_LICENSE",
+			},
 		},
 		{
 			condition: "restricted",
@@ -343,6 +396,10 @@
 				usedBy{"application"},
 				restricted{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/restricted/RESTRICTED_LICENSE",
+			},
 		},
 		{
 			condition: "restricted",
@@ -362,6 +419,11 @@
 				usedBy{"bin/bin1"},
 				reciprocal{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/reciprocal/RECIPROCAL_LICENSE",
+				"testdata/restricted/RESTRICTED_LICENSE",
+			},
 		},
 		{
 			condition: "restricted",
@@ -373,6 +435,7 @@
 				usedBy{"lib/libd.so"},
 				notice{},
 			},
+			expectedDeps: []string{"testdata/notice/NOTICE_LICENSE"},
 		},
 		{
 			condition: "proprietary",
@@ -399,6 +462,11 @@
 				usedBy{"highest.apex/bin/bin1"},
 				proprietary{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/proprietary/PROPRIETARY_LICENSE",
+				"testdata/restricted/RESTRICTED_LICENSE",
+			},
 		},
 		{
 			condition: "proprietary",
@@ -425,6 +493,11 @@
 				usedBy{"container.zip/bin1"},
 				proprietary{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/proprietary/PROPRIETARY_LICENSE",
+				"testdata/restricted/RESTRICTED_LICENSE",
+			},
 		},
 		{
 			condition: "proprietary",
@@ -440,6 +513,10 @@
 				usedBy{"application"},
 				proprietary{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/proprietary/PROPRIETARY_LICENSE",
+			},
 		},
 		{
 			condition: "proprietary",
@@ -457,6 +534,10 @@
 				usedBy{"bin/bin1"},
 				proprietary{},
 			},
+			expectedDeps: []string{
+				"testdata/firstparty/FIRST_PARTY_LICENSE",
+				"testdata/proprietary/PROPRIETARY_LICENSE",
+			},
 		},
 		{
 			condition: "proprietary",
@@ -468,6 +549,7 @@
 				usedBy{"lib/libd.so"},
 				notice{},
 			},
+			expectedDeps: []string{"testdata/notice/NOTICE_LICENSE"},
 		},
 	}
 	for _, tt := range tests {
@@ -480,7 +562,9 @@
 				rootFiles = append(rootFiles, "testdata/"+tt.condition+"/"+r)
 			}
 
-			ctx := context{stdout, stderr, os.DirFS("."), tt.stripPrefix}
+			var deps []string
+
+			ctx := context{stdout, stderr, os.DirFS("."), tt.stripPrefix, &deps}
 
 			err := textNotice(&ctx, rootFiles...)
 			if err != nil {
@@ -512,6 +596,15 @@
 			for ; lineno < len(tt.expectedOut); lineno++ {
 				t.Errorf("textnotice: missing output line %d: ended early, want %q", lineno+1, tt.expectedOut[lineno].String())
 			}
+
+			t.Logf("got deps: %q", deps)
+
+			t.Logf("want deps: %q", tt.expectedDeps)
+
+			if g, w := deps, tt.expectedDeps; !reflect.DeepEqual(g, w) {
+				t.Errorf("unexpected deps, wanted:\n%s\ngot:\n%s\n",
+					strings.Join(w, "\n"), strings.Join(g, "\n"))
+			}
 		})
 	}
 }
diff --git a/tools/compliance/noticeindex.go b/tools/compliance/noticeindex.go
index 58b1c3b..7bebe3d 100644
--- a/tools/compliance/noticeindex.go
+++ b/tools/compliance/noticeindex.go
@@ -62,6 +62,8 @@
 	targetHashes map[*TargetNode]map[hash]struct{}
 	// projectName maps project directory names to project name text.
 	projectName map[string]string
+	// files lists all the files accessed during indexing
+	files []string
 }
 
 // IndexLicenseTexts creates a hashed index of license texts for `lg` and `rs`
@@ -71,14 +73,17 @@
 		rs = ResolveNotices(lg)
 	}
 	ni := &NoticeIndex{
-		lg, rs, ShippedNodes(lg), rootFS,
-		make(map[string]hash),
-		make(map[hash][]byte),
-		make(map[hash]map[string]map[string]struct{}),
-		make(map[string]map[hash]map[string]struct{}),
-		make(map[string]map[hash]struct{}),
-		make(map[*TargetNode]map[hash]struct{}),
-		make(map[string]string),
+		lg:             lg,
+		rs:             rs,
+		shipped:        ShippedNodes(lg),
+		rootFS:         rootFS,
+		hash:           make(map[string]hash),
+		text:           make(map[hash][]byte),
+		hashLibInstall: make(map[hash]map[string]map[string]struct{}),
+		installHashLib: make(map[string]map[hash]map[string]struct{}),
+		libHash:        make(map[string]map[hash]struct{}),
+		targetHashes:   make(map[*TargetNode]map[hash]struct{}),
+		projectName:    make(map[string]string),
 	}
 
 	// index adds all license texts for `tn` to the index.
@@ -208,6 +213,13 @@
 	return c
 }
 
+// InputNoticeFiles returns the list of files that were hashed during IndexLicenseTexts.
+func (ni *NoticeIndex) InputNoticeFiles() []string {
+	files := append([]string(nil), ni.files...)
+	sort.Strings(files)
+	return files
+}
+
 // HashLibs returns the ordered array of library names using the license text
 // hashed as `h`.
 func (ni *NoticeIndex) HashLibs(h hash) []string {
@@ -449,6 +461,8 @@
 		ni.text[hash] = text
 	}
 
+	ni.files = append(ni.files, file)
+
 	return nil
 }
 
diff --git a/tools/compliance/policy_policy.go b/tools/compliance/policy_policy.go
index 442025e..60bdf48 100644
--- a/tools/compliance/policy_policy.go
+++ b/tools/compliance/policy_policy.go
@@ -218,7 +218,7 @@
 // aggregation, per policy it ceases to be a pure aggregation in the context of
 // that derivative work. The `treatAsAggregate` parameter will be false for
 // non-aggregates and for aggregates in non-aggregate contexts.
-func targetConditionsPropagatingToDep(lg *LicenseGraph, e *TargetEdge, targetConditions LicenseConditionSet, treatAsAggregate bool) LicenseConditionSet {
+func targetConditionsPropagatingToDep(lg *LicenseGraph, e *TargetEdge, targetConditions LicenseConditionSet, treatAsAggregate bool, conditionsFn TraceConditions) LicenseConditionSet {
 	result := targetConditions
 
 	// reverse direction -- none of these apply to things depended-on, only to targets depending-on.
@@ -232,7 +232,7 @@
 	if treatAsAggregate {
 		// If the author of a pure aggregate licenses it restricted, apply restricted to immediate dependencies.
 		// Otherwise, restricted does not propagate back down to dependencies.
-		if !LicenseConditionSetFromNames(e.target, e.target.proto.LicenseConditions...).MatchesAnySet(ImpliesRestricted) {
+		if !conditionsFn(e.target).MatchesAnySet(ImpliesRestricted) {
 			result = result.Difference(ImpliesRestricted)
 		}
 		return result
diff --git a/tools/compliance/policy_policy_test.go b/tools/compliance/policy_policy_test.go
index 32dd5fd..27ce16c 100644
--- a/tools/compliance/policy_policy_test.go
+++ b/tools/compliance/policy_policy_test.go
@@ -282,7 +282,7 @@
 						targetConditions = targetConditions.Union(otn.licenseConditions)
 					}
 					t.Logf("calculate dep conditions for edge=%s, target conditions=%v, treatAsAggregate=%v", edge.String(), targetConditions.Names(), tt.treatAsAggregate)
-					cs := targetConditionsPropagatingToDep(lg, edge, targetConditions, tt.treatAsAggregate)
+					cs := targetConditionsPropagatingToDep(lg, edge, targetConditions, tt.treatAsAggregate, AllResolutions)
 					t.Logf("calculated dep conditions as %v", cs.Names())
 					actual := cs.Names()
 					sort.Strings(actual)
diff --git a/tools/compliance/policy_resolve.go b/tools/compliance/policy_resolve.go
index 336894a..d357aec 100644
--- a/tools/compliance/policy_resolve.go
+++ b/tools/compliance/policy_resolve.go
@@ -18,6 +18,16 @@
 	"sync"
 )
 
+var (
+	// AllResolutions is a TraceConditions function that resolves all
+	// unfiltered license conditions.
+	AllResolutions = TraceConditions(func(tn *TargetNode) LicenseConditionSet { return tn.licenseConditions })
+)
+
+// TraceConditions is a function that returns the conditions to trace for each
+// target node `tn`.
+type TraceConditions func(tn *TargetNode) LicenseConditionSet
+
 // ResolveBottomUpConditions performs a bottom-up walk of the LicenseGraph
 // propagating conditions up the graph as necessary according to the properties
 // of each edge and according to each license condition in question.
@@ -29,6 +39,14 @@
 // not resolve the library and its transitive closure, but the later top-down
 // walk will.
 func ResolveBottomUpConditions(lg *LicenseGraph) {
+	TraceBottomUpConditions(lg, AllResolutions)
+}
+
+// TraceBottomUpConditions performs a bottom-up walk of the LicenseGraph
+// propagating trace conditions from `conditionsFn` up the graph as necessary
+// according to the properties of each edge and according to each license
+// condition in question.
+func TraceBottomUpConditions(lg *LicenseGraph, conditionsFn TraceConditions) {
 
 	// short-cut if already walked and cached
 	lg.mu.Lock()
@@ -70,7 +88,7 @@
 				// needs to walk again in non-aggregate context
 				delete(cmap, target)
 			} else {
-				target.resolution |= target.licenseConditions
+				target.resolution |= conditionsFn(target)
 				amap[target] = struct{}{}
 			}
 			if treatAsAggregate {
@@ -123,6 +141,13 @@
 // dependency except restricted. For restricted, the policy is to share the
 // source of any libraries linked to restricted code and to provide notice.
 func ResolveTopDownConditions(lg *LicenseGraph) {
+	TraceTopDownConditions(lg, AllResolutions)
+}
+
+// TraceTopDownCondtions performs a top-down walk of the LicenseGraph
+// propagating trace conditions returned by `conditionsFn` from target to
+// dependency.
+func TraceTopDownConditions(lg *LicenseGraph, conditionsFn TraceConditions) {
 
 	// short-cut if already walked and cached
 	lg.mu.Lock()
@@ -139,7 +164,7 @@
 	lg.mu.Unlock()
 
 	// start with the conditions propagated up the graph
-	ResolveBottomUpConditions(lg)
+	TraceBottomUpConditions(lg, conditionsFn)
 
 	// amap contains the set of targets already walked. (guarded by mu)
 	amap := make(map[*TargetNode]struct{})
@@ -156,7 +181,7 @@
 	walk = func(fnode *TargetNode, cs LicenseConditionSet, treatAsAggregate bool) {
 		defer wg.Done()
 		mu.Lock()
-		fnode.resolution |= fnode.licenseConditions
+		fnode.resolution |= conditionsFn(fnode)
 		fnode.resolution |= cs
 		amap[fnode] = struct{}{}
 		if treatAsAggregate {
@@ -168,7 +193,7 @@
 		for _, edge := range fnode.edges {
 			func(edge *TargetEdge) {
 				// dcs holds the dpendency conditions inherited from the target
-				dcs := targetConditionsPropagatingToDep(lg, edge, cs, treatAsAggregate)
+				dcs := targetConditionsPropagatingToDep(lg, edge, cs, treatAsAggregate, conditionsFn)
 				dnode := edge.dependency
 				mu.Lock()
 				defer mu.Unlock()
diff --git a/tools/compliance/resolutionset.go b/tools/compliance/resolutionset.go
index ef89883..7c8f333 100644
--- a/tools/compliance/resolutionset.go
+++ b/tools/compliance/resolutionset.go
@@ -86,6 +86,22 @@
 	return result
 }
 
+// AllActions returns the set of actions required to resolve the set omitting
+// the attachment.
+func (rs ResolutionSet) AllActions() ActionSet {
+	result := make(ActionSet)
+	for _, as := range rs {
+		for actsOn, cs := range as {
+			if _, ok := result[actsOn]; ok {
+				result[actsOn] = cs.Union(result[actsOn])
+			} else {
+				result[actsOn] = cs
+			}
+		}
+	}
+	return result
+}
+
 // String returns a human-readable string representation of the set.
 func (rs ResolutionSet) String() string {
 	var sb strings.Builder