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 
18 package com.android.build.config;
19 
20 import java.io.IOException;
21 import java.io.Reader;
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Map;
29 import java.util.regex.Pattern;
30 
31 /**
32  * Parses the output of ckati building build/make/core/dumpconfig.mk.
33  *
34  * The format is as follows:
35  *   - All processed lines are colon (':') separated fields.
36  *   - Lines before the dumpconfig_version line are dropped for forward compatibility
37  *   - Lines where the first field is config_var describe variables declared in makefiles
38  *     (implemented by the dump-config-vals macro)
39  *          Field   Description
40  *          0       "config_var" row type
41  *          1       Product makefile being processed
42  *          2       The variable name
43  *          3       The value of the variable
44  *          4       The location of the variable, as best tracked by kati
45  */
46 public class DumpConfigParser {
47     private static final boolean DEBUG = false;
48 
49     private final Errors mErrors;
50     private final String mFilename;
51     private final Reader mReader;
52 
53     private final Map<String,MakeConfig> mResults = new HashMap();
54 
55     private static final Pattern LIST_SEPARATOR = Pattern.compile("\\s+");
56 
57     /**
58      * Constructor.
59      */
DumpConfigParser(Errors errors, String filename, Reader reader)60     private DumpConfigParser(Errors errors, String filename, Reader reader) {
61         mErrors = errors;
62         mFilename = filename;
63         mReader = reader;
64     }
65 
66     /**
67      * Parse the text into a map of the phase names to MakeConfig objects.
68      */
parse(Errors errors, String filename, Reader reader)69     public static Map<String,MakeConfig> parse(Errors errors, String filename, Reader reader)
70             throws CsvParser.ParseException, IOException {
71         DumpConfigParser parser = new DumpConfigParser(errors, filename, reader);
72         parser.parseImpl();
73         return parser.mResults;
74     }
75 
76     /**
77      * Parse the input.
78      */
parseImpl()79     private void parseImpl() throws CsvParser.ParseException, IOException {
80         final List<CsvParser.Line> lines = CsvParser.parse(mReader);
81         final int lineCount = lines.size();
82         int index = 0;
83 
84         int dumpconfigVersion = 0;
85 
86         // Ignore lines until until we get a dumpconfig_version line for forward compatibility.
87         // In a previous life, this loop parsed from all of kati's stdout, not just the file
88         // that dumpconfig.mk writes, but it's harmless to leave this loop in.  It gives us a
89         // little bit of flexibility which we probably won't need anyway, this tool probably
90         // won't diverge from dumpconfig.mk anyway.
91         for (; index < lineCount; index++) {
92             final CsvParser.Line line = lines.get(index);
93             final List<String> fields = line.getFields();
94 
95             if (matchLineType(line, "dumpconfig_version", 1)) {
96                 try {
97                     dumpconfigVersion = Integer.parseInt(fields.get(1));
98                 } catch (NumberFormatException ex) {
99                     mErrors.WARNING_DUMPCONFIG.add(
100                             new Position(mFilename, line.getLine()),
101                             "Couldn't parse dumpconfig_version: " + fields.get(1));
102                 }
103                 break;
104             }
105         }
106 
107         // If we never saw dumpconfig_version, there's a problem with the command, so stop.
108         if (dumpconfigVersion == 0) {
109             mErrors.ERROR_DUMPCONFIG.fatal(
110                     new Position(mFilename),
111                     "Never saw a valid dumpconfig_version line.");
112         }
113 
114         // Any lines before the start signal will be dropped. We create garbage objects
115         // here to avoid having to check for null everywhere.
116         MakeConfig makeConfig = new MakeConfig();
117         MakeConfig.ConfigFile configFile = new MakeConfig.ConfigFile("<ignored>");
118         MakeConfig.Block block = new MakeConfig.Block(MakeConfig.BlockType.UNSET);
119         Map<String, Str> initialVariables = new HashMap();
120         Map<String, Str> finalVariables = new HashMap();
121 
122         // Number of "phases" we've seen so far.
123         for (; index < lineCount; index++) {
124             final CsvParser.Line line = lines.get(index);
125             final List<String> fields = line.getFields();
126             final String lineType = fields.get(0);
127 
128             if (matchLineType(line, "phase", 2)) {
129                 // Start the new one
130                 makeConfig = new MakeConfig();
131                 makeConfig.setPhase(fields.get(1));
132                 makeConfig.setRootNodes(splitList(fields.get(2)));
133                 // If there is a duplicate phase of the same name, continue parsing, but
134                 // don't add it.  Emit a warning.
135                 if (!mResults.containsKey(makeConfig.getPhase())) {
136                     mResults.put(makeConfig.getPhase(), makeConfig);
137                 } else {
138                     mErrors.WARNING_DUMPCONFIG.add(
139                             new Position(mFilename, line.getLine()),
140                             "Duplicate phase: " + makeConfig.getPhase()
141                                 + ". This one will be dropped.");
142                 }
143                 initialVariables = makeConfig.getInitialVariables();
144                 finalVariables = makeConfig.getFinalVariables();
145 
146                 if (DEBUG) {
147                     System.out.println("PHASE:");
148                     System.out.println("  " + makeConfig.getPhase());
149                     System.out.println("  " + makeConfig.getRootNodes());
150                 }
151             } else if (matchLineType(line, "var", 2)) {
152                 final VarType type = "list".equals(fields.get(1)) ? VarType.LIST : VarType.SINGLE;
153                 makeConfig.addProductVar(fields.get(2), type);
154 
155                 if (DEBUG) {
156                     System.out.println("  VAR: " + type + " " + fields.get(2));
157                 }
158             } else if (matchLineType(line, "import", 1)) {
159                 final List<String> importStack = splitList(fields.get(1));
160                 if (importStack.size() == 0) {
161                     mErrors.WARNING_DUMPCONFIG.add(
162                             new Position(mFilename, line.getLine()),
163                             "'import' line with empty include stack.");
164                     continue;
165                 }
166 
167                 // The beginning of importing a new file.
168                 configFile = new MakeConfig.ConfigFile(importStack.get(0));
169                 if (makeConfig.addConfigFile(configFile) != null) {
170                     mErrors.WARNING_DUMPCONFIG.add(
171                             new Position(mFilename, line.getLine()),
172                             "Duplicate file imported in section: " + configFile.getFilename());
173                 }
174                 // We expect a Variable block next.
175                 block = new MakeConfig.Block(MakeConfig.BlockType.BEFORE);
176                 configFile.addBlock(block);
177 
178                 if (DEBUG) {
179                     System.out.println("  IMPORT: " + configFile.getFilename());
180                 }
181             } else if (matchLineType(line, "inherit", 2)) {
182                 final String currentFile = fields.get(1);
183                 final String inheritedFile = fields.get(2);
184                 if (!configFile.getFilename().equals(currentFile)) {
185                     mErrors.WARNING_DUMPCONFIG.add(
186                             new Position(mFilename, line.getLine()),
187                             "Unexpected current file in 'inherit' line '" + currentFile
188                                 + "' while processing '" + configFile.getFilename() + "'");
189                     continue;
190                 }
191 
192                 // There is already a file in progress, so add another var block to that.
193                 block = new MakeConfig.Block(MakeConfig.BlockType.INHERIT);
194                 // TODO: Make dumpconfig.mk also output a Position for inherit-product
195                 block.setInheritedFile(new Str(inheritedFile));
196                 configFile.addBlock(block);
197 
198                 if (DEBUG) {
199                     System.out.println("  INHERIT: " + inheritedFile);
200                 }
201             } else if (matchLineType(line, "imported", 1)) {
202                 final List<String> importStack = splitList(fields.get(1));
203                 if (importStack.size() == 0) {
204                     mErrors.WARNING_DUMPCONFIG.add(
205                             new Position(mFilename, line.getLine()),
206                             "'imported' line with empty include stack.");
207                     continue;
208                 }
209                 final String currentFile = importStack.get(0);
210                 if (!configFile.getFilename().equals(currentFile)) {
211                     mErrors.WARNING_DUMPCONFIG.add(
212                             new Position(mFilename, line.getLine()),
213                             "Unexpected current file in 'imported' line '" + currentFile
214                                 + "' while processing '" + configFile.getFilename() + "'");
215                     continue;
216                 }
217 
218                 // There is already a file in progress, so add another var block to that.
219                 // This will be the last one, but will check that after parsing.
220                 block = new MakeConfig.Block(MakeConfig.BlockType.AFTER);
221                 configFile.addBlock(block);
222 
223                 if (DEBUG) {
224                     System.out.println("  AFTER: " + currentFile);
225                 }
226             } else if (matchLineType(line, "val", 5)) {
227                 final String productMakefile = fields.get(1);
228                 final String blockTypeString = fields.get(2);
229                 final String varName = fields.get(3);
230                 final String varValue = fields.get(4);
231                 final Position pos = Position.parse(fields.get(5));
232                 final Str str = new Str(pos, varValue);
233 
234                 if (blockTypeString.equals("initial")) {
235                     initialVariables.put(varName, str);
236                 } else if (blockTypeString.equals("final")) {
237                     finalVariables.put(varName, str);
238                 } else {
239                     if (!productMakefile.equals(configFile.getFilename())) {
240                         mErrors.WARNING_DUMPCONFIG.add(
241                                 new Position(mFilename, line.getLine()),
242                                 "Mismatched 'val' product makefile."
243                                     + " Expected: " + configFile.getFilename()
244                                     + " Saw: " + productMakefile);
245                         continue;
246                     }
247 
248                     final MakeConfig.BlockType blockType = parseBlockType(line, blockTypeString);
249                     if (blockType == null) {
250                         continue;
251                     }
252                     if (blockType != block.getBlockType()) {
253                         mErrors.WARNING_DUMPCONFIG.add(
254                                 new Position(mFilename, line.getLine()),
255                                 "Mismatched 'val' block type."
256                                     + " Expected: " + block.getBlockType()
257                                     + " Saw: " + blockType);
258                     }
259 
260                     // Add the variable to the block in progress
261                     block.addVar(varName, str);
262                 }
263             } else {
264                 if (DEBUG) {
265                     System.out.print("# ");
266                     for (int d = 0; d < fields.size(); d++) {
267                         System.out.print(fields.get(d));
268                         if (d != fields.size() - 1) {
269                             System.out.print(",");
270                         }
271                     }
272                     System.out.println();
273                 }
274             }
275         }
276     }
277 
278     /**
279      * Return true if the line type matches 'lineType' and there are at least 'fieldCount'
280      * fields (not including the first field which is the line type).
281      */
matchLineType(CsvParser.Line line, String lineType, int fieldCount)282     private boolean matchLineType(CsvParser.Line line, String lineType, int fieldCount) {
283         final List<String> fields = line.getFields();
284         if (!lineType.equals(fields.get(0))) {
285             return false;
286         }
287         if (fields.size() < (fieldCount + 1)) {
288             mErrors.WARNING_DUMPCONFIG.add(new Position(mFilename, line.getLine()),
289                     fields.get(0) + " line has " + fields.size() + " fields. Expected at least "
290                     + (fieldCount + 1) + " fields.");
291             return false;
292         }
293         return true;
294     }
295 
296     /**
297      * Split a string with space separated items (i.e. the make list format) into a List<String>.
298      */
splitList(String text)299     private static List<String> splitList(String text) {
300         // Arrays.asList returns a fixed-length List, so we copy it into an ArrayList to not
301         // propagate that surprise detail downstream.
302         return new ArrayList(Arrays.asList(LIST_SEPARATOR.split(text.trim())));
303     }
304 
305     /**
306      * Parse a BockType or issue a warning if it can't be parsed.
307      */
parseBlockType(CsvParser.Line line, String text)308     private MakeConfig.BlockType parseBlockType(CsvParser.Line line, String text) {
309         if ("before".equals(text)) {
310             return MakeConfig.BlockType.BEFORE;
311         } else if ("inherit".equals(text)) {
312             return MakeConfig.BlockType.INHERIT;
313         } else if ("after".equals(text)) {
314             return MakeConfig.BlockType.AFTER;
315         } else {
316             mErrors.WARNING_DUMPCONFIG.add(
317                     new Position(mFilename, line.getLine()),
318                     "Invalid block type: " + text);
319             return null;
320         }
321     }
322 }
323