Generate FlatConfig objects from GenericConfig objects.
Doesn't include tests. More of those will come later.
Test: build/make/tools/product_config/test.sh
Change-Id: Icd2b455ac5f7b4773ba332fc40e994dc6f024f1b
diff --git a/core/dumpconfig.mk b/core/dumpconfig.mk
index dd3ef43..9b1f2c2 100644
--- a/core/dumpconfig.mk
+++ b/core/dumpconfig.mk
@@ -36,6 +36,10 @@
$(error stopping)
endif
+# Skip the second inclusion of all of the product config files, because
+# we will do these checks in the product_config tool.
+SKIP_ARTIFACT_PATH_REQUIREMENT_PRODUCTS_CHECK := true
+
# Before we do anything else output the format version.
$(file > $(DUMPCONFIG_FILE),dumpconfig_version,1)
$(file >> $(DUMPCONFIG_FILE),dumpconfig_file,$(DUMPCONFIG_FILE))
@@ -75,7 +79,7 @@
endef
# Args:
-# $(1): Config phase (PRODUCT or DEVICE)
+# $(1): Config phase (PRODUCT, EXPAND, or DEVICE)
# $(2): Root nodes to import
# $(3): All variable names
# $(4): Single-value variables
@@ -104,10 +108,21 @@
.KATI_SYMBOLS \
1 \
2 \
+ 3 \
+ 4 \
+ 5 \
+ 6 \
+ 7 \
+ 8 \
+ 9 \
LOCAL_PATH \
MAKEFILE_LIST \
PARENT_PRODUCT_FILES \
current_mk \
+ _eiv_ev \
+ _eiv_i \
+ _eiv_sv \
+ _eiv_tv \
inherit_var \
np \
_node_import_context \
diff --git a/core/product.mk b/core/product.mk
index 170402a..8976dd9 100644
--- a/core/product.mk
+++ b/core/product.mk
@@ -606,6 +606,8 @@
# to a shorthand that is more convenient to read from elsewhere.
#
define strip-product-vars
+$(call dump-phase-start,PRODUCT-EXPAND,,$(_product_var_list),$(_product_single_value_vars), \
+ build/make/core/product.mk) \
$(foreach v,\
$(_product_var_list) \
PRODUCT_ENFORCE_PACKAGES_EXIST \
@@ -613,7 +615,8 @@
$(eval $(v) := $(strip $(PRODUCTS.$(INTERNAL_PRODUCT).$(v)))) \
$(eval get-product-var = $$(if $$(filter $$(1),$$(INTERNAL_PRODUCT)),$$($$(2)),$$(PRODUCTS.$$(strip $$(1)).$$(2)))) \
$(KATI_obsolete_var PRODUCTS.$(INTERNAL_PRODUCT).$(v),Use $(v) instead) \
-)
+) \
+$(call dump-phase-end,build/make/core/product.mk)
endef
define add-to-product-copy-files-if-exists
diff --git a/core/product_config.mk b/core/product_config.mk
index 6d886ec..c1c08d1 100644
--- a/core/product_config.mk
+++ b/core/product_config.mk
@@ -163,12 +163,14 @@
# Quick check
$(check-all-products)
+ifeq ($(SKIP_ARTIFACT_PATH_REQUIREMENT_PRODUCTS_CHECK),)
# Import all the products that have made artifact path requirements, so that we can verify
# the artifacts they produce.
# These are imported after check-all-products because some of them might not be real products.
$(foreach makefile,$(ARTIFACT_PATH_REQUIREMENT_PRODUCTS),\
$(if $(filter-out $(makefile),$(PRODUCTS)),$(eval $(call import-products,$(makefile))))\
)
+endif
ifneq ($(filter dump-products, $(MAKECMDGOALS)),)
$(dump-products)
@@ -181,14 +183,16 @@
ifneq ($(current_product_makefile),$(INTERNAL_PRODUCT))
$(error PRODUCT_NAME inconsistent in $(current_product_makefile) and $(INTERNAL_PRODUCT))
endif
-current_product_makefile :=
-all_product_makefiles :=
-all_product_configs :=
+
############################################################################
# Strip and assign the PRODUCT_ variables.
$(call strip-product-vars)
+current_product_makefile :=
+all_product_makefiles :=
+all_product_configs :=
+
#############################################################################
# Quick check and assign default values
diff --git a/tools/product_config/inherit_tree.py b/tools/product_config/inherit_tree.py
new file mode 100755
index 0000000..ae8a275
--- /dev/null
+++ b/tools/product_config/inherit_tree.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+
+#
+# Run from the root of the tree, after product-config has been run to see
+# the product inheritance hierarchy for the current lunch target.
+#
+
+import csv
+import sys
+
+def PrintNodes(graph, node, prefix):
+ sys.stdout.write("%s%s" % (prefix, node))
+ children = graph.get(node, [])
+ if children:
+ sys.stdout.write(" {\n")
+ for child in sorted(graph.get(node, [])):
+ PrintNodes(graph, child, prefix + " ")
+ sys.stdout.write("%s}\n" % prefix);
+ else:
+ sys.stdout.write("\n")
+
+def main(argv):
+ if len(argv) != 2:
+ print("usage: inherit_tree.py out/$TARGET_PRODUCT-$TARGET_BUILD_VARIANT/dumpconfig.csv")
+ sys.exit(1)
+
+ root = None
+ graph = {}
+ with open(argv[1], newline='') as csvfile:
+ for line in csv.reader(csvfile):
+ if not root:
+ # Look for PRODUCTS
+ if len(line) < 3 or line[0] != "phase" or line[1] != "PRODUCTS":
+ continue
+ root = line[2]
+ else:
+ # Everything else
+ if len(line) < 3 or line[0] != "inherit":
+ continue
+ graph.setdefault(line[1], list()).append(line[2])
+
+ PrintNodes(graph, root, "")
+
+
+if __name__ == "__main__":
+ main(sys.argv)
+
+# vim: set expandtab ts=2 sw=2 sts=2:
+
diff --git a/tools/product_config/src/com/android/build/config/ConvertMakeToGenericConfig.java b/tools/product_config/src/com/android/build/config/ConvertMakeToGenericConfig.java
index ca31cd5..39bd5df 100644
--- a/tools/product_config/src/com/android/build/config/ConvertMakeToGenericConfig.java
+++ b/tools/product_config/src/com/android/build/config/ConvertMakeToGenericConfig.java
@@ -31,14 +31,20 @@
mErrors = errors;
}
- public GenericConfig convert(MakeConfig make) {
+ public GenericConfig convert(Map<String, MakeConfig> make) {
final GenericConfig result = new GenericConfig();
+ final MakeConfig products = make.get("PRODUCTS");
+ if (products == null) {
+ mErrors.ERROR_DUMPCONFIG.add("Could not find PRODUCTS phase in dumpconfig output.");
+ return null;
+ }
+
// Base class fields
- result.copyFrom(make);
+ result.copyFrom(products);
// Each file
- for (MakeConfig.ConfigFile f: make.getConfigFiles()) {
+ for (MakeConfig.ConfigFile f: products.getConfigFiles()) {
final GenericConfig.ConfigFile genericFile
= new GenericConfig.ConfigFile(f.getFilename());
result.addConfigFile(genericFile);
@@ -77,7 +83,7 @@
for (final Map.Entry<String, Str> entry: block.getVars().entrySet()) {
final String varName = entry.getKey();
final GenericConfig.Assign assign = convertAssignment(block.getBlockType(),
- block.getInheritedFile(), make.getVarType(varName), varName,
+ block.getInheritedFile(), products.getVarType(varName), varName,
entry.getValue(), prevBlock.getVar(varName));
if (assign != null) {
genericFile.addStatement(assign);
@@ -100,6 +106,29 @@
prevBlock = block;
}
}
+
+ // Overwrite the final variables with the ones that come from the PRODUCTS-EXPAND phase.
+ // Drop the ones that were newly defined between the two phases, but leave values
+ // that were modified between. We do need to reproduce that logic in this tool.
+ final MakeConfig expand = make.get("PRODUCT-EXPAND");
+ if (expand == null) {
+ mErrors.ERROR_DUMPCONFIG.add("Could not find PRODUCT-EXPAND phase in dumpconfig"
+ + " output.");
+ return null;
+ }
+ final Map<String, Str> productsFinal = products.getFinalVariables();
+ final Map<String, Str> expandInitial = expand.getInitialVariables();
+ final Map<String, Str> expandFinal = expand.getFinalVariables();
+ final Map<String, Str> finalFinal = result.getFinalVariables();
+ finalFinal.clear();
+ for (Map.Entry<String, Str> var: expandFinal.entrySet()) {
+ final String varName = var.getKey();
+ if (expandInitial.containsKey(varName) && !productsFinal.containsKey(varName)) {
+ continue;
+ }
+ finalFinal.put(varName, var.getValue());
+ }
+
return result;
}
@@ -113,7 +142,7 @@
return new GenericConfig.Assign(varName, varVal);
} else if (!varVal.equals(prevVal)) {
// The value changed from the last block.
- if (varVal.equals("")) {
+ if (varVal.length() == 0) {
// It was set to empty
return new GenericConfig.Assign(varName, varVal);
} else {
diff --git a/tools/product_config/src/com/android/build/config/DumpConfigParser.java b/tools/product_config/src/com/android/build/config/DumpConfigParser.java
index 6da96c1..c4cd963 100644
--- a/tools/product_config/src/com/android/build/config/DumpConfigParser.java
+++ b/tools/product_config/src/com/android/build/config/DumpConfigParser.java
@@ -44,13 +44,13 @@
* 4 The location of the variable, as best tracked by kati
*/
public class DumpConfigParser {
- private static final boolean DEBUG = true;
+ private static final boolean DEBUG = false;
private final Errors mErrors;
private final String mFilename;
private final Reader mReader;
- private final ArrayList<MakeConfig> mResults = new ArrayList();
+ private final Map<String,MakeConfig> mResults = new HashMap();
private static final Pattern LIST_SEPARATOR = Pattern.compile("\\s+");
@@ -64,9 +64,9 @@
}
/**
- * Parse the text into a list of MakeConfig objects.
+ * Parse the text into a map of the phase names to MakeConfig objects.
*/
- public static ArrayList<MakeConfig> parse(Errors errors, String filename, Reader reader)
+ public static Map<String,MakeConfig> parse(Errors errors, String filename, Reader reader)
throws CsvParser.ParseException, IOException {
DumpConfigParser parser = new DumpConfigParser(errors, filename, reader);
parser.parseImpl();
@@ -130,7 +130,16 @@
makeConfig = new MakeConfig();
makeConfig.setPhase(fields.get(1));
makeConfig.setRootNodes(splitList(fields.get(2)));
- mResults.add(makeConfig);
+ // If there is a duplicate phase of the same name, continue parsing, but
+ // don't add it. Emit a warning.
+ if (!mResults.containsKey(makeConfig.getPhase())) {
+ mResults.put(makeConfig.getPhase(), makeConfig);
+ } else {
+ mErrors.WARNING_DUMPCONFIG.add(
+ new Position(mFilename, line.getLine()),
+ "Duplicate phase: " + makeConfig.getPhase()
+ + ". This one will be dropped.");
+ }
initialVariables = makeConfig.getInitialVariables();
finalVariables = makeConfig.getFinalVariables();
diff --git a/tools/product_config/src/com/android/build/config/ErrorReporter.java b/tools/product_config/src/com/android/build/config/ErrorReporter.java
index 5d87636..0a0c9f4 100644
--- a/tools/product_config/src/com/android/build/config/ErrorReporter.java
+++ b/tools/product_config/src/com/android/build/config/ErrorReporter.java
@@ -171,7 +171,7 @@
/**
* An instance of an error happening.
*/
- public class Entry {
+ public static class Entry {
private final Category mCategory;
private final Position mPosition;
private final String mMessage;
diff --git a/tools/product_config/src/com/android/build/config/Errors.java b/tools/product_config/src/com/android/build/config/Errors.java
index 92a4b30..b333e78 100644
--- a/tools/product_config/src/com/android/build/config/Errors.java
+++ b/tools/product_config/src/com/android/build/config/Errors.java
@@ -59,4 +59,16 @@
// if we're seeing this.
public final Category ERROR_IMPROPER_PRODUCT_VAR_MARKER = new Category(7, true, Level.ERROR,
"Bad input from dumpvars causing corrupted product variables.");
+
+ public final Category ERROR_MISSING_CONFIG_FILE = new Category(8, true, Level.ERROR,
+ "Unable to find config file.");
+
+ public final Category ERROR_INFINITE_RECURSION = new Category(9, true, Level.ERROR,
+ "A file tries to inherit-product from itself or its own inherited products.");
+
+ // TODO: This will become obsolete when it is possible to have starlark-based product
+ // config files.
+ public final Category WARNING_DIFFERENT_FROM_KATI = new Category(1000, true, Level.WARNING,
+ "The cross-check with the original kati implementation failed.");
+
}
diff --git a/tools/product_config/src/com/android/build/config/FlatConfig.java b/tools/product_config/src/com/android/build/config/FlatConfig.java
new file mode 100644
index 0000000..6f277fe
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/FlatConfig.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+package com.android.build.config;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Flattened configuration -- set of variables after all assignments and inherits have
+ * been executed.
+ */
+public class FlatConfig extends ConfigBase {
+
+ private final TreeMap<String, Value> mValues = new TreeMap();
+
+ public TreeMap<String, Value> getValues() {
+ return mValues;
+ }
+}
diff --git a/tools/product_config/src/com/android/build/config/FlattenConfig.java b/tools/product_config/src/com/android/build/config/FlattenConfig.java
new file mode 100644
index 0000000..a19802b
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/FlattenConfig.java
@@ -0,0 +1,474 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+package com.android.build.config;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.regex.Pattern;
+
+public class FlattenConfig {
+ private static final Pattern RE_SPACE = Pattern.compile("\\p{Space}+");
+ private static final String PRODUCTS_PREFIX = "PRODUCTS";
+
+ private final Errors mErrors;
+ private final GenericConfig mGenericConfig;
+ private final Map<String, GenericConfig.ConfigFile> mGenericConfigs;
+ private final FlatConfig mResult = new FlatConfig();
+ private final Map<String, Value> mVariables;
+ /**
+ * Files that have been visited, to prevent infinite recursion. There are no
+ * conditionals at this point in the processing, so we don't need a stack, just
+ * a single set.
+ */
+ private final Set<Str> mStack = new HashSet();
+
+
+ private FlattenConfig(Errors errors, GenericConfig genericConfig) {
+ mErrors = errors;
+ mGenericConfig = genericConfig;
+ mGenericConfigs = genericConfig.getFiles();
+ mVariables = mResult.getValues();
+
+ // Base class fields
+ mResult.copyFrom(genericConfig);
+ }
+
+ /**
+ * Flatten a GenericConfig to a FlatConfig.
+ *
+ * Makes three passes through the genericConfig, one to flatten the single variables,
+ * one to flatten the list variables, and one to flatten the unknown variables. Each
+ * has a slightly different algorithm.
+ */
+ public static FlatConfig flatten(Errors errors, GenericConfig genericConfig) {
+ final FlattenConfig flattener = new FlattenConfig(errors, genericConfig);
+ return flattener.flattenImpl();
+ }
+
+ private FlatConfig flattenImpl() {
+ final List<String> rootNodes = mGenericConfig.getRootNodes();
+ if (rootNodes.size() == 0) {
+ mErrors.ERROR_DUMPCONFIG.add("No root nodes in PRODUCTS phase.");
+ return null;
+ } else if (rootNodes.size() != 1) {
+ final StringBuilder msg = new StringBuilder(
+ "Ignoring extra root nodes in PRODUCTS phase. All nodes are:");
+ for (final String rn: rootNodes) {
+ msg.append(' ');
+ msg.append(rn);
+ }
+ mErrors.WARNING_DUMPCONFIG.add(msg.toString());
+ }
+ final String root = rootNodes.get(0);
+
+ // TODO: Do we need to worry about the initial state of variables? Anything
+ // that from the product config
+
+ flattenListVars(root);
+ flattenSingleVars(root);
+ flattenUnknownVars(root);
+ flattenInheritsFrom(root);
+
+ setDefaultKnownVars();
+
+ // TODO: This only supports the single product mode of import-nodes, which is all the
+ // real build does. m product-graph and friends will have to be rewritten.
+ mVariables.put("PRODUCTS", new Value(VarType.UNKNOWN, new Str(root)));
+
+ return mResult;
+ }
+
+ interface AssignCallback {
+ void onAssignStatement(GenericConfig.Assign assign);
+ }
+
+ interface InheritCallback {
+ void onInheritStatement(GenericConfig.Inherit assign);
+ }
+
+ /**
+ * Do a bunch of validity checks, and then iterate through each of the statements
+ * in the given file. For Assignments, the callback is only called for variables
+ * matching varType.
+ *
+ * Adds makefiles which have been traversed to the 'seen' set, and will not traverse
+ * into an inherit statement if its makefile has already been seen.
+ */
+ private void forEachStatement(Str filename, VarType varType, Set<String> seen,
+ AssignCallback assigner, InheritCallback inheriter) {
+ if (mStack.contains(filename)) {
+ mErrors.ERROR_INFINITE_RECURSION.add(filename.getPosition(),
+ "File is already in the inherit-product stack: " + filename);
+ return;
+ }
+
+ mStack.add(filename);
+ try {
+ final GenericConfig.ConfigFile genericFile = mGenericConfigs.get(filename.toString());
+
+ if (genericFile == null) {
+ mErrors.ERROR_MISSING_CONFIG_FILE.add(filename.getPosition(),
+ "Unable to find config file: " + filename);
+ return;
+ }
+
+ for (final GenericConfig.Statement statement: genericFile.getStatements()) {
+ if (statement instanceof GenericConfig.Assign) {
+ if (assigner != null) {
+ final GenericConfig.Assign assign = (GenericConfig.Assign)statement;
+ final String varName = assign.getName();
+
+ // Assert that we're not stomping on another variable, which
+ // really should be impossible at this point.
+ assertVarType(filename, varName);
+
+ if (mGenericConfig.getVarType(varName) == varType) {
+ assigner.onAssignStatement(assign);
+ }
+ }
+ } else if (statement instanceof GenericConfig.Inherit) {
+ if (inheriter != null) {
+ final GenericConfig.Inherit inherit = (GenericConfig.Inherit)statement;
+ if (seen != null) {
+ if (seen.contains(inherit.getFilename().toString())) {
+ continue;
+ }
+ seen.add(inherit.getFilename().toString());
+ }
+ inheriter.onInheritStatement(inherit);
+ }
+ }
+ }
+ } finally {
+ // Also executes after return statements, so we always remove this.
+ mStack.remove(filename);
+ }
+ }
+
+ /**
+ * Call 'inheriter' for each child of 'filename' in alphabetical order.
+ */
+ private void forEachInheritAlpha(final Str filename, VarType varType, Set<String> seen,
+ InheritCallback inheriter) {
+ final TreeMap<Str, GenericConfig.Inherit> alpha = new TreeMap();
+ forEachStatement(filename, varType, null, null,
+ (inherit) -> {
+ alpha.put(inherit.getFilename(), inherit);
+ });
+ for (final GenericConfig.Inherit inherit: alpha.values()) {
+ // Handle 'seen' here where we actaully call back, not before, so that
+ // the proper traversal order is preserved.
+ if (seen != null) {
+ if (seen.contains(inherit.getFilename().toString())) {
+ continue;
+ }
+ seen.add(inherit.getFilename().toString());
+ }
+ inheriter.onInheritStatement(inherit);
+ }
+ }
+
+ /**
+ * Traverse the inheritance hierarchy, setting list-value product config variables.
+ */
+ private void flattenListVars(final String filename) {
+ Map<String, Value> vars = flattenListVars(new Str(filename), new HashSet());
+ // Add the result of the recursion to mVariables. We know there will be
+ // no collisions because this function only handles list variables.
+ for (Map.Entry<String, Value> entry: vars.entrySet()) {
+ mVariables.put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * Return the variables defined, recursively, by 'filename.' The 'seen' set
+ * accumulates which nodes have been visited, as each is only done once.
+ *
+ * This convoluted algorithm isn't ideal, but it matches what is in node_fns.mk.
+ */
+ private Map<String, Value> flattenListVars(final Str filename, Set<String> seen) {
+ Map<String, Value> result = new HashMap();
+
+ // Recurse into our children first in alphabetical order, building a map of
+ // that filename to its flattened values. The order matters here because
+ // we will only look at each child once, and when a file appears multiple
+ // times, its variables must have the right set, based on whether it's been
+ // seen before. This preserves the order from node_fns.mk.
+
+ // Child filename --> { varname --> value }
+ final Map<Str, Map<String, Value>> children = new HashMap();
+ forEachInheritAlpha(filename, VarType.LIST, seen,
+ (inherit) -> {
+ final Str child = inherit.getFilename();
+ children.put(child, flattenListVars(child, seen));
+ });
+
+ // Now, traverse the values again in the original source order to concatenate the values.
+ // Note that the contcatenation order is *different* from the inherit order above.
+ forEachStatement(filename, VarType.LIST, null,
+ (assign) -> {
+ assignToListVar(result, assign.getName(), assign.getValue());
+ },
+ (inherit) -> {
+ final Map<String, Value> child = children.get(inherit.getFilename());
+ // child == null happens if this node has been visited before.
+ if (child != null) {
+ for (Map.Entry<String, Value> entry: child.entrySet()) {
+ final String varName = entry.getKey();
+ final Value varVal = entry.getValue();
+ appendToListVar(result, varName, varVal.getList());
+ }
+ }
+ });
+
+ return result;
+ }
+
+ /**
+ * Traverse the inheritance hierarchy, setting single-value product config variables.
+ */
+ private void flattenSingleVars(final String filename) {
+ flattenSingleVars(new Str(filename), new HashSet(), new HashSet());
+ }
+
+ private void flattenSingleVars(final Str filename, Set<String> seen1, Set<String> seen2) {
+ // flattenSingleVars has two loops. The first sets all variables that are
+ // defined for *this* file. The second traverses through the inheritance,
+ // to fill in values that weren't defined in this file. The first appearance of
+ // the variable is the one that wins.
+
+ forEachStatement(filename, VarType.SINGLE, seen1,
+ (assign) -> {
+ final String varName = assign.getName();
+ Value v = mVariables.get(varName);
+ // Only take the first value that we see for single variables.
+ Value value = mVariables.get(varName);
+ if (!mVariables.containsKey(varName)) {
+ final List<Str> valueList = assign.getValue();
+ // There should never be more than one item in this list, because
+ // SINGLE values should never be appended to.
+ if (valueList.size() != 1) {
+ final StringBuilder positions = new StringBuilder("[");
+ for (Str s: valueList) {
+ positions.append(s.getPosition());
+ }
+ positions.append(" ]");
+ throw new RuntimeException("Value list found for SINGLE variable "
+ + varName + " size=" + valueList.size()
+ + "positions=" + positions.toString());
+ }
+ mVariables.put(varName,
+ new Value(VarType.SINGLE,
+ valueList.get(0)));
+ }
+ }, null);
+
+ forEachInheritAlpha(filename, VarType.SINGLE, seen2,
+ (inherit) -> {
+ flattenSingleVars(inherit.getFilename(), seen1, seen2);
+ });
+ }
+
+ /**
+ * Traverse the inheritance hierarchy and flatten the values
+ */
+ private void flattenUnknownVars(String filename) {
+ flattenUnknownVars(new Str(filename), new HashSet());
+ }
+
+ private void flattenUnknownVars(final Str filename, Set<String> seen) {
+ // flattenUnknownVars has two loops: First to attempt to set the variable from
+ // this file, and then a second loop to handle the inheritance. This is odd
+ // but it matches the order the files are included in node_fns.mk. The last appearance
+ // of the value is the one that wins.
+
+ forEachStatement(filename, VarType.UNKNOWN, null,
+ (assign) -> {
+ // Overwrite the current value with whatever is now in the file.
+ mVariables.put(assign.getName(),
+ new Value(VarType.UNKNOWN,
+ flattenAssignList(assign, new Str(""))));
+ }, null);
+
+ forEachInheritAlpha(filename, VarType.UNKNOWN, seen,
+ (inherit) -> {
+ flattenUnknownVars(inherit.getFilename(), seen);
+ });
+ }
+
+ String prefix = "";
+
+ /**
+ * Sets the PRODUCTS.<filename>.INHERITS_FROM variables.
+ */
+ private void flattenInheritsFrom(final String filename) {
+ flattenInheritsFrom(new Str(filename));
+ }
+
+ /**
+ * This flatten function, unlike the others visits all of the nodes regardless
+ * of whether they have been seen before, because that's what the make code does.
+ */
+ private void flattenInheritsFrom(final Str filename) {
+ // Recurse, and gather the list our chlidren
+ final TreeSet<Str> children = new TreeSet();
+ forEachStatement(filename, VarType.LIST, null, null,
+ (inherit) -> {
+ children.add(inherit.getFilename());
+ flattenInheritsFrom(inherit.getFilename());
+ });
+
+ final String varName = "PRODUCTS." + filename + ".INHERITS_FROM";
+ if (children.size() > 0) {
+ // Build the space separated list.
+ boolean first = true;
+ final StringBuilder val = new StringBuilder();
+ for (Str child: children) {
+ if (first) {
+ first = false;
+ } else {
+ val.append(' ');
+ }
+ val.append(child);
+ }
+ mVariables.put(varName, new Value(VarType.UNKNOWN, new Str(val.toString())));
+ } else {
+ // Clear whatever flattenUnknownVars happened to have put in.
+ mVariables.remove(varName);
+ }
+ }
+
+ /**
+ * Throw an exception if there's an existing variable with a different type.
+ */
+ private void assertVarType(Str filename, String varName) {
+ if (mGenericConfig.getVarType(varName) == VarType.UNKNOWN) {
+ final Value prevValue = mVariables.get(varName);
+ if (prevValue != null
+ && prevValue.getVarType() != VarType.UNKNOWN) {
+ throw new RuntimeException("Mismatched var types:"
+ + " filename=" + filename
+ + " varType=" + mGenericConfig.getVarType(varName)
+ + " varName=" + varName
+ + " prevValue=" + Value.debugString(prevValue));
+ }
+ }
+ }
+
+ /**
+ * Depending on whether the assignment is prepending, appending, setting, etc.,
+ * update the value. We can infer which of those operations it is by the length
+ * and contents of the values. Each value in the list was originally separated
+ * by the previous value.
+ */
+ private void assignToListVar(Map<String, Value> vars, String varName, List<Str> items) {
+ final Value value = vars.get(varName);
+ final List<Str> orig = value == null ? new ArrayList() : value.getList();
+ final List<Str> result = new ArrayList();
+ if (items.size() > 0) {
+ for (int i = 0; i < items.size(); i++) {
+ if (i != 0) {
+ result.addAll(orig);
+ }
+ final Str item = items.get(i);
+ addWords(result, item);
+ }
+ }
+ vars.put(varName, new Value(result));
+ }
+
+ /**
+ * Appends all of the words in in 'items' to an entry in vars keyed by 'varName',
+ * creating one if necessary.
+ */
+ private static void appendToListVar(Map<String, Value> vars, String varName, List<Str> items) {
+ Value value = vars.get(varName);
+ if (value == null) {
+ value = new Value(new ArrayList());
+ vars.put(varName, value);
+ }
+ final List<Str> out = value.getList();
+ for (Str item: items) {
+ addWords(out, item);
+ }
+ }
+
+ /**
+ * Split 'item' on spaces, and add each of them as a word to 'out'.
+ */
+ private static void addWords(List<Str> out, Str item) {
+ for (String word: RE_SPACE.split(item.toString().trim())) {
+ if (word.length() > 0) {
+ out.add(new Str(item.getPosition(), word));
+ }
+ }
+ }
+
+ /**
+ * Flatten the list of strings in an Assign statement, using the previous value
+ * as a separator.
+ */
+ private Str flattenAssignList(GenericConfig.Assign assign, Str previous) {
+ final StringBuilder result = new StringBuilder();
+ Position position = previous.getPosition();
+ final List<Str> list = assign.getValue();
+ final int size = list.size();
+ for (int i = 0; i < size; i++) {
+ final Str item = list.get(i);
+ result.append(item.toString());
+ if (i != size - 1) {
+ result.append(previous);
+ }
+ final Position pos = item.getPosition();
+ if (pos != null && pos.getFile() != null) {
+ position = pos;
+ }
+ }
+ return new Str(position, result.toString());
+ }
+
+ /**
+ * Make sure that each of the product config variables has a default value.
+ */
+ private void setDefaultKnownVars() {
+ for (Map.Entry<String, VarType> entry: mGenericConfig.getProductVars().entrySet()) {
+ final String varName = entry.getKey();
+ final VarType varType = entry.getValue();
+
+ final Value val = mVariables.get(varName);
+ if (val == null) {
+ mVariables.put(varName, new Value(varType));
+ }
+ }
+
+
+ // TODO: These two for now as well, until we can rewrite the enforce packages exist
+ // handling.
+ if (!mVariables.containsKey("PRODUCT_ENFORCE_PACKAGES_EXIST")) {
+ mVariables.put("PRODUCT_ENFORCE_PACKAGES_EXIST", new Value(VarType.UNKNOWN));
+ }
+ if (!mVariables.containsKey("PRODUCT_ENFORCE_PACKAGES_EXIST_ALLOW_LIST")) {
+ mVariables.put("PRODUCT_ENFORCE_PACKAGES_EXIST_ALLOW_LIST", new Value(VarType.UNKNOWN));
+ }
+ }
+}
diff --git a/tools/product_config/src/com/android/build/config/Kati.java b/tools/product_config/src/com/android/build/config/Kati.java
index 026ddb5..4fa2297 100644
--- a/tools/product_config/src/com/android/build/config/Kati.java
+++ b/tools/product_config/src/com/android/build/config/Kati.java
@@ -16,11 +16,11 @@
package com.android.build.config;
-import java.util.List;
+import java.util.Map;
/**
* Wrapper for invoking kati.
*/
public interface Kati {
- public MakeConfig loadProductConfig();
+ public Map<String, MakeConfig> loadProductConfig();
}
diff --git a/tools/product_config/src/com/android/build/config/KatiImpl.java b/tools/product_config/src/com/android/build/config/KatiImpl.java
index feb374c..de11f36 100644
--- a/tools/product_config/src/com/android/build/config/KatiImpl.java
+++ b/tools/product_config/src/com/android/build/config/KatiImpl.java
@@ -56,17 +56,16 @@
}
@Override
- public MakeConfig loadProductConfig() {
+ public Map<String, MakeConfig> loadProductConfig() {
final String csvPath = getDumpConfigCsvPath();
try {
File workDir = new File(getWorkDirPath());
- if (!workDir.mkdirs()) {
+ if ((workDir.exists() && !workDir.isDirectory()) || !workDir.mkdirs()) {
mErrors.ERROR_KATI.add("Unable to create directory: " + workDir);
return null; // TODO: throw exception?
}
- System.out.println("running kati");
String out = mCommand.run(new String[] {
"-f", "build/make/core/dumpconfig.mk",
"DUMPCONFIG_FILE=" + csvPath
@@ -89,17 +88,14 @@
}
try (FileReader reader = new FileReader(csvPath)) {
- System.out.println("csvPath=" + csvPath);
- List<MakeConfig> makeConfigs = DumpConfigParser.parse(mErrors, csvPath, reader);
+ Map<String, MakeConfig> makeConfigs = DumpConfigParser.parse(mErrors, csvPath, reader);
if (makeConfigs.size() == 0) {
// TODO: Issue error?
return null;
}
- // TODO: There are multiple passes. That should be cleaned up in the make
- // build system, but for now, the first one is the one we want.
- return makeConfigs.get(0);
+ return makeConfigs;
} catch (CsvParser.ParseException ex) {
mErrors.ERROR_KATI.add(new Position(csvPath, ex.getLine()),
"Unable to parse output of dumpconfig.mk: " + ex.getMessage());
diff --git a/tools/product_config/src/com/android/build/config/Main.java b/tools/product_config/src/com/android/build/config/Main.java
index 7417fc7..5cec55e 100644
--- a/tools/product_config/src/com/android/build/config/Main.java
+++ b/tools/product_config/src/com/android/build/config/Main.java
@@ -18,6 +18,7 @@
import java.util.List;
import java.util.Map;
+import java.util.TreeMap;
import java.util.TreeSet;
public class Main {
@@ -30,30 +31,44 @@
}
void run() {
- System.out.println("Hello World");
-
// TODO: Check the build environment to make sure we're running in a real
// build environment, e.g. actually inside a source tree, with TARGET_PRODUCT
// and TARGET_BUILD_VARIANT defined, etc.
Kati kati = new KatiImpl(mErrors, mOptions);
- MakeConfig makeConfig = kati.loadProductConfig();
- if (makeConfig == null || mErrors.hadError()) {
+ Map<String, MakeConfig> makeConfigs = kati.loadProductConfig();
+ if (makeConfigs == null || mErrors.hadError()) {
return;
}
-
- System.out.println();
- System.out.println("====================");
- System.out.println("PRODUCT CONFIG FILES");
- System.out.println("====================");
- makeConfig.printToStream(System.out);
+ if (false) {
+ for (MakeConfig makeConfig: (new TreeMap<String, MakeConfig>(makeConfigs)).values()) {
+ System.out.println();
+ System.out.println("=======================================");
+ System.out.println("PRODUCT CONFIG FILES : " + makeConfig.getPhase());
+ System.out.println("=======================================");
+ makeConfig.printToStream(System.out);
+ }
+ }
ConvertMakeToGenericConfig m2g = new ConvertMakeToGenericConfig(mErrors);
- GenericConfig generic = m2g.convert(makeConfig);
+ GenericConfig generic = m2g.convert(makeConfigs);
+ if (false) {
+ System.out.println("======================");
+ System.out.println("REGENERATED MAKE FILES");
+ System.out.println("======================");
+ MakeWriter.write(System.out, generic, 0);
+ }
- System.out.println("======================");
- System.out.println("REGENERATED MAKE FILES");
- System.out.println("======================");
- MakeWriter.write(System.out, generic, 0);
+ // TODO: Lookup shortened name as used in PRODUCT_NAME / TARGET_PRODUCT
+ FlatConfig flat = FlattenConfig.flatten(mErrors, generic);
+ if (false) {
+ System.out.println("=======================");
+ System.out.println("FLATTENED VARIABLE LIST");
+ System.out.println("=======================");
+ MakeWriter.write(System.out, flat, 0);
+ }
+
+ OutputChecker checker = new OutputChecker(flat);
+ checker.reportErrors(mErrors);
// TODO: Run kati and extract the variables and convert all that into starlark files.
@@ -97,7 +112,10 @@
} finally {
// Print errors and warnings
errors.printErrors(System.err);
+ if (errors.hadError()) {
+ exitCode = 1;
+ }
+ System.exit(exitCode);
}
- System.exit(exitCode);
}
}
diff --git a/tools/product_config/src/com/android/build/config/MakeWriter.java b/tools/product_config/src/com/android/build/config/MakeWriter.java
index 58dfcc0..15fd095 100644
--- a/tools/product_config/src/com/android/build/config/MakeWriter.java
+++ b/tools/product_config/src/com/android/build/config/MakeWriter.java
@@ -30,15 +30,20 @@
private final boolean mWriteAnnotations;
public static void write(PrintStream out, GenericConfig config, int flags) {
- (new MakeWriter(flags)).write(out, config);
+ (new MakeWriter(flags)).writeGeneric(out, config);
}
+ public static void write(PrintStream out, FlatConfig config, int flags) {
+ (new MakeWriter(flags)).writeFlat(out, config);
+ }
+
+
private MakeWriter(int flags) {
mWriteHeader = (flags & FLAG_WRITE_HEADER) != 0;
mWriteAnnotations = (flags & FLAG_WRITE_ANNOTATIONS) != 0;
}
- private void write(PrintStream out, GenericConfig config) {
+ private void writeGeneric(PrintStream out, GenericConfig config) {
for (GenericConfig.ConfigFile file: config.getFiles().values()) {
out.println("---------------------------------------------------------");
out.println("FILE: " + file.getFilename());
@@ -49,7 +54,7 @@
out.println("---------------------------------------------------------");
out.println("VARIABLES TOUCHED BY MAKE BASED CONFIG:");
out.println("---------------------------------------------------------");
- writeStrVars(out, getModifiedVars(config.getInitialVariables(),
+ writeStrVars(out, OutputChecker.getModifiedVars(config.getInitialVariables(),
config.getFinalVariables()), config);
}
@@ -109,28 +114,6 @@
out.println();
}
- private static Map<String, Str> getModifiedVars(Map<String, Str> before,
- Map<String, Str> after) {
- final HashMap<String, Str> result = new HashMap();
- // Entries that were added or changed.
- for (Map.Entry<String, Str> afterEntry: after.entrySet()) {
- final String varName = afterEntry.getKey();
- final Str afterValue = afterEntry.getValue();
- final Str beforeValue = before.get(varName);
- if (beforeValue == null || !beforeValue.equals(afterValue)) {
- result.put(varName, afterValue);
- }
- }
- // removed Entries that were removed, we just treat them as
- for (Map.Entry<String, Str> beforeEntry: before.entrySet()) {
- final String varName = beforeEntry.getKey();
- if (!after.containsKey(varName)) {
- result.put(varName, new Str(""));
- }
- }
- return result;
- }
-
private static class Var {
Var(String name, Str val) {
this.name = name;
@@ -152,4 +135,27 @@
out.println(var.val.getPosition() + var.name + " := " + var.val);
}
}
+
+ private void writeFlat(PrintStream out, FlatConfig config) {
+ // TODO: Print positions.
+ for (Map.Entry<String, Value> entry: config.getValues().entrySet()) {
+ out.print(entry.getKey());
+ out.print(" := ");
+
+ final Value value = entry.getValue();
+ if (value.getVarType() == VarType.LIST) {
+ final List<Str> list = value.getList();
+ final int size = list.size();
+ for (int i = 0; i < size; i++) {
+ out.print(list.get(i).toString());
+ if (i != size - 1) {
+ out.print(" \\\n ");
+ }
+ }
+ } else {
+ out.print(value.getStr().toString());
+ }
+ out.println();
+ }
+ }
}
diff --git a/tools/product_config/src/com/android/build/config/Options.java b/tools/product_config/src/com/android/build/config/Options.java
index 4e60484..ed544dc 100644
--- a/tools/product_config/src/com/android/build/config/Options.java
+++ b/tools/product_config/src/com/android/build/config/Options.java
@@ -87,7 +87,7 @@
}
static class Parser {
- private class ParseException extends Exception {
+ private static class ParseException extends Exception {
public ParseException(String message) {
super(message);
}
diff --git a/tools/product_config/src/com/android/build/config/OutputChecker.java b/tools/product_config/src/com/android/build/config/OutputChecker.java
new file mode 100644
index 0000000..228f9f1
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/OutputChecker.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+package com.android.build.config;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Compares the make-based configuration as reported by dumpconfig.mk
+ * with what was computed from the new tool.
+ */
+public class OutputChecker {
+ // Differences that we know about, either know issues to be fixed or intentional.
+ private static final RegexSet IGNORED_VARIABLES = new RegexSet(
+ // TODO: Rewrite the enforce packages exist logic into this tool.
+ "PRODUCT_ENFORCE_PACKAGES_EXIST",
+ "PRODUCT_ENFORCE_PACKAGES_EXIST_ALLOW_LIST",
+ "PRODUCTS\\..*\\.PRODUCT_ENFORCE_PACKAGES_EXIST",
+ "PRODUCTS\\..*\\.PRODUCT_ENFORCE_PACKAGES_EXIST_ALLOW_LIST",
+
+ // This is generated by this tool, but comes later in the make build system.
+ "INTERNAL_PRODUCT");
+
+ private final FlatConfig mConfig;
+ private final TreeMap<String, Variable> mVariables;
+
+ /**
+ * Represents the before and after state of a variable.
+ */
+ public static class Variable {
+ public final String name;
+ public final VarType type;
+ public final Str original;
+ public final Value updated;
+
+ public Variable(String name, VarType type, Str original) {
+ this(name, type, original, null);
+ }
+
+ public Variable(String name, VarType type, Str original, Value updated) {
+ this.name = name;
+ this.type = type;
+ this.original = original;
+ this.updated = updated;
+ }
+
+ /**
+ * Return copy of this Variable with the updated field also set.
+ */
+ public Variable addUpdated(Value updated) {
+ return new Variable(name, type, original, updated);
+ }
+
+ /**
+ * Return whether normalizedOriginal and normalizedUpdate are equal.
+ */
+ public boolean isSame() {
+ final Str normalizedOriginal = Value.normalize(original);
+ final Str normalizedUpdated = Value.normalize(updated);
+ if (normalizedOriginal == null && normalizedUpdated == null) {
+ return true;
+ } else if (normalizedOriginal != null) {
+ return normalizedOriginal.equals(normalizedUpdated);
+ } else {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Construct OutputChecker with the config it will check.
+ */
+ public OutputChecker(FlatConfig config) {
+ mConfig = config;
+ mVariables = getVariables(config);
+ }
+
+ /**
+ * Add a WARNING_DIFFERENT_FROM_KATI for each of the variables which have changed.
+ */
+ public void reportErrors(Errors errors) {
+ for (Variable var: getDifferences()) {
+ if (IGNORED_VARIABLES.matches(var.name)) {
+ continue;
+ }
+ errors.WARNING_DIFFERENT_FROM_KATI.add("product_config processing differs from"
+ + " kati processing for " + var.type + " variable " + var.name + ".\n"
+ + "original: "
+ + Value.oneLinePerWord(var.original, "<null>") + "\n"
+ + "updated: "
+ + Value.oneLinePerWord(var.updated, "<null>"));
+ }
+ }
+
+ /**
+ * Get the Variables that are different between the normalized form of the original
+ * and updated. If one is null and the other is not, even if one is an empty string,
+ * the values are considered different.
+ */
+ public List<Variable> getDifferences() {
+ final ArrayList<Variable> result = new ArrayList();
+ for (Variable var: mVariables.values()) {
+ if (!var.isSame()) {
+ result.add(var);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Get all of the variables for this config.
+ *
+ * VisibleForTesting
+ */
+ static TreeMap<String, Variable> getVariables(FlatConfig config) {
+ final TreeMap<String, Variable> result = new TreeMap();
+
+ // Add the original values to mAll
+ for (Map.Entry<String, Str> entry: getModifiedVars(config.getInitialVariables(),
+ config.getFinalVariables()).entrySet()) {
+ final String name = entry.getKey();
+ result.put(name, new Variable(name, config.getVarType(name), entry.getValue()));
+ }
+
+ // Add the updated values to mAll
+ for (Map.Entry<String, Value> entry: config.getValues().entrySet()) {
+ final String name = entry.getKey();
+ final Value value = entry.getValue();
+ Variable var = result.get(name);
+ if (var == null) {
+ result.put(name, new Variable(name, config.getVarType(name), null, value));
+ } else {
+ result.put(name, var.addUpdated(value));
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Get the entries that are different in the two maps.
+ */
+ public static Map<String, Str> getModifiedVars(Map<String, Str> before,
+ Map<String, Str> after) {
+ final HashMap<String, Str> result = new HashMap();
+
+ // Entries that were added or changed.
+ for (Map.Entry<String, Str> afterEntry: after.entrySet()) {
+ final String varName = afterEntry.getKey();
+ final Str afterValue = afterEntry.getValue();
+ final Str beforeValue = before.get(varName);
+ if (beforeValue == null || !beforeValue.equals(afterValue)) {
+ result.put(varName, afterValue);
+ }
+ }
+
+ // removed Entries that were removed, we just treat them as empty string
+ for (Map.Entry<String, Str> beforeEntry: before.entrySet()) {
+ final String varName = beforeEntry.getKey();
+ if (!after.containsKey(varName)) {
+ result.put(varName, new Str(""));
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/tools/product_config/src/com/android/build/config/RegexSet.java b/tools/product_config/src/com/android/build/config/RegexSet.java
new file mode 100644
index 0000000..70fcd29
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/RegexSet.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+package com.android.build.config;
+
+import java.util.regex.Pattern;
+
+/**
+ * Returns whether a string matches one of a set of presupplied regexes.
+ */
+public class RegexSet {
+ private final Pattern[] mPatterns;
+
+ public RegexSet(String... patterns) {
+ mPatterns = new Pattern[patterns.length];
+ for (int i = 0; i < patterns.length; i++) {
+ mPatterns[i] = Pattern.compile(patterns[i]);
+ }
+ }
+
+ public boolean matches(String s) {
+ for (Pattern p: mPatterns) {
+ if (p.matcher(s).matches()) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
+
diff --git a/tools/product_config/src/com/android/build/config/Str.java b/tools/product_config/src/com/android/build/config/Str.java
index 9c345a6..2516b76 100644
--- a/tools/product_config/src/com/android/build/config/Str.java
+++ b/tools/product_config/src/com/android/build/config/Str.java
@@ -22,7 +22,7 @@
/**
* A String and a Position, where it came from in source code.
*/
-public class Str {
+public class Str implements Comparable<Str> {
private String mValue;
private Position mPosition;
@@ -36,6 +36,10 @@
mPosition = pos;
}
+ public int length() {
+ return mValue.length();
+ }
+
@Override
public String toString() {
return mValue;
@@ -51,16 +55,11 @@
*/
@Override
public boolean equals(Object o) {
- if (o == null) {
- return false;
- } else if (o instanceof String) {
- return mValue.equals(o);
- } else if (o instanceof Str) {
- final Str that = (Str)o;
- return mValue.equals(that.mValue);
- } else {
+ if (!(o instanceof Str)) {
return false;
}
+ final Str that = (Str)o;
+ return mValue.equals(that.mValue);
}
@Override
@@ -68,6 +67,11 @@
return mValue.hashCode();
}
+ @Override
+ public int compareTo(Str that) {
+ return this.mValue.compareTo(that.mValue);
+ }
+
public static ArrayList<Str> toList(Position pos, List<String> list) {
final ArrayList<Str> result = new ArrayList(list.size());
for (String s: list) {
diff --git a/tools/product_config/src/com/android/build/config/Value.java b/tools/product_config/src/com/android/build/config/Value.java
new file mode 100644
index 0000000..9bd6401
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/Value.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+package com.android.build.config;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+/**
+ * Class to hold the two types of variables we support, strings and lists of strings.
+ */
+public class Value {
+ private static final Pattern SPACES = Pattern.compile("\\s+");
+
+ private final VarType mVarType;
+ private final Str mStr;
+ private final ArrayList<Str> mList;
+
+ /**
+ * Construct an appropriately typed empty value.
+ */
+ public Value(VarType varType) {
+ mVarType = varType;
+ if (varType == VarType.LIST) {
+ mStr = null;
+ mList = new ArrayList();
+ mList.add(new Str(""));
+ } else {
+ mStr = new Str("");
+ mList = null;
+ }
+ }
+
+ public Value(VarType varType, Str str) {
+ mVarType = varType;
+ mStr = str;
+ mList = null;
+ }
+
+ public Value(List<Str> list) {
+ mVarType = VarType.LIST;
+ mStr = null;
+ mList = new ArrayList(list);
+ }
+
+ public VarType getVarType() {
+ return mVarType;
+ }
+
+ public Str getStr() {
+ return mStr;
+ }
+
+ public List<Str> getList() {
+ return mList;
+ }
+
+ /**
+ * Normalize a string that is behaving as a list.
+ */
+ public static String normalize(String str) {
+ if (str == null) {
+ return null;
+ }
+ return SPACES.matcher(str.trim()).replaceAll(" ").trim();
+ }
+
+ /**
+ * Normalize a string that is behaving as a list.
+ */
+ public static Str normalize(Str str) {
+ if (str == null) {
+ return null;
+ }
+ return new Str(str.getPosition(), normalize(str.toString()));
+ }
+
+ /**
+ * Normalize a this Value into the same format as normalize(Str).
+ */
+ public static Str normalize(Value val) {
+ if (val == null) {
+ return null;
+ }
+ if (val.mStr != null) {
+ return normalize(val.mStr);
+ }
+
+ if (val.mList.size() == 0) {
+ return new Str("");
+ }
+
+ StringBuilder result = new StringBuilder();
+ final int size = val.mList.size();
+ boolean first = true;
+ for (int i = 0; i < size; i++) {
+ String s = val.mList.get(i).toString().trim();
+ if (s.length() > 0) {
+ if (!first) {
+ result.append(" ");
+ } else {
+ first = false;
+ }
+ result.append(s);
+ }
+ }
+
+ // Just use the first item's position.
+ return new Str(val.mList.get(0).getPosition(), result.toString());
+ }
+
+ /**
+ * Put each word in 'str' on its own line in make format. If 'val' is null,
+ * 'nullValue' is returned.
+ */
+ public static String oneLinePerWord(Value val, String nullValue) {
+ if (val == null) {
+ return nullValue;
+ }
+ final String s = normalize(val).toString();
+ final Matcher m = SPACES.matcher(s);
+ final StringBuilder result = new StringBuilder();
+ if (s.length() > 0 && (val.mVarType == VarType.LIST || m.find())) {
+ result.append("\\\n ");
+ }
+ result.append(m.replaceAll(" \\\\\n "));
+ return result.toString();
+ }
+
+ /**
+ * Put each word in 'str' on its own line in make format. If 'str' is null,
+ * nullValue is returned.
+ */
+ public static String oneLinePerWord(Str str, String nullValue) {
+ if (str == null) {
+ return nullValue;
+ }
+ final Matcher m = SPACES.matcher(normalize(str.toString()));
+ final StringBuilder result = new StringBuilder();
+ if (m.find()) {
+ result.append("\\\n ");
+ }
+ result.append(m.replaceAll(" \\\\\n "));
+ return result.toString();
+ }
+
+ /**
+ * Return a string representing this value with detailed debugging information.
+ */
+ public static String debugString(Value val) {
+ if (val == null) {
+ return "null";
+ }
+
+ final StringBuilder str = new StringBuilder("Value(");
+ if (val.mStr != null) {
+ str.append("mStr=");
+ str.append("\"");
+ str.append(val.mStr.toString());
+ str.append("\"");
+ if (false) {
+ str.append(" (");
+ str.append(val.mStr.getPosition().toString());
+ str.append(")");
+ }
+ }
+ if (val.mList != null) {
+ str.append("mList=");
+ str.append("[");
+ for (Str s: val.mList) {
+ str.append(" \"");
+ str.append(s.toString());
+ if (false) {
+ str.append("\" (");
+ str.append(s.getPosition().toString());
+ str.append(")");
+ } else {
+ str.append("\"");
+ }
+ }
+ str.append(" ]");
+ }
+ str.append(")");
+ return str.toString();
+ }
+
+ /**
+ * Get the Positions of all of the parts of this Value.
+ */
+ public List<Position> getPositions() {
+ List<Position> result = new ArrayList();
+ if (mStr != null) {
+ result.add(mStr.getPosition());
+ }
+ if (mList != null) {
+ for (Str str: mList) {
+ result.add(str.getPosition());
+ }
+ }
+ return result;
+ }
+}
+
diff --git a/tools/product_config/test.sh b/tools/product_config/test.sh
new file mode 100755
index 0000000..a910df8
--- /dev/null
+++ b/tools/product_config/test.sh
@@ -0,0 +1,90 @@
+#!/bin/bash
+
+#
+# This script runs the full set of tests for product config:
+# 1. Build the product-config tool.
+# 2. Run the unit tests.
+# 3. Run the product config for every product available in the current
+# source tree, for each of user, userdebug and eng.
+# - To restrict which products or variants are run, set the
+# PRODUCTS or VARIANTS environment variables.
+# - Products for which the make based product config fails are
+# skipped.
+#
+
+# The PRODUCTS variable is used by the build, and setting it in the environment
+# interferes with that, so unset it. (That should probably be fixed)
+products=$PRODUCTS
+variants=$VARIANTS
+unset PRODUCTS
+unset VARIANTS
+
+# Don't use lunch from the user's shell
+unset TARGET_PRODUCT
+unset TARGET_BUILD_VARIANT
+
+function die() {
+ format=$1
+ shift
+ printf "$format\nStopping...\n" $@ >&2
+ exit 1;
+}
+
+[[ -f build/make/envsetup.sh ]] || die "Run this script from the root of the tree."
+: ${products:=$(build/soong/soong_ui.bash --dumpvar-mode all_named_products | sed -e "s/ /\n/g" | sort -u )}
+: ${variants:="user userdebug eng"}
+: ${CKATI_BIN:=prebuilts/build-tools/$(build/soong/soong_ui.bash --dumpvar-mode HOST_PREBUILT_TAG)/bin/ckati}
+
+function if_signal_exit() {
+ [[ $1 -lt 128 ]] || exit $1
+}
+
+build/soong/soong_ui.bash --build-mode --all-modules --dir="$(pwd)" product-config-test product-config \
+ || die "Build failed."
+
+echo
+echo Running unit tests
+java -jar out/host/linux-x86/testcases/product-config-test/product-config-test.jar
+unit_tests=$?
+if_signal_exit $unit_tests
+
+failed_baseline_checks=
+for product in $products ; do
+ for variant in $variants ; do
+ echo
+ echo Checking to see if $product-$variant works with make
+ TARGET_PRODUCT=$product TARGET_BUILD_VARIANT=$variant build/soong/soong_ui.bash --dumpvar-mode TARGET_PRODUCT &> /dev/null
+ exit_status=$?
+ if_signal_exit $exit_status
+ if [ $exit_status -ne 0 ] ; then
+ echo Combo fails with make, skipping product-config test run for $product-$variant
+ else
+ echo Running product-config for $product-$variant
+ rm -rf out/config/$product-$variant
+ TARGET_PRODUCT=$product TARGET_BUILD_VARIANT=$variant product-config \
+ --ckati_bin $CKATI_BIN \
+ --error 1000
+ exit_status=$?
+ if_signal_exit $exit_status
+ if [ $exit_status -ne 0 ] ; then
+ failed_baseline_checks="$failed_baseline_checks $product-$variant"
+ fi
+ fi
+ done
+done
+
+echo
+echo
+echo "------------------------------"
+echo SUMMARY
+echo "------------------------------"
+
+echo -n "Unit tests "
+if [ $unit_tests -eq 0 ] ; then echo PASSED ; else echo FAILED ; fi
+
+echo -n "Baseline checks "
+if [ "$failed_baseline_checks" = "" ] ; then echo PASSED ; else echo FAILED ; fi
+for combo in $failed_baseline_checks ; do
+ echo " ... $combo"
+done
+