/** * Determines whether an action is outdated based on the modification times for its prerequisite * and produced items. * @param RootAction - The action being considered. * @param OutdatedActionDictionary - * @return true if outdated */ public static bool IsActionOutdated(UEBuildTarget Target, Action RootAction, ref Dictionary<Action, bool> OutdatedActionDictionary,ActionHistory ActionHistory, Dictionary<UEBuildTarget, List<FileItem>> TargetToOutdatedPrerequisitesMap ) { // Only compute the outdated-ness for actions that don't aren't cached in the outdated action dictionary. bool bIsOutdated = false; if (!OutdatedActionDictionary.TryGetValue(RootAction, out bIsOutdated)) { // Determine the last time the action was run based on the write times of its produced files. string LatestUpdatedProducedItemName = null; DateTimeOffset LastExecutionTime = DateTimeOffset.MaxValue; foreach (FileItem ProducedItem in RootAction.ProducedItems) { // Optionally skip the action history check, as this only works for local builds if (BuildConfiguration.bUseActionHistory) { // Check if the command-line of the action previously used to produce the item is outdated. string OldProducingCommandLine = ""; string NewProducingCommandLine = RootAction.CommandPath + " " + RootAction.CommandArguments; if (!ActionHistory.GetProducingCommandLine(ProducedItem, out OldProducingCommandLine) || !String.Equals(OldProducingCommandLine, NewProducingCommandLine, StringComparison.InvariantCultureIgnoreCase)) { Log.TraceVerbose( "{0}: Produced item \"{1}\" was produced by outdated command-line.\nOld command-line: {2}\nNew command-line: {3}", RootAction.StatusDescription, Path.GetFileName(ProducedItem.AbsolutePath), OldProducingCommandLine, NewProducingCommandLine ); bIsOutdated = true; // Update the command-line used to produce this item in the action history. ActionHistory.SetProducingCommandLine(ProducedItem, NewProducingCommandLine); } } // If the produced file doesn't exist or has zero size, consider it outdated. The zero size check is to detect cases // where aborting an earlier compile produced invalid zero-sized obj files, but that may cause actions where that's // legitimate output to always be considered outdated. if (ProducedItem.bExists && (ProducedItem.bIsRemoteFile || ProducedItem.Length > 0 || ProducedItem.IsDirectory)) { // When linking incrementally, don't use LIB, EXP pr PDB files when checking for the oldest produced item, // as those files aren't always touched. if( BuildConfiguration.bUseIncrementalLinking ) { String ProducedItemExtension = Path.GetExtension( ProducedItem.AbsolutePath ).ToUpperInvariant(); if( ProducedItemExtension == ".LIB" || ProducedItemExtension == ".EXP" || ProducedItemExtension == ".PDB" ) { continue; } } // Use the oldest produced item's time as the last execution time. if (ProducedItem.LastWriteTime < LastExecutionTime) { LastExecutionTime = ProducedItem.LastWriteTime; LatestUpdatedProducedItemName = ProducedItem.AbsolutePath; } } else { // If any of the produced items doesn't exist, the action is outdated. Log.TraceVerbose( "{0}: Produced item \"{1}\" doesn't exist.", RootAction.StatusDescription, Path.GetFileName(ProducedItem.AbsolutePath) ); bIsOutdated = true; } } Log.WriteLineIf(BuildConfiguration.bLogDetailedActionStats && !String.IsNullOrEmpty( LatestUpdatedProducedItemName ), TraceEventType.Verbose, "{0}: Oldest produced item is {1}", RootAction.StatusDescription, LatestUpdatedProducedItemName); bool bFindCPPIncludePrerequisites = false; if( RootAction.ActionType == ActionType.Compile ) { // Outdated targets don't need their headers scanned yet, because presumably they would already be out of dated based on already-cached // includes before getting this far. However, if we find them to be outdated after processing includes, we'll do a deep scan later // on and cache all of the includes so that we have them for a quick outdatedness check the next run. if( !bIsOutdated && BuildConfiguration.bUseExperimentalFastBuildIteration && UnrealBuildTool.IsAssemblingBuild && RootAction.ActionType == ActionType.Compile ) { bFindCPPIncludePrerequisites = true; } // Were we asked to force an update of our cached includes BEFORE we try to build? This may be needed if our cache can no longer // be trusted and we need to fill it with perfectly valid data (even if we're in assembler only mode) if( BuildConfiguration.bUseExperimentalFastDependencyScan && UnrealBuildTool.bNeedsFullCPPIncludeRescan ) { bFindCPPIncludePrerequisites = true; } } if( bFindCPPIncludePrerequisites ) { // Scan this file for included headers that may be out of date. Note that it's OK if we break out early because we found // the action to be outdated. For outdated actions, we kick off a separate include scan in a background thread later on to // catch all of the other includes and form an exhaustive set. foreach (FileItem PrerequisiteItem in RootAction.PrerequisiteItems) { // @todo ubtmake: Make sure we are catching RC files here too. Anything that the toolchain would have tried it on. Logic should match the CACHING stuff below if( PrerequisiteItem.CachedCPPIncludeInfo != null ) { var BuildPlatform = UEBuildPlatform.GetBuildPlatform( Target.GetTargetInfo().Platform ); var IncludedFileList = CPPEnvironment.FindAndCacheAllIncludedFiles( Target, PrerequisiteItem, BuildPlatform, PrerequisiteItem.CachedCPPIncludeInfo, bOnlyCachedDependencies:BuildConfiguration.bUseExperimentalFastDependencyScan ); foreach( var IncludedFile in IncludedFileList ) // @todo fastubt: @todo ubtmake: Optimization: This is "retesting" a lot of the same files over and over in a single run (common indirect includes) { if( IncludedFile.bExists ) { // allow a 1 second slop for network copies TimeSpan TimeDifference = IncludedFile.LastWriteTime - LastExecutionTime; bool bPrerequisiteItemIsNewerThanLastExecution = TimeDifference.TotalSeconds > 1; if (bPrerequisiteItemIsNewerThanLastExecution) { Log.TraceVerbose( "{0}: Included file {1} is newer than the last execution of the action: {2} vs {3}", RootAction.StatusDescription, Path.GetFileName(IncludedFile.AbsolutePath), IncludedFile.LastWriteTime.LocalDateTime, LastExecutionTime.LocalDateTime ); bIsOutdated = true; // Don't bother checking every single include if we've found one that is out of date break; } } } } if( bIsOutdated ) { break; } } } if(!bIsOutdated) { // Check if any of the prerequisite items are produced by outdated actions, or have changed more recently than // the oldest produced item. foreach (FileItem PrerequisiteItem in RootAction.PrerequisiteItems) { // Only check for outdated import libraries if we were configured to do so. Often, a changed import library // won't affect a dependency unless a public header file was also changed, in which case we would be forced // to recompile anyway. This just allows for faster iteration when working on a subsystem in a DLL, as we // won't have to wait for dependent targets to be relinked after each change. bool bIsImportLibraryFile = false; if( PrerequisiteItem.ProducingAction != null && PrerequisiteItem.ProducingAction.bProducesImportLibrary ) { bIsImportLibraryFile = PrerequisiteItem.AbsolutePath.EndsWith( ".LIB", StringComparison.InvariantCultureIgnoreCase ); } if( !bIsImportLibraryFile || !BuildConfiguration.bIgnoreOutdatedImportLibraries ) { // If the prerequisite is produced by an outdated action, then this action is outdated too. if( PrerequisiteItem.ProducingAction != null ) { if(IsActionOutdated(Target, PrerequisiteItem.ProducingAction,ref OutdatedActionDictionary,ActionHistory, TargetToOutdatedPrerequisitesMap)) { Log.TraceVerbose( "{0}: Prerequisite {1} is produced by outdated action.", RootAction.StatusDescription, Path.GetFileName(PrerequisiteItem.AbsolutePath) ); bIsOutdated = true; } } if( PrerequisiteItem.bExists ) { // allow a 1 second slop for network copies TimeSpan TimeDifference = PrerequisiteItem.LastWriteTime - LastExecutionTime; bool bPrerequisiteItemIsNewerThanLastExecution = TimeDifference.TotalSeconds > 1; if (bPrerequisiteItemIsNewerThanLastExecution) { Log.TraceVerbose( "{0}: Prerequisite {1} is newer than the last execution of the action: {2} vs {3}", RootAction.StatusDescription, Path.GetFileName(PrerequisiteItem.AbsolutePath), PrerequisiteItem.LastWriteTime.LocalDateTime, LastExecutionTime.LocalDateTime ); bIsOutdated = true; } } // GatherAllOutdatedActions will ensure all actions are checked for outdated-ness, so we don't need to recurse with // all this action's prerequisites once we've determined it's outdated. if (bIsOutdated) { break; } } } } // For compile actions, we have C++ files that are actually dependent on header files that could have been changed. We only need to // know about the set of header files that are included for files that are already determined to be out of date (such as if the file // is missing or was modified.) In the case that the file is out of date, we'll perform a deep scan to update our cached set of // includes for this file, so that we'll be able to determine whether it is out of date next time very quickly. if( BuildConfiguration.bUseExperimentalFastDependencyScan ) { var DeepIncludeScanStartTime = DateTime.UtcNow; // @todo fastubt: we may be scanning more files than we need to here -- indirectly outdated files are bIsOutdated=true by this point (for example basemost includes when deeper includes are dirty) if( bIsOutdated && RootAction.ActionType == ActionType.Compile ) // @todo fastubt: Does this work with RC files? See above too. { Log.TraceVerbose( "Outdated action: {0}", RootAction.StatusDescription ); foreach (FileItem PrerequisiteItem in RootAction.PrerequisiteItems) { if( PrerequisiteItem.CachedCPPIncludeInfo != null ) { if( !IsCPPFile( PrerequisiteItem ) ) { throw new BuildException( "Was only expecting C++ files to have CachedCPPEnvironments!" ); } Log.TraceVerbose( " -> DEEP include scan: {0}", PrerequisiteItem.AbsolutePath ); List<FileItem> OutdatedPrerequisites; if( !TargetToOutdatedPrerequisitesMap.TryGetValue( Target, out OutdatedPrerequisites ) ) { OutdatedPrerequisites = new List<FileItem>(); TargetToOutdatedPrerequisitesMap.Add( Target, OutdatedPrerequisites ); } OutdatedPrerequisites.Add( PrerequisiteItem ); } else if( IsCPPImplementationFile( PrerequisiteItem ) || IsCPPResourceFile( PrerequisiteItem ) ) { if( PrerequisiteItem.CachedCPPIncludeInfo == null ) { Log.TraceVerbose( " -> WARNING: No CachedCPPEnvironment: {0}", PrerequisiteItem.AbsolutePath ); } } } } if( BuildConfiguration.bPrintPerformanceInfo ) { double DeepIncludeScanTime = ( DateTime.UtcNow - DeepIncludeScanStartTime ).TotalSeconds; UnrealBuildTool.TotalDeepIncludeScanTime += DeepIncludeScanTime; } } // Cache the outdated-ness of this action. OutdatedActionDictionary.Add(RootAction, bIsOutdated); } return bIsOutdated; }