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