Bläddra i källkod

Add package for printing starlark formatted data

Bug: 216168792
Test: build/bazel/ci/bp2build.sh
Change-Id: I3a06b19396f7ffe1c638042cda7e731dd840f1d6
Liz Kammer 2 år sedan
förälder
incheckning
72beb34609

+ 2 - 0
android/Android.bp

@@ -16,7 +16,9 @@ bootstrap_go_package {
         "soong-remoteexec",
         "soong-response",
         "soong-shared",
+        "soong-starlark-format",
         "soong-ui-metrics_proto",
+
         "golang-protobuf-proto",
         "golang-protobuf-encoding-prototext",
 

+ 3 - 4
android/config.go

@@ -38,6 +38,7 @@ import (
 	"android/soong/android/soongconfig"
 	"android/soong/bazel"
 	"android/soong/remoteexec"
+	"android/soong/starlark_fmt"
 )
 
 // Bool re-exports proptools.Bool for the android package.
@@ -286,14 +287,12 @@ func saveToBazelConfigFile(config *productVariables, outDir string) error {
 		}
 	}
 
-	//TODO(b/216168792) should use common function to print Starlark code
-	nonArchVariantProductVariablesJson, err := json.MarshalIndent(&nonArchVariantProductVariables, "", "    ")
+	nonArchVariantProductVariablesJson := starlark_fmt.PrintStringList(nonArchVariantProductVariables, 0)
 	if err != nil {
 		return fmt.Errorf("cannot marshal product variable data: %s", err.Error())
 	}
 
-	//TODO(b/216168792) should use common function to print Starlark code
-	archVariantProductVariablesJson, err := json.MarshalIndent(&archVariantProductVariables, "", "    ")
+	archVariantProductVariablesJson := starlark_fmt.PrintStringList(archVariantProductVariables, 0)
 	if err != nil {
 		return fmt.Errorf("cannot marshal arch variant product variable data: %s", err.Error())
 	}

+ 40 - 0
android/soong_config_modules_test.go

@@ -386,6 +386,46 @@ func TestNonExistentPropertyInSoongConfigModule(t *testing.T) {
 	})).RunTest(t)
 }
 
+func TestDuplicateStringValueInSoongConfigStringVariable(t *testing.T) {
+	bp := `
+		soong_config_string_variable {
+			name: "board",
+			values: ["soc_a", "soc_b", "soc_c", "soc_a"],
+		}
+
+		soong_config_module_type {
+			name: "acme_test",
+			module_type: "test",
+			config_namespace: "acme",
+			variables: ["board"],
+			properties: ["cflags", "srcs", "defaults"],
+		}
+    `
+
+	fixtureForVendorVars := func(vars map[string]map[string]string) FixturePreparer {
+		return FixtureModifyProductVariables(func(variables FixtureProductVariables) {
+			variables.VendorVars = vars
+		})
+	}
+
+	GroupFixturePreparers(
+		fixtureForVendorVars(map[string]map[string]string{"acme": {"feature1": "1"}}),
+		PrepareForTestWithDefaults,
+		FixtureRegisterWithContext(func(ctx RegistrationContext) {
+			ctx.RegisterModuleType("soong_config_module_type_import", SoongConfigModuleTypeImportFactory)
+			ctx.RegisterModuleType("soong_config_module_type", SoongConfigModuleTypeFactory)
+			ctx.RegisterModuleType("soong_config_string_variable", SoongConfigStringVariableDummyFactory)
+			ctx.RegisterModuleType("soong_config_bool_variable", SoongConfigBoolVariableDummyFactory)
+			ctx.RegisterModuleType("test_defaults", soongConfigTestDefaultsModuleFactory)
+			ctx.RegisterModuleType("test", soongConfigTestModuleFactory)
+		}),
+		FixtureWithRootAndroidBp(bp),
+	).ExtendWithErrorHandler(FixtureExpectsAllErrorsToMatchAPattern([]string{
+		// TODO(b/171232169): improve the error message for non-existent properties
+		`Android.bp: soong_config_string_variable: values property error: duplicate value: "soc_a"`,
+	})).RunTest(t)
+}
+
 func testConfigWithVendorVars(buildDir, bp string, fs map[string][]byte, vendorVars map[string]map[string]string) Config {
 	config := TestConfig(buildDir, nil, bp, fs)
 

+ 1 - 0
android/soongconfig/Android.bp

@@ -10,6 +10,7 @@ bootstrap_go_package {
         "blueprint-parser",
         "blueprint-proptools",
         "soong-bazel",
+        "soong-starlark-format",
     ],
     srcs: [
         "config.go",

+ 34 - 27
android/soongconfig/modules.go

@@ -25,6 +25,8 @@ import (
 	"github.com/google/blueprint"
 	"github.com/google/blueprint/parser"
 	"github.com/google/blueprint/proptools"
+
+	"android/soong/starlark_fmt"
 )
 
 const conditionsDefault = "conditions_default"
@@ -177,10 +179,14 @@ func processStringVariableDef(v *SoongConfigDefinition, def *parser.Module) (err
 		return []error{fmt.Errorf("values property must be set")}
 	}
 
+	vals := make(map[string]bool, len(stringProps.Values))
 	for _, name := range stringProps.Values {
 		if err := checkVariableName(name); err != nil {
 			return []error{fmt.Errorf("soong_config_string_variable: values property error %s", err)}
+		} else if _, ok := vals[name]; ok {
+			return []error{fmt.Errorf("soong_config_string_variable: values property error: duplicate value: %q", name)}
 		}
+		vals[name] = true
 	}
 
 	v.variables[base.variable] = &stringVariable{
@@ -235,7 +241,12 @@ type SoongConfigDefinition struct {
 // string vars, bool vars and value vars created by every
 // soong_config_module_type in this build.
 type Bp2BuildSoongConfigDefinitions struct {
-	StringVars map[string]map[string]bool
+	// varCache contains a cache of string variables namespace + property
+	// The same variable may be used in multiple module types (for example, if need support
+	// for cc_default and java_default), only need to process once
+	varCache map[string]bool
+
+	StringVars map[string][]string
 	BoolVars   map[string]bool
 	ValueVars  map[string]bool
 }
@@ -253,7 +264,7 @@ func (defs *Bp2BuildSoongConfigDefinitions) AddVars(mtDef SoongConfigDefinition)
 	defer bp2buildSoongConfigVarsLock.Unlock()
 
 	if defs.StringVars == nil {
-		defs.StringVars = make(map[string]map[string]bool)
+		defs.StringVars = make(map[string][]string)
 	}
 	if defs.BoolVars == nil {
 		defs.BoolVars = make(map[string]bool)
@@ -261,15 +272,24 @@ func (defs *Bp2BuildSoongConfigDefinitions) AddVars(mtDef SoongConfigDefinition)
 	if defs.ValueVars == nil {
 		defs.ValueVars = make(map[string]bool)
 	}
+	if defs.varCache == nil {
+		defs.varCache = make(map[string]bool)
+	}
 	for _, moduleType := range mtDef.ModuleTypes {
 		for _, v := range moduleType.Variables {
 			key := strings.Join([]string{moduleType.ConfigNamespace, v.variableProperty()}, "__")
+
+			// The same variable may be used in multiple module types (for example, if need support
+			// for cc_default and java_default), only need to process once
+			if _, keyInCache := defs.varCache[key]; keyInCache {
+				continue
+			} else {
+				defs.varCache[key] = true
+			}
+
 			if strVar, ok := v.(*stringVariable); ok {
-				if _, ok := defs.StringVars[key]; !ok {
-					defs.StringVars[key] = make(map[string]bool, 0)
-				}
 				for _, value := range strVar.values {
-					defs.StringVars[key][value] = true
+					defs.StringVars[key] = append(defs.StringVars[key], value)
 				}
 			} else if _, ok := v.(*boolVariable); ok {
 				defs.BoolVars[key] = true
@@ -302,29 +322,16 @@ func sortedStringKeys(m interface{}) []string {
 // String emits the Soong config variable definitions as Starlark dictionaries.
 func (defs Bp2BuildSoongConfigDefinitions) String() string {
 	ret := ""
-	ret += "soong_config_bool_variables = {\n"
-	for _, boolVar := range sortedStringKeys(defs.BoolVars) {
-		ret += fmt.Sprintf("    \"%s\": True,\n", boolVar)
-	}
-	ret += "}\n"
-	ret += "\n"
+	ret += "soong_config_bool_variables = "
+	ret += starlark_fmt.PrintBoolDict(defs.BoolVars, 0)
+	ret += "\n\n"
 
-	ret += "soong_config_value_variables = {\n"
-	for _, valueVar := range sortedStringKeys(defs.ValueVars) {
-		ret += fmt.Sprintf("    \"%s\": True,\n", valueVar)
-	}
-	ret += "}\n"
-	ret += "\n"
+	ret += "soong_config_value_variables = "
+	ret += starlark_fmt.PrintBoolDict(defs.ValueVars, 0)
+	ret += "\n\n"
 
-	ret += "soong_config_string_variables = {\n"
-	for _, stringVar := range sortedStringKeys(defs.StringVars) {
-		ret += fmt.Sprintf("    \"%s\": [\n", stringVar)
-		for _, choice := range sortedStringKeys(defs.StringVars[stringVar]) {
-			ret += fmt.Sprintf("        \"%s\",\n", choice)
-		}
-		ret += fmt.Sprintf("    ],\n")
-	}
-	ret += "}"
+	ret += "soong_config_string_variables = "
+	ret += starlark_fmt.PrintStringListDict(defs.StringVars, 0)
 
 	return ret
 }

+ 35 - 36
android/soongconfig/modules_test.go

@@ -367,19 +367,19 @@ func Test_PropertiesToApply(t *testing.T) {
 
 func Test_Bp2BuildSoongConfigDefinitions(t *testing.T) {
 	testCases := []struct {
+		desc     string
 		defs     Bp2BuildSoongConfigDefinitions
 		expected string
 	}{
 		{
+			desc: "all empty",
 			defs: Bp2BuildSoongConfigDefinitions{},
-			expected: `soong_config_bool_variables = {
-}
+			expected: `soong_config_bool_variables = {}
 
-soong_config_value_variables = {
-}
+soong_config_value_variables = {}
 
-soong_config_string_variables = {
-}`}, {
+soong_config_string_variables = {}`}, {
+			desc: "only bool",
 			defs: Bp2BuildSoongConfigDefinitions{
 				BoolVars: map[string]bool{
 					"bool_var": true,
@@ -389,39 +389,35 @@ soong_config_string_variables = {
     "bool_var": True,
 }
 
-soong_config_value_variables = {
-}
+soong_config_value_variables = {}
 
-soong_config_string_variables = {
-}`}, {
+soong_config_string_variables = {}`}, {
+			desc: "only value vars",
 			defs: Bp2BuildSoongConfigDefinitions{
 				ValueVars: map[string]bool{
 					"value_var": true,
 				},
 			},
-			expected: `soong_config_bool_variables = {
-}
+			expected: `soong_config_bool_variables = {}
 
 soong_config_value_variables = {
     "value_var": True,
 }
 
-soong_config_string_variables = {
-}`}, {
+soong_config_string_variables = {}`}, {
+			desc: "only string vars",
 			defs: Bp2BuildSoongConfigDefinitions{
-				StringVars: map[string]map[string]bool{
-					"string_var": map[string]bool{
-						"choice1": true,
-						"choice2": true,
-						"choice3": true,
+				StringVars: map[string][]string{
+					"string_var": []string{
+						"choice1",
+						"choice2",
+						"choice3",
 					},
 				},
 			},
-			expected: `soong_config_bool_variables = {
-}
+			expected: `soong_config_bool_variables = {}
 
-soong_config_value_variables = {
-}
+soong_config_value_variables = {}
 
 soong_config_string_variables = {
     "string_var": [
@@ -430,6 +426,7 @@ soong_config_string_variables = {
         "choice3",
     ],
 }`}, {
+			desc: "all vars",
 			defs: Bp2BuildSoongConfigDefinitions{
 				BoolVars: map[string]bool{
 					"bool_var_one": true,
@@ -438,15 +435,15 @@ soong_config_string_variables = {
 					"value_var_one": true,
 					"value_var_two": true,
 				},
-				StringVars: map[string]map[string]bool{
-					"string_var_one": map[string]bool{
-						"choice1": true,
-						"choice2": true,
-						"choice3": true,
+				StringVars: map[string][]string{
+					"string_var_one": []string{
+						"choice1",
+						"choice2",
+						"choice3",
 					},
-					"string_var_two": map[string]bool{
-						"foo": true,
-						"bar": true,
+					"string_var_two": []string{
+						"foo",
+						"bar",
 					},
 				},
 			},
@@ -466,15 +463,17 @@ soong_config_string_variables = {
         "choice3",
     ],
     "string_var_two": [
-        "bar",
         "foo",
+        "bar",
     ],
 }`},
 	}
 	for _, test := range testCases {
-		actual := test.defs.String()
-		if actual != test.expected {
-			t.Errorf("Expected:\n%s\nbut got:\n%s", test.expected, actual)
-		}
+		t.Run(test.desc, func(t *testing.T) {
+			actual := test.defs.String()
+			if actual != test.expected {
+				t.Errorf("Expected:\n%s\nbut got:\n%s", test.expected, actual)
+			}
+		})
 	}
 }

+ 1 - 0
bp2build/Android.bp

@@ -28,6 +28,7 @@ bootstrap_go_package {
         "soong-genrule",
         "soong-python",
         "soong-sh",
+        "soong-starlark-format",
         "soong-ui-metrics",
     ],
     testSrcs: [

+ 11 - 45
bp2build/build_conversion.go

@@ -27,6 +27,7 @@ import (
 
 	"android/soong/android"
 	"android/soong/bazel"
+	"android/soong/starlark_fmt"
 
 	"github.com/google/blueprint"
 	"github.com/google/blueprint/proptools"
@@ -559,48 +560,27 @@ func prettyPrint(propertyValue reflect.Value, indent int, emitZeroValues bool) (
 		return "", nil
 	}
 
-	var ret string
 	switch propertyValue.Kind() {
 	case reflect.String:
-		ret = fmt.Sprintf("\"%v\"", escapeString(propertyValue.String()))
+		return fmt.Sprintf("\"%v\"", escapeString(propertyValue.String())), nil
 	case reflect.Bool:
-		ret = strings.Title(fmt.Sprintf("%v", propertyValue.Interface()))
+		return starlark_fmt.PrintBool(propertyValue.Bool()), nil
 	case reflect.Int, reflect.Uint, reflect.Int64:
-		ret = fmt.Sprintf("%v", propertyValue.Interface())
+		return fmt.Sprintf("%v", propertyValue.Interface()), nil
 	case reflect.Ptr:
 		return prettyPrint(propertyValue.Elem(), indent, emitZeroValues)
 	case reflect.Slice:
-		if propertyValue.Len() == 0 {
-			return "[]", nil
-		}
-
-		if propertyValue.Len() == 1 {
-			// Single-line list for list with only 1 element
-			ret += "["
-			indexedValue, err := prettyPrint(propertyValue.Index(0), indent, emitZeroValues)
+		elements := make([]string, 0, propertyValue.Len())
+		for i := 0; i < propertyValue.Len(); i++ {
+			val, err := prettyPrint(propertyValue.Index(i), indent, emitZeroValues)
 			if err != nil {
 				return "", err
 			}
-			ret += indexedValue
-			ret += "]"
-		} else {
-			// otherwise, use a multiline list.
-			ret += "[\n"
-			for i := 0; i < propertyValue.Len(); i++ {
-				indexedValue, err := prettyPrint(propertyValue.Index(i), indent+1, emitZeroValues)
-				if err != nil {
-					return "", err
-				}
-
-				if indexedValue != "" {
-					ret += makeIndent(indent + 1)
-					ret += indexedValue
-					ret += ",\n"
-				}
+			if val != "" {
+				elements = append(elements, val)
 			}
-			ret += makeIndent(indent)
-			ret += "]"
 		}
+		return starlark_fmt.PrintList(elements, indent, "%s"), nil
 
 	case reflect.Struct:
 		// Special cases where the bp2build sends additional information to the codegenerator
@@ -611,18 +591,12 @@ func prettyPrint(propertyValue reflect.Value, indent int, emitZeroValues bool) (
 			return fmt.Sprintf("%q", label.Label), nil
 		}
 
-		ret = "{\n"
 		// Sort and print the struct props by the key.
 		structProps := extractStructProperties(propertyValue, indent)
 		if len(structProps) == 0 {
 			return "", nil
 		}
-		for _, k := range android.SortedStringKeys(structProps) {
-			ret += makeIndent(indent + 1)
-			ret += fmt.Sprintf("%q: %s,\n", k, structProps[k])
-		}
-		ret += makeIndent(indent)
-		ret += "}"
+		return starlark_fmt.PrintDict(structProps, indent), nil
 	case reflect.Interface:
 		// TODO(b/164227191): implement pretty print for interfaces.
 		// Interfaces are used for for arch, multilib and target properties.
@@ -631,7 +605,6 @@ func prettyPrint(propertyValue reflect.Value, indent int, emitZeroValues bool) (
 		return "", fmt.Errorf(
 			"unexpected kind for property struct field: %s", propertyValue.Kind())
 	}
-	return ret, nil
 }
 
 // Converts a reflected property struct value into a map of property names and property values,
@@ -736,13 +709,6 @@ func escapeString(s string) string {
 	return strings.ReplaceAll(s, "\"", "\\\"")
 }
 
-func makeIndent(indent int) string {
-	if indent < 0 {
-		panic(fmt.Errorf("indent column cannot be less than 0, but got %d", indent))
-	}
-	return strings.Repeat("    ", indent)
-}
-
 func targetNameWithVariant(c bpToBuildContext, logicModule blueprint.Module) string {
 	name := ""
 	if c.ModuleSubDir(logicModule) != "" {

+ 4 - 3
bp2build/configurability.go

@@ -6,6 +6,7 @@ import (
 
 	"android/soong/android"
 	"android/soong/bazel"
+	"android/soong/starlark_fmt"
 )
 
 // Configurability support for bp2build.
@@ -250,10 +251,10 @@ func prettyPrintSelectMap(selectMap map[string]reflect.Value, defaultValue *stri
 	} else if defaultValue != nil {
 		// Print an explicit empty list (the default value) even if the value is
 		// empty, to avoid errors about not finding a configuration that matches.
-		ret += fmt.Sprintf("%s\"%s\": %s,\n", makeIndent(indent+1), bazel.ConditionsDefaultSelectKey, *defaultValue)
+		ret += fmt.Sprintf("%s\"%s\": %s,\n", starlark_fmt.Indention(indent+1), bazel.ConditionsDefaultSelectKey, *defaultValue)
 	}
 
-	ret += makeIndent(indent)
+	ret += starlark_fmt.Indention(indent)
 	ret += "})"
 
 	return ret, nil
@@ -262,7 +263,7 @@ func prettyPrintSelectMap(selectMap map[string]reflect.Value, defaultValue *stri
 // prettyPrintSelectEntry converts a reflect.Value into an entry in a select map
 // with a provided key.
 func prettyPrintSelectEntry(value reflect.Value, key string, indent int, emitZeroValues bool) (string, error) {
-	s := makeIndent(indent + 1)
+	s := starlark_fmt.Indention(indent + 1)
 	v, err := prettyPrint(value, indent+1, emitZeroValues)
 	if err != nil {
 		return "", err

+ 1 - 0
cc/config/Android.bp

@@ -8,6 +8,7 @@ bootstrap_go_package {
     deps: [
         "soong-android",
         "soong-remoteexec",
+        "soong-starlark-format",
     ],
     srcs: [
         "bp2build.go",

+ 4 - 33
cc/config/bp2build.go

@@ -22,14 +22,11 @@ import (
 	"strings"
 
 	"android/soong/android"
+	"android/soong/starlark_fmt"
 
 	"github.com/google/blueprint"
 )
 
-const (
-	bazelIndent = 4
-)
-
 type bazelVarExporter interface {
 	asBazel(android.Config, exportedStringVariables, exportedStringListVariables, exportedConfigDependingVariables) []bazelConstant
 }
@@ -73,21 +70,6 @@ func (m exportedStringVariables) Set(k string, v string) {
 	m[k] = v
 }
 
-func bazelIndention(level int) string {
-	return strings.Repeat(" ", level*bazelIndent)
-}
-
-func printBazelList(items []string, indentLevel int) string {
-	list := make([]string, 0, len(items)+2)
-	list = append(list, "[")
-	innerIndent := bazelIndention(indentLevel + 1)
-	for _, item := range items {
-		list = append(list, fmt.Sprintf(`%s"%s",`, innerIndent, item))
-	}
-	list = append(list, bazelIndention(indentLevel)+"]")
-	return strings.Join(list, "\n")
-}
-
 func (m exportedStringVariables) asBazel(config android.Config,
 	stringVars exportedStringVariables, stringListVars exportedStringListVariables, cfgDepVars exportedConfigDependingVariables) []bazelConstant {
 	ret := make([]bazelConstant, 0, len(m))
@@ -139,7 +121,7 @@ func (m exportedStringListVariables) asBazel(config android.Config,
 		// out through a constants struct later.
 		ret = append(ret, bazelConstant{
 			variableName:       k,
-			internalDefinition: printBazelList(expandedVars, 0),
+			internalDefinition: starlark_fmt.PrintStringList(expandedVars, 0),
 		})
 	}
 	return ret
@@ -173,17 +155,6 @@ func (m exportedStringListDictVariables) Set(k string, v map[string][]string) {
 	m[k] = v
 }
 
-func printBazelStringListDict(dict map[string][]string) string {
-	bazelDict := make([]string, 0, len(dict)+2)
-	bazelDict = append(bazelDict, "{")
-	for k, v := range dict {
-		bazelDict = append(bazelDict,
-			fmt.Sprintf(`%s"%s": %s,`, bazelIndention(1), k, printBazelList(v, 1)))
-	}
-	bazelDict = append(bazelDict, "}")
-	return strings.Join(bazelDict, "\n")
-}
-
 // Since dictionaries are not supported in Ninja, we do not expand variables for dictionaries
 func (m exportedStringListDictVariables) asBazel(_ android.Config, _ exportedStringVariables,
 	_ exportedStringListVariables, _ exportedConfigDependingVariables) []bazelConstant {
@@ -191,7 +162,7 @@ func (m exportedStringListDictVariables) asBazel(_ android.Config, _ exportedStr
 	for k, dict := range m {
 		ret = append(ret, bazelConstant{
 			variableName:       k,
-			internalDefinition: printBazelStringListDict(dict),
+			internalDefinition: starlark_fmt.PrintStringListDict(dict, 0),
 		})
 	}
 	return ret
@@ -223,7 +194,7 @@ func bazelToolchainVars(config android.Config, vars ...bazelVarExporter) string
 		definitions = append(definitions,
 			fmt.Sprintf("_%s = %s", b.variableName, b.internalDefinition))
 		constants = append(constants,
-			fmt.Sprintf("%[1]s%[2]s = _%[2]s,", bazelIndention(1), b.variableName))
+			fmt.Sprintf("%[1]s%[2]s = _%[2]s,", starlark_fmt.Indention(1), b.variableName))
 	}
 
 	// Build the exported constants struct.

+ 6 - 18
cc/config/bp2build_test.go

@@ -211,15 +211,11 @@ constants = struct(
 			expectedOut: `# GENERATED FOR BAZEL FROM SOONG. DO NOT EDIT.
 
 _a = {
-    "b1": [
-        "b2",
-    ],
+    "b1": ["b2"],
 }
 
 _c = {
-    "d1": [
-        "d2",
-    ],
+    "d1": ["d2"],
 }
 
 constants = struct(
@@ -246,27 +242,19 @@ constants = struct(
 			expectedOut: `# GENERATED FOR BAZEL FROM SOONG. DO NOT EDIT.
 
 _a = {
-    "a1": [
-        "a2",
-    ],
+    "a1": ["a2"],
 }
 
 _b = "b-val"
 
-_c = [
-    "c-val",
-]
+_c = ["c-val"]
 
 _d = "d-val"
 
-_e = [
-    "e-val",
-]
+_e = ["e-val"]
 
 _f = {
-    "f1": [
-        "f2",
-    ],
+    "f1": ["f2"],
 }
 
 constants = struct(

+ 28 - 0
starlark_fmt/Android.bp

@@ -0,0 +1,28 @@
+// Copyright 2022 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+bootstrap_go_package {
+    name: "soong-starlark-format",
+    pkgPath: "android/soong/starlark_fmt",
+    srcs: [
+        "format.go",
+    ],
+    testSrcs: [
+        "format_test.go",
+    ],
+}

+ 96 - 0
starlark_fmt/format.go

@@ -0,0 +1,96 @@
+// Copyright 2022 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 starlark_fmt
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+)
+
+const (
+	indent = 4
+)
+
+// Indention returns an indent string of the specified level.
+func Indention(level int) string {
+	if level < 0 {
+		panic(fmt.Errorf("indent level cannot be less than 0, but got %d", level))
+	}
+	return strings.Repeat(" ", level*indent)
+}
+
+// PrintBool returns a Starlark compatible bool string.
+func PrintBool(item bool) string {
+	return strings.Title(fmt.Sprintf("%t", item))
+}
+
+// PrintsStringList returns a Starlark-compatible string of a list of Strings/Labels.
+func PrintStringList(items []string, indentLevel int) string {
+	return PrintList(items, indentLevel, `"%s"`)
+}
+
+// PrintList returns a Starlark-compatible string of list formmated as requested.
+func PrintList(items []string, indentLevel int, formatString string) string {
+	if len(items) == 0 {
+		return "[]"
+	} else if len(items) == 1 {
+		return fmt.Sprintf("["+formatString+"]", items[0])
+	}
+	list := make([]string, 0, len(items)+2)
+	list = append(list, "[")
+	innerIndent := Indention(indentLevel + 1)
+	for _, item := range items {
+		list = append(list, fmt.Sprintf(`%s`+formatString+`,`, innerIndent, item))
+	}
+	list = append(list, Indention(indentLevel)+"]")
+	return strings.Join(list, "\n")
+}
+
+// PrintStringListDict returns a Starlark-compatible string formatted as dictionary with
+// string keys and list of string values.
+func PrintStringListDict(dict map[string][]string, indentLevel int) string {
+	formattedValueDict := make(map[string]string, len(dict))
+	for k, v := range dict {
+		formattedValueDict[k] = PrintStringList(v, indentLevel+1)
+	}
+	return PrintDict(formattedValueDict, indentLevel)
+}
+
+// PrintBoolDict returns a starlark-compatible string containing a dictionary with string keys and
+// values printed with no additional formatting.
+func PrintBoolDict(dict map[string]bool, indentLevel int) string {
+	formattedValueDict := make(map[string]string, len(dict))
+	for k, v := range dict {
+		formattedValueDict[k] = PrintBool(v)
+	}
+	return PrintDict(formattedValueDict, indentLevel)
+}
+
+// PrintDict returns a starlark-compatible string containing a dictionary with string keys and
+// values printed with no additional formatting.
+func PrintDict(dict map[string]string, indentLevel int) string {
+	if len(dict) == 0 {
+		return "{}"
+	}
+	items := make([]string, 0, len(dict))
+	for k, v := range dict {
+		items = append(items, fmt.Sprintf(`%s"%s": %s,`, Indention(indentLevel+1), k, v))
+	}
+	sort.Strings(items)
+	return fmt.Sprintf(`{
+%s
+%s}`, strings.Join(items, "\n"), Indention(indentLevel))
+}

+ 169 - 0
starlark_fmt/format_test.go

@@ -0,0 +1,169 @@
+// Copyright 2022 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 starlark_fmt
+
+import (
+	"testing"
+)
+
+func TestPrintEmptyStringList(t *testing.T) {
+	in := []string{}
+	indentLevel := 0
+	out := PrintStringList(in, indentLevel)
+	expectedOut := "[]"
+	if out != expectedOut {
+		t.Errorf("Expected %q, got %q", expectedOut, out)
+	}
+}
+
+func TestPrintSingleElementStringList(t *testing.T) {
+	in := []string{"a"}
+	indentLevel := 0
+	out := PrintStringList(in, indentLevel)
+	expectedOut := `["a"]`
+	if out != expectedOut {
+		t.Errorf("Expected %q, got %q", expectedOut, out)
+	}
+}
+
+func TestPrintMultiElementStringList(t *testing.T) {
+	in := []string{"a", "b"}
+	indentLevel := 0
+	out := PrintStringList(in, indentLevel)
+	expectedOut := `[
+    "a",
+    "b",
+]`
+	if out != expectedOut {
+		t.Errorf("Expected %q, got %q", expectedOut, out)
+	}
+}
+
+func TestPrintEmptyList(t *testing.T) {
+	in := []string{}
+	indentLevel := 0
+	out := PrintList(in, indentLevel, "%s")
+	expectedOut := "[]"
+	if out != expectedOut {
+		t.Errorf("Expected %q, got %q", expectedOut, out)
+	}
+}
+
+func TestPrintSingleElementList(t *testing.T) {
+	in := []string{"1"}
+	indentLevel := 0
+	out := PrintList(in, indentLevel, "%s")
+	expectedOut := `[1]`
+	if out != expectedOut {
+		t.Errorf("Expected %q, got %q", expectedOut, out)
+	}
+}
+
+func TestPrintMultiElementList(t *testing.T) {
+	in := []string{"1", "2"}
+	indentLevel := 0
+	out := PrintList(in, indentLevel, "%s")
+	expectedOut := `[
+    1,
+    2,
+]`
+	if out != expectedOut {
+		t.Errorf("Expected %q, got %q", expectedOut, out)
+	}
+}
+
+func TestListWithNonZeroIndent(t *testing.T) {
+	in := []string{"1", "2"}
+	indentLevel := 1
+	out := PrintList(in, indentLevel, "%s")
+	expectedOut := `[
+        1,
+        2,
+    ]`
+	if out != expectedOut {
+		t.Errorf("Expected %q, got %q", expectedOut, out)
+	}
+}
+
+func TestStringListDictEmpty(t *testing.T) {
+	in := map[string][]string{}
+	indentLevel := 0
+	out := PrintStringListDict(in, indentLevel)
+	expectedOut := `{}`
+	if out != expectedOut {
+		t.Errorf("Expected %q, got %q", expectedOut, out)
+	}
+}
+
+func TestStringListDict(t *testing.T) {
+	in := map[string][]string{
+		"key1": []string{},
+		"key2": []string{"a"},
+		"key3": []string{"1", "2"},
+	}
+	indentLevel := 0
+	out := PrintStringListDict(in, indentLevel)
+	expectedOut := `{
+    "key1": [],
+    "key2": ["a"],
+    "key3": [
+        "1",
+        "2",
+    ],
+}`
+	if out != expectedOut {
+		t.Errorf("Expected %q, got %q", expectedOut, out)
+	}
+}
+
+func TestPrintDict(t *testing.T) {
+	in := map[string]string{
+		"key1": `""`,
+		"key2": `"a"`,
+		"key3": `[
+        1,
+        2,
+    ]`,
+	}
+	indentLevel := 0
+	out := PrintDict(in, indentLevel)
+	expectedOut := `{
+    "key1": "",
+    "key2": "a",
+    "key3": [
+        1,
+        2,
+    ],
+}`
+	if out != expectedOut {
+		t.Errorf("Expected %q, got %q", expectedOut, out)
+	}
+}
+
+func TestPrintDictWithIndent(t *testing.T) {
+	in := map[string]string{
+		"key1": `""`,
+		"key2": `"a"`,
+	}
+	indentLevel := 1
+	out := PrintDict(in, indentLevel)
+	expectedOut := `{
+        "key1": "",
+        "key2": "a",
+    }`
+	if out != expectedOut {
+		t.Errorf("Expected %q, got %q", expectedOut, out)
+	}
+}