diff --git a/core/dumpconfig.mk b/core/dumpconfig.mk new file mode 100644 index 0000000000..dd3ef43429 --- /dev/null +++ b/core/dumpconfig.mk @@ -0,0 +1,129 @@ +# Read and dump the product configuration. + +# Called from the product-config tool, not from the main build system. + +# +# Ensure we are being called correctly +# +ifndef KATI + $(warning Kati must be used to call dumpconfig.mk, not make.) + $(error stopping) +endif + +ifdef DEFAULT_GOAL + $(warning Calling dumpconfig.mk from inside the make build system is not) + $(warning supported. It is only meant to be called via kati by product-confing.) + $(error stopping) +endif + +ifndef TARGET_PRODUCT + $(warning dumpconfig.mk requires TARGET_PRODUCT to be set) + $(error stopping) +endif + +ifndef TARGET_BUILD_VARIANT + $(warning dumpconfig.mk requires TARGET_BUILD_VARIANT to be set) + $(error stopping) +endif + +ifneq (build/make/core/config.mk,$(wildcard build/make/core/config.mk)) + $(warning dumpconfig must be called from the root of the source tree) + $(error stopping) +endif + +ifeq (,$(DUMPCONFIG_FILE)) + $(warning dumpconfig requires DUMPCONFIG_FILE to be set) + $(error stopping) +endif + +# Before we do anything else output the format version. +$(file > $(DUMPCONFIG_FILE),dumpconfig_version,1) +$(file >> $(DUMPCONFIG_FILE),dumpconfig_file,$(DUMPCONFIG_FILE)) + +# Default goal for dumpconfig +dumpconfig: + $(file >> $(DUMPCONFIG_FILE),***DONE***) + @echo ***DONE*** + +# TODO(Remove): These need to be set externally +OUT_DIR := out +TMPDIR = /tmp/build-temp +BUILD_DATETIME_FILE := $(OUT_DIR)/build_date.txt + +# Escape quotation marks for CSV, and wraps in quotation marks. +define escape-for-csv +"$(subst ","",$1)" +endef + +# Args: +# $(1): include stack +define dump-import-start +$(eval $(file >> $(DUMPCONFIG_FILE),import,$(strip $(1)))) +endef + +# Args: +# $(1): include stack +define dump-import-done +$(eval $(file >> $(DUMPCONFIG_FILE),imported,$(strip $(1)))) +endef + +# Args: +# $(1): Current file +# $(2): Inherited file +define dump-inherit +$(eval $(file >> $(DUMPCONFIG_FILE),inherit,$(strip $(1)),$(strip $(2)))) +endef + +# Args: +# $(1): Config phase (PRODUCT or DEVICE) +# $(2): Root nodes to import +# $(3): All variable names +# $(4): Single-value variables +# $(5): Makefile being processed +define dump-phase-start +$(eval $(file >> $(DUMPCONFIG_FILE),phase,$(strip $(1)),$(strip $(2)))) \ +$(foreach var,$(3), \ + $(eval $(file >> $(DUMPCONFIG_FILE),var,$(if $(filter $(4),$(var)),single,list),$(var))) \ +) \ +$(call dump-config-vals,$(strip $(5)),initial) +endef + +# Args: +# $(1): Makefile being processed +define dump-phase-end +$(call dump-config-vals,$(strip $(1)),final) +endef + +define dump-debug +$(eval $(file >> $(DUMPCONFIG_FILE),debug,$(1))) +endef + +# Skip these when dumping. They're not used and they cause a lot of noise in the dump. +DUMPCONFIG_SKIP_VARS := \ + .VARIABLES \ + .KATI_SYMBOLS \ + 1 \ + 2 \ + LOCAL_PATH \ + MAKEFILE_LIST \ + PARENT_PRODUCT_FILES \ + current_mk \ + inherit_var \ + np \ + _node_import_context \ + _included \ + _include_stack \ + _in \ + _nic.% + +# Args: +# $(1): Makefile that was included +# $(2): block (before,import,after,initial,final) +define dump-config-vals +$(foreach var,$(filter-out $(DUMPCONFIG_SKIP_VARS),$(.KATI_SYMBOLS)),\ + $(eval $(file >> $(DUMPCONFIG_FILE),val,$(call escape-for-csv,$(1)),$(2),$(call escape-for-csv,$(var)),$(call escape-for-csv,$($(var))),$(call escape-for-csv,$(KATI_variable_location $(var))))) \ +) +endef + +include build/make/core/config.mk + diff --git a/core/node_fns.mk b/core/node_fns.mk index b81d60c0f5..8d2016011e 100644 --- a/core/node_fns.mk +++ b/core/node_fns.mk @@ -195,7 +195,11 @@ define _import-node $(call clear-var-list, $(3)) $(eval LOCAL_PATH := $(patsubst %/,%,$(dir $(2)))) $(eval MAKEFILE_LIST :=) + $(call dump-import-start,$(_include_stack)) + $(call dump-config-vals,$(2),before) $(eval include $(2)) + $(call dump-import-done,$(_include_stack)) + $(call dump-config-vals,$(2),after) $(eval _included := $(filter-out $(2),$(MAKEFILE_LIST))) $(eval MAKEFILE_LIST :=) $(eval LOCAL_PATH :=) @@ -250,6 +254,7 @@ endef # of the default list semantics # define import-nodes +$(call dump-phase-start,$(1),$(2),$(3),$(4),build/make/core/node_fns.mk) \ $(if \ $(foreach _in,$(2), \ $(eval _node_import_context := _nic.$(1).[[$(_in)]]) \ @@ -263,5 +268,6 @@ $(if \ $(if $(_include_stack),$(eval $(error ASSERTION FAILED: _include_stack \ should be empty here: $(_include_stack))),) \ ) \ -,) +,) \ +$(call dump-phase-end,build/make/core/node_fns.mk) endef diff --git a/core/product.mk b/core/product.mk index ce9bacff04..58902025bf 100644 --- a/core/product.mk +++ b/core/product.mk @@ -460,7 +460,9 @@ define inherit-product $(eval current_mk := $(strip $(word 1,$(_include_stack)))) \ $(eval inherit_var := PRODUCTS.$(current_mk).INHERITS_FROM) \ $(eval $(inherit_var) := $(sort $($(inherit_var)) $(np))) \ - $(eval PARENT_PRODUCT_FILES := $(sort $(PARENT_PRODUCT_FILES) $(current_mk))) + $(eval PARENT_PRODUCT_FILES := $(sort $(PARENT_PRODUCT_FILES) $(current_mk))) \ + $(call dump-inherit,$(strip $(word 1,$(_include_stack))),$(1)) \ + $(call dump-config-vals,$(current_mk),inherit) endef # Specifies a number of path prefixes, relative to PRODUCT_OUT, where the diff --git a/tools/product_config/src/com/android/build/config/ConfigBase.java b/tools/product_config/src/com/android/build/config/ConfigBase.java new file mode 100644 index 0000000000..eb21219f10 --- /dev/null +++ b/tools/product_config/src/com/android/build/config/ConfigBase.java @@ -0,0 +1,121 @@ +/* + * 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.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * Common parts between MakeConfig and the to-be-added GenericConfig, BazelConfig and SoongConfig. + */ +public class ConfigBase { + protected String mPhase; + protected List mRootNodes; + + /** + * State of the make varaible environment from before the first config file. + */ + protected Map mInitialVariables = new HashMap(); + + /** + * State of the make varaible environment from after the first config file. + */ + protected Map mFinalVariables = new HashMap(); + + + /** + * The variables that are handled specially. + */ + protected final TreeMap mProductVars = new TreeMap(); + + /** + * Whether a product config variable is a list or single-value variable. + */ + public enum VarType { + LIST, + SINGLE, + UNKNOWN // For non-product vars + } + + public void setPhase(String phase) { + mPhase = phase; + } + + public String getPhase() { + return mPhase; + } + + public void setRootNodes(List filenames) { + mRootNodes = new ArrayList(filenames); + } + + public List getRootNodes() { + return mRootNodes; + } + + public void addProductVar(String name, VarType type) { + mProductVars.put(name, type); + } + + public TreeMap getProductVars() { + return mProductVars; + } + + public VarType getVarType(String name) { + final VarType t = mProductVars.get(name); + if (t != null) { + return t; + } else { + return VarType.UNKNOWN; + } + } + + public boolean isProductVar(String name) { + return mProductVars.get(name) != null; + } + + /** + * Return the state the make variable environment from before the first config file. + */ + public Map getInitialVariables() { + return mInitialVariables; + } + + /** + * Return the state the make variable environment from before the first config file. + */ + public Map getFinalVariables() { + return mFinalVariables; + } + + /** + * Copy common base class fields from that to this. + */ + public void copyFrom(ConfigBase that) { + setPhase(that.getPhase()); + setRootNodes(that.getRootNodes()); + for (Map.Entry entry: that.getProductVars().entrySet()) { + addProductVar(entry.getKey(), entry.getValue()); + } + mInitialVariables = new HashMap(that.getInitialVariables()); + mFinalVariables = new HashMap(that.getFinalVariables()); + } +} diff --git a/tools/product_config/src/com/android/build/config/ConvertMakeToGenericConfig.java b/tools/product_config/src/com/android/build/config/ConvertMakeToGenericConfig.java new file mode 100644 index 0000000000..369d4d62cf --- /dev/null +++ b/tools/product_config/src/com/android/build/config/ConvertMakeToGenericConfig.java @@ -0,0 +1,206 @@ +/* + * 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.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Converts a MakeConfig into a Generic config by applying heuristics about + * the types of variable assignments that we do. + */ +public class ConvertMakeToGenericConfig { + private final Errors mErrors; + + public ConvertMakeToGenericConfig(Errors errors) { + mErrors = errors; + } + + public GenericConfig convert(MakeConfig make) { + final GenericConfig result = new GenericConfig(); + + // Base class fields + result.copyFrom(make); + + // Each file + for (MakeConfig.ConfigFile f: make.getConfigFiles()) { + final GenericConfig.ConfigFile genericFile + = new GenericConfig.ConfigFile(f.getFilename()); + result.addConfigFile(genericFile); + + final List blocks = f.getBlocks(); + + // Some assertions: + // TODO: Include better context for these errors. + // There should always be at least a BEGIN and an AFTER, so assert this. + if (blocks.size() < 2) { + throw new RuntimeException("expected at least blocks.size() >= 2. Actcual size: " + + blocks.size()); + } + if (blocks.get(0).getBlockType() != MakeConfig.BlockType.BEFORE) { + throw new RuntimeException("expected first block to be BEFORE"); + } + if (blocks.get(blocks.size() - 1).getBlockType() != MakeConfig.BlockType.AFTER) { + throw new RuntimeException("expected first block to be AFTER"); + } + // Everything in between should be an INHERIT block. + for (int index = 1; index < blocks.size() - 1; index++) { + if (blocks.get(index).getBlockType() != MakeConfig.BlockType.INHERIT) { + throw new RuntimeException("expected INHERIT at block " + index); + } + } + + // Each block represents a snapshot of the interpreter variable state (minus a few big + // sets of variables which we don't export because they're used in the internals + // of node_fns.mk, so we know they're not necessary here). The first (BEFORE) one + // is everything that is set before the file is included, so it forms the base + // for everything else. + MakeConfig.Block prevBlock = blocks.get(0); + + for (int index = 1; index < blocks.size(); index++) { + final MakeConfig.Block block = blocks.get(index); + for (final Map.Entry entry: block.getVars().entrySet()) { + final String varName = entry.getKey(); + final GenericConfig.Assign assign = convertAssignment(block.getBlockType(), + block.getInheritedFile(), make.getVarType(varName), varName, + entry.getValue(), prevBlock.getVar(varName)); + if (assign != null) { + genericFile.addStatement(assign); + } + } + // Handle variables that are in prevBlock but not block -- they were + // deleted. Is this even possible, or do they show up as ""? We will + // treat them as positive assigments to empty string + for (String prevName: prevBlock.getVars().keySet()) { + if (!block.getVars().containsKey(prevName)) { + genericFile.addStatement( + new GenericConfig.Assign(prevName, new Str(""))); + } + } + if (block.getBlockType() == MakeConfig.BlockType.INHERIT) { + genericFile.addStatement( + new GenericConfig.Inherit(block.getInheritedFile())); + } + // For next iteration + prevBlock = block; + } + } + return result; + } + + /** + * Converts one variable from a MakeConfig Block into a GenericConfig Assignment. + */ + GenericConfig.Assign convertAssignment(MakeConfig.BlockType blockType, Str inheritedFile, + ConfigBase.VarType varType, String varName, Str varVal, Str prevVal) { + if (prevVal == null) { + // New variable. + return new GenericConfig.Assign(varName, varVal); + } else if (!varVal.equals(prevVal)) { + // The value changed from the last block. + if (varVal.equals("")) { + // It was set to empty + return new GenericConfig.Assign(varName, varVal); + } else { + // Product vars have the @inherit processing. Other vars we + // will just ignore and put in one section at the end, based + // on the difference between the BEFORE and AFTER blocks. + if (varType == ConfigBase.VarType.UNKNOWN) { + if (blockType == MakeConfig.BlockType.AFTER) { + // For UNKNOWN variables, we don't worry about the + // intermediate steps, just take the final value. + return new GenericConfig.Assign(varName, varVal); + } else { + return null; + } + } else { + return convertInheritedVar(blockType, inheritedFile, + varName, varVal, prevVal); + } + } + } else { + // Variable not touched + return null; + } + } + + /** + * Handle the special inherited values, where the inherit-product puts in the + * @inherit:... markers, adding Statements to the ConfigFile. + */ + GenericConfig.Assign convertInheritedVar(MakeConfig.BlockType blockType, Str inheritedFile, + String varName, Str varVal, Str prevVal) { + String varText = varVal.toString(); + String prevText = prevVal.toString().trim(); + if (blockType == MakeConfig.BlockType.INHERIT) { + // inherit-product appends @inherit:... so drop that. + final String marker = "@inherit:" + inheritedFile; + if (varText.endsWith(marker)) { + varText = varText.substring(0, varText.length() - marker.length()).trim(); + } else { + mErrors.ERROR_IMPROPER_PRODUCT_VAR_MARKER.add(varVal.getPosition(), + "Variable didn't end with marker \"" + marker + "\": " + varText); + } + } + + if (!varText.equals(prevText)) { + // If the variable value was actually changed. + final ArrayList words = split(varText, prevText); + if (words.size() == 0) { + // Pure Assignment, none of the previous value is present. + return new GenericConfig.Assign(varName, new Str(varVal.getPosition(), varText)); + } else { + // Self referential value (prepend, append, both). + if (words.size() > 2) { + // This is indicative of a construction that might not be quite + // what we want. The above code will do something that works if it was + // of the form "VAR := a $(VAR) b $(VAR) c", but if the original code + // something else this won't work. This doesn't happen in AOSP, but + // it's a theoretically possibility, so someone might do it. + mErrors.WARNING_VARIABLE_RECURSION.add(varVal.getPosition(), + "Possible unsupported variable recursion: " + + varName + " = " + varVal + " (prev=" + prevVal + ")"); + } + return new GenericConfig.Assign(varName, Str.toList(varVal.getPosition(), words)); + } + } else { + // Variable not touched + return null; + } + } + + /** + * Split 'haystack' on occurrences of 'needle'. Trims each string of whitespace + * to preserve make list semantics. + */ + private static ArrayList split(String haystack, String needle) { + final ArrayList result = new ArrayList(); + final int needleLen = needle.length(); + if (needleLen == 0) { + return result; + } + int start = 0; + int end; + while ((end = haystack.indexOf(needle, start)) >= 0) { + result.add(haystack.substring(start, end).trim()); + start = end + needleLen; + } + result.add(haystack.substring(start).trim()); + return result; + } +} diff --git a/tools/product_config/src/com/android/build/config/CsvParser.java b/tools/product_config/src/com/android/build/config/CsvParser.java new file mode 100644 index 0000000000..1c8b9c37df --- /dev/null +++ b/tools/product_config/src/com/android/build/config/CsvParser.java @@ -0,0 +1,242 @@ + +/* + * 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.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * A CSV parser. + */ +public class CsvParser { + /** + * Internal string buffer grows by this amount. + */ + private static final int CHUNK_SIZE = 64 * 1024; + + /** + * Error parsing. + */ + public static class ParseException extends Exception { + private int mLine; + private int mColumn; + + public ParseException(int line, int column, String message) { + super(message); + mLine = line; + mColumn = column; + } + + /** + * Line number in source file. + */ + public int getLine() { + return mLine; + } + + /** + * Column in source file. + */ + public int getColumn() { + return mColumn; + } + } + + public static class Line { + private final int mLineNumber; + private final List mFields; + + Line(int lineno, List fields) { + mLineNumber = lineno; + mFields = fields; + } + + public int getLine() { + return mLineNumber; + } + + public List getFields() { + return mFields; + } + } + + // Parser States + private static final int STATE_START_LINE = 0; + private static final int STATE_START_FIELD = 1; + private static final int STATE_INSIDE_QUOTED_FIELD = 2; + private static final int STATE_FIRST_QUOTATION_MARK = 3; + private static final int STATE_INSIDE_UNQUOTED_FIELD = 4; + private static final int STATE_DONE = 5; + + // Parser Actions + private static final int ACTION_APPEND_CHAR = 1; + private static final int ACTION_FIELD_COMPLETE = 2; + private static final int ACTION_LINE_COMPLETE = 4; + + /** + * Constructor. + */ + private CsvParser() { + } + + /** + * Reads CSV and returns a list of Line objects. + * + * Handles newlines inside fields quoted with double quotes ("). + * + * Doesn't report blank lines, but does include empty fields. + */ + public static List parse(Reader reader) + throws ParseException, IOException { + ArrayList result = new ArrayList(); + int line = 1; + int column = 1; + int pos = 0; + char[] buf = new char[CHUNK_SIZE]; + HashMap stringPool = new HashMap(); + ArrayList fields = new ArrayList(); + + int state = STATE_START_LINE; + while (state != STATE_DONE) { + int c = reader.read(); + int action = 0; + + if (state == STATE_START_LINE) { + if (c <= 0) { + // No data, skip ACTION_LINE_COMPLETE. + state = STATE_DONE; + } else if (c == '"') { + state = STATE_INSIDE_QUOTED_FIELD; + } else if (c == ',') { + action = ACTION_FIELD_COMPLETE; + state = STATE_START_FIELD; + } else if (c == '\n') { + // Consume the newline, state stays STATE_START_LINE. + } else { + action = ACTION_APPEND_CHAR; + state = STATE_INSIDE_UNQUOTED_FIELD; + } + } else if (state == STATE_START_FIELD) { + if (c <= 0) { + // Field will be empty + action = ACTION_FIELD_COMPLETE | ACTION_LINE_COMPLETE; + state = STATE_DONE; + } else if (c == '"') { + state = STATE_INSIDE_QUOTED_FIELD; + } else if (c == ',') { + action = ACTION_FIELD_COMPLETE; + state = STATE_START_FIELD; + } else if (c == '\n') { + action = ACTION_FIELD_COMPLETE | ACTION_LINE_COMPLETE; + state = STATE_START_LINE; + } else { + action = ACTION_APPEND_CHAR; + state = STATE_INSIDE_UNQUOTED_FIELD; + } + } else if (state == STATE_INSIDE_QUOTED_FIELD) { + if (c <= 0) { + throw new ParseException(line, column, + "Bad input: End of input inside quoted field."); + } else if (c == '"') { + state = STATE_FIRST_QUOTATION_MARK; + } else { + action = ACTION_APPEND_CHAR; + } + } else if (state == STATE_FIRST_QUOTATION_MARK) { + if (c <= 0) { + action = ACTION_FIELD_COMPLETE | ACTION_LINE_COMPLETE; + state = STATE_DONE; + } else if (c == '"') { + action = ACTION_APPEND_CHAR; + state = STATE_INSIDE_QUOTED_FIELD; + } else if (c == ',') { + action = ACTION_FIELD_COMPLETE; + state = STATE_START_FIELD; + } else if (c == '\n') { + action = ACTION_FIELD_COMPLETE | ACTION_LINE_COMPLETE; + state = STATE_START_LINE; + } else { + throw new ParseException(line, column, + "Bad input: Character after field ended or unquoted '\"'."); + } + } else if (state == STATE_INSIDE_UNQUOTED_FIELD) { + if (c <= 0) { + action = ACTION_FIELD_COMPLETE | ACTION_LINE_COMPLETE; + state = STATE_DONE; + } else if (c == ',') { + action = ACTION_FIELD_COMPLETE; + state = STATE_START_FIELD; + } else if (c == '\n') { + action = ACTION_FIELD_COMPLETE | ACTION_LINE_COMPLETE; + state = STATE_START_LINE; + } else { + action = ACTION_APPEND_CHAR; + } + } + + if ((action & ACTION_APPEND_CHAR) != 0) { + // Reallocate buffer if necessary. Hopefully not often because CHUNK_SIZE is big. + if (pos >= buf.length) { + char[] old = buf; + buf = new char[old.length + CHUNK_SIZE]; + System.arraycopy(old, 0, buf, 0, old.length); + } + // Store the character + buf[pos] = (char)c; + pos++; + } + if ((action & ACTION_FIELD_COMPLETE) != 0) { + // A lot of the strings are duplicated, so pool them to reduce peak memory + // usage. This could be made slightly better by having a custom key class + // that does the lookup without making a new String that gets immediately + // thrown away. + String field = new String(buf, 0, pos); + final String cached = stringPool.get(field); + if (cached == null) { + stringPool.put(field, field); + } else { + field = cached; + } + fields.add(field); + pos = 0; + } + if ((action & ACTION_LINE_COMPLETE) != 0) { + // Only report lines with any contents + if (fields.size() > 0) { + result.add(new Line(line, fields)); + fields = new ArrayList(); + } + } + + if (c == '\n') { + line++; + column = 1; + } else { + column++; + } + } + + return result; + } +} diff --git a/tools/product_config/src/com/android/build/config/DumpConfigParser.java b/tools/product_config/src/com/android/build/config/DumpConfigParser.java new file mode 100644 index 0000000000..06663ca3ad --- /dev/null +++ b/tools/product_config/src/com/android/build/config/DumpConfigParser.java @@ -0,0 +1,314 @@ +/* + * 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.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Parses the output of ckati building build/make/core/dumpconfig.mk. + * + * The format is as follows: + * - All processed lines are colon (':') separated fields. + * - Lines before the dumpconfig_version line are dropped for forward compatibility + * - Lines where the first field is config_var describe variables declared in makefiles + * (implemented by the dump-config-vals macro) + * Field Description + * 0 "config_var" row type + * 1 Product makefile being processed + * 2 The variable name + * 3 The value of the variable + * 4 The location of the variable, as best tracked by kati + */ +public class DumpConfigParser { + private static final boolean DEBUG = true; + + private final Errors mErrors; + private final String mFilename; + private final Reader mReader; + + private final ArrayList mResults = new ArrayList(); + + private static final Pattern LIST_SEPARATOR = Pattern.compile("\\s+"); + + /** + * Constructor. + */ + private DumpConfigParser(Errors errors, String filename, Reader reader) { + mErrors = errors; + mFilename = filename; + mReader = reader; + } + + /** + * Parse the text into a list of MakeConfig objects. + */ + public static ArrayList parse(Errors errors, String filename, Reader reader) + throws CsvParser.ParseException, IOException { + DumpConfigParser parser = new DumpConfigParser(errors, filename, reader); + parser.parseImpl(); + return parser.mResults; + } + + /** + * Parse the input. + */ + private void parseImpl() throws CsvParser.ParseException, IOException { + final List lines = CsvParser.parse(mReader); + final int lineCount = lines.size(); + int index = 0; + + int dumpconfigVersion = 0; + + // Ignore lines until until we get a dumpconfig_version line for forward compatibility. + // In a previous life, this loop parsed from all of kati's stdout, not just the file + // that dumpconfig.mk writes, but it's harmless to leave this loop in. It gives us a + // little bit of flexibility which we probably won't need anyway, this tool probably + // won't diverge from dumpconfig.mk anyway. + for (; index < lineCount; index++) { + final CsvParser.Line line = lines.get(index); + final List fields = line.getFields(); + + if (matchLineType(line, "dumpconfig_version", 1)) { + try { + dumpconfigVersion = Integer.parseInt(fields.get(1)); + } catch (NumberFormatException ex) { + mErrors.WARNING_DUMPCONFIG.add( + new Position(mFilename, line.getLine()), + "Couldn't parse dumpconfig_version: " + fields.get(1)); + } + break; + } + } + + // If we never saw dumpconfig_version, there's a problem with the command, so stop. + if (dumpconfigVersion == 0) { + mErrors.ERROR_DUMPCONFIG.fatal( + new Position(mFilename), + "Never saw a valid dumpconfig_version line."); + } + + // Any lines before the start signal will be dropped. We create garbage objects + // here to avoid having to check for null everywhere. + MakeConfig makeConfig = new MakeConfig(); + MakeConfig.ConfigFile configFile = new MakeConfig.ConfigFile(""); + MakeConfig.Block block = new MakeConfig.Block(MakeConfig.BlockType.UNSET); + Map initialVariables = new HashMap(); + Map finalVariables = new HashMap(); + + // Number of "phases" we've seen so far. + for (; index < lineCount; index++) { + final CsvParser.Line line = lines.get(index); + final List fields = line.getFields(); + final String lineType = fields.get(0); + + if (matchLineType(line, "phase", 2)) { + // Start the new one + makeConfig = new MakeConfig(); + makeConfig.setPhase(fields.get(1)); + makeConfig.setRootNodes(splitList(fields.get(2))); + mResults.add(makeConfig); + initialVariables = makeConfig.getInitialVariables(); + finalVariables = makeConfig.getFinalVariables(); + + if (DEBUG) { + System.out.println("PHASE:"); + System.out.println(" " + makeConfig.getPhase()); + System.out.println(" " + makeConfig.getRootNodes()); + } + } else if (matchLineType(line, "var", 2)) { + final MakeConfig.VarType type = "list".equals(fields.get(1)) + ? MakeConfig.VarType.LIST : MakeConfig.VarType.SINGLE; + makeConfig.addProductVar(fields.get(2), type); + + if (DEBUG) { + System.out.println(" VAR: " + type + " " + fields.get(2)); + } + } else if (matchLineType(line, "import", 1)) { + final List importStack = splitList(fields.get(1)); + if (importStack.size() == 0) { + mErrors.WARNING_DUMPCONFIG.add( + new Position(mFilename, line.getLine()), + "'import' line with empty include stack."); + continue; + } + + // The beginning of importing a new file. + configFile = new MakeConfig.ConfigFile(importStack.get(0)); + if (makeConfig.addConfigFile(configFile) != null) { + mErrors.WARNING_DUMPCONFIG.add( + new Position(mFilename, line.getLine()), + "Duplicate file imported in section: " + configFile.getFilename()); + } + // We expect a Variable block next. + block = new MakeConfig.Block(MakeConfig.BlockType.BEFORE); + configFile.addBlock(block); + + if (DEBUG) { + System.out.println(" IMPORT: " + configFile.getFilename()); + } + } else if (matchLineType(line, "inherit", 2)) { + final String currentFile = fields.get(1); + final String inheritedFile = fields.get(2); + if (!configFile.getFilename().equals(currentFile)) { + mErrors.WARNING_DUMPCONFIG.add( + new Position(mFilename, line.getLine()), + "Unexpected current file in 'inherit' line '" + currentFile + + "' while processing '" + configFile.getFilename() + "'"); + continue; + } + + // There is already a file in progress, so add another var block to that. + block = new MakeConfig.Block(MakeConfig.BlockType.INHERIT); + // TODO: Make dumpconfig.mk also output a Position for inherit-product + block.setInheritedFile(new Str(inheritedFile)); + configFile.addBlock(block); + + if (DEBUG) { + System.out.println(" INHERIT: " + inheritedFile); + } + } else if (matchLineType(line, "imported", 1)) { + final List importStack = splitList(fields.get(1)); + if (importStack.size() == 0) { + mErrors.WARNING_DUMPCONFIG.add( + new Position(mFilename, line.getLine()), + "'imported' line with empty include stack."); + continue; + } + final String currentFile = importStack.get(0); + if (!configFile.getFilename().equals(currentFile)) { + mErrors.WARNING_DUMPCONFIG.add( + new Position(mFilename, line.getLine()), + "Unexpected current file in 'imported' line '" + currentFile + + "' while processing '" + configFile.getFilename() + "'"); + continue; + } + + // There is already a file in progress, so add another var block to that. + // This will be the last one, but will check that after parsing. + block = new MakeConfig.Block(MakeConfig.BlockType.AFTER); + configFile.addBlock(block); + + if (DEBUG) { + System.out.println(" AFTER: " + currentFile); + } + } else if (matchLineType(line, "val", 5)) { + final String productMakefile = fields.get(1); + final String blockTypeString = fields.get(2); + final String varName = fields.get(3); + final String varValue = fields.get(4); + final Position pos = Position.parse(fields.get(5)); + final Str str = new Str(pos, varValue); + + if (blockTypeString.equals("initial")) { + initialVariables.put(varName, str); + } else if (blockTypeString.equals("final")) { + finalVariables.put(varName, str); + } else { + if (!productMakefile.equals(configFile.getFilename())) { + mErrors.WARNING_DUMPCONFIG.add( + new Position(mFilename, line.getLine()), + "Mismatched 'val' product makefile." + + " Expected: " + configFile.getFilename() + + " Saw: " + productMakefile); + continue; + } + + final MakeConfig.BlockType blockType = parseBlockType(line, blockTypeString); + if (blockType == null) { + continue; + } + if (blockType != block.getBlockType()) { + mErrors.WARNING_DUMPCONFIG.add( + new Position(mFilename, line.getLine()), + "Mismatched 'val' block type." + + " Expected: " + block.getBlockType() + + " Saw: " + blockType); + } + + // Add the variable to the block in progress + block.addVar(varName, str); + } + } else { + if (DEBUG) { + System.out.print("# "); + for (int d = 0; d < fields.size(); d++) { + System.out.print(fields.get(d)); + if (d != fields.size() - 1) { + System.out.print(","); + } + } + System.out.println(); + } + } + } + } + + /** + * Return true if the line type matches 'lineType' and there are at least 'fieldCount' + * fields (not including the first field which is the line type). + */ + private boolean matchLineType(CsvParser.Line line, String lineType, int fieldCount) { + final List fields = line.getFields(); + if (!lineType.equals(fields.get(0))) { + return false; + } + if (fields.size() < (fieldCount + 1)) { + mErrors.WARNING_DUMPCONFIG.add(new Position(mFilename, line.getLine()), + fields.get(0) + " line has " + fields.size() + " fields. Expected at least " + + (fieldCount + 1) + " fields."); + return false; + } + return true; + } + + /** + * Split a string with space separated items (i.e. the make list format) into a List. + */ + private static List splitList(String text) { + // Arrays.asList returns a fixed-length List, so we copy it into an ArrayList to not + // propagate that surprise detail downstream. + return new ArrayList(Arrays.asList(LIST_SEPARATOR.split(text.trim()))); + } + + /** + * Parse a BockType or issue a warning if it can't be parsed. + */ + private MakeConfig.BlockType parseBlockType(CsvParser.Line line, String text) { + if ("before".equals(text)) { + return MakeConfig.BlockType.BEFORE; + } else if ("inherit".equals(text)) { + return MakeConfig.BlockType.INHERIT; + } else if ("after".equals(text)) { + return MakeConfig.BlockType.AFTER; + } else { + mErrors.WARNING_DUMPCONFIG.add( + new Position(mFilename, line.getLine()), + "Invalid block type: " + text); + return null; + } + } +} 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 63792c8c8f..92a4b30544 100644 --- a/tools/product_config/src/com/android/build/config/Errors.java +++ b/tools/product_config/src/com/android/build/config/Errors.java @@ -30,7 +30,7 @@ import java.util.Map; * Naming Convention: *
    *
  • ERROR_ for Categories with isLevelSettable false and Level.ERROR - *
  • WARNING_ for Categories with isLevelSettable false and default WARNING or HIDDEN + *
  • WARNING_ for Categories with isLevelSettable true and default WARNING or HIDDEN *
  • Don't have isLevelSettable true and not ERROR. (The constructor asserts this). *
*/ @@ -42,4 +42,21 @@ public class Errors extends ErrorReporter { public final Category WARNING_UNKNOWN_COMMAND_LINE_ERROR = new Category(2, true, Level.HIDDEN, "Passing unknown errors on the command line. Hidden by default for\n" + "forward compatibility."); + + public final Category ERROR_KATI = new Category(3, false, Level.ERROR, + "Error executing or reading from Kati."); + + public final Category WARNING_DUMPCONFIG = new Category(4, true, Level.WARNING, + "Anomaly parsing the output of kati and dumpconfig.mk."); + + public final Category ERROR_DUMPCONFIG = new Category(5, false, Level.ERROR, + "Error parsing the output of kati and dumpconfig.mk."); + + public final Category WARNING_VARIABLE_RECURSION = new Category(6, true, Level.WARNING, + "Possible unsupported variable recursion."); + + // This could be a warning, but it's very likely that the data is corrupted somehow + // 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."); } diff --git a/tools/product_config/src/com/android/build/config/GenericConfig.java b/tools/product_config/src/com/android/build/config/GenericConfig.java new file mode 100644 index 0000000000..2ee273564a --- /dev/null +++ b/tools/product_config/src/com/android/build/config/GenericConfig.java @@ -0,0 +1,131 @@ +/* + * 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; + +/** + * Language-agnostic representation of a configuration statement. + */ +public class GenericConfig extends ConfigBase { + /** + * The config files that were imported in this config pass. + */ + protected final TreeMap mConfigFiles = new TreeMap(); + + /** + * A configuration file. + */ + public static class ConfigFile { + /** + * The name of the file, relative to the tree root. + */ + private final String mFilename; + + /** + * Sections of variable definitions and import statements. Product config + * files will always have at least one block. + */ + private final ArrayList mStatements = new ArrayList(); + + public ConfigFile(String filename) { + mFilename = filename; + } + + public String getFilename() { + return mFilename; + } + + public void addStatement(Statement statement) { + mStatements.add(statement); + } + + public ArrayList getStatements() { + return mStatements; + } + } + + /** + * Base class for statements that appear in config files. + */ + public static class Statement { + } + + /** + * A variable assignment. + */ + public static class Assign extends Statement { + private final String mVarName; + private final List mValue; + + /** + * Assignment of a single value + */ + public Assign(String varName, Str value) { + mVarName = varName; + mValue = new ArrayList(); + mValue.add(value); + } + + /** + * Assignment referencing a previous value. + * VAR := $(1) $(VAR) $(2) $(VAR) $(3) + */ + public Assign(String varName, List value) { + mVarName = varName; + mValue = value; + } + + public String getName() { + return mVarName; + } + + public List getValue() { + return mValue; + } + } + + /** + * An $(inherit-product FILENAME) statement + */ + public static class Inherit extends Statement { + private final Str mFilename; + + public Inherit(Str filename) { + mFilename = filename; + } + + public Str getFilename() { + return mFilename; + } + } + + /** + * Adds the given config file. Returns any one previously added, or null. + */ + public ConfigFile addConfigFile(ConfigFile file) { + return mConfigFiles.put(file.getFilename(), file); + } + + public TreeMap getFiles() { + return mConfigFiles; + } +} diff --git a/tools/product_config/src/com/android/build/config/Kati.java b/tools/product_config/src/com/android/build/config/Kati.java new file mode 100644 index 0000000000..026ddb5a59 --- /dev/null +++ b/tools/product_config/src/com/android/build/config/Kati.java @@ -0,0 +1,26 @@ +/* + * 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.List; + +/** + * Wrapper for invoking kati. + */ +public interface Kati { + public MakeConfig loadProductConfig(); +} diff --git a/tools/product_config/src/com/android/build/config/KatiCommand.java b/tools/product_config/src/com/android/build/config/KatiCommand.java new file mode 100644 index 0000000000..f3c71d23b9 --- /dev/null +++ b/tools/product_config/src/com/android/build/config/KatiCommand.java @@ -0,0 +1,43 @@ +/* + * 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.Arrays; +import java.util.List; + +public interface KatiCommand { + public static class KatiException extends Exception { + private String mStderr; + + public KatiException(List cmd, String stderr) { + super("Error running kati: " + Arrays.toString(cmd.toArray())); + mStderr = stderr; + } + + public String getStderr() { + return mStderr; + } + } + + /** + * Run kati directly. Returns stdout data. + * + * @throws KatiException if there is an error. KatiException will contain + * the stderr from the kati invocation. + */ + public String run(String[] args) throws KatiException; +} diff --git a/tools/product_config/src/com/android/build/config/KatiCommandImpl.java b/tools/product_config/src/com/android/build/config/KatiCommandImpl.java new file mode 100644 index 0000000000..53480d4095 --- /dev/null +++ b/tools/product_config/src/com/android/build/config/KatiCommandImpl.java @@ -0,0 +1,139 @@ +/* + * 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.InputStream; +import java.io.InputStreamReader; +import java.io.IOException; +import java.util.ArrayList; +import java.nio.charset.StandardCharsets; + +public class KatiCommandImpl implements KatiCommand { + final Errors mErrors; + final Options mOptions; + + /** + * Runnable that consumes all of an InputStream until EOF, writes the contents + * into a StringBuilder, and then closes the stream. + */ + class OutputReader implements Runnable { + private final InputStream mStream; + private final StringBuilder mOutput; + + OutputReader(InputStream stream, StringBuilder output) { + mStream = stream; + mOutput = output; + } + + @Override + public void run() { + final char[] buf = new char[16*1024]; + final InputStreamReader reader = new InputStreamReader(mStream, StandardCharsets.UTF_8); + try { + int amt; + while ((amt = reader.read(buf, 0, buf.length)) >= 0) { + mOutput.append(buf, 0, amt); + } + } catch (IOException ex) { + mErrors.ERROR_KATI.add("Error reading from kati: " + ex.getMessage()); + } finally { + try { + reader.close(); + } catch (IOException ex) { + // Close doesn't throw + } + } + } + } + + public KatiCommandImpl(Errors errors, Options options) { + mErrors = errors; + mOptions = options; + } + + /** + * Run kati directly. Returns stdout data. + * + * @throws KatiException if there is an error. KatiException will contain + * the stderr from the kati invocation. + */ + public String run(String[] args) throws KatiException { + final ArrayList cmd = new ArrayList(); + cmd.add(mOptions.getCKatiBin()); + for (String arg: args) { + cmd.add(arg); + } + + final ProcessBuilder builder = new ProcessBuilder(cmd); + builder.redirectOutput(ProcessBuilder.Redirect.PIPE); + builder.redirectError(ProcessBuilder.Redirect.PIPE); + + Process process = null; + + try { + process = builder.start(); + } catch (IOException ex) { + throw new KatiException(cmd, "IOException running process: " + ex.getMessage()); + } + + final StringBuilder stdout = new StringBuilder(); + final Thread stdoutThread = new Thread(new OutputReader(process.getInputStream(), stdout), + "kati_stdout_reader"); + stdoutThread.start(); + + final StringBuilder stderr = new StringBuilder(); + final Thread stderrThread = new Thread(new OutputReader(process.getErrorStream(), stderr), + "kati_stderr_reader"); + stderrThread.start(); + + int returnCode = waitForProcess(process); + joinThread(stdoutThread); + joinThread(stderrThread); + + if (returnCode != 0) { + throw new KatiException(cmd, stderr.toString()); + } + + return stdout.toString(); + } + + /** + * Wrap Process.waitFor() because it throws InterruptedException. + */ + private static int waitForProcess(Process proc) { + while (true) { + try { + return proc.waitFor(); + } catch (InterruptedException ex) { + } + } + } + + /** + * Wrap Thread.join() because it throws InterruptedException. + */ + private static void joinThread(Thread thread) { + while (true) { + try { + thread.join(); + return; + } catch (InterruptedException ex) { + } + } + } +} + diff --git a/tools/product_config/src/com/android/build/config/KatiImpl.java b/tools/product_config/src/com/android/build/config/KatiImpl.java new file mode 100644 index 0000000000..feb374cb74 --- /dev/null +++ b/tools/product_config/src/com/android/build/config/KatiImpl.java @@ -0,0 +1,113 @@ +/* + * 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.File; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class KatiImpl implements Kati { + // Subdirectory inside out for config stuff. + private static final String CONFIG_SUBDIR = "config"; + + private final Errors mErrors; + private final Options mOptions; + private final KatiCommand mCommand; + + // TODO: Do we need to consider the whole or a greater subset of the + // environment (or a hash of it?). In theory product-variant is enough, but we know + // people use stuff from the environment, even though we're trying to get rid of that. + private String getWorkDirPath() { + return Paths.get(mOptions.getOutDir(), CONFIG_SUBDIR, + mOptions.getProduct() + '-' + mOptions.getVariant()).toString(); + } + + private String getDumpConfigCsvPath() { + return Paths.get(getWorkDirPath(), "dumpconfig.csv").toString(); + } + + public KatiImpl(Errors errors, Options options) { + this(errors, options, new KatiCommandImpl(errors, options)); + } + + // VisibleForTesting + public KatiImpl(Errors errors, Options options, KatiCommand command) { + mErrors = errors; + mOptions = options; + mCommand = command; + } + + @Override + public MakeConfig loadProductConfig() { + final String csvPath = getDumpConfigCsvPath(); + try { + File workDir = new File(getWorkDirPath()); + + if (!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 + }); + + if (!out.contains("***DONE***")) { + mErrors.ERROR_KATI.add( + "Unknown error with kati, but it didn't print ***DONE*** message"); + return null; // TODO: throw exception? + } + // TODO: Check that output was good. + } catch (KatiCommand.KatiException ex) { + mErrors.ERROR_KATI.add("Error running kati:\n" + ex.getStderr()); + return null; + } + + if (!(new File(csvPath)).canRead()) { + mErrors.ERROR_KATI.add("Kati ran but did not create " + csvPath); + return null; + } + + try (FileReader reader = new FileReader(csvPath)) { + System.out.println("csvPath=" + csvPath); + List 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); + } catch (CsvParser.ParseException ex) { + mErrors.ERROR_KATI.add(new Position(csvPath, ex.getLine()), + "Unable to parse output of dumpconfig.mk: " + ex.getMessage()); + return null; // TODO: throw exception? + } catch (IOException ex) { + System.out.println(ex); + mErrors.ERROR_KATI.add("Unable to read " + csvPath + ": " + ex.getMessage()); + return null; // TODO: throw exception? + } + } +} 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 b792193059..7417fc7dcc 100644 --- a/tools/product_config/src/com/android/build/config/Main.java +++ b/tools/product_config/src/com/android/build/config/Main.java @@ -16,6 +16,10 @@ package com.android.build.config; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; + public class Main { private final Errors mErrors; private final Options mOptions; @@ -31,6 +35,25 @@ public class Main { // 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()) { + return; + } + + System.out.println(); + System.out.println("===================="); + System.out.println("PRODUCT CONFIG FILES"); + System.out.println("===================="); + makeConfig.printToStream(System.out); + + ConvertMakeToGenericConfig m2g = new ConvertMakeToGenericConfig(mErrors); + GenericConfig generic = m2g.convert(makeConfig); + + System.out.println("======================"); + System.out.println("REGENERATED MAKE FILES"); + System.out.println("======================"); + MakeWriter.write(System.out, generic, 0); // TODO: Run kati and extract the variables and convert all that into starlark files. @@ -38,8 +61,6 @@ public class Main { // TODO: Get the variables that were defined in starlark and use that to write // out the make, soong and bazel input files. - mErrors.ERROR_COMMAND_LINE.add("asdf"); - throw new RuntimeException("poop"); } public static void main(String[] args) { @@ -47,7 +68,7 @@ public class Main { int exitCode = 0; try { - Options options = Options.parse(errors, args); + Options options = Options.parse(errors, args, System.getenv()); if (errors.hadError()) { Options.printHelp(System.err); System.err.println(); @@ -62,7 +83,7 @@ public class Main { Options.printHelp(System.out); return; } - } catch (CommandException ex) { + } catch (CommandException | Errors.FatalException ex) { // These are user errors, so don't show a stack trace exitCode = 1; } catch (Throwable ex) { diff --git a/tools/product_config/src/com/android/build/config/MakeConfig.java b/tools/product_config/src/com/android/build/config/MakeConfig.java new file mode 100644 index 0000000000..dda0db9b68 --- /dev/null +++ b/tools/product_config/src/com/android/build/config/MakeConfig.java @@ -0,0 +1,170 @@ +/* + * 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; + +public class MakeConfig extends ConfigBase { + /** + * The config files that were imported in this config pass. + */ + protected final ArrayList mConfigFiles = new ArrayList(); + + public enum BlockType { + UNSET, + BEFORE, + INHERIT, + AFTER + } + + public static class ConfigFile { + /** + * The name of the file, relative to the tree root. + */ + private final String mFilename; + + /** + * Sections of variable definitions and import statements. Product config + * files will always have at least one block. + */ + private final ArrayList mBlocks = new ArrayList(); + + public ConfigFile(String filename) { + mFilename = filename; + } + + public String getFilename() { + return mFilename; + } + + public void addBlock(Block block) { + mBlocks.add(block); + } + + public ArrayList getBlocks() { + return mBlocks; + } + } + + /** + * A set of variables that were defined. + */ + public static class Block { + private final BlockType mBlockType; + private final TreeMap mValues = new TreeMap(); + private Str mInheritedFile; + + public Block(BlockType blockType) { + mBlockType = blockType; + } + + public BlockType getBlockType() { + return mBlockType; + } + + public void addVar(String varName, Str varValue) { + mValues.put(varName, varValue); + } + + public Str getVar(String varName) { + return mValues.get(varName); + } + + public TreeMap getVars() { + return mValues; + } + + public void setInheritedFile(Str filename) { + mInheritedFile = filename; + } + + public Str getInheritedFile() { + return mInheritedFile; + } + } + + /** + * Adds the given config file. Returns any one previously added, or null. + */ + public ConfigFile addConfigFile(ConfigFile file) { + ConfigFile prev = null; + for (ConfigFile f: mConfigFiles) { + if (f.getFilename().equals(file.getFilename())) { + prev = f; + break; + } + } + mConfigFiles.add(file); + return prev; + } + + public List getConfigFiles() { + return mConfigFiles; + } + + public void printToStream(PrintStream out) { + out.println("MakeConfig {"); + out.println(" phase: " + mPhase); + out.println(" rootNodes: " + mRootNodes); + out.print(" singleVars: [ "); + for (Map.Entry entry: mProductVars.entrySet()) { + if (entry.getValue() == VarType.SINGLE) { + out.print(entry.getKey()); + out.print(" "); + } + } + out.println("]"); + out.print(" listVars: [ "); + for (Map.Entry entry: mProductVars.entrySet()) { + if (entry.getValue() == VarType.LIST) { + out.print(entry.getKey()); + out.print(" "); + } + } + out.println("]"); + out.println(" configFiles: ["); + for (final ConfigFile configFile: mConfigFiles) { + out.println(" ConfigFile {"); + out.println(" filename: " + configFile.getFilename()); + out.println(" blocks: ["); + for (Block block: configFile.getBlocks()) { + out.println(" Block {"); + out.println(" type: " + block.getBlockType()); + if (block.getBlockType() == BlockType.INHERIT) { + out.println(" inherited: " + block.getInheritedFile()); + } + out.println(" values: {"); + for (Map.Entry var: block.getVars().entrySet()) { + if (!var.getKey().equals("PRODUCT_PACKAGES")) { + continue; + } + out.println(" " + var.getKey() + ": " + var.getValue()); + } + out.println(" }"); + out.println(" }"); + } + out.println(" ]"); + out.println(" }"); + } + out.println(" ] // configFiles"); + out.println("} // MakeConfig"); + } +} diff --git a/tools/product_config/src/com/android/build/config/MakeWriter.java b/tools/product_config/src/com/android/build/config/MakeWriter.java new file mode 100644 index 0000000000..58dfcc090e --- /dev/null +++ b/tools/product_config/src/com/android/build/config/MakeWriter.java @@ -0,0 +1,155 @@ +/* + * 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.io.PrintStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +public class MakeWriter { + public static final int FLAG_WRITE_HEADER = 1; + public static final int FLAG_WRITE_ANNOTATIONS = 1 << 1; + + private final boolean mWriteHeader; + private final boolean mWriteAnnotations; + + public static void write(PrintStream out, GenericConfig config, int flags) { + (new MakeWriter(flags)).write(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) { + for (GenericConfig.ConfigFile file: config.getFiles().values()) { + out.println("---------------------------------------------------------"); + out.println("FILE: " + file.getFilename()); + out.println("---------------------------------------------------------"); + writeFile(out, config, file); + out.println(); + } + out.println("---------------------------------------------------------"); + out.println("VARIABLES TOUCHED BY MAKE BASED CONFIG:"); + out.println("---------------------------------------------------------"); + writeStrVars(out, getModifiedVars(config.getInitialVariables(), + config.getFinalVariables()), config); + } + + private void writeFile(PrintStream out, GenericConfig config, GenericConfig.ConfigFile file) { + if (mWriteHeader) { + out.println("# This file is generated by the product_config tool"); + } + for (GenericConfig.Statement statement: file.getStatements()) { + if (statement instanceof GenericConfig.Assign) { + writeAssign(out, config, (GenericConfig.Assign)statement); + } else if (statement instanceof GenericConfig.Inherit) { + writeInherit(out, (GenericConfig.Inherit)statement); + } else { + throw new RuntimeException("Unexpected Statement: " + statement); + } + } + } + + private void writeAssign(PrintStream out, GenericConfig config, + GenericConfig.Assign statement) { + final List values = statement.getValue(); + final int size = values.size(); + final String varName = statement.getName(); + Position pos = null; + if (size == 0) { + return; + } else if (size == 1) { + // Plain := + final Str value = values.get(0); + out.print(varName + " := " + value); + pos = value.getPosition(); + } else if (size == 2 && values.get(0).toString().length() == 0) { + // Plain += + final Str value = values.get(1); + out.print(varName + " += " + value); + pos = value.getPosition(); + } else { + // Write it out the long way + out.print(varName + " := " + values.get(0)); + for (int i = 1; i < size; i++) { + out.print("$(" + varName + ") " + values.get(i)); + pos = values.get(i).getPosition(); + } + } + if (mWriteAnnotations) { + out.print(" # " + config.getVarType(varName) + " " + pos); + } + out.println(); + } + + private void writeInherit(PrintStream out, GenericConfig.Inherit statement) { + final Str filename = statement.getFilename(); + out.print("$(call inherit-product " + filename + ")"); + if (mWriteAnnotations) { + out.print(" # " + filename.getPosition()); + } + out.println(); + } + + private static Map getModifiedVars(Map before, + Map after) { + final HashMap result = new HashMap(); + // Entries that were added or changed. + for (Map.Entry 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 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; + this.val = val; + } + final String name; + final Str val; + } + + private static void writeStrVars(PrintStream out, Map vars, ConfigBase config) { + // Sort by file name and var name + TreeMap sorted = new TreeMap(); + for (Map.Entry entry: vars.entrySet()) { + sorted.put(entry.getValue().getPosition().toString() + " " + entry.getKey(), + new Var(entry.getKey(), entry.getValue())); + } + // Print it + for (Var var: sorted.values()) { + out.println(var.val.getPosition() + var.name + " := " + var.val); + } + } +} 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 48146cb73d..4e6048425e 100644 --- a/tools/product_config/src/com/android/build/config/Options.java +++ b/tools/product_config/src/com/android/build/config/Options.java @@ -17,6 +17,7 @@ package com.android.build.config; import java.io.PrintStream; +import java.util.Map; import java.util.TreeMap; public class Options { @@ -27,19 +28,50 @@ public class Options { private Action mAction = Action.DEFAULT; + private String mProduct; + private String mVariant; + private String mOutDir; + private String mCKatiBin; + public Action getAction() { return mAction; } + public String getProduct() { + return mProduct; + } + + public String getVariant() { + return mVariant; + } + + public String getOutDir() { + return mOutDir != null ? mOutDir : "out"; + } + + public String getCKatiBin() { + return mCKatiBin; + } + public static void printHelp(PrintStream out) { out.println("usage: product_config"); out.println(); - out.println("OPTIONS"); + out.println("REQUIRED FLAGS"); + out.println(" --ckati_bin CKATI Kati binary to use."); + out.println(); + out.println("OPTIONAL FLAGS"); out.println(" --hide ERROR_ID Suppress this error."); out.println(" --error ERROR_ID Make this ERROR_ID a fatal error."); out.println(" --help -h This message."); out.println(" --warning ERROR_ID Make this ERROR_ID a warning."); out.println(); + out.println("REQUIRED ENVIRONMENT"); + out.println(" TARGET_PRODUCT Product to build from lunch command."); + out.println(" TARGET_BUILD_VARIANT Build variant from lunch command."); + out.println(); + out.println("OPTIONAL ENVIRONMENT"); + out.println(" OUT_DIR Build output directory. Defaults to \"out\"."); + out.println(); out.println("ERRORS"); out.println(" The following are the errors that can be controlled on the"); out.println(" commandline with the --hide --warning --error flags."); @@ -63,20 +95,26 @@ public class Options { private Errors mErrors; private String[] mArgs; + private Map mEnv; private Options mResult = new Options(); private int mIndex; + private boolean mSkipRequiredArgValidation; - public Parser(Errors errors, String[] args) { + public Parser(Errors errors, String[] args, Map env) { mErrors = errors; mArgs = args; + mEnv = env; } public Options parse() { + // Args try { while (mIndex < mArgs.length) { final String arg = mArgs[mIndex]; - if ("--hide".equals(arg)) { + if ("--ckati_bin".equals(arg)) { + mResult.mCKatiBin = requireNextStringArg(arg); + } else if ("--hide".equals(arg)) { handleErrorCode(arg, Errors.Level.HIDDEN); } else if ("--error".equals(arg)) { handleErrorCode(arg, Errors.Level.ERROR); @@ -99,11 +137,45 @@ public class Options { mErrors.ERROR_COMMAND_LINE.add(ex.getMessage()); } + // Environment + mResult.mProduct = mEnv.get("TARGET_PRODUCT"); + mResult.mVariant = mEnv.get("TARGET_BUILD_VARIANT"); + mResult.mOutDir = mEnv.get("OUT_DIR"); + + validateArgs(); + return mResult; } - private void addWarning(Errors.Category category, String message) { - category.add(message); + /** + * For testing; don't generate errors about missing arguments + */ + public void setSkipRequiredArgValidation() { + mSkipRequiredArgValidation = true; + } + + private void validateArgs() { + if (!mSkipRequiredArgValidation) { + if (mResult.mCKatiBin == null || "".equals(mResult.mCKatiBin)) { + addMissingArgError("--ckati_bin"); + } + if (mResult.mProduct == null) { + addMissingEnvError("TARGET_PRODUCT"); + } + if (mResult.mVariant == null) { + addMissingEnvError("TARGET_BUILD_VARIANT"); + } + } + } + + private void addMissingArgError(String argName) { + mErrors.ERROR_COMMAND_LINE.add("Required command line argument missing: " + + argName); + } + + private void addMissingEnvError(String envName) { + mErrors.ERROR_COMMAND_LINE.add("Required environment variable missing: " + + envName); } private String getNextNonFlagArg() { @@ -117,6 +189,14 @@ public class Options { return mArgs[mIndex]; } + private String requireNextStringArg(String arg) throws ParseException { + final String val = getNextNonFlagArg(); + if (val == null) { + throw new ParseException(arg + " requires a string argument."); + } + return val; + } + private int requireNextNumberArg(String arg) throws ParseException { final String val = getNextNonFlagArg(); if (val == null) { @@ -151,7 +231,7 @@ public class Options { *

* Adds errors encountered to Errors object. */ - public static Options parse(Errors errors, String[] args) { - return (new Parser(errors, args)).parse(); + public static Options parse(Errors errors, String[] args, Map env) { + return (new Parser(errors, args, env)).parse(); } } diff --git a/tools/product_config/src/com/android/build/config/Position.java b/tools/product_config/src/com/android/build/config/Position.java index 795394271e..266021d8bf 100644 --- a/tools/product_config/src/com/android/build/config/Position.java +++ b/tools/product_config/src/com/android/build/config/Position.java @@ -16,6 +16,9 @@ package com.android.build.config; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * Position in a source file. */ @@ -25,6 +28,9 @@ public class Position implements Comparable { */ public static final int NO_LINE = -1; + private static final Pattern REGEX = Pattern.compile("([^:]*)(?::(\\d)*)?:?\\s*"); + public static final String UNKNOWN = ""; + private final String mFile; private final int mLine; @@ -63,12 +69,39 @@ public class Position implements Comparable { return mLine; } + /** + * Return a Position object from a string containing :, or the default + * Position(null, NO_LINE) if the string can't be parsed. + */ + public static Position parse(String str) { + final Matcher m = REGEX.matcher(str); + if (!m.matches()) { + return new Position(); + } + String filename = m.group(1); + if (filename.length() == 0 || UNKNOWN.equals(filename)) { + filename = null; + } + String lineString = m.group(2); + int line; + if (lineString == null || lineString.length() == 0) { + line = NO_LINE; + } else { + try { + line = Integer.parseInt(lineString); + } catch (NumberFormatException ex) { + line = NO_LINE; + } + } + return new Position(filename, line); + } + @Override public String toString() { if (mFile == null && mLine == NO_LINE) { return ""; } else if (mFile == null && mLine != NO_LINE) { - return ":" + mLine + ": "; + return UNKNOWN + ":" + mLine + ": "; } else if (mFile != null && mLine == NO_LINE) { return mFile + ": "; } else { // if (mFile != null && mLine != NO_LINE) diff --git a/tools/product_config/src/com/android/build/config/Str.java b/tools/product_config/src/com/android/build/config/Str.java new file mode 100644 index 0000000000..9c345a6329 --- /dev/null +++ b/tools/product_config/src/com/android/build/config/Str.java @@ -0,0 +1,78 @@ +/* + * 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; + +/** + * A String and a Position, where it came from in source code. + */ +public class Str { + private String mValue; + private Position mPosition; + + public Str(String s) { + mValue = s; + mPosition = new Position(); + } + + public Str(Position pos, String s) { + mValue = s; + mPosition = pos; + } + + @Override + public String toString() { + return mValue; + } + + public Position getPosition() { + return mPosition; + } + + /** + * Str is equal if the string value is equal, regardless of whether the position + * is the same. + */ + @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 { + return false; + } + } + + @Override + public int hashCode() { + return mValue.hashCode(); + } + + public static ArrayList toList(Position pos, List list) { + final ArrayList result = new ArrayList(list.size()); + for (String s: list) { + result.add(new Str(pos, s)); + } + return result; + } +} diff --git a/tools/product_config/test/com/android/build/config/CsvParserTest.java b/tools/product_config/test/com/android/build/config/CsvParserTest.java new file mode 100644 index 0000000000..6f38d684cb --- /dev/null +++ b/tools/product_config/test/com/android/build/config/CsvParserTest.java @@ -0,0 +1,148 @@ + +/* + * 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 org.junit.Assert; +import org.junit.Test; + +import java.io.StringReader; +import java.util.Arrays; +import java.util.List; + +/** + * Test for CSV parser class. + */ +public class CsvParserTest { + public String listsToStrings(String[] expected, List actual) { + return "expected=" + Arrays.toString(expected) + + " actual=" + Arrays.toString(actual.toArray()); + } + + public void assertLineEquals(CsvParser.Line actual, int lineno, String... fields) { + if (actual.getLine() != lineno) { + throw new RuntimeException("lineno mismatch: expected=" + lineno + + " actual=" + actual.getLine()); + } + if (fields.length != actual.getFields().size()) { + throw new RuntimeException("getFields().size() mismatch: expected=" + fields.length + + " actual=" + actual.getFields().size() + + " values: " + listsToStrings(fields, actual.getFields())); + } + for (int i = 0; i < fields.length; i++) { + if (!fields[i].equals(actual.getFields().get(i))) { + throw new RuntimeException("getFields().get(" + i + ") mismatch: expected=" + + fields[i] + " actual=" + actual.getFields().get(i) + + " values: " + listsToStrings(fields, actual.getFields())); + + } + } + } + + @Test + public void testEmptyString() throws Exception { + List lines = CsvParser.parse(new StringReader( + "")); + + Assert.assertEquals(0, lines.size()); + } + + @Test + public void testLexerOneCharacter() throws Exception { + List lines = CsvParser.parse(new StringReader( + "a")); + + Assert.assertEquals(1, lines.size()); + assertLineEquals(lines.get(0), 1, "a"); + } + + @Test + public void testLexerTwoFieldsNoNewline() throws Exception { + List lines = CsvParser.parse(new StringReader( + "a,b")); + + Assert.assertEquals(1, lines.size()); + assertLineEquals(lines.get(0), 1, "a", "b"); + } + + @Test + public void testLexerTwoFieldsNewline() throws Exception { + List lines = CsvParser.parse(new StringReader( + "a,b\n")); + + Assert.assertEquals(1, lines.size()); + assertLineEquals(lines.get(0), 1, "a", "b"); + } + + @Test + public void testEndsWithTwoNewlines() throws Exception { + List lines = CsvParser.parse(new StringReader( + "a,b\n\n")); + + Assert.assertEquals(1, lines.size()); + assertLineEquals(lines.get(0), 1, "a", "b"); + } + + @Test + public void testOnlyNewlines() throws Exception { + List lines = CsvParser.parse(new StringReader( + "\n\n\n\n")); + + Assert.assertEquals(0, lines.size()); + } + + + @Test + public void testLexerComplex() throws Exception { + List lines = CsvParser.parse(new StringReader( + ",\"ab\"\"\nc\",,de\n" + + "fg,\n" + + "\n" + + ",\n" + + "hijk")); + + Assert.assertEquals(4, lines.size()); + assertLineEquals(lines.get(0), 2, "", "ab\"\nc", "", "de"); + assertLineEquals(lines.get(1), 3, "fg", ""); + assertLineEquals(lines.get(2), 5, "", ""); + assertLineEquals(lines.get(3), 6, "hijk"); + } + + @Test + public void testEndInsideQuoted() throws Exception { + try { + List lines = CsvParser.parse(new StringReader( + "\"asd")); + throw new RuntimeException("Didn't throw ParseException"); + } catch (CsvParser.ParseException ex) { + System.out.println("Caught: " + ex); + } + } + + @Test + public void testCharacterAfterQuotedField() throws Exception { + try { + List lines = CsvParser.parse(new StringReader( + "\"\"a")); + throw new RuntimeException("Didn't throw ParseException"); + } catch (CsvParser.ParseException ex) { + System.out.println("Caught: " + ex); + } + } +} + diff --git a/tools/product_config/test/com/android/build/config/OptionsTest.java b/tools/product_config/test/com/android/build/config/OptionsTest.java index 2c36322584..459efa53b0 100644 --- a/tools/product_config/test/com/android/build/config/OptionsTest.java +++ b/tools/product_config/test/com/android/build/config/OptionsTest.java @@ -19,12 +19,24 @@ package com.android.build.config; import org.junit.Assert; import org.junit.Test; +import java.util.HashMap; + public class OptionsTest { + + private Options parse(Errors errors, String[] args) { + final HashMap env = new HashMap(); + env.put("TARGET_PRODUCT", "test_product"); + env.put("TARGET_BUILD_VARIANT", "user"); + final Options.Parser parser = new Options.Parser(errors, args, env); + parser.setSkipRequiredArgValidation(); + return parser.parse(); + } + @Test public void testErrorMissingLast() { final Errors errors = new Errors(); - final Options options = Options.parse(errors, new String[] { + final Options options = parse(errors, new String[] { "--error" }); @@ -37,7 +49,7 @@ public class OptionsTest { public void testErrorMissingNotLast() { final Errors errors = new Errors(); - final Options options = Options.parse(errors, new String[] { + final Options options = parse(errors, new String[] { "--error", "--warning", "2" }); @@ -50,7 +62,7 @@ public class OptionsTest { public void testErrorNotNumeric() { final Errors errors = new Errors(); - final Options options = Options.parse(errors, new String[] { + final Options options = parse(errors, new String[] { "--error", "notgood" }); @@ -63,7 +75,7 @@ public class OptionsTest { public void testErrorInvalidError() { final Errors errors = new Errors(); - final Options options = Options.parse(errors, new String[] { + final Options options = parse(errors, new String[] { "--error", "50000" }); @@ -76,7 +88,7 @@ public class OptionsTest { public void testErrorOne() { final Errors errors = new Errors(); - final Options options = Options.parse(errors, new String[] { + final Options options = parse(errors, new String[] { "--error", "2" }); @@ -89,7 +101,7 @@ public class OptionsTest { public void testWarningOne() { final Errors errors = new Errors(); - final Options options = Options.parse(errors, new String[] { + final Options options = parse(errors, new String[] { "--warning", "2" }); @@ -102,7 +114,7 @@ public class OptionsTest { public void testHideOne() { final Errors errors = new Errors(); - final Options options = Options.parse(errors, new String[] { + final Options options = parse(errors, new String[] { "--hide", "2" }); @@ -110,5 +122,16 @@ public class OptionsTest { Assert.assertEquals(Options.Action.DEFAULT, options.getAction()); Assert.assertFalse(errors.hadWarningOrError()); } + + @Test + public void testEnv() { + final Errors errors = new Errors(); + + final Options options = parse(errors, new String[0]); + + Assert.assertEquals("test_product", options.getProduct()); + Assert.assertEquals("user", options.getVariant()); + Assert.assertFalse(errors.hadWarningOrError()); + } } diff --git a/tools/product_config/test/com/android/build/config/PositionTest.java b/tools/product_config/test/com/android/build/config/PositionTest.java new file mode 100644 index 0000000000..82b5dd4604 --- /dev/null +++ b/tools/product_config/test/com/android/build/config/PositionTest.java @@ -0,0 +1,68 @@ +/* + * 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 org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; + +public class PositionTest { + + @Test + public void testParseEmpty() { + final Position pos = Position.parse(""); + + Assert.assertEquals(null, pos.getFile()); + Assert.assertEquals(Position.NO_LINE, pos.getLine()); + } + + @Test + public void testParseOnlyFile() { + final Position pos = Position.parse("asdf"); + + Assert.assertEquals("asdf", pos.getFile()); + Assert.assertEquals(Position.NO_LINE, pos.getLine()); + } + + @Test + public void testParseBoth() { + final Position pos = Position.parse("asdf:1"); + + Assert.assertEquals("asdf", pos.getFile()); + Assert.assertEquals(1, pos.getLine()); + } + + @Test + public void testParseEndsWithColon() { + final Position pos = Position.parse("asdf:"); + + Assert.assertEquals("asdf", pos.getFile()); + Assert.assertEquals(Position.NO_LINE, pos.getLine()); + } + + @Test + public void testParseEndsWithSpace() { + final Position pos = Position.parse("asdf: "); + + Assert.assertEquals("asdf", pos.getFile()); + Assert.assertEquals(Position.NO_LINE, pos.getLine()); + } + + +} + diff --git a/tools/product_config/test/com/android/build/config/TestRunner.java b/tools/product_config/test/com/android/build/config/TestRunner.java index 9a5ee692c1..546518fbe2 100644 --- a/tools/product_config/test/com/android/build/config/TestRunner.java +++ b/tools/product_config/test/com/android/build/config/TestRunner.java @@ -39,8 +39,10 @@ public class TestRunner { System.out.println(failure.getTrace()); } }); - Result result = junit.run(ErrorReporterTest.class, - OptionsTest.class); + Result result = junit.run(CsvParserTest.class, + ErrorReporterTest.class, + OptionsTest.class, + PositionTest.class); if (!result.wasSuccessful()) { System.out.println("\n*** FAILED ***"); }