Files
UnrealEngine/Engine/Source/Programs/UnrealBuildTool/Executors/Experimental/FASTBuild.cs
2025-05-18 13:04:45 +08:00

1438 lines
47 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
// This is an experimental integration and requires custom FASTBuild binaries available in Engine/Extras/ThirdPartyNotUE/FASTBuild
// Currently only Windows, Mac, iOS and tvOS targets are supported.
///////////////////////////////////////////////////////////////////////////
// Copyright 2018 Yassine Riahi and Liam Flookes. Provided under a MIT License, see license file on github.
// Used to generate a fastbuild .bff file from UnrealBuildTool to allow caching and distributed builds.
///////////////////////////////////////////////////////////////////////////
// Modified by Nick Edwards @ Sumo Digital to implement support for building on
// MacOS for MacOS, iOS and tvOS targets. Includes RiceKab's alterations for
// providing 4.21 support (https://gist.github.com/RiceKab/60d7dd434afaab295d1c21d2fe1981b0)
///////////////////////////////////////////////////////////////////////////
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using EpicGames.Core;
using Microsoft.Extensions.Logging;
using UnrealBuildBase;
using UnrealBuildTool.Artifacts;
namespace UnrealBuildTool
{
///////////////////////////////////////////////////////////////////////
internal static class VCEnvironmentFastbuildExtensions
{
/// <summary>
/// This replaces the VCToolPath64 readonly property that was available in 4.19 . Note that GetVCToolPath64
/// is still used internally, but the property for it is no longer exposed.
/// </summary>
/// <param name="VCEnv"></param>
/// <returns></returns>
public static DirectoryReference GetToolPath(this VCEnvironment VCEnv)
{
return VCEnv.CompilerPath.Directory;
}
/// <summary>
/// This replaces the InstallDir readonly property that was available in 4.19.
///
///
/// </summary>
/// <param name="VCEnv"></param>
/// <returns></returns>
public static DirectoryReference GetVCInstallDirectory(this VCEnvironment VCEnv)
{
// TODO: Check registry values before moving up ParentDirectories (as in 4.19)
return VCEnv.ToolChainDir.ParentDirectory!.ParentDirectory!.ParentDirectory!;
}
}
///////////////////////////////////////////////////////////////////////
internal enum FASTBuildCacheMode
{
ReadWrite, // This machine will both read and write to the cache
ReadOnly, // This machine will only read from the cache, use for developer machines when you have centralized build machines
WriteOnly, // This machine will only write from the cache, use for build machines when you have centralized build machines
}
///////////////////////////////////////////////////////////////////////
sealed class FASTBuild : ActionExecutor
{
/// <summary>
/// Executor to use for local actions
/// </summary>
ActionExecutor LocalExecutor;
public static readonly string DefaultExecutableBasePath = Path.Combine(Unreal.EngineDirectory.FullName, "Extras", "ThirdPartyNotUE", "FASTBuild");
//////////////////////////////////////////
// Tweakables
/////////////////
// Executable
/// <summary>
/// Used to specify the location of fbuild.exe if the distributed binary isn't being used
/// </summary>
[XmlConfigFile]
public static string? FBuildExecutablePath = null;
/////////////////
// Distribution
/// <summary>
/// Controls network build distribution
/// </summary>
[XmlConfigFile]
public static bool bEnableDistribution = true;
/// <summary>
/// Used to specify the location of the brokerage. If null, FASTBuild will fall back to checking FASTBUILD_BROKERAGE_PATH
/// </summary>
[XmlConfigFile]
public static string? FBuildBrokeragePath = null;
/// <summary>
/// Used to specify the FASTBuild coordinator IP or network name. If null, FASTBuild will fall back to checking FASTBUILD_COORDINATOR
/// </summary>
[XmlConfigFile]
public static string? FBuildCoordinator = null;
/////////////////
// Caching
/// <summary>
/// Controls whether to use caching at all. CachePath and FASTCacheMode are only relevant if this is enabled.
/// </summary>
[XmlConfigFile]
public static bool bEnableCaching = true;
/// <summary>
/// Cache access mode - only relevant if bEnableCaching is true;
/// </summary>
[XmlConfigFile]
public static FASTBuildCacheMode CacheMode = FASTBuildCacheMode.ReadOnly;
/// <summary>
/// Used to specify the location of the cache. If null, FASTBuild will fall back to checking FASTBUILD_CACHE_PATH
/// </summary>
[XmlConfigFile]
public static string? FBuildCachePath = null;
/////////////////
// Misc Options
/// <summary>
/// Whether to force remote
/// </summary>
[XmlConfigFile]
public static bool bForceRemote = false;
/// <summary>
/// Whether to stop on error
/// </summary>
[XmlConfigFile]
public static bool bStopOnError = false;
/// <summary>
/// Which MSVC CRT Redist version to use
/// </summary>
[XmlConfigFile]
public static string MsvcCRTRedistVersion = "";
/// <summary>
/// Which MSVC Compiler version to use
/// </summary>
[XmlConfigFile]
public static string CompilerVersion = "";
//////////////////////////////////////////
/// <summary>
/// Constructor
/// </summary>
public FASTBuild(int MaxLocalActions, bool bAllCores, bool bCompactOutput, ILogger Logger)
: base(Logger)
{
XmlConfig.ApplyTo(this);
LocalExecutor = new ParallelExecutor(MaxLocalActions, bAllCores, bCompactOutput, Logger);
}
/// <inheritdoc/>
public new void Dispose()
{
LocalExecutor.Dispose();
base.Dispose();
}
public override string Name => "FASTBuild";
public static string GetExecutableName()
{
return Path.GetFileName(GetExecutablePath())!;
}
public static string? GetExecutablePath()
{
if (String.IsNullOrEmpty(FBuildExecutablePath))
{
string? EnvPath = Environment.GetEnvironmentVariable("FASTBUILD_EXECUTABLE_PATH");
if (!String.IsNullOrEmpty(EnvPath))
{
FBuildExecutablePath = EnvPath;
}
}
return FBuildExecutablePath;
}
public static string? GetCachePath()
{
if (String.IsNullOrEmpty(FBuildCachePath))
{
string? EnvPath = Environment.GetEnvironmentVariable("FASTBUILD_CACHE_PATH");
if (!String.IsNullOrEmpty(EnvPath))
{
FBuildCachePath = EnvPath;
}
}
return FBuildCachePath;
}
public static string? GetBrokeragePath()
{
if (String.IsNullOrEmpty(FBuildBrokeragePath))
{
string? EnvPath = Environment.GetEnvironmentVariable("FASTBUILD_BROKERAGE_PATH");
if (!String.IsNullOrEmpty(EnvPath))
{
FBuildBrokeragePath = EnvPath;
}
}
return FBuildBrokeragePath;
}
public static string? GetCoordinator()
{
if (String.IsNullOrEmpty(FBuildCoordinator))
{
string? EnvPath = Environment.GetEnvironmentVariable("FASTBUILD_COORDINATOR");
if (!String.IsNullOrEmpty(EnvPath))
{
FBuildCoordinator = EnvPath;
}
}
return FBuildCoordinator;
}
public static bool IsAvailable(ILogger Logger)
{
string? ExecutablePath = GetExecutablePath();
if (BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Mac)
{
if (String.IsNullOrEmpty(ExecutablePath))
{
FBuildExecutablePath = Path.Combine(DefaultExecutableBasePath, BuildHostPlatform.Current.Platform.ToString(), "FBuild");
}
}
else if (BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Win64)
{
if (String.IsNullOrEmpty(ExecutablePath))
{
FBuildExecutablePath = Path.Combine(DefaultExecutableBasePath, BuildHostPlatform.Current.Platform.ToString(), "FBuild.exe");
}
}
else
{
// Linux is not supported yet. Win32 likely never.
return false;
}
// UBT is faster than FASTBuild for local only builds, so only allow FASTBuild if the environment is fully set up to use FASTBuild.
// That's when the FASTBuild coordinator or brokerage folder is available.
// On Mac the latter needs the brokerage folder to be mounted, on Windows the brokerage env variable has to be set or the path specified in UBT's config
string? Coordinator = GetCoordinator();
if (String.IsNullOrEmpty(Coordinator))
{
string? BrokeragePath = GetBrokeragePath();
if (String.IsNullOrEmpty(BrokeragePath) || !Directory.Exists(BrokeragePath))
{
return false;
}
}
if (!String.IsNullOrEmpty(FBuildExecutablePath))
{
if (File.Exists(FBuildExecutablePath))
{
return true;
}
Logger.LogWarning("FBuildExecutablePath '{FBuildExecutablePath}' doesn't exist! Attempting to find executable in PATH.", FBuildExecutablePath);
}
// Get the name of the FASTBuild executable.
string FBuildExecutableName = GetExecutableName();
// Search the path for it
string? PathVariable = Environment.GetEnvironmentVariable("PATH");
if (PathVariable != null)
{
foreach (string SearchPath in PathVariable.Split(Path.PathSeparator))
{
try
{
string PotentialPath = Path.Combine(SearchPath, FBuildExecutableName);
if (File.Exists(PotentialPath))
{
FBuildExecutablePath = PotentialPath;
return true;
}
}
catch (ArgumentException)
{
// PATH variable may contain illegal characters; just ignore them.
}
}
}
Logger.LogError("FASTBuild disabled. Unable to find any executable to use.");
return false;
}
private TelemetryExecutorEvent? telemetryEvent;
/// <inheritdoc/>
public override TelemetryExecutorEvent? GetTelemetryEvent() => telemetryEvent ?? LocalExecutor.GetTelemetryEvent();
//////////////////////////////////////////
// Action Helpers
private readonly Dictionary<LinkedAction, long> _actionToId = new();
private long _actionIdIndex = 0;
private long GetActionID(LinkedAction Action)
{
if (!_actionToId.ContainsKey(Action))
{
_actionToId.Add(Action, ++_actionIdIndex);
}
return _actionToId[Action];
}
private string ActionToActionString(LinkedAction Action)
{
return ActionToActionString(GetActionID(Action));
}
private static string ActionToActionString(long UniqueId)
{
return $"Action_{UniqueId}";
}
private static string ActionToDependencyString(long UniqueId, string StatusDescription, string? CommandDescription = null, ActionType? ActionType = null)
{
string? ExtraInfoString = null;
if ((CommandDescription != null) && String.IsNullOrEmpty(CommandDescription))
{
ExtraInfoString = CommandDescription;
}
else if (ActionType != null)
{
ExtraInfoString = ActionType.Value.ToString();
}
if ((ExtraInfoString != null) && !String.IsNullOrEmpty(ExtraInfoString))
{
ExtraInfoString = $" ({ExtraInfoString})";
}
return $"\t\t'{ActionToActionString(UniqueId)}', ;{StatusDescription}{ExtraInfoString}";
}
private string ActionToDependencyString(LinkedAction Action)
{
return ActionToDependencyString(GetActionID(Action), Action.StatusDescription, Action.CommandDescription, Action.ActionType);
}
private readonly HashSet<string> ForceLocalCompileModules = new HashSet<string>()
{
"Module.ProxyLODMeshReduction"
};
private readonly HashSet<string> ForceOverwriteCompilerOptionModules = new HashSet<string>()
{
"Module.USDStageImporter",
"Module.USDUtilities",
"Module.UnrealUSDWrapper",
"Module.USDStage",
"Module.USDSchemas",
"Module.GeometryCacheUSD",
"Module.USDStageEditorViewModels",
"Module.USDTests",
"Module.USDStageEditor",
"Module.USDExporter"
};
private enum FBBuildType
{
Windows,
Apple
}
private FBBuildType BuildType = FBBuildType.Windows;
private static readonly Tuple<string, Func<LinkedAction, string>, FBBuildType>[] BuildTypeSearchParams = new Tuple<string, Func<LinkedAction, string>, FBBuildType>[]
{
Tuple.Create<string, Func<LinkedAction, string>, FBBuildType>
(
"Xcode",
Action => Action.CommandArguments,
FBBuildType.Apple
),
Tuple.Create<string, Func<LinkedAction, string>, FBBuildType>
(
"apple",
Action => Action.CommandArguments.ToLower(),
FBBuildType.Apple
),
Tuple.Create<string, Func<LinkedAction, string>, FBBuildType>
(
"/bin/sh",
Action => Action.CommandPath.FullName.ToLower(),
FBBuildType.Apple
),
Tuple.Create<string, Func<LinkedAction, string>, FBBuildType>
(
"Windows", // Not a great test
Action => Action.CommandPath.FullName,
FBBuildType.Windows
),
Tuple.Create<string, Func<LinkedAction, string>, FBBuildType>
(
"Microsoft", // Not a great test
Action => Action.CommandPath.FullName,
FBBuildType.Windows
),
Tuple.Create<string, Func<LinkedAction, string>, FBBuildType>
(
"Win64",
Action => Action.CommandPath.FullName,
FBBuildType.Windows
),
};
private bool DetectBuildType(IEnumerable<LinkedAction> Actions, ILogger Logger)
{
foreach (LinkedAction Action in Actions)
{
foreach (Tuple<string, Func<LinkedAction, string>, FBBuildType> BuildTypeSearchParam in BuildTypeSearchParams)
{
if (BuildTypeSearchParam.Item3.Equals(FBBuildType.Apple) &&
(BuildTypeSearchParam.Item2(Action).Contains("Win64", StringComparison.OrdinalIgnoreCase) ||
BuildTypeSearchParam.Item2(Action).Contains("X64", StringComparison.OrdinalIgnoreCase)))
{
continue;
}
if (BuildTypeSearchParam.Item2(Action).Contains(BuildTypeSearchParam.Item1))
{
BuildType = BuildTypeSearchParam.Item3;
Logger.LogInformation("Detected build type as {Type} from '{From}' using search term '{Term}'", BuildTypeSearchParam.Item3.ToString(), BuildTypeSearchParam.Item2(Action), BuildTypeSearchParam.Item1);
return true;
}
}
}
Logger.LogError("Couldn't detect build type from actions! Unsupported platform?");
foreach (LinkedAction Action in Actions)
{
PrintActionDetails(Action, Logger);
}
return false;
}
private bool IsMSVC() { return BuildType == FBBuildType.Windows; }
private bool IsApple() { return BuildType == FBBuildType.Apple; }
private string GetCompilerName()
{
switch (BuildType)
{
default:
case FBBuildType.Windows: return "UECompiler";
case FBBuildType.Apple: return "UEAppleCompiler";
}
}
/// <inheritdoc/>
[SupportedOSPlatform("windows")]
public override async Task<bool> ExecuteActionsAsync(IEnumerable<LinkedAction> Actions, ILogger Logger, IActionArtifactCache? actionArtifactCache)
{
if (!Actions.Any())
{
return true;
}
IEnumerable<LinkedAction> CompileActions = Actions.Where(Action => Action.ActionType == ActionType.Compile && Action.bCanExecuteRemotely && Action.bCanExecuteRemotelyWithSNDBS);
if (CompileActions.Any() && DetectBuildType(CompileActions, Logger))
{
DateTime startTimeUTC = DateTime.UtcNow;
string FASTBuildFilePath = Path.Combine(Unreal.EngineDirectory.FullName, "Intermediate", "Build", "fbuild.bff");
if (!CreateBffFile(Actions, FASTBuildFilePath, Logger))
{
return false;
}
bool result = ExecuteBffFile(FASTBuildFilePath, Logger);
telemetryEvent = new TelemetryExecutorEvent(Name, startTimeUTC, result, Actions.Count(), -1, -1, 0, 0, DateTime.UtcNow);
return result;
}
return await LocalExecutor.ExecuteActionsAsync(Actions, Logger, actionArtifactCache);
}
private void AddText(string StringToWrite)
{
byte[] Info = new System.Text.UTF8Encoding(true).GetBytes(StringToWrite);
bffOutputMemoryStream!.Write(Info, 0, Info.Length);
}
private void AddPreBuildDependenciesText(IEnumerable<LinkedAction>? PreBuildDependencies)
{
if (PreBuildDependencies == null || !PreBuildDependencies.Any())
{
return;
}
AddText($"\t.PreBuildDependencies = {{\n");
AddText($"{String.Join("\n", PreBuildDependencies.Select(ActionToDependencyString))}\n");
AddText($"\t}} \n");
}
private string SubstituteEnvironmentVariables(string commandLineString)
{
return commandLineString
.Replace("$(DXSDK_DIR)", "$DXSDK_DIR$")
.Replace("$(CommonProgramFiles)", "$CommonProgramFiles$");
}
private Dictionary<string, string> ParseCommandLineOptions(string LocalToolName, string CompilerCommandLine, string[] SpecialOptions, ILogger Logger, bool SaveResponseFile = false)
{
Dictionary<string, string> ParsedCompilerOptions = new Dictionary<string, string>();
// Make sure we substituted the known environment variables with corresponding BFF friendly imported vars
CompilerCommandLine = SubstituteEnvironmentVariables(CompilerCommandLine);
// Some tricky defines /DTROUBLE=\"\\\" abc 123\\\"\" aren't handled properly by either Unreal or FASTBuild, but we do our best.
char[] SpaceChar = { ' ' };
string[] RawTokens = CompilerCommandLine.Trim().Split(' ');
List<string> ProcessedTokens = new List<string>();
bool QuotesOpened = false;
string PartialToken = "";
string ResponseFilePath = "";
List<string> AllTokens = new List<string>();
int ResponseFileTokenIndex = Array.FindIndex(RawTokens, RawToken => RawToken.StartsWith("@\""));
if (ResponseFileTokenIndex == -1)
{
ResponseFileTokenIndex = Array.FindIndex(RawTokens, RawToken => RawToken.StartsWith("@"));
}
if (ResponseFileTokenIndex > -1) //Response files are in 4.13 by default. Changing VCToolChain to not do this is probably better.
{
string responseCommandline = RawTokens[ResponseFileTokenIndex];
for (int i = 0; i < ResponseFileTokenIndex; ++i)
{
AllTokens.Add(RawTokens[i]);
}
// If we had spaces inside the response file path, we need to reconstruct the path.
for (int i = ResponseFileTokenIndex + 1; i < RawTokens.Length; ++i)
{
if (RawTokens[i - 1].Contains(".response") || RawTokens[i - 1].Contains(".rsp"))
{
break;
}
responseCommandline += " " + RawTokens[i];
}
ResponseFilePath = responseCommandline.TrimStart('"', '@').TrimEnd('"');
try
{
if (!File.Exists(ResponseFilePath))
{
throw new Exception($"ResponseFilePath '{ResponseFilePath}' does not exist!");
}
string ResponseFileText = File.ReadAllText(ResponseFilePath);
// Make sure we substituted the known environment variables with corresponding BFF friendly imported vars
ResponseFileText = SubstituteEnvironmentVariables(ResponseFileText);
string[] Separators = { "\n", " ", "\r" };
if (File.Exists(ResponseFilePath))
{
RawTokens = ResponseFileText.Split(Separators, StringSplitOptions.RemoveEmptyEntries); //Certainly not ideal
}
}
catch (Exception e)
{
if (!String.IsNullOrEmpty(e.Message))
{
Logger.LogInformation("{Message}", e.Message);
}
Logger.LogError("Looks like a response file in: {CompilerCommandLine}, but we could not load it! {Ex}", CompilerCommandLine, e.Message);
ResponseFilePath = "";
}
}
for (int i = 0; i < RawTokens.Length; ++i)
{
AllTokens.Add(RawTokens[i]);
}
// Raw tokens being split with spaces may have split up some two argument options and
// paths with multiple spaces in them also need some love
for (int i = 0; i < AllTokens.Count; ++i)
{
string Token = AllTokens[i];
if (String.IsNullOrEmpty(Token))
{
if (ProcessedTokens.Count > 0 && QuotesOpened)
{
string CurrentToken = ProcessedTokens.Last();
CurrentToken += " ";
}
continue;
}
int numQuotes = 0;
// Look for unescaped " symbols, we want to stick those strings into one token.
for (int j = 0; j < Token.Length; ++j)
{
if (Token[j] == '\\') //Ignore escaped quotes
{
++j;
}
else if (Token[j] == '"')
{
numQuotes++;
}
}
// Handle nested response files
if (Token.StartsWith('@'))
{
foreach (KeyValuePair<string, string> Pair in ParseCommandLineOptions(LocalToolName, Token, SpecialOptions, Logger))
{
ParsedCompilerOptions.Add(Pair.Key, Pair.Value);
}
continue;
}
// Defines can have escaped quotes and other strings inside them
// so we consume tokens until we've closed any open unescaped parentheses.
if ((Token.StartsWith("/D") || Token.StartsWith("-D")) && !QuotesOpened)
{
if (numQuotes == 0 || numQuotes == 2)
{
ProcessedTokens.Add(Token);
}
else
{
PartialToken = Token;
++i;
bool AddedToken = false;
for (; i < AllTokens.Count; ++i)
{
string NextToken = AllTokens[i];
if (String.IsNullOrEmpty(NextToken))
{
PartialToken += " ";
}
else if (!NextToken.EndsWith("\\\"") && NextToken.EndsWith("\"")) //Looking for a token that ends with a non-escaped "
{
ProcessedTokens.Add(PartialToken + " " + NextToken);
AddedToken = true;
break;
}
else
{
PartialToken += " " + NextToken;
}
}
if (!AddedToken)
{
Logger.LogWarning("Warning! Looks like an unterminated string in tokens. Adding PartialToken and hoping for the best. Command line: {CompilerCommandLine}", CompilerCommandLine);
ProcessedTokens.Add(PartialToken);
}
}
continue;
}
if (!QuotesOpened)
{
if (numQuotes % 2 != 0) //Odd number of quotes in this token
{
PartialToken = Token + " ";
QuotesOpened = true;
}
else
{
ProcessedTokens.Add(Token);
}
}
else
{
if (numQuotes % 2 != 0) //Odd number of quotes in this token
{
ProcessedTokens.Add(PartialToken + Token);
QuotesOpened = false;
}
else
{
PartialToken += Token + " ";
}
}
}
//Processed tokens should now have 'whole' tokens, so now we look for any specified special options
foreach (string specialOption in SpecialOptions)
{
for (int i = 0; i < ProcessedTokens.Count; ++i)
{
if (ProcessedTokens[i] == specialOption && i + 1 < ProcessedTokens.Count)
{
ParsedCompilerOptions[specialOption] = ProcessedTokens[i + 1];
ProcessedTokens.RemoveRange(i, 2);
break;
}
else if (ProcessedTokens[i].StartsWith(specialOption))
{
ParsedCompilerOptions[specialOption] = ProcessedTokens[i].Replace(specialOption, null);
ProcessedTokens.RemoveAt(i);
break;
}
}
}
//The search for the input file... we take the first non-argument we can find
for (int i = 0; i < ProcessedTokens.Count; ++i)
{
string Token = ProcessedTokens[i];
if (Token.Length == 0)
{
continue;
}
// Skip the following tokens:
if ((Token == "/I") ||
(Token == "/external:I") ||
(Token == "/l") ||
(Token == "/D") ||
(Token == "-D") ||
(Token == "-x") ||
(Token == "-F") ||
(Token == "-arch") ||
(Token == "-isysroot") ||
(Token == "-include") ||
(Token == "-current_version") ||
(Token == "-compatibility_version") ||
(Token == "-rpath") ||
(Token == "-weak_library") ||
(Token == "-weak_framework") ||
(Token == "-framework") ||
(Token == "/sourceDependencies") ||
(Token == "/sourceDependencies:directives") ||
(Token.Contains("/experimental")))
{
++i;
}
else if (Token == "/we4668")
{
// Replace this to make Windows builds compile happily
ProcessedTokens[i] = "/wd4668";
}
else if (Token.Contains("clang++"))
{
ProcessedTokens.RemoveAt(i);
i--;
}
else if (Token == "--")
{
ProcessedTokens.RemoveAt(i);
ParsedCompilerOptions["CLFilterChildCommand"] = ProcessedTokens[i];
ProcessedTokens.RemoveAt(i);
i--;
}
else if (!Token.StartsWith("/") && !Token.StartsWith("-") && !Token.Contains(".framework"))
{
ParsedCompilerOptions["InputFile"] = Token;
ProcessedTokens.RemoveAt(i);
i--;
}
}
if (ParsedCompilerOptions.ContainsKey("OtherOptions"))
{
ProcessedTokens.Insert(0, ParsedCompilerOptions["OtherOptions"]);
}
ParsedCompilerOptions["OtherOptions"] = String.Join(" ", ProcessedTokens) + " ";
if (SaveResponseFile && !String.IsNullOrEmpty(ResponseFilePath))
{
ParsedCompilerOptions["@"] = ResponseFilePath;
}
return ParsedCompilerOptions;
}
private string GetOptionValue(Dictionary<string, string> OptionsDictionary, string Key, LinkedAction Action, ILogger Logger, bool ProblemIfNotFound = false)
{
string? Value = String.Empty;
if (OptionsDictionary.TryGetValue(Key, out Value))
{
return Value.Trim(new char[] { '\"' });
}
if (ProblemIfNotFound)
{
Logger.LogWarning("We failed to find {Key}, which may be a problem.", Key);
Logger.LogWarning("Action.CommandArguments: {CommandArguments}", Action.CommandArguments);
}
return String.Empty;
}
[SupportedOSPlatform("windows")]
public string GetRegistryValue(string keyName, string valueName, object defaultValue)
{
object? returnValue = Microsoft.Win32.Registry.GetValue("HKEY_LOCAL_MACHINE\\SOFTWARE\\" + keyName, valueName, defaultValue);
if (returnValue != null)
{
return returnValue.ToString()!;
}
returnValue = Microsoft.Win32.Registry.GetValue("HKEY_CURRENT_USER\\SOFTWARE\\" + keyName, valueName, defaultValue);
if (returnValue != null)
{
return returnValue.ToString()!;
}
returnValue = Microsoft.Win32.Registry.GetValue("HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\" + keyName, valueName, defaultValue);
if (returnValue != null)
{
return returnValue.ToString()!;
}
returnValue = Microsoft.Win32.Registry.GetValue("HKEY_CURRENT_USER\\SOFTWARE\\Wow6432Node\\" + keyName, valueName, defaultValue);
if (returnValue != null)
{
return returnValue.ToString()!;
}
return defaultValue.ToString()!;
}
[SupportedOSPlatform("windows")]
private void WriteEnvironmentSetup(ILogger Logger)
{
VCEnvironment? VCEnv = null;
try
{
// This may fail if the caller emptied PATH; we try to ignore the problem since
// it probably means we are building for another platform.
if (BuildType == FBBuildType.Windows)
{
VCEnv = VCEnvironment.Create(
Compiler: WindowsPlatform.GetDefaultCompiler(null, UnrealArch.X64, Logger, true),
ToolChain: WindowsCompiler.Default,
Platform: UnrealTargetPlatform.Win64,
Architecture: UnrealArch.X64,
CompilerVersion: String.IsNullOrEmpty(CompilerVersion) ? null : CompilerVersion,
ToolchainVersion: null,
WindowsSdkVersion: null,
SuppliedSdkDirectoryForVersion: null,
bUseCPPWinRT: false,
bAllowClangLinker: false,
bAllowRadLinker: false,
Logger);
}
}
catch (Exception)
{
Logger.LogWarning("Failed to get Visual Studio environment.");
}
// Copy environment into a case-insensitive dictionary for easier key lookups
Dictionary<string, string> envVars = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (Nullable<DictionaryEntry> entry in Environment.GetEnvironmentVariables())
{
if (entry.HasValue)
{
envVars[(string)entry.Value.Key] = (string)entry.Value.Value!;
}
}
if (BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Win64)
{
if (envVars.ContainsKey("CommonProgramFiles"))
{
AddText("#import CommonProgramFiles\n");
}
if (envVars.ContainsKey("DXSDK_DIR"))
{
AddText("#import DXSDK_DIR\n");
}
if (envVars.ContainsKey("DurangoXDK"))
{
AddText("#import DurangoXDK\n");
}
}
if (VCEnv != null)
{
string platformVersionNumber = "VSVersionUnknown";
AddText($".WindowsSDKBasePath = '{VCEnv.WindowsSdkDir}'\n");
AddText($"Compiler('UEResourceCompiler') \n{{\n");
switch (VCEnv.Compiler)
{
case WindowsCompiler.VisualStudio2022:
// For now we are working with the 140 version, might need to change to 141 or 150 depending on the version of the Toolchain you chose
// to install
platformVersionNumber = "140";
AddText($"\t.Executable = '$WindowsSDKBasePath$/bin/{VCEnv.WindowsSdkVersion}/x64/rc.exe'\n");
break;
default:
string exceptionString = "Error: Unsupported Visual Studio Version.";
Logger.LogError("{Ex}", exceptionString);
throw new BuildException(exceptionString);
}
AddText($"\t.CompilerFamily = 'custom'\n");
AddText($"}}\n\n");
AddText("Compiler('UECompiler') \n{\n");
bool UsingCLFilter = VCEnv.ToolChainVersion < VersionNumber.Parse("14.27");
DirectoryReference CLFilterDirectory = DirectoryReference.Combine(Unreal.EngineDirectory, "Build", "Windows", "cl-filter");
AddText($"\t.Root = '{VCEnv.GetToolPath()}'\n");
if (UsingCLFilter)
{
AddText($"\t.CLFilterRoot = '{CLFilterDirectory.FullName}'\n");
AddText($"\t.Executable = '$CLFilterRoot$\\cl-filter.exe'\n");
}
else
{
AddText($"\t.Executable = '$Root$\\{VCEnv.CompilerPath.GetFileName()}'\n");
}
AddText($"\t.ExtraFiles =\n\t{{\n");
if (UsingCLFilter)
{
AddText($"\t\t'$Root$/cl.exe'\n");
}
AddText($"\t\t'$Root$/c1.dll'\n");
AddText($"\t\t'$Root$/c1xx.dll'\n");
AddText($"\t\t'$Root$/c2.dll'\n");
FileReference? cluiDllPath = null;
string cluiSubDirName = "1033";
if (File.Exists(VCEnv.GetToolPath() + "{cluiSubDirName}/clui.dll")) //Check English first...
{
if (UsingCLFilter)
{
AddText("\t\t'$CLFilterRoot$/{cluiSubDirName}/clui.dll'\n");
}
else
{
AddText("\t\t'$Root$/{cluiSubDirName}/clui.dll'\n");
}
cluiDllPath = new FileReference(VCEnv.GetToolPath() + "{cluiSubDirName}/clui.dll");
}
else
{
IEnumerable<string> numericDirectories = Directory.GetDirectories(VCEnv.GetToolPath().ToString()).Where(d => Path.GetFileName(d).All(Char.IsDigit));
IEnumerable<string> cluiDirectories = numericDirectories.Where(d => Directory.GetFiles(d, "clui.dll").Any());
if (cluiDirectories.Any())
{
cluiSubDirName = Path.GetFileName(cluiDirectories.First());
if (UsingCLFilter)
{
AddText(String.Format("\t\t'$CLFilterRoot$/{0}/clui.dll'\n", cluiSubDirName));
}
else
{
AddText(String.Format("\t\t'$Root$/{0}/clui.dll'\n", cluiSubDirName));
}
cluiDllPath = new FileReference(cluiDirectories.First() + "/clui.dll");
}
}
// FASTBuild only preserves the directory structure of compiler files for files in the same directory or sub-directories of the primary executable
// Since our primary executable is cl-filter.exe and we need clui.dll in a sub-directory on the worker, we need to copy it to cl-filter's subdir
if (UsingCLFilter && cluiDllPath != null)
{
Directory.CreateDirectory(Path.Combine(CLFilterDirectory.FullName, cluiSubDirName));
File.Copy(cluiDllPath.FullName, Path.Combine(CLFilterDirectory.FullName, cluiSubDirName, "clui.dll"), true);
}
AddText("\t\t'$Root$/mspdbsrv.exe'\n");
AddText("\t\t'$Root$/mspdbcore.dll'\n");
AddText($"\t\t'$Root$/mspft{platformVersionNumber}.dll'\n");
AddText($"\t\t'$Root$/msobj{platformVersionNumber}.dll'\n");
AddText($"\t\t'$Root$/mspdb{platformVersionNumber}.dll'\n");
string RedistInstallDirectory = String.Format("{0}/Redist/MSVC", VCEnv.GetVCInstallDirectory());
List<string> PotentialMSVCRedistPaths = new List<string>(Directory.EnumerateDirectories(RedistInstallDirectory));
string? PrefferedMSVCRedistPath = null;
string? FinalMSVCRedistPath = "";
if (PotentialMSVCRedistPaths.Count == 0)
{
// We cannot possibly derive a FinalMSVCRedistPath, as all paths are predicated on there being at least 1 install directory within PotentialMSVCRedistPaths
throw new Exception($"Couldn't find any installations under the VC Redist install directory: {RedistInstallDirectory}");
}
if (MsvcCRTRedistVersion.Length > 0)
{
PrefferedMSVCRedistPath = PotentialMSVCRedistPaths.Find(
delegate (string str)
{
return str.Contains(MsvcCRTRedistVersion);
});
}
if (PrefferedMSVCRedistPath == null)
{
// If upon Visual Studio installation the user selects "latest", two folders are created. If however the user only specifies one, only the selected folder is created.
PrefferedMSVCRedistPath = PotentialMSVCRedistPaths.Count == 1 ? PotentialMSVCRedistPaths[0] : PotentialMSVCRedistPaths[^2];
if (MsvcCRTRedistVersion.Length > 0)
{
Logger.LogInformation("Couldn't find redist path for given MsvcCRTRedistVersion {MsvcCRTRedistVersion}"
+ " (in BuildConfiguration.xml). \n\t...Using this path instead: {PrefferedMSVCRedistPath}", MsvcCRTRedistVersion, PrefferedMSVCRedistPath);
}
else
{
Logger.LogInformation("Using path : {PrefferedMSVCRedistPath} for vccorlib_.dll (MSVC redist)..." +
"\n\t...Add an entry for MsvcCRTRedistVersion in BuildConfiguration.xml to specify a version number", PrefferedMSVCRedistPath.ToString());
}
}
PotentialMSVCRedistPaths = new List<string>(Directory.EnumerateDirectories(String.Format("{0}/{1}", PrefferedMSVCRedistPath, VCEnv.Architecture)));
FinalMSVCRedistPath = PotentialMSVCRedistPaths.Find(x => x.Contains(".CRT"));
if (String.IsNullOrEmpty(FinalMSVCRedistPath))
{
FinalMSVCRedistPath = PrefferedMSVCRedistPath;
}
{
AddText($"\t\t'$Root$/msvcp{platformVersionNumber}.dll'\n");
AddText(String.Format("\t\t'{0}/vccorlib{1}.dll'\n", FinalMSVCRedistPath, platformVersionNumber));
AddText($"\t\t'$Root$/tbbmalloc.dll'\n");
//AddText(string.Format("\t\t'{0}/Redist/MSVC/{1}/x64/Microsoft.VC141.CRT/vccorlib{2}.dll'\n", VCEnv.GetVCInstallDirectory(), VCEnv.ToolChainVersion, platformVersionNumber));
}
AddText("\t}\n"); //End extra files
AddText($"\t.CompilerFamily = 'msvc'\n");
AddText("}\n\n"); //End compiler
}
if (BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Mac)
{
AddText($".MacBaseSDKDir = '{ApplePlatformSDK.GetPlatformSDKDirectory("MacOSX")}'\n");
AddText($".MacToolchainDir = '{ApplePlatformSDK.GetToolchainDirectory()}'\n");
AddText($"Compiler('UEAppleCompiler') \n{{\n");
AddText($"\t.Executable = '$MacToolchainDir$/clang++'\n");
AddText($"\t.ClangRewriteIncludes = false\n"); // This is to fix an issue with iOS clang builds, __has_include, and Objective-C #imports
AddText($"}}\n\n");
}
AddText("Settings \n{\n");
if (bEnableCaching)
{
string? CachePath = GetCachePath();
if (!String.IsNullOrEmpty(CachePath))
{
AddText($"\t.CachePath = '{CachePath}'\n");
}
}
if (bEnableDistribution)
{
string? BrokeragePath = GetBrokeragePath();
if (!String.IsNullOrEmpty(BrokeragePath))
{
AddText($"\t.BrokeragePath = '{BrokeragePath}'\n");
}
}
//Start Environment
AddText("\t.Environment = \n\t{\n");
if (VCEnv != null)
{
AddText(String.Format("\t\t\"PATH={0}\\Common7\\IDE\\;{1};{2}\\bin\\{3}\\x64\",\n", VCEnv.GetVCInstallDirectory(), VCEnv.GetToolPath(), VCEnv.WindowsSdkDir, VCEnv.WindowsSdkVersion));
}
if (!IsApple())
{
if (envVars.ContainsKey("TMP"))
{
AddText($"\t\t\"TMP={envVars["TMP"]}\",\n");
}
if (envVars.ContainsKey("SystemRoot"))
{
AddText($"\t\t\"SystemRoot={envVars["SystemRoot"]}\",\n");
}
if (envVars.ContainsKey("INCLUDE"))
{
AddText($"\t\t\"INCLUDE={envVars["INCLUDE"]}\",\n");
}
if (envVars.ContainsKey("LIB"))
{
AddText($"\t\t\"LIB={envVars["LIB"]}\",\n");
}
}
AddText("\t}\n"); //End environment
AddText("}\n\n"); //End Settings
}
private void AddCompileAction(LinkedAction Action, IEnumerable<LinkedAction> DependencyActions, ILogger Logger)
{
string CompilerName = GetCompilerName();
if (Action.CommandPath.FullName.Contains("rc.exe"))
{
CompilerName = "UEResourceCompiler";
}
string[] SpecialCompilerOptions = { "/Fo", "/fo", "/Yc", "/Yu", "/Fp", "-o", "-dependencies=", "-compiler=" };
Dictionary<string, string> ParsedCompilerOptions = ParseCommandLineOptions(Action.CommandPath.GetFileName(), Action.CommandArguments, SpecialCompilerOptions, Logger);
string OutputObjectFileName = GetOptionValue(ParsedCompilerOptions, IsMSVC() ? "/Fo" : "-o", Action, Logger, ProblemIfNotFound: !IsMSVC());
if (IsMSVC() && String.IsNullOrEmpty(OutputObjectFileName)) // Didn't find /Fo, try /fo
{
OutputObjectFileName = GetOptionValue(ParsedCompilerOptions, "/fo", Action, Logger, ProblemIfNotFound: true);
}
if (String.IsNullOrEmpty(OutputObjectFileName)) //No /Fo or /fo, we're probably in trouble.
{
throw new Exception("We have no OutputObjectFileName. Bailing. Our Action.CommandArguments were: " + Action.CommandArguments);
}
string IntermediatePath = Path.GetDirectoryName(OutputObjectFileName)!;
if (String.IsNullOrEmpty(IntermediatePath))
{
throw new Exception("We have no IntermediatePath. Bailing. Our Action.CommandArguments were: " + Action.CommandArguments);
}
IntermediatePath = IsApple() ? IntermediatePath.Replace("\\", "/") : IntermediatePath;
string InputFile = GetOptionValue(ParsedCompilerOptions, "InputFile", Action, Logger, ProblemIfNotFound: true);
if (String.IsNullOrEmpty(InputFile))
{
throw new Exception("We have no InputFile. Bailing. Our Action.CommandArguments were: " + Action.CommandArguments);
}
AddText($"ObjectList('{ActionToActionString(Action)}')\n{{\n");
AddText($"\t.Compiler = '{CompilerName}'\n");
AddText($"\t.CompilerInputFiles = \"{InputFile}\"\n");
AddText($"\t.CompilerOutputPath = \"{IntermediatePath}\"\n");
if (!Action.bCanExecuteRemotely || !Action.bCanExecuteRemotelyWithSNDBS || ForceLocalCompileModules.Contains(Path.GetFileNameWithoutExtension(InputFile)))
{
AddText("\t.AllowDistribution = false\n");
}
string OtherCompilerOptions = GetOptionValue(ParsedCompilerOptions, "OtherOptions", Action, Logger);
if (ForceOverwriteCompilerOptionModules.Any(x => Action.CommandArguments.Contains(x, StringComparison.OrdinalIgnoreCase)))
{
OtherCompilerOptions = OtherCompilerOptions.Replace("/WX", "");
}
string CompilerOutputExtension = ".unset";
string CLFilterParams = "";
string ShowIncludesParam = "";
if (ParsedCompilerOptions.ContainsKey("CLFilterChildCommand"))
{
CLFilterParams = "-dependencies=\"%CLFilterDependenciesOutput\" -compiler=\"%5\" -stderronly -- \"%5\" ";
ShowIncludesParam = "/showIncludes";
}
if (ParsedCompilerOptions.ContainsKey("/Yc")) //Create PCH
{
string PCHIncludeHeader = GetOptionValue(ParsedCompilerOptions, "/Yc", Action, Logger, ProblemIfNotFound: true);
string PCHOutputFile = GetOptionValue(ParsedCompilerOptions, "/Fp", Action, Logger, ProblemIfNotFound: true);
AddText($"\t.CompilerOptions = '{CLFilterParams}\"%1\" /Fo\"%2\" /Fp\"{PCHOutputFile}\" /Yu\"{PCHIncludeHeader}\" {OtherCompilerOptions} '\n");
AddText($"\t.PCHOptions = '{CLFilterParams}\"%1\" /Fp\"%2\" /Yc\"{PCHIncludeHeader}\" {OtherCompilerOptions} /Fo\"{OutputObjectFileName}\"'\n");
AddText($"\t.PCHInputFile = \"{InputFile}\"\n");
AddText($"\t.PCHOutputFile = \"{PCHOutputFile}\"\n");
CompilerOutputExtension = ".obj";
}
else if (ParsedCompilerOptions.ContainsKey("/Yu")) //Use PCH
{
string PCHIncludeHeader = GetOptionValue(ParsedCompilerOptions, "/Yu", Action, Logger, ProblemIfNotFound: true);
string PCHOutputFile = GetOptionValue(ParsedCompilerOptions, "/Fp", Action, Logger, ProblemIfNotFound: true);
string PCHToForceInclude = PCHOutputFile.Replace(".pch", "");
AddText($"\t.CompilerOptions = '{CLFilterParams}\"%1\" /Fo\"%2\" /Fp\"{PCHOutputFile}\" /Yu\"{PCHIncludeHeader}\" /FI\"{PCHToForceInclude}\" {OtherCompilerOptions} {ShowIncludesParam} '\n");
string InputFileExt = Path.GetExtension(InputFile);
CompilerOutputExtension = InputFileExt + ".obj";
}
else if (Path.GetExtension(OutputObjectFileName) == ".gch") //Create PCH
{
AddText($"\t.CompilerOptions = '{OtherCompilerOptions} -D __BUILDING_WITH_FASTBUILD__ -fno-diagnostics-color -o \"%2\" \"%1\" '\n");
AddText($"\t.PCHOptions = '{OtherCompilerOptions} -o \"%2\" \"%1\" '\n");
AddText($"\t.PCHInputFile = \"{InputFile}\"\n");
AddText($"\t.PCHOutputFile = \"{OutputObjectFileName}\"\n");
CompilerOutputExtension = ".h.gch";
}
else
{
if (CompilerName == "UEResourceCompiler")
{
AddText($"\t.CompilerOptions = '{OtherCompilerOptions} /fo\"%2\" \"%1\" '\n");
CompilerOutputExtension = Path.GetExtension(InputFile) + ".res";
}
else
{
if (IsMSVC())
{
AddText($"\t.CompilerOptions = '{CLFilterParams}{OtherCompilerOptions} /Fo\"%2\" \"%1\" {ShowIncludesParam} '\n");
string InputFileExt = Path.GetExtension(InputFile);
CompilerOutputExtension = InputFileExt + ".obj";
}
else
{
AddText($"\t.CompilerOptions = '{OtherCompilerOptions} -D __BUILDING_WITH_FASTBUILD__ -fno-diagnostics-color -o \"%2\" \"%1\" '\n");
string InputFileExt = Path.GetExtension(InputFile);
CompilerOutputExtension = InputFileExt + ".o";
}
}
}
AddText($"\t.CompilerOutputExtension = '{CompilerOutputExtension}' \n");
AddPreBuildDependenciesText(DependencyActions);
AddText("}\n\n");
}
private void AddExecAction(LinkedAction Action, IEnumerable<LinkedAction> DependencyActions, ILogger Logger)
{
AddText($"Exec('{ActionToActionString(Action)}')\n{{\n");
AddText($"\t.ExecExecutable = '{Action.CommandPath.FullName}' \n");
AddText($"\t.ExecArguments = '{Action.CommandArguments}' \n");
AddText($"\t.ExecWorkingDir = '{Action.WorkingDirectory.FullName}' \n");
AddText($"\t.ExecOutput = '{Action.ProducedItems.First().FullName}' \n");
AddText($"\t.ExecAlways = true \n");
AddPreBuildDependenciesText(DependencyActions);
AddText($"}}\n\n");
}
private void PrintActionDetails(LinkedAction ActionToPrint, ILogger Logger)
{
Logger.LogInformation("{Action}", ActionToActionString(ActionToPrint));
Logger.LogInformation("Action Type: {Type}", ActionToPrint.ActionType.ToString());
Logger.LogInformation("Action CommandPath: {Path}", ActionToPrint.CommandPath.FullName);
Logger.LogInformation("Action CommandArgs: {Args}", ActionToPrint.CommandArguments);
}
private MemoryStream? bffOutputMemoryStream = null;
[SupportedOSPlatform("windows")]
private bool CreateBffFile(IEnumerable<LinkedAction> Actions, string BffFilePath, ILogger Logger)
{
try
{
bffOutputMemoryStream = new MemoryStream();
AddText(";*************************************************************************\n");
AddText(";* Autogenerated bff - see FASTBuild.cs for how this file was generated. *\n");
AddText(";*************************************************************************\n\n");
WriteEnvironmentSetup(Logger); //Compiler, environment variables and base paths
foreach (LinkedAction Action in Actions)
{
// Resolve the list of prerequisite items for this action to
// a list of actions which produce these prerequisites
IEnumerable<LinkedAction> DependencyActions = Action.PrerequisiteActions.Distinct();
AddText($";** Function for Action {GetActionID(Action)} **\n");
AddText($";** CommandPath: {Action.CommandPath.FullName}\n");
AddText($";** CommandArguments: {Action.CommandArguments}\n");
AddText("\n");
if (Action.ActionType == ActionType.Compile && Action.bCanExecuteRemotely && Action.bCanExecuteRemotelyWithSNDBS)
{
AddCompileAction(Action, DependencyActions, Logger);
}
else
{
AddExecAction(Action, DependencyActions, Logger);
}
}
string JoinedActions = Actions
.Select(Action => ActionToDependencyString(Action))
.DefaultIfEmpty(String.Empty)
.Aggregate((str, obj) => str + "\n" + obj);
AddText("Alias( 'all' ) \n{\n");
AddText("\t.Targets = { \n");
AddText(JoinedActions);
AddText("\n\t}\n");
AddText("}\n");
using (FileStream bffOutputFileStream = new FileStream(BffFilePath, FileMode.Create, FileAccess.Write))
{
bffOutputMemoryStream.Position = 0;
bffOutputMemoryStream.CopyTo(bffOutputFileStream);
}
bffOutputMemoryStream.Close();
}
catch (Exception e)
{
Logger.LogError("Exception while creating bff file: {Ex}", e.ToString());
return false;
}
return true;
}
private bool ExecuteBffFile(string BffFilePath, ILogger Logger)
{
string CacheArgument = "";
if (bEnableCaching)
{
switch (CacheMode)
{
case FASTBuildCacheMode.ReadOnly:
CacheArgument = "-cacheread";
break;
case FASTBuildCacheMode.WriteOnly:
CacheArgument = "-cachewrite";
break;
case FASTBuildCacheMode.ReadWrite:
CacheArgument = "-cache";
break;
}
}
string DistArgument = bEnableDistribution ? "-dist" : "";
string ForceRemoteArgument = bForceRemote ? "-forceremote" : "";
string NoStopOnErrorArgument = bStopOnError ? "" : "-nostoponerror";
string IDEArgument = IsApple() ? "" : "-ide";
string MaxProcesses = "-j" + ((ParallelExecutor)LocalExecutor).NumParallelProcesses;
// Interesting flags for FASTBuild:
// -nostoponerror, -verbose, -monitor (if FASTBuild Monitor Visual Studio Extension is installed!)
// Yassine: The -clean is to bypass the FASTBuild internal
// dependencies checks (cached in the fdb) as it could create some conflicts with UBT.
// Basically we want FB to stupidly compile what UBT tells it to.
string FBCommandLine = $"-monitor -summary {DistArgument} {CacheArgument} {IDEArgument} {MaxProcesses} -clean -config \"{BffFilePath}\" {NoStopOnErrorArgument} {ForceRemoteArgument}";
Logger.LogInformation("FBuild Command Line Arguments: '{FBCommandLine}", FBCommandLine);
string FBExecutable = GetExecutablePath()!;
string WorkingDirectory = Path.GetFullPath(Path.Combine(Unreal.EngineDirectory.MakeRelativeTo(DirectoryReference.GetCurrentDirectory()), "Source"));
ProcessStartInfo FBStartInfo = new ProcessStartInfo(FBExecutable, FBCommandLine);
FBStartInfo.UseShellExecute = false;
FBStartInfo.WorkingDirectory = WorkingDirectory;
FBStartInfo.RedirectStandardError = true;
FBStartInfo.RedirectStandardOutput = true;
string? Coordinator = GetCoordinator();
if (!String.IsNullOrEmpty(Coordinator) && !FBStartInfo.EnvironmentVariables.ContainsKey("FASTBUILD_COORDINATOR"))
{
FBStartInfo.EnvironmentVariables.Add("FASTBUILD_COORDINATOR", Coordinator);
}
FBStartInfo.EnvironmentVariables.Remove("FASTBUILD_BROKERAGE_PATH"); // remove stale serialized value and defer to GetBrokeragePath
string? BrokeragePath = GetBrokeragePath();
if (!String.IsNullOrEmpty(BrokeragePath) && !FBStartInfo.EnvironmentVariables.ContainsKey("FASTBUILD_BROKERAGE_PATH"))
{
FBStartInfo.EnvironmentVariables.Add("FASTBUILD_BROKERAGE_PATH", BrokeragePath);
}
string? CachePath = GetCachePath();
if (!String.IsNullOrEmpty(CachePath) && !FBStartInfo.EnvironmentVariables.ContainsKey("FASTBUILD_CACHE_PATH"))
{
FBStartInfo.EnvironmentVariables.Add("FASTBUILD_CACHE_PATH", CachePath);
}
try
{
Process FBProcess = new Process();
FBProcess.StartInfo = FBStartInfo;
FBProcess.EnableRaisingEvents = true;
DataReceivedEventHandler OutputEventHandler = (Sender, Args) =>
{
if (Args.Data != null)
{
Logger.LogInformation("{Output}", Args.Data);
}
};
FBProcess.OutputDataReceived += OutputEventHandler;
FBProcess.ErrorDataReceived += OutputEventHandler;
FBProcess.Start();
FBProcess.BeginOutputReadLine();
FBProcess.BeginErrorReadLine();
FBProcess.WaitForExit();
return FBProcess.ExitCode == 0;
}
catch (Exception e)
{
Logger.LogError("Exception launching fbuild process. Is it in your path? {Ex}", e.ToString());
return false;
}
}
}
}