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 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