• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 com.google.common.base.Charsets;
20 import com.google.common.io.Files;
21 
22 import org.apache.tools.ant.BuildException;
23 
24 import java.io.File;
25 import java.io.IOException;
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 
DependencyGraph(String dependencyFilePath, List<InputPath> newInputPaths)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      */
dependenciesHaveChanged(boolean printStatus)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 
getTargets()123     public Set<File> getTargets() {
124         return Collections.unmodifiableSet(mTargets);
125     }
126 
getFirstPrereq()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      */
parseDependencyFile(String dependencyFilePath)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         List<String> content = readFile(depFile);
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         StringBuilder sb = new StringBuilder();
159         for (String line : content) {
160             line = line.trim();
161             if (line.endsWith("\\")) {
162                 line = line.substring(0, line.length() - 1);
163             }
164             sb.append(line);
165         }
166 
167         // split the left and right part
168         String[] files = sb.toString().split(":");
169 
170         // get the target files:
171         String[] targets = files[0].trim().split(" ");
172 
173         String[] prereqs = {};
174         // Check to make sure our dependency file is okay
175         if (files.length < 1) {
176             System.err.println(
177                     "Warning! Dependency file does not list any prerequisites after ':' ");
178         } else {
179             // and the prerequisite files:
180             prereqs = files[1].trim().split(" ");
181         }
182 
183         mTargets = new HashSet<File>(targets.length);
184         for (String path : targets) {
185             if (path.length() > 0) {
186                 mTargets.add(new File(path));
187             }
188         }
189 
190         mPrereqs = new HashSet<File>(prereqs.length);
191         for (String path : prereqs) {
192             if (path.length() > 0) {
193                 if (DEBUG) {
194                     System.out.println("PREREQ: " + path);
195                 }
196                 File f = new File(path);
197                 if (mFirstPrereq == null) {
198                     mFirstPrereq = f;
199                 }
200                 mPrereqs.add(f);
201             }
202         }
203     }
204 
205     /**
206      * Check all the input files and folders to see if there have been new
207      * files added to them or if any of the existing files have been modified.
208      *
209      * This looks at the input paths, not at the list of known prereq. Therefore this
210      * will not find missing files. It will however remove processed files from the
211      * prereq file list so that we can process those in a 2nd step.
212      *
213      * This should be followed by a call to {@link #checkPrereqFiles(long)} which
214      * will process the remaining files in the prereq list.
215      *
216      * If a change is found, this will return immediately with either
217      * {@link DependencyStatus#NEW_FILE} or {@link DependencyStatus#UPDATED_FILE}.
218      *
219      * @param oldestTarget the timestamp of the oldest output file to compare against.
220      *
221      * @return the status of the file in the watched folders.
222      *
223      */
checkInputs(long oldestTarget)224     private DependencyStatus checkInputs(long oldestTarget) {
225         if (mNewInputs != null) {
226             for (InputPath input : mNewInputs) {
227                 File file = input.getFile();
228                 if (file.isDirectory()) {
229                     DependencyStatus status = checkInputFolder(file, input, oldestTarget);
230                     if (status != DependencyStatus.NONE) {
231                         return status;
232                     }
233                 } else if (file.isFile()) {
234                     DependencyStatus status = checkInputFile(file, input, oldestTarget);
235                     if (status != DependencyStatus.NONE) {
236                         return status;
237                     }
238                 }
239             }
240         }
241 
242         // If we make it all the way through our directories we're good.
243         return DependencyStatus.NONE;
244     }
245 
246     /**
247      * Check all the files in the tree under root and check to see if the files are
248      * listed under the dependencies, or if they have been modified. Recurses into subdirs.
249      *
250      * @param folder the folder to search through.
251      * @param inputFolder the root level inputFolder
252      * @param oldestTarget the time stamp of the oldest output file to compare against.
253      *
254      * @return the status of the file in the folder.
255      */
checkInputFolder(File folder, InputPath inputFolder, long oldestTarget)256     private DependencyStatus checkInputFolder(File folder, InputPath inputFolder,
257             long oldestTarget) {
258         if (inputFolder.ignores(folder)) {
259             return DependencyStatus.NONE;
260         }
261 
262         File[] files = folder.listFiles();
263         if (files == null) {
264             System.err.println("ERROR " + folder.toString() + " is not a dir or can't be read");
265             return DependencyStatus.ERROR;
266         }
267         // Loop through files in this folder
268         for (File file : files) {
269             // If this is a directory, recurse into it
270             if (file.isDirectory()) {
271                 DependencyStatus status = checkInputFolder(file, inputFolder, oldestTarget);
272                 if (status != DependencyStatus.NONE) {
273                     return status;
274                 }
275             } else if (file.isFile()) {
276                 DependencyStatus status = checkInputFile(file, inputFolder, oldestTarget);
277                 if (status != DependencyStatus.NONE) {
278                     return status;
279                 }
280             }
281         }
282         // If we got to here then we didn't find anything interesting
283         return DependencyStatus.NONE;
284     }
285 
checkInputFile(File file, InputPath inputFolder, long oldestTarget)286     private DependencyStatus checkInputFile(File file, InputPath inputFolder,
287             long oldestTarget) {
288         if (inputFolder.ignores(file)) {
289             return DependencyStatus.NONE;
290         }
291 
292         // if it's a file, remove it from the list of prereqs.
293         // This way if files in this folder don't trigger a build we'll have less
294         // files to go through manually
295         if (mPrereqs.remove(file) == false) {
296             // turns out this is a new file!
297 
298             if (DEBUG) {
299                 System.out.println("NEW FILE: " + file.getAbsolutePath());
300             }
301             return DependencyStatus.NEW_FILE;
302         } else {
303             // check the time stamp on this file if it's a file we care about based what the
304             // input folder decides.
305             if (inputFolder.checksForModification(file)) {
306                 if (file.lastModified() > oldestTarget) {
307                     if (DEBUG) {
308                         System.out.println("UPDATED FILE: " + file.getAbsolutePath());
309                     }
310                     return DependencyStatus.UPDATED_FILE;
311                 }
312             }
313         }
314 
315         return DependencyStatus.NONE;
316     }
317 
318     /**
319      * Check all the prereq files we know about to make sure they're still there, or that they
320      * haven't been modified since the last build.
321      *
322      * @param oldestTarget the time stamp of the oldest output file to compare against.
323      *
324      * @return the status of the files
325      */
checkPrereqFiles(long oldestTarget)326     private DependencyStatus checkPrereqFiles(long oldestTarget) {
327         // TODO: Optimize for the case of a specific file as inputPath.
328         //       We should have a map of filepath to inputpath to quickly search through them?
329 
330         // Loop through our prereq files and make sure they still exist
331         for (File prereq : mPrereqs) {
332             if (prereq.exists() == false) {
333                 if (DEBUG) {
334                     System.out.println("MISSING FILE: " + prereq.getAbsolutePath());
335                 }
336                 return DependencyStatus.MISSING_FILE;
337             }
338 
339             // check the time stamp on this file if it's a file we care about.
340             // To know if we care about the file we have to find the matching input.
341             if (mNewInputs != null) {
342                 String filePath = prereq.getAbsolutePath();
343                 for (InputPath input : mNewInputs) {
344                     File inputFile = input.getFile();
345                     // if the input path is a directory, check if the prereq file is in it,
346                     // otherwise check if the prereq file match exactly the input path.
347                     if (inputFile.isDirectory()) {
348                         if (filePath.startsWith(inputFile.getAbsolutePath())) {
349                             // ok file is inside a directory type input folder.
350                             // check if we need to check this type of file, and if yes, check it.
351                             if (input.checksForModification(prereq)) {
352                                 if (prereq.lastModified() > oldestTarget) {
353                                     if (DEBUG) {
354                                         System.out.println(
355                                                 "UPDATED FILE: " + prereq.getAbsolutePath());
356                                     }
357                                     return DependencyStatus.UPDATED_FILE;
358                                 }
359                             }
360                         }
361                     } else {
362                         // this is a file input path, we must check if the match is exact.
363                         if (prereq.equals(inputFile)) {
364                             if (input.checksForModification(prereq)) {
365                                 if (prereq.lastModified() > oldestTarget) {
366                                     if (DEBUG) {
367                                         System.out.println(
368                                                 "UPDATED FILE: " + prereq.getAbsolutePath());
369                                     }
370                                     return DependencyStatus.UPDATED_FILE;
371                                 }
372                             }
373                         }
374                     }
375                 }
376             } else {
377                 // no input? we consider all files.
378                 if (prereq.lastModified() > oldestTarget) {
379                     if (DEBUG) {
380                         System.out.println("UPDATED FILE: " + prereq.getAbsolutePath());
381                     }
382                     return DependencyStatus.UPDATED_FILE;
383                 }
384             }
385         }
386 
387         // If we get this far, then all our prereq are okay
388         return DependencyStatus.NONE;
389     }
390 
391     /**
392      * Check all the target files we know about to make sure they're still there
393      * @return true if any of the target files are missing.
394      */
missingTargetFile()395     private boolean missingTargetFile() {
396         // Loop through our target files and make sure they still exist
397         for (File target : mTargets) {
398             if (target.exists() == false) {
399                 return true;
400             }
401         }
402         // If we get this far, then all our targets are okay
403         return false;
404     }
405 
406     /**
407      * Returns the earliest modification time stamp from all the output targets. If there
408      * are no known target, the dependency file time stamp is returned.
409      */
getOutputLastModified()410     private long getOutputLastModified() {
411         // Find the oldest target
412         long oldestTarget = Long.MAX_VALUE;
413         // if there's no output, then compare to the time of the dependency file.
414         if (mTargets.size() == 0) {
415             oldestTarget = mDepFileLastModified;
416         } else {
417             for (File target : mTargets) {
418                 if (target.lastModified() < oldestTarget) {
419                     oldestTarget = target.lastModified();
420                 }
421             }
422         }
423 
424         return oldestTarget;
425     }
426 
427     /**
428      * Reads and returns the content of a text file.
429      * @param file the file to read
430      * @return null if the file could not be read
431      */
readFile(File file)432     private static List<String> readFile(File file) {
433         try {
434             return Files.readLines(file, Charsets.UTF_8);
435         } catch (IOException e) {
436             // return null below
437         }
438 
439         return null;
440     }
441 }
442