Merge "Repeat metalava errors at the end to make it easy to find them"
diff --git a/OWNERS b/OWNERS
index dbb491d..8355d10 100644
--- a/OWNERS
+++ b/OWNERS
@@ -6,9 +6,8 @@
 per-file * = patricearruda@google.com
 per-file * = paulduffin@google.com
 
-per-file ndk_*.go, *gen_stub_libs.py = danalbert@google.com
+per-file ndk_*.go = danalbert@google.com
 per-file clang.go,global.go = srhines@google.com, chh@google.com, pirama@google.com, yikong@google.com
 per-file tidy.go = srhines@google.com, chh@google.com
 per-file lto.go,pgo.go = srhines@google.com, pirama@google.com, yikong@google.com
 per-file docs/map_files.md = danalbert@google.com, enh@google.com, jiyong@google.com
-per-file *ndk_api_coverage_parser.py = sophiez@google.com
\ No newline at end of file
diff --git a/android/Android.bp b/android/Android.bp
index 487372b..977345b 100644
--- a/android/Android.bp
+++ b/android/Android.bp
@@ -19,6 +19,7 @@
         "csuite_config.go",
         "defaults.go",
         "defs.go",
+        "depset.go",
         "expand.go",
         "filegroup.go",
         "hooks.go",
@@ -61,6 +62,7 @@
         "arch_test.go",
         "config_test.go",
         "csuite_config_test.go",
+        "depset_test.go",
         "expand_test.go",
         "module_test.go",
         "mutator_test.go",
diff --git a/android/androidmk.go b/android/androidmk.go
index 045cb59..dfc68c4 100644
--- a/android/androidmk.go
+++ b/android/androidmk.go
@@ -107,6 +107,25 @@
 	a.EntryMap[name] = []string{path.String()}
 }
 
+func (a *AndroidMkEntries) SetOptionalPath(name string, path OptionalPath) {
+	if path.Valid() {
+		a.SetPath(name, path.Path())
+	}
+}
+
+func (a *AndroidMkEntries) AddPath(name string, path Path) {
+	if _, ok := a.EntryMap[name]; !ok {
+		a.entryOrder = append(a.entryOrder, name)
+	}
+	a.EntryMap[name] = append(a.EntryMap[name], path.String())
+}
+
+func (a *AndroidMkEntries) AddOptionalPath(name string, path OptionalPath) {
+	if path.Valid() {
+		a.AddPath(name, path.Path())
+	}
+}
+
 func (a *AndroidMkEntries) SetBoolIfTrue(name string, flag bool) {
 	if flag {
 		if _, ok := a.EntryMap[name]; !ok {
diff --git a/android/depset.go b/android/depset.go
new file mode 100644
index 0000000..f707094
--- /dev/null
+++ b/android/depset.go
@@ -0,0 +1,190 @@
+// Copyright 2020 Google Inc. All rights reserved.
+//
+// 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 android
+
+import "fmt"
+
+// DepSet is designed to be conceptually compatible with Bazel's depsets:
+// https://docs.bazel.build/versions/master/skylark/depsets.html
+
+// A DepSet efficiently stores Paths from transitive dependencies without copying. It is stored
+// as a DAG of DepSet nodes, each of which has some direct contents and a list of dependency
+// DepSet nodes.
+//
+// A DepSet has an order that will be used to walk the DAG when ToList() is called.  The order
+// can be POSTORDER, PREORDER, or TOPOLOGICAL.  POSTORDER and PREORDER orders return a postordered
+// or preordered left to right flattened list.  TOPOLOGICAL returns a list that guarantees that
+// elements of children are listed after all of their parents (unless there are duplicate direct
+// elements in the DepSet or any of its transitive dependencies, in which case the ordering of the
+// duplicated element is not guaranteed).
+//
+// A DepSet is created by NewDepSet or NewDepSetBuilder.Build from the Paths for direct contents
+// and the *DepSets of dependencies. A DepSet is immutable once created.
+type DepSet struct {
+	preorder   bool
+	reverse    bool
+	order      DepSetOrder
+	direct     Paths
+	transitive []*DepSet
+}
+
+// DepSetBuilder is used to create an immutable DepSet.
+type DepSetBuilder struct {
+	order      DepSetOrder
+	direct     Paths
+	transitive []*DepSet
+}
+
+type DepSetOrder int
+
+const (
+	PREORDER DepSetOrder = iota
+	POSTORDER
+	TOPOLOGICAL
+)
+
+func (o DepSetOrder) String() string {
+	switch o {
+	case PREORDER:
+		return "PREORDER"
+	case POSTORDER:
+		return "POSTORDER"
+	case TOPOLOGICAL:
+		return "TOPOLOGICAL"
+	default:
+		panic(fmt.Errorf("Invalid DepSetOrder %d", o))
+	}
+}
+
+// NewDepSet returns an immutable DepSet with the given order, direct and transitive contents.
+func NewDepSet(order DepSetOrder, direct Paths, transitive []*DepSet) *DepSet {
+	var directCopy Paths
+	var transitiveCopy []*DepSet
+	if order == TOPOLOGICAL {
+		directCopy = ReversePaths(direct)
+		transitiveCopy = reverseDepSets(transitive)
+	} else {
+		// Use copy instead of append(nil, ...) to make a slice that is exactly the size of the input
+		// slice.  The DepSet is immutable, there is no need for additional capacity.
+		directCopy = make(Paths, len(direct))
+		copy(directCopy, direct)
+		transitiveCopy = make([]*DepSet, len(transitive))
+		copy(transitiveCopy, transitive)
+	}
+
+	for _, dep := range transitive {
+		if dep.order != order {
+			panic(fmt.Errorf("incompatible order, new DepSet is %s but transitive DepSet is %s",
+				order, dep.order))
+		}
+	}
+
+	return &DepSet{
+		preorder:   order == PREORDER,
+		reverse:    order == TOPOLOGICAL,
+		order:      order,
+		direct:     directCopy,
+		transitive: transitiveCopy,
+	}
+}
+
+// NewDepSetBuilder returns a DepSetBuilder to create an immutable DepSet with the given order.
+func NewDepSetBuilder(order DepSetOrder) *DepSetBuilder {
+	return &DepSetBuilder{order: order}
+}
+
+// Direct adds direct contents to the DepSet being built by a DepSetBuilder. Newly added direct
+// contents are to the right of any existing direct contents.
+func (b *DepSetBuilder) Direct(direct ...Path) *DepSetBuilder {
+	b.direct = append(b.direct, direct...)
+	return b
+}
+
+// Transitive adds transitive contents to the DepSet being built by a DepSetBuilder. Newly added
+// transitive contents are to the right of any existing transitive contents.
+func (b *DepSetBuilder) Transitive(transitive ...*DepSet) *DepSetBuilder {
+	b.transitive = append(b.transitive, transitive...)
+	return b
+}
+
+// Returns the DepSet being built by this DepSetBuilder.  The DepSetBuilder retains its contents
+// for creating more DepSets.
+func (b *DepSetBuilder) Build() *DepSet {
+	return NewDepSet(b.order, b.direct, b.transitive)
+}
+
+// walk calls the visit method in depth-first order on a DepSet, preordered if d.preorder is set,
+// otherwise postordered.
+func (d *DepSet) walk(visit func(Paths)) {
+	visited := make(map[*DepSet]bool)
+
+	var dfs func(d *DepSet)
+	dfs = func(d *DepSet) {
+		visited[d] = true
+		if d.preorder {
+			visit(d.direct)
+		}
+		for _, dep := range d.transitive {
+			if !visited[dep] {
+				dfs(dep)
+			}
+		}
+
+		if !d.preorder {
+			visit(d.direct)
+		}
+	}
+
+	dfs(d)
+}
+
+// ToList returns the DepSet flattened to a list.  The order in the list is based on the order
+// of the DepSet.  POSTORDER and PREORDER orders return a postordered or preordered left to right
+// flattened list.  TOPOLOGICAL returns a list that guarantees that elements of children are listed
+// after all of their parents (unless there are duplicate direct elements in the DepSet or any of
+// its transitive dependencies, in which case the ordering of the duplicated element is not
+// guaranteed).
+func (d *DepSet) ToList() Paths {
+	var list Paths
+	d.walk(func(paths Paths) {
+		list = append(list, paths...)
+	})
+	list = FirstUniquePaths(list)
+	if d.reverse {
+		reversePathsInPlace(list)
+	}
+	return list
+}
+
+// ToSortedList returns the direct and transitive contents of a DepSet in lexically sorted order
+// with duplicates removed.
+func (d *DepSet) ToSortedList() Paths {
+	list := d.ToList()
+	return SortedUniquePaths(list)
+}
+
+func reversePathsInPlace(list Paths) {
+	for i, j := 0, len(list)-1; i < j; i, j = i+1, j-1 {
+		list[i], list[j] = list[j], list[i]
+	}
+}
+
+func reverseDepSets(list []*DepSet) []*DepSet {
+	ret := make([]*DepSet, len(list))
+	for i := range list {
+		ret[i] = list[len(list)-1-i]
+	}
+	return ret
+}
diff --git a/android/depset_test.go b/android/depset_test.go
new file mode 100644
index 0000000..c328127
--- /dev/null
+++ b/android/depset_test.go
@@ -0,0 +1,304 @@
+// Copyright 2020 Google Inc. All rights reserved.
+//
+// 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 android
+
+import (
+	"fmt"
+	"reflect"
+	"strings"
+	"testing"
+)
+
+func ExampleDepSet_ToList_postordered() {
+	a := NewDepSetBuilder(POSTORDER).Direct(PathForTesting("a")).Build()
+	b := NewDepSetBuilder(POSTORDER).Direct(PathForTesting("b")).Transitive(a).Build()
+	c := NewDepSetBuilder(POSTORDER).Direct(PathForTesting("c")).Transitive(a).Build()
+	d := NewDepSetBuilder(POSTORDER).Direct(PathForTesting("d")).Transitive(b, c).Build()
+
+	fmt.Println(d.ToList().Strings())
+	// Output: [a b c d]
+}
+
+func ExampleDepSet_ToList_preordered() {
+	a := NewDepSetBuilder(PREORDER).Direct(PathForTesting("a")).Build()
+	b := NewDepSetBuilder(PREORDER).Direct(PathForTesting("b")).Transitive(a).Build()
+	c := NewDepSetBuilder(PREORDER).Direct(PathForTesting("c")).Transitive(a).Build()
+	d := NewDepSetBuilder(PREORDER).Direct(PathForTesting("d")).Transitive(b, c).Build()
+
+	fmt.Println(d.ToList().Strings())
+	// Output: [d b a c]
+}
+
+func ExampleDepSet_ToList_topological() {
+	a := NewDepSetBuilder(TOPOLOGICAL).Direct(PathForTesting("a")).Build()
+	b := NewDepSetBuilder(TOPOLOGICAL).Direct(PathForTesting("b")).Transitive(a).Build()
+	c := NewDepSetBuilder(TOPOLOGICAL).Direct(PathForTesting("c")).Transitive(a).Build()
+	d := NewDepSetBuilder(TOPOLOGICAL).Direct(PathForTesting("d")).Transitive(b, c).Build()
+
+	fmt.Println(d.ToList().Strings())
+	// Output: [d b c a]
+}
+
+func ExampleDepSet_ToSortedList() {
+	a := NewDepSetBuilder(POSTORDER).Direct(PathForTesting("a")).Build()
+	b := NewDepSetBuilder(POSTORDER).Direct(PathForTesting("b")).Transitive(a).Build()
+	c := NewDepSetBuilder(POSTORDER).Direct(PathForTesting("c")).Transitive(a).Build()
+	d := NewDepSetBuilder(POSTORDER).Direct(PathForTesting("d")).Transitive(b, c).Build()
+
+	fmt.Println(d.ToSortedList().Strings())
+	// Output: [a b c d]
+}
+
+// Tests based on Bazel's ExpanderTestBase.java to ensure compatibility
+// https://github.com/bazelbuild/bazel/blob/master/src/test/java/com/google/devtools/build/lib/collect/nestedset/ExpanderTestBase.java
+func TestDepSet(t *testing.T) {
+	a := PathForTesting("a")
+	b := PathForTesting("b")
+	c := PathForTesting("c")
+	c2 := PathForTesting("c2")
+	d := PathForTesting("d")
+	e := PathForTesting("e")
+
+	tests := []struct {
+		name                             string
+		depSet                           func(t *testing.T, order DepSetOrder) *DepSet
+		postorder, preorder, topological []string
+	}{
+		{
+			name: "simple",
+			depSet: func(t *testing.T, order DepSetOrder) *DepSet {
+				return NewDepSet(order, Paths{c, a, b}, nil)
+			},
+			postorder:   []string{"c", "a", "b"},
+			preorder:    []string{"c", "a", "b"},
+			topological: []string{"c", "a", "b"},
+		},
+		{
+			name: "simpleNoDuplicates",
+			depSet: func(t *testing.T, order DepSetOrder) *DepSet {
+				return NewDepSet(order, Paths{c, a, a, a, b}, nil)
+			},
+			postorder:   []string{"c", "a", "b"},
+			preorder:    []string{"c", "a", "b"},
+			topological: []string{"c", "a", "b"},
+		},
+		{
+			name: "nesting",
+			depSet: func(t *testing.T, order DepSetOrder) *DepSet {
+				subset := NewDepSet(order, Paths{c, a, e}, nil)
+				return NewDepSet(order, Paths{b, d}, []*DepSet{subset})
+			},
+			postorder:   []string{"c", "a", "e", "b", "d"},
+			preorder:    []string{"b", "d", "c", "a", "e"},
+			topological: []string{"b", "d", "c", "a", "e"},
+		},
+		{
+			name: "builderReuse",
+			depSet: func(t *testing.T, order DepSetOrder) *DepSet {
+				assertEquals := func(t *testing.T, w, g Paths) {
+					if !reflect.DeepEqual(w, g) {
+						t.Errorf("want %q, got %q", w, g)
+					}
+				}
+				builder := NewDepSetBuilder(order)
+				assertEquals(t, nil, builder.Build().ToList())
+
+				builder.Direct(b)
+				assertEquals(t, Paths{b}, builder.Build().ToList())
+
+				builder.Direct(d)
+				assertEquals(t, Paths{b, d}, builder.Build().ToList())
+
+				child := NewDepSetBuilder(order).Direct(c, a, e).Build()
+				builder.Transitive(child)
+				return builder.Build()
+			},
+			postorder:   []string{"c", "a", "e", "b", "d"},
+			preorder:    []string{"b", "d", "c", "a", "e"},
+			topological: []string{"b", "d", "c", "a", "e"},
+		},
+		{
+			name: "builderChaining",
+			depSet: func(t *testing.T, order DepSetOrder) *DepSet {
+				return NewDepSetBuilder(order).Direct(b).Direct(d).
+					Transitive(NewDepSetBuilder(order).Direct(c, a, e).Build()).Build()
+			},
+			postorder:   []string{"c", "a", "e", "b", "d"},
+			preorder:    []string{"b", "d", "c", "a", "e"},
+			topological: []string{"b", "d", "c", "a", "e"},
+		},
+		{
+			name: "transitiveDepsHandledSeparately",
+			depSet: func(t *testing.T, order DepSetOrder) *DepSet {
+				subset := NewDepSetBuilder(order).Direct(c, a, e).Build()
+				builder := NewDepSetBuilder(order)
+				// The fact that we add the transitive subset between the Direct(b) and Direct(d)
+				// calls should not change the result.
+				builder.Direct(b)
+				builder.Transitive(subset)
+				builder.Direct(d)
+				return builder.Build()
+			},
+			postorder:   []string{"c", "a", "e", "b", "d"},
+			preorder:    []string{"b", "d", "c", "a", "e"},
+			topological: []string{"b", "d", "c", "a", "e"},
+		},
+		{
+			name: "nestingNoDuplicates",
+			depSet: func(t *testing.T, order DepSetOrder) *DepSet {
+				subset := NewDepSetBuilder(order).Direct(c, a, e).Build()
+				return NewDepSetBuilder(order).Direct(b, d, e).Transitive(subset).Build()
+			},
+			postorder:   []string{"c", "a", "e", "b", "d"},
+			preorder:    []string{"b", "d", "e", "c", "a"},
+			topological: []string{"b", "d", "c", "a", "e"},
+		},
+		{
+			name: "chain",
+			depSet: func(t *testing.T, order DepSetOrder) *DepSet {
+				c := NewDepSetBuilder(order).Direct(c).Build()
+				b := NewDepSetBuilder(order).Direct(b).Transitive(c).Build()
+				a := NewDepSetBuilder(order).Direct(a).Transitive(b).Build()
+
+				return a
+			},
+			postorder:   []string{"c", "b", "a"},
+			preorder:    []string{"a", "b", "c"},
+			topological: []string{"a", "b", "c"},
+		},
+		{
+			name: "diamond",
+			depSet: func(t *testing.T, order DepSetOrder) *DepSet {
+				d := NewDepSetBuilder(order).Direct(d).Build()
+				c := NewDepSetBuilder(order).Direct(c).Transitive(d).Build()
+				b := NewDepSetBuilder(order).Direct(b).Transitive(d).Build()
+				a := NewDepSetBuilder(order).Direct(a).Transitive(b).Transitive(c).Build()
+
+				return a
+			},
+			postorder:   []string{"d", "b", "c", "a"},
+			preorder:    []string{"a", "b", "d", "c"},
+			topological: []string{"a", "b", "c", "d"},
+		},
+		{
+			name: "extendedDiamond",
+			depSet: func(t *testing.T, order DepSetOrder) *DepSet {
+				d := NewDepSetBuilder(order).Direct(d).Build()
+				e := NewDepSetBuilder(order).Direct(e).Build()
+				b := NewDepSetBuilder(order).Direct(b).Transitive(d).Transitive(e).Build()
+				c := NewDepSetBuilder(order).Direct(c).Transitive(e).Transitive(d).Build()
+				a := NewDepSetBuilder(order).Direct(a).Transitive(b).Transitive(c).Build()
+				return a
+			},
+			postorder:   []string{"d", "e", "b", "c", "a"},
+			preorder:    []string{"a", "b", "d", "e", "c"},
+			topological: []string{"a", "b", "c", "e", "d"},
+		},
+		{
+			name: "extendedDiamondRightArm",
+			depSet: func(t *testing.T, order DepSetOrder) *DepSet {
+				d := NewDepSetBuilder(order).Direct(d).Build()
+				e := NewDepSetBuilder(order).Direct(e).Build()
+				b := NewDepSetBuilder(order).Direct(b).Transitive(d).Transitive(e).Build()
+				c2 := NewDepSetBuilder(order).Direct(c2).Transitive(e).Transitive(d).Build()
+				c := NewDepSetBuilder(order).Direct(c).Transitive(c2).Build()
+				a := NewDepSetBuilder(order).Direct(a).Transitive(b).Transitive(c).Build()
+				return a
+			},
+			postorder:   []string{"d", "e", "b", "c2", "c", "a"},
+			preorder:    []string{"a", "b", "d", "e", "c", "c2"},
+			topological: []string{"a", "b", "c", "c2", "e", "d"},
+		},
+		{
+			name: "orderConflict",
+			depSet: func(t *testing.T, order DepSetOrder) *DepSet {
+				child1 := NewDepSetBuilder(order).Direct(a, b).Build()
+				child2 := NewDepSetBuilder(order).Direct(b, a).Build()
+				parent := NewDepSetBuilder(order).Transitive(child1).Transitive(child2).Build()
+				return parent
+			},
+			postorder:   []string{"a", "b"},
+			preorder:    []string{"a", "b"},
+			topological: []string{"b", "a"},
+		},
+		{
+			name: "orderConflictNested",
+			depSet: func(t *testing.T, order DepSetOrder) *DepSet {
+				a := NewDepSetBuilder(order).Direct(a).Build()
+				b := NewDepSetBuilder(order).Direct(b).Build()
+				child1 := NewDepSetBuilder(order).Transitive(a).Transitive(b).Build()
+				child2 := NewDepSetBuilder(order).Transitive(b).Transitive(a).Build()
+				parent := NewDepSetBuilder(order).Transitive(child1).Transitive(child2).Build()
+				return parent
+			},
+			postorder:   []string{"a", "b"},
+			preorder:    []string{"a", "b"},
+			topological: []string{"b", "a"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Run("postorder", func(t *testing.T) {
+				depSet := tt.depSet(t, POSTORDER)
+				if g, w := depSet.ToList().Strings(), tt.postorder; !reflect.DeepEqual(g, w) {
+					t.Errorf("expected ToList() = %q, got %q", w, g)
+				}
+			})
+			t.Run("preorder", func(t *testing.T) {
+				depSet := tt.depSet(t, PREORDER)
+				if g, w := depSet.ToList().Strings(), tt.preorder; !reflect.DeepEqual(g, w) {
+					t.Errorf("expected ToList() = %q, got %q", w, g)
+				}
+			})
+			t.Run("topological", func(t *testing.T) {
+				depSet := tt.depSet(t, TOPOLOGICAL)
+				if g, w := depSet.ToList().Strings(), tt.topological; !reflect.DeepEqual(g, w) {
+					t.Errorf("expected ToList() = %q, got %q", w, g)
+				}
+			})
+		})
+	}
+}
+
+func TestDepSetInvalidOrder(t *testing.T) {
+	orders := []DepSetOrder{POSTORDER, PREORDER, TOPOLOGICAL}
+
+	run := func(t *testing.T, order1, order2 DepSetOrder) {
+		defer func() {
+			if r := recover(); r != nil {
+				if err, ok := r.(error); !ok {
+					t.Fatalf("expected panic error, got %v", err)
+				} else if !strings.Contains(err.Error(), "incompatible order") {
+					t.Fatalf("expected incompatible order error, got %v", err)
+				}
+			}
+		}()
+		NewDepSet(order1, nil, []*DepSet{NewDepSet(order2, nil, nil)})
+		t.Fatal("expected panic")
+	}
+
+	for _, order1 := range orders {
+		t.Run(order1.String(), func(t *testing.T) {
+			for _, order2 := range orders {
+				t.Run(order2.String(), func(t *testing.T) {
+					if order1 != order2 {
+						run(t, order1, order2)
+					}
+				})
+			}
+		})
+	}
+}
diff --git a/android/module.go b/android/module.go
index f80f37e..2062a4d 100644
--- a/android/module.go
+++ b/android/module.go
@@ -50,6 +50,8 @@
 	Implicit        Path
 	Implicits       Paths
 	OrderOnly       Paths
+	Validation      Path
+	Validations     Paths
 	Default         bool
 	Args            map[string]string
 }
@@ -1561,6 +1563,7 @@
 		Inputs:          params.Inputs.Strings(),
 		Implicits:       params.Implicits.Strings(),
 		OrderOnly:       params.OrderOnly.Strings(),
+		Validations:     params.Validations.Strings(),
 		Args:            params.Args,
 		Optional:        !params.Default,
 	}
@@ -1580,13 +1583,17 @@
 	if params.Implicit != nil {
 		bparams.Implicits = append(bparams.Implicits, params.Implicit.String())
 	}
+	if params.Validation != nil {
+		bparams.Validations = append(bparams.Validations, params.Validation.String())
+	}
 
 	bparams.Outputs = proptools.NinjaEscapeList(bparams.Outputs)
 	bparams.ImplicitOutputs = proptools.NinjaEscapeList(bparams.ImplicitOutputs)
 	bparams.Inputs = proptools.NinjaEscapeList(bparams.Inputs)
 	bparams.Implicits = proptools.NinjaEscapeList(bparams.Implicits)
 	bparams.OrderOnly = proptools.NinjaEscapeList(bparams.OrderOnly)
-	bparams.Depfile = proptools.NinjaEscapeList([]string{bparams.Depfile})[0]
+	bparams.Validations = proptools.NinjaEscapeList(bparams.Validations)
+	bparams.Depfile = proptools.NinjaEscape(bparams.Depfile)
 
 	return bparams
 }
diff --git a/android/paths.go b/android/paths.go
index d13b6d8..20ff55e 100644
--- a/android/paths.go
+++ b/android/paths.go
@@ -486,6 +486,10 @@
 	return ret
 }
 
+func CopyOfPaths(paths Paths) Paths {
+	return append(Paths(nil), paths...)
+}
+
 // FirstUniquePaths returns all unique elements of a Paths, keeping the first copy of each.  It
 // modifies the Paths slice contents in place, and returns a subslice of the original slice.
 func FirstUniquePaths(list Paths) Paths {
@@ -496,7 +500,8 @@
 	return firstUniquePathsList(list)
 }
 
-// SortedUniquePaths returns what its name says
+// SortedUniquePaths returns all unique elements of a Paths in sorted order.  It modifies the
+// Paths slice contents in place, and returns a subslice of the original slice.
 func SortedUniquePaths(list Paths) Paths {
 	unique := FirstUniquePaths(list)
 	sort.Slice(unique, func(i, j int) bool {
diff --git a/apex/androidmk.go b/apex/androidmk.go
index 156906d..b9bcc3a 100644
--- a/apex/androidmk.go
+++ b/apex/androidmk.go
@@ -105,9 +105,10 @@
 		fmt.Fprintln(w, "LOCAL_MODULE :=", moduleName)
 		// /apex/<apex_name>/{lib|framework|...}
 		pathWhenActivated := filepath.Join("$(PRODUCT_OUT)", "apex", apexName, fi.installDir)
+		var modulePath string
 		if apexType == flattenedApex {
 			// /system/apex/<name>/{lib|framework|...}
-			modulePath := filepath.Join(a.installDir.ToMakePath().String(), apexBundleName, fi.installDir)
+			modulePath = filepath.Join(a.installDir.ToMakePath().String(), apexBundleName, fi.installDir)
 			fmt.Fprintln(w, "LOCAL_MODULE_PATH :=", modulePath)
 			if a.primaryApexType && !symbolFilesNotNeeded {
 				fmt.Fprintln(w, "LOCAL_SOONG_SYMBOL_PATH :=", pathWhenActivated)
@@ -131,6 +132,7 @@
 				fmt.Fprintln(w, "LOCAL_NOTICE_FILE :=", strings.Join(fi.module.NoticeFiles().Strings(), " "))
 			}
 		} else {
+			modulePath = pathWhenActivated
 			fmt.Fprintln(w, "LOCAL_MODULE_PATH :=", pathWhenActivated)
 
 			// For non-flattend APEXes, the merged notice file is attached to the APEX itself.
@@ -193,8 +195,13 @@
 			// we need to remove the suffix from LOCAL_MODULE_STEM, otherwise
 			// we will have foo.apk.apk
 			fmt.Fprintln(w, "LOCAL_MODULE_STEM :=", strings.TrimSuffix(fi.Stem(), ".apk"))
-			if app, ok := fi.module.(*java.AndroidApp); ok && len(app.JniCoverageOutputs()) > 0 {
-				fmt.Fprintln(w, "LOCAL_PREBUILT_COVERAGE_ARCHIVE :=", strings.Join(app.JniCoverageOutputs().Strings(), " "))
+			if app, ok := fi.module.(*java.AndroidApp); ok {
+				if jniCoverageOutputs := app.JniCoverageOutputs(); len(jniCoverageOutputs) > 0 {
+					fmt.Fprintln(w, "LOCAL_PREBUILT_COVERAGE_ARCHIVE :=", strings.Join(jniCoverageOutputs.Strings(), " "))
+				}
+				if jniLibSymbols := app.JNISymbolsInstalls(modulePath); len(jniLibSymbols) > 0 {
+					fmt.Fprintln(w, "LOCAL_SOONG_JNI_LIBS_SYMBOLS :=", jniLibSymbols.String())
+				}
 			}
 			fmt.Fprintln(w, "include $(BUILD_SYSTEM)/soong_app_prebuilt.mk")
 		case appSet:
diff --git a/cc/config/x86_darwin_host.go b/cc/config/x86_darwin_host.go
index 8eb79e3..81c907d 100644
--- a/cc/config/x86_darwin_host.go
+++ b/cc/config/x86_darwin_host.go
@@ -66,6 +66,7 @@
 		"10.13",
 		"10.14",
 		"10.15",
+		"11.0",
 	}
 
 	darwinAvailableLibraries = append(
diff --git a/cc/ndk_api_coverage_parser/.gitignore b/cc/ndk_api_coverage_parser/.gitignore
new file mode 100644
index 0000000..fd94eac
--- /dev/null
+++ b/cc/ndk_api_coverage_parser/.gitignore
@@ -0,0 +1,140 @@
+# From https://github.com/github/gitignore/blob/master/Python.gitignore
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
diff --git a/cc/scriptlib/Android.bp b/cc/ndk_api_coverage_parser/Android.bp
similarity index 73%
rename from cc/scriptlib/Android.bp
rename to cc/ndk_api_coverage_parser/Android.bp
index ff9a2f0..8d9827c 100644
--- a/cc/scriptlib/Android.bp
+++ b/cc/ndk_api_coverage_parser/Android.bp
@@ -14,19 +14,33 @@
 // limitations under the License.
 //
 
+python_library_host {
+    name: "ndk_api_coverage_parser_lib",
+    pkg_path: "ndk_api_coverage_parser",
+    srcs: [
+        "__init__.py",
+    ],
+}
+
 python_test_host {
     name: "test_ndk_api_coverage_parser",
     main: "test_ndk_api_coverage_parser.py",
     srcs: [
         "test_ndk_api_coverage_parser.py",
     ],
+    libs: [
+        "ndk_api_coverage_parser_lib",
+        "symbolfile",
+    ],
 }
 
 python_binary_host {
     name: "ndk_api_coverage_parser",
-    main: "ndk_api_coverage_parser.py",
+    main: "__init__.py",
     srcs: [
-        "gen_stub_libs.py",
-        "ndk_api_coverage_parser.py",
+        "__init__.py",
+    ],
+    libs: [
+        "symbolfile",
     ],
 }
diff --git a/cc/ndk_api_coverage_parser/OWNERS b/cc/ndk_api_coverage_parser/OWNERS
new file mode 100644
index 0000000..a90c48c
--- /dev/null
+++ b/cc/ndk_api_coverage_parser/OWNERS
@@ -0,0 +1 @@
+sophiez@google.com
diff --git a/cc/scriptlib/ndk_api_coverage_parser.py b/cc/ndk_api_coverage_parser/__init__.py
similarity index 96%
rename from cc/scriptlib/ndk_api_coverage_parser.py
rename to cc/ndk_api_coverage_parser/__init__.py
index d74035b..7817c78 100755
--- a/cc/scriptlib/ndk_api_coverage_parser.py
+++ b/cc/ndk_api_coverage_parser/__init__.py
@@ -21,7 +21,7 @@
 import sys
 
 from xml.etree.ElementTree import Element, SubElement, tostring
-from gen_stub_libs import ALL_ARCHITECTURES, FUTURE_API_LEVEL, MultiplyDefinedSymbolError, SymbolFileParser
+from symbolfile import ALL_ARCHITECTURES, FUTURE_API_LEVEL, MultiplyDefinedSymbolError, SymbolFileParser
 
 
 ROOT_ELEMENT_TAG = 'ndk-library'
diff --git a/cc/ndk_api_coverage_parser/test_ndk_api_coverage_parser.py b/cc/ndk_api_coverage_parser/test_ndk_api_coverage_parser.py
new file mode 100644
index 0000000..3ec14c1
--- /dev/null
+++ b/cc/ndk_api_coverage_parser/test_ndk_api_coverage_parser.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2016 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.
+#
+"""Tests for ndk_api_coverage_parser.py."""
+import io
+import textwrap
+import unittest
+
+from xml.etree.ElementTree import fromstring
+from symbolfile import FUTURE_API_LEVEL, SymbolFileParser
+import ndk_api_coverage_parser as nparser
+
+
+# pylint: disable=missing-docstring
+
+
+# https://stackoverflow.com/a/24349916/632035
+def etree_equal(elem1, elem2):
+    """Returns true if the two XML elements are equal.
+
+    xml.etree.ElementTree's comparison operator cares about the ordering of
+    elements and attributes, but they are stored in an unordered dict so the
+    ordering is not deterministic.
+
+    lxml is apparently API compatible with xml and does use an OrderedDict, but
+    we don't have it in the tree.
+    """
+    if elem1.tag != elem2.tag:
+        return False
+    if elem1.text != elem2.text:
+        return False
+    if elem1.tail != elem2.tail:
+        return False
+    if elem1.attrib != elem2.attrib:
+        return False
+    if len(elem1) != len(elem2):
+        return False
+    return all(etree_equal(c1, c2) for c1, c2 in zip(elem1, elem2))
+
+
+class ApiCoverageSymbolFileParserTest(unittest.TestCase):
+    def test_parse(self):
+        input_file = io.StringIO(textwrap.dedent(u"""\
+            LIBLOG { # introduced-arm64=24 introduced-x86=24 introduced-x86_64=24
+              global:
+                android_name_to_log_id; # apex llndk introduced=23
+                android_log_id_to_name; # llndk arm
+                __android_log_assert; # introduced-x86=23
+                __android_log_buf_print; # var
+                __android_log_buf_write;
+              local:
+                *;
+            };
+            
+            LIBLOG_PLATFORM {
+                android_fdtrack; # llndk
+                android_net; # introduced=23
+            };
+            
+            LIBLOG_FOO { # var
+                android_var;
+            };
+        """))
+        parser = SymbolFileParser(input_file, {}, "", FUTURE_API_LEVEL, True, True)
+        generator = nparser.XmlGenerator(io.StringIO())
+        result = generator.convertToXml(parser.parse())
+        expected = fromstring('<ndk-library><symbol apex="True" arch="" introduced="23" introduced-arm64="24" introduced-x86="24" introduced-x86_64="24" is_deprecated="False" is_platform="False" llndk="True" name="android_name_to_log_id" /><symbol arch="arm" introduced-arm64="24" introduced-x86="24" introduced-x86_64="24" is_deprecated="False" is_platform="False" llndk="True" name="android_log_id_to_name" /><symbol arch="" introduced-arm64="24" introduced-x86="23" introduced-x86_64="24" is_deprecated="False" is_platform="False" name="__android_log_assert" /><symbol arch="" introduced-arm64="24" introduced-x86="24" introduced-x86_64="24" is_deprecated="False" is_platform="False" name="__android_log_buf_write" /><symbol arch="" is_deprecated="False" is_platform="True" llndk="True" name="android_fdtrack" /><symbol arch="" introduced="23" is_deprecated="False" is_platform="True" name="android_net" /></ndk-library>')
+        self.assertTrue(etree_equal(expected, result))
+
+
+def main():
+    suite = unittest.TestLoader().loadTestsFromName(__name__)
+    unittest.TextTestRunner(verbosity=3).run(suite)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/cc/ndk_library.go b/cc/ndk_library.go
index 6299b00..4578fd3 100644
--- a/cc/ndk_library.go
+++ b/cc/ndk_library.go
@@ -26,17 +26,16 @@
 )
 
 func init() {
+	pctx.HostBinToolVariable("ndkStubGenerator", "ndkstubgen")
 	pctx.HostBinToolVariable("ndk_api_coverage_parser", "ndk_api_coverage_parser")
 }
 
 var (
-	toolPath = pctx.SourcePathVariable("toolPath", "build/soong/cc/scriptlib/gen_stub_libs.py")
-
 	genStubSrc = pctx.AndroidStaticRule("genStubSrc",
 		blueprint.RuleParams{
-			Command: "$toolPath --arch $arch --api $apiLevel --api-map " +
-				"$apiMap $flags $in $out",
-			CommandDeps: []string{"$toolPath"},
+			Command: "$ndkStubGenerator --arch $arch --api $apiLevel " +
+				"--api-map $apiMap $flags $in $out",
+			CommandDeps: []string{"$ndkStubGenerator"},
 		}, "arch", "apiLevel", "apiMap", "flags")
 
 	parseNdkApiRule = pctx.AndroidStaticRule("parseNdkApiRule",
@@ -78,9 +77,9 @@
 	// https://github.com/android-ndk/ndk/issues/265.
 	Unversioned_until *string
 
-	// Private property for use by the mutator that splits per-API level.
-	// can be one of <number:sdk_version> or <codename> or "current"
-	// passed to "gen_stub_libs.py" as it is
+	// Private property for use by the mutator that splits per-API level. Can be
+	// one of <number:sdk_version> or <codename> or "current" passed to
+	// "ndkstubgen.py" as it is
 	ApiLevel string `blueprint:"mutated"`
 
 	// True if this API is not yet ready to be shipped in the NDK. It will be
diff --git a/cc/ndkstubgen/.gitignore b/cc/ndkstubgen/.gitignore
new file mode 100644
index 0000000..fd94eac
--- /dev/null
+++ b/cc/ndkstubgen/.gitignore
@@ -0,0 +1,140 @@
+# From https://github.com/github/gitignore/blob/master/Python.gitignore
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
diff --git a/cc/scriptlib/Android.bp b/cc/ndkstubgen/Android.bp
similarity index 63%
copy from cc/scriptlib/Android.bp
copy to cc/ndkstubgen/Android.bp
index ff9a2f0..85dfaee 100644
--- a/cc/scriptlib/Android.bp
+++ b/cc/ndkstubgen/Android.bp
@@ -14,19 +14,35 @@
 // limitations under the License.
 //
 
-python_test_host {
-    name: "test_ndk_api_coverage_parser",
-    main: "test_ndk_api_coverage_parser.py",
+python_binary_host {
+    name: "ndkstubgen",
+    pkg_path: "ndkstubgen",
+    main: "__init__.py",
     srcs: [
-        "test_ndk_api_coverage_parser.py",
+        "__init__.py",
+    ],
+    libs: [
+        "symbolfile",
     ],
 }
 
-python_binary_host {
-    name: "ndk_api_coverage_parser",
-    main: "ndk_api_coverage_parser.py",
+python_library_host {
+    name: "ndkstubgenlib",
+    pkg_path: "ndkstubgen",
     srcs: [
-        "gen_stub_libs.py",
-        "ndk_api_coverage_parser.py",
+        "__init__.py",
+    ],
+    libs: [
+        "symbolfile",
+    ],
+}
+
+python_test_host {
+    name: "test_ndkstubgen",
+    srcs: [
+        "test_ndkstubgen.py",
+    ],
+    libs: [
+        "ndkstubgenlib",
     ],
 }
diff --git a/cc/ndkstubgen/OWNERS b/cc/ndkstubgen/OWNERS
new file mode 100644
index 0000000..f0d8733
--- /dev/null
+++ b/cc/ndkstubgen/OWNERS
@@ -0,0 +1 @@
+danalbert@google.com
diff --git a/cc/ndkstubgen/__init__.py b/cc/ndkstubgen/__init__.py
new file mode 100755
index 0000000..2f4326a
--- /dev/null
+++ b/cc/ndkstubgen/__init__.py
@@ -0,0 +1,149 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2016 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.
+#
+"""Generates source for stub shared libraries for the NDK."""
+import argparse
+import json
+import logging
+import os
+import sys
+
+import symbolfile
+
+
+class Generator:
+    """Output generator that writes stub source files and version scripts."""
+    def __init__(self, src_file, version_script, arch, api, llndk, apex):
+        self.src_file = src_file
+        self.version_script = version_script
+        self.arch = arch
+        self.api = api
+        self.llndk = llndk
+        self.apex = apex
+
+    def write(self, versions):
+        """Writes all symbol data to the output files."""
+        for version in versions:
+            self.write_version(version)
+
+    def write_version(self, version):
+        """Writes a single version block's data to the output files."""
+        if symbolfile.should_omit_version(version, self.arch, self.api,
+                                          self.llndk, self.apex):
+            return
+
+        section_versioned = symbolfile.symbol_versioned_in_api(
+            version.tags, self.api)
+        version_empty = True
+        pruned_symbols = []
+        for symbol in version.symbols:
+            if symbolfile.should_omit_symbol(symbol, self.arch, self.api,
+                                             self.llndk, self.apex):
+                continue
+
+            if symbolfile.symbol_versioned_in_api(symbol.tags, self.api):
+                version_empty = False
+            pruned_symbols.append(symbol)
+
+        if len(pruned_symbols) > 0:
+            if not version_empty and section_versioned:
+                self.version_script.write(version.name + ' {\n')
+                self.version_script.write('    global:\n')
+            for symbol in pruned_symbols:
+                emit_version = symbolfile.symbol_versioned_in_api(
+                    symbol.tags, self.api)
+                if section_versioned and emit_version:
+                    self.version_script.write('        ' + symbol.name + ';\n')
+
+                weak = ''
+                if 'weak' in symbol.tags:
+                    weak = '__attribute__((weak)) '
+
+                if 'var' in symbol.tags:
+                    self.src_file.write('{}int {} = 0;\n'.format(
+                        weak, symbol.name))
+                else:
+                    self.src_file.write('{}void {}() {{}}\n'.format(
+                        weak, symbol.name))
+
+            if not version_empty and section_versioned:
+                base = '' if version.base is None else ' ' + version.base
+                self.version_script.write('}' + base + ';\n')
+
+
+def parse_args():
+    """Parses and returns command line arguments."""
+    parser = argparse.ArgumentParser()
+
+    parser.add_argument('-v', '--verbose', action='count', default=0)
+
+    parser.add_argument(
+        '--api', required=True, help='API level being targeted.')
+    parser.add_argument(
+        '--arch', choices=symbolfile.ALL_ARCHITECTURES, required=True,
+        help='Architecture being targeted.')
+    parser.add_argument(
+        '--llndk', action='store_true', help='Use the LLNDK variant.')
+    parser.add_argument(
+        '--apex', action='store_true', help='Use the APEX variant.')
+
+    parser.add_argument(
+        '--api-map', type=os.path.realpath, required=True,
+        help='Path to the API level map JSON file.')
+
+    parser.add_argument(
+        'symbol_file', type=os.path.realpath, help='Path to symbol file.')
+    parser.add_argument(
+        'stub_src', type=os.path.realpath,
+        help='Path to output stub source file.')
+    parser.add_argument(
+        'version_script', type=os.path.realpath,
+        help='Path to output version script.')
+
+    return parser.parse_args()
+
+
+def main():
+    """Program entry point."""
+    args = parse_args()
+
+    with open(args.api_map) as map_file:
+        api_map = json.load(map_file)
+    api = symbolfile.decode_api_level(args.api, api_map)
+
+    verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG)
+    verbosity = args.verbose
+    if verbosity > 2:
+        verbosity = 2
+    logging.basicConfig(level=verbose_map[verbosity])
+
+    with open(args.symbol_file) as symbol_file:
+        try:
+            versions = symbolfile.SymbolFileParser(symbol_file, api_map,
+                                                   args.arch, api, args.llndk,
+                                                   args.apex).parse()
+        except symbolfile.MultiplyDefinedSymbolError as ex:
+            sys.exit('{}: error: {}'.format(args.symbol_file, ex))
+
+    with open(args.stub_src, 'w') as src_file:
+        with open(args.version_script, 'w') as version_file:
+            generator = Generator(src_file, version_file, args.arch, api,
+                                  args.llndk, args.apex)
+            generator.write(versions)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/cc/ndkstubgen/test_ndkstubgen.py b/cc/ndkstubgen/test_ndkstubgen.py
new file mode 100755
index 0000000..70bcf78
--- /dev/null
+++ b/cc/ndkstubgen/test_ndkstubgen.py
@@ -0,0 +1,378 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2016 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.
+#
+"""Tests for ndkstubgen.py."""
+import io
+import textwrap
+import unittest
+
+import ndkstubgen
+import symbolfile
+
+
+# pylint: disable=missing-docstring
+
+
+class GeneratorTest(unittest.TestCase):
+    def test_omit_version(self):
+        # Thorough testing of the cases involved here is handled by
+        # OmitVersionTest, PrivateVersionTest, and SymbolPresenceTest.
+        src_file = io.StringIO()
+        version_file = io.StringIO()
+        generator = ndkstubgen.Generator(src_file, version_file, 'arm', 9,
+                                         False, False)
+
+        version = symbolfile.Version('VERSION_PRIVATE', None, [], [
+            symbolfile.Symbol('foo', []),
+        ])
+        generator.write_version(version)
+        self.assertEqual('', src_file.getvalue())
+        self.assertEqual('', version_file.getvalue())
+
+        version = symbolfile.Version('VERSION', None, ['x86'], [
+            symbolfile.Symbol('foo', []),
+        ])
+        generator.write_version(version)
+        self.assertEqual('', src_file.getvalue())
+        self.assertEqual('', version_file.getvalue())
+
+        version = symbolfile.Version('VERSION', None, ['introduced=14'], [
+            symbolfile.Symbol('foo', []),
+        ])
+        generator.write_version(version)
+        self.assertEqual('', src_file.getvalue())
+        self.assertEqual('', version_file.getvalue())
+
+    def test_omit_symbol(self):
+        # Thorough testing of the cases involved here is handled by
+        # SymbolPresenceTest.
+        src_file = io.StringIO()
+        version_file = io.StringIO()
+        generator = ndkstubgen.Generator(src_file, version_file, 'arm', 9,
+                                         False, False)
+
+        version = symbolfile.Version('VERSION_1', None, [], [
+            symbolfile.Symbol('foo', ['x86']),
+        ])
+        generator.write_version(version)
+        self.assertEqual('', src_file.getvalue())
+        self.assertEqual('', version_file.getvalue())
+
+        version = symbolfile.Version('VERSION_1', None, [], [
+            symbolfile.Symbol('foo', ['introduced=14']),
+        ])
+        generator.write_version(version)
+        self.assertEqual('', src_file.getvalue())
+        self.assertEqual('', version_file.getvalue())
+
+        version = symbolfile.Version('VERSION_1', None, [], [
+            symbolfile.Symbol('foo', ['llndk']),
+        ])
+        generator.write_version(version)
+        self.assertEqual('', src_file.getvalue())
+        self.assertEqual('', version_file.getvalue())
+
+        version = symbolfile.Version('VERSION_1', None, [], [
+            symbolfile.Symbol('foo', ['apex']),
+        ])
+        generator.write_version(version)
+        self.assertEqual('', src_file.getvalue())
+        self.assertEqual('', version_file.getvalue())
+
+    def test_write(self):
+        src_file = io.StringIO()
+        version_file = io.StringIO()
+        generator = ndkstubgen.Generator(src_file, version_file, 'arm', 9,
+                                         False, False)
+
+        versions = [
+            symbolfile.Version('VERSION_1', None, [], [
+                symbolfile.Symbol('foo', []),
+                symbolfile.Symbol('bar', ['var']),
+                symbolfile.Symbol('woodly', ['weak']),
+                symbolfile.Symbol('doodly', ['weak', 'var']),
+            ]),
+            symbolfile.Version('VERSION_2', 'VERSION_1', [], [
+                symbolfile.Symbol('baz', []),
+            ]),
+            symbolfile.Version('VERSION_3', 'VERSION_1', [], [
+                symbolfile.Symbol('qux', ['versioned=14']),
+            ]),
+        ]
+
+        generator.write(versions)
+        expected_src = textwrap.dedent("""\
+            void foo() {}
+            int bar = 0;
+            __attribute__((weak)) void woodly() {}
+            __attribute__((weak)) int doodly = 0;
+            void baz() {}
+            void qux() {}
+        """)
+        self.assertEqual(expected_src, src_file.getvalue())
+
+        expected_version = textwrap.dedent("""\
+            VERSION_1 {
+                global:
+                    foo;
+                    bar;
+                    woodly;
+                    doodly;
+            };
+            VERSION_2 {
+                global:
+                    baz;
+            } VERSION_1;
+        """)
+        self.assertEqual(expected_version, version_file.getvalue())
+
+
+class IntegrationTest(unittest.TestCase):
+    def test_integration(self):
+        api_map = {
+            'O': 9000,
+            'P': 9001,
+        }
+
+        input_file = io.StringIO(textwrap.dedent("""\
+            VERSION_1 {
+                global:
+                    foo; # var
+                    bar; # x86
+                    fizz; # introduced=O
+                    buzz; # introduced=P
+                local:
+                    *;
+            };
+
+            VERSION_2 { # arm
+                baz; # introduced=9
+                qux; # versioned=14
+            } VERSION_1;
+
+            VERSION_3 { # introduced=14
+                woodly;
+                doodly; # var
+            } VERSION_2;
+
+            VERSION_4 { # versioned=9
+                wibble;
+                wizzes; # llndk
+                waggle; # apex
+            } VERSION_2;
+
+            VERSION_5 { # versioned=14
+                wobble;
+            } VERSION_4;
+        """))
+        parser = symbolfile.SymbolFileParser(input_file, api_map, 'arm', 9,
+                                             False, False)
+        versions = parser.parse()
+
+        src_file = io.StringIO()
+        version_file = io.StringIO()
+        generator = ndkstubgen.Generator(src_file, version_file, 'arm', 9,
+                                         False, False)
+        generator.write(versions)
+
+        expected_src = textwrap.dedent("""\
+            int foo = 0;
+            void baz() {}
+            void qux() {}
+            void wibble() {}
+            void wobble() {}
+        """)
+        self.assertEqual(expected_src, src_file.getvalue())
+
+        expected_version = textwrap.dedent("""\
+            VERSION_1 {
+                global:
+                    foo;
+            };
+            VERSION_2 {
+                global:
+                    baz;
+            } VERSION_1;
+            VERSION_4 {
+                global:
+                    wibble;
+            } VERSION_2;
+        """)
+        self.assertEqual(expected_version, version_file.getvalue())
+
+    def test_integration_future_api(self):
+        api_map = {
+            'O': 9000,
+            'P': 9001,
+            'Q': 9002,
+        }
+
+        input_file = io.StringIO(textwrap.dedent("""\
+            VERSION_1 {
+                global:
+                    foo; # introduced=O
+                    bar; # introduced=P
+                    baz; # introduced=Q
+                local:
+                    *;
+            };
+        """))
+        parser = symbolfile.SymbolFileParser(input_file, api_map, 'arm', 9001,
+                                             False, False)
+        versions = parser.parse()
+
+        src_file = io.StringIO()
+        version_file = io.StringIO()
+        generator = ndkstubgen.Generator(src_file, version_file, 'arm', 9001,
+                                         False, False)
+        generator.write(versions)
+
+        expected_src = textwrap.dedent("""\
+            void foo() {}
+            void bar() {}
+        """)
+        self.assertEqual(expected_src, src_file.getvalue())
+
+        expected_version = textwrap.dedent("""\
+            VERSION_1 {
+                global:
+                    foo;
+                    bar;
+            };
+        """)
+        self.assertEqual(expected_version, version_file.getvalue())
+
+    def test_multiple_definition(self):
+        input_file = io.StringIO(textwrap.dedent("""\
+            VERSION_1 {
+                global:
+                    foo;
+                    foo;
+                    bar;
+                    baz;
+                    qux; # arm
+                local:
+                    *;
+            };
+
+            VERSION_2 {
+                global:
+                    bar;
+                    qux; # arm64
+            } VERSION_1;
+
+            VERSION_PRIVATE {
+                global:
+                    baz;
+            } VERSION_2;
+
+        """))
+        parser = symbolfile.SymbolFileParser(input_file, {}, 'arm', 16, False,
+                                             False)
+
+        with self.assertRaises(
+                symbolfile.MultiplyDefinedSymbolError) as ex_context:
+            parser.parse()
+        self.assertEqual(['bar', 'foo'],
+                         ex_context.exception.multiply_defined_symbols)
+
+    def test_integration_with_apex(self):
+        api_map = {
+            'O': 9000,
+            'P': 9001,
+        }
+
+        input_file = io.StringIO(textwrap.dedent("""\
+            VERSION_1 {
+                global:
+                    foo; # var
+                    bar; # x86
+                    fizz; # introduced=O
+                    buzz; # introduced=P
+                local:
+                    *;
+            };
+
+            VERSION_2 { # arm
+                baz; # introduced=9
+                qux; # versioned=14
+            } VERSION_1;
+
+            VERSION_3 { # introduced=14
+                woodly;
+                doodly; # var
+            } VERSION_2;
+
+            VERSION_4 { # versioned=9
+                wibble;
+                wizzes; # llndk
+                waggle; # apex
+                bubble; # apex llndk
+                duddle; # llndk apex
+            } VERSION_2;
+
+            VERSION_5 { # versioned=14
+                wobble;
+            } VERSION_4;
+        """))
+        parser = symbolfile.SymbolFileParser(input_file, api_map, 'arm', 9,
+                                             False, True)
+        versions = parser.parse()
+
+        src_file = io.StringIO()
+        version_file = io.StringIO()
+        generator = ndkstubgen.Generator(src_file, version_file, 'arm', 9,
+                                         False, True)
+        generator.write(versions)
+
+        expected_src = textwrap.dedent("""\
+            int foo = 0;
+            void baz() {}
+            void qux() {}
+            void wibble() {}
+            void waggle() {}
+            void bubble() {}
+            void duddle() {}
+            void wobble() {}
+        """)
+        self.assertEqual(expected_src, src_file.getvalue())
+
+        expected_version = textwrap.dedent("""\
+            VERSION_1 {
+                global:
+                    foo;
+            };
+            VERSION_2 {
+                global:
+                    baz;
+            } VERSION_1;
+            VERSION_4 {
+                global:
+                    wibble;
+                    waggle;
+                    bubble;
+                    duddle;
+            } VERSION_2;
+        """)
+        self.assertEqual(expected_version, version_file.getvalue())
+
+def main():
+    suite = unittest.TestLoader().loadTestsFromName(__name__)
+    unittest.TextTestRunner(verbosity=3).run(suite)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/cc/pylintrc b/cc/pylintrc
index ed49dd7..2032d4e 100644
--- a/cc/pylintrc
+++ b/cc/pylintrc
@@ -1,280 +1,11 @@
-[MASTER]
-
-# Specify a configuration file.
-#rcfile=
-
-# Python code to execute, usually for sys.path manipulation such as
-# pygtk.require().
-#init-hook=
-
-# Profiled execution.
-profile=no
-
-# Add files or directories to the blacklist. They should be base names, not
-# paths.
-ignore=CVS
-
-# Pickle collected data for later comparisons.
-persistent=yes
-
-# List of plugins (as comma separated values of python modules names) to load,
-# usually to register additional checkers.
-load-plugins=
-
-
 [MESSAGES CONTROL]
-
-# Enable the message, report, category or checker with the given id(s). You can
-# either give multiple identifier separated by comma (,) or put this option
-# multiple time. See also the "--disable" option for examples.
-#enable=
-
-# Disable the message, report, category or checker with the given id(s). You
-# can either give multiple identifiers separated by comma (,) or put this
-# option multiple times (only on the command line, not in the configuration
-# file where it should appear only once).You can also use "--disable=all" to
-# disable everything first and then reenable specific checks. For example, if
-# you want to run only the similarities checker, you can use "--disable=all
-# --enable=similarities". If you want to run only the classes checker, but have
-# no Warning level messages displayed, use"--disable=all --enable=classes
-# --disable=W"
 disable=design,fixme
 
-
-[REPORTS]
-
-# Set the output format. Available formats are text, parseable, colorized, msvs
-# (visual studio) and html. You can also give a reporter class, eg
-# mypackage.mymodule.MyReporterClass.
-output-format=text
-
-# Put messages in a separate file for each module / package specified on the
-# command line instead of printing them on stdout. Reports (if any) will be
-# written in a file name "pylint_global.[txt|html]".
-files-output=no
-
-# Tells whether to display a full report or only the messages
-reports=yes
-
-# Python expression which should return a note less than 10 (10 is the highest
-# note). You have access to the variables errors warning, statement which
-# respectively contain the number of errors / warnings messages and the total
-# number of statements analyzed. This is used by the global evaluation report
-# (RP0004).
-evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
-
-# Add a comment according to your evaluation note. This is used by the global
-# evaluation report (RP0004).
-comment=no
-
-# Template used to display messages. This is a python new-style format string
-# used to format the message information. See doc for all details
-#msg-template=
-
-
 [BASIC]
-
-# Required attributes for module, separated by a comma
-required-attributes=
-
-# List of builtins function names that should not be used, separated by a comma
-bad-functions=map,filter,apply,input
-
-# Regular expression which should only match correct module names
-module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
-
-# Regular expression which should only match correct module level names
-const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
-
-# Regular expression which should only match correct class names
-class-rgx=[A-Z_][a-zA-Z0-9]+$
-
-# Regular expression which should only match correct function names
-function-rgx=[a-z_][a-z0-9_]{2,30}$
-
-# Regular expression which should only match correct method names
-method-rgx=[a-z_][a-z0-9_]{2,30}$
-
-# Regular expression which should only match correct instance attribute names
-attr-rgx=[a-z_][a-z0-9_]{2,30}$
-
-# Regular expression which should only match correct argument names
-argument-rgx=[a-z_][a-z0-9_]{2,30}$
-
-# Regular expression which should only match correct variable names
-variable-rgx=[a-z_][a-z0-9_]{2,30}$
-
-# Regular expression which should only match correct attribute names in class
-# bodies
-class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
-
-# Regular expression which should only match correct list comprehension /
-# generator expression variable names
-inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
-
-# Good variable names which should always be accepted, separated by a comma
 good-names=i,j,k,ex,Run,_
 
-# Bad variable names which should always be refused, separated by a comma
-bad-names=foo,bar,baz,toto,tutu,tata
-
-# Regular expression which should only match function or class names that do
-# not require a docstring.
-no-docstring-rgx=__.*__
-
-# Minimum line length for functions/classes that require docstrings, shorter
-# ones are exempt.
-docstring-min-length=-1
-
-
-[TYPECHECK]
-
-# Tells whether missing members accessed in mixin class should be ignored. A
-# mixin class is detected if its name ends with "mixin" (case insensitive).
-ignore-mixin-members=yes
-
-# List of classes names for which member attributes should not be checked
-# (useful for classes with attributes dynamically set).
-ignored-classes=SQLObject
-
-# When zope mode is activated, add a predefined set of Zope acquired attributes
-# to generated-members.
-zope=no
-
-# List of members which are set dynamically and missed by pylint inference
-# system, and so shouldn't trigger E0201 when accessed. Python regular
-# expressions are accepted.
-generated-members=REQUEST,acl_users,aq_parent
-
-
-[MISCELLANEOUS]
-
-# List of note tags to take in consideration, separated by a comma.
-notes=FIXME,XXX,TODO
-
-
 [SIMILARITIES]
-
-# Minimum lines number of a similarity.
-min-similarity-lines=4
-
-# Ignore comments when computing similarities.
-ignore-comments=yes
-
-# Ignore docstrings when computing similarities.
-ignore-docstrings=yes
-
-# Ignore imports when computing similarities.
-ignore-imports=no
-
+ignore-imports=yes
 
 [VARIABLES]
-
-# Tells whether we should check for unused import in __init__ files.
-init-import=no
-
-# A regular expression matching the beginning of the name of dummy variables
-# (i.e. not used).
 dummy-variables-rgx=_|dummy
-
-# List of additional names supposed to be defined in builtins. Remember that
-# you should avoid to define new builtins when possible.
-additional-builtins=
-
-
-[FORMAT]
-
-# Maximum number of characters on a single line.
-max-line-length=80
-
-# Regexp for a line that is allowed to be longer than the limit.
-ignore-long-lines=^\s*(# )?<?https?://\S+>?$
-
-# Allow the body of an if to be on the same line as the test if there is no
-# else.
-single-line-if-stmt=no
-
-# List of optional constructs for which whitespace checking is disabled
-no-space-check=trailing-comma,dict-separator
-
-# Maximum number of lines in a module
-max-module-lines=1000
-
-# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
-# tab).
-indent-string='    '
-
-
-[IMPORTS]
-
-# Deprecated modules which should not be used, separated by a comma
-deprecated-modules=regsub,TERMIOS,Bastion,rexec
-
-# Create a graph of every (i.e. internal and external) dependencies in the
-# given file (report RP0402 must not be disabled)
-import-graph=
-
-# Create a graph of external dependencies in the given file (report RP0402 must
-# not be disabled)
-ext-import-graph=
-
-# Create a graph of internal dependencies in the given file (report RP0402 must
-# not be disabled)
-int-import-graph=
-
-
-[DESIGN]
-
-# Maximum number of arguments for function / method
-max-args=5
-
-# Argument names that match this expression will be ignored. Default to name
-# with leading underscore
-ignored-argument-names=_.*
-
-# Maximum number of locals for function / method body
-max-locals=15
-
-# Maximum number of return / yield for function / method body
-max-returns=6
-
-# Maximum number of branch for function / method body
-max-branches=12
-
-# Maximum number of statements in function / method body
-max-statements=50
-
-# Maximum number of parents for a class (see R0901).
-max-parents=7
-
-# Maximum number of attributes for a class (see R0902).
-max-attributes=7
-
-# Minimum number of public methods for a class (see R0903).
-min-public-methods=2
-
-# Maximum number of public methods for a class (see R0904).
-max-public-methods=20
-
-
-[CLASSES]
-
-# List of interface methods to ignore, separated by a comma. This is used for
-# instance to not check methods defines in Zope's Interface base class.
-ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
-
-# List of method names used to declare (i.e. assign) instance attributes.
-defining-attr-methods=__init__,__new__,setUp
-
-# List of valid names for the first argument in a class method.
-valid-classmethod-first-arg=cls
-
-# List of valid names for the first argument in a metaclass class method.
-valid-metaclass-classmethod-first-arg=mcs
-
-
-[EXCEPTIONS]
-
-# Exceptions that will emit a warning when being caught. Defaults to
-# "Exception"
-overgeneral-exceptions=Exception
diff --git a/cc/scriptlib/__init__.py b/cc/scriptlib/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/cc/scriptlib/__init__.py
+++ /dev/null
diff --git a/cc/scriptlib/test_gen_stub_libs.py b/cc/scriptlib/test_gen_stub_libs.py
deleted file mode 100755
index 0b45e71..0000000
--- a/cc/scriptlib/test_gen_stub_libs.py
+++ /dev/null
@@ -1,807 +0,0 @@
-#!/usr/bin/env python
-#
-# Copyright (C) 2016 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.
-#
-"""Tests for gen_stub_libs.py."""
-import io
-import textwrap
-import unittest
-
-import gen_stub_libs as gsl
-
-
-# pylint: disable=missing-docstring
-
-
-class DecodeApiLevelTest(unittest.TestCase):
-    def test_decode_api_level(self):
-        self.assertEqual(9, gsl.decode_api_level('9', {}))
-        self.assertEqual(9000, gsl.decode_api_level('O', {'O': 9000}))
-
-        with self.assertRaises(KeyError):
-            gsl.decode_api_level('O', {})
-
-
-class TagsTest(unittest.TestCase):
-    def test_get_tags_no_tags(self):
-        self.assertEqual([], gsl.get_tags(''))
-        self.assertEqual([], gsl.get_tags('foo bar baz'))
-
-    def test_get_tags(self):
-        self.assertEqual(['foo', 'bar'], gsl.get_tags('# foo bar'))
-        self.assertEqual(['bar', 'baz'], gsl.get_tags('foo # bar baz'))
-
-    def test_split_tag(self):
-        self.assertTupleEqual(('foo', 'bar'), gsl.split_tag('foo=bar'))
-        self.assertTupleEqual(('foo', 'bar=baz'), gsl.split_tag('foo=bar=baz'))
-        with self.assertRaises(ValueError):
-            gsl.split_tag('foo')
-
-    def test_get_tag_value(self):
-        self.assertEqual('bar', gsl.get_tag_value('foo=bar'))
-        self.assertEqual('bar=baz', gsl.get_tag_value('foo=bar=baz'))
-        with self.assertRaises(ValueError):
-            gsl.get_tag_value('foo')
-
-    def test_is_api_level_tag(self):
-        self.assertTrue(gsl.is_api_level_tag('introduced=24'))
-        self.assertTrue(gsl.is_api_level_tag('introduced-arm=24'))
-        self.assertTrue(gsl.is_api_level_tag('versioned=24'))
-
-        # Shouldn't try to process things that aren't a key/value tag.
-        self.assertFalse(gsl.is_api_level_tag('arm'))
-        self.assertFalse(gsl.is_api_level_tag('introduced'))
-        self.assertFalse(gsl.is_api_level_tag('versioned'))
-
-        # We don't support arch specific `versioned` tags.
-        self.assertFalse(gsl.is_api_level_tag('versioned-arm=24'))
-
-    def test_decode_api_level_tags(self):
-        api_map = {
-            'O': 9000,
-            'P': 9001,
-        }
-
-        tags = [
-            'introduced=9',
-            'introduced-arm=14',
-            'versioned=16',
-            'arm',
-            'introduced=O',
-            'introduced=P',
-        ]
-        expected_tags = [
-            'introduced=9',
-            'introduced-arm=14',
-            'versioned=16',
-            'arm',
-            'introduced=9000',
-            'introduced=9001',
-        ]
-        self.assertListEqual(
-            expected_tags, gsl.decode_api_level_tags(tags, api_map))
-
-        with self.assertRaises(gsl.ParseError):
-            gsl.decode_api_level_tags(['introduced=O'], {})
-
-
-class PrivateVersionTest(unittest.TestCase):
-    def test_version_is_private(self):
-        self.assertFalse(gsl.version_is_private('foo'))
-        self.assertFalse(gsl.version_is_private('PRIVATE'))
-        self.assertFalse(gsl.version_is_private('PLATFORM'))
-        self.assertFalse(gsl.version_is_private('foo_private'))
-        self.assertFalse(gsl.version_is_private('foo_platform'))
-        self.assertFalse(gsl.version_is_private('foo_PRIVATE_'))
-        self.assertFalse(gsl.version_is_private('foo_PLATFORM_'))
-
-        self.assertTrue(gsl.version_is_private('foo_PRIVATE'))
-        self.assertTrue(gsl.version_is_private('foo_PLATFORM'))
-
-
-class SymbolPresenceTest(unittest.TestCase):
-    def test_symbol_in_arch(self):
-        self.assertTrue(gsl.symbol_in_arch([], 'arm'))
-        self.assertTrue(gsl.symbol_in_arch(['arm'], 'arm'))
-
-        self.assertFalse(gsl.symbol_in_arch(['x86'], 'arm'))
-
-    def test_symbol_in_api(self):
-        self.assertTrue(gsl.symbol_in_api([], 'arm', 9))
-        self.assertTrue(gsl.symbol_in_api(['introduced=9'], 'arm', 9))
-        self.assertTrue(gsl.symbol_in_api(['introduced=9'], 'arm', 14))
-        self.assertTrue(gsl.symbol_in_api(['introduced-arm=9'], 'arm', 14))
-        self.assertTrue(gsl.symbol_in_api(['introduced-arm=9'], 'arm', 14))
-        self.assertTrue(gsl.symbol_in_api(['introduced-x86=14'], 'arm', 9))
-        self.assertTrue(gsl.symbol_in_api(
-            ['introduced-arm=9', 'introduced-x86=21'], 'arm', 14))
-        self.assertTrue(gsl.symbol_in_api(
-            ['introduced=9', 'introduced-x86=21'], 'arm', 14))
-        self.assertTrue(gsl.symbol_in_api(
-            ['introduced=21', 'introduced-arm=9'], 'arm', 14))
-        self.assertTrue(gsl.symbol_in_api(
-            ['future'], 'arm', gsl.FUTURE_API_LEVEL))
-
-        self.assertFalse(gsl.symbol_in_api(['introduced=14'], 'arm', 9))
-        self.assertFalse(gsl.symbol_in_api(['introduced-arm=14'], 'arm', 9))
-        self.assertFalse(gsl.symbol_in_api(['future'], 'arm', 9))
-        self.assertFalse(gsl.symbol_in_api(
-            ['introduced=9', 'future'], 'arm', 14))
-        self.assertFalse(gsl.symbol_in_api(
-            ['introduced-arm=9', 'future'], 'arm', 14))
-        self.assertFalse(gsl.symbol_in_api(
-            ['introduced-arm=21', 'introduced-x86=9'], 'arm', 14))
-        self.assertFalse(gsl.symbol_in_api(
-            ['introduced=9', 'introduced-arm=21'], 'arm', 14))
-        self.assertFalse(gsl.symbol_in_api(
-            ['introduced=21', 'introduced-x86=9'], 'arm', 14))
-
-        # Interesting edge case: this symbol should be omitted from the
-        # library, but this call should still return true because none of the
-        # tags indiciate that it's not present in this API level.
-        self.assertTrue(gsl.symbol_in_api(['x86'], 'arm', 9))
-
-    def test_verioned_in_api(self):
-        self.assertTrue(gsl.symbol_versioned_in_api([], 9))
-        self.assertTrue(gsl.symbol_versioned_in_api(['versioned=9'], 9))
-        self.assertTrue(gsl.symbol_versioned_in_api(['versioned=9'], 14))
-
-        self.assertFalse(gsl.symbol_versioned_in_api(['versioned=14'], 9))
-
-
-class OmitVersionTest(unittest.TestCase):
-    def test_omit_private(self):
-        self.assertFalse(
-            gsl.should_omit_version(
-                gsl.Version('foo', None, [], []), 'arm', 9, False, False))
-
-        self.assertTrue(
-            gsl.should_omit_version(
-                gsl.Version('foo_PRIVATE', None, [], []), 'arm', 9, False, False))
-        self.assertTrue(
-            gsl.should_omit_version(
-                gsl.Version('foo_PLATFORM', None, [], []), 'arm', 9, False, False))
-
-        self.assertTrue(
-            gsl.should_omit_version(
-                gsl.Version('foo', None, ['platform-only'], []), 'arm', 9,
-                False, False))
-
-    def test_omit_llndk(self):
-        self.assertTrue(
-            gsl.should_omit_version(
-                gsl.Version('foo', None, ['llndk'], []), 'arm', 9, False, False))
-
-        self.assertFalse(
-            gsl.should_omit_version(
-                gsl.Version('foo', None, [], []), 'arm', 9, True, False))
-        self.assertFalse(
-            gsl.should_omit_version(
-                gsl.Version('foo', None, ['llndk'], []), 'arm', 9, True, False))
-
-    def test_omit_apex(self):
-        self.assertTrue(
-            gsl.should_omit_version(
-                gsl.Version('foo', None, ['apex'], []), 'arm', 9, False, False))
-
-        self.assertFalse(
-            gsl.should_omit_version(
-                gsl.Version('foo', None, [], []), 'arm', 9, False, True))
-        self.assertFalse(
-            gsl.should_omit_version(
-                gsl.Version('foo', None, ['apex'], []), 'arm', 9, False, True))
-
-    def test_omit_arch(self):
-        self.assertFalse(
-            gsl.should_omit_version(
-                gsl.Version('foo', None, [], []), 'arm', 9, False, False))
-        self.assertFalse(
-            gsl.should_omit_version(
-                gsl.Version('foo', None, ['arm'], []), 'arm', 9, False, False))
-
-        self.assertTrue(
-            gsl.should_omit_version(
-                gsl.Version('foo', None, ['x86'], []), 'arm', 9, False, False))
-
-    def test_omit_api(self):
-        self.assertFalse(
-            gsl.should_omit_version(
-                gsl.Version('foo', None, [], []), 'arm', 9, False, False))
-        self.assertFalse(
-            gsl.should_omit_version(
-                gsl.Version('foo', None, ['introduced=9'], []), 'arm', 9,
-                False, False))
-
-        self.assertTrue(
-            gsl.should_omit_version(
-                gsl.Version('foo', None, ['introduced=14'], []), 'arm', 9,
-                False, False))
-
-
-class OmitSymbolTest(unittest.TestCase):
-    def test_omit_llndk(self):
-        self.assertTrue(
-            gsl.should_omit_symbol(
-                gsl.Symbol('foo', ['llndk']), 'arm', 9, False, False))
-
-        self.assertFalse(
-            gsl.should_omit_symbol(gsl.Symbol('foo', []), 'arm', 9, True, False))
-        self.assertFalse(
-            gsl.should_omit_symbol(
-                gsl.Symbol('foo', ['llndk']), 'arm', 9, True, False))
-
-    def test_omit_apex(self):
-        self.assertTrue(
-            gsl.should_omit_symbol(
-                gsl.Symbol('foo', ['apex']), 'arm', 9, False, False))
-
-        self.assertFalse(
-            gsl.should_omit_symbol(gsl.Symbol('foo', []), 'arm', 9, False, True))
-        self.assertFalse(
-            gsl.should_omit_symbol(
-                gsl.Symbol('foo', ['apex']), 'arm', 9, False, True))
-
-    def test_omit_arch(self):
-        self.assertFalse(
-            gsl.should_omit_symbol(gsl.Symbol('foo', []), 'arm', 9, False, False))
-        self.assertFalse(
-            gsl.should_omit_symbol(
-                gsl.Symbol('foo', ['arm']), 'arm', 9, False, False))
-
-        self.assertTrue(
-            gsl.should_omit_symbol(
-                gsl.Symbol('foo', ['x86']), 'arm', 9, False, False))
-
-    def test_omit_api(self):
-        self.assertFalse(
-            gsl.should_omit_symbol(gsl.Symbol('foo', []), 'arm', 9, False, False))
-        self.assertFalse(
-            gsl.should_omit_symbol(
-                gsl.Symbol('foo', ['introduced=9']), 'arm', 9, False, False))
-
-        self.assertTrue(
-            gsl.should_omit_symbol(
-                gsl.Symbol('foo', ['introduced=14']), 'arm', 9, False, False))
-
-
-class SymbolFileParseTest(unittest.TestCase):
-    def test_next_line(self):
-        input_file = io.StringIO(textwrap.dedent("""\
-            foo
-
-            bar
-            # baz
-            qux
-        """))
-        parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, False)
-        self.assertIsNone(parser.current_line)
-
-        self.assertEqual('foo', parser.next_line().strip())
-        self.assertEqual('foo', parser.current_line.strip())
-
-        self.assertEqual('bar', parser.next_line().strip())
-        self.assertEqual('bar', parser.current_line.strip())
-
-        self.assertEqual('qux', parser.next_line().strip())
-        self.assertEqual('qux', parser.current_line.strip())
-
-        self.assertEqual('', parser.next_line())
-        self.assertEqual('', parser.current_line)
-
-    def test_parse_version(self):
-        input_file = io.StringIO(textwrap.dedent("""\
-            VERSION_1 { # foo bar
-                baz;
-                qux; # woodly doodly
-            };
-
-            VERSION_2 {
-            } VERSION_1; # asdf
-        """))
-        parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, False)
-
-        parser.next_line()
-        version = parser.parse_version()
-        self.assertEqual('VERSION_1', version.name)
-        self.assertIsNone(version.base)
-        self.assertEqual(['foo', 'bar'], version.tags)
-
-        expected_symbols = [
-            gsl.Symbol('baz', []),
-            gsl.Symbol('qux', ['woodly', 'doodly']),
-        ]
-        self.assertEqual(expected_symbols, version.symbols)
-
-        parser.next_line()
-        version = parser.parse_version()
-        self.assertEqual('VERSION_2', version.name)
-        self.assertEqual('VERSION_1', version.base)
-        self.assertEqual([], version.tags)
-
-    def test_parse_version_eof(self):
-        input_file = io.StringIO(textwrap.dedent("""\
-            VERSION_1 {
-        """))
-        parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, False)
-        parser.next_line()
-        with self.assertRaises(gsl.ParseError):
-            parser.parse_version()
-
-    def test_unknown_scope_label(self):
-        input_file = io.StringIO(textwrap.dedent("""\
-            VERSION_1 {
-                foo:
-            }
-        """))
-        parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, False)
-        parser.next_line()
-        with self.assertRaises(gsl.ParseError):
-            parser.parse_version()
-
-    def test_parse_symbol(self):
-        input_file = io.StringIO(textwrap.dedent("""\
-            foo;
-            bar; # baz qux
-        """))
-        parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, False)
-
-        parser.next_line()
-        symbol = parser.parse_symbol()
-        self.assertEqual('foo', symbol.name)
-        self.assertEqual([], symbol.tags)
-
-        parser.next_line()
-        symbol = parser.parse_symbol()
-        self.assertEqual('bar', symbol.name)
-        self.assertEqual(['baz', 'qux'], symbol.tags)
-
-    def test_wildcard_symbol_global(self):
-        input_file = io.StringIO(textwrap.dedent("""\
-            VERSION_1 {
-                *;
-            };
-        """))
-        parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, False)
-        parser.next_line()
-        with self.assertRaises(gsl.ParseError):
-            parser.parse_version()
-
-    def test_wildcard_symbol_local(self):
-        input_file = io.StringIO(textwrap.dedent("""\
-            VERSION_1 {
-                local:
-                    *;
-            };
-        """))
-        parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, False)
-        parser.next_line()
-        version = parser.parse_version()
-        self.assertEqual([], version.symbols)
-
-    def test_missing_semicolon(self):
-        input_file = io.StringIO(textwrap.dedent("""\
-            VERSION_1 {
-                foo
-            };
-        """))
-        parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, False)
-        parser.next_line()
-        with self.assertRaises(gsl.ParseError):
-            parser.parse_version()
-
-    def test_parse_fails_invalid_input(self):
-        with self.assertRaises(gsl.ParseError):
-            input_file = io.StringIO('foo')
-            parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, False)
-            parser.parse()
-
-    def test_parse(self):
-        input_file = io.StringIO(textwrap.dedent("""\
-            VERSION_1 {
-                local:
-                    hidden1;
-                global:
-                    foo;
-                    bar; # baz
-            };
-
-            VERSION_2 { # wasd
-                # Implicit global scope.
-                    woodly;
-                    doodly; # asdf
-                local:
-                    qwerty;
-            } VERSION_1;
-        """))
-        parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, False)
-        versions = parser.parse()
-
-        expected = [
-            gsl.Version('VERSION_1', None, [], [
-                gsl.Symbol('foo', []),
-                gsl.Symbol('bar', ['baz']),
-            ]),
-            gsl.Version('VERSION_2', 'VERSION_1', ['wasd'], [
-                gsl.Symbol('woodly', []),
-                gsl.Symbol('doodly', ['asdf']),
-            ]),
-        ]
-
-        self.assertEqual(expected, versions)
-
-    def test_parse_llndk_apex_symbol(self):
-        input_file = io.StringIO(textwrap.dedent("""\
-            VERSION_1 {
-                foo;
-                bar; # llndk
-                baz; # llndk apex
-                qux; # apex
-            };
-        """))
-        parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, True)
-
-        parser.next_line()
-        version = parser.parse_version()
-        self.assertEqual('VERSION_1', version.name)
-        self.assertIsNone(version.base)
-
-        expected_symbols = [
-            gsl.Symbol('foo', []),
-            gsl.Symbol('bar', ['llndk']),
-            gsl.Symbol('baz', ['llndk', 'apex']),
-            gsl.Symbol('qux', ['apex']),
-        ]
-        self.assertEqual(expected_symbols, version.symbols)
-
-
-class GeneratorTest(unittest.TestCase):
-    def test_omit_version(self):
-        # Thorough testing of the cases involved here is handled by
-        # OmitVersionTest, PrivateVersionTest, and SymbolPresenceTest.
-        src_file = io.StringIO()
-        version_file = io.StringIO()
-        generator = gsl.Generator(src_file, version_file, 'arm', 9, False, False)
-
-        version = gsl.Version('VERSION_PRIVATE', None, [], [
-            gsl.Symbol('foo', []),
-        ])
-        generator.write_version(version)
-        self.assertEqual('', src_file.getvalue())
-        self.assertEqual('', version_file.getvalue())
-
-        version = gsl.Version('VERSION', None, ['x86'], [
-            gsl.Symbol('foo', []),
-        ])
-        generator.write_version(version)
-        self.assertEqual('', src_file.getvalue())
-        self.assertEqual('', version_file.getvalue())
-
-        version = gsl.Version('VERSION', None, ['introduced=14'], [
-            gsl.Symbol('foo', []),
-        ])
-        generator.write_version(version)
-        self.assertEqual('', src_file.getvalue())
-        self.assertEqual('', version_file.getvalue())
-
-    def test_omit_symbol(self):
-        # Thorough testing of the cases involved here is handled by
-        # SymbolPresenceTest.
-        src_file = io.StringIO()
-        version_file = io.StringIO()
-        generator = gsl.Generator(src_file, version_file, 'arm', 9, False, False)
-
-        version = gsl.Version('VERSION_1', None, [], [
-            gsl.Symbol('foo', ['x86']),
-        ])
-        generator.write_version(version)
-        self.assertEqual('', src_file.getvalue())
-        self.assertEqual('', version_file.getvalue())
-
-        version = gsl.Version('VERSION_1', None, [], [
-            gsl.Symbol('foo', ['introduced=14']),
-        ])
-        generator.write_version(version)
-        self.assertEqual('', src_file.getvalue())
-        self.assertEqual('', version_file.getvalue())
-
-        version = gsl.Version('VERSION_1', None, [], [
-            gsl.Symbol('foo', ['llndk']),
-        ])
-        generator.write_version(version)
-        self.assertEqual('', src_file.getvalue())
-        self.assertEqual('', version_file.getvalue())
-
-        version = gsl.Version('VERSION_1', None, [], [
-            gsl.Symbol('foo', ['apex']),
-        ])
-        generator.write_version(version)
-        self.assertEqual('', src_file.getvalue())
-        self.assertEqual('', version_file.getvalue())
-
-    def test_write(self):
-        src_file = io.StringIO()
-        version_file = io.StringIO()
-        generator = gsl.Generator(src_file, version_file, 'arm', 9, False, False)
-
-        versions = [
-            gsl.Version('VERSION_1', None, [], [
-                gsl.Symbol('foo', []),
-                gsl.Symbol('bar', ['var']),
-                gsl.Symbol('woodly', ['weak']),
-                gsl.Symbol('doodly', ['weak', 'var']),
-            ]),
-            gsl.Version('VERSION_2', 'VERSION_1', [], [
-                gsl.Symbol('baz', []),
-            ]),
-            gsl.Version('VERSION_3', 'VERSION_1', [], [
-                gsl.Symbol('qux', ['versioned=14']),
-            ]),
-        ]
-
-        generator.write(versions)
-        expected_src = textwrap.dedent("""\
-            void foo() {}
-            int bar = 0;
-            __attribute__((weak)) void woodly() {}
-            __attribute__((weak)) int doodly = 0;
-            void baz() {}
-            void qux() {}
-        """)
-        self.assertEqual(expected_src, src_file.getvalue())
-
-        expected_version = textwrap.dedent("""\
-            VERSION_1 {
-                global:
-                    foo;
-                    bar;
-                    woodly;
-                    doodly;
-            };
-            VERSION_2 {
-                global:
-                    baz;
-            } VERSION_1;
-        """)
-        self.assertEqual(expected_version, version_file.getvalue())
-
-
-class IntegrationTest(unittest.TestCase):
-    def test_integration(self):
-        api_map = {
-            'O': 9000,
-            'P': 9001,
-        }
-
-        input_file = io.StringIO(textwrap.dedent("""\
-            VERSION_1 {
-                global:
-                    foo; # var
-                    bar; # x86
-                    fizz; # introduced=O
-                    buzz; # introduced=P
-                local:
-                    *;
-            };
-
-            VERSION_2 { # arm
-                baz; # introduced=9
-                qux; # versioned=14
-            } VERSION_1;
-
-            VERSION_3 { # introduced=14
-                woodly;
-                doodly; # var
-            } VERSION_2;
-
-            VERSION_4 { # versioned=9
-                wibble;
-                wizzes; # llndk
-                waggle; # apex
-            } VERSION_2;
-
-            VERSION_5 { # versioned=14
-                wobble;
-            } VERSION_4;
-        """))
-        parser = gsl.SymbolFileParser(input_file, api_map, 'arm', 9, False, False)
-        versions = parser.parse()
-
-        src_file = io.StringIO()
-        version_file = io.StringIO()
-        generator = gsl.Generator(src_file, version_file, 'arm', 9, False, False)
-        generator.write(versions)
-
-        expected_src = textwrap.dedent("""\
-            int foo = 0;
-            void baz() {}
-            void qux() {}
-            void wibble() {}
-            void wobble() {}
-        """)
-        self.assertEqual(expected_src, src_file.getvalue())
-
-        expected_version = textwrap.dedent("""\
-            VERSION_1 {
-                global:
-                    foo;
-            };
-            VERSION_2 {
-                global:
-                    baz;
-            } VERSION_1;
-            VERSION_4 {
-                global:
-                    wibble;
-            } VERSION_2;
-        """)
-        self.assertEqual(expected_version, version_file.getvalue())
-
-    def test_integration_future_api(self):
-        api_map = {
-            'O': 9000,
-            'P': 9001,
-            'Q': 9002,
-        }
-
-        input_file = io.StringIO(textwrap.dedent("""\
-            VERSION_1 {
-                global:
-                    foo; # introduced=O
-                    bar; # introduced=P
-                    baz; # introduced=Q
-                local:
-                    *;
-            };
-        """))
-        parser = gsl.SymbolFileParser(input_file, api_map, 'arm', 9001, False, False)
-        versions = parser.parse()
-
-        src_file = io.StringIO()
-        version_file = io.StringIO()
-        generator = gsl.Generator(src_file, version_file, 'arm', 9001, False, False)
-        generator.write(versions)
-
-        expected_src = textwrap.dedent("""\
-            void foo() {}
-            void bar() {}
-        """)
-        self.assertEqual(expected_src, src_file.getvalue())
-
-        expected_version = textwrap.dedent("""\
-            VERSION_1 {
-                global:
-                    foo;
-                    bar;
-            };
-        """)
-        self.assertEqual(expected_version, version_file.getvalue())
-
-    def test_multiple_definition(self):
-        input_file = io.StringIO(textwrap.dedent("""\
-            VERSION_1 {
-                global:
-                    foo;
-                    foo;
-                    bar;
-                    baz;
-                    qux; # arm
-                local:
-                    *;
-            };
-
-            VERSION_2 {
-                global:
-                    bar;
-                    qux; # arm64
-            } VERSION_1;
-
-            VERSION_PRIVATE {
-                global:
-                    baz;
-            } VERSION_2;
-
-        """))
-        parser = gsl.SymbolFileParser(input_file, {}, 'arm', 16, False, False)
-
-        with self.assertRaises(gsl.MultiplyDefinedSymbolError) as cm:
-            parser.parse()
-        self.assertEquals(['bar', 'foo'],
-                          cm.exception.multiply_defined_symbols)
-
-    def test_integration_with_apex(self):
-        api_map = {
-            'O': 9000,
-            'P': 9001,
-        }
-
-        input_file = io.StringIO(textwrap.dedent("""\
-            VERSION_1 {
-                global:
-                    foo; # var
-                    bar; # x86
-                    fizz; # introduced=O
-                    buzz; # introduced=P
-                local:
-                    *;
-            };
-
-            VERSION_2 { # arm
-                baz; # introduced=9
-                qux; # versioned=14
-            } VERSION_1;
-
-            VERSION_3 { # introduced=14
-                woodly;
-                doodly; # var
-            } VERSION_2;
-
-            VERSION_4 { # versioned=9
-                wibble;
-                wizzes; # llndk
-                waggle; # apex
-                bubble; # apex llndk
-                duddle; # llndk apex
-            } VERSION_2;
-
-            VERSION_5 { # versioned=14
-                wobble;
-            } VERSION_4;
-        """))
-        parser = gsl.SymbolFileParser(input_file, api_map, 'arm', 9, False, True)
-        versions = parser.parse()
-
-        src_file = io.StringIO()
-        version_file = io.StringIO()
-        generator = gsl.Generator(src_file, version_file, 'arm', 9, False, True)
-        generator.write(versions)
-
-        expected_src = textwrap.dedent("""\
-            int foo = 0;
-            void baz() {}
-            void qux() {}
-            void wibble() {}
-            void waggle() {}
-            void bubble() {}
-            void duddle() {}
-            void wobble() {}
-        """)
-        self.assertEqual(expected_src, src_file.getvalue())
-
-        expected_version = textwrap.dedent("""\
-            VERSION_1 {
-                global:
-                    foo;
-            };
-            VERSION_2 {
-                global:
-                    baz;
-            } VERSION_1;
-            VERSION_4 {
-                global:
-                    wibble;
-                    waggle;
-                    bubble;
-                    duddle;
-            } VERSION_2;
-        """)
-        self.assertEqual(expected_version, version_file.getvalue())
-
-def main():
-    suite = unittest.TestLoader().loadTestsFromName(__name__)
-    unittest.TextTestRunner(verbosity=3).run(suite)
-
-
-if __name__ == '__main__':
-    main()
diff --git a/cc/scriptlib/test_ndk_api_coverage_parser.py b/cc/scriptlib/test_ndk_api_coverage_parser.py
deleted file mode 100644
index a3c9b70..0000000
--- a/cc/scriptlib/test_ndk_api_coverage_parser.py
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/usr/bin/env python
-#
-# Copyright (C) 2016 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.
-#
-"""Tests for ndk_api_coverage_parser.py."""
-import io
-import textwrap
-import unittest
-
-from xml.etree.ElementTree import tostring
-from gen_stub_libs import FUTURE_API_LEVEL, SymbolFileParser
-import ndk_api_coverage_parser as nparser
-
-
-# pylint: disable=missing-docstring
-
-
-class ApiCoverageSymbolFileParserTest(unittest.TestCase):
-    def test_parse(self):
-        input_file = io.StringIO(textwrap.dedent(u"""\
-            LIBLOG { # introduced-arm64=24 introduced-x86=24 introduced-x86_64=24
-              global:
-                android_name_to_log_id; # apex llndk introduced=23
-                android_log_id_to_name; # llndk arm
-                __android_log_assert; # introduced-x86=23
-                __android_log_buf_print; # var
-                __android_log_buf_write;
-              local:
-                *;
-            };
-            
-            LIBLOG_PLATFORM {
-                android_fdtrack; # llndk
-                android_net; # introduced=23
-            };
-            
-            LIBLOG_FOO { # var
-                android_var;
-            };
-        """))
-        parser = SymbolFileParser(input_file, {}, "", FUTURE_API_LEVEL, True, True)
-        generator = nparser.XmlGenerator(io.StringIO())
-        result = tostring(generator.convertToXml(parser.parse())).decode()
-        expected = '<ndk-library><symbol apex="True" arch="" introduced="23" introduced-arm64="24" introduced-x86="24" introduced-x86_64="24" is_deprecated="False" is_platform="False" llndk="True" name="android_name_to_log_id" /><symbol arch="arm" introduced-arm64="24" introduced-x86="24" introduced-x86_64="24" is_deprecated="False" is_platform="False" llndk="True" name="android_log_id_to_name" /><symbol arch="" introduced-arm64="24" introduced-x86="23" introduced-x86_64="24" is_deprecated="False" is_platform="False" name="__android_log_assert" /><symbol arch="" introduced-arm64="24" introduced-x86="24" introduced-x86_64="24" is_deprecated="False" is_platform="False" name="__android_log_buf_write" /><symbol arch="" is_deprecated="False" is_platform="True" llndk="True" name="android_fdtrack" /><symbol arch="" introduced="23" is_deprecated="False" is_platform="True" name="android_net" /></ndk-library>'
-        self.assertEqual(expected, result)
-
-
-def main():
-    suite = unittest.TestLoader().loadTestsFromName(__name__)
-    unittest.TextTestRunner(verbosity=3).run(suite)
-
-
-if __name__ == '__main__':
-    main()
diff --git a/cc/symbolfile/.gitignore b/cc/symbolfile/.gitignore
new file mode 100644
index 0000000..fd94eac
--- /dev/null
+++ b/cc/symbolfile/.gitignore
@@ -0,0 +1,140 @@
+# From https://github.com/github/gitignore/blob/master/Python.gitignore
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
diff --git a/cc/scriptlib/Android.bp b/cc/symbolfile/Android.bp
similarity index 69%
copy from cc/scriptlib/Android.bp
copy to cc/symbolfile/Android.bp
index ff9a2f0..5b43309 100644
--- a/cc/scriptlib/Android.bp
+++ b/cc/symbolfile/Android.bp
@@ -14,19 +14,20 @@
 // limitations under the License.
 //
 
-python_test_host {
-    name: "test_ndk_api_coverage_parser",
-    main: "test_ndk_api_coverage_parser.py",
+python_library_host {
+    name: "symbolfile",
+    pkg_path: "symbolfile",
     srcs: [
-        "test_ndk_api_coverage_parser.py",
+        "__init__.py",
     ],
 }
 
-python_binary_host {
-    name: "ndk_api_coverage_parser",
-    main: "ndk_api_coverage_parser.py",
+python_test_host {
+    name: "test_symbolfile",
     srcs: [
-        "gen_stub_libs.py",
-        "ndk_api_coverage_parser.py",
+        "test_symbolfile.py",
+    ],
+    libs: [
+        "symbolfile",
     ],
 }
diff --git a/cc/symbolfile/OWNERS b/cc/symbolfile/OWNERS
new file mode 100644
index 0000000..f0d8733
--- /dev/null
+++ b/cc/symbolfile/OWNERS
@@ -0,0 +1 @@
+danalbert@google.com
diff --git a/cc/scriptlib/gen_stub_libs.py b/cc/symbolfile/__init__.py
old mode 100755
new mode 100644
similarity index 72%
rename from cc/scriptlib/gen_stub_libs.py
rename to cc/symbolfile/__init__.py
index d61dfbb..faa3823
--- a/cc/scriptlib/gen_stub_libs.py
+++ b/cc/symbolfile/__init__.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
 #
 # Copyright (C) 2016 The Android Open Source Project
 #
@@ -14,13 +13,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
-"""Generates source for stub shared libraries for the NDK."""
-import argparse
-import json
+"""Parser for Android's version script information."""
 import logging
-import os
 import re
-import sys
 
 
 ALL_ARCHITECTURES = (
@@ -57,6 +52,24 @@
     return False
 
 
+def decode_api_level(api, api_map):
+    """Decodes the API level argument into the API level number.
+
+    For the average case, this just decodes the integer value from the string,
+    but for unreleased APIs we need to translate from the API codename (like
+    "O") to the future API level for that codename.
+    """
+    try:
+        return int(api)
+    except ValueError:
+        pass
+
+    if api == "current":
+        return FUTURE_API_LEVEL
+
+    return api_map[api]
+
+
 def decode_api_level_tags(tags, api_map):
     """Decodes API level code names in a list of tags.
 
@@ -118,7 +131,8 @@
     if 'platform-only' in version.tags:
         return True
 
-    no_llndk_no_apex = 'llndk' not in version.tags and 'apex' not in version.tags
+    no_llndk_no_apex = ('llndk' not in version.tags
+                        and 'apex' not in version.tags)
     keep = no_llndk_no_apex or \
            ('llndk' in version.tags and llndk) or \
            ('apex' in version.tags and apex)
@@ -205,7 +219,6 @@
 
 class ParseError(RuntimeError):
     """An error that occurred while parsing a symbol file."""
-    pass
 
 
 class MultiplyDefinedSymbolError(RuntimeError):
@@ -217,7 +230,7 @@
         self.multiply_defined_symbols = multiply_defined_symbols
 
 
-class Version(object):
+class Version:
     """A version block of a symbol file."""
     def __init__(self, name, base, tags, symbols):
         self.name = name
@@ -237,7 +250,7 @@
         return True
 
 
-class Symbol(object):
+class Symbol:
     """A symbol definition from a symbol file."""
     def __init__(self, name, tags):
         self.name = name
@@ -247,7 +260,7 @@
         return self.name == other.name and set(self.tags) == set(other.tags)
 
 
-class SymbolFileParser(object):
+class SymbolFileParser:
     """Parses NDK symbol files."""
     def __init__(self, input_file, api_map, arch, api, llndk, apex):
         self.input_file = input_file
@@ -283,11 +296,13 @@
         symbol_names = set()
         multiply_defined_symbols = set()
         for version in versions:
-            if should_omit_version(version, self.arch, self.api, self.llndk, self.apex):
+            if should_omit_version(version, self.arch, self.api, self.llndk,
+                                   self.apex):
                 continue
 
             for symbol in version.symbols:
-                if should_omit_symbol(symbol, self.arch, self.api, self.llndk, self.apex):
+                if should_omit_symbol(symbol, self.arch, self.api, self.llndk,
+                                      self.apex):
                     continue
 
                 if symbol.name in symbol_names:
@@ -367,141 +382,3 @@
                 break
         self.current_line = line
         return self.current_line
-
-
-class Generator(object):
-    """Output generator that writes stub source files and version scripts."""
-    def __init__(self, src_file, version_script, arch, api, llndk, apex):
-        self.src_file = src_file
-        self.version_script = version_script
-        self.arch = arch
-        self.api = api
-        self.llndk = llndk
-        self.apex = apex
-
-    def write(self, versions):
-        """Writes all symbol data to the output files."""
-        for version in versions:
-            self.write_version(version)
-
-    def write_version(self, version):
-        """Writes a single version block's data to the output files."""
-        if should_omit_version(version, self.arch, self.api, self.llndk, self.apex):
-            return
-
-        section_versioned = symbol_versioned_in_api(version.tags, self.api)
-        version_empty = True
-        pruned_symbols = []
-        for symbol in version.symbols:
-            if should_omit_symbol(symbol, self.arch, self.api, self.llndk, self.apex):
-                continue
-
-            if symbol_versioned_in_api(symbol.tags, self.api):
-                version_empty = False
-            pruned_symbols.append(symbol)
-
-        if len(pruned_symbols) > 0:
-            if not version_empty and section_versioned:
-                self.version_script.write(version.name + ' {\n')
-                self.version_script.write('    global:\n')
-            for symbol in pruned_symbols:
-                emit_version = symbol_versioned_in_api(symbol.tags, self.api)
-                if section_versioned and emit_version:
-                    self.version_script.write('        ' + symbol.name + ';\n')
-
-                weak = ''
-                if 'weak' in symbol.tags:
-                    weak = '__attribute__((weak)) '
-
-                if 'var' in symbol.tags:
-                    self.src_file.write('{}int {} = 0;\n'.format(
-                        weak, symbol.name))
-                else:
-                    self.src_file.write('{}void {}() {{}}\n'.format(
-                        weak, symbol.name))
-
-            if not version_empty and section_versioned:
-                base = '' if version.base is None else ' ' + version.base
-                self.version_script.write('}' + base + ';\n')
-
-
-def decode_api_level(api, api_map):
-    """Decodes the API level argument into the API level number.
-
-    For the average case, this just decodes the integer value from the string,
-    but for unreleased APIs we need to translate from the API codename (like
-    "O") to the future API level for that codename.
-    """
-    try:
-        return int(api)
-    except ValueError:
-        pass
-
-    if api == "current":
-        return FUTURE_API_LEVEL
-
-    return api_map[api]
-
-
-def parse_args():
-    """Parses and returns command line arguments."""
-    parser = argparse.ArgumentParser()
-
-    parser.add_argument('-v', '--verbose', action='count', default=0)
-
-    parser.add_argument(
-        '--api', required=True, help='API level being targeted.')
-    parser.add_argument(
-        '--arch', choices=ALL_ARCHITECTURES, required=True,
-        help='Architecture being targeted.')
-    parser.add_argument(
-        '--llndk', action='store_true', help='Use the LLNDK variant.')
-    parser.add_argument(
-        '--apex', action='store_true', help='Use the APEX variant.')
-
-    parser.add_argument(
-        '--api-map', type=os.path.realpath, required=True,
-        help='Path to the API level map JSON file.')
-
-    parser.add_argument(
-        'symbol_file', type=os.path.realpath, help='Path to symbol file.')
-    parser.add_argument(
-        'stub_src', type=os.path.realpath,
-        help='Path to output stub source file.')
-    parser.add_argument(
-        'version_script', type=os.path.realpath,
-        help='Path to output version script.')
-
-    return parser.parse_args()
-
-
-def main():
-    """Program entry point."""
-    args = parse_args()
-
-    with open(args.api_map) as map_file:
-        api_map = json.load(map_file)
-    api = decode_api_level(args.api, api_map)
-
-    verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG)
-    verbosity = args.verbose
-    if verbosity > 2:
-        verbosity = 2
-    logging.basicConfig(level=verbose_map[verbosity])
-
-    with open(args.symbol_file) as symbol_file:
-        try:
-            versions = SymbolFileParser(symbol_file, api_map, args.arch, api,
-                                        args.llndk, args.apex).parse()
-        except MultiplyDefinedSymbolError as ex:
-            sys.exit('{}: error: {}'.format(args.symbol_file, ex))
-
-    with open(args.stub_src, 'w') as src_file:
-        with open(args.version_script, 'w') as version_file:
-            generator = Generator(src_file, version_file, args.arch, api,
-                                  args.llndk, args.apex)
-            generator.write(versions)
-
-
-if __name__ == '__main__':
-    main()
diff --git a/cc/symbolfile/test_symbolfile.py b/cc/symbolfile/test_symbolfile.py
new file mode 100644
index 0000000..c91131f
--- /dev/null
+++ b/cc/symbolfile/test_symbolfile.py
@@ -0,0 +1,493 @@
+#
+# Copyright (C) 2016 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.
+#
+"""Tests for symbolfile."""
+import io
+import textwrap
+import unittest
+
+import symbolfile
+
+# pylint: disable=missing-docstring
+
+
+class DecodeApiLevelTest(unittest.TestCase):
+    def test_decode_api_level(self):
+        self.assertEqual(9, symbolfile.decode_api_level('9', {}))
+        self.assertEqual(9000, symbolfile.decode_api_level('O', {'O': 9000}))
+
+        with self.assertRaises(KeyError):
+            symbolfile.decode_api_level('O', {})
+
+
+class TagsTest(unittest.TestCase):
+    def test_get_tags_no_tags(self):
+        self.assertEqual([], symbolfile.get_tags(''))
+        self.assertEqual([], symbolfile.get_tags('foo bar baz'))
+
+    def test_get_tags(self):
+        self.assertEqual(['foo', 'bar'], symbolfile.get_tags('# foo bar'))
+        self.assertEqual(['bar', 'baz'], symbolfile.get_tags('foo # bar baz'))
+
+    def test_split_tag(self):
+        self.assertTupleEqual(('foo', 'bar'), symbolfile.split_tag('foo=bar'))
+        self.assertTupleEqual(('foo', 'bar=baz'), symbolfile.split_tag('foo=bar=baz'))
+        with self.assertRaises(ValueError):
+            symbolfile.split_tag('foo')
+
+    def test_get_tag_value(self):
+        self.assertEqual('bar', symbolfile.get_tag_value('foo=bar'))
+        self.assertEqual('bar=baz', symbolfile.get_tag_value('foo=bar=baz'))
+        with self.assertRaises(ValueError):
+            symbolfile.get_tag_value('foo')
+
+    def test_is_api_level_tag(self):
+        self.assertTrue(symbolfile.is_api_level_tag('introduced=24'))
+        self.assertTrue(symbolfile.is_api_level_tag('introduced-arm=24'))
+        self.assertTrue(symbolfile.is_api_level_tag('versioned=24'))
+
+        # Shouldn't try to process things that aren't a key/value tag.
+        self.assertFalse(symbolfile.is_api_level_tag('arm'))
+        self.assertFalse(symbolfile.is_api_level_tag('introduced'))
+        self.assertFalse(symbolfile.is_api_level_tag('versioned'))
+
+        # We don't support arch specific `versioned` tags.
+        self.assertFalse(symbolfile.is_api_level_tag('versioned-arm=24'))
+
+    def test_decode_api_level_tags(self):
+        api_map = {
+            'O': 9000,
+            'P': 9001,
+        }
+
+        tags = [
+            'introduced=9',
+            'introduced-arm=14',
+            'versioned=16',
+            'arm',
+            'introduced=O',
+            'introduced=P',
+        ]
+        expected_tags = [
+            'introduced=9',
+            'introduced-arm=14',
+            'versioned=16',
+            'arm',
+            'introduced=9000',
+            'introduced=9001',
+        ]
+        self.assertListEqual(
+            expected_tags, symbolfile.decode_api_level_tags(tags, api_map))
+
+        with self.assertRaises(symbolfile.ParseError):
+            symbolfile.decode_api_level_tags(['introduced=O'], {})
+
+
+class PrivateVersionTest(unittest.TestCase):
+    def test_version_is_private(self):
+        self.assertFalse(symbolfile.version_is_private('foo'))
+        self.assertFalse(symbolfile.version_is_private('PRIVATE'))
+        self.assertFalse(symbolfile.version_is_private('PLATFORM'))
+        self.assertFalse(symbolfile.version_is_private('foo_private'))
+        self.assertFalse(symbolfile.version_is_private('foo_platform'))
+        self.assertFalse(symbolfile.version_is_private('foo_PRIVATE_'))
+        self.assertFalse(symbolfile.version_is_private('foo_PLATFORM_'))
+
+        self.assertTrue(symbolfile.version_is_private('foo_PRIVATE'))
+        self.assertTrue(symbolfile.version_is_private('foo_PLATFORM'))
+
+
+class SymbolPresenceTest(unittest.TestCase):
+    def test_symbol_in_arch(self):
+        self.assertTrue(symbolfile.symbol_in_arch([], 'arm'))
+        self.assertTrue(symbolfile.symbol_in_arch(['arm'], 'arm'))
+
+        self.assertFalse(symbolfile.symbol_in_arch(['x86'], 'arm'))
+
+    def test_symbol_in_api(self):
+        self.assertTrue(symbolfile.symbol_in_api([], 'arm', 9))
+        self.assertTrue(symbolfile.symbol_in_api(['introduced=9'], 'arm', 9))
+        self.assertTrue(symbolfile.symbol_in_api(['introduced=9'], 'arm', 14))
+        self.assertTrue(symbolfile.symbol_in_api(['introduced-arm=9'], 'arm', 14))
+        self.assertTrue(symbolfile.symbol_in_api(['introduced-arm=9'], 'arm', 14))
+        self.assertTrue(symbolfile.symbol_in_api(['introduced-x86=14'], 'arm', 9))
+        self.assertTrue(symbolfile.symbol_in_api(
+            ['introduced-arm=9', 'introduced-x86=21'], 'arm', 14))
+        self.assertTrue(symbolfile.symbol_in_api(
+            ['introduced=9', 'introduced-x86=21'], 'arm', 14))
+        self.assertTrue(symbolfile.symbol_in_api(
+            ['introduced=21', 'introduced-arm=9'], 'arm', 14))
+        self.assertTrue(symbolfile.symbol_in_api(
+            ['future'], 'arm', symbolfile.FUTURE_API_LEVEL))
+
+        self.assertFalse(symbolfile.symbol_in_api(['introduced=14'], 'arm', 9))
+        self.assertFalse(symbolfile.symbol_in_api(['introduced-arm=14'], 'arm', 9))
+        self.assertFalse(symbolfile.symbol_in_api(['future'], 'arm', 9))
+        self.assertFalse(symbolfile.symbol_in_api(
+            ['introduced=9', 'future'], 'arm', 14))
+        self.assertFalse(symbolfile.symbol_in_api(
+            ['introduced-arm=9', 'future'], 'arm', 14))
+        self.assertFalse(symbolfile.symbol_in_api(
+            ['introduced-arm=21', 'introduced-x86=9'], 'arm', 14))
+        self.assertFalse(symbolfile.symbol_in_api(
+            ['introduced=9', 'introduced-arm=21'], 'arm', 14))
+        self.assertFalse(symbolfile.symbol_in_api(
+            ['introduced=21', 'introduced-x86=9'], 'arm', 14))
+
+        # Interesting edge case: this symbol should be omitted from the
+        # library, but this call should still return true because none of the
+        # tags indiciate that it's not present in this API level.
+        self.assertTrue(symbolfile.symbol_in_api(['x86'], 'arm', 9))
+
+    def test_verioned_in_api(self):
+        self.assertTrue(symbolfile.symbol_versioned_in_api([], 9))
+        self.assertTrue(symbolfile.symbol_versioned_in_api(['versioned=9'], 9))
+        self.assertTrue(symbolfile.symbol_versioned_in_api(['versioned=9'], 14))
+
+        self.assertFalse(symbolfile.symbol_versioned_in_api(['versioned=14'], 9))
+
+
+class OmitVersionTest(unittest.TestCase):
+    def test_omit_private(self):
+        self.assertFalse(
+            symbolfile.should_omit_version(
+                symbolfile.Version('foo', None, [], []), 'arm', 9, False,
+                False))
+
+        self.assertTrue(
+            symbolfile.should_omit_version(
+                symbolfile.Version('foo_PRIVATE', None, [], []), 'arm', 9,
+                False, False))
+        self.assertTrue(
+            symbolfile.should_omit_version(
+                symbolfile.Version('foo_PLATFORM', None, [], []), 'arm', 9,
+                False, False))
+
+        self.assertTrue(
+            symbolfile.should_omit_version(
+                symbolfile.Version('foo', None, ['platform-only'], []), 'arm',
+                9, False, False))
+
+    def test_omit_llndk(self):
+        self.assertTrue(
+            symbolfile.should_omit_version(
+                symbolfile.Version('foo', None, ['llndk'], []), 'arm', 9,
+                False, False))
+
+        self.assertFalse(
+            symbolfile.should_omit_version(
+                symbolfile.Version('foo', None, [], []), 'arm', 9, True,
+                False))
+        self.assertFalse(
+            symbolfile.should_omit_version(
+                symbolfile.Version('foo', None, ['llndk'], []), 'arm', 9, True,
+                False))
+
+    def test_omit_apex(self):
+        self.assertTrue(
+            symbolfile.should_omit_version(
+                symbolfile.Version('foo', None, ['apex'], []), 'arm', 9, False,
+                False))
+
+        self.assertFalse(
+            symbolfile.should_omit_version(
+                symbolfile.Version('foo', None, [], []), 'arm', 9, False,
+                True))
+        self.assertFalse(
+            symbolfile.should_omit_version(
+                symbolfile.Version('foo', None, ['apex'], []), 'arm', 9, False,
+                True))
+
+    def test_omit_arch(self):
+        self.assertFalse(
+            symbolfile.should_omit_version(
+                symbolfile.Version('foo', None, [], []), 'arm', 9, False,
+                False))
+        self.assertFalse(
+            symbolfile.should_omit_version(
+                symbolfile.Version('foo', None, ['arm'], []), 'arm', 9, False,
+                False))
+
+        self.assertTrue(
+            symbolfile.should_omit_version(
+                symbolfile.Version('foo', None, ['x86'], []), 'arm', 9, False,
+                False))
+
+    def test_omit_api(self):
+        self.assertFalse(
+            symbolfile.should_omit_version(
+                symbolfile.Version('foo', None, [], []), 'arm', 9, False,
+                False))
+        self.assertFalse(
+            symbolfile.should_omit_version(
+                symbolfile.Version('foo', None, ['introduced=9'], []), 'arm',
+                9, False, False))
+
+        self.assertTrue(
+            symbolfile.should_omit_version(
+                symbolfile.Version('foo', None, ['introduced=14'], []), 'arm',
+                9, False, False))
+
+
+class OmitSymbolTest(unittest.TestCase):
+    def test_omit_llndk(self):
+        self.assertTrue(
+            symbolfile.should_omit_symbol(symbolfile.Symbol('foo', ['llndk']),
+                                          'arm', 9, False, False))
+
+        self.assertFalse(
+            symbolfile.should_omit_symbol(symbolfile.Symbol('foo', []), 'arm',
+                                          9, True, False))
+        self.assertFalse(
+            symbolfile.should_omit_symbol(symbolfile.Symbol('foo', ['llndk']),
+                                          'arm', 9, True, False))
+
+    def test_omit_apex(self):
+        self.assertTrue(
+            symbolfile.should_omit_symbol(symbolfile.Symbol('foo', ['apex']),
+                                          'arm', 9, False, False))
+
+        self.assertFalse(
+            symbolfile.should_omit_symbol(symbolfile.Symbol('foo', []), 'arm',
+                                          9, False, True))
+        self.assertFalse(
+            symbolfile.should_omit_symbol(symbolfile.Symbol('foo', ['apex']),
+                                          'arm', 9, False, True))
+
+    def test_omit_arch(self):
+        self.assertFalse(
+            symbolfile.should_omit_symbol(symbolfile.Symbol('foo', []), 'arm',
+                                          9, False, False))
+        self.assertFalse(
+            symbolfile.should_omit_symbol(symbolfile.Symbol('foo', ['arm']),
+                                          'arm', 9, False, False))
+
+        self.assertTrue(
+            symbolfile.should_omit_symbol(symbolfile.Symbol('foo', ['x86']),
+                                          'arm', 9, False, False))
+
+    def test_omit_api(self):
+        self.assertFalse(
+            symbolfile.should_omit_symbol(symbolfile.Symbol('foo', []), 'arm',
+                                          9, False, False))
+        self.assertFalse(
+            symbolfile.should_omit_symbol(
+                symbolfile.Symbol('foo', ['introduced=9']), 'arm', 9, False,
+                False))
+
+        self.assertTrue(
+            symbolfile.should_omit_symbol(
+                symbolfile.Symbol('foo', ['introduced=14']), 'arm', 9, False,
+                False))
+
+
+class SymbolFileParseTest(unittest.TestCase):
+    def test_next_line(self):
+        input_file = io.StringIO(textwrap.dedent("""\
+            foo
+
+            bar
+            # baz
+            qux
+        """))
+        parser = symbolfile.SymbolFileParser(input_file, {}, 'arm', 16, False, False)
+        self.assertIsNone(parser.current_line)
+
+        self.assertEqual('foo', parser.next_line().strip())
+        self.assertEqual('foo', parser.current_line.strip())
+
+        self.assertEqual('bar', parser.next_line().strip())
+        self.assertEqual('bar', parser.current_line.strip())
+
+        self.assertEqual('qux', parser.next_line().strip())
+        self.assertEqual('qux', parser.current_line.strip())
+
+        self.assertEqual('', parser.next_line())
+        self.assertEqual('', parser.current_line)
+
+    def test_parse_version(self):
+        input_file = io.StringIO(textwrap.dedent("""\
+            VERSION_1 { # foo bar
+                baz;
+                qux; # woodly doodly
+            };
+
+            VERSION_2 {
+            } VERSION_1; # asdf
+        """))
+        parser = symbolfile.SymbolFileParser(input_file, {}, 'arm', 16, False, False)
+
+        parser.next_line()
+        version = parser.parse_version()
+        self.assertEqual('VERSION_1', version.name)
+        self.assertIsNone(version.base)
+        self.assertEqual(['foo', 'bar'], version.tags)
+
+        expected_symbols = [
+            symbolfile.Symbol('baz', []),
+            symbolfile.Symbol('qux', ['woodly', 'doodly']),
+        ]
+        self.assertEqual(expected_symbols, version.symbols)
+
+        parser.next_line()
+        version = parser.parse_version()
+        self.assertEqual('VERSION_2', version.name)
+        self.assertEqual('VERSION_1', version.base)
+        self.assertEqual([], version.tags)
+
+    def test_parse_version_eof(self):
+        input_file = io.StringIO(textwrap.dedent("""\
+            VERSION_1 {
+        """))
+        parser = symbolfile.SymbolFileParser(input_file, {}, 'arm', 16, False, False)
+        parser.next_line()
+        with self.assertRaises(symbolfile.ParseError):
+            parser.parse_version()
+
+    def test_unknown_scope_label(self):
+        input_file = io.StringIO(textwrap.dedent("""\
+            VERSION_1 {
+                foo:
+            }
+        """))
+        parser = symbolfile.SymbolFileParser(input_file, {}, 'arm', 16, False, False)
+        parser.next_line()
+        with self.assertRaises(symbolfile.ParseError):
+            parser.parse_version()
+
+    def test_parse_symbol(self):
+        input_file = io.StringIO(textwrap.dedent("""\
+            foo;
+            bar; # baz qux
+        """))
+        parser = symbolfile.SymbolFileParser(input_file, {}, 'arm', 16, False, False)
+
+        parser.next_line()
+        symbol = parser.parse_symbol()
+        self.assertEqual('foo', symbol.name)
+        self.assertEqual([], symbol.tags)
+
+        parser.next_line()
+        symbol = parser.parse_symbol()
+        self.assertEqual('bar', symbol.name)
+        self.assertEqual(['baz', 'qux'], symbol.tags)
+
+    def test_wildcard_symbol_global(self):
+        input_file = io.StringIO(textwrap.dedent("""\
+            VERSION_1 {
+                *;
+            };
+        """))
+        parser = symbolfile.SymbolFileParser(input_file, {}, 'arm', 16, False, False)
+        parser.next_line()
+        with self.assertRaises(symbolfile.ParseError):
+            parser.parse_version()
+
+    def test_wildcard_symbol_local(self):
+        input_file = io.StringIO(textwrap.dedent("""\
+            VERSION_1 {
+                local:
+                    *;
+            };
+        """))
+        parser = symbolfile.SymbolFileParser(input_file, {}, 'arm', 16, False, False)
+        parser.next_line()
+        version = parser.parse_version()
+        self.assertEqual([], version.symbols)
+
+    def test_missing_semicolon(self):
+        input_file = io.StringIO(textwrap.dedent("""\
+            VERSION_1 {
+                foo
+            };
+        """))
+        parser = symbolfile.SymbolFileParser(input_file, {}, 'arm', 16, False, False)
+        parser.next_line()
+        with self.assertRaises(symbolfile.ParseError):
+            parser.parse_version()
+
+    def test_parse_fails_invalid_input(self):
+        with self.assertRaises(symbolfile.ParseError):
+            input_file = io.StringIO('foo')
+            parser = symbolfile.SymbolFileParser(input_file, {}, 'arm', 16,
+                                                 False, False)
+            parser.parse()
+
+    def test_parse(self):
+        input_file = io.StringIO(textwrap.dedent("""\
+            VERSION_1 {
+                local:
+                    hidden1;
+                global:
+                    foo;
+                    bar; # baz
+            };
+
+            VERSION_2 { # wasd
+                # Implicit global scope.
+                    woodly;
+                    doodly; # asdf
+                local:
+                    qwerty;
+            } VERSION_1;
+        """))
+        parser = symbolfile.SymbolFileParser(input_file, {}, 'arm', 16, False, False)
+        versions = parser.parse()
+
+        expected = [
+            symbolfile.Version('VERSION_1', None, [], [
+                symbolfile.Symbol('foo', []),
+                symbolfile.Symbol('bar', ['baz']),
+            ]),
+            symbolfile.Version('VERSION_2', 'VERSION_1', ['wasd'], [
+                symbolfile.Symbol('woodly', []),
+                symbolfile.Symbol('doodly', ['asdf']),
+            ]),
+        ]
+
+        self.assertEqual(expected, versions)
+
+    def test_parse_llndk_apex_symbol(self):
+        input_file = io.StringIO(textwrap.dedent("""\
+            VERSION_1 {
+                foo;
+                bar; # llndk
+                baz; # llndk apex
+                qux; # apex
+            };
+        """))
+        parser = symbolfile.SymbolFileParser(input_file, {}, 'arm', 16, False, True)
+
+        parser.next_line()
+        version = parser.parse_version()
+        self.assertEqual('VERSION_1', version.name)
+        self.assertIsNone(version.base)
+
+        expected_symbols = [
+            symbolfile.Symbol('foo', []),
+            symbolfile.Symbol('bar', ['llndk']),
+            symbolfile.Symbol('baz', ['llndk', 'apex']),
+            symbolfile.Symbol('qux', ['apex']),
+        ]
+        self.assertEqual(expected_symbols, version.symbols)
+
+
+def main():
+    suite = unittest.TestLoader().loadTestsFromName(__name__)
+    unittest.TextTestRunner(verbosity=3).run(suite)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/cc/testing.go b/cc/testing.go
index a106d46..4d0b28b 100644
--- a/cc/testing.go
+++ b/cc/testing.go
@@ -30,6 +30,7 @@
 	ctx.RegisterModuleType("toolchain_library", ToolchainLibraryFactory)
 	ctx.RegisterModuleType("llndk_library", LlndkLibraryFactory)
 	ctx.RegisterModuleType("cc_object", ObjectFactory)
+	ctx.RegisterModuleType("cc_genrule", genRuleFactory)
 	ctx.RegisterModuleType("ndk_prebuilt_shared_stl", NdkPrebuiltSharedStlFactory)
 	ctx.RegisterModuleType("ndk_prebuilt_object", NdkPrebuiltObjectFactory)
 	ctx.RegisterModuleType("ndk_library", NdkLibraryFactory)
@@ -39,6 +40,7 @@
 	ret := `
 		toolchain_library {
 			name: "libatomic",
+			defaults: ["linux_bionic_supported"],
 			vendor_available: true,
 			recovery_available: true,
 			native_bridge_supported: true,
@@ -92,6 +94,7 @@
 
 		toolchain_library {
 			name: "libclang_rt.builtins-x86_64-android",
+			defaults: ["linux_bionic_supported"],
 			vendor_available: true,
 			recovery_available: true,
 			native_bridge_supported: true,
@@ -121,6 +124,7 @@
 
 		toolchain_library {
 			name: "libclang_rt.fuzzer-x86_64-android",
+			defaults: ["linux_bionic_supported"],
 			vendor_available: true,
 			recovery_available: true,
 			src: "",
@@ -144,6 +148,7 @@
 
 		toolchain_library {
 			name: "libgcc",
+			defaults: ["linux_bionic_supported"],
 			vendor_available: true,
 			recovery_available: true,
 			src: "",
@@ -151,6 +156,7 @@
 
 		toolchain_library {
 			name: "libgcc_stripped",
+			defaults: ["linux_bionic_supported"],
 			vendor_available: true,
 			recovery_available: true,
 			sdk_version: "current",
@@ -159,6 +165,7 @@
 
 		cc_library {
 			name: "libc",
+			defaults: ["linux_bionic_supported"],
 			no_libcrt: true,
 			nocrt: true,
 			stl: "none",
@@ -175,6 +182,7 @@
 		}
 		cc_library {
 			name: "libm",
+			defaults: ["linux_bionic_supported"],
 			no_libcrt: true,
 			nocrt: true,
 			stl: "none",
@@ -234,6 +242,7 @@
 
 		cc_library {
 			name: "libdl",
+			defaults: ["linux_bionic_supported"],
 			no_libcrt: true,
 			nocrt: true,
 			stl: "none",
@@ -326,6 +335,7 @@
 
 		cc_defaults {
 			name: "crt_defaults",
+			defaults: ["linux_bionic_supported"],
 			recovery_available: true,
 			vendor_available: true,
 			native_bridge_supported: true,
@@ -437,6 +447,7 @@
 		}
 	`
 
+	supportLinuxBionic := false
 	for _, os := range oses {
 		if os == android.Fuchsia {
 			ret += `
@@ -465,7 +476,59 @@
 		}
 		`
 		}
+		if os == android.LinuxBionic {
+			supportLinuxBionic = true
+			ret += `
+				cc_binary {
+					name: "linker",
+					defaults: ["linux_bionic_supported"],
+					recovery_available: true,
+					stl: "none",
+					nocrt: true,
+					static_executable: true,
+					native_coverage: false,
+					system_shared_libs: [],
+				}
+
+				cc_genrule {
+					name: "host_bionic_linker_flags",
+					host_supported: true,
+					device_supported: false,
+					target: {
+						host: {
+							enabled: false,
+						},
+						linux_bionic: {
+							enabled: true,
+						},
+					},
+					out: ["linker.flags"],
+				}
+
+				cc_defaults {
+					name: "linux_bionic_supported",
+					host_supported: true,
+					target: {
+						host: {
+							enabled: false,
+						},
+						linux_bionic: {
+							enabled: true,
+						},
+					},
+				}
+			`
+		}
 	}
+
+	if !supportLinuxBionic {
+		ret += `
+			cc_defaults {
+				name: "linux_bionic_supported",
+			}
+		`
+	}
+
 	return ret
 }
 
diff --git a/java/androidmk.go b/java/androidmk.go
index 618e15d..03994bf 100644
--- a/java/androidmk.go
+++ b/java/androidmk.go
@@ -131,6 +131,10 @@
 						entries.SetPath("LOCAL_SOONG_PROGUARD_DICT", library.proguardDictionary)
 					}
 					entries.SetString("LOCAL_MODULE_STEM", library.Stem())
+
+					entries.AddOptionalPath("LOCAL_SOONG_LINT_REPORTS", library.linter.outputs.transitiveHTMLZip)
+					entries.AddOptionalPath("LOCAL_SOONG_LINT_REPORTS", library.linter.outputs.transitiveTextZip)
+					entries.AddOptionalPath("LOCAL_SOONG_LINT_REPORTS", library.linter.outputs.transitiveXMLZip)
 				},
 			},
 		}
@@ -370,9 +374,15 @@
 				entries.SetString("LOCAL_CERTIFICATE", app.certificate.AndroidMkString())
 				entries.AddStrings("LOCAL_OVERRIDES_PACKAGES", app.getOverriddenPackages()...)
 
-				for _, jniLib := range app.installJniLibs {
-					entries.AddStrings("LOCAL_SOONG_JNI_LIBS_"+jniLib.target.Arch.ArchType.String(), jniLib.name)
+				if app.embeddedJniLibs {
+					jniSymbols := app.JNISymbolsInstalls(app.installPathForJNISymbols.String())
+					entries.SetString("LOCAL_SOONG_JNI_LIBS_SYMBOLS", jniSymbols.String())
+				} else {
+					for _, jniLib := range app.jniLibs {
+						entries.AddStrings("LOCAL_SOONG_JNI_LIBS_"+jniLib.target.Arch.ArchType.String(), jniLib.name)
+					}
 				}
+
 				if len(app.jniCoverageOutputs) > 0 {
 					entries.AddStrings("LOCAL_PREBUILT_COVERAGE_ARCHIVE", app.jniCoverageOutputs.Strings()...)
 				}
@@ -383,6 +393,10 @@
 					install := app.onDeviceDir + "/" + extra.Base()
 					entries.AddStrings("LOCAL_SOONG_BUILT_INSTALLED", extra.String()+":"+install)
 				}
+
+				entries.AddOptionalPath("LOCAL_SOONG_LINT_REPORTS", app.linter.outputs.transitiveHTMLZip)
+				entries.AddOptionalPath("LOCAL_SOONG_LINT_REPORTS", app.linter.outputs.transitiveTextZip)
+				entries.AddOptionalPath("LOCAL_SOONG_LINT_REPORTS", app.linter.outputs.transitiveXMLZip)
 			},
 		},
 		ExtraFooters: []android.AndroidMkExtraFootersFunc{
diff --git a/java/app.go b/java/app.go
index 4071c0a..4031cfe 100755
--- a/java/app.go
+++ b/java/app.go
@@ -292,8 +292,10 @@
 
 	overridableAppProperties overridableAppProperties
 
-	installJniLibs     []jniLib
-	jniCoverageOutputs android.Paths
+	jniLibs                  []jniLib
+	installPathForJNISymbols android.Path
+	embeddedJniLibs          bool
+	jniCoverageOutputs       android.Paths
 
 	bundleFile android.Path
 
@@ -570,8 +572,7 @@
 	a.Module.extraProguardFlagFiles = append(a.Module.extraProguardFlagFiles, a.proguardOptionsFile)
 }
 
-func (a *AndroidApp) dexBuildActions(ctx android.ModuleContext) android.Path {
-
+func (a *AndroidApp) installPath(ctx android.ModuleContext) android.InstallPath {
 	var installDir string
 	if ctx.ModuleName() == "framework-res" {
 		// framework-res.apk is installed as system/framework/framework-res.apk
@@ -581,7 +582,12 @@
 	} else {
 		installDir = filepath.Join("app", a.installApkName)
 	}
-	a.dexpreopter.installPath = android.PathForModuleInstall(ctx, installDir, a.installApkName+".apk")
+
+	return android.PathForModuleInstall(ctx, installDir, a.installApkName+".apk")
+}
+
+func (a *AndroidApp) dexBuildActions(ctx android.ModuleContext) android.Path {
+	a.dexpreopter.installPath = a.installPath(ctx)
 	if a.deviceProperties.Uncompress_dex == nil {
 		// If the value was not force-set by the user, use reasonable default based on the module.
 		a.deviceProperties.Uncompress_dex = proptools.BoolPtr(a.shouldUncompressDex(ctx))
@@ -603,8 +609,10 @@
 func (a *AndroidApp) jniBuildActions(jniLibs []jniLib, ctx android.ModuleContext) android.WritablePath {
 	var jniJarFile android.WritablePath
 	if len(jniLibs) > 0 {
+		a.jniLibs = jniLibs
 		if a.shouldEmbedJnis(ctx) {
 			jniJarFile = android.PathForModuleOut(ctx, "jnilibs.zip")
+			a.installPathForJNISymbols = a.installPath(ctx).ToMakePath()
 			TransformJniLibsToJar(ctx, jniJarFile, jniLibs, a.useEmbeddedNativeLibs(ctx))
 			for _, jni := range jniLibs {
 				if jni.coverageFile.Valid() {
@@ -622,13 +630,25 @@
 					}
 				}
 			}
-		} else {
-			a.installJniLibs = jniLibs
+			a.embeddedJniLibs = true
 		}
 	}
 	return jniJarFile
 }
 
+func (a *AndroidApp) JNISymbolsInstalls(installPath string) android.RuleBuilderInstalls {
+	var jniSymbols android.RuleBuilderInstalls
+	for _, jniLib := range a.jniLibs {
+		if jniLib.unstrippedFile != nil {
+			jniSymbols = append(jniSymbols, android.RuleBuilderInstall{
+				From: jniLib.unstrippedFile,
+				To:   filepath.Join(installPath, targetToJniDir(jniLib.target), jniLib.unstrippedFile.Base()),
+			})
+		}
+	}
+	return jniSymbols
+}
+
 func (a *AndroidApp) noticeBuildActions(ctx android.ModuleContext) {
 	// Collect NOTICE files from all dependencies.
 	seenModules := make(map[android.Module]bool)
@@ -756,6 +776,7 @@
 	a.linter.mergedManifest = a.aapt.mergedManifestFile
 	a.linter.manifest = a.aapt.manifestPath
 	a.linter.resources = a.aapt.resourceFiles
+	a.linter.buildModuleReportZip = ctx.Config().UnbundledBuildApps()
 
 	dexJarFile := a.dexBuildActions(ctx)
 
@@ -844,10 +865,11 @@
 
 				if lib.Valid() {
 					jniLibs = append(jniLibs, jniLib{
-						name:         ctx.OtherModuleName(module),
-						path:         path,
-						target:       module.Target(),
-						coverageFile: dep.CoverageOutputFile(),
+						name:           ctx.OtherModuleName(module),
+						path:           path,
+						target:         module.Target(),
+						coverageFile:   dep.CoverageOutputFile(),
+						unstrippedFile: dep.UnstrippedOutputFile(),
 					})
 				} else {
 					ctx.ModuleErrorf("dependency %q missing output file", otherName)
diff --git a/java/java.go b/java/java.go
index 46ef98b..367b09c 100644
--- a/java/java.go
+++ b/java/java.go
@@ -635,10 +635,11 @@
 }
 
 type jniLib struct {
-	name         string
-	path         android.Path
-	target       android.Target
-	coverageFile android.OptionalPath
+	name           string
+	path           android.Path
+	target         android.Target
+	coverageFile   android.OptionalPath
+	unstrippedFile android.Path
 }
 
 func (j *Module) shouldInstrument(ctx android.BaseModuleContext) bool {
diff --git a/java/lint.go b/java/lint.go
index 6fbef18..20a7dc4 100644
--- a/java/lint.go
+++ b/java/lint.go
@@ -67,12 +67,32 @@
 	kotlinLanguageLevel string
 	outputs             lintOutputs
 	properties          LintProperties
+
+	buildModuleReportZip bool
 }
 
 type lintOutputs struct {
 	html android.ModuleOutPath
 	text android.ModuleOutPath
 	xml  android.ModuleOutPath
+
+	transitiveHTML *android.DepSet
+	transitiveText *android.DepSet
+	transitiveXML  *android.DepSet
+
+	transitiveHTMLZip android.OptionalPath
+	transitiveTextZip android.OptionalPath
+	transitiveXMLZip  android.OptionalPath
+}
+
+type lintOutputIntf interface {
+	lintOutputs() *lintOutputs
+}
+
+var _ lintOutputIntf = (*linter)(nil)
+
+func (l *linter) lintOutputs() *lintOutputs {
+	return &l.outputs
 }
 
 func (l *linter) enabled() bool {
@@ -213,9 +233,22 @@
 
 	projectXML, lintXML, cacheDir, homeDir, deps := l.writeLintProjectXML(ctx, rule)
 
-	l.outputs.html = android.PathForModuleOut(ctx, "lint-report.html")
-	l.outputs.text = android.PathForModuleOut(ctx, "lint-report.txt")
-	l.outputs.xml = android.PathForModuleOut(ctx, "lint-report.xml")
+	html := android.PathForModuleOut(ctx, "lint-report.html")
+	text := android.PathForModuleOut(ctx, "lint-report.txt")
+	xml := android.PathForModuleOut(ctx, "lint-report.xml")
+
+	htmlDeps := android.NewDepSetBuilder(android.POSTORDER).Direct(html)
+	textDeps := android.NewDepSetBuilder(android.POSTORDER).Direct(text)
+	xmlDeps := android.NewDepSetBuilder(android.POSTORDER).Direct(xml)
+
+	ctx.VisitDirectDepsWithTag(staticLibTag, func(dep android.Module) {
+		if depLint, ok := dep.(lintOutputIntf); ok {
+			depLintOutputs := depLint.lintOutputs()
+			htmlDeps.Transitive(depLintOutputs.transitiveHTML)
+			textDeps.Transitive(depLintOutputs.transitiveText)
+			xmlDeps.Transitive(depLintOutputs.transitiveXML)
+		}
+	})
 
 	rule.Command().Text("rm -rf").Flag(cacheDir.String()).Flag(homeDir.String())
 	rule.Command().Text("mkdir -p").Flag(cacheDir.String()).Flag(homeDir.String())
@@ -240,9 +273,9 @@
 		Flag("--quiet").
 		FlagWithInput("--project ", projectXML).
 		FlagWithInput("--config ", lintXML).
-		FlagWithOutput("--html ", l.outputs.html).
-		FlagWithOutput("--text ", l.outputs.text).
-		FlagWithOutput("--xml ", l.outputs.xml).
+		FlagWithOutput("--html ", html).
+		FlagWithOutput("--text ", text).
+		FlagWithOutput("--xml ", xml).
 		FlagWithArg("--compile-sdk-version ", l.compileSdkVersion).
 		FlagWithArg("--java-language-level ", l.javaLanguageLevel).
 		FlagWithArg("--kotlin-language-level ", l.kotlinLanguageLevel).
@@ -250,23 +283,37 @@
 		Flag("--exitcode").
 		Flags(l.properties.Lint.Flags).
 		Implicits(deps).
-		Text("|| (").Text("cat").Input(l.outputs.text).Text("; exit 7)").
+		Text("|| (").Text("cat").Input(text).Text("; exit 7)").
 		Text(")")
 
 	rule.Command().Text("rm -rf").Flag(cacheDir.String()).Flag(homeDir.String())
 
 	rule.Build(pctx, ctx, "lint", "lint")
-}
 
-func (l *linter) lintOutputs() *lintOutputs {
-	return &l.outputs
-}
+	l.outputs = lintOutputs{
+		html: html,
+		text: text,
+		xml:  xml,
 
-type lintOutputIntf interface {
-	lintOutputs() *lintOutputs
-}
+		transitiveHTML: htmlDeps.Build(),
+		transitiveText: textDeps.Build(),
+		transitiveXML:  xmlDeps.Build(),
+	}
 
-var _ lintOutputIntf = (*linter)(nil)
+	if l.buildModuleReportZip {
+		htmlZip := android.PathForModuleOut(ctx, "lint-report-html.zip")
+		l.outputs.transitiveHTMLZip = android.OptionalPathForPath(htmlZip)
+		lintZip(ctx, l.outputs.transitiveHTML.ToSortedList(), htmlZip)
+
+		textZip := android.PathForModuleOut(ctx, "lint-report-text.zip")
+		l.outputs.transitiveTextZip = android.OptionalPathForPath(textZip)
+		lintZip(ctx, l.outputs.transitiveText.ToSortedList(), textZip)
+
+		xmlZip := android.PathForModuleOut(ctx, "lint-report-xml.zip")
+		l.outputs.transitiveXMLZip = android.OptionalPathForPath(xmlZip)
+		lintZip(ctx, l.outputs.transitiveXML.ToSortedList(), xmlZip)
+	}
+}
 
 type lintSingleton struct {
 	htmlZip android.WritablePath
@@ -356,18 +403,7 @@
 			paths = append(paths, get(output))
 		}
 
-		sort.Slice(paths, func(i, j int) bool {
-			return paths[i].String() < paths[j].String()
-		})
-
-		rule := android.NewRuleBuilder()
-
-		rule.Command().BuiltTool(ctx, "soong_zip").
-			FlagWithOutput("-o ", outputPath).
-			FlagWithArg("-C ", android.PathForIntermediates(ctx).String()).
-			FlagWithRspFileInputList("-l ", paths)
-
-		rule.Build(pctx, ctx, outputPath.Base(), outputPath.Base())
+		lintZip(ctx, paths, outputPath)
 	}
 
 	l.htmlZip = android.PathForOutput(ctx, "lint-report-html.zip")
@@ -394,3 +430,20 @@
 	android.RegisterSingletonType("lint",
 		func() android.Singleton { return &lintSingleton{} })
 }
+
+func lintZip(ctx android.BuilderContext, paths android.Paths, outputPath android.WritablePath) {
+	paths = android.SortedUniquePaths(android.CopyOfPaths(paths))
+
+	sort.Slice(paths, func(i, j int) bool {
+		return paths[i].String() < paths[j].String()
+	})
+
+	rule := android.NewRuleBuilder()
+
+	rule.Command().BuiltTool(ctx, "soong_zip").
+		FlagWithOutput("-o ", outputPath).
+		FlagWithArg("-C ", android.PathForIntermediates(ctx).String()).
+		FlagWithRspFileInputList("-l ", paths)
+
+	rule.Build(pctx, ctx, outputPath.Base(), outputPath.Base())
+}
diff --git a/sdk/cc_sdk_test.go b/sdk/cc_sdk_test.go
index 2dd23c7..935d348 100644
--- a/sdk/cc_sdk_test.go
+++ b/sdk/cc_sdk_test.go
@@ -21,20 +21,20 @@
 	"android/soong/cc"
 )
 
+var ccTestFs = map[string][]byte{
+	"Test.cpp":                      nil,
+	"include/Test.h":                nil,
+	"include-android/AndroidTest.h": nil,
+	"include-host/HostTest.h":       nil,
+	"arm64/include/Arm64Test.h":     nil,
+	"libfoo.so":                     nil,
+	"aidl/foo/bar/Test.aidl":        nil,
+	"some/where/stubslib.map.txt":   nil,
+}
+
 func testSdkWithCc(t *testing.T, bp string) *testSdkResult {
 	t.Helper()
-
-	fs := map[string][]byte{
-		"Test.cpp":                      nil,
-		"include/Test.h":                nil,
-		"include-android/AndroidTest.h": nil,
-		"include-host/HostTest.h":       nil,
-		"arm64/include/Arm64Test.h":     nil,
-		"libfoo.so":                     nil,
-		"aidl/foo/bar/Test.aidl":        nil,
-		"some/where/stubslib.map.txt":   nil,
-	}
-	return testSdkWithFs(t, bp, fs)
+	return testSdkWithFs(t, bp, ccTestFs)
 }
 
 // Contains tests for SDK members provided by the cc package.
diff --git a/sdk/testing.go b/sdk/testing.go
index 378ce1f..34ea8f0 100644
--- a/sdk/testing.go
+++ b/sdk/testing.go
@@ -29,7 +29,9 @@
 	"android/soong/java"
 )
 
-func testSdkContext(bp string, fs map[string][]byte) (*android.TestContext, android.Config) {
+func testSdkContext(bp string, fs map[string][]byte, extraOsTypes []android.OsType) (*android.TestContext, android.Config) {
+	extraOsTypes = append(extraOsTypes, android.Android, android.Windows)
+
 	bp = bp + `
 		apex_key {
 			name: "myapex.key",
@@ -41,7 +43,7 @@
 			name: "myapex.cert",
 			certificate: "myapex",
 		}
-	` + cc.GatherRequiredDepsForTest(android.Android, android.Windows)
+	` + cc.GatherRequiredDepsForTest(extraOsTypes...)
 
 	mockFS := map[string][]byte{
 		"build/make/target/product/security":           nil,
@@ -69,6 +71,15 @@
 		{android.Windows, android.Arch{ArchType: android.X86_64}, android.NativeBridgeDisabled, "", ""},
 	}
 
+	for _, extraOsType := range extraOsTypes {
+		switch extraOsType {
+		case android.LinuxBionic:
+			config.Targets[android.LinuxBionic] = []android.Target{
+				{android.LinuxBionic, android.Arch{ArchType: android.X86_64}, android.NativeBridgeDisabled, "", ""},
+			}
+		}
+	}
+
 	ctx := android.NewTestArchContext()
 
 	// Enable androidmk support.
@@ -117,9 +128,8 @@
 	return ctx, config
 }
 
-func testSdkWithFs(t *testing.T, bp string, fs map[string][]byte) *testSdkResult {
+func runTests(t *testing.T, ctx *android.TestContext, config android.Config) *testSdkResult {
 	t.Helper()
-	ctx, config := testSdkContext(bp, fs)
 	_, errs := ctx.ParseBlueprintsFiles(".")
 	android.FailIfErrored(t, errs)
 	_, errs = ctx.PrepareBuildActions(config)
@@ -131,9 +141,15 @@
 	}
 }
 
+func testSdkWithFs(t *testing.T, bp string, fs map[string][]byte) *testSdkResult {
+	t.Helper()
+	ctx, config := testSdkContext(bp, fs, nil)
+	return runTests(t, ctx, config)
+}
+
 func testSdkError(t *testing.T, pattern, bp string) {
 	t.Helper()
-	ctx, config := testSdkContext(bp, nil)
+	ctx, config := testSdkContext(bp, nil, nil)
 	_, errs := ctx.ParseFileList(".", []string{"Android.bp"})
 	if len(errs) > 0 {
 		android.FailIfNoMatchingErrors(t, pattern, errs)
diff --git a/ui/build/config.go b/ui/build/config.go
index a60d70e..ba477e6 100644
--- a/ui/build/config.go
+++ b/ui/build/config.go
@@ -184,7 +184,8 @@
 	// Tell python not to spam the source tree with .pyc files.
 	ret.environ.Set("PYTHONDONTWRITEBYTECODE", "1")
 
-	ret.environ.Set("TMPDIR", absPath(ctx, ret.TempDir()))
+	tmpDir := absPath(ctx, ret.TempDir())
+	ret.environ.Set("TMPDIR", tmpDir)
 
 	// Always set ASAN_SYMBOLIZER_PATH so that ASAN-based tools can symbolize any crashes
 	symbolizerPath := filepath.Join("prebuilts/clang/host", ret.HostPrebuiltTag(),
@@ -258,6 +259,12 @@
 
 	ret.environ.Set("BUILD_DATETIME_FILE", buildDateTimeFile)
 
+	if ret.UseRBE() {
+		for k, v := range getRBEVars(ctx, tmpDir) {
+			ret.environ.Set(k, v)
+		}
+	}
+
 	return Config{ret}
 }
 
diff --git a/ui/build/rbe.go b/ui/build/rbe.go
index fcdab3b..fd3b7ab 100644
--- a/ui/build/rbe.go
+++ b/ui/build/rbe.go
@@ -15,8 +15,11 @@
 package build
 
 import (
+	"fmt"
+	"math/rand"
 	"os"
 	"path/filepath"
+	"time"
 
 	"android/soong/ui/metrics"
 )
@@ -49,6 +52,11 @@
 	return cmdPath
 }
 
+func getRBEVars(ctx Context, tmpDir string) map[string]string {
+	rand.Seed(time.Now().UnixNano())
+	return map[string]string{"RBE_server_address": fmt.Sprintf("unix://%v/reproxy_%v.sock", tmpDir, rand.Intn(1000))}
+}
+
 func startRBE(ctx Context, config Config) {
 	ctx.BeginTrace(metrics.RunSetupTool, "rbe_bootstrap")
 	defer ctx.EndTrace()
diff --git a/ui/build/soong.go b/ui/build/soong.go
index 749acb3..fb21430 100644
--- a/ui/build/soong.go
+++ b/ui/build/soong.go
@@ -38,7 +38,7 @@
 		ctx.BeginTrace(metrics.RunSoong, "blueprint bootstrap")
 		defer ctx.EndTrace()
 
-		cmd := Command(ctx, config, "blueprint bootstrap", "build/blueprint/bootstrap.bash", "-t")
+		cmd := Command(ctx, config, "blueprint bootstrap", "build/blueprint/bootstrap.bash", "-t", "-n")
 		cmd.Environment.Set("BLUEPRINTDIR", "./build/blueprint")
 		cmd.Environment.Set("BOOTSTRAP", "./build/blueprint/bootstrap.bash")
 		cmd.Environment.Set("BUILDDIR", config.SoongOutDir())