Merge "convert flag options and values in the same line"
diff --git a/android/module.go b/android/module.go
index 196b095..126d629 100644
--- a/android/module.go
+++ b/android/module.go
@@ -2812,30 +2812,57 @@
 	return m.bp
 }
 
-// SrcIsModule decodes module references in the format ":name" into the module name, or empty string if the input
-// was not a module reference.
+// SrcIsModule decodes module references in the format ":unqualified-name" or "//namespace:name"
+// into the module name, or empty string if the input was not a module reference.
 func SrcIsModule(s string) (module string) {
-	if len(s) > 1 && s[0] == ':' {
-		return s[1:]
+	if len(s) > 1 {
+		if s[0] == ':' {
+			module = s[1:]
+			if !isUnqualifiedModuleName(module) {
+				// The module name should be unqualified but is not so do not treat it as a module.
+				module = ""
+			}
+		} else if s[0] == '/' && s[1] == '/' {
+			module = s
+		}
 	}
-	return ""
+	return module
 }
 
-// SrcIsModule decodes module references in the format ":name{.tag}" into the module name and tag, ":name" into the
-// module name and an empty string for the tag, or empty strings if the input was not a module reference.
+// SrcIsModule decodes module references in the format ":unqualified-name{.tag}" or
+// "//namespace:name{.tag}" into the module name and an empty string for the tag, or empty strings
+// if the input was not a module reference.
 func SrcIsModuleWithTag(s string) (module, tag string) {
-	if len(s) > 1 && s[0] == ':' {
-		module = s[1:]
-		if tagStart := strings.IndexByte(module, '{'); tagStart > 0 {
-			if module[len(module)-1] == '}' {
-				tag = module[tagStart+1 : len(module)-1]
-				module = module[:tagStart]
-				return module, tag
+	if len(s) > 1 {
+		if s[0] == ':' {
+			module = s[1:]
+		} else if s[0] == '/' && s[1] == '/' {
+			module = s
+		}
+
+		if module != "" {
+			if tagStart := strings.IndexByte(module, '{'); tagStart > 0 {
+				if module[len(module)-1] == '}' {
+					tag = module[tagStart+1 : len(module)-1]
+					module = module[:tagStart]
+				}
+			}
+
+			if s[0] == ':' && !isUnqualifiedModuleName(module) {
+				// The module name should be unqualified but is not so do not treat it as a module.
+				module = ""
+				tag = ""
 			}
 		}
-		return module, ""
 	}
-	return "", ""
+
+	return module, tag
+}
+
+// isUnqualifiedModuleName makes sure that the supplied module is an unqualified module name, i.e.
+// does not contain any /.
+func isUnqualifiedModuleName(module string) bool {
+	return strings.IndexByte(module, '/') == -1
 }
 
 type sourceOrOutputDependencyTag struct {
diff --git a/android/module_test.go b/android/module_test.go
index 9ac9291..9e2b0ca 100644
--- a/android/module_test.go
+++ b/android/module_test.go
@@ -55,6 +55,27 @@
 			},
 			wantModule: "foo:bar",
 		},
+		{
+			name: "fully qualified",
+			args: args{
+				s: "//foo:bar",
+			},
+			wantModule: "//foo:bar",
+		},
+		{
+			name: "fully qualified with tag",
+			args: args{
+				s: "//foo:bar{.tag}",
+			},
+			wantModule: "//foo:bar{.tag}",
+		},
+		{
+			name: "invalid unqualified name",
+			args: args{
+				s: ":foo/bar",
+			},
+			wantModule: "",
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -128,6 +149,35 @@
 			},
 			wantModule: "foo.bar}",
 		},
+		{
+			name: "fully qualified",
+			args: args{
+				s: "//foo:bar",
+			},
+			wantModule: "//foo:bar",
+		},
+		{
+			name: "fully qualified with tag",
+			args: args{
+				s: "//foo:bar{.tag}",
+			},
+			wantModule: "//foo:bar",
+			wantTag:    ".tag",
+		},
+		{
+			name: "invalid unqualified name",
+			args: args{
+				s: ":foo/bar",
+			},
+			wantModule: "",
+		},
+		{
+			name: "invalid unqualified name with tag",
+			args: args{
+				s: ":foo/bar{.tag}",
+			},
+			wantModule: "",
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/android/paths.go b/android/paths.go
index c5e4806..99d5ba7 100644
--- a/android/paths.go
+++ b/android/paths.go
@@ -1268,10 +1268,11 @@
 // PathForModuleSrc returns a Path representing the paths... under the
 // module's local source directory.
 func PathForModuleSrc(ctx ModuleMissingDepsPathContext, pathComponents ...string) Path {
-	p, err := validatePath(pathComponents...)
-	if err != nil {
-		reportPathError(ctx, err)
-	}
+	// Just join the components textually just to make sure that it does not corrupt a fully qualified
+	// module reference, e.g. if the pathComponents is "://other:foo" then using filepath.Join() or
+	// validatePath() will corrupt it, e.g. replace "//" with "/". If the path is not a module
+	// reference then it will be validated by expandOneSrcPath anyway when it calls expandOneSrcPath.
+	p := strings.Join(pathComponents, string(filepath.Separator))
 	paths, err := expandOneSrcPath(ctx, p, nil)
 	if err != nil {
 		if depErr, ok := err.(missingDependencyError); ok {
diff --git a/android/paths_test.go b/android/paths_test.go
index 6f5d79e..7675905 100644
--- a/android/paths_test.go
+++ b/android/paths_test.go
@@ -1125,6 +1125,12 @@
 	rels []string
 	src  string
 	rel  string
+
+	// Make test specific preparations to the test fixture.
+	preparer FixturePreparer
+
+	// A test specific error handler.
+	errorHandler FixtureErrorHandler
 }
 
 func testPathForModuleSrc(t *testing.T, tests []pathForModuleSrcTestCase) {
@@ -1157,14 +1163,23 @@
 				"foo/src_special/$": nil,
 			}
 
+			errorHandler := test.errorHandler
+			if errorHandler == nil {
+				errorHandler = FixtureExpectsNoErrors
+			}
+
 			result := GroupFixturePreparers(
 				FixtureRegisterWithContext(func(ctx RegistrationContext) {
 					ctx.RegisterModuleType("test", pathForModuleSrcTestModuleFactory)
 					ctx.RegisterModuleType("output_file_provider", pathForModuleSrcOutputFileProviderModuleFactory)
-					ctx.RegisterModuleType("filegroup", FileGroupFactory)
 				}),
+				PrepareForTestWithFilegroup,
+				PrepareForTestWithNamespace,
 				mockFS.AddToFixture(),
-			).RunTest(t)
+				OptionalFixturePreparer(test.preparer),
+			).
+				ExtendWithErrorHandler(errorHandler).
+				RunTest(t)
 
 			m := result.ModuleForTests("foo", "").Module().(*pathForModuleSrcTestModule)
 
@@ -1333,6 +1348,74 @@
 			src: "foo/src_special/$",
 			rel: "src_special/$",
 		},
+		{
+			// This test makes sure that an unqualified module name cannot contain characters that make
+			// it appear as a qualified module name.
+			name: "output file provider, invalid fully qualified name",
+			bp: `
+			test {
+				name: "foo",
+				src: "://other:b",
+				srcs: ["://other:c"],
+			}`,
+			preparer: FixtureAddTextFile("other/Android.bp", `
+				soong_namespace {}
+
+				output_file_provider {
+					name: "b",
+					outs: ["gen/b"],
+				}
+
+				output_file_provider {
+					name: "c",
+					outs: ["gen/c"],
+				}
+			`),
+			src:  "foo/:/other:b",
+			rel:  ":/other:b",
+			srcs: []string{"foo/:/other:c"},
+			rels: []string{":/other:c"},
+		},
+		{
+			name: "output file provider, missing fully qualified name",
+			bp: `
+			test {
+				name: "foo",
+				src: "//other:b",
+				srcs: ["//other:c"],
+			}`,
+			errorHandler: FixtureExpectsAllErrorsToMatchAPattern([]string{
+				`"foo" depends on undefined module "//other:b"`,
+				`"foo" depends on undefined module "//other:c"`,
+			}),
+		},
+		{
+			// TODO(b/193228441): Fix broken test.
+			name: "output file provider, fully qualified name",
+			bp: `
+			test {
+				name: "foo",
+				src: "//other:b",
+				srcs: ["//other:c"],
+			}`,
+			preparer: FixtureAddTextFile("other/Android.bp", `
+				soong_namespace {}
+
+				output_file_provider {
+					name: "b",
+					outs: ["gen/b"],
+				}
+
+				output_file_provider {
+					name: "c",
+					outs: ["gen/c"],
+				}
+			`),
+			errorHandler: FixtureExpectsAllErrorsToMatchAPattern([]string{
+				`"foo": missing dependencies: //other:b, is the property annotated with android:"path"`,
+				`"foo": missing dependency on "//other:c", is the property annotated with android:"path"`,
+			}),
+		},
 	}
 
 	testPathForModuleSrc(t, tests)
diff --git a/cc/cc.go b/cc/cc.go
index 90c0237..aeebaef 100644
--- a/cc/cc.go
+++ b/cc/cc.go
@@ -2014,9 +2014,9 @@
 }
 
 func GetSnapshot(c LinkableInterface, snapshotInfo **SnapshotInfo, actx android.BottomUpMutatorContext) SnapshotInfo {
-	// Only modules with BOARD_VNDK_VERSION uses snapshot.  Others use the zero value of
+	// Only device modules with BOARD_VNDK_VERSION uses snapshot.  Others use the zero value of
 	// SnapshotInfo, which provides no mappings.
-	if *snapshotInfo == nil {
+	if *snapshotInfo == nil && c.Device() {
 		// Only retrieve the snapshot on demand in order to avoid circular dependencies
 		// between the modules in the snapshot and the snapshot itself.
 		var snapshotModule []blueprint.Module
@@ -2025,16 +2025,16 @@
 		} else if recoverySnapshotVersion := actx.DeviceConfig().RecoverySnapshotVersion(); recoverySnapshotVersion != "current" && recoverySnapshotVersion != "" && c.InRecovery() {
 			snapshotModule = actx.AddVariationDependencies(nil, nil, "recovery_snapshot")
 		}
-		if len(snapshotModule) > 0 {
+		if len(snapshotModule) > 0 && snapshotModule[0] != nil {
 			snapshot := actx.OtherModuleProvider(snapshotModule[0], SnapshotInfoProvider).(SnapshotInfo)
 			*snapshotInfo = &snapshot
 			// republish the snapshot for use in later mutators on this module
 			actx.SetProvider(SnapshotInfoProvider, snapshot)
-		} else {
-			*snapshotInfo = &SnapshotInfo{}
 		}
 	}
-
+	if *snapshotInfo == nil {
+		*snapshotInfo = &SnapshotInfo{}
+	}
 	return **snapshotInfo
 }
 
diff --git a/cc/linkable.go b/cc/linkable.go
index 84141e2..6232efb 100644
--- a/cc/linkable.go
+++ b/cc/linkable.go
@@ -125,6 +125,7 @@
 	IsPrebuilt() bool
 	Toc() android.OptionalPath
 
+	Device() bool
 	Host() bool
 
 	InRamdisk() bool
diff --git a/java/lint.go b/java/lint.go
index dd5e4fb..fe3218e 100644
--- a/java/lint.go
+++ b/java/lint.go
@@ -391,8 +391,9 @@
 	rule.Command().Text("rm -f").Output(html).Output(text).Output(xml)
 
 	var apiVersionsName, apiVersionsPrebuilt string
-	if l.compileSdkKind == android.SdkModule {
-		// When compiling an SDK module we use the filtered database because otherwise lint's
+	if l.compileSdkKind == android.SdkModule || l.compileSdkKind == android.SdkSystemServer {
+		// When compiling an SDK module (or system server) we use the filtered
+		// database because otherwise lint's
 		// NewApi check produces too many false positives; This database excludes information
 		// about classes created in mainline modules hence removing those false positives.
 		apiVersionsName = "api_versions_public_filtered.xml"
diff --git a/java/lint_test.go b/java/lint_test.go
index 9cf1c33..456e6ba 100644
--- a/java/lint_test.go
+++ b/java/lint_test.go
@@ -261,6 +261,9 @@
 }
 
 func TestJavaLintDatabaseSelectionPublicFiltered(t *testing.T) {
+	testCases := []string{
+		"module_current", "system_server_current",
+	}
 	bp := `
 		java_library {
 			name: "foo",
@@ -274,17 +277,20 @@
 			},
 		}
 `
-	result := android.GroupFixturePreparers(PrepareForTestWithJavaDefaultModules).
-		RunTestWithBp(t, bp)
+	for _, testCase := range testCases {
+		thisBp := strings.Replace(bp, "XXX", testCase, 1)
+		result := android.GroupFixturePreparers(PrepareForTestWithJavaDefaultModules).
+			RunTestWithBp(t, thisBp)
 
-	foo := result.ModuleForTests("foo", "android_common")
-	sboxProto := android.RuleBuilderSboxProtoForTests(t, foo.Output("lint.sbox.textproto"))
-	if !strings.Contains(*sboxProto.Commands[0].Command,
-		"/api_versions_public_filtered.xml") {
-		t.Error("did not use public-filtered lint api database", *sboxProto.Commands[0].Command)
-	}
-	if strings.Contains(*sboxProto.Commands[0].Command,
-		"/api_versions.xml") {
-		t.Error("used full api database")
+		foo := result.ModuleForTests("foo", "android_common")
+		sboxProto := android.RuleBuilderSboxProtoForTests(t, foo.Output("lint.sbox.textproto"))
+		if !strings.Contains(*sboxProto.Commands[0].Command,
+			"/api_versions_public_filtered.xml") {
+			t.Error("did not use public-filtered lint api database for case", testCase)
+		}
+		if strings.Contains(*sboxProto.Commands[0].Command,
+			"/api_versions.xml") {
+			t.Error("used full api database for case", testCase)
+		}
 	}
 }
diff --git a/mk2rbc/android_products.go b/mk2rbc/android_products.go
new file mode 100644
index 0000000..de38391
--- /dev/null
+++ b/mk2rbc/android_products.go
@@ -0,0 +1,110 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mk2rbc
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	mkparser "android/soong/androidmk/parser"
+)
+
+// Implements mkparser.Scope, to be used by mkparser.Value.Value()
+type localDirEval struct {
+	localDir  string
+	hasErrors bool
+}
+
+func (l *localDirEval) Get(name string) string {
+	if name == "LOCAL_DIR" {
+		return l.localDir
+	}
+	l.hasErrors = true
+	return fmt.Sprintf("$(%s)", name)
+}
+
+func (l *localDirEval) Set(_, _ string) {
+}
+
+func (l *localDirEval) Call(_ string, _ []string) []string {
+	l.hasErrors = true
+	return []string{"$(call ...)"}
+}
+
+func (l *localDirEval) SetFunc(_ string, _ func([]string) []string) {
+}
+
+// UpdateProductConfigMap builds product configuration map.
+// The product configuration map maps a product name (i.e., the value of the
+// TARGET_PRODUCT variable) to the top-level configuration file.
+// In the Android's Make-based build machinery, the equivalent of the
+// product configuration map is $(PRODUCT_MAKEFILES), which is the list
+// of <product>:<configuration makefile> pairs (if <product>: is missing,
+// <product> is the basename of the configuration makefile).
+// UpdateProductConfigMap emulates this build logic by processing the
+// assignments to PRODUCT_MAKEFILES in the file passed to it.
+func UpdateProductConfigMap(configMap map[string]string, configMakefile string) error {
+	contents, err := ioutil.ReadFile(configMakefile)
+	if err != nil {
+		return err
+	}
+	parser := mkparser.NewParser(configMakefile, bytes.NewBuffer(contents))
+	nodes, errs := parser.Parse()
+	if len(errs) > 0 {
+		for _, e := range errs {
+			fmt.Fprintln(os.Stderr, "ERROR:", e)
+		}
+		return fmt.Errorf("cannot parse %s", configMakefile)
+	}
+
+	ldEval := &localDirEval{localDir: filepath.Dir(configMakefile)}
+
+	for _, node := range nodes {
+		// We are interested in assignments to 'PRODUCT_MAKEFILES'
+		asgn, ok := node.(*mkparser.Assignment)
+		if !ok {
+			continue
+		}
+		if !(asgn.Name.Const() && asgn.Name.Strings[0] == "PRODUCT_MAKEFILES") {
+			continue
+		}
+
+		// Resolve the references to $(LOCAL_DIR) in $(PRODUCT_MAKEFILES).
+		ldEval.hasErrors = false
+		value := asgn.Value.Value(ldEval)
+		if ldEval.hasErrors {
+			return fmt.Errorf("cannot evaluate %s", asgn.Value.Dump())
+		}
+		// Each item is either <product>:<configuration makefile>, or
+		// just <configuration makefile>
+		for _, token := range strings.Fields(value) {
+			var product, config_path string
+			if n := strings.Index(token, ":"); n >= 0 {
+				product = token[0:n]
+				config_path = token[n+1:]
+			} else {
+				config_path = token
+				product = filepath.Base(config_path)
+				product = strings.TrimSuffix(product, filepath.Ext(product))
+			}
+			configMap[product] = config_path
+		}
+	}
+	return nil
+}
diff --git a/mk2rbc/android_products_test.go b/mk2rbc/android_products_test.go
new file mode 100644
index 0000000..f8c930a
--- /dev/null
+++ b/mk2rbc/android_products_test.go
@@ -0,0 +1,38 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mk2rbc
+
+import (
+	"path/filepath"
+	"reflect"
+	"testing"
+)
+
+func TestProductsMakefile(t *testing.T) {
+	testDir := getTestDirectory()
+	abspath := func(relPath string) string { return filepath.Join(testDir, relPath) }
+	actualProducts := make(map[string]string)
+	if err := UpdateProductConfigMap(actualProducts, abspath("android_products.mk.test")); err != nil {
+		t.Fatal(err)
+	}
+	expectedProducts := map[string]string{
+		"aosp_cf_x86_tv": abspath("vsoc_x86/tv/device.mk"),
+		"aosp_tv_arm":    abspath("aosp_tv_arm.mk"),
+		"aosp_tv_arm64":  abspath("aosp_tv_arm64.mk"),
+	}
+	if !reflect.DeepEqual(actualProducts, expectedProducts) {
+		t.Errorf("\nExpected: %v\n  Actual: %v", expectedProducts, actualProducts)
+	}
+}
diff --git a/mk2rbc/config_variables.go b/mk2rbc/config_variables.go
new file mode 100644
index 0000000..dac509c
--- /dev/null
+++ b/mk2rbc/config_variables.go
@@ -0,0 +1,67 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mk2rbc
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"strings"
+
+	mkparser "android/soong/androidmk/parser"
+)
+
+// Extracts the list of product config variables from a file, calling
+// given registrar for each variable.
+func FindConfigVariables(mkFile string, vr variableRegistrar) error {
+	mkContents, err := ioutil.ReadFile(mkFile)
+	if err != nil {
+		return err
+	}
+	parser := mkparser.NewParser(mkFile, bytes.NewBuffer(mkContents))
+	nodes, errs := parser.Parse()
+	if len(errs) > 0 {
+		for _, e := range errs {
+			fmt.Fprintln(os.Stderr, "ERROR:", e)
+		}
+		return fmt.Errorf("cannot parse %s", mkFile)
+	}
+	for _, node := range nodes {
+		asgn, ok := node.(*mkparser.Assignment)
+		if !ok {
+			continue
+		}
+		// We are looking for a variable called '_product_list_vars'
+		// or '_product_single_value_vars'.
+		if !asgn.Name.Const() {
+			continue
+		}
+		varName := asgn.Name.Strings[0]
+		var starType starlarkType
+		if varName == "_product_list_vars" {
+			starType = starlarkTypeList
+		} else if varName == "_product_single_value_vars" {
+			starType = starlarkTypeUnknown
+		} else {
+			continue
+		}
+		for _, name := range strings.Fields(asgn.Value.Dump()) {
+			vr.NewVariable(name, VarClassConfig, starType)
+		}
+
+	}
+	return nil
+}
diff --git a/mk2rbc/config_variables_test.go b/mk2rbc/config_variables_test.go
new file mode 100644
index 0000000..f5a5180
--- /dev/null
+++ b/mk2rbc/config_variables_test.go
@@ -0,0 +1,60 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mk2rbc
+
+import (
+	"path/filepath"
+	"reflect"
+	"runtime"
+	"testing"
+)
+
+type testVar struct {
+	name string
+	cl   varClass
+	ty   starlarkType
+}
+
+type testVariables struct {
+	v []testVar
+}
+
+func (v *testVariables) NewVariable(name string, varClass varClass, valueType starlarkType) {
+	v.v = append(v.v, testVar{name, varClass, valueType})
+}
+
+// getTestDirectory returns the test directory, which should be the test/ subdirectory
+func getTestDirectory() string {
+	_, myFile, _, _ := runtime.Caller(1)
+	return filepath.Join(filepath.Dir(myFile), "test")
+}
+
+func TestConfigVariables(t *testing.T) {
+	testFile := filepath.Join(getTestDirectory(), "config_variables.mk.test")
+	var actual testVariables
+	if err := FindConfigVariables(testFile, &actual); err != nil {
+		t.Fatal(err)
+	}
+	expected := testVariables{[]testVar{
+		{"PRODUCT_NAME", VarClassConfig, starlarkTypeUnknown},
+		{"PRODUCT_MODEL", VarClassConfig, starlarkTypeUnknown},
+		{"PRODUCT_LOCALES", VarClassConfig, starlarkTypeList},
+		{"PRODUCT_AAPT_CONFIG", VarClassConfig, starlarkTypeList},
+		{"PRODUCT_AAPT_PREF_CONFIG", VarClassConfig, starlarkTypeUnknown},
+	}}
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("\nExpected: %v\n  Actual: %v", expected, actual)
+	}
+}
diff --git a/mk2rbc/soong_variables.go b/mk2rbc/soong_variables.go
new file mode 100644
index 0000000..de46925
--- /dev/null
+++ b/mk2rbc/soong_variables.go
@@ -0,0 +1,151 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mk2rbc
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"regexp"
+	"strings"
+
+	mkparser "android/soong/androidmk/parser"
+)
+
+type context struct {
+	includeFileScope mkparser.Scope
+	registrar        variableRegistrar
+}
+
+// Scans the makefile Soong uses to generate soong.variables file,
+// collecting variable names and types from the lines that look like this:
+//    $(call add_json_XXX,  <...>,             $(VAR))
+//
+func FindSoongVariables(mkFile string, includeFileScope mkparser.Scope, registrar variableRegistrar) error {
+	ctx := context{includeFileScope, registrar}
+	return ctx.doFind(mkFile)
+}
+
+func (ctx *context) doFind(mkFile string) error {
+	mkContents, err := ioutil.ReadFile(mkFile)
+	if err != nil {
+		return err
+	}
+	parser := mkparser.NewParser(mkFile, bytes.NewBuffer(mkContents))
+	nodes, errs := parser.Parse()
+	if len(errs) > 0 {
+		for _, e := range errs {
+			fmt.Fprintln(os.Stderr, "ERROR:", e)
+		}
+		return fmt.Errorf("cannot parse %s", mkFile)
+	}
+	for _, node := range nodes {
+		switch t := node.(type) {
+		case *mkparser.Variable:
+			ctx.handleVariable(t)
+		case *mkparser.Directive:
+			ctx.handleInclude(t)
+		}
+	}
+	return nil
+}
+
+func (ctx context) NewSoongVariable(name, typeString string) {
+	var valueType starlarkType
+	switch typeString {
+	case "bool":
+		valueType = starlarkTypeBool
+	case "csv":
+		// Only PLATFORM_VERSION_ALL_CODENAMES, and it's a list
+		valueType = starlarkTypeList
+	case "list":
+		valueType = starlarkTypeList
+	case "str":
+		valueType = starlarkTypeString
+	case "val":
+		// Only PLATFORM_SDK_VERSION uses this, and it's integer
+		valueType = starlarkTypeInt
+	default:
+		panic(fmt.Errorf("unknown Soong variable type %s", typeString))
+	}
+
+	ctx.registrar.NewVariable(name, VarClassSoong, valueType)
+}
+
+func (ctx context) handleInclude(t *mkparser.Directive) {
+	if t.Name != "include" && t.Name != "-include" {
+		return
+	}
+	includedPath := t.Args.Value(ctx.includeFileScope)
+	err := ctx.doFind(includedPath)
+	if err != nil && t.Name == "include" {
+		fmt.Fprintf(os.Stderr, "cannot include %s: %s", includedPath, err)
+	}
+}
+
+var callFuncRex = regexp.MustCompile("^call +add_json_(str|val|bool|csv|list) *,")
+
+func (ctx context) handleVariable(t *mkparser.Variable) {
+	// From the variable reference looking as follows:
+	//  $(call json_add_TYPE,arg1,$(VAR))
+	// we infer that the type of $(VAR) is TYPE
+	// VAR can be a simple variable name, or another call
+	// (e.g., $(call invert_bool, $(X)), from which we can infer
+	// that the type of X is bool
+	if prefix, v, ok := prefixedVariable(t.Name); ok && strings.HasPrefix(prefix, "call add_json") {
+		if match := callFuncRex.FindStringSubmatch(prefix); match != nil {
+			ctx.inferSoongVariableType(match[1], v)
+			// NOTE(asmundak): sometimes arg1 (the name of the Soong variable defined
+			// in this statement) may indicate that there is a Make counterpart. E.g, from
+			//     $(call add_json_bool, DisablePreopt, $(call invert_bool,$(ENABLE_PREOPT)))
+			// it may be inferred that there is a Make boolean variable DISABLE_PREOPT.
+			// Unfortunately, Soong variable names have no 1:1 correspondence to Make variables,
+			// for instance,
+			//       $(call add_json_list, PatternsOnSystemOther, $(SYSTEM_OTHER_ODEX_FILTER))
+			// does not mean that there is PATTERNS_ON_SYSTEM_OTHER
+			// Our main interest lies in finding the variables whose values are lists, and
+			// so far there are none that can be found this way, so it is not important.
+		} else {
+			panic(fmt.Errorf("cannot match the call: %s", prefix))
+		}
+	}
+}
+
+var (
+	callInvertBoolRex = regexp.MustCompile("^call +invert_bool *, *$")
+	callFilterBoolRex = regexp.MustCompile("^(filter|filter-out) +(true|false), *$")
+)
+
+func (ctx context) inferSoongVariableType(vType string, n *mkparser.MakeString) {
+	if n.Const() {
+		ctx.NewSoongVariable(n.Strings[0], vType)
+		return
+	}
+	if prefix, v, ok := prefixedVariable(n); ok {
+		if callInvertBoolRex.MatchString(prefix) || callFilterBoolRex.MatchString(prefix) {
+			// It is $(call invert_bool, $(VAR)) or $(filter[-out] [false|true],$(VAR))
+			ctx.inferSoongVariableType("bool", v)
+		}
+	}
+}
+
+// If MakeString is foo$(BAR), returns 'foo', BAR(as *MakeString) and true
+func prefixedVariable(s *mkparser.MakeString) (string, *mkparser.MakeString, bool) {
+	if len(s.Strings) != 2 || s.Strings[1] != "" {
+		return "", nil, false
+	}
+	return s.Strings[0], s.Variables[0].Name, true
+}
diff --git a/mk2rbc/soong_variables_test.go b/mk2rbc/soong_variables_test.go
new file mode 100644
index 0000000..c883882
--- /dev/null
+++ b/mk2rbc/soong_variables_test.go
@@ -0,0 +1,51 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mk2rbc
+
+import (
+	"fmt"
+	"path/filepath"
+	"reflect"
+	"testing"
+)
+
+type dirResolverForTest struct {
+	ScopeBase
+}
+
+func (t dirResolverForTest) Get(name string) string {
+	if name != "BUILD_SYSTEM" {
+		return fmt.Sprintf("$(%s)", name)
+	}
+	return getTestDirectory()
+}
+
+func TestSoongVariables(t *testing.T) {
+	testFile := filepath.Join(getTestDirectory(), "soong_variables.mk.test")
+	var actual testVariables
+	if err := FindSoongVariables(testFile, dirResolverForTest{}, &actual); err != nil {
+		t.Fatal(err)
+	}
+	expected := testVariables{[]testVar{
+		{"BUILD_ID", VarClassSoong, starlarkTypeString},
+		{"PLATFORM_SDK_VERSION", VarClassSoong, starlarkTypeInt},
+		{"DEVICE_PACKAGE_OVERLAYS", VarClassSoong, starlarkTypeList},
+		{"ENABLE_CFI", VarClassSoong, starlarkTypeBool},
+		{"ENABLE_PREOPT", VarClassSoong, starlarkTypeBool},
+	}}
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("\nExpected: %v\n  Actual: %v", expected, actual)
+	}
+}
diff --git a/mk2rbc/test/android_products.mk.test b/mk2rbc/test/android_products.mk.test
new file mode 100644
index 0000000..a2220ed
--- /dev/null
+++ b/mk2rbc/test/android_products.mk.test
@@ -0,0 +1,4 @@
+PRODUCT_MAKEFILES := \
+    $(LOCAL_DIR)/aosp_tv_arm.mk \
+    $(LOCAL_DIR)/aosp_tv_arm64.mk \
+    aosp_cf_x86_tv:$(LOCAL_DIR)/vsoc_x86/tv/device.mk
\ No newline at end of file
diff --git a/mk2rbc/test/config_variables.mk.test b/mk2rbc/test/config_variables.mk.test
new file mode 100644
index 0000000..e5cd0e9
--- /dev/null
+++ b/mk2rbc/test/config_variables.mk.test
@@ -0,0 +1,12 @@
+_product_single_value_vars :=
+
+# Variables that are lists of values.
+_product_list_vars :=
+
+_product_single_value_vars += PRODUCT_NAME
+_product_single_value_vars += PRODUCT_MODEL
+
+# The resoure configuration options to use for this product.
+_product_list_vars += PRODUCT_LOCALES
+_product_list_vars += PRODUCT_AAPT_CONFIG
+_product_single_value_vars += PRODUCT_AAPT_PREF_CONFIG
diff --git a/mk2rbc/test/soong_included.mk.test b/mk2rbc/test/soong_included.mk.test
new file mode 100644
index 0000000..255ecc5
--- /dev/null
+++ b/mk2rbc/test/soong_included.mk.test
@@ -0,0 +1 @@
+  $(call add_json_bool, DisablePreopt,                           $(call invert_bool,$(ENABLE_PREOPT)))
diff --git a/mk2rbc/test/soong_variables.mk.test b/mk2rbc/test/soong_variables.mk.test
new file mode 100644
index 0000000..ca60c9c
--- /dev/null
+++ b/mk2rbc/test/soong_variables.mk.test
@@ -0,0 +1,5 @@
+$(call add_json_str,  BuildId,                           $(BUILD_ID))
+$(call add_json_val,  Platform_sdk_version,              $(PLATFORM_SDK_VERSION))
+$(call add_json_list, DeviceResourceOverlays,            $(DEVICE_PACKAGE_OVERLAYS))
+$(call add_json_bool, EnableCFI,                         $(call invert_bool,$(filter false,$(ENABLE_CFI))))
+include $(BUILD_SYSTEM)/soong_included.mk.test
diff --git a/mk2rbc/types.go b/mk2rbc/types.go
new file mode 100644
index 0000000..22c8b58
--- /dev/null
+++ b/mk2rbc/types.go
@@ -0,0 +1,55 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mk2rbc
+
+// Starlark expression types we use
+type starlarkType int
+
+const (
+	starlarkTypeUnknown starlarkType = iota
+	starlarkTypeList    starlarkType = iota
+	starlarkTypeString  starlarkType = iota
+	starlarkTypeInt     starlarkType = iota
+	starlarkTypeBool    starlarkType = iota
+	starlarkTypeVoid    starlarkType = iota
+)
+
+type varClass int
+
+const (
+	VarClassConfig varClass = iota
+	VarClassSoong  varClass = iota
+	VarClassLocal  varClass = iota
+)
+
+type variableRegistrar interface {
+	NewVariable(name string, varClass varClass, valueType starlarkType)
+}
+
+// ScopeBase is a dummy implementation of the mkparser.Scope.
+// All our scopes are read-only and resolve only simple variables.
+type ScopeBase struct{}
+
+func (s ScopeBase) Set(_, _ string) {
+	panic("implement me")
+}
+
+func (s ScopeBase) Call(_ string, _ []string) []string {
+	panic("implement me")
+}
+
+func (s ScopeBase) SetFunc(_ string, _ func([]string) []string) {
+	panic("implement me")
+}