Skip to content

Instantly share code, notes, and snippets.

@ricekab
Created December 10, 2018 16:22
Show Gist options
  • Save ricekab/60d7dd434afaab295d1c21d2fe1981b0 to your computer and use it in GitHub Desktop.
Save ricekab/60d7dd434afaab295d1c21d2fe1981b0 to your computer and use it in GitHub Desktop.
FASTBuild for UE 4.21
// 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.
// Tested with Windows 10, Visual Studio 2015/2017, Unreal Engine 4.19.1, FastBuild v0.95
// Durango is fully supported (Compiles with VS2015).
// Orbis will likely require some changes.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Diagnostics;
using System.Linq;
using Tools.DotNETCommon;
namespace UnrealBuildTool
{
class FASTBuild : ActionExecutor
{
/*---- Configurable User settings ----*/
// Used to specify a non-standard location for the FBuild.exe, for example if you have not added it to your PATH environment variable.
public static string FBuildExePathOverride = "";
// Controls network build distribution
private bool bEnableDistribution = true;
// Controls whether to use caching at all. CachePath and CacheMode are only relevant if this is enabled.
private bool bEnableCaching = false;
// Location of the shared cache, it could be a local or network path (i.e: @"\\DESKTOP-BEAST\FASTBuildCache").
// Only relevant if bEnableCaching is true;
private string CachePath = @"\\SharedDrive\FASTBuildCache";
public enum eCacheMode
{
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
}
// Cache access mode
// Only relevant if bEnableCaching is true;
private eCacheMode CacheMode = eCacheMode.ReadWrite;
/*--------------------------------------*/
public override string Name
{
get { return "FASTBuild"; }
}
public static bool IsAvailable()
{
if (FBuildExePathOverride != "")
{
return File.Exists(FBuildExePathOverride);
}
// Get the name of the FASTBuild executable.
string fbuild = "fbuild";
if (BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Win64)
{
fbuild = "fbuild.exe";
}
// Search the path for it
string PathVariable = Environment.GetEnvironmentVariable("PATH");
foreach (string SearchPath in PathVariable.Split(Path.PathSeparator))
{
try
{
string PotentialPath = Path.Combine(SearchPath, fbuild);
if (File.Exists(PotentialPath))
{
return true;
}
}
catch (ArgumentException)
{
// PATH variable may contain illegal characters; just ignore them.
}
}
return false;
}
private HashSet<string> ForceLocalCompileModules = new HashSet<string>()
{"Module.ProxyLODMeshReduction"};
private enum FBBuildType
{
Windows,
XBOne,
PS4
}
private FBBuildType BuildType = FBBuildType.Windows;
private void DetectBuildType(List<Action> Actions)
{
foreach (Action action in Actions)
{
if (action.ActionType != ActionType.Compile && action.ActionType != ActionType.Link)
continue;
if (action.CommandPath.Contains("orbis"))
{
BuildType = FBBuildType.PS4;
return;
}
else if (action.CommandArguments.Contains("Intermediate\\Build\\XboxOne"))
{
BuildType = FBBuildType.XBOne;
return;
}
else if (action.CommandPath.Contains("Microsoft")) //Not a great test.
{
BuildType = FBBuildType.Windows;
return;
}
}
}
private bool IsMSVC() { return BuildType == FBBuildType.Windows || BuildType == FBBuildType.XBOne; }
private bool IsXBOnePDBUtil(Action action) { return action.CommandPath.Contains("XboxOnePDBFileUtil.exe"); }
private string GetCompilerName()
{
switch (BuildType)
{
default:
case FBBuildType.XBOne:
case FBBuildType.Windows: return "UE4Compiler";
case FBBuildType.PS4: return "UE4PS4Compiler";
}
}
//Run FASTBuild on the list of actions. Relies on fbuild.exe being in the path.
public override bool ExecuteActions(List<Action> Actions, bool bLogDetailedActionStats)
{
bool FASTBuildResult = true;
if (Actions.Count > 0)
{
DetectBuildType(Actions);
// DEBUG ROPE -- BFF FILE HAS REFERENCE TO THE BAD FILE
//
//
string FASTBuildFilePath = Path.Combine(UnrealBuildTool.EngineDirectory.FullName, "Intermediate", "Build", "fbuild.bff");
if (CreateBffFile(Actions, FASTBuildFilePath))
{
FASTBuildResult = ExecuteBffFile(FASTBuildFilePath);
}
else
{
FASTBuildResult = false;
}
}
return FASTBuildResult;
}
private void AddText(string StringToWrite)
{
byte[] Info = new System.Text.UTF8Encoding(true).GetBytes(StringToWrite);
bffOutputFileStream.Write(Info, 0, Info.Length);
}
private string SubstituteEnvironmentVariables(string commandLineString)
{
string outputString = commandLineString.Replace("$(DurangoXDK)", "$DurangoXDK$");
outputString = outputString.Replace("$(SCE_ORBIS_SDK_DIR)", "$SCE_ORBIS_SDK_DIR$");
outputString = outputString.Replace("$(DXSDK_DIR)", "$DXSDK_DIR$");
outputString = outputString.Replace("$(CommonProgramFiles)", "$CommonProgramFiles$");
return outputString;
}
private Dictionary<string, string> ParseCommandLineOptions(string CompilerCommandLine, string[] SpecialOptions, 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 = "";
if (RawTokens.Length >= 1 && RawTokens[0].StartsWith("@\"")) //Response files are in 4.13 by default. Changing VCToolChain to not do this is probably better.
{
string responseCommandline = RawTokens[0];
// If we had spaces inside the response file path, we need to reconstruct the path.
for (int i = 1; i < RawTokens.Length; ++i)
{
responseCommandline += " " + RawTokens[i];
}
ResponseFilePath = responseCommandline.Substring(2, responseCommandline.Length - 3); // bit of a bodge to get the @"response.txt" path...
try
{
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)
{
Console.WriteLine("Looks like a response file in: " + CompilerCommandLine + ", but we could not load it! " + e.Message);
ResponseFilePath = "";
}
}
// 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 < RawTokens.Length; ++i)
{
string Token = RawTokens[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++;
}
// 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 < RawTokens.Length; ++i)
{
string NextToken = RawTokens[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)
{
Console.WriteLine("Warning! Looks like an unterminated string in tokens. Adding PartialToken and hoping for the best. Command line: " + 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;
}
if (Token == "/I" || Token == "/l" || Token == "/D" || Token == "-D" || Token == "-x") // Skip tokens with values, I for cpp includes, l for resource compiler includes
{
++i;
}
else if (!Token.StartsWith("/") && !Token.StartsWith("-"))
{
ParsedCompilerOptions["InputFile"] = Token;
ProcessedTokens.RemoveAt(i);
break;
}
}
ParsedCompilerOptions["OtherOptions"] = string.Join(" ", ProcessedTokens) + " ";
if (SaveResponseFile && !string.IsNullOrEmpty(ResponseFilePath))
{
ParsedCompilerOptions["@"] = ResponseFilePath;
}
return ParsedCompilerOptions;
}
private List<Action> SortActions(List<Action> InActions)
{
List<Action> Actions = InActions;
int NumSortErrors = 0;
for (int ActionIndex = 0; ActionIndex < InActions.Count; ActionIndex++)
{
Action Action = InActions[ActionIndex];
foreach (FileItem Item in Action.PrerequisiteItems)
{
if (Item.ProducingAction != null && InActions.Contains(Item.ProducingAction))
{
int DepIndex = InActions.IndexOf(Item.ProducingAction);
if (DepIndex > ActionIndex)
{
NumSortErrors++;
}
}
}
}
if (NumSortErrors > 0)
{
Actions = new List<Action>();
var UsedActions = new HashSet<int>();
for (int ActionIndex = 0; ActionIndex < InActions.Count; ActionIndex++)
{
if (UsedActions.Contains(ActionIndex))
{
continue;
}
Action Action = InActions[ActionIndex];
foreach (FileItem Item in Action.PrerequisiteItems)
{
if (Item.ProducingAction != null && InActions.Contains(Item.ProducingAction))
{
int DepIndex = InActions.IndexOf(Item.ProducingAction);
if (UsedActions.Contains(DepIndex))
{
continue;
}
Actions.Add(Item.ProducingAction);
UsedActions.Add(DepIndex);
}
}
Actions.Add(Action);
UsedActions.Add(ActionIndex);
}
for (int ActionIndex = 0; ActionIndex < Actions.Count; ActionIndex++)
{
Action Action = Actions[ActionIndex];
foreach (FileItem Item in Action.PrerequisiteItems)
{
if (Item.ProducingAction != null && Actions.Contains(Item.ProducingAction))
{
int DepIndex = Actions.IndexOf(Item.ProducingAction);
if (DepIndex > ActionIndex)
{
Console.WriteLine("Action is not topologically sorted.");
Console.WriteLine(" {0} {1}", Action.CommandPath, Action.CommandArguments);
Console.WriteLine("Dependency");
Console.WriteLine(" {0} {1}", Item.ProducingAction.CommandPath, Item.ProducingAction.CommandArguments);
throw new BuildException("Cyclical Dependency in action graph.");
}
}
}
}
}
return Actions;
}
private string GetOptionValue(Dictionary<string, string> OptionsDictionary, string Key, Action Action, bool ProblemIfNotFound = false)
{
string Value = string.Empty;
if (OptionsDictionary.TryGetValue(Key, out Value))
{
return Value.Trim(new Char[] { '\"' });
}
if (ProblemIfNotFound)
{
Console.WriteLine("We failed to find " + Key + ", which may be a problem.");
Console.WriteLine("Action.CommandArguments: " + Action.CommandArguments);
}
return Value;
}
public string GetRegistryValue(string keyName, string valueName, object defaultValue)
{
object returnValue = (string)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 = (string)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();
}
private void WriteEnvironmentSetup()
{
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)
{
// 4.19.x -- VCEnv = VCEnvironment.SetEnvironment(CppPlatform.Win64, WindowsPlatform.GetDefaultCompiler(null));
// 4.21.x - DOC - The version strings accept "Latest" to use the latest one, `null` to use the default one or a specified version.
VCEnv = VCEnvironment.Create(WindowsPlatform.GetDefaultCompiler(null), CppPlatform.Win64, null, null);
}
else if (BuildType == FBBuildType.XBOne)
{
// If you have XboxOne source access, uncommenting the line below will be better for selecting the appropriate version of the compiler.
// Translate the XboxOne compiler to the right Windows compiler to set the VC environment vars correctly...
//WindowsCompiler windowsCompiler = XboxOnePlatform.GetDefaultCompiler() == XboxOneCompiler.VisualStudio2015 ? WindowsCompiler.VisualStudio2015 : WindowsCompiler.VisualStudio2017;
//VCEnv = VCEnvironment.SetEnvironment(CppPlatform.Win64, windowsCompiler);
}
}
catch (Exception)
{
Console.WriteLine("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 (DictionaryEntry entry in Environment.GetEnvironmentVariables())
{
envVars[(string)entry.Key] = (string)entry.Value;
}
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";
switch (VCEnv.Compiler)
{
case WindowsCompiler.VisualStudio2015:
platformVersionNumber = "140";
break;
case WindowsCompiler.VisualStudio2017:
// 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";
break;
default:
string exceptionString = "Error: Unsupported Visual Studio Version.";
Console.WriteLine(exceptionString);
throw new BuildException(exceptionString);
}
// Different relative paths for Windows SDK 8.1 and 10 . SDK 10 includes a version directory.
string relativePathToResourceCompiler = VCEnv.GetWinSdkResourceCompilerPath().MakeRelativeTo(VCEnv.WindowsSdkDir);
AddText(string.Format(".WindowsSDKBasePath = '{0}'\n", VCEnv.WindowsSdkDir));
AddText("Compiler('UE4ResourceCompiler') \n{\n");
AddText(string.Format("\t.Executable = '$WindowsSDKBasePath$/{0}'\n", relativePathToResourceCompiler));
AddText("\t.CompilerFamily = 'custom'\n");
AddText("}\n\n");
AddText("Compiler('UE4Compiler') \n{\n");
//AddText(string.Format("\t.Root = '{0}'\n", VCEnv.VCToolPath64));
AddText(string.Format("\t.Root = '{0}'\n", VCEnv.GetToolPath()));
AddText("\t.Executable = '$Root$/cl.exe'\n");
AddText("\t.ExtraFiles =\n\t{\n");
AddText("\t\t'$Root$/c1.dll'\n");
AddText("\t\t'$Root$/c1xx.dll'\n");
AddText("\t\t'$Root$/c2.dll'\n");
//if (File.Exists(VCEnv.VCToolPath64 + "1033/clui.dll")) //Check English first...
if (File.Exists(VCEnv.GetToolPath() + "1033/clui.dll")) //Check English first...
{
AddText("\t\t'$Root$/1033/clui.dll'\n");
}
else
{
//var numericDirectories = Directory.GetDirectories(VCEnv.VCToolPath64.ToString()).Where(d => Path.GetFileName(d).All(char.IsDigit));
var numericDirectories = Directory.GetDirectories(VCEnv.GetToolPath().ToString()).Where(d => Path.GetFileName(d).All(char.IsDigit));
var cluiDirectories = numericDirectories.Where(d => Directory.GetFiles(d, "clui.dll").Any());
if (cluiDirectories.Any())
{
AddText(string.Format("\t\t'$Root$/{0}/clui.dll'\n", Path.GetFileName(cluiDirectories.First())));
}
}
AddText("\t\t'$Root$/mspdbsrv.exe'\n");
AddText("\t\t'$Root$/mspdbcore.dll'\n");
AddText(string.Format("\t\t'$Root$/mspft{0}.dll'\n", platformVersionNumber));
AddText(string.Format("\t\t'$Root$/msobj{0}.dll'\n", platformVersionNumber));
AddText(string.Format("\t\t'$Root$/mspdb{0}.dll'\n", platformVersionNumber));
if (VCEnv.Compiler == WindowsCompiler.VisualStudio2015)
{
//AddText(string.Format("\t\t'{0}/redist/x64/Microsoft.VC{1}.CRT/msvcp{2}.dll'\n", VCEnv.VCInstallDir, platformVersionNumber, platformVersionNumber));
AddText(string.Format("\t\t'{0}/redist/x64/Microsoft.VC{1}.CRT/msvcp{2}.dll'\n", VCEnv.GetVCInstallDirectory(), platformVersionNumber, platformVersionNumber));
//AddText(string.Format("\t\t'{0}/redist/x64/Microsoft.VC{1}.CRT/vccorlib{2}.dll'\n", VCEnv.VCInstallDir, platformVersionNumber, platformVersionNumber));
AddText(string.Format("\t\t'{0}/redist/x64/Microsoft.VC{1}.CRT/vccorlib{2}.dll'\n", VCEnv.GetVCInstallDirectory(), platformVersionNumber, platformVersionNumber));
}
else
{
//VS 2017 is really confusing in terms of version numbers and paths so these values might need to be modified depending on what version of the tool chain you
// chose to install.
//AddText(string.Format("\t\t'{0}/Redist/MSVC/14.16.27012/x64/Microsoft.VC141.CRT/msvcp{1}.dll'\n", VCEnv.VCInstallDir, platformVersionNumber));
AddText(string.Format("\t\t'{0}/Redist/MSVC/14.16.27012/x64/Microsoft.VC141.CRT/msvcp{1}.dll'\n", VCEnv.GetVCInstallDirectory(), platformVersionNumber));
//AddText(string.Format("\t\t'{0}/Redist/MSVC/14.16.27012/x64/Microsoft.VC141.CRT/vccorlib{1}.dll'\n", VCEnv.VCInstallDir, platformVersionNumber));
AddText(string.Format("\t\t'{0}/Redist/MSVC/14.16.27012/x64/Microsoft.VC141.CRT/vccorlib{1}.dll'\n", VCEnv.GetVCInstallDirectory(), platformVersionNumber));
}
AddText("\t}\n"); //End extra files
AddText("}\n\n"); //End compiler
}
if (envVars.ContainsKey("SCE_ORBIS_SDK_DIR"))
{
AddText(string.Format(".SCE_ORBIS_SDK_DIR = '{0}'\n", envVars["SCE_ORBIS_SDK_DIR"]));
AddText(string.Format(".PS4BasePath = '{0}/host_tools/bin'\n\n", envVars["SCE_ORBIS_SDK_DIR"]));
AddText("Compiler('UE4PS4Compiler') \n{\n");
AddText("\t.Executable = '$PS4BasePath$/orbis-clang.exe'\n");
AddText("}\n\n");
}
AddText("Settings \n{\n");
// Optional cachePath user setting
if (bEnableCaching && CachePath != "")
{
AddText(string.Format("\t.CachePath = '{0}'\n", CachePath));
}
//Start Environment
AddText("\t.Environment = \n\t{\n");
if (VCEnv != null)
{
//AddText(string.Format("\t\t\"PATH={0}\\Common7\\IDE\\;{1}\",\n", VCEnv.VCInstallDir, VCEnv.VCToolPath64));
AddText(string.Format("\t\t\"PATH={0}\\Common7\\IDE\\;{1}\",\n", VCEnv.GetVCInstallDirectory(), VCEnv.GetToolPath()));
}
if (envVars.ContainsKey("TMP"))
AddText(string.Format("\t\t\"TMP={0}\",\n", envVars["TMP"]));
if (envVars.ContainsKey("SystemRoot"))
AddText(string.Format("\t\t\"SystemRoot={0}\",\n", envVars["SystemRoot"]));
string includePathsString = string.Join(";", VCEnv.IncludePaths);
if (envVars.ContainsKey("INCLUDE"))
AddText(string.Format("\t\t\"INCLUDE={0}\",\n", envVars["INCLUDE"]));
else
AddText(string.Format("\t\t\"INCLUDE={0}\",\n", includePathsString));
string libPathsString = string.Join(";", VCEnv.LibraryPaths);
if (envVars.ContainsKey("LIB"))
AddText(string.Format("\t\t\"LIB={0}\",\n", envVars["LIB"]));
else
AddText(string.Format("\t\t\"LIB={0}\",\n", libPathsString));
AddText("\t}\n"); //End environment
AddText("}\n\n"); //End Settings
}
private void AddCompileAction(Action Action, int ActionIndex, List<int> DependencyIndices)
{
string CompilerName = GetCompilerName();
if (Action.CommandPath.Contains("rc.exe"))
{
CompilerName = "UE4ResourceCompiler";
}
string[] SpecialCompilerOptions = { "/Fo", "/fo", "/Yc", "/Yu", "/Fp", "-o" };
var ParsedCompilerOptions = ParseCommandLineOptions(Action.CommandArguments, SpecialCompilerOptions);
string OutputObjectFileName = GetOptionValue(ParsedCompilerOptions, IsMSVC() ? "/Fo" : "-o", Action, ProblemIfNotFound: !IsMSVC());
if (IsMSVC() && string.IsNullOrEmpty(OutputObjectFileName)) // Didn't find /Fo, try /fo
{
OutputObjectFileName = GetOptionValue(ParsedCompilerOptions, "/fo", Action, ProblemIfNotFound: true);
}
if (string.IsNullOrEmpty(OutputObjectFileName)) //No /Fo or /fo, we're probably in trouble.
{
Console.WriteLine("We have no OutputObjectFileName. Bailing.");
return;
}
string IntermediatePath = Path.GetDirectoryName(OutputObjectFileName);
if (string.IsNullOrEmpty(IntermediatePath))
{
Console.WriteLine("We have no IntermediatePath. Bailing.");
Console.WriteLine("Our Action.CommandArguments were: " + Action.CommandArguments);
return;
}
string InputFile = GetOptionValue(ParsedCompilerOptions, "InputFile", Action, ProblemIfNotFound: true);
if (string.IsNullOrEmpty(InputFile))
{
Console.WriteLine("We have no InputFile. Bailing.");
return;
}
AddText(string.Format("ObjectList('Action_{0}')\n{{\n", ActionIndex));
AddText(string.Format("\t.Compiler = '{0}' \n", CompilerName));
AddText(string.Format("\t.CompilerInputFiles = \"{0}\"\n", InputFile));
AddText(string.Format("\t.CompilerOutputPath = \"{0}\"\n", IntermediatePath));
if (!Action.bCanExecuteRemotely || !Action.bCanExecuteRemotelyWithSNDBS || ForceLocalCompileModules.Contains(Path.GetFileNameWithoutExtension(InputFile)))
{
AddText(string.Format("\t.AllowDistribution = false\n"));
}
string OtherCompilerOptions = GetOptionValue(ParsedCompilerOptions, "OtherOptions", Action);
string CompilerOutputExtension = ".unset";
if (ParsedCompilerOptions.ContainsKey("/Yc")) //Create PCH
{
string PCHIncludeHeader = GetOptionValue(ParsedCompilerOptions, "/Yc", Action, ProblemIfNotFound: true);
string PCHOutputFile = GetOptionValue(ParsedCompilerOptions, "/Fp", Action, ProblemIfNotFound: true);
AddText(string.Format("\t.CompilerOptions = '\"%1\" /Fo\"%2\" /Fp\"{0}\" /Yu\"{1}\" {2} '\n", PCHOutputFile, PCHIncludeHeader, OtherCompilerOptions));
AddText(string.Format("\t.PCHOptions = '\"%1\" /Fp\"%2\" /Yc\"{0}\" {1} /Fo\"{2}\"'\n", PCHIncludeHeader, OtherCompilerOptions, OutputObjectFileName));
AddText(string.Format("\t.PCHInputFile = \"{0}\"\n", InputFile));
AddText(string.Format("\t.PCHOutputFile = \"{0}\"\n", PCHOutputFile));
CompilerOutputExtension = ".obj";
}
else if (ParsedCompilerOptions.ContainsKey("/Yu")) //Use PCH
{
string PCHIncludeHeader = GetOptionValue(ParsedCompilerOptions, "/Yu", Action, ProblemIfNotFound: true);
string PCHOutputFile = GetOptionValue(ParsedCompilerOptions, "/Fp", Action, ProblemIfNotFound: true);
string PCHToForceInclude = PCHOutputFile.Replace(".pch", "");
AddText(string.Format("\t.CompilerOptions = '\"%1\" /Fo\"%2\" /Fp\"{0}\" /Yu\"{1}\" /FI\"{2}\" {3} '\n", PCHOutputFile, PCHIncludeHeader, PCHToForceInclude, OtherCompilerOptions));
CompilerOutputExtension = ".cpp.obj";
}
else
{
if (CompilerName == "UE4ResourceCompiler")
{
AddText(string.Format("\t.CompilerOptions = '{0} /fo\"%2\" \"%1\" '\n", OtherCompilerOptions));
CompilerOutputExtension = Path.GetExtension(InputFile) + ".res";
}
else
{
if (IsMSVC())
{
AddText(string.Format("\t.CompilerOptions = '{0} /Fo\"%2\" \"%1\" '\n", OtherCompilerOptions));
CompilerOutputExtension = ".cpp.obj";
}
else
{
AddText(string.Format("\t.CompilerOptions = '{0} -o \"%2\" \"%1\" '\n", OtherCompilerOptions));
CompilerOutputExtension = ".cpp.o";
}
}
}
AddText(string.Format("\t.CompilerOutputExtension = '{0}' \n", CompilerOutputExtension));
if (DependencyIndices.Count > 0)
{
List<string> DependencyNames = DependencyIndices.ConvertAll(x => string.Format("'Action_{0}'", x));
AddText(string.Format("\t.PreBuildDependencies = {{ {0} }}\n", string.Join(",", DependencyNames.ToArray())));
}
AddText(string.Format("}}\n\n"));
}
private void AddLinkAction(List<Action> Actions, int ActionIndex, List<int> DependencyIndices)
{
Action Action = Actions[ActionIndex];
string[] SpecialLinkerOptions = { "/OUT:", "@", "-o" };
var ParsedLinkerOptions = ParseCommandLineOptions(Action.CommandArguments, SpecialLinkerOptions, SaveResponseFile: true);
string OutputFile;
if (IsXBOnePDBUtil(Action))
{
OutputFile = ParsedLinkerOptions["OtherOptions"].Trim(' ').Trim('"');
}
else if (IsMSVC())
{
OutputFile = GetOptionValue(ParsedLinkerOptions, "/OUT:", Action, ProblemIfNotFound: true);
}
else //PS4
{
OutputFile = GetOptionValue(ParsedLinkerOptions, "-o", Action, ProblemIfNotFound: false);
if (string.IsNullOrEmpty(OutputFile))
{
OutputFile = GetOptionValue(ParsedLinkerOptions, "InputFile", Action, ProblemIfNotFound: true);
}
}
if (string.IsNullOrEmpty(OutputFile))
{
Console.WriteLine("Failed to find output file. Bailing.");
return;
}
string ResponseFilePath = GetOptionValue(ParsedLinkerOptions, "@", Action);
string OtherCompilerOptions = GetOptionValue(ParsedLinkerOptions, "OtherOptions", Action);
List<int> PrebuildDependencies = new List<int>();
if (IsXBOnePDBUtil(Action))
{
AddText(string.Format("Exec('Action_{0}')\n{{\n", ActionIndex));
AddText(string.Format("\t.ExecExecutable = '{0}'\n", Action.CommandPath));
AddText(string.Format("\t.ExecArguments = '{0}'\n", Action.CommandArguments));
AddText(string.Format("\t.ExecInput = {{ {0} }} \n", ParsedLinkerOptions["InputFile"]));
AddText(string.Format("\t.ExecOutput = '{0}' \n", OutputFile));
AddText(string.Format("\t.PreBuildDependencies = {{ {0} }} \n", ParsedLinkerOptions["InputFile"]));
AddText(string.Format("}}\n\n"));
}
else if (Action.CommandPath.Contains("lib.exe") || Action.CommandPath.Contains("orbis-snarl"))
{
if (DependencyIndices.Count > 0)
{
for (int i = 0; i < DependencyIndices.Count; ++i) //Don't specify pch or resource files, they have the wrong name and the response file will have them anyways.
{
int depIndex = DependencyIndices[i];
foreach (FileItem item in Actions[depIndex].ProducedItems)
{
if (item.ToString().Contains(".pch") || item.ToString().Contains(".res"))
{
DependencyIndices.RemoveAt(i);
i--;
PrebuildDependencies.Add(depIndex);
break;
}
}
}
}
AddText(string.Format("Library('Action_{0}')\n{{\n", ActionIndex));
AddText(string.Format("\t.Compiler = '{0}'\n", GetCompilerName()));
if (IsMSVC())
AddText(string.Format("\t.CompilerOptions = '\"%1\" /Fo\"%2\" /c'\n"));
else
AddText(string.Format("\t.CompilerOptions = '\"%1\" -o \"%2\" -c'\n"));
AddText(string.Format("\t.CompilerOutputPath = \"{0}\"\n", Path.GetDirectoryName(OutputFile)));
AddText(string.Format("\t.Librarian = '{0}' \n", Action.CommandPath));
if (!string.IsNullOrEmpty(ResponseFilePath))
{
if (IsMSVC())
AddText(string.Format("\t.LibrarianOptions = ' /OUT:\"%2\" @\"{0}\" \"%1\"' \n", ResponseFilePath));
else
AddText(string.Format("\t.LibrarianOptions = '\"%2\" @\"%1\" {0}' \n", OtherCompilerOptions));
}
else
{
if (IsMSVC())
AddText(string.Format("\t.LibrarianOptions = ' /OUT:\"%2\" {0} \"%1\"' \n", OtherCompilerOptions));
}
if (DependencyIndices.Count > 0)
{
List<string> DependencyNames = DependencyIndices.ConvertAll(x => string.Format("'Action_{0}'", x));
if (!string.IsNullOrEmpty(ResponseFilePath))
AddText(string.Format("\t.LibrarianAdditionalInputs = {{ {0} }} \n", DependencyNames[0])); // Hack...Because FastBuild needs at least one Input file
else if (IsMSVC())
AddText(string.Format("\t.LibrarianAdditionalInputs = {{ {0} }} \n", string.Join(",", DependencyNames.ToArray())));
PrebuildDependencies.AddRange(DependencyIndices);
}
else
{
AddText(string.Format("\t.LibrarianAdditionalInputs = {{ '{0}' }} \n", GetOptionValue(ParsedLinkerOptions, "InputFile", Action, ProblemIfNotFound: true)));
}
if (PrebuildDependencies.Count > 0)
{
List<string> PrebuildDependencyNames = PrebuildDependencies.ConvertAll(x => string.Format("'Action_{0}'", x));
AddText(string.Format("\t.PreBuildDependencies = {{ {0} }} \n", string.Join(",", PrebuildDependencyNames.ToArray())));
}
AddText(string.Format("\t.LibrarianOutput = '{0}' \n", OutputFile));
AddText(string.Format("}}\n\n"));
}
else if (Action.CommandPath.Contains("link.exe") || Action.CommandPath.Contains("orbis-clang"))
{
if (DependencyIndices.Count > 0) //Insert a dummy node to make sure all of the dependencies are finished.
//If FASTBuild supports PreBuildDependencies on the Executable action we can remove this.
{
string dummyText = string.IsNullOrEmpty(ResponseFilePath) ? GetOptionValue(ParsedLinkerOptions, "InputFile", Action) : ResponseFilePath;
File.SetLastAccessTimeUtc(dummyText, DateTime.UtcNow);
AddText(string.Format("Copy('Action_{0}_dummy')\n{{ \n", ActionIndex));
AddText(string.Format("\t.Source = '{0}' \n", dummyText));
AddText(string.Format("\t.Dest = '{0}' \n", dummyText + ".dummy"));
List<string> DependencyNames = DependencyIndices.ConvertAll(x => string.Format("\t\t'Action_{0}', ;{1}", x, Actions[x].StatusDescription));
AddText(string.Format("\t.PreBuildDependencies = {{\n{0}\n\t}} \n", string.Join("\n", DependencyNames.ToArray())));
AddText(string.Format("}}\n\n"));
}
AddText(string.Format("Executable('Action_{0}')\n{{ \n", ActionIndex));
AddText(string.Format("\t.Linker = '{0}' \n", Action.CommandPath));
if (DependencyIndices.Count == 0)
{
AddText(string.Format("\t.Libraries = {{ '{0}' }} \n", ResponseFilePath));
if (IsMSVC())
{
if (BuildType == FBBuildType.XBOne)
{
AddText(string.Format("\t.LinkerOptions = '/TLBOUT:\"%1\" /Out:\"%2\" @\"{0}\" {1} ' \n", ResponseFilePath, OtherCompilerOptions)); // The TLBOUT is a huge bodge to consume the %1.
}
else
{
AddText(string.Format("\t.LinkerOptions = '/TLBOUT:\"%1\" /Out:\"%2\" @\"{0}\" ' \n", ResponseFilePath)); // The TLBOUT is a huge bodge to consume the %1.
}
}
else
AddText(string.Format("\t.LinkerOptions = '-o \"%2\" @\"{0}\" {1} -MQ \"%1\"' \n", ResponseFilePath, OtherCompilerOptions)); // The MQ is a huge bodge to consume the %1.
}
else
{
AddText(string.Format("\t.Libraries = 'Action_{0}_dummy' \n", ActionIndex));
if (IsMSVC())
{
if (BuildType == FBBuildType.XBOne)
{
AddText(string.Format("\t.LinkerOptions = '/TLBOUT:\"%1\" /Out:\"%2\" @\"{0}\" {1} ' \n", ResponseFilePath, OtherCompilerOptions)); // The TLBOUT is a huge bodge to consume the %1.
}
else
{
AddText(string.Format("\t.LinkerOptions = '/TLBOUT:\"%1\" /Out:\"%2\" @\"{0}\" ' \n", ResponseFilePath)); // The TLBOUT is a huge bodge to consume the %1.
}
}
else
AddText(string.Format("\t.LinkerOptions = '-o \"%2\" @\"{0}\" {1} -MQ \"%1\"' \n", ResponseFilePath, OtherCompilerOptions)); // The MQ is a huge bodge to consume the %1.
}
AddText(string.Format("\t.LinkerOutput = '{0}' \n", OutputFile));
AddText(string.Format("}}\n\n"));
}
}
/**
* As far as I'm aware it only copies a file from one location to another. BuildProject actions may be used
* for things other than copying so this may still break.
*
* Worked for full rebuild on 4.21.0
*/
private void AddBuildProjectAction(Action Action, int ActionIndex, List<int> DependencyIndices)
{
AddText(string.Format("Copy('Action_{0}')\n{{\n", ActionIndex));
AddText(string.Format("\t.Source = \"{0}\"\n", Action.PrerequisiteItems.First().AbsolutePath));
AddText(string.Format("\t.Dest = \"{0}\"\n", Action.ProducedItems.First().AbsolutePath));
AddText(string.Format("}}\n\n"));
}
private FileStream bffOutputFileStream = null;
private bool CreateBffFile(List<Action> InActions, string BffFilePath)
{
List<Action> Actions = SortActions(InActions);
try
{
bffOutputFileStream = new FileStream(BffFilePath, FileMode.Create, FileAccess.Write);
WriteEnvironmentSetup(); //Compiler, environment variables and base paths
for (int ActionIndex = 0; ActionIndex < Actions.Count; ActionIndex++)
{
Action Action = Actions[ActionIndex];
// Resolve dependencies
List<int> DependencyIndices = new List<int>();
foreach (FileItem Item in Action.PrerequisiteItems)
{
if (Item.ProducingAction != null)
{
int ProducingActionIndex = Actions.IndexOf(Item.ProducingAction);
if (ProducingActionIndex >= 0)
{
DependencyIndices.Add(ProducingActionIndex);
}
}
}
switch (Action.ActionType)
{
case ActionType.Compile: AddCompileAction(Action, ActionIndex, DependencyIndices); break;
case ActionType.Link: AddLinkAction(Actions, ActionIndex, DependencyIndices); break;
case ActionType.BuildProject: AddBuildProjectAction(Action, ActionIndex, DependencyIndices); break;
default: Console.WriteLine("Fastbuild is ignoring an unsupported action: " + Action.ActionType.ToString()); break;
}
}
AddText("Alias( 'all' ) \n{\n");
AddText("\t.Targets = { \n");
for (int ActionIndex = 0; ActionIndex < Actions.Count; ActionIndex++)
{
AddText(string.Format("\t\t'Action_{0}'{1}", ActionIndex, ActionIndex < (Actions.Count - 1) ? ",\n" : "\n\t}\n"));
}
AddText("}\n");
bffOutputFileStream.Close();
}
catch (Exception e)
{
Console.WriteLine("Exception while creating bff file: " + e.ToString());
return false;
}
return true;
}
private bool ExecuteBffFile(string BffFilePath)
{
string cacheArgument = "";
if (bEnableCaching)
{
switch (CacheMode)
{
case eCacheMode.ReadOnly:
cacheArgument = "-cacheread";
break;
case eCacheMode.WriteOnly:
cacheArgument = "-cachewrite";
break;
case eCacheMode.ReadWrite:
cacheArgument = "-cache";
break;
}
}
string distArgument = bEnableDistribution ? "-dist" : "";
//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 = string.Format("-monitor -summary {0} {1} -ide -clean -config {2}", distArgument, cacheArgument, BffFilePath);
ProcessStartInfo FBStartInfo = new ProcessStartInfo(string.IsNullOrEmpty(FBuildExePathOverride) ? "fbuild" : FBuildExePathOverride, FBCommandLine);
FBStartInfo.UseShellExecute = false;
FBStartInfo.WorkingDirectory = Path.Combine(UnrealBuildTool.EngineDirectory.MakeRelativeTo(DirectoryReference.GetCurrentDirectory()), "Source");
try
{
Process FBProcess = new Process();
FBProcess.StartInfo = FBStartInfo;
FBStartInfo.RedirectStandardError = true;
FBStartInfo.RedirectStandardOutput = true;
FBProcess.EnableRaisingEvents = true;
DataReceivedEventHandler OutputEventHandler = (Sender, Args) =>
{
if (Args.Data != null)
Console.WriteLine(Args.Data);
};
FBProcess.OutputDataReceived += OutputEventHandler;
FBProcess.ErrorDataReceived += OutputEventHandler;
FBProcess.Start();
FBProcess.BeginOutputReadLine();
FBProcess.BeginErrorReadLine();
FBProcess.WaitForExit();
return FBProcess.ExitCode == 0;
}
catch (Exception e)
{
Console.WriteLine("Exception launching fbuild process. Is it in your path?" + e.ToString());
return false;
}
}
}
}
// ...
public bool bEnableUndefinedIdentifierWarnings = false; // With Windows SDK 10 this breaks distributed builds.
// ...
using Tools.DotNETCommon;
namespace UnrealBuildTool
{
/**
* A collection of extension methods (on VCEnvironment) to expose properties for FASTBuild.cs .
* This will likely have to be updated with every minor (major?) Unreal version upgrade (4.x -> 4.y).
* At least this was the case between 4.19, 4.20 and 4.21 .
*
* CURRENT UNREAL VERSION:
*
* 4.21.0
*
* TODO: DOCUMENT EXPECTED TOOL LOCATIONS
*/
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.CompilerDir.ParentDirectory.ParentDirectory.ParentDirectory;
}
/// <summary>
/// The windows sdk folder structure is different between 8.1 and 10 . In 10 they are subdivided into WindowsSdkVersion folders.
/// This extension method returns the path that actually contains the actual resource compiler so that relevant parts can be
/// extracted for Fastbuild.
/// </summary>
/// <param name="VCEnv"></param>
/// <returns></returns>
public static FileReference GetWinSdkResourceCompilerPath(this VCEnvironment VCEnv)
{
FileReference ResourceCompilerPath = FileReference.Combine(VCEnv.WindowsSdkDir, "bin", VCEnv.WindowsSdkVersion.ToString(), "x64", "rc.exe");
if (FileReference.Exists(ResourceCompilerPath))
{
return ResourceCompilerPath;
}
ResourceCompilerPath = FileReference.Combine(VCEnv.WindowsSdkDir, "bin", "x64", "rc.exe");
if (FileReference.Exists(ResourceCompilerPath))
{
return ResourceCompilerPath;
}
throw new BuildException("Unable to find path to the Windows resource compiler under {0} (version {1})", VCEnv.WindowsSdkDir, VCEnv.WindowsSdkVersion);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment