aboutsummaryrefslogtreecommitdiff
path: root/builder
diff options
context:
space:
mode:
authorXavier Ducrohet <xav@android.com>2012-09-04 18:37:00 -0700
committerXavier Ducrohet <xav@android.com>2012-09-05 17:12:52 -0700
commit6ac01a98a1b3a98f359443cab131cdf5e690c161 (patch)
tree2306a41b24f15d30cf688af18e5bd0956dc1f2f5 /builder
parentccb67c7385d22ca72ec5dd1c83f3ceb1c2dd5f75 (diff)
downloadbuild-6ac01a98a1b3a98f359443cab131cdf5e690c161.tar.gz
Add support for building AILD files.
This brings in the SourceProcessor, SourceGenerator and DependencyGraph from the ant tasks. The dependency graph is only needed because Gradle doesn't give us the list of changed files just yet. Change-Id: I4e9c8ccb032078529516af29b2287b21adac05cc
Diffstat (limited to 'builder')
-rw-r--r--builder/src/main/java/com/android/builder/AndroidBuilder.java76
-rw-r--r--builder/src/main/java/com/android/builder/AndroidDependency.java5
-rw-r--r--builder/src/main/java/com/android/builder/CommandLineRunner.java3
-rw-r--r--builder/src/main/java/com/android/builder/VariantConfiguration.java23
-rw-r--r--builder/src/main/java/com/android/builder/compiler/AidlProcessor.java108
-rw-r--r--builder/src/main/java/com/android/builder/compiler/DependencyGraph.java440
-rw-r--r--builder/src/main/java/com/android/builder/compiler/InputPath.java107
-rw-r--r--builder/src/main/java/com/android/builder/compiler/SourceGenerator.java187
-rw-r--r--builder/src/test/java/com/android/builder/MockSourceSet.java5
-rw-r--r--builder/src/test/java/com/android/builder/VariantConfigurationTest.java2
10 files changed, 949 insertions, 7 deletions
diff --git a/builder/src/main/java/com/android/builder/AndroidBuilder.java b/builder/src/main/java/com/android/builder/AndroidBuilder.java
index 5489f84..9be56f1 100644
--- a/builder/src/main/java/com/android/builder/AndroidBuilder.java
+++ b/builder/src/main/java/com/android/builder/AndroidBuilder.java
@@ -19,6 +19,8 @@ package com.android.builder;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
+import com.android.builder.compiler.AidlProcessor;
+import com.android.builder.compiler.SourceGenerator;
import com.android.builder.packaging.DuplicateFileException;
import com.android.builder.packaging.JavaResourceProcessor;
import com.android.builder.packaging.Packager;
@@ -57,7 +59,7 @@ import java.util.Set;
* {@link #generateBuildConfig(String, java.util.List)}
* {@link #processManifest(String)}
* {@link #processResources(String, String, String, String, String, AaptOptions)}
- * {@link #convertBytecode(java.util.List, String, DexOptions)}
+ * {@link #convertBytecode(java.util.List, java.util.List, String, DexOptions)}
* {@link #packageApk(String, String, String, String)}
*
* Java compilation is not handled but the builder provides the runtime classpath with
@@ -404,6 +406,24 @@ public class AndroidBuilder {
}
}
+ /**
+ *
+ * Process the resources and generate R.java and/or the packaged resources.
+ *
+ * Call this directly if you don't care about checking whether the inputs have changed.
+ * Otherwise, get the input first to check with {@link VariantConfiguration#getResourceInputs()}
+ * and then call (or not),
+ * {@link #processResources(String, String, java.util.List, String, String, String, AaptOptions)}.
+
+ * @param manifestFile the location of the manifest file
+ * @param preprocessResDir the pre-processed folder
+ * @param sourceOutputDir optional source folder to generate R.java
+ * @param resPackageOutput optional filepath for packaged resources
+ * @param proguardOutput optional filepath for proguard file to generate
+ * @param options the {@link AaptOptions}
+ * @throws IOException
+ * @throws InterruptedException
+ */
public void processResources(
@NonNull String manifestFile,
@Nullable String preprocessResDir,
@@ -416,7 +436,21 @@ public class AndroidBuilder {
resPackageOutput, proguardOutput, options);
}
- public void processResources(
+ /**
+ * Process the resources and generate R.java and/or the packaged resources.
+ *
+ *
+ * @param manifestFile the location of the manifest file
+ * @param preprocessResDir the pre-processed folder
+ * @param resInputs the res folder inputs
+ * @param sourceOutputDir optional source folder to generate R.java
+ * @param resPackageOutput optional filepath for packaged resources
+ * @param proguardOutput optional filepath for proguard file to generate
+ * @param options the {@link AaptOptions}
+ * @throws IOException
+ * @throws InterruptedException
+ */
+ public void processResources(
@NonNull String manifestFile,
@Nullable String preprocessResDir,
@NonNull List<File> resInputs,
@@ -602,6 +636,44 @@ public class AndroidBuilder {
mCmdLineRunner.runCmdLine(command);
}
+ /**
+ * compiles all AIDL files.
+ *
+ * Call this directly if you don't care about checking whether the imports have changed.
+ * Otherwise, get the imports first to check with
+ * {@link com.android.builder.VariantConfiguration#getAidlImports()}
+ * and then call (or not), {@link #compileAidl(java.util.List, java.io.File, java.util.List)}.
+ *
+ * @param sourceFolders
+ * @param sourceOutputDir
+ * @throws IOException
+ * @throws InterruptedException
+ */
+ public void compileAidl(@NonNull List<File> sourceFolders,
+ @NonNull File sourceOutputDir)
+ throws IOException, InterruptedException {
+ compileAidl(sourceFolders, sourceOutputDir, mVariant.getAidlImports());
+ }
+
+ public void compileAidl(@NonNull List<File> sourceFolders,
+ @NonNull File sourceOutputDir,
+ @NonNull List<File> importFolders)
+ throws IOException, InterruptedException {
+
+ SourceGenerator compiler = new SourceGenerator(mLogger);
+
+ @SuppressWarnings("deprecation")
+ String aidlPath = mTarget.getPath(IAndroidTarget.AIDL);
+
+ AidlProcessor processor = new AidlProcessor(
+ aidlPath,
+ mTarget.getPath(IAndroidTarget.ANDROID_AIDL),
+ importFolders,
+ mCmdLineRunner);
+
+ compiler.processFiles(processor, sourceFolders, sourceOutputDir);
+ }
+
public void convertBytecode(
@NonNull List<String> classesLocation,
@NonNull List<String> libraries,
diff --git a/builder/src/main/java/com/android/builder/AndroidDependency.java b/builder/src/main/java/com/android/builder/AndroidDependency.java
index b667fa6..9bbb84a 100644
--- a/builder/src/main/java/com/android/builder/AndroidDependency.java
+++ b/builder/src/main/java/com/android/builder/AndroidDependency.java
@@ -56,6 +56,11 @@ public interface AndroidDependency {
File getJniFolder();
/**
+ * Returns the location of the aidl import folder.
+ */
+ File getAidlFolder();
+
+ /**
* Returns the location of the proguard files.
*/
File getProguardRules();
diff --git a/builder/src/main/java/com/android/builder/CommandLineRunner.java b/builder/src/main/java/com/android/builder/CommandLineRunner.java
index 99e55ad..b3cdd76 100644
--- a/builder/src/main/java/com/android/builder/CommandLineRunner.java
+++ b/builder/src/main/java/com/android/builder/CommandLineRunner.java
@@ -25,7 +25,7 @@ import com.android.utils.ILogger;
import java.io.IOException;
import java.util.List;
-class CommandLineRunner {
+public class CommandLineRunner {
private final ILogger mLogger;
@@ -51,7 +51,6 @@ class CommandLineRunner {
/**
* Get the stderr output of a process and return when the process is done.
* @param process The process to get the output from
- * @param stderr The array to store the stderr output
* @return the process return code.
* @throws InterruptedException
*/
diff --git a/builder/src/main/java/com/android/builder/VariantConfiguration.java b/builder/src/main/java/com/android/builder/VariantConfiguration.java
index bd5f96c..b151c4f 100644
--- a/builder/src/main/java/com/android/builder/VariantConfiguration.java
+++ b/builder/src/main/java/com/android/builder/VariantConfiguration.java
@@ -260,11 +260,11 @@ public class VariantConfiguration {
return !mDirectLibraryProjects.isEmpty();
}
- public Iterable<AndroidDependency> getDirectLibraries() {
+ public List<AndroidDependency> getDirectLibraries() {
return mDirectLibraryProjects;
}
- public Iterable<AndroidDependency> getFlatLibraries() {
+ public List<AndroidDependency> getFlatLibraries() {
return mFlatLibraryProjects;
}
@@ -439,6 +439,25 @@ public class VariantConfiguration {
}
/**
+ * Returns all the aidl import folder that are outside of the current project.
+ *
+ * @return
+ */
+ public List<File> getAidlImports() {
+ List<File> list = new ArrayList<File>();
+
+ for (AndroidDependency lib : mFlatLibraryProjects) {
+ File aidlLib = lib.getAidlFolder();
+ if (aidlLib != null && aidlLib.isDirectory()) {
+ list.add(aidlLib);
+ }
+ }
+
+ return list;
+ }
+
+
+ /**
* Returns the compile classpath for this config. If the config tests a library, this
* will include the classpath of the tested config
* @return
diff --git a/builder/src/main/java/com/android/builder/compiler/AidlProcessor.java b/builder/src/main/java/com/android/builder/compiler/AidlProcessor.java
new file mode 100644
index 0000000..f5d633a
--- /dev/null
+++ b/builder/src/main/java/com/android/builder/compiler/AidlProcessor.java
@@ -0,0 +1,108 @@
+/*
+ * 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.builder.compiler;
+
+import com.android.annotations.NonNull;
+import com.android.builder.CommandLineRunner;
+import com.android.builder.compiler.SourceGenerator.DisplayType;
+import com.android.utils.ILogger;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ */
+public class AidlProcessor implements SourceGenerator.Processor {
+
+ private final String mAidlExecutable;
+ private final String mFrameworkLocation;
+ private final List<File> mImportFolders;
+ private final CommandLineRunner mRunner;
+
+ public AidlProcessor(@NonNull String aidlExecutable,
+ @NonNull String frameworkLocation,
+ @NonNull List<File> importFolders,
+ @NonNull CommandLineRunner runner) {
+ mAidlExecutable = aidlExecutable;
+ mFrameworkLocation = frameworkLocation;
+ mImportFolders = importFolders;
+ mRunner = runner;
+ }
+
+ @Override
+ public String getSourceFileExtension() {
+ return "aidl";
+ }
+
+ @Override
+ public void process(File filePath, List<File> sourceFolders, File sourceOutputDir,
+ ILogger logger)
+ throws IOException, InterruptedException {
+
+ ArrayList<String> command = new ArrayList<String>();
+
+ command.add(mAidlExecutable);
+
+ command.add("-p" + mFrameworkLocation);
+ command.add("-o" + sourceOutputDir.getAbsolutePath());
+ // add all the source folders as import in case an aidl file in a source folder
+ // imports a parcelable from another source folder.
+ for (File sourceFolder : sourceFolders) {
+ if (sourceFolder.isDirectory()) {
+ command.add("-I" + sourceFolder.getAbsolutePath());
+ }
+ }
+
+ // add all the library aidl folders to access parcelables that are in libraries
+ for (File f : mImportFolders) {
+ command.add("-I" + f.getAbsolutePath());
+ }
+
+ // set auto dependency file creation
+ command.add("-a");
+
+ command.add(filePath.getAbsolutePath());
+
+ logger.info("aidl command: %s", command.toString());
+
+ mRunner.runCmdLine(command);
+ }
+
+ @Override
+ public void displayMessage(ILogger logger, DisplayType type, int count) {
+ switch (type) {
+ case FOUND:
+ logger.info("Found %1$d AIDL files.", count);
+ break;
+ case COMPILING:
+ if (count > 0) {
+ logger.info("Compiling %1$d AIDL files.", count);
+ } else {
+ logger.info("No AIDL files to compile.");
+ }
+ break;
+ case REMOVE_OUTPUT:
+ logger.info("Found %1$d obsolete output files to remove.", count);
+ break;
+ case REMOVE_DEP:
+ logger.info("Found %1$d obsolete dependency files to remove.", count);
+ break;
+ }
+ }
+}
diff --git a/builder/src/main/java/com/android/builder/compiler/DependencyGraph.java b/builder/src/main/java/com/android/builder/compiler/DependencyGraph.java
new file mode 100644
index 0000000..1ae67da
--- /dev/null
+++ b/builder/src/main/java/com/android/builder/compiler/DependencyGraph.java
@@ -0,0 +1,440 @@
+/*
+ * 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.builder.compiler;
+
+import com.android.utils.ILogger;
+import com.google.common.base.Charsets;
+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;
+
+/**
+ * This class takes care of dependency tracking for all targets and prerequisites listed in
+ * a single dependency file. A dependency graph always has a dependency file associated with it
+ * for the duration of its lifetime
+ */
+public class DependencyGraph {
+
+ private final static boolean DEBUG = false;
+ private final ILogger mLogger;
+
+ private static enum DependencyStatus {
+ NONE, NEW_FILE, UPDATED_FILE, MISSING_FILE, ERROR;
+ }
+
+ // Files that we know about from the dependency file
+ private Set<File> mTargets = Collections.emptySet();
+ private Set<File> mPrereqs = mTargets;
+ private File mFirstPrereq = null;
+ private boolean mMissingDepFile = false;
+ private long mDepFileLastModified;
+ private final List<InputPath> mNewInputs;
+
+ public DependencyGraph(File dependencyFilePath, List<InputPath> newInputPaths, ILogger logger) {
+ mNewInputs = newInputPaths;
+ parseDependencyFile(dependencyFilePath);
+ mLogger = logger;
+ }
+
+ /**
+ * Check all the dependencies to see if anything has changed.
+ *
+ * @param printStatus will print to {@link System#out} the dependencies status.
+ * @return true if new prerequisites have appeared, target files are missing or if
+ * prerequisite files have been modified since the last target generation.
+ */
+ public boolean dependenciesHaveChanged(boolean printStatus) {
+ // If no dependency file has been set up, then we'll just return true
+ // if we have a dependency file, we'll check to see what's been changed
+ if (mMissingDepFile) {
+ mLogger.info("No Dependency File Found");
+ return true;
+ }
+
+ // check for missing output first
+ if (missingTargetFile()) {
+ if (printStatus) {
+ mLogger.info("Found Deleted Target File");
+ }
+ return true;
+ }
+
+ // get the time stamp of the oldest target.
+ long oldestTarget = getOutputLastModified();
+
+ // first look through the input folders and look for new files or modified files.
+ DependencyStatus status = checkInputs(oldestTarget);
+
+ // this can't find missing files. This is done later.
+ switch (status) {
+ case ERROR:
+ throw new RuntimeException();
+ case NEW_FILE:
+ if (printStatus) {
+ mLogger.info("Found new input file");
+ }
+ return true;
+ case UPDATED_FILE:
+ if (printStatus) {
+ mLogger.info("Found modified input file");
+ }
+ return true;
+ }
+
+ // now do a full check on the remaining files.
+ status = checkPrereqFiles(oldestTarget);
+ // this can't find new input files. This is done above.
+ switch (status) {
+ case ERROR:
+ throw new RuntimeException();
+ case MISSING_FILE:
+ if (printStatus) {
+ mLogger.info("Found deleted input file");
+ }
+ return true;
+ case UPDATED_FILE:
+ if (printStatus) {
+ mLogger.info("Found modified input file");
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ public Set<File> getTargets() {
+ return Collections.unmodifiableSet(mTargets);
+ }
+
+ public File getFirstPrereq() {
+ return mFirstPrereq;
+ }
+
+ /**
+ * Parses the given dependency file and stores the file paths
+ *
+ * @param dependencyFile the dependency file
+ */
+ private void parseDependencyFile(File dependencyFile) {
+ // first check if the dependency file is here.
+ if (dependencyFile.isFile() == false) {
+ mMissingDepFile = true;
+ return;
+ }
+
+ // get the modification time of the dep file as we may need it later
+ mDepFileLastModified = dependencyFile.lastModified();
+
+ // Read in our dependency file
+ List<String> content = readFile(dependencyFile);
+ if (content == null) {
+ mLogger.error(null, "ERROR: Couldn't read " + dependencyFile.getAbsolutePath());
+ return;
+ }
+
+ // The format is something like:
+ // output1 output2 [...]: dep1 dep2 [...]
+ // move it back to a single line first
+ StringBuilder sb = new StringBuilder();
+ for (String line : content) {
+ line = line.trim();
+ if (line.endsWith("\\")) {
+ line = line.substring(0, line.length() - 1);
+ }
+ sb.append(line);
+ }
+
+ // split the left and right part
+ String[] files = sb.toString().split(":");
+
+ // get the target files:
+ String[] targets = files[0].trim().split(" ");
+
+ String[] prereqs = {};
+ // Check to make sure our dependency file is okay
+ if (files.length < 1) {
+ mLogger.warning(
+ "Warning! Dependency file does not list any prerequisites after ':' ");
+ } else {
+ // and the prerequisite files:
+ prereqs = files[1].trim().split(" ");
+ }
+
+ mTargets = new HashSet<File>(targets.length);
+ for (String path : targets) {
+ if (path.length() > 0) {
+ mTargets.add(new File(path));
+ }
+ }
+
+ mPrereqs = new HashSet<File>(prereqs.length);
+ for (String path : prereqs) {
+ if (path.length() > 0) {
+ if (DEBUG) {
+ mLogger.info("PREREQ: " + path);
+ }
+ File f = new File(path);
+ if (mFirstPrereq == null) {
+ mFirstPrereq = f;
+ }
+ mPrereqs.add(f);
+ }
+ }
+ }
+
+ /**
+ * Check all the input files and folders to see if there have been new
+ * files added to them or if any of the existing files have been modified.
+ *
+ * This looks at the input paths, not at the list of known prereq. Therefore this
+ * will not find missing files. It will however remove processed files from the
+ * prereq file list so that we can process those in a 2nd step.
+ *
+ * This should be followed by a call to {@link #checkPrereqFiles(long)} which
+ * will process the remaining files in the prereq list.
+ *
+ * If a change is found, this will return immediately with either
+ * {@link DependencyStatus#NEW_FILE} or {@link DependencyStatus#UPDATED_FILE}.
+ *
+ * @param oldestTarget the timestamp of the oldest output file to compare against.
+ *
+ * @return the status of the file in the watched folders.
+ *
+ */
+ private DependencyStatus checkInputs(long oldestTarget) {
+ if (mNewInputs != null) {
+ for (InputPath input : mNewInputs) {
+ File file = input.getFile();
+ if (file.isDirectory()) {
+ DependencyStatus status = checkInputFolder(file, input, oldestTarget);
+ if (status != DependencyStatus.NONE) {
+ return status;
+ }
+ } else if (file.isFile()) {
+ DependencyStatus status = checkInputFile(file, input, oldestTarget);
+ if (status != DependencyStatus.NONE) {
+ return status;
+ }
+ }
+ }
+ }
+
+ // If we make it all the way through our directories we're good.
+ return DependencyStatus.NONE;
+ }
+
+ /**
+ * Check all the files in the tree under root and check to see if the files are
+ * listed under the dependencies, or if they have been modified. Recurses into subdirs.
+ *
+ * @param folder the folder to search through.
+ * @param inputFolder the root level inputFolder
+ * @param oldestTarget the time stamp of the oldest output file to compare against.
+ *
+ * @return the status of the file in the folder.
+ */
+ private DependencyStatus checkInputFolder(File folder, InputPath inputFolder,
+ long oldestTarget) {
+ if (inputFolder.ignores(folder)) {
+ return DependencyStatus.NONE;
+ }
+
+ File[] files = folder.listFiles();
+ if (files == null) {
+ mLogger.error(null, "ERROR " + folder.toString() + " is not a dir or can't be read");
+ return DependencyStatus.ERROR;
+ }
+ // Loop through files in this folder
+ for (File file : files) {
+ // If this is a directory, recurse into it
+ if (file.isDirectory()) {
+ DependencyStatus status = checkInputFolder(file, inputFolder, oldestTarget);
+ if (status != DependencyStatus.NONE) {
+ return status;
+ }
+ } else if (file.isFile()) {
+ DependencyStatus status = checkInputFile(file, inputFolder, oldestTarget);
+ if (status != DependencyStatus.NONE) {
+ return status;
+ }
+ }
+ }
+ // If we got to here then we didn't find anything interesting
+ return DependencyStatus.NONE;
+ }
+
+ private DependencyStatus checkInputFile(File file, InputPath inputFolder,
+ long oldestTarget) {
+ if (inputFolder.ignores(file)) {
+ return DependencyStatus.NONE;
+ }
+
+ // if it's a file, remove it from the list of prereqs.
+ // This way if files in this folder don't trigger a build we'll have less
+ // files to go through manually
+ if (mPrereqs.remove(file) == false) {
+ // turns out this is a new file!
+
+ if (DEBUG) {
+ mLogger.info("NEW FILE: " + file.getAbsolutePath());
+ }
+ return DependencyStatus.NEW_FILE;
+ } else {
+ // check the time stamp on this file if it's a file we care about based what the
+ // input folder decides.
+ if (inputFolder.checksForModification(file)) {
+ if (file.lastModified() > oldestTarget) {
+ if (DEBUG) {
+ mLogger.info("UPDATED FILE: " + file.getAbsolutePath());
+ }
+ return DependencyStatus.UPDATED_FILE;
+ }
+ }
+ }
+
+ return DependencyStatus.NONE;
+ }
+
+ /**
+ * Check all the prereq files we know about to make sure they're still there, or that they
+ * haven't been modified since the last build.
+ *
+ * @param oldestTarget the time stamp of the oldest output file to compare against.
+ *
+ * @return the status of the files
+ */
+ private DependencyStatus checkPrereqFiles(long oldestTarget) {
+ // TODO: Optimize for the case of a specific file as inputPath.
+ // We should have a map of filepath to inputpath to quickly search through them?
+
+ // Loop through our prereq files and make sure they still exist
+ for (File prereq : mPrereqs) {
+ if (prereq.exists() == false) {
+ if (DEBUG) {
+ mLogger.info("MISSING FILE: " + prereq.getAbsolutePath());
+ }
+ return DependencyStatus.MISSING_FILE;
+ }
+
+ // check the time stamp on this file if it's a file we care about.
+ // To know if we care about the file we have to find the matching input.
+ if (mNewInputs != null) {
+ String filePath = prereq.getAbsolutePath();
+ for (InputPath input : mNewInputs) {
+ File inputFile = input.getFile();
+ // if the input path is a directory, check if the prereq file is in it,
+ // otherwise check if the prereq file match exactly the input path.
+ if (inputFile.isDirectory()) {
+ if (filePath.startsWith(inputFile.getAbsolutePath())) {
+ // ok file is inside a directory type input folder.
+ // check if we need to check this type of file, and if yes, check it.
+ if (input.checksForModification(prereq)) {
+ if (prereq.lastModified() > oldestTarget) {
+ if (DEBUG) {
+ mLogger.info(
+ "UPDATED FILE: " + prereq.getAbsolutePath());
+ }
+ return DependencyStatus.UPDATED_FILE;
+ }
+ }
+ }
+ } else {
+ // this is a file input path, we must check if the match is exact.
+ if (prereq.equals(inputFile)) {
+ if (input.checksForModification(prereq)) {
+ if (prereq.lastModified() > oldestTarget) {
+ if (DEBUG) {
+ mLogger.info(
+ "UPDATED FILE: " + prereq.getAbsolutePath());
+ }
+ return DependencyStatus.UPDATED_FILE;
+ }
+ }
+ }
+ }
+ }
+ } else {
+ // no input? we consider all files.
+ if (prereq.lastModified() > oldestTarget) {
+ if (DEBUG) {
+ mLogger.info("UPDATED FILE: " + prereq.getAbsolutePath());
+ }
+ return DependencyStatus.UPDATED_FILE;
+ }
+ }
+ }
+
+ // If we get this far, then all our prereq are okay
+ return DependencyStatus.NONE;
+ }
+
+ /**
+ * Check all the target files we know about to make sure they're still there
+ * @return true if any of the target files are missing.
+ */
+ private boolean missingTargetFile() {
+ // Loop through our target files and make sure they still exist
+ for (File target : mTargets) {
+ if (target.exists() == false) {
+ return true;
+ }
+ }
+ // If we get this far, then all our targets are okay
+ return false;
+ }
+
+ /**
+ * Returns the earliest modification time stamp from all the output targets. If there
+ * are no known target, the dependency file time stamp is returned.
+ */
+ private long getOutputLastModified() {
+ // Find the oldest target
+ long oldestTarget = Long.MAX_VALUE;
+ // if there's no output, then compare to the time of the dependency file.
+ if (mTargets.size() == 0) {
+ oldestTarget = mDepFileLastModified;
+ } else {
+ for (File target : mTargets) {
+ if (target.lastModified() < oldestTarget) {
+ oldestTarget = target.lastModified();
+ }
+ }
+ }
+
+ return oldestTarget;
+ }
+
+ /**
+ * Reads and returns the content of a text file.
+ * @param file the file path to the text file
+ * @return null if the file could not be read
+ */
+ private static List<String> readFile(File file) {
+ try {
+ return Files.readLines(file, Charsets.UTF_8);
+ } catch (IOException e) {
+ // return null below
+ }
+
+ return null;
+ }
+}
diff --git a/builder/src/main/java/com/android/builder/compiler/InputPath.java b/builder/src/main/java/com/android/builder/compiler/InputPath.java
new file mode 100644
index 0000000..5f422a5
--- /dev/null
+++ b/builder/src/main/java/com/android/builder/compiler/InputPath.java
@@ -0,0 +1,107 @@
+/*
+ * 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.builder.compiler;
+
+import java.io.File;
+import java.util.Set;
+
+public class InputPath {
+
+ private final File mFile;
+ /**
+ * A set of extensions. Only files with an extension in this set will
+ * be considered for a modification check. All deleted/created files will still be
+ * checked.
+ */
+ private final Set<String> mTouchedExtensions;
+
+ public InputPath(File file) {
+ this(file, null);
+ }
+
+ public InputPath(File file, Set<String> extensionsToCheck) {
+ if (file == null) {
+ throw new RuntimeException("File in InputPath(File) can't be null");
+ }
+ mFile = file;
+ mTouchedExtensions = extensionsToCheck;
+ }
+
+ public File getFile() {
+ return mFile;
+ }
+
+ /**
+ * Returns whether this input path (likely actually a folder) must check this files for
+ * modification (all files are checked for add/delete).
+ *
+ * This is configured by constructing the {@link InputPath} with additional restriction
+ * parameters such as specific extensions.
+ * @param file the file to check
+ * @return true if the file must be checked for modification.
+ */
+ public boolean checksForModification(File file) {
+ if (ignores(file)) {
+ return false;
+ }
+
+ if (mTouchedExtensions != null &&
+ mTouchedExtensions.contains(getExtension(file)) == false) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns whether the InputPath ignores a given file or folder. If it is ignored then
+ * the file (or folder) is not checked for any event (modification/add/delete).
+ * If it's a folder, then it and its content are completely ignored.
+ * @param file the file or folder to check
+ * @return true if the file or folder are ignored.
+ */
+ public boolean ignores(File file) {
+ // always ignore hidden files/folders.
+ return file.getName().startsWith(".");
+ }
+
+ /**
+ * Gets the extension (if present) on a file by looking at the filename
+ * @param file the file to get the extension from
+ * @return the extension if present, or the empty string if the filename doesn't have
+ * and extension.
+ */
+ protected static String getExtension(File file) {
+ return getExtension(file.getName());
+ }
+
+ /**
+ * Gets the extension (if present) on a file by looking at the filename
+ * @param fileName the filename to get the extension from
+ * @return the extension if present, or the empty string if the filename doesn't have
+ * and extension.
+ */
+ protected static String getExtension(String fileName) {
+ int index = fileName.lastIndexOf('.');
+ if (index == -1) {
+ return "";
+ }
+ // Don't include the leading '.' in the extension
+ return fileName.substring(index + 1);
+ }
+
+}
diff --git a/builder/src/main/java/com/android/builder/compiler/SourceGenerator.java b/builder/src/main/java/com/android/builder/compiler/SourceGenerator.java
new file mode 100644
index 0000000..c5b8c23
--- /dev/null
+++ b/builder/src/main/java/com/android/builder/compiler/SourceGenerator.java
@@ -0,0 +1,187 @@
+/*
+ * 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.builder.compiler;
+
+import com.android.utils.ILogger;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Class to help generating Source code.
+ */
+public class SourceGenerator {
+
+ private final ILogger mLogger;
+
+ static enum DisplayType {
+ FOUND, COMPILING, REMOVE_OUTPUT, REMOVE_DEP;
+ }
+
+ interface Processor {
+ String getSourceFileExtension();
+ void process(File filePath, List<File> sourceFolders, File sourceOutputDir, ILogger logger)
+ throws IOException, InterruptedException;
+ void displayMessage(ILogger logger, DisplayType type, int count);
+ }
+
+ public SourceGenerator(ILogger logger) {
+ mLogger = logger;
+ }
+
+ public void processFiles(Processor processor, List<File> sourceFolders,
+ File sourceOutputDir) throws IOException, InterruptedException {
+
+ String extension = processor.getSourceFileExtension();
+
+ // gather all the source files from all the source folders.
+ Map<File, File> sourceFiles = getFilesByNameEntryFilter(sourceFolders, extension);
+ if (sourceFiles.size() > 0) {
+ processor.displayMessage(mLogger, DisplayType.FOUND, sourceFiles.size());
+ }
+
+ // go look for all dependency files in the gen folder. This will have all dependency
+ // files but we can filter them based on the first pre-req file.
+ List<File> depFiles = getFilesByNameEntryFilter(sourceOutputDir, "d");
+
+ // parse all the dep files and keep the ones that are of the proper type and check if
+ // they require compilation again.
+ Map<File, File> toCompile = new HashMap<File, File>();
+ ArrayList<File> toRemove = new ArrayList<File>();
+ ArrayList<File> depsToRemove = new ArrayList<File>();
+ for (File depFile : depFiles) {
+ DependencyGraph graph = new DependencyGraph(depFile, null /*watchPaths*/, mLogger);
+
+ // get the source file. it's the first item in the pre-reqs
+ File sourceFile = graph.getFirstPrereq();
+ String sourceFilePath = sourceFile.getAbsolutePath();
+
+ // The gen folder may contain other dependency files not generated by this particular
+ // processor.
+ // We only care if the first pre-rep is of the right extension.
+ if (sourceFilePath.toLowerCase(Locale.US).endsWith("." + extension)) {
+ // remove from the list of sourceFiles to mark as "processed" (but not compiled
+ // yet, that'll be done by adding it to toCompile)
+ File sourceFolder = sourceFiles.get(sourceFile);
+ if (sourceFolder == null) {
+ // looks like the source file does not exist anymore!
+ // we'll have to remove the output!
+ Set<File> outputFiles = graph.getTargets();
+ toRemove.addAll(outputFiles);
+
+ // also need to remove the dep file.
+ depsToRemove.add(depFile);
+ } else {
+ // Source file is present. remove it from the list as being processed.
+ sourceFiles.remove(sourceFile);
+
+ // check if it needs to be recompiled.
+ if (graph.dependenciesHaveChanged(false /*printStatus*/)) {
+ toCompile.put(sourceFile, sourceFolder);
+ }
+ }
+ }
+ }
+
+ // add to the list of files to compile, whatever is left in sourceFiles. Those are
+ // new files that have never been compiled.
+ toCompile.putAll(sourceFiles);
+
+ processor.displayMessage(mLogger, DisplayType.COMPILING, toCompile.size());
+ if (toCompile.size() > 0) {
+ for (Map.Entry<File, File> toCompilePath : toCompile.entrySet()) {
+ processor.process(toCompilePath.getKey(), sourceFolders, sourceOutputDir, mLogger);
+ }
+ }
+
+ if (toRemove.size() > 0) {
+ processor.displayMessage(mLogger, DisplayType.REMOVE_OUTPUT, toRemove.size());
+
+ for (File toRemoveFile : toRemove) {
+ if (toRemoveFile.delete() == false) {
+ mLogger.warning("Failed to remove " + toRemoveFile.getAbsolutePath());
+ }
+ }
+ }
+
+ // remove the dependency files that are obsolete
+ if (depsToRemove.size() > 0) {
+ processor.displayMessage(mLogger, DisplayType.REMOVE_DEP, toRemove.size());
+
+ for (File file : depsToRemove) {
+ if (file.delete() == false) {
+ mLogger.warning("Failed to remove " + file.getAbsolutePath());
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns a list of files found in given folders, all matching a given filter.
+ * The result is a map of (file, folder).
+ * @param folders the folders to search
+ * @param extension the filter for the files. Typically a glob.
+ * @return a map of (file, folder)
+ */
+ private Map<File, File> getFilesByNameEntryFilter(List<File> folders, String extension) {
+ Map<File, File> sourceFiles = new HashMap<File, File>();
+
+ for (File folder : folders) {
+ List<File> files = getFilesByNameEntryFilter(folder, extension);
+
+ for (File f : files) {
+ sourceFiles.put(f, folder);
+ }
+ }
+
+ return sourceFiles;
+ }
+
+ /**
+ * Returns a list of files found in a given folder, matching a given filter.
+ * @param sourceFolder the folders to search
+ * @param extension The file extension
+ * @return an iterator.
+ */
+ private List<File> getFilesByNameEntryFilter(File sourceFolder, String extension) {
+ ArrayList<File> result = new ArrayList<File>();
+
+ if (sourceFolder.isDirectory()) {
+ gatherFiles(sourceFolder, extension, result);
+ }
+ return result;
+ }
+
+ private void gatherFiles(File folder, String extension, ArrayList<File> result) {
+ for (File f : folder.listFiles()) {
+ if (f.isFile()) {
+ String name = f.getName();
+ if (name.substring(name.lastIndexOf('.') + 1).equals(extension)) {
+ result.add(f);
+ }
+ } else if (f.isDirectory()) {
+ gatherFiles(f, extension, result);
+ }
+ }
+ }
+}
diff --git a/builder/src/test/java/com/android/builder/MockSourceSet.java b/builder/src/test/java/com/android/builder/MockSourceSet.java
index 6a5d15c..2d2c174 100644
--- a/builder/src/test/java/com/android/builder/MockSourceSet.java
+++ b/builder/src/test/java/com/android/builder/MockSourceSet.java
@@ -37,6 +37,11 @@ class MockSourceSet implements SourceSet {
}
@Override
+ public Iterable<File> getCompileClasspath() {
+ return null;
+ }
+
+ @Override
public File getAndroidResources() {
return new File(mRoot, "res");
}
diff --git a/builder/src/test/java/com/android/builder/VariantConfigurationTest.java b/builder/src/test/java/com/android/builder/VariantConfigurationTest.java
index 2be0a48..c1ee5a5 100644
--- a/builder/src/test/java/com/android/builder/VariantConfigurationTest.java
+++ b/builder/src/test/java/com/android/builder/VariantConfigurationTest.java
@@ -110,7 +110,7 @@ public class VariantConfigurationTest extends TestCase {
mBuildType, new MockSourceSet("debug"),
VariantConfiguration.Type.DEFAULT) {
@Override
- String getPackageFromManifest() {
+ public String getPackageFromManifest() {
return packageName;
}
// don't do validation.