// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using UnrealBuildTool; using System.Diagnostics; using EpicGames.Core; using System.Reflection; using UnrealBuildBase; using System.Runtime.Serialization; namespace AutomationTool { public class SingleTargetProperties { public string TargetName; public string TargetClassName; public TargetRules Rules; } /// /// Autodetected project properties. /// public class ProjectProperties { /// /// Full Project path. Must be a .uproject file /// public FileReference RawProjectPath; /// /// True if the uproject contains source code. /// public bool bIsCodeBasedProject; /// /// List of all targets detected for this project. /// public List Targets = new List(); /// /// List of all scripts that were compiled to create the list of Targets /// public List TargetScripts = new List(); /// /// List of all Engine ini files for this project /// public Dictionary EngineConfigs = new Dictionary(); /// /// List of all Game ini files for this project /// public Dictionary GameConfigs = new Dictionary(); /// /// List of all programs detected for this project. /// public List Programs = new List(); /// /// Specifies if the target files were generated /// public bool bWasGenerated = false; internal ProjectProperties() { } } /// /// Project related utility functions. /// public class ProjectUtils { private static Dictionary PropertiesCache = new Dictionary(StringComparer.InvariantCultureIgnoreCase); /// /// Gets a short project name (QAGame, Elemental, etc) /// /// Full project path. /// True if a uproject. /// Short project name public static string GetShortProjectName(FileReference RawProjectPath) { return CommandUtils.GetFilenameWithoutAnyExtensions(RawProjectPath.FullName); } /// /// Gets a short alphanumeric identifier for the project path. /// /// Full project path. /// Project path identifier public static string GetProjectPathId(FileReference RawProjectPath) { string UniformProjectPath = FileReference.FindCorrectCase(RawProjectPath).ToNormalizedPath(); string ProjectPathHash = ContentHash.MD5(Encoding.UTF8.GetBytes(UniformProjectPath)).ToString(); return String.Format("{0}.{1}", GetShortProjectName(RawProjectPath), ProjectPathHash.Substring(0, 8)); } /// /// Gets project properties. /// /// Full project path. /// Properties of the project. public static ProjectProperties GetProjectProperties(FileReference RawProjectPath, List ClientTargetPlatforms = null, List ClientTargetConfigurations = null, bool AssetNativizationRequested = false) { string ProjectKey = "UE4"; if (RawProjectPath != null) { ProjectKey = CommandUtils.ConvertSeparators(PathSeparator.Slash, RawProjectPath.FullName); } ProjectProperties Properties; if (PropertiesCache.TryGetValue(ProjectKey, out Properties) == false) { Properties = DetectProjectProperties(RawProjectPath, ClientTargetPlatforms, ClientTargetConfigurations, AssetNativizationRequested); PropertiesCache.Add(ProjectKey, Properties); } return Properties; } /// /// Checks if the project is a UProject file with source code. /// /// Full project path. /// True if the project is a UProject file with source code. public static bool IsCodeBasedUProjectFile(FileReference RawProjectPath, List ClientTargetConfigurations = null) { return GetProjectProperties(RawProjectPath, null, ClientTargetConfigurations).bIsCodeBasedProject; } /// /// Returns a path to the client binaries folder. /// /// Full project path. /// Platform type. /// Path to the binaries folder. public static DirectoryReference GetProjectClientBinariesFolder(DirectoryReference ProjectClientBinariesPath, UnrealTargetPlatform Platform) { ProjectClientBinariesPath = DirectoryReference.Combine(ProjectClientBinariesPath, Platform.ToString()); return ProjectClientBinariesPath; } private static bool ProjectHasCode(FileReference RawProjectPath) { // check to see if we already have a Target.cs file if (File.Exists(Path.Combine(Path.GetDirectoryName(RawProjectPath.FullName), "Source", RawProjectPath.GetFileNameWithoutExtension() + ".Target.cs"))) { return true; } else if (Directory.Exists(Path.Combine(Path.GetDirectoryName(RawProjectPath.FullName), "Source"))) { // wasn't one in the main Source directory, let's check all sub-directories //@todo: may want to read each target.cs to see if it has a target corresponding to the project name as a final check FileInfo[] Files = (new DirectoryInfo(Path.Combine(Path.GetDirectoryName(RawProjectPath.FullName), "Source")).GetFiles("*.Target.cs", SearchOption.AllDirectories)); if (Files.Length > 0) { return true; } } return false; } private static bool RequiresTempTarget(FileReference RawProjectPath, List Platforms, List Configurations, bool AssetNativizationRequested) { bool bHasCode = ProjectHasCode(RawProjectPath); foreach (UnrealTargetPlatform Platform in Platforms) { foreach(UnrealTargetConfiguration Configuration in Configurations) { string Reason; if(RequiresTempTarget(RawProjectPath, bHasCode, Platform, Configuration, TargetType.Game, AssetNativizationRequested, true, out Reason)) { Log.TraceInformation("{0} requires a temporary target.cs to be generated ({1})", RawProjectPath.GetFileName(), Reason); return true; } } } return false; } /// /// NOTE: This function must mirror the functionality of TargetPlatformBase::RequiresTempTarget /// private static bool RequiresTempTarget(FileReference RawProjectPath, bool bProjectHasCode, UnrealTargetPlatform Platform, UnrealTargetConfiguration Configuration, TargetType TargetType, bool bRequiresAssetNativization, bool bRequiresCookedData, out string OutReason) { // check to see if we already have a Target.cs file if (bProjectHasCode) { OutReason = null; return false; } // check if asset nativization is enabled if (bRequiresAssetNativization) { OutReason = "asset nativization is enabled"; return true; } // Check if encryption or signing is enabled EncryptionAndSigning.CryptoSettings Settings = EncryptionAndSigning.ParseCryptoSettings(RawProjectPath.Directory, Platform, Log.Logger); if (Settings.IsAnyEncryptionEnabled() || Settings.IsPakSigningEnabled()) { OutReason = "encryption/signing is enabled"; return true; } // check the target platforms for any differences in build settings or additional plugins if(!Unreal.IsEngineInstalled() && !PlatformExports.HasDefaultBuildConfig(RawProjectPath, Platform)) { OutReason = "project has non-default build configuration"; return true; } if(PlatformExports.RequiresBuild(RawProjectPath, Platform)) { OutReason = "overriden by target platform"; return true; } // Read the project descriptor, and find all the plugins available to this project ProjectDescriptor Project = ProjectDescriptor.FromFile(RawProjectPath); // Enumerate all the available plugins Dictionary AllPlugins = Plugins.ReadAvailablePlugins(Unreal.EngineDirectory, DirectoryReference.FromFile(RawProjectPath), new List()).ToDictionary(x => x.Name, x => x, StringComparer.OrdinalIgnoreCase); // find if there are any plugins enabled or disabled which differ from the default string Reason; if (RequiresTempTargetForCodePlugin(Project, Platform, Configuration, TargetType, AllPlugins, out Reason)) { OutReason = Reason; return true; } OutReason = null; return false; } /// /// NOTE: This function must mirror FPluginManager::RequiresTempTargetForCodePlugin /// static bool RequiresTempTargetForCodePlugin(ProjectDescriptor ProjectDescriptor, UnrealTargetPlatform Platform, UnrealTargetConfiguration Configuration, TargetType TargetType, Dictionary AllPlugins, out string OutReason) { PluginReferenceDescriptor MissingPlugin; HashSet ProjectCodePlugins = new HashSet(StringComparer.OrdinalIgnoreCase); if (!GetCodePluginsForTarget(ProjectDescriptor, Platform, Configuration, TargetType, ProjectCodePlugins, AllPlugins, out MissingPlugin)) { OutReason = String.Format("{0} plugin is referenced by target but not found", MissingPlugin.Name); return true; } HashSet DefaultCodePlugins = new HashSet(StringComparer.OrdinalIgnoreCase); if (!GetCodePluginsForTarget(null, Platform, Configuration, TargetType, DefaultCodePlugins, AllPlugins, out MissingPlugin)) { OutReason = String.Format("{0} plugin is referenced by the default target but not found", MissingPlugin.Name); return true; } foreach (string ProjectCodePlugin in ProjectCodePlugins) { if (!DefaultCodePlugins.Contains(ProjectCodePlugin)) { OutReason = String.Format("{0} plugin is enabled", ProjectCodePlugin); return true; } } foreach (string DefaultCodePlugin in DefaultCodePlugins) { if (!ProjectCodePlugins.Contains(DefaultCodePlugin)) { OutReason = String.Format("{0} plugin is disabled", DefaultCodePlugin); return true; } } OutReason = null; return false; } /// /// NOTE: This function must mirror FPluginManager::GetCodePluginsForTarget /// static bool GetCodePluginsForTarget(ProjectDescriptor ProjectDescriptor, UnrealTargetPlatform Platform, UnrealTargetConfiguration Configuration, TargetType TargetType, HashSet CodePluginNames, Dictionary AllPlugins, out PluginReferenceDescriptor OutMissingPlugin) { bool bLoadPluginsForTargetPlatforms = (TargetType == TargetType.Editor); // Map of all enabled plugins Dictionary EnabledPlugins = new Dictionary(StringComparer.OrdinalIgnoreCase); // Keep a set of all the plugin names that have been configured. We read configuration data from different places, but only configure a plugin from the first place that it's referenced. HashSet ConfiguredPluginNames = new HashSet(StringComparer.OrdinalIgnoreCase); bool bAllowEnginePluginsEnabledByDefault = true; // Find all the plugin references in the project file if (ProjectDescriptor != null) { bAllowEnginePluginsEnabledByDefault = !ProjectDescriptor.DisableEnginePluginsByDefault; if (ProjectDescriptor.Plugins != null) { // Copy the plugin references, since we may modify the project if any plugins are missing foreach (PluginReferenceDescriptor PluginReference in ProjectDescriptor.Plugins) { if (!ConfiguredPluginNames.Contains(PluginReference.Name)) { PluginReferenceDescriptor MissingPlugin; if (!ConfigureEnabledPluginForTarget(PluginReference, ProjectDescriptor, null, Platform, Configuration, TargetType, bLoadPluginsForTargetPlatforms, AllPlugins, EnabledPlugins, out MissingPlugin)) { OutMissingPlugin = MissingPlugin; return false; } ConfiguredPluginNames.Add(PluginReference.Name); } } } } // Add the plugins which are enabled by default foreach (KeyValuePair PluginPair in AllPlugins) { if (PluginPair.Value.IsEnabledByDefault(bAllowEnginePluginsEnabledByDefault) && !ConfiguredPluginNames.Contains(PluginPair.Key)) { PluginReferenceDescriptor MissingPlugin; if (!ConfigureEnabledPluginForTarget(new PluginReferenceDescriptor(PluginPair.Key, null, true), ProjectDescriptor, null, Platform, Configuration, TargetType, bLoadPluginsForTargetPlatforms, AllPlugins, EnabledPlugins, out MissingPlugin)) { OutMissingPlugin = MissingPlugin; return false; } ConfiguredPluginNames.Add(PluginPair.Key); } } // Figure out which plugins have code bool bBuildDeveloperTools = (TargetType == TargetType.Editor || TargetType == TargetType.Program || (Configuration != UnrealTargetConfiguration.Test && Configuration != UnrealTargetConfiguration.Shipping)); bool bRequiresCookedData = (TargetType != TargetType.Editor); foreach (KeyValuePair Pair in EnabledPlugins) { if (Pair.Value.Descriptor.Modules != null) { foreach (ModuleDescriptor Module in Pair.Value.Descriptor.Modules) { if (Module.IsCompiledInConfiguration(Platform, Configuration, null, TargetType, bBuildDeveloperTools, bRequiresCookedData)) { CodePluginNames.Add(Pair.Key); break; } } } } OutMissingPlugin = null; return true; } /// /// NOTE: This function should mirror FPluginManager::ConfigureEnabledPluginForTarget /// static bool ConfigureEnabledPluginForTarget(PluginReferenceDescriptor FirstReference, ProjectDescriptor ProjectDescriptor, string TargetName, UnrealTargetPlatform Platform, UnrealTargetConfiguration Configuration, TargetType TargetType, bool bLoadPluginsForTargetPlatforms, Dictionary AllPlugins, Dictionary EnabledPlugins, out PluginReferenceDescriptor OutMissingPlugin) { if (!EnabledPlugins.ContainsKey(FirstReference.Name)) { // Set of plugin names we've added to the queue for processing HashSet NewPluginNames = new HashSet(StringComparer.OrdinalIgnoreCase); NewPluginNames.Add(FirstReference.Name); // Queue of plugin references to consider List NewPluginReferences = new List(); NewPluginReferences.Add(FirstReference); // Loop through the queue of plugin references that need to be enabled, queuing more items as we go for (int Idx = 0; Idx < NewPluginReferences.Count; Idx++) { PluginReferenceDescriptor Reference = NewPluginReferences[Idx]; // Check if the plugin is required for this platform if(!Reference.IsEnabledForPlatform(Platform) || !Reference.IsEnabledForTargetConfiguration(Configuration) || !Reference.IsEnabledForTarget(TargetType)) { Log.TraceLog("Ignoring plugin '{0}' for platform/configuration", Reference.Name); continue; } // Check if the plugin is required for this platform if(!bLoadPluginsForTargetPlatforms && !Reference.IsSupportedTargetPlatform(Platform)) { Log.TraceLog("Ignoring plugin '{0}' due to unsupported platform", Reference.Name); continue; } // Find the plugin being enabled PluginInfo Plugin; if (!AllPlugins.TryGetValue(Reference.Name, out Plugin)) { // Ignore any optional plugins if (Reference.bOptional) { Log.TraceLog("Ignored optional reference to '%s' plugin; plugin was not found.", Reference.Name); continue; } // Add it to the missing list OutMissingPlugin = Reference; return false; } // Check the plugin supports this platform if(!bLoadPluginsForTargetPlatforms && !Plugin.Descriptor.SupportsTargetPlatform(Platform)) { Log.TraceLog("Ignoring plugin '{0}' due to unsupported platform in plugin descriptor", Reference.Name); continue; } // Check that this plugin supports the current program if (TargetType == TargetType.Program && !Plugin.Descriptor.SupportedPrograms.Contains(TargetName)) { Log.TraceLog("Ignoring plugin '{0}' due to absence from the supported programs list", Reference.Name); continue; } // Add references to all its dependencies if (Plugin.Descriptor.Plugins != null) { foreach (PluginReferenceDescriptor NextReference in Plugin.Descriptor.Plugins) { if (!EnabledPlugins.ContainsKey(NextReference.Name) && !NewPluginNames.Contains(NextReference.Name)) { NewPluginNames.Add(NextReference.Name); NewPluginReferences.Add(NextReference); } } } // Add the plugin EnabledPlugins.Add(Plugin.Name, Plugin); } } OutMissingPlugin = null; return true; } private static void GenerateTempTarget(FileReference RawProjectPath) { DirectoryReference TempDir = DirectoryReference.Combine(RawProjectPath.Directory, "Intermediate", "Source"); DirectoryReference.CreateDirectory(TempDir); // Get the project name for use in temporary files string ProjectName = RawProjectPath.GetFileNameWithoutExtension(); // Create a target.cs file MemoryStream TargetStream = new MemoryStream(); using (StreamWriter Writer = new StreamWriter(TargetStream)) { Writer.WriteLine("using UnrealBuildTool;"); Writer.WriteLine(); Writer.WriteLine("public class {0}Target : TargetRules", ProjectName); Writer.WriteLine("{"); Writer.WriteLine("\tpublic {0}Target(TargetInfo Target) : base(Target)", ProjectName); Writer.WriteLine("\t{"); Writer.WriteLine("\t\tDefaultBuildSettings = BuildSettingsVersion.V2;"); Writer.WriteLine("\t\tType = TargetType.Game;"); Writer.WriteLine("\t\tExtraModuleNames.Add(\"{0}\");", ProjectName); Writer.WriteLine("\t}"); Writer.WriteLine("}"); } FileReference TargetLocation = FileReference.Combine(TempDir, ProjectName + ".Target.cs"); FileReference.WriteAllBytesIfDifferent(TargetLocation, TargetStream.ToArray()); // Create a build.cs file MemoryStream ModuleStream = new MemoryStream(); using (StreamWriter Writer = new StreamWriter(ModuleStream)) { Writer.WriteLine("using UnrealBuildTool;"); Writer.WriteLine(); Writer.WriteLine("public class {0} : ModuleRules", ProjectName); Writer.WriteLine("{"); Writer.WriteLine("\tpublic {0}(ReadOnlyTargetRules Target) : base(Target)", ProjectName); Writer.WriteLine("\t{"); Writer.WriteLine("\t\tPCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;"); Writer.WriteLine(); Writer.WriteLine("\t\tPrivateDependencyModuleNames.Add(\"Core\");"); Writer.WriteLine("\t\tPrivateDependencyModuleNames.Add(\"Core\");"); Writer.WriteLine("\t}"); Writer.WriteLine("}"); } FileReference ModuleLocation = FileReference.Combine(TempDir, ProjectName + ".Build.cs"); FileReference.WriteAllBytesIfDifferent(ModuleLocation, ModuleStream.ToArray()); // Create a main module cpp file MemoryStream SourceFileStream = new MemoryStream(); using (StreamWriter Writer = new StreamWriter(SourceFileStream)) { Writer.WriteLine("#include \"CoreTypes.h\""); Writer.WriteLine("#include \"Modules/ModuleManager.h\""); Writer.WriteLine(); Writer.WriteLine("IMPLEMENT_PRIMARY_GAME_MODULE(FDefaultModuleImpl, {0}, \"{0}\");", ProjectName); } FileReference SourceFileLocation = FileReference.Combine(TempDir, ProjectName + ".cpp"); FileReference.WriteAllBytesIfDifferent(SourceFileLocation, SourceFileStream.ToArray()); } /// /// Attempts to autodetect project properties. /// /// Full project path. /// Project properties. private static ProjectProperties DetectProjectProperties(FileReference RawProjectPath, List ClientTargetPlatforms, List ClientTargetConfigurations, bool AssetNativizationRequested) { ProjectProperties Properties = new ProjectProperties(); Properties.RawProjectPath = RawProjectPath; // detect if the project is content only, but has non-default build settings List ExtraSearchPaths = null; if (RawProjectPath != null) { // no Target file, now check to see if build settings have changed List TargetPlatforms = ClientTargetPlatforms; if (ClientTargetPlatforms == null || ClientTargetPlatforms.Count < 1) { // No client target platforms, add all in TargetPlatforms = new List(); foreach (UnrealTargetPlatform TargetPlatformType in UnrealTargetPlatform.GetValidPlatforms()) { TargetPlatforms.Add(TargetPlatformType); } } List TargetConfigurations = ClientTargetConfigurations; if (TargetConfigurations == null || TargetConfigurations.Count < 1) { // No client target configurations, add all in TargetConfigurations = new List(); foreach (UnrealTargetConfiguration TargetConfigurationType in Enum.GetValues(typeof(UnrealTargetConfiguration))) { if (TargetConfigurationType != UnrealTargetConfiguration.Unknown) { TargetConfigurations.Add(TargetConfigurationType); } } } string TempTargetDir = CommandUtils.CombinePaths(Path.GetDirectoryName(RawProjectPath.FullName), "Intermediate", "Source"); if (RequiresTempTarget(RawProjectPath, TargetPlatforms, TargetConfigurations, AssetNativizationRequested)) { GenerateTempTarget(RawProjectPath); Properties.bWasGenerated = true; ExtraSearchPaths = new List(); ExtraSearchPaths.Add(TempTargetDir); } else if (File.Exists(Path.Combine(Path.GetDirectoryName(RawProjectPath.FullName), "Intermediate", "Source", Path.GetFileNameWithoutExtension(RawProjectPath.FullName) + ".Target.cs"))) { File.Delete(Path.Combine(Path.GetDirectoryName(RawProjectPath.FullName), "Intermediate", "Source", Path.GetFileNameWithoutExtension(RawProjectPath.FullName) + ".Target.cs")); } // in case the RulesCompiler (what we use to find all the // Target.cs files) has already cached the contents of this // directory, then we need to invalidate that cache (so // it'll find/use the new Target.cs file) Rules.InvalidateRulesFileCache(TempTargetDir); } if (CommandUtils.CmdEnv.HasCapabilityToCompile) { DetectTargetsForProject(Properties, ExtraSearchPaths); Properties.bIsCodeBasedProject = !CommandUtils.IsNullOrEmpty(Properties.Targets) || !CommandUtils.IsNullOrEmpty(Properties.Programs); } else { // should never ask for engine targets if we can't compile if (RawProjectPath == null) { throw new AutomationException("Cannot determine engine targets if we can't compile."); } Properties.bIsCodeBasedProject = Properties.bWasGenerated; // if there's a Source directory with source code in it, then mark us as having source code string SourceDir = CommandUtils.CombinePaths(Path.GetDirectoryName(RawProjectPath.FullName), "Source"); if (Directory.Exists(SourceDir)) { string[] CppFiles = Directory.GetFiles(SourceDir, "*.cpp", SearchOption.AllDirectories); string[] HFiles = Directory.GetFiles(SourceDir, "*.h", SearchOption.AllDirectories); Properties.bIsCodeBasedProject |= (CppFiles.Length > 0 || HFiles.Length > 0); } } // check to see if the uproject loads modules, only if we haven't already determined it is a code based project if (!Properties.bIsCodeBasedProject && RawProjectPath != null) { string uprojectStr = File.ReadAllText(RawProjectPath.FullName); Properties.bIsCodeBasedProject = uprojectStr.Contains("\"Modules\""); } // Get all ini files if (RawProjectPath != null) { CommandUtils.LogVerbose("Loading ini files for {0}", RawProjectPath); foreach (UnrealTargetPlatform TargetPlatformType in UnrealTargetPlatform.GetValidPlatforms()) { ConfigHierarchy EngineConfig = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, RawProjectPath.Directory, TargetPlatformType); Properties.EngineConfigs.Add(TargetPlatformType, EngineConfig); ConfigHierarchy GameConfig = ConfigCache.ReadHierarchy(ConfigHierarchyType.Game, RawProjectPath.Directory, TargetPlatformType); Properties.GameConfigs.Add(TargetPlatformType, GameConfig); } } return Properties; } /// /// Gets the project's root binaries folder. /// /// Full project path. /// Target type. /// True if uproject file. /// Binaries path. public static DirectoryReference GetClientProjectBinariesRootPath(FileReference RawProjectPath, TargetType TargetType, bool bIsCodeBasedProject) { DirectoryReference BinPath = null; switch (TargetType) { case TargetType.Program: BinPath = DirectoryReference.Combine(Unreal.RootDirectory, "Engine", "Binaries"); break; case TargetType.Client: case TargetType.Game: if (!bIsCodeBasedProject) { BinPath = DirectoryReference.Combine(Unreal.RootDirectory, "Engine", "Binaries"); } else { BinPath = DirectoryReference.Combine(RawProjectPath.Directory, "Binaries"); } break; } return BinPath; } /// /// Gets the location where all rules assemblies should go /// private static string GetRulesAssemblyFolder() { string RulesFolder; if (Unreal.IsEngineInstalled()) { RulesFolder = CommandUtils.CombinePaths(Path.GetTempPath(), "UAT", CommandUtils.EscapePath(CommandUtils.CmdEnv.LocalRoot), "Rules"); } else { RulesFolder = CommandUtils.CombinePaths(CommandUtils.CmdEnv.EngineSavedFolder, "Rules"); } return RulesFolder; } /// /// Finds all targets for the project. /// /// Project properties. /// Additional search paths. private static void DetectTargetsForProject(ProjectProperties Properties, List ExtraSearchPaths = null) { Properties.Targets = new List(); FileReference TargetsDllFilename; string FullProjectPath = null; List GameFolders = new List(); DirectoryReference RulesFolder = new DirectoryReference(GetRulesAssemblyFolder()); if (Properties.RawProjectPath != null) { CommandUtils.LogVerbose("Looking for targets for project {0}", Properties.RawProjectPath); TargetsDllFilename = FileReference.Combine(RulesFolder, String.Format("UATRules-{0}.dll", ContentHash.MD5(Properties.RawProjectPath.FullName.ToUpperInvariant()).ToString())); FullProjectPath = CommandUtils.GetDirectoryName(Properties.RawProjectPath.FullName); GameFolders.Add(new DirectoryReference(FullProjectPath)); CommandUtils.LogVerbose("Searching for target rule files in {0}", FullProjectPath); } else { TargetsDllFilename = FileReference.Combine(RulesFolder, String.Format("UATRules{0}.dll", "_BaseEngine_")); } // the UBT code assumes a certain CWD, but artists don't have this CWD. string SourceDir = CommandUtils.CombinePaths(CommandUtils.CmdEnv.LocalRoot, "Engine", "Source"); bool DirPushed = false; if (CommandUtils.DirectoryExists_NoExceptions(SourceDir)) { CommandUtils.PushDir(SourceDir); DirPushed = true; } List ExtraSearchDirectories = (ExtraSearchPaths == null)? null : ExtraSearchPaths.Select(x => new DirectoryReference(x)).ToList(); List TargetScripts = Rules.FindAllRulesSourceFiles(Rules.RulesFileType.Target, GameFolders: GameFolders, ForeignPlugins: null, AdditionalSearchPaths: ExtraSearchDirectories); if (DirPushed) { CommandUtils.PopDir(); } if (!CommandUtils.IsNullOrEmpty(TargetScripts)) { // We only care about project target script so filter out any scripts not in the project folder, or take them all if we are just doing engine stuff List ProjectTargetScripts = new List(); foreach (FileReference TargetScript in TargetScripts) { if (FullProjectPath == null || TargetScript.IsUnderDirectory(new DirectoryReference(FullProjectPath))) { // skip target rules that are platform extension or platform group specializations string[] TargetPathSplit = TargetScript.GetFileNameWithoutAnyExtensions().Split(new char[]{'_'}, StringSplitOptions.RemoveEmptyEntries ); if (TargetPathSplit.Length > 1 && (UnrealTargetPlatform.IsValidName(TargetPathSplit.Last()) || UnrealPlatformGroup.IsValidName(TargetPathSplit.Last()) ) ) { continue; } ProjectTargetScripts.Add(TargetScript); } } TargetScripts = ProjectTargetScripts; } if (!CommandUtils.IsNullOrEmpty(TargetScripts)) { CommandUtils.LogVerbose("Found {0} target rule files:", TargetScripts.Count); foreach (FileReference Filename in TargetScripts) { CommandUtils.LogVerbose(" {0}", Filename); } // Check if the scripts require compilation bool DoNotCompile = false; if (!CommandUtils.IsBuildMachine && !CheckIfScriptAssemblyIsOutOfDate(TargetsDllFilename, TargetScripts)) { Log.TraceVerbose("Targets DLL {0} is up to date.", TargetsDllFilename); DoNotCompile = true; } if (!DoNotCompile && CommandUtils.FileExists_NoExceptions(TargetsDllFilename.FullName)) { if (!CommandUtils.DeleteFile_NoExceptions(TargetsDllFilename.FullName, true)) { DoNotCompile = true; CommandUtils.LogVerbose("Could not delete {0} assuming it is up to date and reusable for a recursive UAT call.", TargetsDllFilename); } } CompileAndLoadTargetsAssembly(Properties, TargetsDllFilename, DoNotCompile, TargetScripts); } } /// /// Optionally compiles and loads target rules assembly. /// /// /// /// /// private static void CompileAndLoadTargetsAssembly(ProjectProperties Properties, FileReference TargetsDllFilename, bool DoNotCompile, List TargetScripts) { Properties.TargetScripts = new List(TargetScripts); CommandUtils.LogVerbose("Compiling targets DLL: {0}", TargetsDllFilename); List ReferencedAssemblies = new List() { typeof(UnrealBuildTool.PlatformExports).Assembly.Location }; List PreprocessorDefinitions = RulesAssembly.GetPreprocessorDefinitions(); Assembly TargetsDLL = DynamicCompilation.CompileAndLoadAssembly(TargetsDllFilename, new HashSet(TargetScripts), Log.Logger, ReferencedAssemblies, PreprocessorDefinitions, DoNotCompile); Type[] AllCompiledTypes = TargetsDLL.GetTypes(); foreach (Type TargetType in AllCompiledTypes) { // Find TargetRules but skip all "UnrealEditor", "UnrealGame" targets. if (typeof(TargetRules).IsAssignableFrom(TargetType) && !TargetType.IsAbstract) { string TargetName = GetTargetName(TargetType); TargetInfo DummyTargetInfo = new TargetInfo(TargetName, BuildHostPlatform.Current.Platform, UnrealTargetConfiguration.Development, "", Properties.RawProjectPath, null); // Create an instance of this type CommandUtils.LogVerbose("Creating target rules object: {0}", TargetType.Name); TargetRules Rules = TargetRules.Create(TargetType, DummyTargetInfo, null, null, null, Log.Logger); CommandUtils.LogVerbose("Adding target: {0} ({1})", TargetType.Name, Rules.Type); SingleTargetProperties TargetData = new SingleTargetProperties(); TargetData.TargetName = GetTargetName(TargetType); TargetData.TargetClassName = TargetType.FullName; TargetData.Rules = Rules; if (Rules.Type == global::UnrealBuildTool.TargetType.Program) { Properties.Programs.Add(TargetData); } else { Properties.Targets.Add(TargetData); } } } } /// /// Checks if any of the script files in newer than the generated assembly. /// /// /// /// True if the generated assembly is out of date. private static bool CheckIfScriptAssemblyIsOutOfDate(FileReference TargetsDllFilename, List TargetScripts) { bool bOutOfDate = false; FileInfo AssemblyInfo = new FileInfo(TargetsDllFilename.FullName); if (AssemblyInfo.Exists) { foreach (FileReference ScriptFilename in TargetScripts) { FileInfo ScriptInfo = new FileInfo(ScriptFilename.FullName); if (ScriptInfo.Exists && ScriptInfo.LastWriteTimeUtc > AssemblyInfo.LastWriteTimeUtc) { bOutOfDate = true; break; } } } else { bOutOfDate = true; } return bOutOfDate; } /// /// Converts class type name (usually ends with Target) to a target name (without the postfix). /// /// Tagert class. /// Target name private static string GetTargetName(Type TargetRulesType) { const string TargetPostfix = "Target"; string Name = TargetRulesType.Name; if (Name.EndsWith(TargetPostfix, StringComparison.InvariantCultureIgnoreCase)) { Name = Name.Substring(0, Name.Length - TargetPostfix.Length); } return Name; } /// /// Performs initial cleanup of target rules folder /// public static void CleanupFolders() { CommandUtils.LogVerbose("Cleaning up project rules folder"); string RulesFolder = GetRulesAssemblyFolder(); if (CommandUtils.DirectoryExists(RulesFolder)) { CommandUtils.DeleteDirectoryContents(RulesFolder); } } /// /// Takes a game name (e.g "ShooterGame") and tries to find the path to the project file /// /// /// public static FileReference FindProjectFileFromName(string GameName) { // if they passed in a path then easy. if (File.Exists(GameName)) { return new FileReference(GameName); } // Start with the gamename regardless of what they passed in GameName = Path.GetFileNameWithoutExtension(GameName); // Turn Foo into Foo.uproject string ProjectFile = GameName; if (string.IsNullOrEmpty(Path.GetExtension(ProjectFile))) { // if project was specified but had no extension then just add it. ProjectFile = Path.ChangeExtension(GameName, ".uproject"); } // Turn Foo.uproject into Foo/Foo.uproject ProjectFile = Path.Combine(GameName, ProjectFile); GameName = Path.GetFileNameWithoutExtension(GameName); // check for sibling to engine if (File.Exists(ProjectFile)) { return new FileReference(ProjectFile); } // Search NativeProjects (sibling folders). IEnumerable Projects = NativeProjects.EnumerateProjectFiles(Log.Logger); FileReference ProjectPath = Projects.Where(R => string.Equals(R.GetFileName(), ProjectFile, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); if (ProjectPath == null) { // read .uprojectdirs List SearchPaths = new List(); SearchPaths.Add(""); string ProjectDirsFile = Directory.EnumerateFiles(Environment.CurrentDirectory, "*.uprojectdirs").FirstOrDefault(); if (ProjectDirsFile != null) { foreach (string FilePath in File.ReadAllLines(ProjectDirsFile)) { string Trimmed = FilePath.Trim(); if (!Trimmed.StartsWith("./", StringComparison.OrdinalIgnoreCase) && !Trimmed.StartsWith(";", StringComparison.OrdinalIgnoreCase) && Trimmed.IndexOfAny(Path.GetInvalidPathChars()) < 0) { SearchPaths.Add(Trimmed); } } string ResolvedFile = SearchPaths.Select(P => Path.Combine(P, ProjectFile)) .Where(P => File.Exists(P)) .FirstOrDefault(); if (ResolvedFile != null) { ProjectPath = new FileReference(ResolvedFile); } } } // either valid or we're out of ideas... return ProjectPath; } /// /// Full path to the Project executable for the current platform. /// /// Path to Project file /// Target type /// Target platform /// Target build configuration /// Do you want the console subsystem/commandlet executable? /// public static FileSystemReference GetProjectTarget(FileReference ProjectFile, UnrealBuildTool.TargetType TargetType, UnrealBuildTool.UnrealTargetPlatform TargetPlatform, UnrealBuildTool.UnrealTargetConfiguration TargetConfiguration = UnrealBuildTool.UnrealTargetConfiguration.Development, bool Cmd = false) { ProjectProperties Properties = ProjectUtils.GetProjectProperties(ProjectFile); List Targets = Properties.Targets.Where(x => x.Rules.Type == TargetType).ToList(); string TargetName = null; switch (Targets.Count) { case 0: return null; case 1: TargetName = Targets.First().TargetName; break; default: Properties.EngineConfigs[TargetPlatform].GetString("/Script/BuildSettings.BuildSettings", "DefaultEditorTarget", out TargetName); break; } FileReference TargetReceiptFileName = UnrealBuildTool.TargetReceipt.GetDefaultPath(ProjectFile.Directory, TargetName, TargetPlatform, TargetConfiguration, ""); UnrealBuildTool.TargetReceipt TargetReceipt = UnrealBuildTool.TargetReceipt.Read(TargetReceiptFileName); if (Cmd) { return TargetReceipt.LaunchCmd; } if (TargetPlatform == UnrealTargetPlatform.Mac) { // Remove trailing "/Contents/MacOS/UnrealEngine" to get back to .app directory return TargetReceipt.Launch.Directory.ParentDirectory.ParentDirectory; } return TargetReceipt.Launch; } } public class BranchInfo { [DebuggerDisplay("{GameName}")] public class BranchUProject { public string GameName; public FileReference FilePath; private ProjectProperties CachedProperties; public ProjectProperties Properties { get { if(CachedProperties == null) { CachedProperties = ProjectUtils.GetProjectProperties(FilePath); } return CachedProperties; } } public BranchUProject(FileReference ProjectFile) { GameName = ProjectFile.GetFileNameWithoutExtension(); //not sure what the heck this path is relative to FilePath = ProjectFile; if (!CommandUtils.FileExists_NoExceptions(FilePath.FullName)) { throw new AutomationException("Could not resolve relative path corrctly {0} -> {1} which doesn't exist.", ProjectFile, FilePath); } } } public List AllProjects = new List(); public BranchInfo() { IEnumerable ProjectFiles = UnrealBuildTool.NativeProjects.EnumerateProjectFiles(Log.Logger); foreach (FileReference InfoEntry in ProjectFiles) { AllProjects.Add(new BranchUProject(InfoEntry)); } CommandUtils.LogVerbose(" {0} projects:", AllProjects.Count); foreach (BranchUProject Proj in AllProjects) { CommandUtils.LogLog(" {0}: {1}", Proj.GameName, Proj.FilePath); } } public BranchUProject FindGame(string GameName) { foreach (BranchUProject Proj in AllProjects) { if (Proj.GameName.Equals(GameName, StringComparison.InvariantCultureIgnoreCase)) { return Proj; } } return null; } public BranchUProject FindGameChecked(string GameName) { BranchUProject Project = FindGame(GameName); if(Project == null) { throw new AutomationException("Cannot find project '{0}' in branch", GameName); } return Project; } } }