Merge changes I9c1995f8,Id6763781,I52e5c07f,I4706e32f,I7d74b226

* changes:
  Keep the first and last snapshot of variables.
  Generate GenericConfig objects from MakeConfig objects.
  Emit and parse the product config variables from kati/make
  Add class to fork and exec kati, based on the commandline option given.
  Add a CSV parser to parse the output from kati.
This commit is contained in:
Joe Onorato 2021-02-19 16:56:13 +00:00 committed by Gerrit Code Review
commit 2fd88e0600
23 changed files with 2291 additions and 24 deletions

129
core/dumpconfig.mk Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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<String> mRootNodes;
/**
* State of the make varaible environment from before the first config file.
*/
protected Map<String, Str> mInitialVariables = new HashMap();
/**
* State of the make varaible environment from after the first config file.
*/
protected Map<String, Str> mFinalVariables = new HashMap();
/**
* The variables that are handled specially.
*/
protected final TreeMap<String, VarType> 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<String> filenames) {
mRootNodes = new ArrayList(filenames);
}
public List<String> getRootNodes() {
return mRootNodes;
}
public void addProductVar(String name, VarType type) {
mProductVars.put(name, type);
}
public TreeMap<String, VarType> 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<String, Str> getInitialVariables() {
return mInitialVariables;
}
/**
* Return the state the make variable environment from before the first config file.
*/
public Map<String, Str> 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<String, ConfigBase.VarType> entry: that.getProductVars().entrySet()) {
addProductVar(entry.getKey(), entry.getValue());
}
mInitialVariables = new HashMap(that.getInitialVariables());
mFinalVariables = new HashMap(that.getFinalVariables());
}
}

View File

@ -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<MakeConfig.Block> 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<String, Str> 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<String> 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<String> split(String haystack, String needle) {
final ArrayList<String> 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;
}
}

View File

@ -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<String> mFields;
Line(int lineno, List<String> fields) {
mLineNumber = lineno;
mFields = fields;
}
public int getLine() {
return mLineNumber;
}
public List<String> 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<Line> parse(Reader reader)
throws ParseException, IOException {
ArrayList<Line> result = new ArrayList();
int line = 1;
int column = 1;
int pos = 0;
char[] buf = new char[CHUNK_SIZE];
HashMap<String,String> stringPool = new HashMap();
ArrayList<String> 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;
}
}

View File

@ -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<MakeConfig> 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<MakeConfig> 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<CsvParser.Line> 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<String> 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("<ignored>");
MakeConfig.Block block = new MakeConfig.Block(MakeConfig.BlockType.UNSET);
Map<String, Str> initialVariables = new HashMap();
Map<String, Str> finalVariables = new HashMap();
// Number of "phases" we've seen so far.
for (; index < lineCount; index++) {
final CsvParser.Line line = lines.get(index);
final List<String> 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<String> 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<String> 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<String> 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<String>.
*/
private static List<String> 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;
}
}
}

View File

@ -30,7 +30,7 @@ import java.util.Map;
* <b>Naming Convention:</b>
* <ul>
* <li>ERROR_ for Categories with isLevelSettable false and Level.ERROR
* <li>WARNING_ for Categories with isLevelSettable false and default WARNING or HIDDEN
* <li>WARNING_ for Categories with isLevelSettable true and default WARNING or HIDDEN
* <li>Don't have isLevelSettable true and not ERROR. (The constructor asserts this).
* </ul>
*/
@ -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.");
}

View File

@ -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<String, ConfigFile> 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<Statement> mStatements = new ArrayList();
public ConfigFile(String filename) {
mFilename = filename;
}
public String getFilename() {
return mFilename;
}
public void addStatement(Statement statement) {
mStatements.add(statement);
}
public ArrayList<Statement> 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<Str> 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<Str> value) {
mVarName = varName;
mValue = value;
}
public String getName() {
return mVarName;
}
public List<Str> 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<String, ConfigFile> getFiles() {
return mConfigFiles;
}
}

View File

@ -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();
}

View File

@ -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<String> 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;
}

View File

@ -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<String> 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) {
}
}
}
}

View File

@ -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<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);
} 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?
}
}
}

View File

@ -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) {

View File

@ -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<ConfigFile> 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<Block> mBlocks = new ArrayList();
public ConfigFile(String filename) {
mFilename = filename;
}
public String getFilename() {
return mFilename;
}
public void addBlock(Block block) {
mBlocks.add(block);
}
public ArrayList<Block> getBlocks() {
return mBlocks;
}
}
/**
* A set of variables that were defined.
*/
public static class Block {
private final BlockType mBlockType;
private final TreeMap<String, Str> 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<String, Str> 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<ConfigFile> 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<String,VarType> entry: mProductVars.entrySet()) {
if (entry.getValue() == VarType.SINGLE) {
out.print(entry.getKey());
out.print(" ");
}
}
out.println("]");
out.print(" listVars: [ ");
for (Map.Entry<String,VarType> 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<String,Str> 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");
}
}

View File

@ -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<Str> 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<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;
this.val = val;
}
final String name;
final Str val;
}
private static void writeStrVars(PrintStream out, Map<String, Str> vars, ConfigBase config) {
// Sort by file name and var name
TreeMap<String, Var> sorted = new TreeMap();
for (Map.Entry<String, Str> 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);
}
}
}

View File

@ -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<String,String> 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<String,String> 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 {
* <p>
* 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<String, String> env) {
return (new Parser(errors, args, env)).parse();
}
}

View File

@ -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<Position> {
*/
public static final int NO_LINE = -1;
private static final Pattern REGEX = Pattern.compile("([^:]*)(?::(\\d)*)?:?\\s*");
public static final String UNKNOWN = "<unknown>";
private final String mFile;
private final int mLine;
@ -63,12 +69,39 @@ public class Position implements Comparable<Position> {
return mLine;
}
/**
* Return a Position object from a string containing <filename>:<line>, 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 "<unknown>:" + mLine + ": ";
return UNKNOWN + ":" + mLine + ": ";
} else if (mFile != null && mLine == NO_LINE) {
return mFile + ": ";
} else { // if (mFile != null && mLine != NO_LINE)

View File

@ -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<Str> toList(Position pos, List<String> list) {
final ArrayList<Str> result = new ArrayList(list.size());
for (String s: list) {
result.add(new Str(pos, s));
}
return result;
}
}

View File

@ -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<String> 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<CsvParser.Line> lines = CsvParser.parse(new StringReader(
""));
Assert.assertEquals(0, lines.size());
}
@Test
public void testLexerOneCharacter() throws Exception {
List<CsvParser.Line> lines = CsvParser.parse(new StringReader(
"a"));
Assert.assertEquals(1, lines.size());
assertLineEquals(lines.get(0), 1, "a");
}
@Test
public void testLexerTwoFieldsNoNewline() throws Exception {
List<CsvParser.Line> 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<CsvParser.Line> 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<CsvParser.Line> 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<CsvParser.Line> lines = CsvParser.parse(new StringReader(
"\n\n\n\n"));
Assert.assertEquals(0, lines.size());
}
@Test
public void testLexerComplex() throws Exception {
List<CsvParser.Line> 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<CsvParser.Line> 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<CsvParser.Line> lines = CsvParser.parse(new StringReader(
"\"\"a"));
throw new RuntimeException("Didn't throw ParseException");
} catch (CsvParser.ParseException ex) {
System.out.println("Caught: " + ex);
}
}
}

View File

@ -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<String, String> 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());
}
}

View File

@ -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());
}
}

View File

@ -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 ***");
}