/* * 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; } }