Add skeleton for product-config tool.

There's also a bunch of boilerplate error handling and option parsing
stuff that I wish someone would put into a library that's available to
the android tree.

Test: atest product-config-test
Change-Id: Ieebcc7bd47a8644d1374fb02c146e9038859f4a2
This commit is contained in:
Joe Onorato 2020-10-19 23:28:59 -07:00
parent 95bd22edb6
commit 841c3e3ea6
11 changed files with 955 additions and 0 deletions

View File

@ -0,0 +1,23 @@
java_defaults {
name: "product-config-defaults",
srcs: ["src/**/*.java"],
}
java_binary_host {
name: "product-config",
defaults: ["product-config-defaults"],
manifest: "MANIFEST.MF"
}
java_test_host {
name: "product-config-test",
defaults: ["product-config-defaults"],
srcs: [
"test/**/*.java",
],
static_libs: [
"junit"
],
test_suites: ["general-tests"]
}

View File

@ -0,0 +1,2 @@
Manifest-Version: 1.0
Main-Class: com.android.build.config.Main

View File

@ -0,0 +1,7 @@
{
"presubmit": [
{
"name": "product_config_test"
}
]
}

View File

@ -0,0 +1,263 @@
/*
* 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.lang.reflect.Field;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Base class for reporting errors.
*/
public class ErrorReporter {
/**
* List of Entries that have occurred.
*/
// Also used as the lock for this object.
private final ArrayList<Entry> mEntries = new ArrayList();
/**
* The categories that are for this Errors object.
*/
private Map<Integer, Category> mCategories;
/**
* Whether there has been a warning or an error yet.
*/
private boolean mHadWarningOrError;
/**
* Whether there has been an error yet.
*/
private boolean mHadError;
/**
* Whether errors are errors, warnings or hidden.
*/
public static enum Level {
HIDDEN("hidden"),
WARNING("warning"),
ERROR("error");
private final String mLabel;
Level(String label) {
mLabel = label;
}
String getLabel() {
return mLabel;
}
}
/**
* The available error codes.
*/
public class Category {
private final int mCode;
private boolean mIsLevelSettable;
private Level mLevel;
private String mHelp;
/**
* Construct a Category object.
*/
public Category(int code, boolean isLevelSettable, Level level, String help) {
if (!isLevelSettable && level != Level.ERROR) {
throw new RuntimeException("Don't have WARNING or HIDDEN without isLevelSettable");
}
mCode = code;
mIsLevelSettable = isLevelSettable;
mLevel = level;
mHelp = help;
}
/**
* Get the numeric code for the Category, which can be used to set the level.
*/
public int getCode() {
return mCode;
}
/**
* Get whether the level of this Category can be changed.
*/
public boolean isLevelSettable() {
return mIsLevelSettable;
}
/**
* Set the level of this category.
*/
public void setLevel(Level level) {
if (!mIsLevelSettable) {
throw new RuntimeException("Can't set level for error " + mCode);
}
mLevel = level;
}
/**
* Return the level, including any overrides.
*/
public Level getLevel() {
return mLevel;
}
/**
* Return the category's help text.
*/
public String getHelp() {
return mHelp;
}
}
/**
* An instance of an error happening.
*/
public class Entry {
private final Category mCategory;
private final Position mPosition;
private final String mMessage;
Entry(Category category, Position position, String message) {
mCategory = category;
mPosition = position;
mMessage = message;
}
public Category getCategory() {
return mCategory;
}
public Position getPosition() {
return mPosition;
}
public String getMessage() {
return mMessage;
}
}
private void initLocked() {
if (mCategories == null) {
HashMap<Integer, Category> categories = new HashMap();
for (Field field: getClass().getFields()) {
if (Category.class.isAssignableFrom(field.getType())) {
Category category = null;
try {
category = (Category)field.get(this);
} catch (IllegalAccessException ex) {
// Wrap and rethrow, this is always on this class, so it's
// our programming error if this happens.
throw new RuntimeException("Categories on Errors should be public.", ex);
}
Category prev = categories.put(category.getCode(), category);
if (prev != null) {
throw new RuntimeException("Duplicate categories with code "
+ category.getCode());
}
}
}
mCategories = Collections.unmodifiableMap(categories);
}
}
/**
* Returns a map of the category codes to the categories.
*/
public Map<Integer, Category> getCategories() {
synchronized (mEntries) {
initLocked();
return mCategories;
}
}
/**
* Add an error with no source position.
*/
public void add(Category category, String message) {
add(category, new Position(), message);
}
/**
* Add an error.
*/
public void add(Category category, Position pos, String message) {
synchronized (mEntries) {
initLocked();
if (mCategories.get(category.getCode()) != category) {
throw new RuntimeException("Errors.Category used from the wrong Errors object.");
}
mEntries.add(new Entry(category, pos, message));
final Level level = category.getLevel();
if (level == Level.WARNING || level == Level.ERROR) {
mHadWarningOrError = true;
}
if (level == Level.ERROR) {
mHadError = true;
}
}
}
/**
* Returns whether there has been a warning or an error yet.
*/
public boolean hadWarningOrError() {
synchronized (mEntries) {
return mHadWarningOrError;
}
}
/**
* Returns whether there has been an error yet.
*/
public boolean hadError() {
synchronized (mEntries) {
return mHadError;
}
}
/**
* Returns a list of all entries that were added.
*/
public List<Entry> getEntries() {
synchronized (mEntries) {
return new ArrayList<Entry>(mEntries);
}
}
/**
* Prints the errors.
*/
public void printErrors(PrintStream out) {
synchronized (mEntries) {
for (Entry entry: mEntries) {
final Category category = entry.getCategory();
final Level level = category.getLevel();
if (level == Level.HIDDEN) {
continue;
}
out.println(entry.getPosition() + "[" + level.getLabel() + " "
+ category.getCode() + "] " + entry.getMessage());
}
}
}
}

View File

@ -0,0 +1,45 @@
/*
* 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.lang.reflect.Field;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Error constants and error reporting.
* <p>
* <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>Don't have isLevelSettable true and not ERROR. (The constructor asserts this).
* </ul>
*/
public class Errors extends ErrorReporter {
public final Category ERROR_COMMAND_LINE = new Category(1, false, Level.ERROR,
"Error on the command line.");
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.");
}

View File

@ -0,0 +1,64 @@
/*
* 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;
public class Main {
private final Errors mErrors;
private final Options mOptions;
public Main(Errors errors, Options options) {
mErrors = errors;
mOptions = options;
}
void run() {
System.out.println("Hello World");
// TODO: Check the build environment to make sure we're running in a real
// build environment, e.g. actually inside a source tree, with TARGET_PRODUCT
// and TARGET_BUILD_VARIANT defined, etc.
// TODO: Run kati and extract the variables and convert all that into starlark files.
// TODO: Run starlark with all the generated ones and the hand written ones.
// TODO: Get the variables that were defined in starlark and use that to write
// out the make, soong and bazel input files.
}
public static void main(String[] args) {
Errors errors = new Errors();
Options options = Options.parse(errors, args);
if (errors.hadError()) {
Options.printHelp(System.err);
System.err.println();
errors.printErrors(System.err);
System.exit(1);
}
switch (options.getAction()) {
case DEFAULT:
(new Main(errors, options)).run();
errors.printErrors(System.err);
return;
case HELP:
Options.printHelp(System.out);
return;
}
}
}

View File

@ -0,0 +1,158 @@
/*
* 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.TreeMap;
public class Options {
public enum Action {
DEFAULT,
HELP
}
private Action mAction = Action.DEFAULT;
public Action getAction() {
return mAction;
}
public static void printHelp(PrintStream out) {
out.println("usage: product_config");
out.println();
out.println("OPTIONS");
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("ERRORS");
out.println(" The following are the errors that can be controlled on the");
out.println(" commandline with the --hide --warning --error flags.");
TreeMap<Integer,Errors.Category> sorted = new TreeMap((new Errors()).getCategories());
for (final Errors.Category category: sorted.values()) {
if (category.isLevelSettable()) {
out.println(String.format(" %-3d %s", category.getCode(),
category.getHelp().replace("\n", "\n ")));
}
}
}
static class Parser {
private class ParseException extends Exception {
public ParseException(String message) {
super(message);
}
}
private Errors mErrors;
private String[] mArgs;
private Options mResult = new Options();
private int mIndex;
public Parser(Errors errors, String[] args) {
mErrors = errors;
mArgs = args;
}
public Options parse() {
try {
while (mIndex < mArgs.length) {
final String arg = mArgs[mIndex];
if ("--hide".equals(arg)) {
handleErrorCode(arg, Errors.Level.HIDDEN);
} else if ("--error".equals(arg)) {
handleErrorCode(arg, Errors.Level.ERROR);
} else if ("--help".equals(arg) || "-h".equals(arg)) {
// Help overrides all other commands if there isn't an error, but
// we will stop here.
if (!mErrors.hadError()) {
mResult.mAction = Action.HELP;
}
return mResult;
} else if ("--warning".equals(arg)) {
handleErrorCode(arg, Errors.Level.WARNING);
} else {
throw new ParseException("Unknown command line argument: " + arg);
}
mIndex++;
}
} catch (ParseException ex) {
mErrors.add(mErrors.ERROR_COMMAND_LINE, ex.getMessage());
}
return mResult;
}
private void addWarning(Errors.Category category, String message) {
mErrors.add(category, message);
}
private String getNextNonFlagArg() {
if (mIndex == mArgs.length - 1) {
return null;
}
if (mArgs[mIndex + 1].startsWith("-")) {
return null;
}
mIndex++;
return mArgs[mIndex];
}
private int requireNextNumberArg(String arg) throws ParseException {
final String val = getNextNonFlagArg();
if (val == null) {
throw new ParseException(arg + " requires a numeric argument.");
}
try {
return Integer.parseInt(val);
} catch (NumberFormatException ex) {
throw new ParseException(arg + " requires a numeric argument. found: " + val);
}
}
private void handleErrorCode(String arg, Errors.Level level) throws ParseException {
final int code = requireNextNumberArg(arg);
final Errors.Category category = mErrors.getCategories().get(code);
if (category == null) {
mErrors.add(mErrors.WARNING_UNKNOWN_COMMAND_LINE_ERROR,
"Unknown error code: " + code);
return;
}
if (!category.isLevelSettable()) {
mErrors.add(mErrors.ERROR_COMMAND_LINE, "Can't set level for error " + code);
return;
}
category.setLevel(level);
}
}
/**
* Parse the arguments and return an options object.
* <p>
* Updates errors with the hidden / warning / error levels.
* <p>
* Adds errors encountered to Errors object.
*/
public static Options parse(Errors errors, String[] args) {
return (new Parser(errors, args)).parse();
}
}

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;
/**
* Position in a source file.
*/
public class Position implements Comparable<Position> {
/**
* Sentinel line number for when there is no known line number.
*/
public static final int NO_LINE = -1;
private final String mFile;
private final int mLine;
public Position() {
mFile = null;
mLine = NO_LINE;
}
public Position(String file) {
mFile = file;
mLine = NO_LINE;
}
public Position(String file, int line) {
if (line < NO_LINE) {
throw new IllegalArgumentException("Negative line number. file=" + file
+ " line=" + line);
}
mFile = file;
mLine = line;
}
public int compareTo(Position that) {
int result = mFile.compareTo(that.mFile);
if (result != 0) {
return result;
}
return mLine - that.mLine;
}
public String getFile() {
return mFile;
}
public int getLine() {
return mLine;
}
@Override
public String toString() {
if (mFile == null && mLine == NO_LINE) {
return "";
} else if (mFile == null && mLine != NO_LINE) {
return "<unknown>:" + mLine + ": ";
} else if (mFile != null && mLine == NO_LINE) {
return mFile + ": ";
} else { // if (mFile != null && mLine != NO_LINE)
return mFile + ':' + mLine + ": ";
}
}
}

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 org.junit.Assert;
import org.junit.Test;
import java.util.HashSet;
import java.util.List;
public class ErrorReporterTest {
/**
* Test that errors can be recorded and retrieved.
*/
@Test
public void testAdding() {
TestErrors errors = new TestErrors();
errors.add(errors.ERROR, new Position("a", 12), "Errrororrrr");
Assert.assertTrue(errors.hadWarningOrError());
Assert.assertTrue(errors.hadError());
List<TestErrors.Entry> entries = errors.getEntries();
Assert.assertEquals(1, entries.size());
TestErrors.Entry entry = entries.get(0);
Assert.assertEquals(errors.ERROR, entry.getCategory());
Assert.assertEquals("a", entry.getPosition().getFile());
Assert.assertEquals(12, entry.getPosition().getLine());
Assert.assertEquals("Errrororrrr", entry.getMessage());
Assert.assertNotEquals("", errors.getErrorMessages());
}
/**
* Test that not adding an error doesn't record errors.
*/
@Test
public void testNoError() {
TestErrors errors = new TestErrors();
Assert.assertFalse(errors.hadWarningOrError());
Assert.assertFalse(errors.hadError());
Assert.assertEquals("", errors.getErrorMessages());
}
/**
* Test that not adding a warning doesn't record errors.
*/
@Test
public void testWarning() {
TestErrors errors = new TestErrors();
errors.add(errors.WARNING, "Waaaaarninggggg");
Assert.assertTrue(errors.hadWarningOrError());
Assert.assertFalse(errors.hadError());
Assert.assertNotEquals("", errors.getErrorMessages());
}
/**
* Test that hidden warnings don't report.
*/
@Test
public void testHidden() {
TestErrors errors = new TestErrors();
errors.add(errors.HIDDEN, "Hidddeennn");
Assert.assertFalse(errors.hadWarningOrError());
Assert.assertFalse(errors.hadError());
Assert.assertEquals("", errors.getErrorMessages());
}
/**
* Test changing an error level.
*/
@Test
public void testSetLevel() {
TestErrors errors = new TestErrors();
Assert.assertEquals(TestErrors.Level.ERROR, errors.ERROR.getLevel());
errors.ERROR.setLevel(TestErrors.Level.WARNING);
Assert.assertEquals(TestErrors.Level.WARNING, errors.ERROR.getLevel());
}
/**
* Test that changing a fixed error fails.
*/
@Test
public void testSetLevelFails() {
TestErrors errors = new TestErrors();
Assert.assertEquals(TestErrors.Level.ERROR, errors.ERROR_FIXED.getLevel());
boolean exceptionThrown = false;
try {
errors.ERROR_FIXED.setLevel(TestErrors.Level.WARNING);
} catch (RuntimeException ex) {
exceptionThrown = true;
}
Assert.assertTrue(exceptionThrown);
Assert.assertEquals(TestErrors.Level.ERROR, errors.ERROR_FIXED.getLevel());
}
}

View File

@ -0,0 +1,114 @@
/*
* 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;
public class OptionsTest {
@Test
public void testErrorMissingLast() {
final Errors errors = new Errors();
final Options options = Options.parse(errors, new String[] {
"--error"
});
Assert.assertNotEquals("", TestErrors.getErrorMessages(errors));
Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
TestErrors.assertHasEntry(errors.ERROR_COMMAND_LINE, errors);
}
@Test
public void testErrorMissingNotLast() {
final Errors errors = new Errors();
final Options options = Options.parse(errors, new String[] {
"--error", "--warning", "2"
});
Assert.assertNotEquals("", TestErrors.getErrorMessages(errors));
Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
TestErrors.assertHasEntry(errors.ERROR_COMMAND_LINE, errors);
}
@Test
public void testErrorNotNumeric() {
final Errors errors = new Errors();
final Options options = Options.parse(errors, new String[] {
"--error", "notgood"
});
Assert.assertNotEquals("", TestErrors.getErrorMessages(errors));
Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
TestErrors.assertHasEntry(errors.ERROR_COMMAND_LINE, errors);
}
@Test
public void testErrorInvalidError() {
final Errors errors = new Errors();
final Options options = Options.parse(errors, new String[] {
"--error", "50000"
});
Assert.assertEquals("", TestErrors.getErrorMessages(errors));
Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
TestErrors.assertHasEntry(errors.WARNING_UNKNOWN_COMMAND_LINE_ERROR, errors);
}
@Test
public void testErrorOne() {
final Errors errors = new Errors();
final Options options = Options.parse(errors, new String[] {
"--error", "2"
});
Assert.assertEquals("", TestErrors.getErrorMessages(errors));
Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
Assert.assertFalse(errors.hadWarningOrError());
}
@Test
public void testWarningOne() {
final Errors errors = new Errors();
final Options options = Options.parse(errors, new String[] {
"--warning", "2"
});
Assert.assertEquals("", TestErrors.getErrorMessages(errors));
Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
Assert.assertFalse(errors.hadWarningOrError());
}
@Test
public void testHideOne() {
final Errors errors = new Errors();
final Options options = Options.parse(errors, new String[] {
"--hide", "2"
});
Assert.assertEquals("", TestErrors.getErrorMessages(errors));
Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
Assert.assertFalse(errors.hadWarningOrError());
}
}

View File

@ -0,0 +1,80 @@
/*
* 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.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
/**
* Errors for testing.
*/
public class TestErrors extends ErrorReporter {
public static final int ERROR_CODE = 1;
public final Category ERROR = new Category(ERROR_CODE, true, Level.ERROR,
"An error.");
public static final int WARNING_CODE = 2;
public final Category WARNING = new Category(WARNING_CODE, true, Level.WARNING,
"A warning.");
public static final int HIDDEN_CODE = 3;
public final Category HIDDEN = new Category(HIDDEN_CODE, true, Level.HIDDEN,
"A hidden warning.");
public static final int ERROR_FIXED_CODE = 4;
public final Category ERROR_FIXED = new Category(ERROR_FIXED_CODE, false, Level.ERROR,
"An error that can't have its level changed.");
public void assertHasEntry(Errors.Category category) {
assertHasEntry(category, this);
}
public String getErrorMessages() {
return getErrorMessages(this);
}
public static void assertHasEntry(Errors.Category category, ErrorReporter errors) {
StringBuilder found = new StringBuilder();
for (Errors.Entry entry: errors.getEntries()) {
if (entry.getCategory() == category) {
return;
}
found.append(' ');
found.append(entry.getCategory().getCode());
}
throw new AssertionError("No error category " + category.getCode() + " found."
+ " Found category codes were:" + found);
}
public static String getErrorMessages(ErrorReporter errors) {
final ByteArrayOutputStream stream = new ByteArrayOutputStream();
try {
errors.printErrors(new PrintStream(stream, true, StandardCharsets.UTF_8.name()));
} catch (UnsupportedEncodingException ex) {
// utf-8 is always supported
}
return new String(stream.toByteArray(), StandardCharsets.UTF_8);
}
}