// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using System.Text.RegularExpressions; using AutomationTool; using UnrealBuildTool; using Microsoft.Win32; using System.Diagnostics; using EpicGames.Core; using UnrealBuildBase; using System.Runtime.Versioning; using System.Threading.Tasks; using System.Reflection.PortableExecutable; using Microsoft.Extensions.Logging; using static AutomationTool.CommandUtils; using System.Xml.Linq; public class Win64Platform : Platform { public Win64Platform() : base(UnrealTargetPlatform.Win64) { } protected Win64Platform(UnrealTargetPlatform PlatformType) : base(PlatformType) { } public override DeviceInfo[] GetDevices() { List Devices = new List(); if (HostPlatform.Current.HostEditorPlatform == UnrealTargetPlatform.Win64) { DeviceInfo LocalMachine = new DeviceInfo(UnrealTargetPlatform.Win64, Unreal.MachineName, Unreal.MachineName, Environment.OSVersion.Version.ToString(), "Computer", true, true); Devices.Add(LocalMachine); Devices.AddRange(SteamDeckSupport.GetDevices(UnrealTargetPlatform.Win64)); } return Devices.ToArray(); } public override void Deploy(ProjectParams Params, DeploymentContext SC) { // We only care about deploying for SteamDeck if (Params.Devices.Count == 1 && GetDevices().FirstOrDefault(x => x.Id == Params.DeviceNames[0])?.Type == "SteamDeck") { SteamDeckSupport.Deploy(UnrealTargetPlatform.Win64, Params, SC); } } public override IProcessResult RunClient(ERunOptions ClientRunFlags, string ClientApp, string ClientCmdLine, ProjectParams Params) { if (Params.Devices.Count == 1 && GetDevices().FirstOrDefault(x => x.Id == Params.DeviceNames[0])?.Type == "SteamDeck") { return SteamDeckSupport.RunClient(UnrealTargetPlatform.Win64, ClientRunFlags, ClientApp, ClientCmdLine, Params); } return base.RunClient(ClientRunFlags, ClientApp, ClientCmdLine, Params); } protected override string GetPlatformExeExtension() { return ".exe"; } public override bool IsSupported { get { return true; } } public virtual UnrealTargetPlatform? BootstrapExePlatform { get { return null; } } public override void GetFilesToDeployOrStage(ProjectParams Params, DeploymentContext SC) { // Engine non-ufs (binaries) if (SC.bStageCrashReporter) { FileReference ReceiptFileName = TargetReceipt.GetDefaultPath(Unreal.EngineDirectory, "CrashReportClient", CrashReportPlatform ?? SC.StageTargetPlatform.PlatformType, UnrealTargetConfiguration.Shipping, null); if(FileReference.Exists(ReceiptFileName)) { TargetReceipt Receipt = TargetReceipt.Read(ReceiptFileName); SC.StageBuildProductsFromReceipt(Receipt, true, false); } } // Stage all the build products foreach(StageTarget Target in SC.StageTargets) { SC.StageBuildProductsFromReceipt(Target.Receipt, Target.RequireFilesExist, Params.bTreatNonShippingBinariesAsDebugFiles); } // Copy the splash screen, windows specific FileReference SplashImage = FileReference.Combine(SC.ProjectRoot, "Content", "Splash", "Splash.bmp"); if(FileReference.Exists(SplashImage)) { SC.StageFile(StagedFileType.NonUFS, SplashImage); } // Stage cloud metadata DirectoryReference ProjectCloudPath = DirectoryReference.Combine(SC.ProjectRoot, "Platforms/Windows/Build/Cloud"); if (DirectoryReference.Exists(ProjectCloudPath)) { SC.StageFiles(StagedFileType.SystemNonUFS, ProjectCloudPath, StageFilesSearch.AllDirectories, new StagedDirectoryReference("Cloud")); } else { Logger.LogDebug("Can't find cloud directory {Arg0}", ProjectCloudPath.FullName); } // Stage the bootstrap executable if (!Params.NoBootstrapExe) { // one bootstrap per configuration, per target foreach (UnrealTargetConfiguration Configuration in SC.StageTargetConfigurations) { foreach (string TargetName in SC.StageTargets.Select( T => T.Receipt.TargetName ).Distinct()) { Dictionary RemappedStagedFiles = []; BuildProduct PrimaryExecutable = null; StageTarget? PrimaryTarget = null; // collected the staged executable files for each staged architecture foreach (StageTarget Target in SC.StageTargets.Where( T => T.Receipt.Configuration == Configuration && T.Receipt.TargetName == TargetName)) { BuildProduct Executable = Target.Receipt.BuildProducts.FirstOrDefault(x => x.Type == BuildProductType.Executable); if (Executable != null) { // only create bootstraps for executables List StagedFiles = SC.FilesToStage.NonUFSFiles.Where(x => x.Value == Executable.Path).Select(x => x.Key).ToList(); if (StagedFiles.Count > 0 && Executable.Path.HasExtension(".exe")) { UnrealArch Architecture = Target.Receipt.Architectures.SingleArchitecture; RemappedStagedFiles[Architecture] = DeploymentContext.ApplyDirectoryRemap(SC, StagedFiles.First()); if (PrimaryExecutable == null || Architecture.bIsX64) // prefer x64 for the BootstrapExeName because it is undecorated { PrimaryExecutable = Executable; PrimaryTarget = Target; } } } } if (RemappedStagedFiles.Any()) { string BootstrapArguments = ""; if (!ShouldStageCommandLine(Params, SC)) { if (!SC.IsCodeBasedProject) { BootstrapArguments = String.Format("..\\..\\..\\{0}\\{0}.uproject", SC.ShortProjectName); } else { BootstrapArguments = SC.ShortProjectName; } } string BootstrapExeName; if(SC.StageTargetConfigurations.Count > 1) { BootstrapExeName = PrimaryExecutable.Path.GetFileName(); } else if(Params.IsCodeBasedProject) { BootstrapExeName = PrimaryTarget.Value.Receipt.TargetName + ".exe"; } else { BootstrapExeName = SC.ShortProjectName + ".exe"; } if (RemappedStagedFiles.Count == 1) { StageBootstrapExecutable(SC, BootstrapExeName, PrimaryExecutable.Path, RemappedStagedFiles.Values.First(), BootstrapArguments); } else { StageBootstrapExecutable(SC, BootstrapExeName, PrimaryExecutable.Path, null, BootstrapArguments, RemappedStagedFiles); } } } } } if (Params.Prereqs) { //SC.StageFile(StagedFileType.NonUFS, FileReference.Combine(SC.EngineRoot, "Extras", "Redist", "en-us", "UEPrereqSetup_x64.exe")); SC.StageFile(StagedFileType.NonUFS, FileReference.Combine(SC.EngineRoot, "Extras", "Redist", "en-us", "vc_redist.x64.exe")); SC.StageFile(StagedFileType.NonUFS, FileReference.Combine(SC.EngineRoot, "Extras", "Redist", "en-us", "vc_redist.arm64.exe")); // always staging arm64 redist in case we are run on an arm64 device in emulation mode } if (!string.IsNullOrWhiteSpace(Params.AppLocalDirectory)) { foreach (UnrealArch Architecture in SC.StageTargets.Select( T => T.Receipt.Architectures.SingleArchitecture ).Distinct() ) { StageAppLocalDependencies(Params, SC, "Win64", Architecture); } } } public override void ExtractPackage(ProjectParams Params, string SourcePath, string DestinationPath) { } public override void GetTargetFile(string RemoteFilePath, string LocalFile, ProjectParams Params) { var SourceFile = FileReference.Combine(new DirectoryReference(Params.BaseStageDirectory), GetCookPlatform(Params.HasServerCookedTargets, Params.HasClientTargetDetected), RemoteFilePath); CommandUtils.CopyFile(SourceFile.FullName, LocalFile); } void StageBootstrapExecutable(DeploymentContext SC, string ExeName, FileReference TargetFile, StagedFileReference StagedRelativeTargetPath, string StagedArguments, Dictionary PerArchitectureStagedRelativeTargetPath = null) { UnrealTargetPlatform BootstrapPlatform = (BootstrapExePlatform ?? SC.StageTargetPlatform.PlatformType); FileReference InputFile = FileReference.Combine(SC.LocalRoot, "Engine", "Binaries", BootstrapPlatform.ToString(), String.Format("BootstrapPackagedGame-{0}-Shipping.exe", BootstrapPlatform)); if(FileReference.Exists(InputFile)) { // Create the new bootstrap program DirectoryReference IntermediateDir = DirectoryReference.Combine(SC.ProjectRoot, "Intermediate", "Staging"); DirectoryReference.CreateDirectory(IntermediateDir); FileReference IntermediateFile = FileReference.Combine(IntermediateDir, ExeName); CommandUtils.CopyFile(InputFile.FullName, IntermediateFile.FullName); CommandUtils.SetFileAttributes(IntermediateFile.FullName, ReadOnly: false); if (UnrealBuildTool.BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Win64) { Logger.LogInformation("Patching bootstrap executable; {Arg0}", IntermediateFile.FullName); // Get the icon from the build directory if possible GroupIconResource GroupIcon = null; if(FileReference.Exists(FileReference.Combine(SC.ProjectRoot, "Build/Windows/Application.ico"))) { GroupIcon = GroupIconResource.FromIco(FileReference.Combine(SC.ProjectRoot, "Build/Windows/Application.ico").FullName); } if(GroupIcon == null) { GroupIcon = GroupIconResource.FromExe(TargetFile.FullName); } // Update the resources in the new file using(ModuleResourceUpdate Update = new ModuleResourceUpdate(IntermediateFile.FullName, false)) { const int IconResourceId = 101; if(GroupIcon != null) Update.SetIcons(IconResourceId, GroupIcon); if (PerArchitectureStagedRelativeTargetPath == null) { const int ExecFileResourceId = 201; Update.SetData(ExecFileResourceId, ResourceType.RawData, Encoding.Unicode.GetBytes(StagedRelativeTargetPath.ToString().Replace('/', '\\') + "\0")); } else { foreach (KeyValuePair ExtraTargetPath in PerArchitectureStagedRelativeTargetPath) { if (ExtraTargetPath.Key == UnrealArch.X64) { const int ExecFileResourceId_X64 = 201; Update.SetData(ExecFileResourceId_X64, ResourceType.RawData, Encoding.Unicode.GetBytes(ExtraTargetPath.Value.ToString().Replace('/', '\\') + "\0")); } else if (ExtraTargetPath.Key == UnrealArch.Arm64) { const int ExecFileResourceId_ARM64 = 203; Update.SetData(ExecFileResourceId_ARM64, ResourceType.RawData, Encoding.Unicode.GetBytes(ExtraTargetPath.Value.ToString().Replace('/', '\\') + "\0")); } else if (ExtraTargetPath.Key == UnrealArch.Arm64ec) { const int ExecFileResourceId_ARM64EC = 204; Update.SetData(ExecFileResourceId_ARM64EC, ResourceType.RawData, Encoding.Unicode.GetBytes(ExtraTargetPath.Value.ToString().Replace('/', '\\') + "\0")); } else { throw new AutomationException("Unexpected architecture {arch} for bootstrap executable", ExtraTargetPath.Key); } } } const int ExecArgsResourceId = 202; Update.SetData(ExecArgsResourceId, ResourceType.RawData, Encoding.Unicode.GetBytes(StagedArguments + "\0")); } } else { Logger.LogInformation("Skipping patching of bootstrap executable (unsupported host platform)"); } // Copy it to the staging directory SC.StageFile(StagedFileType.SystemNonUFS, IntermediateFile, new StagedFileReference(ExeName)); } } public override string GetCookPlatform(bool bDedicatedServer, bool bIsClientOnly) { const string NoEditorCookPlatform = "Windows"; const string ServerCookPlatform = "WindowsServer"; const string ClientCookPlatform = "WindowsClient"; if (bDedicatedServer) { return ServerCookPlatform; } else if (bIsClientOnly) { return ClientCookPlatform; } else { return NoEditorCookPlatform; } } public override string GetEditorCookPlatform() { return "WindowsEditor"; } public override string GetPlatformPakCommandLine(ProjectParams Params, DeploymentContext SC) { string PakParams = " -patchpaddingalign=2048"; if (!SC.DedicatedServer) { string OodleDllPath = DirectoryReference.Combine(SC.ProjectRoot, "Binaries/ThirdParty/Oodle/Win64/UnrealPakPlugin.dll").FullName; if (File.Exists(OodleDllPath)) { PakParams += String.Format(" -customcompressor=\"{0}\"", OodleDllPath); } } return PakParams; } public override void Package(ProjectParams Params, DeploymentContext SC, int WorkingCL) { // If this is a content-only project and there's a custom icon, update the executable if (!Params.HasDLCName && !Params.IsCodeBasedProject) { FileReference IconFile = FileReference.Combine(Params.RawProjectPath.Directory, "Build", "Windows", "Application.ico"); if(FileReference.Exists(IconFile)) { Logger.LogInformation("Updating executable with custom icon from {IconFile}", IconFile); GroupIconResource GroupIcon = GroupIconResource.FromIco(IconFile.FullName); List ExecutablePaths = GetExecutableNames(SC); foreach (FileReference ExecutablePath in ExecutablePaths) { using (ModuleResourceUpdate Update = new ModuleResourceUpdate(ExecutablePath.FullName, false)) { const int IconResourceId = 123; // As defined in Engine\Source\Runtime\Launch\Resources\Windows\resource.h if (GroupIcon != null) { Update.SetIcons(IconResourceId, GroupIcon); } } } } } PrintRunTime(); } public override bool UseAbsLog { get { return BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Win64; } } public override bool CanHostPlatform(UnrealTargetPlatform Platform) { if (Platform == UnrealTargetPlatform.Mac) { return false; } return true; } public override bool ShouldStageCommandLine(ProjectParams Params, DeploymentContext SC) { return false; // !String.IsNullOrEmpty(Params.StageCommandline) || !String.IsNullOrEmpty(Params.RunCommandline) || (!Params.IsCodeBasedProject && Params.NoBootstrapExe); } public override List GetDebugFileExtensions() { return new List { ".pdb", ".map" }; } public override CustomDeploymentHandler GetCustomDeploymentHandler(ProjectParams Params, DeploymentContext SC) { ConfigHierarchy EngineIni = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, Params.RawProjectPath.Directory, PlatformType, SC.CustomConfig); EngineIni.GetString("/Script/WindowsTargetPlatform.WindowsTargetSettings", "CustomDeployment", out string CustomDeploymentName); if (string.IsNullOrEmpty(CustomDeploymentName)) { CustomDeploymentName = Params.CustomDeploymentHandler; } if (!string.IsNullOrEmpty(CustomDeploymentName)) { return CustomDeploymentHandler.Create(CustomDeploymentName, this); } return base.GetCustomDeploymentHandler(Params, SC); } public override bool SignExecutables(DeploymentContext SC, ProjectParams Params) { // Sign everything we built List FilesToSign = GetExecutableNames(SC); CodeSign.SignMultipleFilesIfEXEOrDLL(FilesToSign); return true; } public void StageAppLocalDependencies(ProjectParams Params, DeploymentContext SC, string SourcePlatformDir, UnrealArch Architecture) { Dictionary PathVariables = new Dictionary(); PathVariables["EngineDir"] = SC.EngineRoot.FullName; PathVariables["ProjectDir"] = SC.ProjectRoot.FullName; // support multiple comma-separated paths string[] AppLocalDirectories = Params.AppLocalDirectory.Split(';'); foreach (string AppLocalDirectory in AppLocalDirectories) { string ExpandedAppLocalDir = Utils.ExpandVariables(AppLocalDirectory, PathVariables); string ArchitectureDir = Architecture.WindowsToolChain; DirectoryReference BaseAppLocalDependenciesPath = Path.IsPathRooted(ExpandedAppLocalDir) ? new DirectoryReference(CombinePaths(ExpandedAppLocalDir, SourcePlatformDir, ArchitectureDir)) : DirectoryReference.Combine(SC.ProjectRoot, ExpandedAppLocalDir, SourcePlatformDir, ArchitectureDir); if (!DirectoryReference.Exists(BaseAppLocalDependenciesPath)) { BaseAppLocalDependenciesPath = BaseAppLocalDependenciesPath.ParentDirectory!; // no architecture subfolder - must be user-supplied dependency folder in need of upgrade if (DirectoryReference.Exists(BaseAppLocalDependenciesPath)) { Logger.LogWarning("{path} should have {arch} subfolder with the redist DLLs inside. Falling back to use {path} directly", BaseAppLocalDependenciesPath, Architecture, BaseAppLocalDependenciesPath); } } if (DirectoryReference.Exists(BaseAppLocalDependenciesPath)) { StageAppLocalDependenciesToDir(SC, BaseAppLocalDependenciesPath, Architecture, StagedDirectoryReference.Combine("Engine", "Binaries", PlatformType.ToString())); StageAppLocalDependenciesToDir(SC, BaseAppLocalDependenciesPath, Architecture, StagedDirectoryReference.Combine(SC.RelativeProjectRootForStage, "Binaries", PlatformType.ToString())); } else { Logger.LogWarning("Unable to deploy AppLocalDirectory dependencies. No such path: {BaseAppLocalDependenciesPath}", BaseAppLocalDependenciesPath); } } } static void StageAppLocalDependenciesToDir(DeploymentContext SC, DirectoryReference BaseAppLocalDependenciesPath, UnrealArch Architecture, StagedDirectoryReference StagedBinariesDir) { // x64 DLL dependencies are placed directly in StagedBinariesDir bool bUseArchitectureSubdirectory = !Architecture.bIsX64; string AssemblyName = Architecture.ToString().ToLower(); StagedDirectoryReference StagedArchitectureBinariesDir = bUseArchitectureSubdirectory ? StagedDirectoryReference.Combine(StagedBinariesDir, AssemblyName) : StagedBinariesDir; // Check if there are any executables being staged in this directory. Usually we only need to stage runtime dependencies next to the executable, but we may be staging // other engine executables too (eg. CEF) List FilesInTargetDir = SC.FilesToStage.NonUFSFiles.Keys.Where(x => x.IsUnderDirectory(StagedBinariesDir) && (x.HasExtension(".exe") || x.HasExtension(".dll"))).ToList(); if(FilesInTargetDir.Count > 0) { Logger.LogInformation("Copying AppLocal dependencies from {BaseAppLocalDependenciesPath} to {StagedArchitectureBinariesDir}", BaseAppLocalDependenciesPath, StagedArchitectureBinariesDir); // Stage files in subdirs foreach (DirectoryReference DependencyDirectory in DirectoryReference.EnumerateDirectories(BaseAppLocalDependenciesPath)) { SC.StageFiles(StagedFileType.NonUFS, DependencyDirectory, StageFilesSearch.AllDirectories, StagedArchitectureBinariesDir); } } } public void UpdateStagedSxSManifest( DeploymentContext SC, UnrealArch Architecture, StagedDirectoryReference StagedBinariesDir) { // x64 doesn't need SxS manifest - DLL dependencies are placed directly in StagedBinariesDir if (Architecture.bIsX64) { return; } // try to update the staged SxS manifest string AssemblyName = Architecture.ToString().ToLower(); DirectoryReference ArchitectureDir = DirectoryReference.Combine( SC.StageDirectory, StagedBinariesDir.Name, AssemblyName ); FileReference ManifestFile = FileReference.Combine(ArchitectureDir, AssemblyName + ".manifest"); if (FileReference.Exists(ManifestFile)) { try { XDocument XmlManifest = XDocument.Load(ManifestFile.FullName); if (XmlManifest.Root != null && XmlManifest.Root.Name.LocalName == "assembly") { XNamespace XmlNs = XmlManifest.Root.Name.Namespace; // remove all existing file elements & add the new ones (this ensures that stale items from the Binaries folder are not included) XmlManifest.Root.Elements(XmlNs+"file").ToList().ForEach( X => X.Remove() ); foreach (FileReference DLLFile in DirectoryReference.EnumerateFiles(ArchitectureDir, "*.dll")) { string DLLName = DLLFile.MakeRelativeTo(ArchitectureDir); XmlManifest.Root.Add( new XElement(XmlNs+"file", new XAttribute("name", DLLName ) ) ); } XmlManifest.Save(ManifestFile.FullName); } } catch(Exception e) { Logger.LogWarning("Could not update SxS manifest {name} : {msg}", ManifestFile.GetFileName(), e.Message); } } } public override void PostStagingFileCopy(ProjectParams Params, DeploymentContext SC) { base.PostStagingFileCopy(Params, SC); // refresh the architecture SxS manifests if they are in use foreach (UnrealArch Architecture in SC.StageTargets.Select( Target => Target.Receipt.Architectures.SingleArchitecture ).Distinct()) { UpdateStagedSxSManifest(SC, Architecture, StagedDirectoryReference.Combine("Engine", "Binaries", PlatformType.ToString())); UpdateStagedSxSManifest(SC, Architecture, StagedDirectoryReference.Combine(SC.RelativeProjectRootForStage, "Binaries", PlatformType.ToString())); } } /// /// Try to get the symstore.exe path from the given Windows SDK version /// /// Path to symstore.exe [SupportedOSPlatform("windows")] private static FileReference GetSymStoreExe() { // Trying first to look for auto sdk latest WindowsKits debugger tools DirectoryReference HostAutoSdkDir = null; if (UEBuildPlatformSDK.TryGetHostPlatformAutoSDKDir(out HostAutoSdkDir)) { DirectoryReference WindowsKitsDebuggersDirAutoSdk = DirectoryReference.Combine(HostAutoSdkDir, "Win64", "Windows Kits", "Debuggers"); if (DirectoryReference.Exists(WindowsKitsDebuggersDirAutoSdk)) { FileReference SymStoreExe64 = FileReference.Combine(WindowsKitsDebuggersDirAutoSdk, "x64", "symstore.exe"); if (FileReference.Exists(SymStoreExe64)) { return SymStoreExe64; } } } List> WindowsSdkDirs = WindowsExports.GetWindowsSdkDirs(); foreach (DirectoryReference WindowsSdkDir in WindowsSdkDirs.Select(x => x.Value)) { FileReference SymStoreExe64 = FileReference.Combine(WindowsSdkDir, "Debuggers", "x64", "symstore.exe"); if (FileReference.Exists(SymStoreExe64)) { return SymStoreExe64; } } throw new AutomationException("Unable to find a Windows SDK installation containing Debuggers/x64/symstore.exe"); } [SupportedOSPlatform("windows")] public static bool TryGetPdbCopyLocation(out FileReference OutLocation) { // Trying first to look for auto sdk latest WindowsKits debugger tools DirectoryReference HostAutoSdkDir = null; if (UEBuildPlatformSDK.TryGetHostPlatformAutoSDKDir(out HostAutoSdkDir)) { DirectoryReference WindowsKitsDebuggersDirAutoSdk = DirectoryReference.Combine(HostAutoSdkDir, "Win64", "Windows Kits", "Debuggers"); if (DirectoryReference.Exists(WindowsKitsDebuggersDirAutoSdk)) { FileReference PdbCopyExe64 = FileReference.Combine(WindowsKitsDebuggersDirAutoSdk, "x64", "pdbcopy.exe"); if (FileReference.Exists(PdbCopyExe64)) { OutLocation = PdbCopyExe64; return true; } } } // Try to find an installation of the Windows 10 SDK List> WindowsSdkDirs = WindowsExports.GetWindowsSdkDirs(); foreach (DirectoryReference WindowsSdkDir in WindowsSdkDirs.Select(x => x.Value)) { FileReference PdbCopyExe64 = FileReference.Combine(WindowsSdkDir, "Debuggers", "x64", "pdbcopy.exe"); if (FileReference.Exists(PdbCopyExe64)) { OutLocation = PdbCopyExe64; return true; } } // Otherwise fail OutLocation = null; return false; } [SupportedOSPlatform("windows")] public override void StripSymbols(FileReference SourceFile, FileReference TargetFile) { bool bStripInPlace = false; if (SourceFile == TargetFile) { // PDBCopy only supports creation of a brand new stripped file so we have to create a temporary filename TargetFile = new FileReference(Path.Combine(TargetFile.Directory.FullName, Guid.NewGuid().ToString() + TargetFile.GetExtension())); bStripInPlace = true; } FileReference PdbCopyLocation; if(!TryGetPdbCopyLocation(out PdbCopyLocation)) { throw new AutomationException("Unable to find installation of pdbcopy.exe, which is required to strip symbols. This tool is included as part of the 'Windows Debugging Tools' component of the Windows 10 SDK (https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk)."); } ProcessStartInfo StartInfo = new ProcessStartInfo(); StartInfo.FileName = PdbCopyLocation.FullName; StartInfo.Arguments = String.Format("\"{0}\" \"{1}\" -p", SourceFile.FullName, TargetFile.FullName); StartInfo.UseShellExecute = false; StartInfo.CreateNoWindow = true; Utils.RunLocalProcessAndLogOutput(StartInfo, Log.Logger); if (bStripInPlace) { // Copy stripped file to original location and delete the temporary file File.Copy(TargetFile.FullName, SourceFile.FullName, true); FileReference.Delete(TargetFile); } } [SupportedOSPlatform("windows")] public override bool PublishSymbols(DirectoryReference SymbolStoreDirectory, List Files, bool bIndexSources, List SourceFiles, string Product, string Branch, int Change, string BuildVersion = null) { Logger.LogInformation("Publishing symbols to \"{SymbolStoreDirectory}\" (source indexing: {bIndexSources})", SymbolStoreDirectory, bIndexSources); // Get the symstore.exe path, using the latest SDK version we can find. FileReference SymStoreExe = GetSymStoreExe(); Logger.LogInformation("Using '{Path}' Version {Version}", SymStoreExe, FileVersionInfo.GetVersionInfo(SymStoreExe.FullName).FileVersion); List FilesToAdd = Files.Where(x => x.HasExtension(".pdb") || x.HasExtension(".exe") || x.HasExtension(".dll")).ToList(); if(FilesToAdd.Count > 0) { DateTime Start = DateTime.Now; DirectoryReference TempSymStoreDir = DirectoryReference.Combine(Unreal.RootDirectory, "Saved", "SymStore"); if (DirectoryReference.Exists(TempSymStoreDir)) { CommandUtils.DeleteDirectory(TempSymStoreDir); DirectoryReference.CreateDirectory(TempSymStoreDir); } DirectoryReference TempSymStoreIndexedDir = DirectoryReference.Combine(Unreal.RootDirectory, "Engine", "Intermediate", "SymStoreIndexed"); string TempFileName = Path.GetTempFileName(); try { IEnumerable SymbolsToIndex = Enumerable.Empty(); IEnumerable SymbolsAfterIndexing = Enumerable.Empty(); if (bIndexSources) { // Skip read-only PDBs as we won't be able to add data to them. They are likely to be symbols for third-party libraries checked into the source control. SymbolsToIndex = FilesToAdd.Where(x => x.HasExtension(".pdb") && !(new FileInfo(x.FullName).IsReadOnly)); // We can't write to the original symbol files because they may have been generated by a different build step, // and clobbering such build products is considered an error. // For this reason, we copy the symbol files and modify the copied. SymbolsAfterIndexing = CopySymbolsWithSourceIndexing(TempSymStoreIndexedDir, SymbolsToIndex, SourceFiles, Branch, Change); } File.WriteAllLines(TempFileName, FilesToAdd.Except(SymbolsToIndex).Union(SymbolsAfterIndexing).Select(x => x.FullName), Encoding.ASCII); // Copy everything to the temp symstore ProcessStartInfo StartInfo = new ProcessStartInfo(); StartInfo.FileName = SymStoreExe.FullName; StartInfo.Arguments = string.Format("add /f \"@{0}\" /s \"{1}\" /t \"{2}\" /o", TempFileName, TempSymStoreDir, Product); StartInfo.UseShellExecute = false; StartInfo.CreateNoWindow = true; if (Utils.RunLocalProcessAndLogOutput(StartInfo, Log.Logger) != 0) { return false; } } finally { File.Delete(TempFileName); CommandUtils.DeleteDirectory(TempSymStoreIndexedDir); } DateTime CompressDone = DateTime.Now; Logger.LogInformation("Took {Arg0}s to compress the symbol files to temp path {TempSymStoreDir}", (CompressDone - Start).TotalSeconds, TempSymStoreDir); int CopiedCount = 0; // Take each new compressed file made and try and copy it to the real symstore. Exclude any symstore admin files foreach(FileReference File in DirectoryReference.EnumerateFiles(TempSymStoreDir, "*.*", SearchOption.AllDirectories).Where(File => IsSymbolFile(File))) { string RelativePath = File.MakeRelativeTo(DirectoryReference.Combine(TempSymStoreDir)); FileReference ActualDestinationFile = FileReference.Combine(SymbolStoreDirectory, RelativePath); // Try and add a version file. Do this before checking to see if the symbol is there already in the case of exact matches (multiple builds could use the same pdb, for example) if (!string.IsNullOrWhiteSpace(BuildVersion)) { FileReference BuildVersionFile = FileReference.Combine(ActualDestinationFile.Directory, string.Format("{0}.version", BuildVersion)); // Attempt to create the file. Just continue if it fails. try { DirectoryReference.CreateDirectory(BuildVersionFile.Directory); FileReference.WriteAllText(BuildVersionFile, string.Empty); } catch (Exception Ex) { Logger.LogWarning("Failed to write the version file, reason {Arg0}", Ex.ToString()); } } // Don't bother copying the temp file if the destination file is there already. if (FileReference.Exists(ActualDestinationFile)) { Logger.LogInformation("Destination file {Arg0} already exists, skipping", ActualDestinationFile.FullName); continue; } FileReference TempDestinationFile = new FileReference(ActualDestinationFile.FullName + Guid.NewGuid().ToString()); try { CommandUtils.CopyFile(File.FullName, TempDestinationFile.FullName); } catch(Exception Ex) { throw new AutomationException("Couldn't copy the symbol file to the temp store! Reason: {0}", Ex.ToString()); } // Move the file in the temp store over. try { FileReference.Move(TempDestinationFile, ActualDestinationFile); //LogVerbose("Moved {0} to {1}", TempDestinationFile, ActualDestinationFile); CopiedCount++; } catch (Exception Ex) { // If the file is there already, it was likely either copied elsewhere (and this is an ioexception) or it had a file handle open already. // Either way, it's fine to just continue on. if (FileReference.Exists(ActualDestinationFile)) { Logger.LogInformation("Destination file {Arg0} already exists or was in use, skipping.", ActualDestinationFile.FullName); continue; } // If it doesn't exist, we actually failed to copy it entirely. else { Logger.LogWarning("Couldn't move temp file {Arg0} to the symbol store at location {Arg1}! Reason: {Arg2}", TempDestinationFile.FullName, ActualDestinationFile.FullName, Ex.ToString()); } } // Delete the temp one no matter what, don't want them hanging around in the symstore finally { FileReference.Delete(TempDestinationFile); } } Logger.LogInformation("Took {Arg0}s to copy {CopiedCount} symbol files to the store at {SymbolStoreDirectory}", (DateTime.Now - CompressDone).TotalSeconds, CopiedCount, SymbolStoreDirectory); FileReference PingmeFile = FileReference.Combine(SymbolStoreDirectory, "pingme.txt"); if (!FileReference.Exists(PingmeFile)) { Logger.LogInformation("Creating {PingmeFile} to mark path as three-tiered symbol location", PingmeFile); File.WriteAllText(PingmeFile.FullName, "Exists to mark this as a three-tiered symbol location"); } } return true; } [SupportedOSPlatform("windows")] private IEnumerable CopySymbolsWithSourceIndexing(DirectoryReference TargetDirectory, IEnumerable SymbolFiles, IEnumerable SourceFiles, string Branch, int Change) { if (DirectoryReference.Exists(TargetDirectory)) { CommandUtils.DeleteDirectory(TargetDirectory); DirectoryReference.CreateDirectory(TargetDirectory); } List CopiedSymbolFiles = new List(); // Copy all the symbol files to the given directory. foreach (FileReference File in SymbolFiles) { string RelativePath = File.MakeRelativeTo(Unreal.RootDirectory); FileReference DestinationFile = FileReference.Combine(TargetDirectory, RelativePath); try { CommandUtils.CopyFile(File.FullName, DestinationFile.FullName); } catch (Exception Ex) { throw new AutomationException("Couldn't copy the pdb file to the temp directory for indexing! Reason: {0}", Ex.ToString()); } CopiedSymbolFiles.Add(DestinationFile); } // Index all symbol files in one go (indexing source code from the source control is shared work between all pdbs). AddSourceIndexToSymbols(CopiedSymbolFiles, SourceFiles, Branch, Change); return CopiedSymbolFiles; } bool IsSymbolFile(FileReference File) { if (File.HasExtension(".dll") || File.HasExtension(".exe") || File.HasExtension(".pdb")) { return true; } if (File.HasExtension(".dl_") || File.HasExtension(".ex_") || File.HasExtension(".pd_")) { return true; } return false; } /// /// Build a database of source code files in the current Perforce workspace. /// By relying on the standard layout for UE projects i.e. the fact that source code is in Source directories, /// we may very significantly reduce the about of data sent to us from the server. /// /// Perforce pattern path to query e.g. //UE/Branch/.../Source/... /// protected static Dictionary BuildSourceDatabase(string Pattern) { List Files = null; P4Connection DefaultConnection = new P4Connection(User: null, Client: null, ServerAndPort: null); try { Files = DefaultConnection.HaveFiles(Pattern); } catch (P4Exception e) { Logger.LogError("Failed to fetch source code information from Perforce for '{Pattern}' ({Message}).", Pattern, e.Message); return null; } return Files.ToDictionary(file => file.ClientFile, file => file, StringComparer.InvariantCultureIgnoreCase); } /// /// /// /// /// /// /// [SupportedOSPlatform("windows")] public void AddSourceIndexToSymbols(IEnumerable PdbFiles, IEnumerable SourceFiles, string Branch, int Change) { Logger.LogInformation("Adding source control information to PDB files..."); string DepotFilter = ".../Source/..."; Dictionary SourceDatabase = BuildSourceDatabase(DepotFilter); if (SourceDatabase == null) { throw new AutomationException($"Failed to query the source code information for '{DepotFilter}'."); } // Get the pdbstr.exe path, using the latest SDK version we can find. FileReference PdbStrExe = GetPdbStrExe(); // Get the path to the generated SRCSRV.INI file FileReference SrcSrvIni = FileReference.Combine(Unreal.RootDirectory, "Engine", "Intermediate", "SrcSrv.ini"); DirectoryReference.CreateDirectory(SrcSrvIni.Directory); // Generate the SRCSRV.INI file using (StreamWriter Writer = new StreamWriter(SrcSrvIni.FullName)) { int MissingFilesCount = 0; Writer.WriteLine("SRCSRV: ini------------------------------------------------"); Writer.WriteLine("VERSION=1"); Writer.WriteLine("VERCTRL=Perforce"); Writer.WriteLine("SRCSRV: variables------------------------------------------"); Writer.WriteLine("SRCSRVTRG=%sdtrg%"); Writer.WriteLine("SRCSRVCMD=%sdcmd%"); Writer.WriteLine("SDCMD=p4.exe print -o %srcsrvtrg% \"//%var2%#%var3%\""); Writer.WriteLine("SDTRG=%targ%\\%fnbksl%(%var2%)#%var3%"); Writer.WriteLine("SRCSRV: source files ---------------------------------------"); foreach (FileReference SourceFile in SourceFiles) { P4HaveRecord SourceInfo; if (SourceDatabase.TryGetValue(SourceFile.FullName, out SourceInfo)) { Writer.WriteLine("{0}*{1}*{2}", SourceFile.FullName, SourceInfo.DepotFile.Replace("//", ""), SourceInfo.Revision); } else { ++MissingFilesCount; } } Writer.WriteLine("SRCSRV: end------------------------------------------------"); if (MissingFilesCount > 0) { Logger.LogInformation("Skipped {MissingFilesCount} files (out of {SourceFileCount}) for which source control files couldn't be located.", MissingFilesCount, SourceFiles.Count()); } } // Execute PDBSTR on the PDB files in parallel. Parallel.ForEach(PdbFiles, (PdbFile, State) => { ExecutePdbStrTool(PdbStrExe, PdbFile, SrcSrvIni, State); }); } /// /// Executes the PdbStr tool. /// /// Path to pdbstr.exe /// The PDB file to embed source information for /// Ini file containing settings to embed /// The current loop state /// True if the tool executed successfully static void ExecutePdbStrTool(FileReference PdbStrExe, FileReference PdbFile, FileReference SrcSrvIni, ParallelLoopState State) { FileInfo PdbInfo = new FileInfo(PdbFile.FullName); FileInfo IniInfo = new FileInfo(SrcSrvIni.FullName); using (Process Process = new Process()) { List Messages = new List(); Messages.Add(String.Format("Writing source server data: {0}", PdbFile)); DataReceivedEventHandler OutputHandler = (s, e) => { if (e.Data != null) { Messages.Add(e.Data); } }; Process.StartInfo.FileName = PdbStrExe.FullName; Process.StartInfo.Arguments = String.Format("-w -p:\"{0}\" -i:\"{1}\" -s:srcsrv", PdbFile.FullName, SrcSrvIni.FullName); Process.StartInfo.UseShellExecute = false; Process.StartInfo.RedirectStandardOutput = true; Process.StartInfo.RedirectStandardError = true; Process.StartInfo.RedirectStandardInput = false; Process.StartInfo.CreateNoWindow = true; Process.OutputDataReceived += OutputHandler; Process.ErrorDataReceived += OutputHandler; Process.Start(); Process.BeginOutputReadLine(); Process.BeginErrorReadLine(); Process.WaitForExit(); if (Process.ExitCode != 0) { Messages.Add($"Failed to embed source server data for {PdbFile} (exit code: {Process.ExitCode})"); } lock (State) { foreach (string Message in Messages) { Logger.LogInformation("{Text}", Message); } } } } /// /// Try to get the pdbstr.exe path from the Windows SDK /// /// Path to pdbstr.exe [SupportedOSPlatform("windows")] static FileReference GetPdbStrExe() { List> WindowsSdkDirs = WindowsExports.GetWindowsSdkDirs(); // Trying first to look for auto sdk latest WindowsKits debugger tools DirectoryReference HostAutoSdkDir = null; if (UEBuildPlatformSDK.TryGetHostPlatformAutoSDKDir(out HostAutoSdkDir)) { DirectoryReference WindowsKitsDebuggersDirAutoSdk = DirectoryReference.Combine(HostAutoSdkDir, "Win64", "Windows Kits", "Debuggers"); if (DirectoryReference.Exists(WindowsKitsDebuggersDirAutoSdk)) { FileReference CheckPdbStrExe64 = FileReference.Combine(WindowsKitsDebuggersDirAutoSdk, "x64", "srcsrv", "pdbstr.exe"); if (FileReference.Exists(CheckPdbStrExe64)) { return CheckPdbStrExe64; } } } foreach (DirectoryReference WindowsSdkDir in WindowsSdkDirs.Select(x => x.Value)) { FileReference CheckPdbStrExe64 = FileReference.Combine(WindowsSdkDir, "Debuggers", "x64", "srcsrv", "pdbstr.exe"); if (FileReference.Exists(CheckPdbStrExe64)) { return CheckPdbStrExe64; } } throw new AutomationException("Unable to find a Windows SDK installation containing Debuggers/x64/srcsrv/pdbstr.exe"); } public override string[] SymbolServerDirectoryStructure { get { return new string[] { "{0}*.pdb;{0}*.exe;{0}*.dll", // Binary File Directory (e.g. QAGameClient-Win64-Test.exe --- .pdb, .dll and .exe are allowed extensions) "*", // Hash Directory (e.g. A92F5744D99F416EB0CCFD58CCE719CD1) }; } } // Lock file no longer needed since files are moved over the top from the temp symstore public override bool SymbolServerRequiresLock { get { return false; } } }