/// <summary> /// Loads a UBTMakefile from disk /// </summary> /// <param name="MakefilePath">Path to the makefile to load</param> /// <param name="ProjectFile">Path to the project file</param> /// <param name="ReasonNotLoaded">If the function returns null, this string will contain the reason why</param> /// <param name="WorkingSet">Interface to query which source files are in the working set</param> /// <returns>The loaded makefile, or null if it failed for some reason. On failure, the 'ReasonNotLoaded' variable will contain information about why</returns> public static UBTMakefile LoadUBTMakefile(FileReference MakefilePath, FileReference ProjectFile, ISourceFileWorkingSet WorkingSet, out string ReasonNotLoaded) { // Check the directory timestamp on the project files directory. If the user has generated project files more // recently than the UBTMakefile, then we need to consider the file to be out of date FileInfo UBTMakefileInfo = new FileInfo(MakefilePath.FullName); if (!UBTMakefileInfo.Exists) { // UBTMakefile doesn't even exist, so we won't bother loading it ReasonNotLoaded = "no existing makefile"; return(null); } // Check the build version FileInfo BuildVersionFileInfo = new FileInfo(BuildVersion.GetDefaultFileName().FullName); if (BuildVersionFileInfo.Exists && UBTMakefileInfo.LastWriteTime.CompareTo(BuildVersionFileInfo.LastWriteTime) < 0) { Log.TraceVerbose("Existing makefile is older than Build.version, ignoring it"); ReasonNotLoaded = "Build.version is newer"; return(null); } // @todo ubtmake: This will only work if the directory timestamp actually changes with every single GPF. Force delete existing files before creating new ones? Eh... really we probably just want to delete + create a file in that folder // -> UPDATE: Seems to work OK right now though on Windows platform, maybe due to GUID changes // @todo ubtmake: Some platforms may not save any files into this folder. We should delete + generate a "touch" file to force the directory timestamp to be updated (or just check the timestamp file itself. We could put it ANYWHERE, actually) // Installed Build doesn't need to check engine projects for outdatedness if (!UnrealBuildTool.IsEngineInstalled()) { if (DirectoryReference.Exists(ProjectFileGenerator.IntermediateProjectFilesPath)) { DateTime EngineProjectFilesLastUpdateTime = new FileInfo(ProjectFileGenerator.ProjectTimestampFile).LastWriteTime; if (UBTMakefileInfo.LastWriteTime.CompareTo(EngineProjectFilesLastUpdateTime) < 0) { // Engine project files are newer than UBTMakefile Log.TraceVerbose("Existing makefile is older than generated engine project files, ignoring it"); ReasonNotLoaded = "project files are newer"; return(null); } } } // Check the game project directory too if (ProjectFile != null) { string ProjectFilename = ProjectFile.FullName; FileInfo ProjectFileInfo = new FileInfo(ProjectFilename); if (!ProjectFileInfo.Exists || UBTMakefileInfo.LastWriteTime.CompareTo(ProjectFileInfo.LastWriteTime) < 0) { // .uproject file is newer than UBTMakefile Log.TraceVerbose("Makefile is older than .uproject file, ignoring it"); ReasonNotLoaded = ".uproject file is newer"; return(null); } DirectoryReference MasterProjectRelativePath = ProjectFile.Directory; string GameIntermediateProjectFilesPath = Path.Combine(MasterProjectRelativePath.FullName, "Intermediate", "ProjectFiles"); if (Directory.Exists(GameIntermediateProjectFilesPath)) { DateTime GameProjectFilesLastUpdateTime = new DirectoryInfo(GameIntermediateProjectFilesPath).LastWriteTime; if (UBTMakefileInfo.LastWriteTime.CompareTo(GameProjectFilesLastUpdateTime) < 0) { // Game project files are newer than UBTMakefile Log.TraceVerbose("Makefile is older than generated game project files, ignoring it"); ReasonNotLoaded = "game project files are newer"; return(null); } } } // Check to see if UnrealBuildTool.exe was compiled more recently than the UBTMakefile DateTime UnrealBuildToolTimestamp = new FileInfo(Assembly.GetExecutingAssembly().Location).LastWriteTime; if (UBTMakefileInfo.LastWriteTime.CompareTo(UnrealBuildToolTimestamp) < 0) { // UnrealBuildTool.exe was compiled more recently than the UBTMakefile Log.TraceVerbose("Makefile is older than UnrealBuildTool.exe, ignoring it"); ReasonNotLoaded = "UnrealBuildTool.exe is newer"; return(null); } // Check to see if any BuildConfiguration files have changed since the last build List <XmlConfig.InputFile> InputFiles = XmlConfig.FindInputFiles(); foreach (XmlConfig.InputFile InputFile in InputFiles) { FileInfo InputFileInfo = new FileInfo(InputFile.Location.FullName); if (InputFileInfo.LastWriteTime > UBTMakefileInfo.LastWriteTime) { Log.TraceVerbose("Makefile is older than BuildConfiguration.xml, ignoring it"); ReasonNotLoaded = "BuildConfiguration.xml is newer"; return(null); } } UBTMakefile LoadedUBTMakefile = null; try { DateTime LoadUBTMakefileStartTime = DateTime.UtcNow; using (FileStream Stream = new FileStream(UBTMakefileInfo.FullName, FileMode.Open, FileAccess.Read)) { BinaryFormatter Formatter = new BinaryFormatter(); LoadedUBTMakefile = Formatter.Deserialize(Stream) as UBTMakefile; } if (UnrealBuildTool.bPrintPerformanceInfo) { double LoadUBTMakefileTime = (DateTime.UtcNow - LoadUBTMakefileStartTime).TotalSeconds; Log.TraceInformation("LoadUBTMakefile took " + LoadUBTMakefileTime + "s"); } } catch (Exception Ex) { Log.TraceWarning("Failed to read makefile: {0}", Ex.Message); ReasonNotLoaded = "couldn't read existing makefile"; return(null); } if (!LoadedUBTMakefile.IsValidMakefile()) { Log.TraceWarning("Loaded makefile appears to have invalid contents, ignoring it ({0})", UBTMakefileInfo.FullName); ReasonNotLoaded = "existing makefile appears to be invalid"; return(null); } // Check if any of the target's Build.cs files are newer than the makefile foreach (UEBuildTarget Target in LoadedUBTMakefile.Targets) { string TargetCsFilename = Target.TargetRulesFile.FullName; if (TargetCsFilename != null) { FileInfo TargetCsFile = new FileInfo(TargetCsFilename); bool bTargetCsFileExists = TargetCsFile.Exists; if (!bTargetCsFileExists || TargetCsFile.LastWriteTime > UBTMakefileInfo.LastWriteTime) { Log.TraceVerbose("{0} has been {1} since makefile was built, ignoring it ({2})", TargetCsFilename, bTargetCsFileExists ? "changed" : "deleted", UBTMakefileInfo.FullName); ReasonNotLoaded = string.Format("changes to target files"); return(null); } } IEnumerable <string> BuildCsFilenames = Target.GetAllModuleBuildCsFilenames(); foreach (string BuildCsFilename in BuildCsFilenames) { if (BuildCsFilename != null) { FileInfo BuildCsFile = new FileInfo(BuildCsFilename); bool bBuildCsFileExists = BuildCsFile.Exists; if (!bBuildCsFileExists || BuildCsFile.LastWriteTime > UBTMakefileInfo.LastWriteTime) { Log.TraceVerbose("{0} has been {1} since makefile was built, ignoring it ({2})", BuildCsFilename, bBuildCsFileExists ? "changed" : "deleted", UBTMakefileInfo.FullName); ReasonNotLoaded = string.Format("changes to module files"); return(null); } } } foreach (FlatModuleCsDataType FlatCsModuleData in Target.FlatModuleCsData.Values) { if (FlatCsModuleData.BuildCsFilename != null && FlatCsModuleData.ExternalDependencies.Count > 0) { string BaseDir = Path.GetDirectoryName(FlatCsModuleData.BuildCsFilename); foreach (string ExternalDependency in FlatCsModuleData.ExternalDependencies) { FileInfo DependencyFile = new FileInfo(Path.Combine(BaseDir, ExternalDependency)); bool bDependencyFileExists = DependencyFile.Exists; if (!bDependencyFileExists || DependencyFile.LastWriteTime > UBTMakefileInfo.LastWriteTime) { Log.TraceVerbose("{0} has been {1} since makefile was built, ignoring it ({2})", DependencyFile.FullName, bDependencyFileExists ? "changed" : "deleted", UBTMakefileInfo.FullName); ReasonNotLoaded = string.Format("changes to external dependency"); return(null); } } } } } // We do a check to see if any modules' headers have changed which have // acquired or lost UHT types. If so, which should be rare, // we'll just invalidate the entire makefile and force it to be rebuilt. foreach (UEBuildTarget Target in LoadedUBTMakefile.Targets) { // Get all H files in processed modules newer than the makefile itself HashSet <string> HFilesNewerThanMakefile = new HashSet <string>( Target.FlatModuleCsData .SelectMany(x => x.Value.ModuleSourceFolder != null ? Directory.EnumerateFiles(x.Value.ModuleSourceFolder.FullName, "*.h", SearchOption.AllDirectories) : Enumerable.Empty <string>()) .Where(y => Directory.GetLastWriteTimeUtc(y) > UBTMakefileInfo.LastWriteTimeUtc) .OrderBy(z => z).Distinct() ); // Get all H files in all modules processed in the last makefile build HashSet <string> AllUHTHeaders = new HashSet <string>(Target.FlatModuleCsData.Select(x => x.Value).SelectMany(x => x.UHTHeaderNames)); // Check whether any headers have been deleted. If they have, we need to regenerate the makefile since the module might now be empty. If we don't, // and the file has been moved to a different module, we may include stale generated headers. foreach (string FileName in AllUHTHeaders) { if (!File.Exists(FileName)) { Log.TraceVerbose("File processed by UHT was deleted ({0}); invalidating makefile", FileName); ReasonNotLoaded = string.Format("UHT file was deleted"); return(null); } } // Makefile is invalid if: // * There are any newer files which contain no UHT data, but were previously in the makefile // * There are any newer files contain data which needs processing by UHT, but weren't not previously in the makefile foreach (string Filename in HFilesNewerThanMakefile) { bool bContainsUHTData = CPPHeaders.DoesFileContainUObjects(Filename); bool bWasProcessed = AllUHTHeaders.Contains(Filename); if (bContainsUHTData != bWasProcessed) { Log.TraceVerbose("{0} {1} contain UHT types and now {2} , ignoring it ({3})", Filename, bWasProcessed ? "used to" : "didn't", bWasProcessed ? "doesn't" : "does", UBTMakefileInfo.FullName); ReasonNotLoaded = string.Format("new files with reflected types"); return(null); } } } // If adaptive unity build is enabled, do a check to see if there are any source files that became part of the // working set since the Makefile was created (or, source files were removed from the working set.) If anything // changed, then we'll force a new Makefile to be created so that we have fresh unity build blobs. We always // want to make sure that source files in the working set are excluded from those unity blobs (for fastest possible // iteration times.) if (LoadedUBTMakefile.bUseAdaptiveUnityBuild) { // Check if any source files in the working set no longer belong in it foreach (FileItem SourceFile in LoadedUBTMakefile.SourceFileWorkingSet) { if (!WorkingSet.Contains(SourceFile.Location) && File.GetLastWriteTimeUtc(SourceFile.AbsolutePath) > UBTMakefileInfo.LastWriteTimeUtc) { Log.TraceVerbose("{0} was part of source working set and now is not; invalidating makefile ({1})", SourceFile.AbsolutePath, UBTMakefileInfo.FullName); ReasonNotLoaded = string.Format("working set of source files changed"); return(null); } } // Check if any source files that are eligible for being in the working set have been modified foreach (FileItem SourceFile in LoadedUBTMakefile.CandidateSourceFilesForWorkingSet) { if (WorkingSet.Contains(SourceFile.Location) && File.GetLastWriteTimeUtc(SourceFile.AbsolutePath) > UBTMakefileInfo.LastWriteTimeUtc) { Log.TraceVerbose("{0} was part of source working set and now is not; invalidating makefile ({1})", SourceFile.AbsolutePath, UBTMakefileInfo.FullName); ReasonNotLoaded = string.Format("working set of source files changed"); return(null); } } } ReasonNotLoaded = null; return(LoadedUBTMakefile); }
/// <summary> /// Saves a UBTMakefile to disk /// </summary> /// <param name="TargetDescs">List of targets. Order is not important</param> /// <param name="HotReload">The hot reload state</param> /// <param name="UBTMakefile">The UBT makefile</param> public static void SaveUBTMakefile(List <TargetDescriptor> TargetDescs, EHotReload HotReload, UBTMakefile UBTMakefile) { if (!UBTMakefile.IsValidMakefile()) { throw new BuildException("Can't save a makefile that has invalid contents. See UBTMakefile.IsValidMakefile()"); } DateTime TimerStartTime = DateTime.UtcNow; FileItem UBTMakefileItem = FileItem.GetItemByFileReference(GetUBTMakefilePath(TargetDescs, HotReload)); // @todo ubtmake: Optimization: The UBTMakefile saved for game projects is upwards of 9 MB. We should try to shrink its content if possible // @todo ubtmake: Optimization: C# Serialization may be too slow for these big Makefiles. Loading these files often shows up as the slower part of the assembling phase. // Serialize the cache to disk. try { Directory.CreateDirectory(Path.GetDirectoryName(UBTMakefileItem.AbsolutePath)); using (FileStream Stream = new FileStream(UBTMakefileItem.AbsolutePath, FileMode.Create, FileAccess.Write)) { BinaryFormatter Formatter = new BinaryFormatter(); Formatter.Serialize(Stream, UBTMakefile); } } catch (Exception Ex) { Log.TraceError("Failed to write makefile: {0}", Ex.Message); } if (UnrealBuildTool.bPrintPerformanceInfo) { TimeSpan TimerDuration = DateTime.UtcNow - TimerStartTime; Log.TraceInformation("Saving makefile took " + TimerDuration.TotalSeconds + "s"); } }