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.util.ArrayList;
20 import java.util.HashMap;
21 import java.util.HashSet;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Set;
25 import java.util.TreeMap;
26 import java.util.TreeSet;
27 import java.util.regex.Pattern;
28 
29 public class FlattenConfig {
30     private static final Pattern RE_SPACE = Pattern.compile("\\p{Space}+");
31     private static final String PRODUCTS_PREFIX = "PRODUCTS";
32 
33     private final Errors mErrors;
34     private final GenericConfig mGenericConfig;
35     private final Map<String, GenericConfig.ConfigFile> mGenericConfigs;
36     private final FlatConfig mResult = new FlatConfig();
37     private final Map<String, Value> mVariables;
38     /**
39      * Files that have been visited, to prevent infinite recursion. There are no
40      * conditionals at this point in the processing, so we don't need a stack, just
41      * a single set.
42      */
43     private final Set<Str> mStack = new HashSet();
44 
45 
FlattenConfig(Errors errors, GenericConfig genericConfig)46     private FlattenConfig(Errors errors, GenericConfig genericConfig) {
47         mErrors = errors;
48         mGenericConfig = genericConfig;
49         mGenericConfigs = genericConfig.getFiles();
50         mVariables = mResult.getValues();
51 
52         // Base class fields
53         mResult.copyFrom(genericConfig);
54     }
55 
56     /**
57      * Flatten a GenericConfig to a FlatConfig.
58      *
59      * Makes three passes through the genericConfig, one to flatten the single variables,
60      * one to flatten the list variables, and one to flatten the unknown variables. Each
61      * has a slightly different algorithm.
62      */
flatten(Errors errors, GenericConfig genericConfig)63     public static FlatConfig flatten(Errors errors, GenericConfig genericConfig) {
64         final FlattenConfig flattener = new FlattenConfig(errors, genericConfig);
65         return flattener.flattenImpl();
66     }
67 
flattenImpl()68     private FlatConfig flattenImpl() {
69         final List<String> rootNodes = mGenericConfig.getRootNodes();
70         if (rootNodes.size() == 0) {
71             mErrors.ERROR_DUMPCONFIG.add("No root nodes in PRODUCTS phase.");
72             return null;
73         } else if (rootNodes.size() != 1) {
74             final StringBuilder msg = new StringBuilder(
75                     "Ignoring extra root nodes in PRODUCTS phase. All nodes are:");
76             for (final String rn: rootNodes) {
77                 msg.append(' ');
78                 msg.append(rn);
79             }
80             mErrors.WARNING_DUMPCONFIG.add(msg.toString());
81         }
82         final String root = rootNodes.get(0);
83 
84         // TODO: Do we need to worry about the initial state of variables? Anything
85         // that from the product config
86 
87         flattenListVars(root);
88         flattenSingleVars(root);
89         flattenUnknownVars(root);
90         flattenInheritsFrom(root);
91 
92         setDefaultKnownVars();
93 
94         // TODO: This only supports the single product mode of import-nodes, which is all the
95         // real build does. m product-graph and friends will have to be rewritten.
96         mVariables.put("PRODUCTS", new Value(VarType.UNKNOWN, new Str(root)));
97 
98         return mResult;
99     }
100 
101     interface AssignCallback {
onAssignStatement(GenericConfig.Assign assign)102         void onAssignStatement(GenericConfig.Assign assign);
103     }
104 
105     interface InheritCallback {
onInheritStatement(GenericConfig.Inherit assign)106         void onInheritStatement(GenericConfig.Inherit assign);
107     }
108 
109     /**
110      * Do a bunch of validity checks, and then iterate through each of the statements
111      * in the given file.  For Assignments, the callback is only called for variables
112      * matching varType.
113      *
114      * Adds makefiles which have been traversed to the 'seen' set, and will not traverse
115      * into an inherit statement if its makefile has already been seen.
116      */
forEachStatement(Str filename, VarType varType, Set<String> seen, AssignCallback assigner, InheritCallback inheriter)117     private void forEachStatement(Str filename, VarType varType, Set<String> seen,
118             AssignCallback assigner, InheritCallback inheriter) {
119         if (mStack.contains(filename)) {
120             mErrors.ERROR_INFINITE_RECURSION.add(filename.getPosition(),
121                     "File is already in the inherit-product stack: " + filename);
122             return;
123         }
124 
125         mStack.add(filename);
126         try {
127             final GenericConfig.ConfigFile genericFile = mGenericConfigs.get(filename.toString());
128 
129             if (genericFile == null) {
130                 mErrors.ERROR_MISSING_CONFIG_FILE.add(filename.getPosition(),
131                         "Unable to find config file: " + filename);
132                 return;
133             }
134 
135             for (final GenericConfig.Statement statement: genericFile.getStatements()) {
136                 if (statement instanceof GenericConfig.Assign) {
137                     if (assigner != null) {
138                         final GenericConfig.Assign assign = (GenericConfig.Assign)statement;
139                         final String varName = assign.getName();
140 
141                         // Assert that we're not stomping on another variable, which
142                         // really should be impossible at this point.
143                         assertVarType(filename, varName);
144 
145                         if (mGenericConfig.getVarType(varName) == varType) {
146                             assigner.onAssignStatement(assign);
147                         }
148                     }
149                 } else if (statement instanceof GenericConfig.Inherit) {
150                     if (inheriter != null) {
151                         final GenericConfig.Inherit inherit = (GenericConfig.Inherit)statement;
152                         if (seen != null) {
153                             if (seen.contains(inherit.getFilename().toString())) {
154                                 continue;
155                             }
156                             seen.add(inherit.getFilename().toString());
157                         }
158                         inheriter.onInheritStatement(inherit);
159                     }
160                 }
161             }
162         } finally {
163             // Also executes after return statements, so we always remove this.
164             mStack.remove(filename);
165         }
166     }
167 
168     /**
169      * Call 'inheriter' for each child of 'filename' in alphabetical order.
170      */
forEachInheritAlpha(final Str filename, VarType varType, Set<String> seen, InheritCallback inheriter)171     private void forEachInheritAlpha(final Str filename, VarType varType, Set<String> seen,
172             InheritCallback inheriter) {
173         final TreeMap<Str, GenericConfig.Inherit> alpha = new TreeMap();
174         forEachStatement(filename, varType, null, null,
175                 (inherit) -> {
176                     alpha.put(inherit.getFilename(), inherit);
177                 });
178         for (final GenericConfig.Inherit inherit: alpha.values()) {
179             // Handle 'seen' here where we actaully call back, not before, so that
180             // the proper traversal order is preserved.
181             if (seen != null) {
182                 if (seen.contains(inherit.getFilename().toString())) {
183                     continue;
184                 }
185                 seen.add(inherit.getFilename().toString());
186             }
187             inheriter.onInheritStatement(inherit);
188         }
189     }
190 
191     /**
192      * Traverse the inheritance hierarchy, setting list-value product config variables.
193      */
flattenListVars(final String filename)194     private void flattenListVars(final String filename) {
195         Map<String, Value> vars = flattenListVars(new Str(filename), new HashSet());
196         // Add the result of the recursion to mVariables. We know there will be
197         // no collisions because this function only handles list variables.
198         for (Map.Entry<String, Value> entry: vars.entrySet()) {
199             mVariables.put(entry.getKey(), entry.getValue());
200         }
201     }
202 
203     /**
204      * Return the variables defined, recursively, by 'filename.' The 'seen' set
205      * accumulates which nodes have been visited, as each is only done once.
206      *
207      * This convoluted algorithm isn't ideal, but it matches what is in node_fns.mk.
208      */
flattenListVars(final Str filename, Set<String> seen)209     private Map<String, Value> flattenListVars(final Str filename, Set<String> seen) {
210         Map<String, Value> result = new HashMap();
211 
212         // Recurse into our children first in alphabetical order, building a map of
213         // that filename to its flattened values.  The order matters here because
214         // we will only look at each child once, and when a file appears multiple
215         // times, its variables must have the right set, based on whether it's been
216         // seen before. This preserves the order from node_fns.mk.
217 
218         // Child filename --> { varname --> value }
219         final Map<Str, Map<String, Value>> children = new HashMap();
220         forEachInheritAlpha(filename, VarType.LIST, seen,
221                 (inherit) -> {
222                     final Str child = inherit.getFilename();
223                     children.put(child, flattenListVars(child, seen));
224                 });
225 
226         // Now, traverse the values again in the original source order to concatenate the values.
227         // Note that the contcatenation order is *different* from the inherit order above.
228         forEachStatement(filename, VarType.LIST, null,
229                 (assign) -> {
230                     assignToListVar(result, assign.getName(), assign.getValue());
231                 },
232                 (inherit) -> {
233                     final Map<String, Value> child = children.get(inherit.getFilename());
234                     // child == null happens if this node has been visited before.
235                     if (child != null) {
236                         for (Map.Entry<String, Value> entry: child.entrySet()) {
237                             final String varName = entry.getKey();
238                             final Value varVal = entry.getValue();
239                             appendToListVar(result, varName, varVal.getList());
240                         }
241                     }
242                 });
243 
244         return result;
245     }
246 
247     /**
248      * Traverse the inheritance hierarchy, setting single-value product config variables.
249      */
flattenSingleVars(final String filename)250     private void flattenSingleVars(final String filename) {
251         flattenSingleVars(new Str(filename), new HashSet(), new HashSet());
252     }
253 
flattenSingleVars(final Str filename, Set<String> seen1, Set<String> seen2)254     private void flattenSingleVars(final Str filename, Set<String> seen1, Set<String> seen2) {
255         // flattenSingleVars has two loops.  The first sets all variables that are
256         // defined for *this* file.  The second traverses through the inheritance,
257         // to fill in values that weren't defined in this file.  The first appearance of
258         // the variable is the one that wins.
259 
260         forEachStatement(filename, VarType.SINGLE, seen1,
261                 (assign) -> {
262                     final String varName = assign.getName();
263                     Value v = mVariables.get(varName);
264                     // Only take the first value that we see for single variables.
265                     Value value = mVariables.get(varName);
266                     if (!mVariables.containsKey(varName)) {
267                         final List<Str> valueList = assign.getValue();
268                         // There should never be more than one item in this list, because
269                         // SINGLE values should never be appended to.
270                         if (valueList.size() != 1) {
271                             final StringBuilder positions = new StringBuilder("[");
272                             for (Str s: valueList) {
273                                 positions.append(s.getPosition());
274                             }
275                             positions.append(" ]");
276                             throw new RuntimeException("Value list found for SINGLE variable "
277                                     + varName + " size=" + valueList.size()
278                                     + "positions=" + positions.toString());
279                         }
280                         mVariables.put(varName,
281                                 new Value(VarType.SINGLE,
282                                     valueList.get(0)));
283                     }
284                 }, null);
285 
286         forEachInheritAlpha(filename, VarType.SINGLE, seen2,
287                 (inherit) -> {
288                     flattenSingleVars(inherit.getFilename(), seen1, seen2);
289                 });
290     }
291 
292     /**
293      * Traverse the inheritance hierarchy and flatten the values
294      */
flattenUnknownVars(String filename)295     private void flattenUnknownVars(String filename) {
296         flattenUnknownVars(new Str(filename), new HashSet());
297     }
298 
flattenUnknownVars(final Str filename, Set<String> seen)299     private void flattenUnknownVars(final Str filename, Set<String> seen) {
300         // flattenUnknownVars has two loops: First to attempt to set the variable from
301         // this file, and then a second loop to handle the inheritance.  This is odd
302         // but it matches the order the files are included in node_fns.mk. The last appearance
303         // of the value is the one that wins.
304 
305         forEachStatement(filename, VarType.UNKNOWN, null,
306                 (assign) -> {
307                     // Overwrite the current value with whatever is now in the file.
308                     mVariables.put(assign.getName(),
309                             new Value(VarType.UNKNOWN,
310                                 flattenAssignList(assign, new Str(""))));
311                 }, null);
312 
313         forEachInheritAlpha(filename, VarType.UNKNOWN, seen,
314                 (inherit) -> {
315                     flattenUnknownVars(inherit.getFilename(), seen);
316                 });
317     }
318 
319     String prefix = "";
320 
321     /**
322      * Sets the PRODUCTS.<filename>.INHERITS_FROM variables.
323      */
flattenInheritsFrom(final String filename)324     private void flattenInheritsFrom(final String filename) {
325         flattenInheritsFrom(new Str(filename));
326     }
327 
328     /**
329      * This flatten function, unlike the others visits all of the nodes regardless
330      * of whether they have been seen before, because that's what the make code does.
331      */
flattenInheritsFrom(final Str filename)332     private void flattenInheritsFrom(final Str filename) {
333         // Recurse, and gather the list our chlidren
334         final TreeSet<Str> children = new TreeSet();
335         forEachStatement(filename, VarType.LIST, null, null,
336                 (inherit) -> {
337                     children.add(inherit.getFilename());
338                     flattenInheritsFrom(inherit.getFilename());
339                 });
340 
341         final String varName = "PRODUCTS." + filename + ".INHERITS_FROM";
342         if (children.size() > 0) {
343             // Build the space separated list.
344             boolean first = true;
345             final StringBuilder val = new StringBuilder();
346             for (Str child: children) {
347                 if (first) {
348                     first = false;
349                 } else {
350                     val.append(' ');
351                 }
352                 val.append(child);
353             }
354             mVariables.put(varName, new Value(VarType.UNKNOWN, new Str(val.toString())));
355         } else {
356             // Clear whatever flattenUnknownVars happened to have put in.
357             mVariables.remove(varName);
358         }
359     }
360 
361     /**
362      * Throw an exception if there's an existing variable with a different type.
363      */
assertVarType(Str filename, String varName)364     private void assertVarType(Str filename, String varName) {
365         if (mGenericConfig.getVarType(varName) == VarType.UNKNOWN) {
366             final Value prevValue = mVariables.get(varName);
367             if (prevValue != null
368                     && prevValue.getVarType() != VarType.UNKNOWN) {
369                 throw new RuntimeException("Mismatched var types:"
370                         + " filename=" + filename
371                         + " varType=" + mGenericConfig.getVarType(varName)
372                         + " varName=" + varName
373                         + " prevValue=" + Value.debugString(prevValue));
374             }
375         }
376     }
377 
378     /**
379      * Depending on whether the assignment is prepending, appending, setting, etc.,
380      * update the value.  We can infer which of those operations it is by the length
381      * and contents of the values. Each value in the list was originally separated
382      * by the previous value.
383      */
assignToListVar(Map<String, Value> vars, String varName, List<Str> items)384     private void assignToListVar(Map<String, Value> vars, String varName, List<Str> items) {
385         final Value value = vars.get(varName);
386         final List<Str> orig = value == null ? new ArrayList() : value.getList();
387         final List<Str> result = new ArrayList();
388         if (items.size() > 0) {
389             for (int i = 0; i < items.size(); i++) {
390                 if (i != 0) {
391                     result.addAll(orig);
392                 }
393                 final Str item = items.get(i);
394                 addWords(result, item);
395             }
396         }
397         vars.put(varName, new Value(result));
398     }
399 
400     /**
401      * Appends all of the words in in 'items' to an entry in vars keyed by 'varName',
402      * creating one if necessary.
403      */
appendToListVar(Map<String, Value> vars, String varName, List<Str> items)404     private static void appendToListVar(Map<String, Value> vars, String varName, List<Str> items) {
405         Value value = vars.get(varName);
406         if (value == null) {
407             value = new Value(new ArrayList());
408             vars.put(varName, value);
409         }
410         final List<Str> out = value.getList();
411         for (Str item: items) {
412             addWords(out, item);
413         }
414     }
415 
416     /**
417      * Split 'item' on spaces, and add each of them as a word to 'out'.
418      */
addWords(List<Str> out, Str item)419     private static void addWords(List<Str> out, Str item) {
420         for (String word: RE_SPACE.split(item.toString().trim())) {
421             if (word.length() > 0) {
422                 out.add(new Str(item.getPosition(), word));
423             }
424         }
425     }
426 
427     /**
428      * Flatten the list of strings in an Assign statement, using the previous value
429      * as a separator.
430      */
flattenAssignList(GenericConfig.Assign assign, Str previous)431     private Str flattenAssignList(GenericConfig.Assign assign, Str previous) {
432         final StringBuilder result = new StringBuilder();
433         Position position = previous.getPosition();
434         final List<Str> list = assign.getValue();
435         final int size = list.size();
436         for (int i = 0; i < size; i++) {
437             final Str item = list.get(i);
438             result.append(item.toString());
439             if (i != size - 1) {
440                 result.append(previous);
441             }
442             final Position pos = item.getPosition();
443             if (pos != null && pos.getFile() != null) {
444                 position = pos;
445             }
446         }
447         return new Str(position, result.toString());
448     }
449 
450     /**
451      * Make sure that each of the product config variables has a default value.
452      */
setDefaultKnownVars()453     private void setDefaultKnownVars() {
454         for (Map.Entry<String, VarType> entry: mGenericConfig.getProductVars().entrySet()) {
455             final String varName = entry.getKey();
456             final VarType varType = entry.getValue();
457 
458             final Value val = mVariables.get(varName);
459             if (val == null) {
460                 mVariables.put(varName, new Value(varType));
461             }
462         }
463 
464 
465         // TODO: These two for now as well, until we can rewrite the enforce packages exist
466         // handling.
467         if (!mVariables.containsKey("PRODUCT_ENFORCE_PACKAGES_EXIST")) {
468             mVariables.put("PRODUCT_ENFORCE_PACKAGES_EXIST", new Value(VarType.UNKNOWN));
469         }
470         if (!mVariables.containsKey("PRODUCT_ENFORCE_PACKAGES_EXIST_ALLOW_LIST")) {
471             mVariables.put("PRODUCT_ENFORCE_PACKAGES_EXIST_ALLOW_LIST", new Value(VarType.UNKNOWN));
472         }
473     }
474 }
475