// Copyright Epic Games, Inc. All Rights Reserved. using IncludeTool.Support; using EpicGames.Core; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Xml; namespace IncludeTool { /// /// The type of the compiler /// public enum CompilerType { Unknown, Clang, VisualC, } /// /// Encapsulates a compiler option /// [Serializable] public class CompileOption { /// /// The option name /// public readonly string Name; /// /// The option value. May be null. /// public readonly string Value; /// /// Constructor /// /// The option name /// The option value. May be null. public CompileOption(string InName, string InValue) { Name = InName; Value = InValue; } /// /// Formats the option as it would appear on the command line. /// /// String containing the option public override string ToString() { if(Value == null) { return Name; } else if(Name == "/Fo" || Name == "/Yc") { return Name + Utility.QuoteArgument(Value); } else { return Name + " " + Utility.QuoteArgument(Value); } } } /// /// Environment used to compile a file /// [Serializable] class CompileEnvironment { /// /// Path to the compiler executable /// public FileReference Compiler; /// /// The type of compiler /// public CompilerType CompilerType; /// /// All the options, with the exception of definitions and include paths /// public List Options = new List(); /// /// List of macro definitions /// public List Definitions = new List(); /// /// List of include paths /// public List IncludePaths = new List(); /// /// List of force-included headers /// public List ForceIncludeFiles = new List(); /// /// Default constructor /// public CompileEnvironment(FileReference InCompiler, CompilerType InCompilerType) { Compiler = InCompiler; CompilerType = InCompilerType; } /// /// Clone another compile environment /// /// The compile environment to copy public CompileEnvironment(CompileEnvironment Other) { Compiler = Other.Compiler; CompilerType = Other.CompilerType; Options = new List(Other.Options); Definitions = new List(Other.Definitions); IncludePaths = new List(Other.IncludePaths); ForceIncludeFiles = new List(Other.ForceIncludeFiles); } /// /// Gets the full command line to compile with, with the exception of any source files /// /// The complete command line public string GetCommandLine() { List Arguments = new List(); Arguments.AddRange(Options.Select(x => Utility.QuoteArgument(x.ToString()))); if(CompilerType == CompilerType.Clang) { Arguments.AddRange(Definitions.Select(x => "-D " + x)); Arguments.AddRange(IncludePaths.Select(x => "-I \"" + x + "\"")); Arguments.AddRange(ForceIncludeFiles.Select(x => "-include \"" + x + "\"")); } else { Arguments.AddRange(Definitions.Select(x => "/D " + x)); Arguments.AddRange(IncludePaths.Select(x => "/I \"" + x + "\"")); Arguments.AddRange(ForceIncludeFiles.Select(x => "/FI\"" + x + "\"")); } return String.Join(" ", Arguments); } /// /// Enumerates all the options with the given name /// /// Option to look for /// Sequence of the matching values for the given name public IEnumerable GetOptionValues(string InName) { foreach (CompileOption Option in Options) { if (Option.Name == InName) { yield return Option.Value; } } } /// /// Writes a response file which will compile the given source file /// /// The response file to write to /// The source file to compile public void WriteResponseFile(FileReference ResponseFile, FileReference SourceFile) { using (StreamWriter Writer = new StreamWriter(ResponseFile.FullName)) { foreach (CompileOption Option in Options) { if(CompilerType == CompilerType.Clang) { Writer.WriteLine(Option.ToString().Replace('\\', '/')); } else { Writer.WriteLine(Option.ToString()); } } foreach (string Definition in Definitions) { if(CompilerType == CompilerType.Clang) { Writer.WriteLine("-D {0}", Utility.QuoteArgument(Definition)); } else { Writer.WriteLine("/D {0}", Utility.QuoteArgument(Definition)); } } foreach (DirectoryReference IncludePath in IncludePaths) { if(CompilerType == CompilerType.Clang) { Writer.WriteLine("-I {0}", Utility.QuoteArgument(IncludePath.FullName.Replace('\\', '/'))); } else { Writer.WriteLine("/I {0}", Utility.QuoteArgument(IncludePath.FullName)); } } foreach (FileReference ForceIncludeFile in ForceIncludeFiles) { if(CompilerType == CompilerType.Clang) { Writer.WriteLine("-include {0}", Utility.QuoteArgument(ForceIncludeFile.FullName.Replace('\\', '/'))); } else { Writer.WriteLine("/FI{0}", Utility.QuoteArgument(ForceIncludeFile.FullName)); } } if(CompilerType == CompilerType.Clang) { Writer.WriteLine(Utility.QuoteArgument(SourceFile.FullName.Replace('\\', '/'))); } else { Writer.WriteLine("/Tp\"{0}\"", SourceFile.FullName); } } } /// /// Reads an exported XGE task list /// /// File to read from /// Mapping from source file to compile environment public static void ReadTaskList(FileReference TaskListFile, DirectoryReference BaseDir, out Dictionary FileToEnvironment) { XmlDocument Document = new XmlDocument(); Document.Load(TaskListFile.FullName); // Read the standard include paths from the INCLUDE environment variable in the script List StandardIncludePaths = new List(); foreach (XmlNode Node in Document.SelectNodes("BuildSet/Environments/Environment/Variables/Variable")) { XmlAttribute NameAttr = Node.Attributes["Name"]; if(NameAttr != null && String.Compare(NameAttr.InnerText, "INCLUDE") == 0) { foreach(string IncludePath in Node.Attributes["Value"].InnerText.Split(';')) { StandardIncludePaths.Add(new DirectoryReference(IncludePath)); } } } // Read all the individual compiles Dictionary NewFileToEnvironment = new Dictionary(); foreach (XmlNode Node in Document.SelectNodes("BuildSet/Environments/Environment/Tools/Tool")) { XmlAttribute ToolPathAttr = Node.Attributes["Path"]; if (ToolPathAttr != null) { // Get the full path to the tool FileReference ToolLocation = new FileReference(ToolPathAttr.InnerText); // Get the compiler type CompilerType CompilerType; if(GetCompilerType(ToolLocation, out CompilerType)) { string Name = Node.Attributes["Name"].InnerText; string Params = Node.Attributes["Params"].InnerText; // Construct the compile environment CompileEnvironment Environment = new CompileEnvironment(ToolLocation, CompilerType); // Parse a list of tokens List Tokens = new List(); ParseArgumentTokens(CompilerType, Params, Tokens, BaseDir); // Parse it into a list of options, removing any that don't apply List SourceFiles = new List(); List OutputFiles = new List(); for (int Idx = 0; Idx < Tokens.Count; Idx++) { if(Tokens[Idx] == "/Fo" || Tokens[Idx] == "/Fp" || Tokens[Idx] == "-o") { string OutputPath = Tokens[++Idx]; if (!Path.IsPathRooted(OutputPath)) { OutputPath = Path.Combine(BaseDir.FullName, OutputPath); } OutputFiles.Add(new FileReference(OutputPath)); } else if(Tokens[Idx].StartsWith("/Fo") || Tokens[Idx].StartsWith("/Fp")) { string OutputPath = Tokens[Idx].Substring(3); if (!Path.IsPathRooted(OutputPath)) { OutputPath = Path.Combine(BaseDir.FullName, OutputPath); } OutputFiles.Add(new FileReference(OutputPath)); } else if (Tokens[Idx] == "/D" || Tokens[Idx] == "-D") { Environment.Definitions.Add(Tokens[++Idx]); } else if(Tokens[Idx].StartsWith("/D")) { Environment.Definitions.Add(Tokens[Idx].Substring(2)); } else if (Tokens[Idx] == "/I" || Tokens[Idx] == "/external:I" || Tokens[Idx] == "-I" || Tokens[Idx] == "/imsvc" || Tokens[Idx] == "-isystem") { string IncludePath = Tokens[++Idx].Replace("//", "/"); if (!Path.IsPathRooted(IncludePath)) { IncludePath = Path.Combine(BaseDir.FullName, IncludePath); } Environment.IncludePaths.Add(new DirectoryReference(IncludePath.ToLowerInvariant())); } else if (Tokens[Idx].StartsWith("-I")) { string IncludePath = Tokens[Idx].Substring(2).Replace("//", "/"); if (!Path.IsPathRooted(IncludePath)) { IncludePath = Path.Combine(BaseDir.FullName, IncludePath); } Environment.IncludePaths.Add(new DirectoryReference(IncludePath.ToLowerInvariant())); } else if (Tokens[Idx].StartsWith("-isystem")) { string IncludePath = Tokens[Idx].Substring(8).Replace("//", "/"); if (!Path.IsPathRooted(IncludePath)) { IncludePath = Path.Combine(BaseDir.FullName, IncludePath); } Environment.IncludePaths.Add(new DirectoryReference(IncludePath.ToLowerInvariant())); } else if(Tokens[Idx].StartsWith("/FI")) { string ForceIncludePath = Tokens[Idx].Substring(3); if (!Path.IsPathRooted(ForceIncludePath)) { ForceIncludePath = Path.Combine(BaseDir.FullName, ForceIncludePath); } Environment.ForceIncludeFiles.Add(new FileReference(ForceIncludePath)); } else if(Tokens[Idx] == "-include") { string ForceIncludePath = Tokens[++Idx]; if (!Path.IsPathRooted(ForceIncludePath)) { ForceIncludePath = Path.Combine(BaseDir.FullName, ForceIncludePath); } Environment.ForceIncludeFiles.Add(new FileReference(ForceIncludePath)); } else if (Tokens[Idx] == "-Xclang" || Tokens[Idx] == "-target" || Tokens[Idx] == "--target" || Tokens[Idx] == "-x" || Tokens[Idx] == "-o") { Environment.Options.Add(new CompileOption(Tokens[Idx], Tokens[++Idx])); } else if (Tokens[Idx].StartsWith("/") || Tokens[Idx].StartsWith("-")) { Environment.Options.Add(new CompileOption(Tokens[Idx], null)); } else { SourceFiles.Add(FileReference.Combine(BaseDir, Tokens[Idx])); } } // Make sure we're not compiling a precompiled header if(!OutputFiles.Any(x => x.HasExtension(".gch")) && !Environment.Options.Any(x => x.Name.StartsWith("/Yc"))) { // Add all the standard include paths Environment.IncludePaths.AddRange(StandardIncludePaths); // Add to the output map if there are any source files. Use the extension to distinguish between a source file and linker invocation on Clang. foreach (FileReference SourceFile in SourceFiles) { if(!SourceFile.HasExtension(".a")) { if(NewFileToEnvironment.ContainsKey(SourceFile)) { Console.WriteLine("Source file {0} is compiled with multiple environments", SourceFile); } else { NewFileToEnvironment.Add(SourceFile, Environment); } } } } } } } FileToEnvironment = NewFileToEnvironment; } /// /// Determine the compiler type /// /// Path to the compiler /// Type of the compiler /// True if the path is a valid known compiler, and CompilerType is set static bool GetCompilerType(FileReference Compiler, out CompilerType CompilerType) { string FileName = Compiler.GetFileName().ToLowerInvariant(); if(FileName == "cl.exe") { CompilerType = CompilerType.VisualC; return true; } else if(FileName == "clang++.exe") { CompilerType = CompilerType.Clang; return true; } else { CompilerType = CompilerType.Unknown; return false; } } /// /// Parse a parameter list into a series of tokens, reading response files as appropriate /// /// The line of text to parse /// List to be filled with the parsed tokens /// Base directory to root relative pathed response files from static void ParseArgumentTokens(CompilerType CompilerType, string Text, List Tokens, DirectoryReference BaseDir) { for (int Idx = 0; Idx < Text.Length; Idx++) { if (!Char.IsWhiteSpace(Text[Idx])) { // Read until the end of the token StringBuilder Token = new StringBuilder(); while (Idx < Text.Length && !Char.IsWhiteSpace(Text[Idx])) { if (Text[Idx] == '"') { Idx++; while (Text[Idx] != '"') { if(Idx + 1 < Text.Length && Text[Idx] == '\\' && Text[Idx + 1] == '\"' && CompilerType == CompilerType.Clang) { Idx++; } Token.Append(Text[Idx++]); } Idx++; } else { Token.Append(Text[Idx++]); } } // Add the token to the list. If it's a response file, recursively parse the tokens out of that. if(Token[0] == '@') { string ResponsePath = Token.ToString().Substring(1); if (!Path.IsPathRooted(ResponsePath)) { ResponsePath = Path.Combine(BaseDir.FullName, ResponsePath); } ParseArgumentTokens(CompilerType, File.ReadAllText(ResponsePath), Tokens, BaseDir); } else { Tokens.Add(Token.ToString()); } } } } /// /// Returns the full command line for this compile environment /// /// String containing the full command line public override string ToString() { return GetCommandLine(); } } }