• 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 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 
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         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      */
checkInputs(long oldestTarget)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      */
checkInputFolder(File folder, InputPath inputFolder, long oldestTarget)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 
checkInputFile(File file, InputPath inputFolder, long oldestTarget)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      */
checkPrereqFiles(long oldestTarget)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      */
missingTargetFile()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      */
getOutputLastModified()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      */
readFile(String filepath)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                 try {
440                     String line;
441                     StringBuilder total = new StringBuilder(reader.readLine());
442                     while ((line = reader.readLine()) != null) {
443                         total.append('\n');
444                         total.append(line);
445                     }
446                     return total.toString();
447                 } finally {
448                     reader.close();
449                 }
450             }
451         } catch (IOException e) {
452             // we'll just return null
453         }
454         return null;
455     }
456 }
457