diff --git a/tools/idegen/index-gen.sh b/tools/idegen/index-gen.sh index 5b9e24bb1..33633081b 100755 --- a/tools/idegen/index-gen.sh +++ b/tools/idegen/index-gen.sh @@ -38,8 +38,8 @@ if [ ! -e $root_dir/.repo ]; then exit 1 fi fi -tmp_file=tmp.txt -dest_file=module-index.txt +tmp_file=${root_dir}/tmp.txt +dest_file=${root_dir}/module-index.txt echo "Generating index file $dest_file..." start=$(($(date +%s%N) / 1000000)) diff --git a/tools/idegen/intellij-gen.sh b/tools/idegen/intellij-gen.sh index d67c1f867..860bd3c67 100755 --- a/tools/idegen/intellij-gen.sh +++ b/tools/idegen/intellij-gen.sh @@ -32,13 +32,15 @@ set -e progname=`basename $0` -if [ $# -ne 1 ] +if [ $# -lt 2 ] then - echo "Usage: $progname " + echo "Usage: $progname project_dir module_dir ..." exit 1 fi -module_name=$1 - +project_dir=${PWD}/$1 +shift +module_dirs=$@ +echo $module_dirs script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" root_dir=$PWD if [ ! -e $root_dir/.repo ]; then @@ -63,8 +65,8 @@ fi echo "Checking for $idegenjar" if [ -e "$idegenjar" ]; then - echo "Generating project files for $module_name" - cmd="java -cp $idegenjar com.android.idegen.IntellijProject $index_file $module_name" + echo "Generating project files for $module_dirs" + cmd="java -cp $idegenjar com.android.idegen.IntellijProject $index_file $project_dir $module_dirs" echo $cmd $cmd else diff --git a/tools/idegen/src/com/android/idegen/AggregatedModule.java b/tools/idegen/src/com/android/idegen/AggregatedModule.java deleted file mode 100644 index 1497da711..000000000 --- a/tools/idegen/src/com/android/idegen/AggregatedModule.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.idegen; - -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; - -import java.io.File; -import java.io.IOException; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.logging.Logger; - -/** - * Module while is a composition of many other modules. - *

- * This is needed since intellij does not allow two modules to share the same content root. - */ -public class AggregatedModule extends Module { - - private static final Logger logger = Logger.getLogger(AggregatedModule.class.getName()); - - private String aggregatedModuleName; - private Set modules; - private HashSet directDependencies = Sets.newHashSet(); - - public AggregatedModule(String aggregatedName, Set modules) { - this.aggregatedModuleName = Preconditions.checkNotNull(aggregatedName); - this.modules = Preconditions.checkNotNull(modules); - } - - public void build() throws IOException { - // Create an iml file that contains all the srcs of modules. - buildDependentModules(); - buildDirectDependencies(); - //buildImlFile(); - } - - @Override - protected File getDir() { - // All modules should be in the same directory so just pull the first. - return modules.iterator().next().getDir(); - } - - @Override - protected boolean isAndroidModule() { - for (Module module : modules) { - if (module.isAndroidModule()) { - return true; - } - } - return false; - } - - @Override - protected List getIntermediatesDirs() { - List result = Lists.newArrayList(); - for (Module module : modules) { - Iterables.addAll(result, module.getIntermediatesDirs()); - } - return result; - } - - public void buildDirectDependencies() { - for (Module module : modules) { - Set deps = module.getDirectDependencies(); - directDependencies.addAll(deps); - } - } - - @Override - public Set getDirectDependencies() { - return directDependencies; - } - - @Override - protected ImmutableList getSourceDirs() { - ImmutableList.Builder builder = ImmutableList.builder(); - for (Module module : modules) { - builder.addAll(module.getSourceDirs()); - } - return builder.build(); - } - - - @Override - protected ImmutableList getExcludeDirs() { - ImmutableList.Builder builder = ImmutableList.builder(); - for (Module module : modules) { - builder.addAll(module.getExcludeDirs()); - } - return builder.build(); - } - - @Override - public Set getAllDependentImlFiles() { - Set result = Sets.newHashSet(); - for (Module module : modules) { - result.addAll(module.getAllDependentImlFiles()); - } - return result; - } - - @Override - public File getRepoRoot() { - return modules.iterator().next().getRepoRoot(); - } - - @Override - public String getName() { - return aggregatedModuleName; - } - -} diff --git a/tools/idegen/src/com/android/idegen/DirectorySearch.java b/tools/idegen/src/com/android/idegen/DirectorySearch.java index 2ff23e3de..c289ac251 100644 --- a/tools/idegen/src/com/android/idegen/DirectorySearch.java +++ b/tools/idegen/src/com/android/idegen/DirectorySearch.java @@ -23,6 +23,7 @@ import com.google.common.collect.Sets; import java.io.File; import java.io.FileNotFoundException; import java.io.FilenameFilter; +import java.io.IOException; import java.net.URISyntaxException; import java.util.HashSet; import java.util.logging.Level; @@ -37,7 +38,8 @@ public class DirectorySearch { private static final Logger logger = Logger.getLogger(DirectorySearch.class.getName()); - private static final HashSet SOURCE_DIRS = Sets.newHashSet(); + public static final HashSet SOURCE_DIRS = Sets.newHashSet(); + static { SOURCE_DIRS.add("src"); SOURCE_DIRS.add("java"); @@ -50,38 +52,72 @@ public class DirectorySearch { public static final String REL_TEMPLATE_PATH_FROM_ROOT = "development/tools/idegen/" + REL_TEMPLATE_DIR; + /** + * Returns the previously initialized repo root. + */ + public static File getRepoRoot() { + Preconditions.checkNotNull(repoRoot, "repoRoot has not been initialized yet. Call " + + "findAndInitRepoRoot() first."); + return repoRoot; + } + /** * Find the repo root. This is the root branch directory of a full repo checkout. * * @param file any file inside the root. * @return the root directory. */ - public static File findRepoRoot(File file) { + public static void findAndInitRepoRoot(File file) { Preconditions.checkNotNull(file); if (repoRoot != null) { - return repoRoot; + return; } if (file.isDirectory()) { File[] files = file.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { - if (".repo".equals(name)) { - return true; - } - return false; + return ".repo".equals(name); } }); if (files.length > 0) { repoRoot = file; - return file; } } File parent = file.getParentFile(); if (parent == null) { - return null; + throw new IllegalStateException("Repo root not found from starting point " + + file.getPath()); } - return findRepoRoot(parent); + findAndInitRepoRoot(parent); + } + + /** + * Searches up the parent chain to find the closes module root directory. A module root is one + * with an Android.mk file in it.

For example, the module root for directory + * package/apps/Contacts/src is packages/apps/Contacts + * + * @return the module root. + * @throws IOException when module root is not found. + */ + public static File findModuleRoot(File path) throws IOException { + Preconditions.checkNotNull(path); + File dir; + if (path.isFile()) { + dir = path.getParentFile(); + } else { + dir = path; + } + while (dir != null) { + File makeFile = new File(dir, "Android.mk"); + if (makeFile.exists()) { + return dir; + } else { + dir = dir.getParentFile(); + } + } + // At this point, there are no parents and we have not found a module. Error. + throw new IOException("Module root not found for path " + path.getCanonicalPath()); } /** @@ -105,10 +141,19 @@ public class DirectorySearch { File[] children = file.listFiles(); for (File child : children) { if (child.isDirectory()) { - if (SOURCE_DIRS.contains(child.getName())) { - builder.add(child); + // Recurse further down the tree first to cover case of: + // + // src/java + // or + // java/src + // + // In either of these cases, we don't want the parent. + ImmutableList dirs = findSourceDirs(child); + if (dirs.isEmpty()) { + if (SOURCE_DIRS.contains(child.getName())) { + builder.add(child); + } } else { - ImmutableList dirs = findSourceDirs(child); builder.addAll(dirs); } } @@ -151,20 +196,28 @@ public class DirectorySearch { private static File templateDirCurrent = null; private static File templateDirRoot = null; - public static File findTemplateDir() throws FileNotFoundException { + public static File findTemplateDir() throws IOException { // Cache optimization. - if (templateDirCurrent != null && templateDirCurrent.exists()) return templateDirCurrent; - if (templateDirRoot != null && templateDirRoot.exists()) return templateDirRoot; + if (templateDirCurrent != null && templateDirCurrent.exists()) { + return templateDirCurrent; + } + if (templateDirRoot != null && templateDirRoot.exists()) { + return templateDirRoot; + } File currentDir = null; try { - currentDir = new File(IntellijProject.class.getProtectionDomain().getCodeSource() - .getLocation().toURI().getPath()).getParentFile(); + currentDir = new File( + IntellijProject.class.getProtectionDomain().getCodeSource().getLocation() + .toURI().getPath()).getParentFile(); } catch (URISyntaxException e) { logger.log(Level.SEVERE, "Could not get jar location.", e); return null; } - + // Support for program execution in intellij. + if (currentDir.getPath().endsWith("out/production")) { + return new File(currentDir.getParentFile().getParentFile(), REL_TEMPLATE_DIR); + } // First check relative to current run directory. templateDirCurrent = new File(currentDir, REL_TEMPLATE_DIR); if (templateDirCurrent.exists()) { @@ -178,7 +231,7 @@ public class DirectorySearch { } throw new FileNotFoundException( "Unable to find template dir. Tried the following locations:\n" + - templateDirCurrent.getAbsolutePath() + "\n" + - templateDirRoot.getAbsolutePath()); + templateDirCurrent.getCanonicalPath() + "\n" + + templateDirRoot.getCanonicalPath()); } } diff --git a/tools/idegen/src/com/android/idegen/FrameworkModule.java b/tools/idegen/src/com/android/idegen/FrameworkModule.java index 8743925e4..47ea35a72 100644 --- a/tools/idegen/src/com/android/idegen/FrameworkModule.java +++ b/tools/idegen/src/com/android/idegen/FrameworkModule.java @@ -16,33 +16,35 @@ package com.android.idegen; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import java.io.File; +import java.io.IOException; /** * Special module used for framework to build one off resource directory. */ -public class FrameworkModule extends StandardModule { +public class FrameworkModule extends Module { // Framework needs a special constant for it's intermediates because it does not follow // normal conventions. private static final String FRAMEWORK_INTERMEDIATES = "framework-res_intermediates"; - public FrameworkModule(String moduleName, String makeFile) { - super(IntellijProject.FRAMEWORK_MODULE, makeFile, true); + public FrameworkModule(File moduleDir) throws IOException { + super(Preconditions.checkNotNull(moduleDir), false); } @Override - protected String buildIntermediates() { + protected String buildIntermediates() throws IOException { StringBuilder sb = new StringBuilder(); - File intermediates = new File(repoRoot, + File intermediates = new File(DirectorySearch.getRepoRoot(), REL_OUT_APP_DIR + File.separator + FRAMEWORK_INTERMEDIATES); ImmutableList intermediateSrcDirs = DirectorySearch.findSourceDirs(intermediates); sb.append(" \n"); for (File src : intermediateSrcDirs) { sb.append(" \n"); + .append(src.getCanonicalPath()).append("\" isTestSource=\"false\" />\n"); } sb.append(" \n"); return sb.toString(); diff --git a/tools/idegen/src/com/android/idegen/IntellijProject.java b/tools/idegen/src/com/android/idegen/IntellijProject.java index e49a12b72..e3b8d9456 100644 --- a/tools/idegen/src/com/android/idegen/IntellijProject.java +++ b/tools/idegen/src/com/android/idegen/IntellijProject.java @@ -17,14 +17,14 @@ package com.android.idegen; import com.google.common.base.Preconditions; -import com.google.common.collect.Sets; +import com.google.common.collect.Lists; import com.google.common.io.Files; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.Arrays; -import java.util.Set; import java.util.logging.Logger; /** @@ -32,7 +32,7 @@ import java.util.logging.Logger; */ public class IntellijProject { - public static final String FRAMEWORK_MODULE = "framework"; + public static final String FRAMEWORK_MODULE_DIR = "frameworks/base"; public static final Charset CHARSET = Charset.forName("UTF-8"); private static final Logger logger = Logger.getLogger(IntellijProject.class.getName()); @@ -42,147 +42,116 @@ public class IntellijProject { ModuleCache cache = ModuleCache.getInstance(); + boolean buildFramework; File indexFile; - File repoRoot; - File projectIdeaDir; - String moduleName; + File projectPath; + ArrayList moduleDirs; - public IntellijProject(String indexFile, String moduleName) { + public IntellijProject(String indexFile, String projectPath, ArrayList moduleDirs, + boolean buildFramework) { this.indexFile = new File(Preconditions.checkNotNull(indexFile)); - this.moduleName = Preconditions.checkNotNull(moduleName); - } - - private void init() throws IOException { - repoRoot = DirectorySearch.findRepoRoot(indexFile); - cache.init(indexFile); + this.projectPath = new File(Preconditions.checkNotNull(projectPath)); + this.moduleDirs = Preconditions.checkNotNull(moduleDirs); + this.buildFramework = buildFramework; + DirectorySearch.findAndInitRepoRoot(this.indexFile); } public void build() throws IOException { - init(); - buildFrameWorkModule(); - - // First pass, find all dependencies and cache them. - Module module = cache.getAndCache(moduleName); - if (module == null) { - logger.info("Module '" + moduleName + "' not found." + - " Module names are case senstive."); - return; - } - projectIdeaDir = new File(module.getDir(), ".idea"); - projectIdeaDir.mkdir(); - copyTemplates(); - - // Second phase, build aggregate modules. - Set deps = module.getAllDependencies(); - for (String dep : deps) { - cache.buildAndCacheAggregatedModule(dep); + cache.init(indexFile); + File repoRoot = DirectorySearch.getRepoRoot(); + if (buildFramework) { + File frameworkDir = new File(repoRoot, FRAMEWORK_MODULE_DIR); + // Some unbundled apps/branches do not include the framework. + if (frameworkDir.exists()) { + buildFrameWorkModule(new File(repoRoot, FRAMEWORK_MODULE_DIR)); + } } - // Third phase, replace individual modules with aggregated modules - Iterable modules = cache.getModules(); - for (Module mod : modules) { - replaceWithAggregate(mod); + for (String moduleDir : moduleDirs) { + // First pass, find all dependencies and cache them. + File dir = new File(repoRoot, moduleDir); + if (!dir.exists()) { + logger.info("Directory " + moduleDir + " does not exist in " + repoRoot + + ". Are you sure the directory is correct?"); + return; + } + Module module = cache.getAndCacheByDir(dir); + if (module == null) { + logger.info("Module '" + dir.getPath() + "' not found." + + " Module names are case senstive."); + return; + } } // Finally create iml files for dependencies + Iterable modules = cache.getModules(); for (Module mod : modules) { mod.buildImlFile(); } - createModulesFile(module); - createVcsFile(module); - createNameFile(moduleName); + createProjectFiles(); } - private void replaceWithAggregate(Module module) { - replaceWithAggregate(module.getDirectDependencies(), module.getName()); - replaceWithAggregate(module.getAllDependencies(), module.getName()); - - } - - private void replaceWithAggregate(Set deps, String moduleName) { - for (String dep : Sets.newHashSet(deps)) { - String replacement = cache.getAggregateReplacementName(dep); - if (replacement != null) { - - deps.remove(dep); - // There could be dependencies on self due to aggregation. - // Only add if the replacement is not self. - if (!replacement.equals(moduleName)) { - deps.add(replacement); - } - } - } + private void createProjectFiles() throws IOException { + File ideaDir = new File(projectPath, ".idea"); + ideaDir.mkdirs(); + copyTemplates(ideaDir); + createModulesFile(ideaDir, cache.getModules()); + createVcsFile(ideaDir, cache.getModules()); + createNameFile(ideaDir, projectPath.getName()); } /** * Framework module needs special handling due to one off resource path: * frameworks/base/Android.mk */ - private void buildFrameWorkModule() throws IOException { - String makeFile = cache.getMakeFile(FRAMEWORK_MODULE); - if (makeFile == null) { - logger.warning("Unable to find framework module: " + FRAMEWORK_MODULE + - ". Skipping."); - } else { - logger.info("makefile: " + makeFile); - StandardModule frameworkModule = new FrameworkModule(FRAMEWORK_MODULE, - makeFile); - frameworkModule.build(); - cache.put(frameworkModule); - } + private void buildFrameWorkModule(File frameworkModuleDir) throws IOException { + FrameworkModule frameworkModule = new FrameworkModule(frameworkModuleDir); + frameworkModule.build(); + cache.put(frameworkModule); } - private void createModulesFile(Module module) throws IOException { - String modulesContent = Files.toString( - new File(DirectorySearch.findTemplateDir(), - "idea" + File.separator + MODULES_TEMPLATE_FILE_NAME), - CHARSET); + private void createModulesFile(File ideaDir, Iterable modules) throws IOException { + String modulesContent = Files.toString(new File(DirectorySearch.findTemplateDir(), + "idea" + File.separator + MODULES_TEMPLATE_FILE_NAME), CHARSET); StringBuilder sb = new StringBuilder(); - File moduleIml = module.getImlFile(); - sb.append(" \n"); - for (String name : module.getAllDependencies()) { - Module mod = cache.getAndCache(name); + for (Module mod : modules) { File iml = mod.getImlFile(); - sb.append(" \n"); + sb.append(" \n"); } modulesContent = modulesContent.replace("@MODULES@", sb.toString()); - File out = new File(projectIdeaDir, "modules.xml"); - logger.info("Creating " + out.getAbsolutePath()); + File out = new File(ideaDir, "modules.xml"); + logger.info("Creating " + out.getCanonicalPath()); Files.write(modulesContent, out, CHARSET); } - private void createVcsFile(Module module) throws IOException { - String vcsTemplate = Files.toString( - new File(DirectorySearch.findTemplateDir(), - "idea" + File.separator + VCS_TEMPLATE_FILE_NAME), - CHARSET); + private void createVcsFile(File ideaDir, Iterable modules) throws IOException { + String vcsTemplate = Files.toString(new File(DirectorySearch.findTemplateDir(), + "idea" + File.separator + VCS_TEMPLATE_FILE_NAME), CHARSET); StringBuilder sb = new StringBuilder(); - for (String name : module.getAllDependencies()) { - Module mod = cache.getAndCache(name); + for (Module mod : modules) { File dir = mod.getDir(); File gitRoot = new File(dir, ".git"); if (gitRoot.exists()) { - sb.append(" \n"); + sb.append(" \n"); } } vcsTemplate = vcsTemplate.replace("@VCS@", sb.toString()); - Files.write(vcsTemplate, new File(projectIdeaDir, "vcs.xml"), CHARSET); + Files.write(vcsTemplate, new File(ideaDir, "vcs.xml"), CHARSET); } - private void createNameFile(String name) throws IOException { - File out = new File(projectIdeaDir, ".name"); + private void createNameFile(File ideaDir, String name) throws IOException { + File out = new File(ideaDir, ".name"); Files.write(name, out, CHARSET); } - private void copyTemplates() throws IOException { + private void copyTemplates(File ideaDir) throws IOException { File templateDir = DirectorySearch.findTemplateDir(); - copyTemplates(new File(templateDir, "idea"), projectIdeaDir); + copyTemplates(new File(templateDir, "idea"), ideaDir); } private void copyTemplates(File fromDir, File toDir) throws IOException { @@ -191,11 +160,14 @@ public class IntellijProject { for (File file : files) { if (file.isDirectory()) { File destDir = new File(toDir, file.getName()); + if (!destDir.exists()) { + destDir.mkdirs(); + } copyTemplates(file, destDir); } else { File toFile = new File(toDir, file.getName()); - logger.info("copying " + file.getAbsolutePath() + " to " + - toFile.getAbsolutePath()); + logger.info("copying " + file.getCanonicalPath() + " to " + + toFile.getCanonicalPath()); Files.copy(file, toFile); } } @@ -204,10 +176,32 @@ public class IntellijProject { public static void main(String[] args) { logger.info("Args: " + Arrays.toString(args)); - String indexFile = args[0]; - String module = args[1]; + if (args.length < 3) { + logger.severe("Not enough input arguments. Aborting"); + return; + } - IntellijProject intellij = new IntellijProject(indexFile, module); + boolean buildFramework = true; + int argIndex = 0; + String arg = args[argIndex]; + while (arg.startsWith("--")) { + if (arg.equals("--no-framework")) { + buildFramework = false; + } + argIndex++; + arg = args[argIndex]; + } + + String indexFile = args[argIndex++]; + String projectPath = args[argIndex++]; + // Remaining args are module directories + ArrayList moduleDirs = Lists.newArrayList(); + for (int i = argIndex; i < args.length; i++) { + moduleDirs.add(args[i]); + } + + IntellijProject intellij = new IntellijProject(indexFile, projectPath, moduleDirs, + buildFramework); try { intellij.build(); } catch (IOException e) { diff --git a/tools/idegen/src/com/android/idegen/MakeFileParser.java b/tools/idegen/src/com/android/idegen/MakeFileParser.java index 3594d5020..9a41b09b3 100644 --- a/tools/idegen/src/com/android/idegen/MakeFileParser.java +++ b/tools/idegen/src/com/android/idegen/MakeFileParser.java @@ -22,6 +22,7 @@ import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import com.google.common.collect.Sets; import com.google.common.io.Files; import com.google.common.io.LineProcessor; @@ -29,7 +30,9 @@ import java.io.File; import java.io.IOException; import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.logging.Logger; @@ -41,16 +44,7 @@ public class MakeFileParser { private static final Logger logger = Logger.getLogger(MakeFileParser.class.getName()); public static final String VALUE_DELIMITER = "|"; - private enum State { - NEW, CONTINUE - } - private enum ModuleNameKey { - LOCAL_PACKAGE_NAME, - LOCAL_MODULE - }; - private File makeFile; - private String moduleName; private HashMap values; /** @@ -59,11 +53,9 @@ public class MakeFileParser { * A make file may contain multiple modules. * * @param makeFile The make file to parse. - * @param moduleName The module to extract. */ - public MakeFileParser(File makeFile, String moduleName) { + public MakeFileParser(File makeFile) { this.makeFile = Preconditions.checkNotNull(makeFile); - this.moduleName = Preconditions.checkNotNull(moduleName); } public Iterable getValues(String key) { @@ -71,163 +63,214 @@ public class MakeFileParser { if (str == null) { return null; } - return Splitter.on(VALUE_DELIMITER) - .trimResults() - .omitEmptyStrings() - .split(str); + return Splitter.on(VALUE_DELIMITER).trimResults().omitEmptyStrings().split(str); } /** - * Extracts the relevant portion of the make file and converts into key value pairs. - *

- * Since each make file may contain multiple build targets (modules), this method will determine - * which part is the correct portion for the given module name. - * - * @throws IOException + * Extracts the relevant portion of the make file and converts into key value pairs.

Since + * each make file may contain multiple build targets (modules), this method will determine which + * part is the correct portion for the given module name. */ public void parse() throws IOException { values = Maps.newHashMap(); - logger.info("Parsing " + makeFile.getAbsolutePath() + " for module " + moduleName); + logger.info("Parsing " + makeFile.getCanonicalPath()); - Files.readLines(makeFile, Charset.forName("UTF-8"), new LineProcessor() { - - private String key; - - private State state = State.NEW; - - @Override - public boolean processLine(String line) throws IOException { - String trimmed = line.trim(); - if (Strings.isNullOrEmpty(trimmed)) { - state = State.NEW; - return true; - } - if (trimmed.equals("include $(CLEAR_VARS)")) { - // See if we are in the right module. - if (moduleName.equals(getModuleName())) { - return false; - } else { - values.clear(); - } - } else { - switch (state) { - case NEW: - trimmed = checkContinue(trimmed); - if (trimmed.contains("=")) { - String[] arr; - if (trimmed.contains(":")) { - arr = trimmed.split(":="); - } else { - arr = trimmed.split("\\+="); - } - if (arr.length > 2) { - logger.info("Malformed line " + line); - } else { - // Store the key in case the line continues - this.key = arr[0].trim(); - if (arr.length == 2) { - // There may be multiple values on one line. - List valuesArr = tokenizeValue(arr[1].trim()); - for (String value : valuesArr) { - appendValue(this.key, value); - } - - } - } - } else { - //logger.info("Skipping line " + line); - } - break; - case CONTINUE: - // append - trimmed = checkContinue(trimmed); - appendValue(key, trimmed); - break; - default: - - } - } - return true; - } - - private List tokenizeValue(String value) { - // Value may contain function calls such as "$(call all-java-files-under)". - // Tokens are separated by spaces unless it's between parens. - StringBuilder token = new StringBuilder(); - ArrayList tokens = Lists.newArrayList(); - int parenCount = 0; - for (int i = 0; i < value.length(); i++) { - char ch = value.charAt(i); - if (parenCount == 0 && ch == ' ') { - // Not in a paren and delimiter encountered. - // end token - if (token.length() > 0) { - tokens.add(token.toString()); - token = new StringBuilder(); - } - } else { - token.append(ch); - } - if (ch == '(') { - parenCount++; - } else if (ch == ')') { - parenCount--; - } - } - // end of line check - if (token.length() > 0) { - tokens.add(token.toString()); - } - return tokens; - } - - private String getModuleName() { - for (ModuleNameKey key : ModuleNameKey.values()) { - String name = values.get(key.name()); - if (name != null) { - return name; - } - } - return null; - } - - @Override - public Object getResult() { - return null; - } - - private String checkContinue(String value) { - // Check for continuation character - if (value.charAt(value.length() - 1) == '\\') { - state = State.CONTINUE; - return value.substring(0, value.length() - 1); - } - state = State.NEW; - return value; - } - - /** - * Add a value to the hash map. If the key already exists, will append instead of - * over-writing the existing value. - * - * @param key The hashmap key - * @param newValue The value to append. - */ - private void appendValue(String key, String newValue) { - String value = values.get(key); - if (value == null) { - values.put(key, newValue); - } else { - values.put(key, value + VALUE_DELIMITER + newValue); - } - } - }); + Files.readLines(makeFile, Charset.forName("UTF-8"), new MakeFileLineProcessor()); } @Override public String toString() { - return Objects.toStringHelper(this) - .add("values", values) - .toString(); + return Objects.toStringHelper(this).add("values", values).toString(); + } + + private class MakeFileLineProcessor implements LineProcessor { + + private StringBuilder lineBuffer; + + // Keep a list of LOCAL_ variables to clear when CLEAR_VARS is encountered. + private HashSet localVars = Sets.newHashSet(); + + @Override + public boolean processLine(String line) throws IOException { + String trimmed = line.trim(); + // Skip comments. + if (!trimmed.isEmpty() && trimmed.charAt(0) == '#') { + return true; + } + appendPartialLine(trimmed); + + if (!trimmed.isEmpty() && trimmed.charAt(trimmed.length() - 1) == '\\') { + // This is a partial line. Do not process yet. + return true; + } + + String completeLine = lineBuffer.toString().trim(); + // Reset the line buffer. + lineBuffer = null; + + if (Strings.isNullOrEmpty(completeLine)) { + return true; + } + + processKeyValuePairs(completeLine); + return true; + } + + private void processKeyValuePairs(String line) { + if (line.contains("=")) { + String[] arr; + if (line.contains(":")) { + arr = line.split(":="); + } else { + arr = line.split("\\+="); + } + if (arr.length > 2) { + logger.info("Malformed line " + line); + } else { + // Store the key in case the line continues + String key = arr[0].trim(); + if (arr.length == 2) { + // There may be multiple values on one line. + List valuesArr = tokenizeValue(arr[1]); + for (String value : valuesArr) { + appendValue(key, value); + } + + } + } + } else { + //logger.info("Skipping line " + line); + } + } + + private void appendPartialLine(String line) { + if (lineBuffer == null) { + lineBuffer = new StringBuilder(); + } else { + lineBuffer.append(" "); + } + if (line.endsWith("\\")) { + lineBuffer.append(line.substring(0, line.length() - 2).trim()); + } else { + lineBuffer.append(line); + } + } + + private List tokenizeValue(String rawValue) { + String value = rawValue.trim(); + ArrayList result = Lists.newArrayList(); + if (value.isEmpty()) { + return result; + } + + // Value may contain function calls such as "$(call all-java-files-under)" or refer + // to variables such as "$(my_var)" + value = findVariables(value); + + String[] tokens = value.split(" "); + Collections.addAll(result, tokens); + return result; + } + + private String findVariables(String value) { + + int variableStart = value.indexOf('$'); + // Keep going until we substituted all variables. + while (variableStart > -1) { + StringBuilder sb = new StringBuilder(); + sb.append(value.substring(0, variableStart)); + + // variable found + int variableEnd = findClosingParen(value, variableStart); + if (variableEnd > variableStart) { + String result = substituteVariables(value.substring(variableStart + 2, variableEnd)); + sb.append(result); + } else { + throw new IllegalArgumentException( + "Malformed variable reference in make file: " + value); + } + if (variableEnd + 1 < value.length()) { + sb.append(value.substring(variableEnd + 1)); + } + value = sb.toString(); + variableStart = value.indexOf('$'); + } + return value; + } + + private int findClosingParen(String value, int startIndex) { + int openParenCount = 0; + for (int i = startIndex; i < value.length(); i++) { + char ch = value.charAt(i); + if (ch == ')') { + openParenCount--; + if (openParenCount == 0) { + return i; + } + } else if (ch == '(') { + openParenCount++; + } + } + return -1; + } + + /** + * Look for and handle $(...) variables. + */ + private String substituteVariables(String rawValue) { + if (rawValue.isEmpty()) { + return rawValue; + } + String value = rawValue; + if (value.startsWith("call all-java-files-under")) { + // Ignore the call and function, keep the args. + value = value.substring(25).trim(); + } else if (value.startsWith("call")) { + value = value.substring(4).trim(); + } + + // Check for single variable + if (value.indexOf(' ') == -1) { + // Substitute. + value = values.get(value); + if (value == null) { + value = ""; + } + return value; + } else { + return findVariables(value); + } + } + + @Override + public Object getResult() { + return null; + } + + /** + * Add a value to the hash map. If the key already exists, will append instead of + * over-writing the existing value. + * + * @param key The hashmap key + * @param newValue The value to append. + */ + private void appendValue(String key, String newValue) { + String value = values.get(key); + if (value == null) { + values.put(key, newValue); + } else { + values.put(key, value + VALUE_DELIMITER + newValue); + } + } + } + + public static void main(String[] args) { + MakeFileParser parser = new MakeFileParser(new File(args[0])); + try { + parser.parse(); + } catch (IOException e) { + e.printStackTrace(); + } + System.out.println(parser.toString()); } } diff --git a/tools/idegen/src/com/android/idegen/Module.java b/tools/idegen/src/com/android/idegen/Module.java index deb22811f..4695ca3f6 100644 --- a/tools/idegen/src/com/android/idegen/Module.java +++ b/tools/idegen/src/com/android/idegen/Module.java @@ -17,24 +17,44 @@ package com.android.idegen; import com.google.common.base.Objects; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.io.Files; import java.io.File; import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.logging.Logger; /** - * Super class for all modules. + * Module constructed from a make file. + * + * TODO: read the make file and understand included source dirs in addition to searching + * sub-directories. Make files can include sources that are not sub-directories. For example, the + * framework module includes sources from: + * + * external/libphonenumber/java/src + * + * to provide: + * + * com.android.i18n.phonenumbers.PhoneNumberUtil; */ -public abstract class Module { +public class Module { private static final Logger logger = Logger.getLogger(Module.class.getName()); + public static final String REL_OUT_APP_DIR = "out/target/common/obj/APPS"; + private static final String IML_TEMPLATE_FILE_NAME = "module-template.iml"; + private static final String[] AUTO_DEPENDENCIES = new String[]{"framework", "libcore"}; + private static final String[] DIRS_WITH_AUTO_DEPENDENCIES = new String[]{"packages", "vendor", + "frameworks/ex", "frameworks/opt", "frameworks/support"}; /** * All possible attributes for the make file. @@ -45,31 +65,155 @@ public abstract class Module { LOCAL_SRC_FILES } - ModuleCache moduleCache = ModuleCache.getInstance(); + private ModuleCache moduleCache = ModuleCache.getInstance(); private File imlFile; - private Set allDependencies = Sets.newHashSet(); // direct + indirect - private Set allDependentImlFiles = Sets.newHashSet(); - protected abstract void build() throws IOException; + private File makeFile; + private File moduleRoot; + private HashSet sourceFiles = Sets.newHashSet(); - protected abstract String getName(); + // Module dependencies come from LOCAL_STATIC_JAVA_LIBRARIES or LOCAL_JAVA_LIBRARIES + Set explicitModuleNameDependencies = Sets.newHashSet(); + // Implicit module dependencies come from src files that fall outside the module root directory. + // For example, if packages/apps/Contacts includes src files from packages/apps/ContactsCommon, + // that is an implicit module dependency. It's not a module dependency from the build + // perspective but it needs to be a separate module in intellij so that the src files can be + // shared by multiple intellij modules. + Set implicitModulePathDependencies = Sets.newHashSet(); - protected abstract File getDir(); + String relativeIntermediatesDir; + MakeFileParser makeFileParser; + boolean parseMakeFileForSource; - protected abstract boolean isAndroidModule(); + public Module(File moduleDir) throws IOException { + this(moduleDir, true); + } - protected abstract List getIntermediatesDirs(); + public Module(File moduleDir, boolean parseMakeFileForSource) throws IOException { + this.moduleRoot = Preconditions.checkNotNull(moduleDir); + this.makeFile = new File(moduleDir, "Android.mk"); + this.relativeIntermediatesDir = calculateRelativePartToRepoRoot() + REL_OUT_APP_DIR + + File.separatorChar + getName() + "_intermediates" + File.separator + "src"; + this.parseMakeFileForSource = parseMakeFileForSource; - public abstract Set getDirectDependencies(); + // TODO: auto-detect when framework dependency is needed instead of using coded list. + for (String dir : DIRS_WITH_AUTO_DEPENDENCIES) { + // length + 2 to account for slash + boolean isDir = makeFile.getCanonicalPath().startsWith( + DirectorySearch.getRepoRoot() + "/" + dir); + if (isDir) { + Collections.addAll(this.explicitModuleNameDependencies, AUTO_DEPENDENCIES); + } + } - protected abstract ImmutableList getSourceDirs(); + makeFileParser = new MakeFileParser(makeFile); + } - protected abstract ImmutableList getExcludeDirs(); + private String calculateRelativePartToRepoRoot() throws IOException { + String rel = moduleRoot.getCanonicalPath().substring( + DirectorySearch.getRepoRoot().getCanonicalPath().length()); + int count = 0; + // Count the number of slashes to determine how far back to go. + for (int i = 0; i < rel.length(); i++) { + if (rel.charAt(i) == '/') { + count++; + } + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) { + sb.append("../"); + } + return sb.toString(); + } - public abstract File getRepoRoot(); + public void build() throws IOException { + makeFileParser.parse(); + buildDependencyList(); + buildDependentModules(); + logger.info("Done building module " + moduleRoot); + logger.info(toString()); + } + + public File getDir() { + return moduleRoot; + } + + public String getName() { + return moduleRoot.getName(); + } + + private List getRelativeIntermediatesDirs() throws IOException { + return Lists.newArrayList(relativeIntermediatesDir); + } + + private ImmutableList getSourceDirs() { + return ImmutableList.copyOf(sourceFiles); + } + + private ImmutableList getExcludeDirs() { + return DirectorySearch.findExcludeDirs(makeFile); + } + + private boolean isAndroidModule() { + File manifest = new File(moduleRoot, "AndroidManifest.xml"); + return manifest.exists(); + } + + private void findSourceFilesAndImplicitDependencies() throws IOException { + Iterable values = makeFileParser.getValues(Key.LOCAL_SRC_FILES.name()); + if (values != null) { + for (String value : values) { + File src = new File(moduleRoot, value); + + // value may contain garbage at this point due to relaxed make file parsing. + // filter by existing file. + if (src.exists()) { + // Look for directories outside the current module directory. + if (value.contains("..")) { + // Find the closest Android make file. + File moduleRoot = DirectorySearch.findModuleRoot(src); + implicitModulePathDependencies.add(moduleRoot); + } else { + if (parseMakeFileForSource) { + // Check if source files are subdirectories of generic parent src + // directories. If so, no need to add since they are already included. + boolean alreadyIncluded = false; + for (String parentDir : DirectorySearch.SOURCE_DIRS) { + if (value.startsWith(parentDir)) { + alreadyIncluded = true; + break; + } + } + + if (!alreadyIncluded) { + sourceFiles.add(src); + } + } + } + } + } + } + + sourceFiles.addAll(DirectorySearch.findSourceDirs(moduleRoot)); + } + + private void buildDependencyList() throws IOException { + parseDirectDependencies(Key.LOCAL_STATIC_JAVA_LIBRARIES); + parseDirectDependencies(Key.LOCAL_JAVA_LIBRARIES); + findSourceFilesAndImplicitDependencies(); + } + + private void parseDirectDependencies(Key key) { + Iterable names = makeFileParser.getValues(key.name()); + if (names != null) { + for (String dependency : names) { + explicitModuleNameDependencies.add(dependency); + } + } + } public void buildImlFile() throws IOException { String imlTemplate = Files.toString( @@ -82,19 +226,24 @@ public abstract class Module { } imlTemplate = imlTemplate.replace("@FACETS@", facetXml); - String moduleDir = getDir().getAbsolutePath(); + String moduleDir = getDir().getCanonicalPath(); StringBuilder sourceDirectories = new StringBuilder(); sourceDirectories.append(" \n"); ImmutableList srcDirs = getSourceDirs(); for (File src : srcDirs) { - String relative = src.getAbsolutePath().substring(moduleDir.length()); + String relative = src.getCanonicalPath().substring(moduleDir.length()); + boolean isTestSource = false; + if (relative.startsWith("/test")) { + isTestSource = true; + } sourceDirectories.append(" \n"); + .append(relative).append("\" isTestSource=\"").append(isTestSource) + .append("\" />\n"); } ImmutableList excludeDirs = getExcludeDirs(); for (File src : excludeDirs) { - String relative = src.getAbsolutePath().substring(moduleDir.length()); + String relative = src.getCanonicalPath().substring(moduleDir.length()); sourceDirectories.append(" \n"); } @@ -106,43 +255,62 @@ public abstract class Module { imlTemplate = imlTemplate.replace("@SOURCES@", sourceDirectories.toString()); StringBuilder moduleDependencies = new StringBuilder(); - for (String dependency : getDirectDependencies()) { + for (String dependency : getAllDependencies()) { + Module module = moduleCache.getAndCacheByDir(new File(dependency)); moduleDependencies.append(" \n"); + .append(module.getName()).append("\" />\n"); } imlTemplate = imlTemplate.replace("@MODULE_DEPENDENCIES@", moduleDependencies.toString()); imlFile = new File(moduleDir, getName() + ".iml"); - logger.info("Creating " + imlFile.getAbsolutePath()); + logger.info("Creating " + imlFile.getCanonicalPath()); Files.write(imlTemplate, imlFile, IntellijProject.CHARSET); } - protected String buildIntermediates() { + protected String buildIntermediates() throws IOException { StringBuilder sb = new StringBuilder(); - for (File intermediatesDir : getIntermediatesDirs()) { - sb.append(" \n"); - sb.append(" \n"); + sb.append(" \n"); sb.append(" \n"); } return sb.toString(); } - protected void buildDependentModules() throws IOException { - Set directDependencies = getDirectDependencies(); - String[] copy = directDependencies.toArray(new String[directDependencies.size()]); - for (String dependency : copy) { + private void buildDependentModules() throws IOException { + Set moduleNameDependencies = explicitModuleNameDependencies; - Module child = moduleCache.getAndCache(dependency); + String[] copy = moduleNameDependencies.toArray(new String[moduleNameDependencies.size()]); + for (String dependency : copy) { + logger.info("Building dependency " + dependency); + Module child = moduleCache.getAndCacheByName(dependency); if (child == null) { - directDependencies.remove(dependency); + moduleNameDependencies.remove(dependency); } else { - addAllDependencies(dependency); - addAllDependencies(child.getAllDependencies()); + allDependencies.add(child.getDir().getCanonicalPath()); + //allDependencies.addAll(child.getAllDependencies()); //logger.info("Adding iml " + child.getName() + " " + child.getImlFile()); allDependentImlFiles.add(child.getImlFile()); - allDependentImlFiles.addAll(child.getAllDependentImlFiles()); + //allDependentImlFiles.addAll(child.getAllDependentImlFiles()); + } + } + // Don't include self. The current module may have been brought in by framework + // dependencies which will create a circular reference. + allDependencies.remove(this.getDir().getCanonicalPath()); + allDependentImlFiles.remove(this.getImlFile()); + + // TODO: add implicit dependencies. Convert all modules to be based on directory. + for (File dependency : implicitModulePathDependencies) { + Module child = moduleCache.getAndCacheByDir(dependency); + if (child != null) { + allDependencies.add(child.getDir().getCanonicalPath()); + //allDependencies.addAll(child.getAllDependencies()); + //logger.info("Adding iml " + child.getName() + " " + child.getImlFile()); + allDependentImlFiles.add(child.getImlFile()); + //allDependentImlFiles.addAll(child.getAllDependentImlFiles()); } } } @@ -155,29 +323,22 @@ public abstract class Module { return allDependencies; } - public void addAllDependencies(String dependency) { - this.allDependencies.add(dependency); - } - - public void addAllDependencies(Set dependencies) { - this.allDependencies.addAll(dependencies); - } - public Set getAllDependentImlFiles() { return allDependentImlFiles; } - private String buildAndroidFacet() { + private String buildAndroidFacet() throws IOException { // Not sure how to handle android facet for multi-module since there could be more than // one intermediates directory. - String dir = getIntermediatesDirs().get(0).getAbsolutePath(); + String dir = getRelativeIntermediatesDirs().get(0); String xml = "" + " \n" + " \n" + " \n" - + "