1 /* 2 * Copyright (C) 2021 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.util.ArrayList; 20 import java.util.List; 21 import java.util.Map; 22 23 /** 24 * Converts a MakeConfig into a Generic config by applying heuristics about 25 * the types of variable assignments that we do. 26 */ 27 public class ConvertMakeToGenericConfig { 28 private final Errors mErrors; 29 ConvertMakeToGenericConfig(Errors errors)30 public ConvertMakeToGenericConfig(Errors errors) { 31 mErrors = errors; 32 } 33 convert(Map<String, MakeConfig> make)34 public GenericConfig convert(Map<String, MakeConfig> make) { 35 final GenericConfig result = new GenericConfig(); 36 37 final MakeConfig products = make.get("PRODUCTS"); 38 if (products == null) { 39 mErrors.ERROR_DUMPCONFIG.add("Could not find PRODUCTS phase in dumpconfig output."); 40 return null; 41 } 42 43 // Base class fields 44 result.copyFrom(products); 45 46 // Each file 47 for (MakeConfig.ConfigFile f: products.getConfigFiles()) { 48 final GenericConfig.ConfigFile genericFile 49 = new GenericConfig.ConfigFile(f.getFilename()); 50 result.addConfigFile(genericFile); 51 52 final List<MakeConfig.Block> blocks = f.getBlocks(); 53 54 // Some assertions: 55 // TODO: Include better context for these errors. 56 // There should always be at least a BEGIN and an AFTER, so assert this. 57 if (blocks.size() < 2) { 58 throw new RuntimeException("expected at least blocks.size() >= 2. Actcual size: " 59 + blocks.size()); 60 } 61 if (blocks.get(0).getBlockType() != MakeConfig.BlockType.BEFORE) { 62 throw new RuntimeException("expected first block to be BEFORE"); 63 } 64 if (blocks.get(blocks.size() - 1).getBlockType() != MakeConfig.BlockType.AFTER) { 65 throw new RuntimeException("expected first block to be AFTER"); 66 } 67 // Everything in between should be an INHERIT block. 68 for (int index = 1; index < blocks.size() - 1; index++) { 69 if (blocks.get(index).getBlockType() != MakeConfig.BlockType.INHERIT) { 70 throw new RuntimeException("expected INHERIT at block " + index); 71 } 72 } 73 74 // Each block represents a snapshot of the interpreter variable state (minus a few big 75 // sets of variables which we don't export because they're used in the internals 76 // of node_fns.mk, so we know they're not necessary here). The first (BEFORE) one 77 // is everything that is set before the file is included, so it forms the base 78 // for everything else. 79 MakeConfig.Block prevBlock = blocks.get(0); 80 81 for (int index = 1; index < blocks.size(); index++) { 82 final MakeConfig.Block block = blocks.get(index); 83 for (final Map.Entry<String, Str> entry: block.getVars().entrySet()) { 84 final String varName = entry.getKey(); 85 final GenericConfig.Assign assign = convertAssignment(block.getBlockType(), 86 block.getInheritedFile(), products.getVarType(varName), varName, 87 entry.getValue(), prevBlock.getVar(varName)); 88 if (assign != null) { 89 genericFile.addStatement(assign); 90 } 91 } 92 // Handle variables that are in prevBlock but not block -- they were 93 // deleted. Is this even possible, or do they show up as ""? We will 94 // treat them as positive assigments to empty string 95 for (String prevName: prevBlock.getVars().keySet()) { 96 if (!block.getVars().containsKey(prevName)) { 97 genericFile.addStatement( 98 new GenericConfig.Assign(prevName, new Str(""))); 99 } 100 } 101 if (block.getBlockType() == MakeConfig.BlockType.INHERIT) { 102 genericFile.addStatement( 103 new GenericConfig.Inherit(block.getInheritedFile())); 104 } 105 // For next iteration 106 prevBlock = block; 107 } 108 } 109 110 // Overwrite the final variables with the ones that come from the PRODUCTS-EXPAND phase. 111 // Drop the ones that were newly defined between the two phases, but leave values 112 // that were modified between. We do need to reproduce that logic in this tool. 113 final MakeConfig expand = make.get("PRODUCT-EXPAND"); 114 if (expand == null) { 115 mErrors.ERROR_DUMPCONFIG.add("Could not find PRODUCT-EXPAND phase in dumpconfig" 116 + " output."); 117 return null; 118 } 119 final Map<String, Str> productsFinal = products.getFinalVariables(); 120 final Map<String, Str> expandInitial = expand.getInitialVariables(); 121 final Map<String, Str> expandFinal = expand.getFinalVariables(); 122 final Map<String, Str> finalFinal = result.getFinalVariables(); 123 finalFinal.clear(); 124 for (Map.Entry<String, Str> var: expandFinal.entrySet()) { 125 final String varName = var.getKey(); 126 if (expandInitial.containsKey(varName) && !productsFinal.containsKey(varName)) { 127 continue; 128 } 129 finalFinal.put(varName, var.getValue()); 130 } 131 132 return result; 133 } 134 135 /** 136 * Converts one variable from a MakeConfig Block into a GenericConfig Assignment. 137 */ convertAssignment(MakeConfig.BlockType blockType, Str inheritedFile, VarType varType, String varName, Str varVal, Str prevVal)138 GenericConfig.Assign convertAssignment(MakeConfig.BlockType blockType, Str inheritedFile, 139 VarType varType, String varName, Str varVal, Str prevVal) { 140 if (prevVal == null) { 141 // New variable. 142 return new GenericConfig.Assign(varName, varVal); 143 } else if (!varVal.equals(prevVal)) { 144 // The value changed from the last block. 145 if (varVal.length() == 0) { 146 // It was set to empty 147 return new GenericConfig.Assign(varName, varVal); 148 } else { 149 // Product vars have the @inherit processing. Other vars we 150 // will just ignore and put in one section at the end, based 151 // on the difference between the BEFORE and AFTER blocks. 152 if (varType == VarType.UNKNOWN) { 153 if (blockType == MakeConfig.BlockType.AFTER) { 154 // For UNKNOWN variables, we don't worry about the 155 // intermediate steps, just take the final value. 156 return new GenericConfig.Assign(varName, varVal); 157 } else { 158 return null; 159 } 160 } else { 161 return convertInheritedVar(blockType, inheritedFile, 162 varName, varVal, prevVal); 163 } 164 } 165 } else { 166 // Variable not touched 167 return null; 168 } 169 } 170 171 /** 172 * Handle the special inherited values, where the inherit-product puts in the 173 * @inherit:... markers, adding Statements to the ConfigFile. 174 */ convertInheritedVar(MakeConfig.BlockType blockType, Str inheritedFile, String varName, Str varVal, Str prevVal)175 GenericConfig.Assign convertInheritedVar(MakeConfig.BlockType blockType, Str inheritedFile, 176 String varName, Str varVal, Str prevVal) { 177 String varText = varVal.toString(); 178 String prevText = prevVal.toString().trim(); 179 if (blockType == MakeConfig.BlockType.INHERIT) { 180 // inherit-product appends @inherit:... so drop that. 181 final String marker = "@inherit:" + inheritedFile; 182 if (varText.endsWith(marker)) { 183 varText = varText.substring(0, varText.length() - marker.length()).trim(); 184 } else { 185 mErrors.ERROR_IMPROPER_PRODUCT_VAR_MARKER.add(varVal.getPosition(), 186 "Variable didn't end with marker \"" + marker + "\": " + varText); 187 } 188 } 189 190 if (!varText.equals(prevText)) { 191 // If the variable value was actually changed. 192 final ArrayList<String> words = split(varText, prevText); 193 if (words.size() == 0) { 194 // Pure Assignment, none of the previous value is present. 195 return new GenericConfig.Assign(varName, new Str(varVal.getPosition(), varText)); 196 } else { 197 // Self referential value (prepend, append, both). 198 if (words.size() > 2) { 199 // This is indicative of a construction that might not be quite 200 // what we want. The above code will do something that works if it was 201 // of the form "VAR := a $(VAR) b $(VAR) c", but if the original code 202 // something else this won't work. This doesn't happen in AOSP, but 203 // it's a theoretically possibility, so someone might do it. 204 mErrors.WARNING_VARIABLE_RECURSION.add(varVal.getPosition(), 205 "Possible unsupported variable recursion: " 206 + varName + " = " + varVal + " (prev=" + prevVal + ")"); 207 } 208 return new GenericConfig.Assign(varName, Str.toList(varVal.getPosition(), words)); 209 } 210 } else { 211 // Variable not touched 212 return null; 213 } 214 } 215 216 /** 217 * Split 'haystack' on occurrences of 'needle'. Trims each string of whitespace 218 * to preserve make list semantics. 219 */ split(String haystack, String needle)220 private static ArrayList<String> split(String haystack, String needle) { 221 final ArrayList<String> result = new ArrayList(); 222 final int needleLen = needle.length(); 223 if (needleLen == 0) { 224 return result; 225 } 226 int start = 0; 227 int end; 228 while ((end = haystack.indexOf(needle, start)) >= 0) { 229 result.add(haystack.substring(start, end).trim()); 230 start = end + needleLen; 231 } 232 result.add(haystack.substring(start).trim()); 233 return result; 234 } 235 } 236