1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.build.config;
18 
19 import java.io.PrintStream;
20 import java.util.Map;
21 import java.util.TreeMap;
22 
23 public class Options {
24     public enum Action {
25         DEFAULT,
26         HELP
27     }
28 
29     private Action mAction = Action.DEFAULT;
30 
31     private String mProduct;
32     private String mVariant;
33     private String mOutDir;
34     private String mCKatiBin;
35 
getAction()36     public Action getAction() {
37         return mAction;
38     }
39 
getProduct()40     public String getProduct() {
41         return mProduct;
42     }
43 
getVariant()44     public String getVariant() {
45         return mVariant;
46     }
47 
getOutDir()48     public String getOutDir() {
49         return mOutDir != null ? mOutDir : "out";
50     }
51 
getCKatiBin()52     public String getCKatiBin() {
53         return mCKatiBin;
54     }
55 
printHelp(PrintStream out)56     public static void printHelp(PrintStream out) {
57         out.println("usage: product_config");
58         out.println();
59         out.println("REQUIRED FLAGS");
60         out.println("  --ckati_bin CKATI        Kati binary to use.");
61         out.println();
62         out.println("OPTIONAL FLAGS");
63         out.println("  --hide ERROR_ID          Suppress this error.");
64         out.println("  --error ERROR_ID         Make this ERROR_ID a fatal error.");
65         out.println("  --help -h                This message.");
66         out.println("  --warning ERROR_ID       Make this ERROR_ID a warning.");
67         out.println();
68         out.println("REQUIRED ENVIRONMENT");
69         out.println("  TARGET_PRODUCT           Product to build from lunch command.");
70         out.println("  TARGET_BUILD_VARIANT     Build variant from lunch command.");
71         out.println();
72         out.println("OPTIONAL ENVIRONMENT");
73         out.println("  OUT_DIR                  Build output directory. Defaults to \"out\".");
74         out.println();
75         out.println("ERRORS");
76         out.println("  The following are the errors that can be controlled on the");
77         out.println("  commandline with the --hide --warning --error flags.");
78 
79         TreeMap<Integer,Errors.Category> sorted = new TreeMap((new Errors()).getCategories());
80 
81         for (final Errors.Category category: sorted.values()) {
82             if (category.isLevelSettable()) {
83                 out.println(String.format("    %-3d      %s", category.getCode(),
84                 category.getHelp().replace("\n", "\n             ")));
85             }
86         }
87     }
88 
89     static class Parser {
90         private static class ParseException extends Exception {
ParseException(String message)91             public ParseException(String message) {
92                 super(message);
93             }
94         }
95 
96         private Errors mErrors;
97         private String[] mArgs;
98         private Map<String,String> mEnv;
99         private Options mResult = new Options();
100         private int mIndex;
101         private boolean mSkipRequiredArgValidation;
102 
Parser(Errors errors, String[] args, Map<String,String> env)103         public Parser(Errors errors, String[] args, Map<String,String> env) {
104             mErrors = errors;
105             mArgs = args;
106             mEnv = env;
107         }
108 
parse()109         public Options parse() {
110             // Args
111             try {
112                 while (mIndex < mArgs.length) {
113                     final String arg = mArgs[mIndex];
114 
115                     if ("--ckati_bin".equals(arg)) {
116                         mResult.mCKatiBin = requireNextStringArg(arg);
117                     } else if ("--hide".equals(arg)) {
118                         handleErrorCode(arg, Errors.Level.HIDDEN);
119                     } else if ("--error".equals(arg)) {
120                         handleErrorCode(arg, Errors.Level.ERROR);
121                     } else if ("--help".equals(arg) || "-h".equals(arg)) {
122                         // Help overrides all other commands if there isn't an error, but
123                         // we will stop here.
124                         if (!mErrors.hadError()) {
125                             mResult.mAction = Action.HELP;
126                         }
127                         return mResult;
128                     } else if ("--warning".equals(arg)) {
129                         handleErrorCode(arg, Errors.Level.WARNING);
130                     } else {
131                         throw new ParseException("Unknown command line argument: " + arg);
132                     }
133 
134                     mIndex++;
135                 }
136             } catch (ParseException ex) {
137                 mErrors.ERROR_COMMAND_LINE.add(ex.getMessage());
138             }
139 
140             // Environment
141             mResult.mProduct = mEnv.get("TARGET_PRODUCT");
142             mResult.mVariant = mEnv.get("TARGET_BUILD_VARIANT");
143             mResult.mOutDir = mEnv.get("OUT_DIR");
144 
145             validateArgs();
146 
147             return mResult;
148         }
149 
150         /**
151          * For testing; don't generate errors about missing arguments
152          */
setSkipRequiredArgValidation()153         public void setSkipRequiredArgValidation() {
154             mSkipRequiredArgValidation = true;
155         }
156 
validateArgs()157         private void validateArgs() {
158             if (!mSkipRequiredArgValidation) {
159                 if (mResult.mCKatiBin == null || "".equals(mResult.mCKatiBin)) {
160                     addMissingArgError("--ckati_bin");
161                 }
162                 if (mResult.mProduct == null) {
163                     addMissingEnvError("TARGET_PRODUCT");
164                 }
165                 if (mResult.mVariant == null) {
166                     addMissingEnvError("TARGET_BUILD_VARIANT");
167                 }
168             }
169         }
170 
addMissingArgError(String argName)171         private void addMissingArgError(String argName) {
172             mErrors.ERROR_COMMAND_LINE.add("Required command line argument missing: "
173                     + argName);
174         }
175 
addMissingEnvError(String envName)176         private void addMissingEnvError(String envName) {
177             mErrors.ERROR_COMMAND_LINE.add("Required environment variable missing: "
178                     + envName);
179         }
180 
getNextNonFlagArg()181         private String getNextNonFlagArg() {
182             if (mIndex == mArgs.length - 1) {
183                 return null;
184             }
185             if (mArgs[mIndex + 1].startsWith("-")) {
186                 return null;
187             }
188             mIndex++;
189             return mArgs[mIndex];
190         }
191 
requireNextStringArg(String arg)192         private String requireNextStringArg(String arg) throws ParseException {
193             final String val = getNextNonFlagArg();
194             if (val == null) {
195                 throw new ParseException(arg + " requires a string argument.");
196             }
197             return val;
198         }
199 
requireNextNumberArg(String arg)200         private int requireNextNumberArg(String arg) throws ParseException {
201             final String val = getNextNonFlagArg();
202             if (val == null) {
203                 throw new ParseException(arg + " requires a numeric argument.");
204             }
205             try {
206                 return Integer.parseInt(val);
207             } catch (NumberFormatException ex) {
208                 throw new ParseException(arg + " requires a numeric argument. found: " + val);
209             }
210         }
211 
handleErrorCode(String arg, Errors.Level level)212         private void handleErrorCode(String arg, Errors.Level level) throws ParseException {
213             final int code = requireNextNumberArg(arg);
214             final Errors.Category category = mErrors.getCategories().get(code);
215             if (category == null) {
216                 mErrors.WARNING_UNKNOWN_COMMAND_LINE_ERROR.add("Unknown error code: " + code);
217                 return;
218             }
219             if (!category.isLevelSettable()) {
220                 mErrors.ERROR_COMMAND_LINE.add("Can't set level for error " + code);
221                 return;
222             }
223             category.setLevel(level);
224         }
225     }
226 
227     /**
228      * Parse the arguments and return an options object.
229      * <p>
230      * Updates errors with the hidden / warning / error levels.
231      * <p>
232      * Adds errors encountered to Errors object.
233      */
parse(Errors errors, String[] args, Map<String, String> env)234     public static Options parse(Errors errors, String[] args, Map<String, String> env) {
235         return (new Parser(errors, args, env)).parse();
236     }
237 }
238