Home | History | Annotate | Download | only in ant
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ant;
     18 
     19 import org.apache.tools.ant.BuildException;
     20 
     21 import java.io.BufferedReader;
     22 import java.io.File;
     23 import java.io.FileInputStream;
     24 import java.io.IOException;
     25 import java.io.InputStreamReader;
     26 import java.util.Collections;
     27 import java.util.HashSet;
     28 import java.util.List;
     29 import java.util.Set;
     30 
     31 /**
     32  *  This class takes care of dependency tracking for all targets and prerequisites listed in
     33  *  a single dependency file. A dependency graph always has a dependency file associated with it
     34  *  for the duration of its lifetime
     35  */
     36 public class DependencyGraph {
     37 
     38     private final static boolean DEBUG = false;
     39 
     40     private static enum DependencyStatus {
     41         NONE, NEW_FILE, UPDATED_FILE, MISSING_FILE, ERROR;
     42     }
     43 
     44     // Files that we know about from the dependency file
     45     private Set<File> mTargets = Collections.emptySet();
     46     private Set<File> mPrereqs = mTargets;
     47     private File mFirstPrereq = null;
     48     private boolean mMissingDepFile = false;
     49     private long mDepFileLastModified;
     50     private final List<InputPath> mNewInputs;
     51 
     52     public DependencyGraph(String dependencyFilePath, List<InputPath> newInputPaths) {
     53         mNewInputs = newInputPaths;
     54         parseDependencyFile(dependencyFilePath);
     55     }
     56 
     57     /**
     58      * Check all the dependencies to see if anything has changed.
     59      *
     60      * @param printStatus will print to {@link System#out} the dependencies status.
     61      * @return true if new prerequisites have appeared, target files are missing or if
     62      *         prerequisite files have been modified since the last target generation.
     63      */
     64     public boolean dependenciesHaveChanged(boolean printStatus) {
     65         // If no dependency file has been set up, then we'll just return true
     66         // if we have a dependency file, we'll check to see what's been changed
     67         if (mMissingDepFile) {
     68             System.out.println("No Dependency File Found");
     69             return true;
     70         }
     71 
     72         // check for missing output first
     73         if (missingTargetFile()) {
     74             if (printStatus) {
     75                 System.out.println("Found Deleted Target File");
     76             }
     77             return true;
     78         }
     79 
     80         // get the time stamp of the oldest target.
     81         long oldestTarget = getOutputLastModified();
     82 
     83         // first look through the input folders and look for new files or modified files.
     84         DependencyStatus status = checkInputs(oldestTarget);
     85 
     86         // this can't find missing files. This is done later.
     87         switch (status) {
     88             case ERROR:
     89                 throw new BuildException();
     90             case NEW_FILE:
     91                 if (printStatus) {
     92                     System.out.println("Found new input file");
     93                 }
     94                 return true;
     95             case UPDATED_FILE:
     96                 if (printStatus) {
     97                     System.out.println("Found modified input file");
     98                 }
     99                 return true;
    100         }
    101 
    102         // now do a full check on the remaining files.
    103         status = checkPrereqFiles(oldestTarget);
    104         // this can't find new input files. This is done above.
    105         switch (status) {
    106             case ERROR:
    107                 throw new BuildException();
    108             case MISSING_FILE:
    109                 if (printStatus) {
    110                     System.out.println("Found deleted input file");
    111                 }
    112                 return true;
    113             case UPDATED_FILE:
    114                 if (printStatus) {
    115                     System.out.println("Found modified input file");
    116                 }
    117                 return true;
    118         }
    119 
    120         return false;
    121     }
    122 
    123     public Set<File> getTargets() {
    124         return Collections.unmodifiableSet(mTargets);
    125     }
    126 
    127     public File getFirstPrereq() {
    128         return mFirstPrereq;
    129     }
    130 
    131     /**
    132      * Parses the given dependency file and stores the file paths
    133      *
    134      * @param dependencyFilePath the dependency file
    135      */
    136     private void parseDependencyFile(String dependencyFilePath) {
    137         // first check if the dependency file is here.
    138         File depFile = new File(dependencyFilePath);
    139         if (depFile.isFile() == false) {
    140             mMissingDepFile = true;
    141             return;
    142         }
    143 
    144         // get the modification time of the dep file as we may need it later
    145         mDepFileLastModified = depFile.lastModified();
    146 
    147         // Read in our dependency file
    148         String content = readFile(dependencyFilePath);
    149         if (content == null) {
    150             System.err.println("ERROR: Couldn't read " + dependencyFilePath);
    151             return;
    152         }
    153 
    154         // The format is something like:
    155         // output1 output2 [...]: dep1 dep2 [...]
    156         // expect it's likely split on several lines. So let's move it back on a single line
    157         // first
    158         String[] lines = content.toString().split("\n");
    159         StringBuilder sb = new StringBuilder(content.length());
    160         for (String line : lines) {
    161             line = line.trim();
    162             if (line.endsWith("\\")) {
    163                 line = line.substring(0, line.length() - 1);
    164             }
    165             sb.append(line);
    166         }
    167 
    168         // split the left and right part
    169         String[] files = sb.toString().split(":");
    170 
    171         // get the target files:
    172         String[] targets = files[0].trim().split(" ");
    173 
    174         String[] prereqs = {};
    175         // Check to make sure our dependency file is okay
    176         if (files.length < 1) {
    177             System.err.println(
    178                     "Warning! Dependency file does not list any prerequisites after ':' ");
    179         } else {
    180             // and the prerequisite files:
    181             prereqs = files[1].trim().split(" ");
    182         }
    183 
    184         mTargets = new HashSet<File>(targets.length);
    185         for (String path : targets) {
    186             if (path.length() > 0) {
    187                 mTargets.add(new File(path));
    188             }
    189         }
    190 
    191         mPrereqs = new HashSet<File>(prereqs.length);
    192         for (String path : prereqs) {
    193             if (path.length() > 0) {
    194                 if (DEBUG) {
    195                     System.out.println("PREREQ: " + path);
    196                 }
    197                 File f = new File(path);
    198                 if (mFirstPrereq == null) {
    199                     mFirstPrereq = f;
    200                 }
    201                 mPrereqs.add(f);
    202             }
    203         }
    204     }
    205 
    206     /**
    207      * Check all the input files and folders to see if there have been new
    208      * files added to them or if any of the existing files have been modified.
    209      *
    210      * This looks at the input paths, not at the list of known prereq. Therefore this
    211      * will not find missing files. It will however remove processed files from the
    212      * prereq file list so that we can process those in a 2nd step.
    213      *
    214      * This should be followed by a call to {@link #checkPrereqFiles(long)} which
    215      * will process the remaining files in the prereq list.
    216      *
    217      * If a change is found, this will return immediately with either
    218      * {@link DependencyStatus#NEW_FILE} or {@link DependencyStatus#UPDATED_FILE}.
    219      *
    220      * @param oldestTarget the timestamp of the oldest output file to compare against.
    221      *
    222      * @return the status of the file in the watched folders.
    223      *
    224      */
    225     private DependencyStatus checkInputs(long oldestTarget) {
    226         if (mNewInputs != null) {
    227             for (InputPath input : mNewInputs) {
    228                 File file = input.getFile();
    229                 if (file.isDirectory()) {
    230                     DependencyStatus status = checkInputFolder(file, input, oldestTarget);
    231                     if (status != DependencyStatus.NONE) {
    232                         return status;
    233                     }
    234                 } else if (file.isFile()) {
    235                     DependencyStatus status = checkInputFile(file, input, oldestTarget);
    236                     if (status != DependencyStatus.NONE) {
    237                         return status;
    238                     }
    239                 }
    240             }
    241         }
    242 
    243         // If we make it all the way through our directories we're good.
    244         return DependencyStatus.NONE;
    245     }
    246 
    247     /**
    248      * Check all the files in the tree under root and check to see if the files are
    249      * listed under the dependencies, or if they have been modified. Recurses into subdirs.
    250      *
    251      * @param folder the folder to search through.
    252      * @param inputFolder the root level inputFolder
    253      * @param oldestTarget the time stamp of the oldest output file to compare against.
    254      *
    255      * @return the status of the file in the folder.
    256      */
    257     private DependencyStatus checkInputFolder(File folder, InputPath inputFolder,
    258             long oldestTarget) {
    259         if (inputFolder.ignores(folder)) {
    260             return DependencyStatus.NONE;
    261         }
    262 
    263         File[] files = folder.listFiles();
    264         if (files == null) {
    265             System.err.println("ERROR " + folder.toString() + " is not a dir or can't be read");
    266             return DependencyStatus.ERROR;
    267         }
    268         // Loop through files in this folder
    269         for (File file : files) {
    270             // If this is a directory, recurse into it
    271             if (file.isDirectory()) {
    272                 DependencyStatus status = checkInputFolder(file, inputFolder, oldestTarget);
    273                 if (status != DependencyStatus.NONE) {
    274                     return status;
    275                 }
    276             } else if (file.isFile()) {
    277                 DependencyStatus status = checkInputFile(file, inputFolder, oldestTarget);
    278                 if (status != DependencyStatus.NONE) {
    279                     return status;
    280                 }
    281             }
    282         }
    283         // If we got to here then we didn't find anything interesting
    284         return DependencyStatus.NONE;
    285     }
    286 
    287     private DependencyStatus checkInputFile(File file, InputPath inputFolder,
    288             long oldestTarget) {
    289         if (inputFolder.ignores(file)) {
    290             return DependencyStatus.NONE;
    291         }
    292 
    293         // if it's a file, remove it from the list of prereqs.
    294         // This way if files in this folder don't trigger a build we'll have less
    295         // files to go through manually
    296         if (mPrereqs.remove(file) == false) {
    297             // turns out this is a new file!
    298 
    299             if (DEBUG) {
    300                 System.out.println("NEW FILE: " + file.getAbsolutePath());
    301             }
    302             return DependencyStatus.NEW_FILE;
    303         } else {
    304             // check the time stamp on this file if it's a file we care about based what the
    305             // input folder decides.
    306             if (inputFolder.checksForModification(file)) {
    307                 if (file.lastModified() > oldestTarget) {
    308                     if (DEBUG) {
    309                         System.out.println("UPDATED FILE: " + file.getAbsolutePath());
    310                     }
    311                     return DependencyStatus.UPDATED_FILE;
    312                 }
    313             }
    314         }
    315 
    316         return DependencyStatus.NONE;
    317     }
    318 
    319     /**
    320      * Check all the prereq files we know about to make sure they're still there, or that they
    321      * haven't been modified since the last build.
    322      *
    323      * @param oldestTarget the time stamp of the oldest output file to compare against.
    324      *
    325      * @return the status of the files
    326      */
    327     private DependencyStatus checkPrereqFiles(long oldestTarget) {
    328         // TODO: Optimize for the case of a specific file as inputPath.
    329         //       We should have a map of filepath to inputpath to quickly search through them?
    330 
    331         // Loop through our prereq files and make sure they still exist
    332         for (File prereq : mPrereqs) {
    333             if (prereq.exists() == false) {
    334                 if (DEBUG) {
    335                     System.out.println("MISSING FILE: " + prereq.getAbsolutePath());
    336                 }
    337                 return DependencyStatus.MISSING_FILE;
    338             }
    339 
    340             // check the time stamp on this file if it's a file we care about.
    341             // To know if we care about the file we have to find the matching input.
    342             if (mNewInputs != null) {
    343                 String filePath = prereq.getAbsolutePath();
    344                 for (InputPath input : mNewInputs) {
    345                     File inputFile = input.getFile();
    346                     // if the input path is a directory, check if the prereq file is in it,
    347                     // otherwise check if the prereq file match exactly the input path.
    348                     if (inputFile.isDirectory()) {
    349                         if (filePath.startsWith(inputFile.getAbsolutePath())) {
    350                             // ok file is inside a directory type input folder.
    351                             // check if we need to check this type of file, and if yes, check it.
    352                             if (input.checksForModification(prereq)) {
    353                                 if (prereq.lastModified() > oldestTarget) {
    354                                     if (DEBUG) {
    355                                         System.out.println(
    356                                                 "UPDATED FILE: " + prereq.getAbsolutePath());
    357                                     }
    358                                     return DependencyStatus.UPDATED_FILE;
    359                                 }
    360                             }
    361                         }
    362                     } else {
    363                         // this is a file input path, we must check if the match is exact.
    364                         if (prereq.equals(inputFile)) {
    365                             if (input.checksForModification(prereq)) {
    366                                 if (prereq.lastModified() > oldestTarget) {
    367                                     if (DEBUG) {
    368                                         System.out.println(
    369                                                 "UPDATED FILE: " + prereq.getAbsolutePath());
    370                                     }
    371                                     return DependencyStatus.UPDATED_FILE;
    372                                 }
    373                             }
    374                         }
    375                     }
    376                 }
    377             } else {
    378                 // no input? we consider all files.
    379                 if (prereq.lastModified() > oldestTarget) {
    380                     if (DEBUG) {
    381                         System.out.println("UPDATED FILE: " + prereq.getAbsolutePath());
    382                     }
    383                     return DependencyStatus.UPDATED_FILE;
    384                 }
    385             }
    386         }
    387 
    388         // If we get this far, then all our prereq are okay
    389         return DependencyStatus.NONE;
    390     }
    391 
    392     /**
    393      * Check all the target files we know about to make sure they're still there
    394      * @return true if any of the target files are missing.
    395      */
    396     private boolean missingTargetFile() {
    397         // Loop through our target files and make sure they still exist
    398         for (File target : mTargets) {
    399             if (target.exists() == false) {
    400                 return true;
    401             }
    402         }
    403         // If we get this far, then all our targets are okay
    404         return false;
    405     }
    406 
    407     /**
    408      * Returns the earliest modification time stamp from all the output targets. If there
    409      * are no known target, the dependency file time stamp is returned.
    410      */
    411     private long getOutputLastModified() {
    412         // Find the oldest target
    413         long oldestTarget = Long.MAX_VALUE;
    414         // if there's no output, then compare to the time of the dependency file.
    415         if (mTargets.size() == 0) {
    416             oldestTarget = mDepFileLastModified;
    417         } else {
    418             for (File target : mTargets) {
    419                 if (target.lastModified() < oldestTarget) {
    420                     oldestTarget = target.lastModified();
    421                 }
    422             }
    423         }
    424 
    425         return oldestTarget;
    426     }
    427 
    428     /**
    429      * Reads and returns the content of a text file.
    430      * @param filepath the file path to the text file
    431      * @return null if the file could not be read
    432      */
    433     private static String readFile(String filepath) {
    434         try {
    435             FileInputStream fStream = new FileInputStream(filepath);
    436             if (fStream != null) {
    437                 BufferedReader reader = new BufferedReader(new InputStreamReader(fStream));
    438 
    439                 String line;
    440                 StringBuilder total = new StringBuilder(reader.readLine());
    441                 while ((line = reader.readLine()) != null) {
    442                     total.append('\n');
    443                     total.append(line);
    444                 }
    445                 return total.toString();
    446             }
    447         } catch (IOException e) {
    448             // we'll just return null
    449         }
    450         return null;
    451     }
    452 }
    453