blob: 36f4b22e1b26f2e4a003518672d0920a185829b9 [file] [log] [blame]
/*
* Copyright (C) 2011 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.ant;
import com.google.common.base.Charsets;
import com.google.common.io.Files;
import org.apache.tools.ant.BuildException;
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 static final boolean DEBUG = false;
private 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(String dependencyFilePath, List<InputPath> newInputPaths) {
mNewInputs = newInputPaths;
parseDependencyFile(dependencyFilePath);
}
/**
* 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) {
System.out.println("No Dependency File Found");
return true;
}
// check for missing output first
if (missingTargetFile()) {
if (printStatus) {
System.out.println("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 BuildException();
case NEW_FILE:
if (printStatus) {
System.out.println("Found new input file");
}
return true;
case UPDATED_FILE:
if (printStatus) {
System.out.println("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 BuildException();
case MISSING_FILE:
if (printStatus) {
System.out.println("Found deleted input file");
}
return true;
case UPDATED_FILE:
if (printStatus) {
System.out.println("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 dependencyFilePath the dependency file
*/
private void parseDependencyFile(String dependencyFilePath) {
// first check if the dependency file is here.
File depFile = new File(dependencyFilePath);
if (!depFile.isFile()) {
mMissingDepFile = true;
return;
}
// get the modification time of the dep file as we may need it later
mDepFileLastModified = depFile.lastModified();
// Read in our dependency file
List<String> content = readFile(depFile);
if (content == null) {
System.err.println("ERROR: Couldn't read " + dependencyFilePath);
return;
}
// The format is something like:
// output1 output2 [...]: dep1 dep2 [...]
// expect it's likely split on several lines. So let's move it back on 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) {
System.err.println(
"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.isEmpty()) {
mTargets.add(new File(path));
}
}
mPrereqs = new HashSet<File>(prereqs.length);
for (String path : prereqs) {
if (!path.isEmpty()) {
if (DEBUG) {
System.out.println("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) {
System.err.println("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)) {
// turns out this is a new file!
if (DEBUG) {
System.out.println("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) {
System.out.println("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()) {
if (DEBUG) {
System.out.println("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) {
System.out.println(
"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) {
System.out.println(
"UPDATED FILE: " + prereq.getAbsolutePath());
}
return DependencyStatus.UPDATED_FILE;
}
}
}
}
}
} else {
// no input? we consider all files.
if (prereq.lastModified() > oldestTarget) {
if (DEBUG) {
System.out.println("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()) {
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.isEmpty()) {
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 to read
* @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;
}
}