1 /*
2  * Copyright (C) 2012 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.idegen;
18 
19 import com.google.common.base.Objects;
20 import com.google.common.base.Preconditions;
21 import com.google.common.collect.ImmutableList;
22 import com.google.common.collect.Iterables;
23 import com.google.common.collect.Lists;
24 import com.google.common.collect.Sets;
25 import com.google.common.io.Files;
26 
27 import java.io.File;
28 import java.io.IOException;
29 import java.util.Collections;
30 import java.util.HashSet;
31 import java.util.List;
32 import java.util.Set;
33 import java.util.logging.Logger;
34 
35 /**
36  * Module constructed from a make file.
37  *
38  * TODO: read the make file and understand included source dirs in addition to searching
39  * sub-directories.  Make files can include sources that are not sub-directories.  For example, the
40  * framework module includes sources from:
41  *
42  * external/libphonenumber/java/src
43  *
44  * to provide:
45  *
46  * com.android.i18n.phonenumbers.PhoneNumberUtil;
47  */
48 public class Module {
49 
50     private static final Logger logger = Logger.getLogger(Module.class.getName());
51 
52     public static final String REL_OUT_APP_DIR = "out/target/common/obj/APPS";
53 
54     private static final String IML_TEMPLATE_FILE_NAME = "module-template.iml";
55     private static final String[] AUTO_DEPENDENCIES = new String[]{"framework", "libcore"};
56     private static final String[] DIRS_WITH_AUTO_DEPENDENCIES = new String[]{"packages", "vendor",
57             "frameworks/ex", "frameworks/opt", "frameworks/support"};
58 
59     /**
60      * All possible attributes for the make file.
61      */
62     protected enum Key {
63         LOCAL_STATIC_JAVA_LIBRARIES,
64         LOCAL_JAVA_LIBRARIES,
65         LOCAL_SRC_FILES
66     }
67 
68     private ModuleCache moduleCache = ModuleCache.getInstance();
69 
70     private File imlFile;
71     private Set<String> allDependencies = Sets.newHashSet(); // direct + indirect
72     private Set<File> allDependentImlFiles = Sets.newHashSet();
73 
74     private File makeFile;
75     private File moduleRoot;
76     private HashSet<File> sourceFiles = Sets.newHashSet();
77 
78     // Module dependencies come from LOCAL_STATIC_JAVA_LIBRARIES or LOCAL_JAVA_LIBRARIES
79     Set<String> explicitModuleNameDependencies = Sets.newHashSet();
80     // Implicit module dependencies come from src files that fall outside the module root directory.
81     // For example, if packages/apps/Contacts includes src files from packages/apps/ContactsCommon,
82     // that is an implicit module dependency.  It's not a module dependency from the build
83     // perspective but it needs to be a separate module in intellij so that the src files can be
84     // shared by multiple intellij modules.
85     Set<File> implicitModulePathDependencies = Sets.newHashSet();
86 
87     String relativeIntermediatesDir;
88     MakeFileParser makeFileParser;
89     boolean parseMakeFileForSource;
90 
Module(File moduleDir)91     public Module(File moduleDir) throws IOException {
92         this(moduleDir, true);
93     }
94 
Module(File moduleDir, boolean parseMakeFileForSource)95     public Module(File moduleDir, boolean parseMakeFileForSource) throws IOException {
96         this.moduleRoot = Preconditions.checkNotNull(moduleDir);
97         this.makeFile = new File(moduleDir, "Android.mk");
98         this.relativeIntermediatesDir = calculateRelativePartToRepoRoot() + REL_OUT_APP_DIR +
99                 File.separatorChar + getName() + "_intermediates" + File.separator + "src";
100         this.parseMakeFileForSource = parseMakeFileForSource;
101 
102         // TODO: auto-detect when framework dependency is needed instead of using coded list.
103         for (String dir : DIRS_WITH_AUTO_DEPENDENCIES) {
104             // length + 2 to account for slash
105             boolean isDir = makeFile.getCanonicalPath().startsWith(
106                     DirectorySearch.getRepoRoot() + "/" + dir);
107             if (isDir) {
108                 Collections.addAll(this.explicitModuleNameDependencies, AUTO_DEPENDENCIES);
109             }
110         }
111 
112         makeFileParser = new MakeFileParser(makeFile);
113     }
114 
calculateRelativePartToRepoRoot()115     private String calculateRelativePartToRepoRoot() throws IOException {
116         String rel = moduleRoot.getCanonicalPath().substring(
117                 DirectorySearch.getRepoRoot().getCanonicalPath().length());
118         int count = 0;
119         // Count the number of slashes to determine how far back to go.
120         for (int i = 0; i < rel.length(); i++) {
121             if (rel.charAt(i) == '/') {
122                 count++;
123             }
124         }
125         StringBuilder sb = new StringBuilder();
126         for (int i = 0; i < count; i++) {
127             sb.append("../");
128         }
129         return sb.toString();
130     }
131 
build()132     public void build() throws IOException {
133         makeFileParser.parse();
134         buildDependencyList();
135         buildDependentModules();
136         logger.info("Done building module " + moduleRoot);
137         logger.info(toString());
138     }
139 
getDir()140     public File getDir() {
141         return moduleRoot;
142     }
143 
getName()144     public String getName() {
145         return moduleRoot.getName();
146     }
147 
getRelativeIntermediatesDirs()148     private List<String> getRelativeIntermediatesDirs() throws IOException {
149         return Lists.newArrayList(relativeIntermediatesDir);
150     }
151 
getSourceDirs()152     private ImmutableList<File> getSourceDirs() {
153         return ImmutableList.copyOf(sourceFiles);
154     }
155 
getExcludeDirs()156     private ImmutableList<File> getExcludeDirs() {
157         return DirectorySearch.findExcludeDirs(makeFile);
158     }
159 
isAndroidModule()160     private boolean isAndroidModule() {
161         File manifest = new File(moduleRoot, "AndroidManifest.xml");
162         return manifest.exists();
163     }
164 
findSourceFilesAndImplicitDependencies()165     private void findSourceFilesAndImplicitDependencies() throws IOException {
166         Iterable<String> values = makeFileParser.getValues(Key.LOCAL_SRC_FILES.name());
167         if (values != null) {
168             for (String value : values) {
169                 File src = new File(moduleRoot, value);
170 
171                 // value may contain garbage at this point due to relaxed make file parsing.
172                 // filter by existing file.
173                 if (src.exists()) {
174                     // Look for directories outside the current module directory.
175                     if (value.contains("..")) {
176                         // Find the closest Android make file.
177                         File moduleRoot = DirectorySearch.findModuleRoot(src);
178                         implicitModulePathDependencies.add(moduleRoot);
179                     } else {
180                         if (parseMakeFileForSource) {
181                             // Check if source files are subdirectories of generic parent src
182                             // directories.  If so, no need to add since they are already included.
183                             boolean alreadyIncluded = false;
184                             for (String parentDir : DirectorySearch.SOURCE_DIRS) {
185                                 if (value.startsWith(parentDir)) {
186                                     alreadyIncluded = true;
187                                     break;
188                                 }
189                             }
190 
191                             if (!alreadyIncluded) {
192                                 sourceFiles.add(src);
193                             }
194                         }
195                     }
196                 }
197             }
198         }
199 
200         sourceFiles.addAll(DirectorySearch.findSourceDirs(moduleRoot));
201     }
202 
buildDependencyList()203     private void buildDependencyList() throws IOException {
204         parseDirectDependencies(Key.LOCAL_STATIC_JAVA_LIBRARIES);
205         parseDirectDependencies(Key.LOCAL_JAVA_LIBRARIES);
206         findSourceFilesAndImplicitDependencies();
207     }
208 
parseDirectDependencies(Key key)209     private void parseDirectDependencies(Key key) {
210         Iterable<String> names = makeFileParser.getValues(key.name());
211         if (names != null) {
212             for (String dependency : names) {
213                 explicitModuleNameDependencies.add(dependency);
214             }
215         }
216     }
217 
buildImlFile()218     public void buildImlFile() throws IOException {
219         String imlTemplate = Files.toString(
220                 new File(DirectorySearch.findTemplateDir(), IML_TEMPLATE_FILE_NAME),
221                 IntellijProject.CHARSET);
222 
223         String facetXml = "";
224         if (isAndroidModule()) {
225             facetXml = buildAndroidFacet();
226         }
227         imlTemplate = imlTemplate.replace("@FACETS@", facetXml);
228 
229         String moduleDir = getDir().getCanonicalPath();
230 
231         StringBuilder sourceDirectories = new StringBuilder();
232         sourceDirectories.append("    <content url=\"file://$MODULE_DIR$\">\n");
233         ImmutableList<File> srcDirs = getSourceDirs();
234         for (File src : srcDirs) {
235             String relative = src.getCanonicalPath().substring(moduleDir.length());
236             boolean isTestSource = false;
237             // This covers directories like .../test[s]/...
238             if (relative.matches(".*/tests?/.*")) {
239                 isTestSource = true;
240             }
241             sourceDirectories.append("      <sourceFolder url=\"file://$MODULE_DIR$")
242                     .append(relative).append("\" isTestSource=\"").append(isTestSource)
243                     .append("\" />\n");
244         }
245         ImmutableList<File> excludeDirs = getExcludeDirs();
246         for (File src : excludeDirs) {
247             String relative = src.getCanonicalPath().substring(moduleDir.length());
248             sourceDirectories.append("      <excludeFolder url=\"file://$MODULE_DIR$")
249                     .append(relative).append("\"/>\n");
250         }
251         sourceDirectories.append("    </content>\n");
252 
253         // Intermediates.
254         sourceDirectories.append(buildIntermediates());
255 
256         imlTemplate = imlTemplate.replace("@SOURCES@", sourceDirectories.toString());
257 
258         StringBuilder moduleDependencies = new StringBuilder();
259         for (String dependency : getAllDependencies()) {
260             Module module = moduleCache.getAndCacheByDir(new File(dependency));
261             moduleDependencies.append("    <orderEntry type=\"module\" module-name=\"")
262                     .append(module.getName()).append("\" />\n");
263         }
264         imlTemplate = imlTemplate.replace("@MODULE_DEPENDENCIES@", moduleDependencies.toString());
265 
266         imlFile = new File(moduleDir, getName() + ".iml");
267         logger.info("Creating " + imlFile.getCanonicalPath());
268         Files.write(imlTemplate, imlFile, IntellijProject.CHARSET);
269     }
270 
buildIntermediates()271     protected String buildIntermediates() throws IOException {
272         StringBuilder sb = new StringBuilder();
273         for (String intermediatesDir : getRelativeIntermediatesDirs()) {
274             sb.append("    <content url=\"file://$MODULE_DIR$/").append(intermediatesDir)
275                     .append("\">\n");
276             sb.append("      <sourceFolder url=\"file://$MODULE_DIR$/")
277                     .append(intermediatesDir)
278                     .append("\" isTestSource=\"false\" />\n");
279             sb.append("    </content>\n");
280         }
281         return sb.toString();
282     }
283 
buildDependentModules()284     private void buildDependentModules() throws IOException {
285         Set<String> moduleNameDependencies = explicitModuleNameDependencies;
286 
287         String[] copy = moduleNameDependencies.toArray(new String[moduleNameDependencies.size()]);
288         for (String dependency : copy) {
289             logger.info("Building dependency " + dependency);
290             Module child = moduleCache.getAndCacheByName(dependency);
291             if (child == null) {
292                 moduleNameDependencies.remove(dependency);
293             } else {
294                 allDependencies.add(child.getDir().getCanonicalPath());
295                 //allDependencies.addAll(child.getAllDependencies());
296                 //logger.info("Adding iml " + child.getName() + " " + child.getImlFile());
297                 allDependentImlFiles.add(child.getImlFile());
298                 //allDependentImlFiles.addAll(child.getAllDependentImlFiles());
299             }
300         }
301         // Don't include self.  The current module may have been brought in by framework
302         // dependencies which will create a circular reference.
303         allDependencies.remove(this.getDir().getCanonicalPath());
304         allDependentImlFiles.remove(this.getImlFile());
305 
306         // TODO: add implicit dependencies.  Convert all modules to be based on directory.
307         for (File dependency : implicitModulePathDependencies) {
308             Module child = moduleCache.getAndCacheByDir(dependency);
309             if (child != null) {
310                 allDependencies.add(child.getDir().getCanonicalPath());
311                 //allDependencies.addAll(child.getAllDependencies());
312                 //logger.info("Adding iml " + child.getName() + " " + child.getImlFile());
313                 allDependentImlFiles.add(child.getImlFile());
314                 //allDependentImlFiles.addAll(child.getAllDependentImlFiles());
315             }
316         }
317     }
318 
getImlFile()319     public File getImlFile() {
320         return imlFile;
321     }
322 
getAllDependencies()323     public Set<String> getAllDependencies() {
324         return allDependencies;
325     }
326 
getAllDependentImlFiles()327     public Set<File> getAllDependentImlFiles() {
328         return allDependentImlFiles;
329     }
330 
buildAndroidFacet()331     private String buildAndroidFacet() throws IOException {
332         // Not sure how to handle android facet for multi-module since there could be more than
333         // one intermediates directory.
334         String dir = getRelativeIntermediatesDirs().get(0);
335         String xml = ""
336                 + "  <component name=\"FacetManager\">\n"
337                 + "    <facet type=\"android\" name=\"Android\">\n"
338                 + "      <configuration>\n"
339                 + "        <option name=\"GEN_FOLDER_RELATIVE_PATH_APT\" value=\"" +
340                 dir + "\" />\n"
341                 + "        <option name=\"GEN_FOLDER_RELATIVE_PATH_AIDL\" value=\"" +
342                 dir + "\" />\n"
343                 + "        <option name=\"MANIFEST_FILE_RELATIVE_PATH\" value=\""
344                 + "/AndroidManifest.xml\" />\n"
345                 + "        <option name=\"RES_FOLDER_RELATIVE_PATH\" value=\"/res\" />\n"
346                 + "        <option name=\"ASSETS_FOLDER_RELATIVE_PATH\" value=\"/assets\" />\n"
347                 + "        <option name=\"LIBS_FOLDER_RELATIVE_PATH\" value=\"/libs\" />\n"
348                 + "        <option name=\"REGENERATE_R_JAVA\" value=\"true\" />\n"
349                 + "        <option name=\"REGENERATE_JAVA_BY_AIDL\" value=\"true\" />\n"
350                 + "        <option name=\"USE_CUSTOM_APK_RESOURCE_FOLDER\" value=\"false\" />\n"
351                 + "        <option name=\"CUSTOM_APK_RESOURCE_FOLDER\" value=\"\" />\n"
352                 + "        <option name=\"USE_CUSTOM_COMPILER_MANIFEST\" value=\"false\" />\n"
353                 + "        <option name=\"CUSTOM_COMPILER_MANIFEST\" value=\"\" />\n"
354                 + "        <option name=\"APK_PATH\" value=\"\" />\n"
355                 + "        <option name=\"LIBRARY_PROJECT\" value=\"false\" />\n"
356                 + "        <option name=\"RUN_PROCESS_RESOURCES_MAVEN_TASK\" value=\"true\" />\n"
357                 + "        <option name=\"GENERATE_UNSIGNED_APK\" value=\"false\" />\n"
358                 + "      </configuration>\n"
359                 + "    </facet>\n"
360                 + "  </component>\n";
361         return xml;
362     }
363 
364     @Override
hashCode()365     public int hashCode() {
366         return Objects.hashCode(getName());
367     }
368 
369     @Override
equals(Object obj)370     public boolean equals(Object obj) {
371         if (this == obj) {
372             return true;
373         }
374         if (obj == null || getClass() != obj.getClass()) {
375             return false;
376         }
377         Module other = (Module) obj;
378         return Objects.equal(getName(), other.getName());
379     }
380 
381     @Override
toString()382     public String toString() {
383         return Objects.toStringHelper(this)
384                 .add("name", getName())
385                 .add("allDependencies", allDependencies)
386                 .add("iml files", allDependentImlFiles).add("imlFile", imlFile)
387                 .add("makeFileParser", makeFileParser)
388                 .add("explicitModuleNameDependencies", Iterables.toString(
389                         explicitModuleNameDependencies))
390                 .add("implicitModulePathDependencies", Iterables.toString(
391                         implicitModulePathDependencies))
392                 .toString();
393     }
394 }
395 
396