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