Esempio n. 1
0
        /// <summary>
        /// Saves a UBTMakefile to disk
        /// </summary>
        /// <param name="TargetDescs">List of targets.  Order is not important</param>
        static void SaveUBTMakefile( List<TargetDescriptor> TargetDescs, UBTMakefile UBTMakefile )
        {
            if( !UBTMakefile.IsValidMakefile() )
            {
                throw new BuildException( "Can't save a makefile that has invalid contents.  See UBTMakefile.IsValidMakefile()" );
            }

            var TimerStartTime = DateTime.UtcNow;

            var UBTMakefileItem = FileItem.GetItemByFullPath( GetUBTMakefilePath( TargetDescs ) );

            // @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)
            {
                Console.Error.WriteLine("Failed to write makefile: {0}", Ex.Message);
            }

            if (BuildConfiguration.bPrintPerformanceInfo)
            {
                var TimerDuration = DateTime.UtcNow - TimerStartTime;
                Log.TraceInformation("Saving makefile took " + TimerDuration.TotalSeconds + "s");
            }
        }
Esempio n. 2
0
        public static ECompilationResult RunUBT(string[] Arguments)
        {
            bool bSuccess = true;

			
			var RunUBTInitStartTime = DateTime.UtcNow;


            // Reset global configurations
            ActionGraph.ResetAllActions();

            // We need to allow the target platform to perform the 'reset' as well...
            UnrealTargetPlatform ResetPlatform = UnrealTargetPlatform.Unknown;
            UnrealTargetConfiguration ResetConfiguration;
            UEBuildTarget.ParsePlatformAndConfiguration(Arguments, out ResetPlatform, out ResetConfiguration);
			var BuildPlatform = UEBuildPlatform.GetBuildPlatform(ResetPlatform);
			BuildPlatform.ResetBuildConfiguration(ResetPlatform, ResetConfiguration);

            // now that we have the platform, we can set the intermediate path to include the platform/architecture name
            BuildConfiguration.PlatformIntermediateFolder = Path.Combine(BuildConfiguration.BaseIntermediateFolder, ResetPlatform.ToString(), BuildPlatform.GetActiveArchitecture());

            string ExecutorName = "Unknown";
            ECompilationResult BuildResult = ECompilationResult.Succeeded;

            var ToolChain = UEToolChain.GetPlatformToolChain(BuildPlatform.GetCPPTargetPlatform(ResetPlatform));

            string EULAViolationWarning = null;
			Thread CPPIncludesThread = null;

            try
            {
                List<string[]> TargetSettings = ParseCommandLineFlags(Arguments);

                int ArgumentIndex;
                // action graph implies using the dependency resolve cache
                bool GeneratingActionGraph = Utils.ParseCommandLineFlag(Arguments, "-graph", out ArgumentIndex );
                if (GeneratingActionGraph)
                {
                    BuildConfiguration.bUseIncludeDependencyResolveCache = true;
                }

                bool CreateStub = Utils.ParseCommandLineFlag(Arguments, "-nocreatestub", out ArgumentIndex);
                if (CreateStub || (String.IsNullOrEmpty(Environment.GetEnvironmentVariable("uebp_LOCAL_ROOT")) && BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Mac))
                {
                    BuildConfiguration.bCreateStubIPA = false;
                }


				if( BuildConfiguration.bPrintPerformanceInfo )
				{ 
					var RunUBTInitTime = (DateTime.UtcNow - RunUBTInitStartTime).TotalSeconds;
					Log.TraceInformation( "RunUBT initialization took " + RunUBTInitTime + "s" );
				}

	            var TargetDescs = new List<TargetDescriptor>();
				{ 
					var TargetDescstStartTime = DateTime.UtcNow;

					foreach (string[] TargetSetting in TargetSettings)
					{
						TargetDescs.AddRange( UEBuildTarget.ParseTargetCommandLine( TargetSetting ) );
					}

					if( BuildConfiguration.bPrintPerformanceInfo )
					{ 
						var TargetDescsTime = (DateTime.UtcNow - TargetDescstStartTime).TotalSeconds;
						Log.TraceInformation( "Target descriptors took " + TargetDescsTime + "s" );
					}
				}

				if (UnrealBuildTool.bIsInvalidatingMakefilesOnly)
				{
					Log.TraceInformation("Invalidating makefiles only in this run.");
					if (TargetDescs.Count != 1)
					{
						Log.TraceError("You have to provide one target name for makefile invalidation.");
						return ECompilationResult.OtherCompilationError;
					}

					InvalidateMakefiles(TargetDescs[0]);
					return ECompilationResult.Succeeded;
				}

				UEBuildConfiguration.bHotReloadFromIDE = UEBuildConfiguration.bAllowHotReloadFromIDE && TargetDescs.Count == 1 && !TargetDescs[0].bIsEditorRecompile && ShouldDoHotReloadFromIDE(TargetDescs[0]);
				bool bIsHotReload = UEBuildConfiguration.bHotReloadFromIDE || ( TargetDescs.Count == 1 && TargetDescs[0].OnlyModules.Count > 0 && TargetDescs[0].ForeignPlugins.Count == 0 );
				TargetDescriptor HotReloadTargetDesc = bIsHotReload ? TargetDescs[0] : null;

				if (ProjectFileGenerator.bGenerateProjectFiles)
				{
					// Create empty timestamp file to record when was the last time we regenerated projects.
					Directory.CreateDirectory( Path.GetDirectoryName( ProjectFileGenerator.ProjectTimestampFile ) );
					File.Create(ProjectFileGenerator.ProjectTimestampFile).Dispose();
				}

                if( !ProjectFileGenerator.bGenerateProjectFiles )
                {
					if( BuildConfiguration.bUseUBTMakefiles )
                    {
                        // If we're building UHT without a Mutex, we'll need to assume that we're building the same targets and that no caches
                        // should be invalidated for this run.  This is important when UBT is invoked from within UBT in order to compile
                        // UHT.  In that (very common) case we definitely don't want to have to rebuild our cache from scratch.
                        bool bMustAssumeSameTargets = false;

                        if( TargetDescs[0].TargetName.Equals( "UnrealHeaderTool", StringComparison.InvariantCultureIgnoreCase ) )
                        {
                            int NoMutexArgumentIndex;
                            if (Utils.ParseCommandLineFlag(Arguments, "-NoMutex", out NoMutexArgumentIndex))
                            {
                                bMustAssumeSameTargets = true;
                            }
                        }

	                    bool bIsBuildingSameTargetsAsLastTime = false;
                        if( bMustAssumeSameTargets )
                        {
                            bIsBuildingSameTargetsAsLastTime = true;
                        }
                        else
                        {
                            string TargetCollectionName = MakeTargetCollectionName( TargetDescs );

							string LastBuiltTargetsFileName = bIsHotReload ? "HotReloadLastBuiltTargets.txt" : "LastBuiltTargets.txt";
                            string LastBuiltTargetsFilePath = Path.Combine( BuildConfiguration.BaseIntermediatePath, LastBuiltTargetsFileName );
                            if( File.Exists( LastBuiltTargetsFilePath ) && Utils.ReadAllText( LastBuiltTargetsFilePath ) == TargetCollectionName )
                            {
								// @todo ubtmake: Because we're using separate files for hot reload vs. full compiles, it's actually possible that includes will
								// become out of date without us knowing if the developer ping-pongs between hot reloading target A and building target B normally.
								// To fix this we can not use a different file name for last built targets, but the downside is slower performance when
								// performing the first hot reload after compiling normally (forces full include dependency scan)
                                bIsBuildingSameTargetsAsLastTime = true;
                            }

                            // Save out the name of the targets we're building
                            if( !bIsBuildingSameTargetsAsLastTime )
                            {
                                Directory.CreateDirectory( Path.GetDirectoryName( LastBuiltTargetsFilePath ) );
                                File.WriteAllText( LastBuiltTargetsFilePath, TargetCollectionName, Encoding.UTF8 );
                            }


                            if( !bIsBuildingSameTargetsAsLastTime )
                            {
                                // Can't use super fast include checking unless we're building the same set of targets as last time, because
                                // we might not know about all of the C++ include dependencies for already-up-to-date shared build products
                                // between the targets
                                bNeedsFullCPPIncludeRescan = true;
								Log.TraceInformation( "Performing full C++ include scan ({0} a new target)", bIsHotReload ? "hot reloading" : "building" );
                            }
                        }
                    }
                }
            


                UBTMakefile UBTMakefile = null;
                { 
                    // If we're generating project files, then go ahead and wipe out the existing UBTMakefile for every target, to make sure that
                    // it gets a full dependency scan next time.
                    // NOTE: This is just a safeguard and doesn't have to be perfect.  We also check for newer project file timestamps in LoadUBTMakefile()
                    string UBTMakefilePath = UnrealBuildTool.GetUBTMakefilePath( TargetDescs );
                    if( ProjectFileGenerator.bGenerateProjectFiles )	// @todo ubtmake: This is only hit when generating IntelliSense for project files.  Probably should be done right inside ProjectFileGenerator.bat
                    {													// @todo ubtmake: Won't catch multi-target cases as GPF always builds one target at a time for Intellisense
                        // Delete the UBTMakefile
                        if (File.Exists(UBTMakefilePath))
                        {
                            UEBuildTarget.CleanFile(UBTMakefilePath);
                        }
                    }

                    // Make sure the gather phase is executed if we're not actually building anything
                    if( ProjectFileGenerator.bGenerateProjectFiles || UEBuildConfiguration.bGenerateManifest || UEBuildConfiguration.bCleanProject || BuildConfiguration.bXGEExport || UEBuildConfiguration.bGenerateExternalFileList || GeneratingActionGraph)
                    {
                        UnrealBuildTool.bIsGatheringBuild_Unsafe = true;						
                    }

                    // Were we asked to run in 'assembler only' mode?  If so, let's check to see if that's even possible by seeing if
                    // we have a valid UBTMakefile already saved to disk, ready for us to load.
                    if( UnrealBuildTool.bIsAssemblingBuild_Unsafe && !UnrealBuildTool.bIsGatheringBuild_Unsafe )
                    {
                        // @todo ubtmake: Mildly terrified of BuildConfiguration/UEBuildConfiguration globals that were set during the Prepare phase but are not set now.  We may need to save/load all of these, otherwise
                        //		we'll need to call SetupGlobalEnvironment on all of the targets (maybe other stuff, too.  See PreBuildStep())

                        // Try to load the UBTMakefile.  It will only be loaded if it has valid content and is not determined to be out of date.    
						string ReasonNotLoaded;
                        UBTMakefile = LoadUBTMakefile( UBTMakefilePath, out ReasonNotLoaded );

						// Invalid makefile if only modules have changed
						if (UBTMakefile != null && !TargetDescs.SelectMany(x => x.OnlyModules)
								.Select(x => new Tuple<string, bool>(x.OnlyModuleName.ToLower(), string.IsNullOrWhiteSpace(x.OnlyModuleSuffix)))
								.SequenceEqual(
									UBTMakefile.Targets.SelectMany(x => x.OnlyModules)
									.Select(x => new Tuple<string, bool>(x.OnlyModuleName.ToLower(), string.IsNullOrWhiteSpace(x.OnlyModuleSuffix)))
								)
							)
						{
							UBTMakefile     = null;
							ReasonNotLoaded = "modules to compile have changed";
						}

                        if( UBTMakefile == null )
                        { 
                            // If the Makefile couldn't be loaded, then we're not going to be able to continue in "assembler only" mode.  We'll do both
                            // a 'gather' and 'assemble' in the same run.  This will take a while longer, but subsequent runs will be fast!
                            UnrealBuildTool.bIsGatheringBuild_Unsafe = true;

							FileItem.ClearCaches();

                            Log.TraceInformation( "Creating makefile for {0}{1}{2} ({3})", 
								bIsHotReload ? "hot reloading " : "", 
								TargetDescs[0].TargetName, 
								TargetDescs.Count > 1 ? ( " (and " + ( TargetDescs.Count - 1 ).ToString() + " more)" ) : "", 
								ReasonNotLoaded );
                        }
                    }

                    // OK, after this point it is safe to access the UnrealBuildTool.IsGatheringBuild and UnrealBuildTool.IsAssemblingBuild properties.
                    // These properties will not be changing again during this session/
                    bIsSafeToCheckIfGatheringOrAssemblingBuild = true;
                }


				List<UEBuildTarget> Targets;
				if( UBTMakefile != null && !IsGatheringBuild && IsAssemblingBuild )
				{
					// If we've loaded a makefile, then we can fill target information from this file!
					Targets = UBTMakefile.Targets;
				}
				else
				{ 
					var TargetInitStartTime = DateTime.UtcNow;

					Targets = new List<UEBuildTarget>();
					foreach( var TargetDesc in TargetDescs )
					{
						var Target = UEBuildTarget.CreateTarget( TargetDesc );
						if ((Target == null) && (UEBuildConfiguration.bCleanProject))
						{
							continue;
						}
						Targets.Add(Target);
					}

					if( BuildConfiguration.bPrintPerformanceInfo )
					{ 
						var TargetInitTime = (DateTime.UtcNow - TargetInitStartTime).TotalSeconds;
						Log.TraceInformation( "Target init took " + TargetInitTime + "s" );
					}
				}

                // Build action lists for all passed in targets.
                var OutputItemsForAllTargets = new List<FileItem>();
                var TargetNameToUObjectModules = new Dictionary<string, List<UHTModuleInfo>>( StringComparer.InvariantCultureIgnoreCase );
                foreach (var Target in Targets)
                {
                    var TargetStartTime = DateTime.UtcNow;

                    if (bIsHotReload)
                    {
						// Don't produce new DLLs if there's been no code changes
						UEBuildConfiguration.bSkipLinkingWhenNothingToCompile = true;
                        Log.TraceInformation("Compiling game modules for hot reload");
                    }

                    // When in 'assembler only' mode, we'll load this cache later on a worker thread.  It takes a long time to load!
                    if( !( !UnrealBuildTool.IsGatheringBuild && UnrealBuildTool.IsAssemblingBuild ) )
                    {	
                        // Load the direct include dependency cache.
                        CPPEnvironment.IncludeDependencyCache.Add( Target, DependencyCache.Create( DependencyCache.GetDependencyCachePathForTarget(Target) ) );
                    }

                    // We don't need this dependency cache in 'gather only' mode
                    if( BuildConfiguration.bUseUBTMakefiles &&
                        !( UnrealBuildTool.IsGatheringBuild && !UnrealBuildTool.IsAssemblingBuild ) )
                    { 
                        // Load the cache that contains the list of flattened resolved includes for resolved source files
                        // @todo ubtmake: Ideally load this asynchronously at startup and only block when it is first needed and not finished loading
                        CPPEnvironment.FlatCPPIncludeDependencyCache.Add( Target, FlatCPPIncludeDependencyCache.Create( Target ) );
                    }

                    if( UnrealBuildTool.IsGatheringBuild )
                    { 
                        List<FileItem> TargetOutputItems;
                        List<UHTModuleInfo> TargetUObjectModules;
                        BuildResult = Target.Build(ToolChain, out TargetOutputItems, out TargetUObjectModules, out EULAViolationWarning);
                        if(BuildResult != ECompilationResult.Succeeded)
                        {
                            break;
                        }

                        OutputItemsForAllTargets.AddRange( TargetOutputItems );

                        // Update mapping of the target name to the list of UObject modules in this target
                        TargetNameToUObjectModules[ Target.GetTargetName() ] = TargetUObjectModules;

                        if ( (BuildConfiguration.bXGEExport && UEBuildConfiguration.bGenerateManifest) || (!ProjectFileGenerator.bGenerateProjectFiles && !UEBuildConfiguration.bGenerateManifest && !UEBuildConfiguration.bCleanProject))
                        {
                            // Generate an action graph if we were asked to do that.  The graph generation needs access to the include dependency cache, so
                            // we generate it before saving and cleaning that up.
                            if( GeneratingActionGraph )
                            {
                                // The graph generation feature currently only works with a single target at a time.  This is because of how we need access
                                // to include dependencies for the target, but those aren't kept around as we process multiple targets
                                if( TargetSettings.Count != 1 )
                                {
                                    throw new BuildException( "ERROR: The '-graph' option only works with a single target at a time" );
                                }
                                ActionGraph.FinalizeActionGraph();
                                var ActionsToExecute = ActionGraph.AllActions;

                                var VisualizationType = ActionGraph.ActionGraphVisualizationType.OnlyCPlusPlusFilesAndHeaders;
                                ActionGraph.SaveActionGraphVisualization( Target, Path.Combine( BuildConfiguration.BaseIntermediatePath, Target.GetTargetName() + ".gexf" ), Target.GetTargetName(), VisualizationType, ActionsToExecute );
                            }
                        }

                        var TargetBuildTime = (DateTime.UtcNow - TargetStartTime).TotalSeconds;

                        // Send out telemetry for this target
                        Telemetry.SendEvent("TargetBuildStats.2",
                            "AppName", Target.AppName,
                            "GameName", Target.TargetName,
                            "Platform", Target.Platform.ToString(),
                            "Configuration", Target.Configuration.ToString(),
                            "CleanTarget", UEBuildConfiguration.bCleanProject.ToString(),
                            "Monolithic", Target.ShouldCompileMonolithic().ToString(),
                            "CreateDebugInfo", Target.IsCreatingDebugInfo().ToString(),
                            "TargetType", Target.TargetType.ToString(),
                            "TargetCreateTimeSec", TargetBuildTime.ToString("0.00")
                            );
                    }
                }

                if (BuildResult == ECompilationResult.Succeeded &&
                    (
                        (BuildConfiguration.bXGEExport && UEBuildConfiguration.bGenerateManifest) ||
                        (!GeneratingActionGraph && !ProjectFileGenerator.bGenerateProjectFiles && !UEBuildConfiguration.bGenerateManifest && !UEBuildConfiguration.bCleanProject && !UEBuildConfiguration.bGenerateExternalFileList)
                    ))
                {
                    if( UnrealBuildTool.IsGatheringBuild )
                    {
                        ActionGraph.FinalizeActionGraph();

                        UBTMakefile = new UBTMakefile();
                        UBTMakefile.AllActions = ActionGraph.AllActions;

                        // For now simply treat all object files as the root target.
                        { 
                            var PrerequisiteActionsSet = new HashSet<Action>();
                            foreach (var OutputItem in OutputItemsForAllTargets)
                            {
                                ActionGraph.GatherPrerequisiteActions(OutputItem, ref PrerequisiteActionsSet);
                            }
                            UBTMakefile.PrerequisiteActions = PrerequisiteActionsSet.ToArray();
                        }			

                        foreach( System.Collections.DictionaryEntry EnvironmentVariable in Environment.GetEnvironmentVariables() )
                        {
                            UBTMakefile.EnvironmentVariables.Add( Tuple.Create( (string)EnvironmentVariable.Key, (string)EnvironmentVariable.Value ) );
                        }
                        UBTMakefile.TargetNameToUObjectModules = TargetNameToUObjectModules;
						UBTMakefile.Targets = Targets;

						if( BuildConfiguration.bUseUBTMakefiles )
						{ 
							// We've been told to prepare to build, so let's go ahead and save out our action graph so that we can use in a later invocation 
							// to assemble the build.  Even if we are configured to assemble the build in this same invocation, we want to save out the
							// Makefile so that it can be used on subsequent 'assemble only' runs, for the fastest possible iteration times
							// @todo ubtmake: Optimization: We could make 'gather + assemble' mode slightly faster by saving this while busy compiling (on our worker thread)
							SaveUBTMakefile( TargetDescs, UBTMakefile );
						}
                    }

                    if( UnrealBuildTool.IsAssemblingBuild )
                    {
                        // If we didn't build the graph in this session, then we'll need to load a cached one
                        if( !UnrealBuildTool.IsGatheringBuild )
                        {
                            ActionGraph.AllActions = UBTMakefile.AllActions;

							// Patch action history for hot reload when running in assembler mode.  In assembler mode, the suffix on the output file will be
							// the same for every invocation on that makefile, but we need a new suffix each time.
							if( bIsHotReload )
							{
								PatchActionHistoryForHotReloadAssembling( HotReloadTargetDesc.OnlyModules );
							}


                            foreach( var EnvironmentVariable in UBTMakefile.EnvironmentVariables )
                            {
                                // @todo ubtmake: There may be some variables we do NOT want to clobber.
                                Environment.SetEnvironmentVariable( EnvironmentVariable.Item1, EnvironmentVariable.Item2 );
                            }

                            // If any of the targets need UHT to be run, we'll go ahead and do that now
                            foreach( var Target in Targets )
                            {
                                List<UHTModuleInfo> TargetUObjectModules;
                                if( UBTMakefile.TargetNameToUObjectModules.TryGetValue( Target.GetTargetName(), out TargetUObjectModules ) )
                                {
                                    if( TargetUObjectModules.Count > 0 )
                                    {
                                        // Execute the header tool
                                        string ModuleInfoFileName = Path.GetFullPath( Path.Combine( Target.ProjectIntermediateDirectory, "UnrealHeaderTool.manifest" ) );
                                        ECompilationResult UHTResult = ECompilationResult.OtherCompilationError;
                                        if (!ExternalExecution.ExecuteHeaderToolIfNecessary(Target, GlobalCompileEnvironment:null, UObjectModules:TargetUObjectModules, ModuleInfoFileName:ModuleInfoFileName, UHTResult:ref UHTResult))
                                        {
                                            Log.TraceInformation("UnrealHeaderTool failed for target '" + Target.GetTargetName() + "' (platform: " + Target.Platform.ToString() + ", module info: " + ModuleInfoFileName + ").");
                                            BuildResult = UHTResult;
                                            break;
                                        }
                                    }
                                }
                            }
                        }

                        if( BuildResult.Succeeded() )
                        { 
                            // Make sure any old DLL files from in-engine recompiles aren't lying around.  Must be called after the action graph is finalized.
                            ActionGraph.DeleteStaleHotReloadDLLs();

                            // Plan the actions to execute for the build.
                            Dictionary<UEBuildTarget,List<FileItem>> TargetToOutdatedPrerequisitesMap;
                            List<Action> ActionsToExecute = ActionGraph.GetActionsToExecute(UBTMakefile.PrerequisiteActions, Targets, out TargetToOutdatedPrerequisitesMap);

                            // Display some stats to the user.
                            Log.TraceVerbose(
                                    "{0} actions, {1} outdated and requested actions",
                                    ActionGraph.AllActions.Count,
                                    ActionsToExecute.Count
                                    );

							if (!bIsHotReload)
							{
								// clean up any stale modules
								foreach (UEBuildTarget Target in Targets)
								{
									Target.CleanStaleModules();
								}
							}
							
							ToolChain.PreBuildSync();

                    
                            // Cache indirect includes for all outdated C++ files.  We kick this off as a background thread so that it can
                            // perform the scan while we're compiling.  It usually only takes up to a few seconds, but we don't want to hurt
                            // our best case UBT iteration times for this task which can easily be performed asynchronously
                            if( BuildConfiguration.bUseUBTMakefiles && TargetToOutdatedPrerequisitesMap.Count > 0 )
                            {
                                CPPIncludesThread = CreateThreadForCachingCPPIncludes( TargetToOutdatedPrerequisitesMap );
                                CPPIncludesThread.Start();
                            }

                            // Execute the actions.
                            bSuccess = ActionGraph.ExecuteActions(ActionsToExecute, out ExecutorName);

                            // if the build succeeded, write the receipts and do any needed syncing
                            if (bSuccess)
                            {
                                foreach (UEBuildTarget Target in Targets)
                                {
									Target.WriteReceipt();
                                    ToolChain.PostBuildSync(Target);
                                }
								if (ActionsToExecute.Count == 0 && UEBuildConfiguration.bSkipLinkingWhenNothingToCompile)
								{
									BuildResult = ECompilationResult.UpToDate;
								}
                            }
                            else
                            {
                                BuildResult = ECompilationResult.OtherCompilationError;
                            }
                        }
                    }
                }
            }
            catch (BuildException Exception)
            {
                // Output the message only, without the call stack
                Log.TraceInformation(Exception.Message);
                BuildResult = ECompilationResult.OtherCompilationError;
            }
            catch (Exception Exception)
            {
                Log.TraceInformation("ERROR: {0}", Exception);
                BuildResult = ECompilationResult.OtherCompilationError;
            }

			// Wait until our CPPIncludes dependency scanner thread has finished
			if( CPPIncludesThread != null )
			{ 
			    CPPIncludesThread.Join();
			}

            // Save the include dependency cache.
            { 
				// NOTE: It's very important that we save the include cache, even if a build exception was thrown (compile error, etc), because we need to make sure that
				//    any C++ include dependencies that we computed for out of date source files are saved.  Remember, the build may fail *after* some build products
				//    are successfully built.  If we didn't save our dependency cache after build failures, source files for those build products that were successsfully
				//    built before the failure would not be considered out of date on the next run, so this is our only chance to cache C++ includes for those files!

                foreach( var DependencyCache in CPPEnvironment.IncludeDependencyCache.Values )
                { 
                    DependencyCache.Save();
                }
                CPPEnvironment.IncludeDependencyCache.Clear();

                foreach( var FlatCPPIncludeDependencyCache in CPPEnvironment.FlatCPPIncludeDependencyCache.Values )
                { 
                    FlatCPPIncludeDependencyCache.Save();
                }
                CPPEnvironment.FlatCPPIncludeDependencyCache.Clear();
            }

            if(EULAViolationWarning != null)
            {
                Log.TraceWarning("WARNING: {0}", EULAViolationWarning);
            }

            // Figure out how long we took to execute.
            double BuildDuration = (DateTime.UtcNow - StartTime).TotalSeconds;
            if (ExecutorName == "Local" || ExecutorName == "Distcc" || ExecutorName == "SNDBS")
            {
                Log.WriteLineIf(BuildConfiguration.bLogDetailedActionStats || BuildConfiguration.bPrintDebugInfo,
					TraceEventType.Information, 
					"Cumulative action seconds ({0} processors): {1:0.00} building projects, {2:0.00} compiling, {3:0.00} creating app bundles, {4:0.00} generating debug info, {5:0.00} linking, {6:0.00} other",
                    System.Environment.ProcessorCount,
                    TotalBuildProjectTime,
                    TotalCompileTime,
                    TotalCreateAppBundleTime,
                    TotalGenerateDebugInfoTime,
                    TotalLinkTime,
                    TotalOtherActionsTime
                );
                Telemetry.SendEvent("BuildStatsTotal.2",
                    "ExecutorName", ExecutorName,
                    "TotalUBTWallClockTimeSec", BuildDuration.ToString("0.00"),
                    "TotalBuildProjectThreadTimeSec", TotalBuildProjectTime.ToString("0.00"),
                    "TotalCompileThreadTimeSec", TotalCompileTime.ToString("0.00"),
                    "TotalCreateAppBundleThreadTimeSec", TotalCreateAppBundleTime.ToString("0.00"),
                    "TotalGenerateDebugInfoThreadTimeSec", TotalGenerateDebugInfoTime.ToString("0.00"),
                    "TotalLinkThreadTimeSec", TotalLinkTime.ToString("0.00"),
                    "TotalOtherActionsThreadTimeSec", TotalOtherActionsTime.ToString("0.00")
                    );

                Log.TraceInformation("Total build time: {0:0.00} seconds", BuildDuration);

                // reset statistics
                TotalBuildProjectTime = 0;
                TotalCompileTime = 0;
                TotalCreateAppBundleTime = 0;
                TotalGenerateDebugInfoTime = 0;
                TotalLinkTime = 0;
                TotalOtherActionsTime = 0;
            }
            else
            {
                if (ExecutorName == "XGE")
                {
                    Log.TraceInformation("XGE execution time: {0:0.00} seconds", BuildDuration);
                }
                Telemetry.SendEvent("BuildStatsTotal.2",
                    "ExecutorName", ExecutorName,
                    "TotalUBTWallClockTimeSec", BuildDuration.ToString("0.00")
                    );
            }

            return BuildResult;
        }