// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.IO; using System.Threading; using System.Reflection; using AutomationTool; using UnrealBuildTool; using System.Linq; using EpicGames.Core; using EpicGames.ProjectStore; public struct StageTarget { public TargetReceipt Receipt; public bool RequireFilesExist; } /// /// Controls which directories are searched when staging files /// public enum StageFilesSearch { /// /// Only search the top directory /// TopDirectoryOnly, /// /// Search the entire directory tree /// AllDirectories, } /// /// Contains the set of files to be staged /// public class FilesToStage { /// /// After staging, this is a map from staged file to source file. These file are content, and can go into a pak file. /// public Dictionary UFSFiles = new Dictionary(); /// /// After staging, this is a map from staged file to source file. These file are binaries, etc and can't go into a pak file. /// public Dictionary NonUFSFiles = new Dictionary(); /// /// After staging, this is a map from staged file to source file. These file are for debugging, and should not go into a pak file. /// public Dictionary NonUFSDebugFiles = new Dictionary(); /// /// After staging, this is a map from staged file to source file. These files are system files, and should not be renamed or remapped. /// public Dictionary NonUFSSystemFiles = new Dictionary(); /// /// Adds a file to be staged as the given type /// /// The type of file to be staged /// The staged file location /// The input file public void Add(StagedFileType FileType, StagedFileReference StagedFile, FileReference InputFile) { if (FileType == StagedFileType.UFS) { AddToDictionary(UFSFiles, StagedFile, InputFile); } else if (FileType == StagedFileType.NonUFS) { AddToDictionary(NonUFSFiles, StagedFile, InputFile); } else if (FileType == StagedFileType.DebugNonUFS) { AddToDictionary(NonUFSDebugFiles, StagedFile, InputFile); } else if(FileType == StagedFileType.SystemNonUFS) { AddToDictionary(NonUFSSystemFiles, StagedFile, InputFile); } } /// /// Adds a file to be staged to the given dictionary /// /// Dictionary of files to be staged /// The staged file location /// The input file private void AddToDictionary(Dictionary FilesToStage, StagedFileReference StagedFile, FileReference InputFile) { FilesToStage[StagedFile] = InputFile; } } public class ZenCookedFile { public string Filename; public IoHash ChunkId; } public class PackageStoreData { public ZenServerStoreData ZenServerStore { get; set; } public string ManifestFullPath { get; set; } public string MarkerFullPath { get; set; } public IList ZenCookedFiles { get; set; } } public class DeploymentContext //: ProjectParams { /// /// Full path to the .uproject file /// public FileReference RawProjectPath; /// /// true if we should stage crash reporter /// public bool bStageCrashReporter; /// /// CookPlatform, where to get the cooked data from and use for sandboxes /// public string CookPlatform; /// /// FinalCookPlatform, directory to stage and archive the final result to /// public string FinalCookPlatform; /// /// Source platform to get the cooked data from /// public Platform CookSourcePlatform; /// /// Target platform used for sandboxes and stage directory names /// public Platform StageTargetPlatform; /// /// Configurations to stage. Used to determine which ThirdParty configurations to copy. /// public List StageTargetConfigurations; /// /// Receipts for the build targets that should be staged. /// public List StageTargets; /// /// Extra subdirectory to load config files out of, for making multiple types of builds with the same platform /// public string CustomConfig; /// /// Allows a platform to change how it is packaged, staged and deployed - for example, when packaging for a specific game store /// public CustomDeploymentHandler CustomDeployment = null; /// /// This is the root directory that contains the engine: d:\a\UE\ /// public DirectoryReference LocalRoot; /// /// This is the directory that contains the engine. /// public DirectoryReference EngineRoot; /// /// The directory that contains the project: d:\a\UE\ShooterGame /// public DirectoryReference ProjectRoot; /// /// The directory that contains the DLC being processed (or null for non-DLC) /// public DirectoryReference DLCRoot; /// /// The list of AdditionalPluginDirectories from the project.uproject. Files in plugins in these /// directories are staged into $(StageRoot)/RemappedPlugins/PluginName. /// public List AdditionalPluginDirectories; /// /// raw name used for platform subdirectories Win32 /// public string PlatformDir; /// /// Directory to put all of the files in: d:\stagedir\Windows /// public DirectoryReference StageDirectory; /// /// Directory to put all of the debug files in: d:\stagedir\Windows /// public DirectoryReference DebugStageDirectory; /// /// Directory to put all of the optional (ie editor only) files in: d:\stagedir\Windows /// public DirectoryReference OptionalFileStageDirectory = null; /// /// Directory to read all of the optional (ie editor only) files from (written earlier with OptionalFileStageDirectory) /// public DirectoryReference OptionalFileInputDirectory = null; /// /// If this is specified, any files written into the receipt with the CookerSupportFiles tag will be copied into here during staging /// public string CookerSupportFilesSubdirectory = null; /// /// Directory name for staged projects /// public StagedDirectoryReference RelativeProjectRootForStage; /// /// This is what you use to test the engine which uproject you want. Many cases. /// public string ProjectArgForCommandLines; /// /// The directory containing the cooked data to be staged. This may be different to the target platform, eg. when creating cooked data for dedicated servers. /// public DirectoryReference CookSourceRuntimeRootDir; /// /// This is the root that we are going to run from. This will be the stage directory if we're staging, or the input directory if not. /// public DirectoryReference RuntimeRootDir; /// /// This is the project root that we are going to run from. Many cases. /// public DirectoryReference RuntimeProjectRootDir; /// /// The directory containing the metadata from the cooker /// public DirectoryReference MetadataDir; /// /// The directory containing the cooker generated data for this platform /// public DirectoryReference PlatformCookDir; /// /// List of executables we are going to stage /// public List StageExecutables; /// /// Probably going away, used to construct ProjectArgForCommandLines in the case that we are running staged /// public const string UProjectCommandLineArgInternalRoot = "../../../"; /// /// Probably going away, used to construct the pak file list /// public string PakFileInternalRoot = "../../../"; /// /// Cooked package store data if available /// public PackageStoreData PackageStoreData; /// /// List of files to be staged /// public FilesToStage FilesToStage = new FilesToStage(); /// /// Map of staged crash reporter file to source location /// public Dictionary CrashReporterUFSFiles = new Dictionary(); /// /// List of files to be archived /// public Dictionary ArchivedFiles = new Dictionary(); /// /// List of restricted folder names which are not permitted in staged build /// public HashSet RestrictedFolderNames = new HashSet(StringComparer.OrdinalIgnoreCase); /// /// List of directories to remap during staging, allowing moving files to different final paths /// This list is read from the +RemapDirectories=(From=, To=) array in the [Staging] section of *Game.ini files /// public List> RemapDirectories = new List>(); /// /// List of directories to allow staging, even if they contain restricted folder names /// This list is read from the +AllowedDirectories=... array in the [Staging] section of *Game.ini files /// public List DirectoriesAllowList = new List(); /// /// Set of config files which are allow listed to be staged. By default, we warn for config files which are not well known to prevent internal data (eg. editor/server settings) /// leaking in packaged builds. This list is read from the +AllowedConfigFiles=... array in the [Staging] section of *Game.ini files. /// public HashSet ConfigFilesAllowList = new HashSet(); /// /// Set of config files which are denied from staging. By default, we warn for config files which are not well known to prevent internal data (eg. editor/server settings) /// leaking in packaged builds. This list is read from the +DisallowedConfigFiles=... array in the [Staging] section of *Game.ini files. /// public HashSet ConfigFilesDenyList = new HashSet(); /// /// Set files which are allow listed to be staged that would otherwise be excluded by RestrictedFolderNames. /// This list is read from the +ExtraAllowedFiles=... array in the [Staging] section of *Game.ini files. /// public HashSet ExtraFilesAllowList = new HashSet(); /// /// Optional stage handler that during CopyOrWriteManifestFilesToStageDir will handle the copy operation of files and creation /// of the plugin manifest file. /// public CustomStageCopyHandler CustomStageCopyHandler = null; /// /// List of ini keys to strip when staging /// public List IniKeyDenyList = null; /// /// List of ini sections to strip when staging /// public List IniSectionDenyList = null; /// /// List of ini suffixes to always stage /// public List IniSuffixAllowList = null; /// /// List of ini suffixes to never stage /// public List IniSuffixDenyList = null; /// /// List of localization targets that are not included in staged build. By default, all project Content/Localization targets are automatically staged. /// This list is read from the +DisallowedLocalizationTargets=... array in the [Staging] section of *Game.ini files. /// public List LocalizationTargetsDenyList = new List(); /// /// Directory to archive all of the files in: d:\archivedir\Windows /// public DirectoryReference ArchiveDirectory; /// /// Directory to project binaries /// public DirectoryReference ProjectBinariesFolder; /// /// The client connects to dedicated server to get data /// public bool DedicatedServer; /// /// True if this build is staged /// public bool Stage; /// /// True if this build is archived /// public bool Archive; /// /// True if this project has code /// public bool IsCodeBasedProject; /// /// Project name (name of the uproject file without extension or directory name where the project is localed) /// public string ShortProjectName; /// /// If true, multiple platforms are being merged together - some behavior needs to change (but not much) /// public bool bIsCombiningMultiplePlatforms = false; /// /// If true if this platform is using streaming install chunk manifests /// public bool PlatformUsesChunkManifests = false; /// /// Temporary setting to exclude non cooked packages from I/O store container file(s) /// public bool OnlyAllowPackagesFromStdCookPathInIoStore = false; /// /// Allows a target to ignore PakFileRules.ini when the project rules are still needed for most cases /// public bool UsePakFileRulesIni = true; public DeploymentContext( FileReference RawProjectPathOrName, DirectoryReference InLocalRoot, DirectoryReference BaseStageDirectory, DirectoryReference OptionalFileStageDirectory, DirectoryReference OptionalFileInputDirectory, DirectoryReference BaseArchiveDirectory, string CookerSupportFilesSubdirectory, Platform InSourcePlatform, Platform InTargetPlatform, List InTargetConfigurations, IEnumerable InStageTargets, List InStageExecutables, bool InServer, bool InCooked, bool InStageCrashReporter, bool InStage, bool InCookOnTheFly, bool InArchive, bool InProgram, bool IsClientInsteadOfNoEditor, bool InForceChunkManifests, bool InSeparateDebugStageDirectory, DirectoryReference InDLCRoot, List InAdditionalPluginDirectories ) { bStageCrashReporter = InStageCrashReporter; RawProjectPath = RawProjectPathOrName; DedicatedServer = InServer; LocalRoot = InLocalRoot; CookSourcePlatform = InSourcePlatform; StageTargetPlatform = InTargetPlatform; StageTargetConfigurations = new List(InTargetConfigurations); StageTargets = new List(InStageTargets); StageExecutables = InStageExecutables; IsCodeBasedProject = ProjectUtils.IsCodeBasedUProjectFile(RawProjectPath, StageTargetPlatform.PlatformType, StageTargetConfigurations); ShortProjectName = ProjectUtils.GetShortProjectName(RawProjectPath); Stage = InStage; Archive = InArchive; DLCRoot = InDLCRoot; AdditionalPluginDirectories = InAdditionalPluginDirectories; if (CookSourcePlatform != null && InCooked) { CookPlatform = CookSourcePlatform.GetCookPlatform(DedicatedServer, IsClientInsteadOfNoEditor); } else if (CookSourcePlatform != null && InProgram) { CookPlatform = CookSourcePlatform.GetCookPlatform(false, false); } else { CookPlatform = ""; } if (StageTargetPlatform != null && InCooked) { FinalCookPlatform = StageTargetPlatform.GetCookPlatform(DedicatedServer, IsClientInsteadOfNoEditor); } else if (StageTargetPlatform != null && InProgram) { FinalCookPlatform = StageTargetPlatform.GetCookPlatform(false, false); } else { FinalCookPlatform = ""; } PlatformDir = StageTargetPlatform.PlatformType.ToString(); if (BaseStageDirectory != null) { StageDirectory = DirectoryReference.Combine(BaseStageDirectory, FinalCookPlatform); DebugStageDirectory = InSeparateDebugStageDirectory ? DirectoryReference.Combine(BaseStageDirectory, FinalCookPlatform + "Debug") : StageDirectory; } this.OptionalFileStageDirectory = OptionalFileStageDirectory; this.OptionalFileInputDirectory = OptionalFileInputDirectory; this.CookerSupportFilesSubdirectory = CookerSupportFilesSubdirectory; if (BaseArchiveDirectory != null) { // If the user specifies a path that contains the platform or cooked platform names, don't append to it. string PlatformName = StageTargetPlatform.GetStagePlatforms().FirstOrDefault().ToString(); IEnumerable PathComponents = new DirectoryInfo(BaseArchiveDirectory.FullName).FullName.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries); if (!PathComponents.Any(C => C.StartsWith(FinalCookPlatform, StringComparison.OrdinalIgnoreCase) || C.StartsWith(PlatformName, StringComparison.OrdinalIgnoreCase))) { ArchiveDirectory = DirectoryReference.Combine(BaseArchiveDirectory, FinalCookPlatform); } else { ArchiveDirectory = BaseArchiveDirectory; } } if (!FileReference.Exists(RawProjectPath)) { throw new AutomationException("Can't find uproject file {0}.", RawProjectPathOrName); } EngineRoot = DirectoryReference.Combine(LocalRoot, "Engine"); ProjectRoot = RawProjectPath.Directory; RelativeProjectRootForStage = new StagedDirectoryReference(ShortProjectName); ProjectArgForCommandLines = string.Format("-project={0}", CommandUtils.MakePathSafeToUseWithCommandLine(RawProjectPath.FullName)); CookSourceRuntimeRootDir = RuntimeRootDir = LocalRoot; RuntimeProjectRootDir = ProjectRoot; // Parse the custom config dir out of the receipts foreach (StageTarget Target in StageTargets) { var Results = Target.Receipt.AdditionalProperties.Where(x => x.Name == "CustomConfig"); foreach (var Property in Results) { string FoundCustomConfig = Property.Value; if (String.IsNullOrEmpty(FoundCustomConfig)) { continue; } else if (String.IsNullOrEmpty(CustomConfig)) { CustomConfig = FoundCustomConfig; } else if (CustomConfig != FoundCustomConfig) { throw new AutomationException("Cannot deploy targts with conflicting CustomConfig values! {0} does not match {1}", FoundCustomConfig, CustomConfig); } } } if (Stage) { CommandUtils.CreateDirectory(StageDirectory.FullName); RuntimeRootDir = StageDirectory; CookSourceRuntimeRootDir = DirectoryReference.Combine(BaseStageDirectory, CookPlatform); RuntimeProjectRootDir = DirectoryReference.Combine(StageDirectory, RelativeProjectRootForStage.Name); ProjectArgForCommandLines = string.Format("-project={0}", CommandUtils.MakePathSafeToUseWithCommandLine(UProjectCommandLineArgInternalRoot + RelativeProjectRootForStage.Name + "/" + ShortProjectName + ".uproject")); } if (Archive) { CommandUtils.CreateDirectory(ArchiveDirectory.FullName); } ProjectArgForCommandLines = ProjectArgForCommandLines.Replace("\\", "/"); ProjectBinariesFolder = DirectoryReference.Combine(ProjectUtils.GetClientProjectBinariesRootPath(RawProjectPath, TargetType.Game, IsCodeBasedProject), PlatformDir); // Build a list of restricted folder names. This will comprise all other restricted platforms, plus standard restricted folder names such as NoRedist, NotForLicensees, etc... RestrictedFolderNames.UnionWith(PlatformExports.GetPlatformFolderNames()); RestrictedFolderNames.UnionWith(RestrictedFolder.GetNames()); foreach (UnrealTargetPlatform StagePlatform in StageTargetPlatform.GetStagePlatforms()) { RestrictedFolderNames.ExceptWith(PlatformExports.GetIncludedFolderNames(StagePlatform)); } RestrictedFolderNames.Remove(StageTargetPlatform.IniPlatformType.ToString()); // Read the game config files ConfigHierarchy GameConfig = ConfigCache.ReadHierarchy(ConfigHierarchyType.Game, ProjectRoot, InTargetPlatform.PlatformType, CustomConfig); // Read the list of directories to remap when staging List RemapDirectoriesList; if (GameConfig.GetArray("Staging", "RemapDirectories", out RemapDirectoriesList)) { foreach (string RemapDirectory in RemapDirectoriesList) { Dictionary Properties; if (!ConfigHierarchy.TryParse(RemapDirectory, out Properties)) { throw new AutomationException("Unable to parse '{0}'", RemapDirectory); } string FromDir; if (!Properties.TryGetValue("From", out FromDir)) { throw new AutomationException("Missing 'From' property in '{0}'", RemapDirectory); } string ToDir; if (!Properties.TryGetValue("To", out ToDir)) { throw new AutomationException("Missing 'To' property in '{0}'", RemapDirectory); } RemapDirectories.Add(Tuple.Create(new StagedDirectoryReference(FromDir), new StagedDirectoryReference(ToDir))); } } // Read the list of directories to allow (prevent from warning about from restricted folders) List DirectoriesAllowListStrings; if (GameConfig.GetArray("Staging", "AllowedDirectories", out DirectoriesAllowListStrings)) { foreach (string AllowedDir in DirectoriesAllowListStrings) { DirectoriesAllowList.Add(new StagedDirectoryReference(AllowedDir)); } } List LocTargetsDenyListStrings; if (GameConfig.GetArray("Staging", "DisallowedLocalizationTargets", out LocTargetsDenyListStrings)) { foreach (string DeniedLocTarget in LocTargetsDenyListStrings) { LocalizationTargetsDenyList.Add(DeniedLocTarget); } } // Read the list of files which are allow listed to be staged ReadAllowDenyFileList(GameConfig, "Staging", "AllowedConfigFiles", ConfigFilesAllowList); ReadAllowDenyFileList(GameConfig, "Staging", "DisallowedConfigFiles", ConfigFilesDenyList); ReadAllowDenyFileList(GameConfig, "Staging", "ExtraAllowedFiles", ExtraFilesAllowList); // Grab the game ini data String PackagingIniPath = "/Script/UnrealEd.ProjectPackagingSettings"; // Read the config deny lists GameConfig.GetArray(PackagingIniPath, "IniKeyDenylist", out IniKeyDenyList); GameConfig.GetArray(PackagingIniPath, "IniSectionDenylist", out IniSectionDenyList); // TODO: Drive these lists from a config file IniSuffixAllowList = new List { ".ini", "compat.ini", "deviceprofiles.ini", "engine.ini", "enginechunkoverrides.ini", "game.ini", "gameplaytags.ini", "gameusersettings.ini", "hardware.ini", "input.ini", "materialexpressions.ini", "scalability.ini", "runtimeoptions.ini", "installbundle.ini" }; IniSuffixDenyList = new List { "crypto.ini", "editor.ini", "editorgameagnostic.ini", "editorkeybindings.ini", "editorlayout.ini", "editorperprojectusersettings.ini", "editorsettings.ini", "editorusersettings.ini", "lightmass.ini", "pakfilerules.ini", "sourcecontrolsettings.ini" }; // If we were configured to use manifests across the whole project, then this platform should use manifests. // Otherwise, read whether we are generating chunks from the ProjectPackagingSettings ini. if (InForceChunkManifests) { PlatformUsesChunkManifests = true; } else if (DLCRoot != null) { PlatformUsesChunkManifests = false; } else { bool bSetting = false; if (GameConfig.GetBool(PackagingIniPath, "bGenerateChunks", out bSetting)) { PlatformUsesChunkManifests = bSetting; } } } /// /// Read a list of allowed or denied files names from a config file /// /// The config hierarchy to read from /// The section name /// The key name to read from /// Receives a list of file paths private static void ReadAllowDenyFileList(ConfigHierarchy Config, string SectionName, string KeyName, HashSet FilesRef) { List FileNames; if (Config.GetArray(SectionName, KeyName, out FileNames)) { foreach (string FileName in FileNames) { FilesRef.Add(new StagedFileReference(FileName)); } } } /// /// Finds files to stage under a given base directory. /// /// The directory to search under /// Options for the search /// List of files to be staged public List FindFilesToStage(DirectoryReference BaseDir, StageFilesSearch Option) { return FindFilesToStage(BaseDir, "*", Option); } /// /// Finds files to stage under a given base directory. /// /// The directory to search under /// Pattern for files to match /// Options for the search /// List of files to be staged public List FindFilesToStage(DirectoryReference BaseDir, string Pattern, StageFilesSearch Option) { List Files = new List(); FindFilesToStageInternal(BaseDir, Pattern, Option, Files); return Files; } /// /// Finds files to stage under a given base directory. /// /// The directory to search under /// Pattern for files to match /// Options for the search /// List to receive the enumerated files private void FindFilesToStageInternal(DirectoryReference BaseDir, string Pattern, StageFilesSearch Option, List Files) { // if the directory doesn't exist, this will crash in EnumerateFiles if (!DirectoryReference.Exists(BaseDir)) { return; } // Enumerate all the files in this directory Files.AddRange(DirectoryReference.EnumerateFiles(BaseDir, Pattern)); // Recurse through subdirectories if necessary if (Option == StageFilesSearch.AllDirectories) { foreach (DirectoryReference SubDir in DirectoryReference.EnumerateDirectories(BaseDir)) { string Name = SubDir.GetDirectoryName(); if (!RestrictedFolderNames.Contains(Name)) { FindFilesToStageInternal(SubDir, Pattern, Option, Files); } } } } /// /// Gets the default location to stage an input file /// /// Location of the file in the file system /// Staged file location public StagedFileReference GetStagedFileLocation(FileReference InputFile) { StagedFileReference OutputFile; foreach (DirectoryReference AdditionalPluginDir in AdditionalPluginDirectories) { if (InputFile.IsUnderDirectory(AdditionalPluginDir)) { // This is a plugin that lives outside of the Engine/Plugins or Game/Plugins directory so needs to be remapped for staging/packaging // We need to remap C:\SomePath\PluginName\RelativePath to RemappedPlugins\PluginName\RelativePath OutputFile = new StagedFileReference( String.Format("RemappedPlugins/{0}", InputFile.MakeRelativeTo(AdditionalPluginDir))); return OutputFile; } } if (InputFile.IsUnderDirectory(ProjectRoot)) { OutputFile = StagedFileReference.Combine(RelativeProjectRootForStage, InputFile.MakeRelativeTo(ProjectRoot)); } else if (InputFile.IsUnderDirectory(LocalRoot)) { OutputFile = new StagedFileReference(InputFile.MakeRelativeTo(LocalRoot)); } else if (DLCRoot != null && InputFile.IsUnderDirectory(DLCRoot)) { OutputFile = new StagedFileReference(InputFile.MakeRelativeTo(DLCRoot)); } else { throw new AutomationException("Can't deploy {0} because it doesn't start with {1} or {2}", InputFile, ProjectRoot, LocalRoot); } return OutputFile; } /// /// Stage a single file to its default location /// /// The type of file being staged /// Path to the file public void StageFile(StagedFileType FileType, FileReference InputFile) { StagedFileReference OutputFile = GetStagedFileLocation(InputFile); StageFile(FileType, InputFile, OutputFile); } /// /// Stage a single file /// /// The type for the staged file /// The source file /// The staged file location public void StageFile(StagedFileType FileType, FileReference InputFile, StagedFileReference OutputFile) { FilesToStage.Add(FileType, OutputFile, InputFile); } /// /// Stage multiple files /// /// The type for the staged files /// The files to stage public void StageFiles(StagedFileType FileType, IEnumerable Files) { foreach (FileReference File in Files) { StageFile(FileType, File); } } /// /// Stage multiple files /// /// The type for the staged files /// /// The files to stage /// public void StageFiles(StagedFileType FileType, DirectoryReference InputDir, IEnumerable Files, StagedDirectoryReference OutputDir) { foreach (FileReference File in Files) { StagedFileReference OutputFile = StagedFileReference.Combine(OutputDir, File.MakeRelativeTo(InputDir)); StageFile(FileType, File, OutputFile); } } /// /// Stage multiple files /// /// The type for the staged files /// Input directory /// Whether to stage all subdirectories or just the top-level directory public void StageFiles(StagedFileType FileType, DirectoryReference InputDir, StageFilesSearch Option) { StageFiles(FileType, InputDir, "*", Option); } /// /// Stage multiple files /// /// The type for the staged files /// Input directory /// Whether to stage all subdirectories or just the top-level directory /// Base directory for output files public void StageFiles(StagedFileType FileType, DirectoryReference InputDir, StageFilesSearch Option, StagedDirectoryReference OutputDir) { StageFiles(FileType, InputDir, "*", Option, OutputDir); } /// /// Stage multiple files /// /// The type for the staged files /// Input directory /// /// public void StageFiles(StagedFileType FileType, DirectoryReference InputDir, string Pattern, StageFilesSearch Option) { List InputFiles = FindFilesToStage(InputDir, Pattern, Option); foreach (FileReference InputFile in InputFiles) { StageFile(FileType, InputFile); } } /// /// Stage multiple files /// /// The type for the staged files /// Input directory /// /// /// Output directory public void StageFiles(StagedFileType FileType, DirectoryReference InputDir, string Pattern, StageFilesSearch Option, StagedDirectoryReference OutputDir) { List InputFiles = FindFilesToStage(InputDir, Pattern, Option); foreach (FileReference InputFile in InputFiles) { StagedFileReference OutputFile = StagedFileReference.Combine(OutputDir, InputFile.MakeRelativeTo(InputDir)); StageFile(FileType, InputFile, OutputFile); } } /// /// Stages a file for use by crash reporter. /// /// The type of the staged file /// Location of the input file /// Location of the file in the staging directory public void StageCrashReporterFile(StagedFileType FileType, FileReference InputFile, StagedFileReference StagedFile) { if (FileType == StagedFileType.UFS) { CrashReporterUFSFiles[StagedFile] = InputFile; } else { StageFile(FileType, InputFile, StagedFile); } } /// /// Stage multiple files for use by crash reporter /// /// The type of the staged file /// Location of the input directory /// Whether to stage all subdirectories or just the top-level directory public void StageCrashReporterFiles(StagedFileType FileType, DirectoryReference InputDir, StageFilesSearch Option) { StageCrashReporterFiles(FileType, InputDir, Option, new StagedDirectoryReference(InputDir.MakeRelativeTo(LocalRoot))); } /// /// Stage multiple files for use by crash reporter /// /// The type of the staged file /// Location of the input directory /// Whether to stage all subdirectories or just the top-level directory /// Location of the output directory within the staging folder public void StageCrashReporterFiles(StagedFileType FileType, DirectoryReference InputDir, StageFilesSearch Option, StagedDirectoryReference OutputDir) { List InputFiles = FindFilesToStage(InputDir, Option); foreach (FileReference InputFile in InputFiles) { StagedFileReference StagedFile = StagedFileReference.Combine(OutputDir, InputFile.MakeRelativeTo(InputDir)); StageCrashReporterFile(FileType, InputFile, StagedFile); } } public void StageVulkanValidationLayerFiles(ProjectParams Params, StagedFileType FileType, DirectoryReference InputDir, StageFilesSearch Option) { StageVulkanValidationLayerFiles(Params, FileType, InputDir, Option, new StagedDirectoryReference(InputDir.MakeRelativeTo(LocalRoot))); } public void StageVulkanValidationLayerFiles(ProjectParams Params, StagedFileType FileType, DirectoryReference InputDir, StageFilesSearch Option, StagedDirectoryReference OutputDir) { // This needs to match the c++ define VULKAN_HAS_DEBUGGING_ENABLED to avoid mismatched functionality/files bool bShouldStageVulkanLayers = !Params.IsProgramTarget && (StageTargetConfigurations.Contains(UnrealTargetConfiguration.Debug) || StageTargetConfigurations.Contains(UnrealTargetConfiguration.Development)); if (bShouldStageVulkanLayers) { List InputFiles = FindFilesToStage(InputDir, Option); foreach (FileReference InputFile in InputFiles) { StagedFileReference StagedFile = StagedFileReference.Combine(OutputDir, InputFile.MakeRelativeTo(InputDir)); StageFile(FileType, InputFile, StagedFile); } } } public void StageBuildProductsFromReceipt(TargetReceipt Receipt, bool RequireDependenciesToExist, bool TreatNonShippingBinariesAsDebugFiles) { // Stage all the build products needed at runtime foreach (BuildProduct BuildProduct in Receipt.BuildProducts) { // allow missing files if needed if (RequireDependenciesToExist == false && FileReference.Exists(BuildProduct.Path) == false) { continue; } if (BuildProduct.Type == BuildProductType.Executable || BuildProduct.Type == BuildProductType.DynamicLibrary || BuildProduct.Type == BuildProductType.RequiredResource) { StagedFileType FileTypeToUse = StagedFileType.NonUFS; if (TreatNonShippingBinariesAsDebugFiles && Receipt.Configuration != UnrealTargetConfiguration.Shipping) { FileTypeToUse = StagedFileType.DebugNonUFS; } StageFile(FileTypeToUse, BuildProduct.Path); } else if (BuildProduct.Type == BuildProductType.SymbolFile || BuildProduct.Type == BuildProductType.MapFile) { // Symbol files aren't true dependencies so we can skip if they don't exist if (FileReference.Exists(BuildProduct.Path)) { StageFile(StagedFileType.DebugNonUFS, BuildProduct.Path); } } } } public void StageRuntimeDependenciesFromReceipt(TargetReceipt Receipt, bool RequireDependenciesToExist, bool bUsingPakFile) { // Patterns to exclude from wildcard searches. Any maps and assets must be cooked. List ExcludePatterns = new List(); ExcludePatterns.Add(".../*.umap"); ExcludePatterns.Add(".../*.uasset"); // Also stage any additional runtime dependencies, like ThirdParty DLLs foreach (RuntimeDependency RuntimeDependency in Receipt.RuntimeDependencies) { // allow missing files if needed if ((RequireDependenciesToExist && RuntimeDependency.Type != StagedFileType.DebugNonUFS) || FileReference.Exists(RuntimeDependency.Path)) { StageFile(RuntimeDependency.Type, RuntimeDependency.Path); } } } public int ArchiveFiles(string InPath, string Wildcard = "*", bool bRecursive = true, string[] ExcludeWildcard = null, string NewPath = null, UnrealTargetPlatform[] AdditionalPlatforms = null) { int FilesAdded = 0; if (CommandUtils.DirectoryExists(InPath)) { List All = new(); CommandUtils.FindFilesAndSymlinks(InPath, Wildcard, bRecursive, All); var Exclude = new HashSet(); if (ExcludeWildcard != null) { foreach (var Excl in ExcludeWildcard) { var Remove = CommandUtils.FindFiles(Excl, bRecursive, InPath); foreach (var File in Remove) { Exclude.Add(CommandUtils.CombinePaths(File)); } } } foreach (var AllFile in All) { var FileToCopy = CommandUtils.CombinePaths(AllFile); if (Exclude.Contains(FileToCopy)) { continue; } if (!bIsCombiningMultiplePlatforms) { FileReference InputFile = new FileReference(FileToCopy); bool OtherPlatform = false; foreach (UnrealTargetPlatform Plat in UnrealTargetPlatform.GetValidPlatforms()) { if (Plat != StageTargetPlatform.PlatformType) { if (AdditionalPlatforms != null && AdditionalPlatforms.Contains(Plat)) { break; } var Search = FileToCopy; if (InputFile.IsUnderDirectory(LocalRoot)) { Search = InputFile.MakeRelativeTo(LocalRoot); } else if (InputFile.IsUnderDirectory(ProjectRoot)) { Search = InputFile.MakeRelativeTo(ProjectRoot); } if (Search.IndexOf(CommandUtils.CombinePaths("/" + Plat.ToString() + "/"), 0, StringComparison.InvariantCultureIgnoreCase) >= 0) { OtherPlatform = true; break; } } } if (OtherPlatform) { continue; } } string Dest; if (!FileToCopy.StartsWith(InPath)) { throw new AutomationException("Can't archive {0}; it was supposed to start with {1}", FileToCopy, InPath); } // If the specified a new directory, first we deal with that, then apply the other things // this is used to collapse the sandbox, among other things if (NewPath != null) { Dest = FileToCopy.Substring(InPath.Length); if (Dest.StartsWith("/") || Dest.StartsWith("\\")) { Dest = Dest.Substring(1); } Dest = CommandUtils.CombinePaths(NewPath, Dest); } else { Dest = FileToCopy.Substring(InPath.Length); } if (Dest.StartsWith("/") || Dest.StartsWith("\\")) { Dest = Dest.Substring(1); } if (ArchivedFiles.ContainsKey(FileToCopy)) { if (ArchivedFiles[FileToCopy] != Dest) { throw new AutomationException("Can't archive {0}: it was already in the files to archive with a different destination '{1}'", FileToCopy, Dest); } } else { ArchivedFiles.Add(FileToCopy, Dest); } FilesAdded++; } } return FilesAdded; } private static string GetSanitizedDeviceNameSuffix(string DeviceName) { if (string.IsNullOrWhiteSpace(DeviceName)) return string.Empty; return "_" + DeviceName .Replace(":", "") .Replace("/", "") .Replace("\\", "") .Replace("-", "") .Replace(".exe", ""); } public string GetUFSDeploymentDeltaPath(string DeviceName) { return Path.Combine(StageDirectory.FullName, string.Format("Manifest_DeltaUFSFiles{0}.txt", GetSanitizedDeviceNameSuffix(DeviceName))); } public string GetNonUFSDeploymentDeltaPath(string DeviceName) { return Path.Combine(StageDirectory.FullName, string.Format("Manifest_DeltaNonUFSFiles{0}.txt", GetSanitizedDeviceNameSuffix(DeviceName))); } public string GetUFSDeploymentObsoletePath(string DeviceName) { return Path.Combine(StageDirectory.FullName, string.Format("Manifest_ObsoleteUFSFiles{0}.txt", GetSanitizedDeviceNameSuffix(DeviceName))); } public string GetNonUFSDeploymentObsoletePath(string DeviceName) { return Path.Combine(StageDirectory.FullName, string.Format("Manifest_ObsoleteNonUFSFiles{0}.txt", GetSanitizedDeviceNameSuffix(DeviceName))); } public string GetNonUFSDeployedManifestFileName(string DeviceName) { return string.Format("Manifest_NonUFSFiles_{0}{1}.txt", StageTargetPlatform.PlatformType, GetSanitizedDeviceNameSuffix(DeviceName)); } public string GetUFSDeployedManifestFileName(string DeviceName) { return string.Format("Manifest_UFSFiles_{0}{1}.txt", StageTargetPlatform.PlatformType, GetSanitizedDeviceNameSuffix(DeviceName)); } public static StagedFileReference ApplyDirectoryRemap(DeploymentContext SC, StagedFileReference InputFile) { StagedFileReference CurrentFile = InputFile; foreach (Tuple RemapDirectory in SC.RemapDirectories) { StagedFileReference NewFile; if (StagedFileReference.TryRemap(CurrentFile, RemapDirectory.Item1, RemapDirectory.Item2, out NewFile)) { CurrentFile = NewFile; } } return CurrentFile; } public static StagedFileReference MakeRelativeStagedReference(DeploymentContext SC, FileSystemReference Ref) { return MakeRelativeStagedReference(SC, Ref, out _); } public static StagedFileReference MakeRelativeStagedReference(DeploymentContext SC, FileSystemReference Ref, out DirectoryReference RootDir) { foreach (DirectoryReference AdditionalPluginDir in SC.AdditionalPluginDirectories) { if (Ref.IsUnderDirectory(AdditionalPluginDir)) { // This is a plugin that lives outside of the Engine/Plugins or Game/Plugins directory so needs to be remapped for staging/packaging // We need to remap C:\SomePath\PluginName\RelativePath to RemappedPlugins\PluginName\RelativePath string RemainingPath = Ref.MakeRelativeTo(AdditionalPluginDir).Replace('\\', '/'); int PluginEndIndex = RemainingPath.IndexOf("/"); if (PluginEndIndex >= 0 && PluginEndIndex < RemainingPath.Length - 1) { string PluginName = RemainingPath.Substring(0, PluginEndIndex); RemainingPath = RemainingPath.Substring(PluginEndIndex + 1); RootDir = DirectoryReference.Combine(AdditionalPluginDir, PluginName); StagedFileReference StagedFile = new StagedFileReference(String.Format("RemappedPlugins/{0}/{1}", PluginName, RemainingPath)); return ApplyDirectoryRemap(SC, StagedFile); } } } if (Ref.IsUnderDirectory(SC.ProjectRoot)) { RootDir = SC.ProjectRoot; return ApplyDirectoryRemap(SC, new StagedFileReference(SC.ShortProjectName + "/" + Ref.MakeRelativeTo(SC.ProjectRoot).Replace('\\', '/'))); } if (Ref.IsUnderDirectory(SC.EngineRoot)) { RootDir = SC.EngineRoot; return ApplyDirectoryRemap(SC, new StagedFileReference("Engine/" + Ref.MakeRelativeTo(SC.EngineRoot).Replace('\\', '/'))); } throw new Exception(); } public static FileReference UnmakeRelativeStagedReference(DeploymentContext SC, StagedFileReference Ref) { // paths will be in the form "Engine/Foo" or "{ProjectName}/Foo" or "RemappedPlugins/{PluginName}/Foo // Anything else we don't handle. // So, replace the Engine/ with {EngineDir} and {ProjectName}/ with {ProjectDir}, or change PluginDir to RemappedPlugins/{PluginName} // with the plugin path from AdditionalPluginDirectories, and then append Foo string RemappedPluginsStr = "RemappedPlugins/"; if (Ref.Name.StartsWith(RemappedPluginsStr, StringComparison.CurrentCultureIgnoreCase)) { int PluginEndIndex = Ref.Name.IndexOf("/", RemappedPluginsStr.Length); if (PluginEndIndex >= 0 && PluginEndIndex < Ref.Name.Length - 1) { string PluginName = Ref.Name.Substring(RemappedPluginsStr.Length, PluginEndIndex - RemappedPluginsStr.Length); foreach (DirectoryReference AdditionalPluginDir in SC.AdditionalPluginDirectories) { DirectoryReference PossiblePluginDir = DirectoryReference.Combine(AdditionalPluginDir, PluginName); if (System.IO.Directory.Exists(PossiblePluginDir.FullName)) { return FileReference.Combine(PossiblePluginDir, Ref.Name.Substring(PluginEndIndex + 1)); } } } } if (Ref.Name.StartsWith("Engine/", StringComparison.CurrentCultureIgnoreCase)) { // skip over "Engine/" which is 7 chars long return FileReference.Combine(SC.EngineRoot, Ref.Name.Substring(7)); } if (Ref.Name.StartsWith(SC.ShortProjectName + "/", StringComparison.CurrentCultureIgnoreCase)) { return FileReference.Combine(SC.ProjectRoot, Ref.Name.Substring(SC.ShortProjectName.Length + 1)); } throw new Exception($"Don't know how to convert staged file {Ref.Name} to its original editor path, because it is not in a recognized root directory."); } }