Files
UnrealEngine/Engine/Source/Programs/UnrealGameSync/UnrealGameSyncShared/ConfigUtils.cs
2025-05-18 13:04:45 +08:00

666 lines
23 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using EpicGames.Core;
using EpicGames.Perforce;
using Microsoft.Extensions.Logging;
namespace UnrealGameSync
{
public class TargetReceipt
{
public string? Configuration { get; set; }
public string? Launch { get; set; }
public string? LaunchCmd { get; set; }
public static bool TryRead(FileReference location, DirectoryReference? engineDir, DirectoryReference? projectDir, [NotNullWhen(true)] out TargetReceipt? receipt)
{
if (Utility.TryLoadJson(location, out receipt))
{
receipt.Launch = ExpandReceiptVariables(receipt.Launch, engineDir, projectDir);
receipt.LaunchCmd = ExpandReceiptVariables(receipt.LaunchCmd, engineDir, projectDir);
return true;
}
return false;
}
[return: NotNullIfNotNull("line")]
private static string? ExpandReceiptVariables(string? line, DirectoryReference? engineDir, DirectoryReference? projectDir)
{
string? expandedLine = line;
if (expandedLine != null)
{
if (engineDir != null)
{
expandedLine = expandedLine.Replace("$(EngineDir)", engineDir.FullName, StringComparison.OrdinalIgnoreCase);
}
if (projectDir != null)
{
expandedLine = expandedLine.Replace("$(ProjectDir)", projectDir.FullName, StringComparison.OrdinalIgnoreCase);
}
}
return expandedLine;
}
}
public static class ConfigUtils
{
public static string HostPlatform { get; } = GetHostPlatform();
public static string HostArchitectureSuffix { get; } = String.Empty;
static string GetHostPlatform()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return "Win64";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return "Mac";
}
else
{
return "Linux";
}
}
public static Task<ConfigFile> ReadProjectConfigFileAsync(IPerforceConnection perforce, ProjectInfo projectInfo, ILogger logger, CancellationToken cancellationToken)
{
return ReadProjectConfigFileAsync(perforce, projectInfo, new List<KeyValuePair<FileReference, DateTime>>(), logger, cancellationToken);
}
public static Task<ConfigFile> ReadProjectConfigFileAsync(IPerforceConnection perforce, ProjectInfo projectInfo, List<KeyValuePair<FileReference, DateTime>> localConfigFiles, ILogger logger, CancellationToken cancellationToken)
{
return ReadProjectConfigFileAsync(perforce, projectInfo.ClientRootPath, projectInfo.ClientFileName, projectInfo.CacheFolder, localConfigFiles, logger, cancellationToken);
}
public static async Task<ConfigFile> ReadProjectConfigFileAsync(IPerforceConnection perforce, string branchClientPath, string selectedClientFileName, DirectoryReference cacheFolder, List<KeyValuePair<FileReference, DateTime>> localConfigFiles, ILogger logger, CancellationToken cancellationToken)
{
List<string> configFilePaths = Utility.GetDepotConfigPaths(branchClientPath + "/Engine", selectedClientFileName);
ConfigFile projectConfig = new ConfigFile();
List<PerforceResponse<FStatRecord>> responses = await perforce.TryFStatAsync(FStatOptions.IncludeFileSizes, configFilePaths, cancellationToken).ToListAsync(cancellationToken);
foreach (PerforceResponse<FStatRecord> response in responses)
{
if (response.Succeeded)
{
string[]? lines = null;
// Skip file records which are still in the workspace, but were synced from a different branch. For these files, the action seems to be empty, so filter against that.
FStatRecord fileRecord = response.Data;
if (fileRecord.HeadAction == FileAction.None)
{
continue;
}
// If this file is open for edit, read the local version
string? localFileName = fileRecord.ClientFile;
if (localFileName != null && File.Exists(localFileName) && (File.GetAttributes(localFileName) & FileAttributes.ReadOnly) == 0)
{
try
{
DateTime lastModifiedTime = File.GetLastWriteTimeUtc(localFileName);
localConfigFiles.Add(new KeyValuePair<FileReference, DateTime>(new FileReference(localFileName), lastModifiedTime));
lines = await File.ReadAllLinesAsync(localFileName, cancellationToken);
}
catch (Exception ex)
{
logger.LogInformation(ex, "Failed to read local config file for {Path}", localFileName);
}
}
// Otherwise try to get it from perforce
if (lines == null && fileRecord.DepotFile != null)
{
lines = await Utility.TryPrintFileUsingCacheAsync(perforce, fileRecord.DepotFile, cacheFolder, fileRecord.Digest, logger, cancellationToken);
}
// Merge the text with the config file
if (lines != null)
{
try
{
projectConfig.Parse(lines.ToArray());
logger.LogDebug("Read config file from {DepotFile}", fileRecord.DepotFile);
}
catch (Exception ex)
{
logger.LogInformation(ex, "Failed to read config file from {DepotFile}", fileRecord.DepotFile);
}
}
}
}
return projectConfig;
}
public static async Task<List<string[]>> ReadConfigFiles(IPerforceConnection perforce, IEnumerable<string> depotPaths, List<KeyValuePair<FileReference, DateTime>> localFiles, DirectoryReference cacheFolder, ILogger logger, CancellationToken cancellationToken)
{
List<string[]> contents = new List<string[]>();
List<PerforceResponse<FStatRecord>> responses = await perforce.TryFStatAsync(FStatOptions.IncludeFileSizes, depotPaths.ToArray(), cancellationToken).ToListAsync(cancellationToken);
foreach (PerforceResponse<FStatRecord> response in responses)
{
if (response.Succeeded)
{
string[]? lines = null;
// Skip file records which are still in the workspace, but were synced from a different branch. For these files, the action seems to be empty, so filter against that.
FStatRecord fileRecord = response.Data;
if (fileRecord.HeadAction == FileAction.None)
{
continue;
}
// If this file is open for edit, read the local version
string? localFileName = fileRecord.ClientFile;
if (localFileName != null && File.Exists(localFileName) && (File.GetAttributes(localFileName) & FileAttributes.ReadOnly) == 0)
{
try
{
DateTime lastModifiedTime = File.GetLastWriteTimeUtc(localFileName);
localFiles.Add(new KeyValuePair<FileReference, DateTime>(new FileReference(localFileName), lastModifiedTime));
lines = await File.ReadAllLinesAsync(localFileName, cancellationToken);
}
catch (Exception ex)
{
logger.LogInformation(ex, "Failed to read local config file for {Path}", localFileName);
}
}
// Otherwise try to get it from perforce
if (lines == null && fileRecord.DepotFile != null)
{
lines = await Utility.TryPrintFileUsingCacheAsync(perforce, fileRecord.DepotFile, cacheFolder, fileRecord.Digest, logger, cancellationToken);
}
// Merge the text with the config file
if (lines != null)
{
contents.Add(lines);
}
}
}
return contents;
}
public static FileReference GetEditorTargetFile(ProjectInfo projectInfo, ConfigFile projectConfig)
{
if (projectInfo.ProjectPath.EndsWith(".uproject", StringComparison.OrdinalIgnoreCase))
{
List<FileReference> targetFiles = FindTargets(projectInfo.LocalFileName.Directory);
FileReference? targetFile = targetFiles.OrderBy(x => x.FullName, StringComparer.OrdinalIgnoreCase).FirstOrDefault(x => x.FullName.EndsWith("Editor.target.cs", StringComparison.OrdinalIgnoreCase));
if (targetFile != null)
{
return targetFile;
}
}
string defaultEditorTargetName = GetDefaultEditorTargetName(projectInfo, projectConfig);
return FileReference.Combine(projectInfo.LocalRootPath, "Engine", "Source", $"{defaultEditorTargetName}.Target.cs");
}
public static FileReference GetEditorReceiptFile(ProjectInfo projectInfo, ConfigFile projectConfig, BuildConfig config)
{
FileReference targetFile = GetEditorTargetFile(projectInfo, projectConfig);
return GetReceiptFile(projectInfo, projectConfig, targetFile, config.ToString());
}
private static List<FileReference> FindTargets(DirectoryReference engineOrProjectDir)
{
List<FileReference> targets = new List<FileReference>();
DirectoryReference sourceDir = DirectoryReference.Combine(engineOrProjectDir, "Source");
if (DirectoryReference.Exists(sourceDir))
{
foreach (FileReference targetFile in DirectoryReference.EnumerateFiles(sourceDir))
{
const string extension = ".target.cs";
if (targetFile.FullName.EndsWith(extension, StringComparison.OrdinalIgnoreCase))
{
targets.Add(targetFile);
}
}
}
return targets;
}
public static string GetDefaultEditorTargetName(ProjectInfo projectInfo, ConfigFile projectConfigFile)
{
string? editorTarget;
if (!TryGetProjectSetting(projectConfigFile, projectInfo.ProjectIdentifier, "EditorTarget", out editorTarget))
{
if (projectInfo.IsEnterpriseProject)
{
editorTarget = "StudioEditor";
}
else
{
editorTarget = "UE4Editor";
}
}
return editorTarget;
}
public static bool TryReadEditorReceipt(ProjectInfo projectInfo, FileReference receiptFile, [NotNullWhen(true)] out TargetReceipt? receipt)
{
DirectoryReference engineDir = DirectoryReference.Combine(projectInfo.LocalRootPath, "Engine");
DirectoryReference projectDir = projectInfo.LocalFileName.Directory;
if (receiptFile.IsUnderDirectory(projectDir))
{
return TargetReceipt.TryRead(receiptFile, engineDir, projectDir, out receipt);
}
else
{
return TargetReceipt.TryRead(receiptFile, engineDir, null, out receipt);
}
}
public static TargetReceipt CreateDefaultEditorReceipt(ProjectInfo projectInfo, ConfigFile projectConfigFile, BuildConfig configuration)
{
string baseName = GetDefaultEditorTargetName(projectInfo, projectConfigFile);
if (configuration != BuildConfig.Development || !String.IsNullOrEmpty(HostArchitectureSuffix))
{
if (configuration != BuildConfig.DebugGame || projectConfigFile.GetValue("Options.DebugGameHasSeparateExecutable", false))
{
baseName += $"-{HostPlatform}-{configuration}{HostArchitectureSuffix}";
}
}
string extension = String.Empty;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
extension = ".exe";
}
TargetReceipt receipt = new TargetReceipt();
receipt.Configuration = configuration.ToString();
receipt.Launch = FileReference.Combine(projectInfo.LocalRootPath, "Engine", "Binaries", HostPlatform, $"{baseName}{extension}").FullName;
receipt.LaunchCmd = FileReference.Combine(projectInfo.LocalRootPath, "Engine", "Binaries", HostPlatform, $"{baseName}-Cmd{extension}").FullName;
return receipt;
}
private static bool UseSharedEditorReceipt(ProjectInfo projectInfo, ConfigFile projectConfig)
{
string? setting;
if (TryGetProjectSetting(projectConfig, projectInfo.ProjectIdentifier, "UseSharedEditor", out setting))
{
bool value;
if (Boolean.TryParse(setting, out value))
{
return value;
}
}
return false;
}
public static FileReference GetReceiptFile(ProjectInfo projectInfo, ConfigFile projectConfig, FileReference targetFile, string configuration)
{
string targetName = targetFile.GetFileNameWithoutAnyExtensions();
DirectoryReference? projectDir = projectInfo.ProjectDir;
if (projectDir != null && (targetFile.IsUnderDirectory(projectDir) || !UseSharedEditorReceipt(projectInfo, projectConfig)))
{
return GetReceiptFile(projectDir, targetName, configuration);
}
else
{
return GetReceiptFile(projectInfo.EngineDir, targetName, configuration);
}
}
public static FileReference GetReceiptFile(DirectoryReference baseDir, string targetName, string configuration)
{
return GetReceiptFile(baseDir, targetName, HostPlatform, configuration, HostArchitectureSuffix);
}
public static FileReference GetReceiptFile(DirectoryReference baseDir, string targetName, string platform, string configuration, string architectureSuffix)
{
if (String.IsNullOrEmpty(architectureSuffix) && configuration.Equals("Development", StringComparison.OrdinalIgnoreCase))
{
return FileReference.Combine(baseDir, "Binaries", platform, $"{targetName}.target");
}
else
{
return FileReference.Combine(baseDir, "Binaries", platform, $"{targetName}-{platform}-{configuration}{architectureSuffix}.target");
}
}
public static Dictionary<Guid, ConfigObject> GetDefaultBuildStepObjects(ProjectInfo projectInfo, string editorTarget, BuildConfig editorConfig, ConfigFile latestProjectConfigFile, bool shouldSyncPrecompiledEditor)
{
string projectArgument = "";
if (projectInfo.LocalFileName.HasExtension(".uproject"))
{
projectArgument = String.Format("\"{0}\"", projectInfo.LocalFileName);
}
bool useCrashReportClientEditor = latestProjectConfigFile.GetValue("Options.UseCrashReportClientEditor", false);
string hostPlatform;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
hostPlatform = "Mac";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
hostPlatform = "Linux";
}
else
{
hostPlatform = "Win64";
}
List<BuildStep> defaultBuildSteps = new List<BuildStep>();
if (latestProjectConfigFile.GetValue("Options.BuildUnrealHeaderTool", true))
{
defaultBuildSteps.Add(new BuildStep(new Guid("{01F66060-73FA-4CC8-9CB3-E217FBBA954E}"), 0, "Compile UnrealHeaderTool", "Compiling UnrealHeaderTool...", 1, "UnrealHeaderTool", hostPlatform, "Development", "", !shouldSyncPrecompiledEditor));
}
defaultBuildSteps.Add(new BuildStep(new Guid("{F097FF61-C916-4058-8391-35B46C3173D5}"), 1, $"Compile {editorTarget}", $"Compiling {editorTarget}...", 10, editorTarget, hostPlatform, editorConfig.ToString(), projectArgument, !shouldSyncPrecompiledEditor));
defaultBuildSteps.Add(new BuildStep(new Guid("{C6E633A1-956F-4AD3-BC95-6D06D131E7B4}"), 2, "Compile ShaderCompileWorker", "Compiling ShaderCompileWorker...", 1, "ShaderCompileWorker", hostPlatform, "Development", "", !shouldSyncPrecompiledEditor));
defaultBuildSteps.Add(new BuildStep(new Guid("{24FFD88C-7901-4899-9696-AE1066B4B6E8}"), 3, "Compile UnrealLightmass", "Compiling UnrealLightmass...", 1, "UnrealLightmass", hostPlatform, "Development", "", !shouldSyncPrecompiledEditor));
defaultBuildSteps.Add(new BuildStep(new Guid("{FFF20379-06BF-4205-8A3E-C53427736688}"), 4, "Compile CrashReportClient", "Compiling CrashReportClient...", 1, "CrashReportClient", hostPlatform, "Shipping", "", !shouldSyncPrecompiledEditor && !useCrashReportClientEditor));
defaultBuildSteps.Add(new BuildStep(new Guid("{7143D861-58D3-4F83-BADC-BC5DCB2079F6}"), 5, "Compile CrashReportClientEditor", "Compiling CrashReportClientEditor...", 1, "CrashReportClientEditor", hostPlatform, "Shipping", "", !shouldSyncPrecompiledEditor && useCrashReportClientEditor));
return defaultBuildSteps.ToDictionary(x => x.UniqueId, x => x.ToConfigObject());
}
public static Dictionary<string, string> GetWorkspaceVariables(ProjectInfo projectInfo, int changeNumber, int codeChangeNumber, TargetReceipt? editorTarget, ConfigFile? projectConfigFile, IPerforceSettings perforceSettings)
{
Dictionary<string, string> variables = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (projectInfo.StreamName != null)
{
variables.Add("Stream", projectInfo.StreamName);
}
variables.Add("Change", changeNumber.ToString());
variables.Add("CodeChange", codeChangeNumber.ToString());
variables.Add("ClientName", projectInfo.ClientName);
variables.Add("BranchDir", projectInfo.LocalRootPath.FullName);
variables.Add("ProjectDir", projectInfo.LocalFileName.Directory.FullName);
variables.Add("ProjectFile", projectInfo.LocalFileName.FullName);
variables.Add("UseIncrementalBuilds", "1");
string editorConfig = editorTarget?.Configuration ?? String.Empty;
variables.Add("EditorConfig", editorConfig);
string editorLaunch = editorTarget?.Launch ?? String.Empty;
variables.Add("EditorExe", editorLaunch);
string editorLaunchCmd = editorTarget?.LaunchCmd ?? editorLaunch.Replace(".exe", "-Cmd.exe", StringComparison.OrdinalIgnoreCase);
variables.Add("EditorCmdExe", editorLaunchCmd);
// Legacy
variables.Add("UE4EditorConfig", editorConfig);
variables.Add("UE4EditorDebugArg", (editorConfig.Equals("Debug", StringComparison.Ordinal) || editorConfig.Equals("DebugGame", StringComparison.Ordinal)) ? " -debug" : "");
variables.Add("UE4EditorExe", editorLaunch);
variables.Add("UE4EditorCmdExe", editorLaunchCmd);
if (projectConfigFile != null)
{
if (TryGetProjectSetting(projectConfigFile, projectInfo.ProjectIdentifier, "SdkInstallerDir", out string? sdkInstallerDir))
{
variables.Add("SdkInstallerDir", sdkInstallerDir);
}
}
variables.Add("PerforceServerAndPort", perforceSettings.ServerAndPort);
variables.Add("PerforceUserName", perforceSettings.UserName);
if (perforceSettings.ClientName != null)
{
variables.Add("PerforceClientName", perforceSettings.ClientName);
}
return variables;
}
public static Dictionary<string, string> GetWorkspaceVariables(ProjectInfo projectInfo, int changeNumber, int codeChangeNumber, TargetReceipt? editorTarget, ConfigFile? projectConfigFile, IPerforceSettings perforceSettings, IEnumerable<KeyValuePair<string, string>> additionalVariables)
{
Dictionary<string, string> variables = GetWorkspaceVariables(projectInfo, changeNumber, codeChangeNumber, editorTarget, projectConfigFile, perforceSettings);
foreach ((string key, string value) in additionalVariables)
{
variables[key] = value;
}
return variables;
}
public static bool TryGetProjectSetting(ConfigFile projectConfigFile, string selectedProjectIdentifier, string name, [NotNullWhen(true)] out string? value)
{
string path = selectedProjectIdentifier;
for (; ; )
{
ConfigSection? projectSection = projectConfigFile.FindSection(path);
if (projectSection != null)
{
string? newValue = projectSection.GetValue(name, null);
if (newValue != null)
{
value = newValue;
return true;
}
}
int lastSlash = path.LastIndexOf('/');
if (lastSlash < 2)
{
break;
}
path = path.Substring(0, lastSlash);
}
ConfigSection? defaultSection = projectConfigFile.FindSection("Default");
if (defaultSection != null)
{
string? newValue = defaultSection.GetValue(name, null);
if (newValue != null)
{
value = newValue;
return true;
}
}
value = null;
return false;
}
public static void GetProjectSettings(ConfigFile projectConfigFile, string selectedProjectIdentifier, string name, List<string> values)
{
string path = selectedProjectIdentifier;
for (; ; )
{
ConfigSection? projectSection = projectConfigFile.FindSection(path);
if (projectSection != null)
{
values.AddRange(projectSection.GetValues(name, Array.Empty<string>()));
}
int lastSlash = path.LastIndexOf('/');
if (lastSlash < 2)
{
break;
}
path = path.Substring(0, lastSlash);
}
ConfigSection? defaultSection = projectConfigFile.FindSection("Default");
if (defaultSection != null)
{
values.AddRange(defaultSection.GetValues(name, Array.Empty<string>()));
}
}
public static Dictionary<Guid, WorkspaceSyncCategory> GetSyncCategories(ConfigFile projectConfigFile)
{
Dictionary<Guid, WorkspaceSyncCategory> uniqueIdToCategory = new Dictionary<Guid, WorkspaceSyncCategory>();
if (projectConfigFile != null)
{
string[] categoryLines = projectConfigFile.GetValues("Options.SyncCategory", Array.Empty<string>());
foreach (string categoryLine in categoryLines)
{
ConfigObject obj = new ConfigObject(categoryLine);
Guid uniqueId;
if (Guid.TryParse(obj.GetValue("UniqueId", ""), out uniqueId))
{
WorkspaceSyncCategory? category;
if (!uniqueIdToCategory.TryGetValue(uniqueId, out category))
{
category = new WorkspaceSyncCategory(uniqueId);
uniqueIdToCategory.Add(uniqueId, category);
}
if (obj.GetValue("Clear", false))
{
category.Paths.Clear();
category.Requires.Clear();
}
category.Name = obj.GetValue("Name", category.Name);
category.Enable = obj.GetValue("Enable", category.Enable);
string[] paths = Enumerable.Concat(category.Paths, obj.GetValue("Paths", "").Split(';').Select(x => x.Trim())).Where(x => x.Length > 0).Distinct().OrderBy(x => x).ToArray();
category.Paths.Clear();
category.Paths.AddRange(paths);
category.Hidden = obj.GetValue("Hidden", category.Hidden);
Guid[] requires = Enumerable.Concat(category.Requires, ParseGuids(obj.GetValue("Requires", "").Split(';'))).Distinct().OrderBy(x => x).ToArray();
category.Requires.Clear();
category.Requires.AddRange(requires);
}
}
}
return uniqueIdToCategory;
}
public static IDictionary<string, Preset> GetPresets(ConfigFile? projectConfigFile, string projectIdentifier)
{
Dictionary<string, Preset> presetsDictionary = new(StringComparer.OrdinalIgnoreCase);
if (projectConfigFile == null)
{
return presetsDictionary;
}
List<string> presetDefinitions = new List<string>();
presetDefinitions.AddRange(projectConfigFile.GetValues("Presets.Preset", []));
if (TryGetProjectSetting(projectConfigFile, projectIdentifier, "Preset", out string? projectPresets))
{
using StringReader sr = new StringReader(projectPresets);
while (sr.Peek() != -1)
{
string? line = sr.ReadLine();
if (String.IsNullOrWhiteSpace(line))
{
continue;
}
presetDefinitions.Add(line);
}
}
foreach (string presetDefinition in presetDefinitions)
{
ConfigObject obj = new ConfigObject(presetDefinition);
Preset? preset = new Preset();
preset.Name = obj.GetValue("Name", String.Empty);
// do not allow empty role name
if (String.IsNullOrWhiteSpace(preset.Name))
{
continue;
}
IEnumerable<string> categories = obj.GetValue("Categories", String.Empty)
.Split(';')
.Select(x => x.Trim())
.Where(x => x.Length > 0)
.Distinct()
.OrderBy(x => x);
foreach (string categoryLine in categories)
{
string[] values = categoryLine
.Split(',')
.Select(x => x.Trim())
.ToArray();
// all categories shall be completely defined
if (values.Length != 2)
{
continue;
}
RoleCategory category = new();
if (Guid.TryParse(values[0], out Guid guid))
{
category.Id = guid;
}
if (Boolean.TryParse(values[1], out bool enabled))
{
category.Enabled = enabled;
}
if (category.Id != Guid.Empty)
{
preset.Categories.TryAdd(category.Id, category);
}
}
IEnumerable<string> views = obj.GetValue("Views", String.Empty)
.Split(';')
.Select(x => x.Trim())
.Where(x => x.Length > 0)
.Distinct()
;
foreach (string view in views)
{
preset.Views.Add(view);
}
if (presetsDictionary.ContainsKey(preset.Name))
{
presetsDictionary[preset.Name].Import(preset);
}
else
{
presetsDictionary.TryAdd(preset.Name, preset);
}
}
return presetsDictionary;
}
static IEnumerable<Guid> ParseGuids(IEnumerable<string> values)
{
foreach (string value in values)
{
Guid guid;
if (Guid.TryParse(value, out guid))
{
yield return guid;
}
}
}
}
}