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