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