Files
UnrealEngine/Engine/Source/Programs/UnrealBuildTool/Platform/Windows/AppXManifestGeneratorBase.cs
2025-05-18 13:04:45 +08:00

592 lines
22 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using EpicGames.Core;
using Microsoft.Extensions.Logging;
using UnrealBuildBase;
namespace UnrealBuildTool
{
/// <summary>
/// Base class for VC appx manifest generation
/// </summary>
public abstract class AppXManifestGeneratorBase
{
/// config section for platform-specific target settings
protected virtual string IniSection_PlatformTargetSettings => String.Format("/Script/{0}PlatformEditor.{0}TargetSettings", Platform.ToString());
/// config section for platform-specific general target settings (i.e. settings with are unrelated to manifest generation)
protected virtual string? IniSection_GeneralPlatformSettings => null;
/// config section for general target settings
protected virtual string IniSection_GeneralProjectSettings => "/Script/EngineSettings.GeneralProjectSettings";
/// default subdirectory for build resources
protected const string BuildResourceSubPath = "Resources";
/// default subdirectory for engine resources
protected const string EngineResourceSubPath = "DefaultImages";
/// Manifest compliance values
protected const int MaxResourceEntries = 200;
/// cached engine ini
protected ConfigHierarchy? EngineIni;
/// cached game ini
protected ConfigHierarchy? GameIni;
/// AppX package resource generator
protected UEAppXResources? AppXResources;
/// the default culture to use
protected string? DefaultAppXCultureId;
/// lookup table for UE's CultureToStage StageId to AppX CultureId
protected Dictionary<string, string> UEStageIdToAppXCultureId = new Dictionary<string, string>();
/// the platform to generate the manifest for
protected UnrealTargetPlatform Platform;
/// project file to use
protected FileReference? ProjectFile;
/// target name to use
protected string? TargetName;
/// Logger for output
protected readonly ILogger Logger;
/// Whether we have logged the deprecation warning for PerCultureResources CultureId being replaced by StageIdOverrides
protected static bool bHasWarnedAboutDeprecatedCultureId = false;
/// CustomConfig to use when reading the ini files
public string CustomConfig = "";
/// <summary>
/// Create a manifest generator for the given platform variant.
/// </summary>
protected AppXManifestGeneratorBase(UnrealTargetPlatform InPlatform, ILogger InLogger)
{
Platform = InPlatform;
Logger = InLogger;
}
/// <summary>
/// Returns a valid version of the given package version string
/// </summary>
protected string? ValidatePackageVersion(string InVersionNumber)
{
string WorkingVersionNumber = Regex.Replace(InVersionNumber, "[^.0-9]", "");
string CompletedVersionString = "";
if (WorkingVersionNumber != null)
{
string[] SplitVersionString = WorkingVersionNumber.Split(new char[] { '.' });
int NumVersionElements = Math.Min(4, SplitVersionString.Length);
for (int VersionElement = 0; VersionElement < NumVersionElements; VersionElement++)
{
string QuadElement = SplitVersionString[VersionElement];
int QuadValue = 0;
if (QuadElement.Length == 0 || !Int32.TryParse(QuadElement, out QuadValue))
{
CompletedVersionString += "0";
}
else
{
if (QuadValue < 0)
{
QuadValue = 0;
}
if (QuadValue > 65535)
{
QuadValue = 65535;
}
CompletedVersionString += QuadValue;
}
if (VersionElement < 3)
{
CompletedVersionString += ".";
}
}
for (int VersionElement = NumVersionElements; VersionElement < 4; VersionElement++)
{
CompletedVersionString += "0";
if (VersionElement < 3)
{
CompletedVersionString += ".";
}
}
}
if (CompletedVersionString == null || CompletedVersionString.Length <= 0)
{
Logger.LogError("Invalid package version {Ver}. Package versions must be in the format #.#.#.# where # is a number 0-65535.", InVersionNumber);
Logger.LogError("Consider setting [{IniSection}]:PackageVersion to provide a specific value.", IniSection_PlatformTargetSettings);
}
return CompletedVersionString;
}
/// <summary>
/// Returns a valid version of the given application id
/// </summary>
protected string? ValidateProjectBaseName(string InApplicationId)
{
string ReturnVal = Regex.Replace(InApplicationId, "[^A-Za-z0-9]", "");
if (ReturnVal != null)
{
// Remove any leading numbers (must start with a letter)
ReturnVal = Regex.Replace(ReturnVal, "^[0-9]*", "");
}
if (ReturnVal == null || ReturnVal.Length <= 0)
{
Logger.LogError("Invalid application ID {AppId}. Application IDs must only contain letters and numbers. And they must begin with a letter.", InApplicationId);
}
return ReturnVal;
}
/// <summary>
/// Reads an integer from the cached ini files
/// </summary>
[return: NotNullIfNotNull("DefaultValue")]
protected string? ReadIniString(string? Key, string Section, string? DefaultValue = null)
{
if (Key == null)
{
return DefaultValue;
}
string Value;
if (GameIni!.GetString(Section, Key, out Value) && !String.IsNullOrWhiteSpace(Value))
{
return Value;
}
if (EngineIni!.GetString(Section, Key, out Value) && !String.IsNullOrWhiteSpace(Value))
{
return Value;
}
return DefaultValue;
}
/// <summary>
/// Reads a string from the cached ini files
/// </summary>
[return: NotNullIfNotNull("DefaultValue")]
protected string? GetConfigString(string PlatformKey, string? GenericKey, string? DefaultValue = null)
{
string? GeneralPlatformValue = (IniSection_GeneralPlatformSettings != null) ? ReadIniString(PlatformKey, IniSection_GeneralPlatformSettings) : null;
string? GenericValue = ReadIniString(GenericKey, IniSection_GeneralProjectSettings, DefaultValue);
return GeneralPlatformValue ?? ReadIniString(PlatformKey, IniSection_PlatformTargetSettings, GenericValue);
}
/// <summary>
/// Reads a bool from the cached ini files
/// </summary>
protected bool GetConfigBool(string PlatformKey, string? GenericKey, bool DefaultValue = false)
{
string? GeneralPlatformValue = (IniSection_GeneralPlatformSettings != null) ? ReadIniString(PlatformKey, IniSection_GeneralPlatformSettings) : null;
string? GenericValue = ReadIniString(GenericKey, IniSection_GeneralProjectSettings, null);
string? ResultStr = GeneralPlatformValue ?? ReadIniString(PlatformKey, IniSection_PlatformTargetSettings, GenericValue);
if (ResultStr == null)
{
return DefaultValue;
}
ResultStr = ResultStr.Trim().ToLower();
return ResultStr == "true" || ResultStr == "1" || ResultStr == "yes";
}
/// <summary>
/// Reads a color from the cached ini files
/// </summary>
protected string GetConfigColor(string PlatformConfigKey, string DefaultValue)
{
string? ConfigValue = GetConfigString(PlatformConfigKey, null, null);
if (ConfigValue == null)
{
return DefaultValue;
}
Dictionary<string, string>? Pairs;
int R, G, B;
if (ConfigHierarchy.TryParse(ConfigValue, out Pairs) &&
Int32.TryParse(Pairs["R"], out R) &&
Int32.TryParse(Pairs["G"], out G) &&
Int32.TryParse(Pairs["B"], out B))
{
return "#" + R.ToString("X2") + G.ToString("X2") + B.ToString("X2");
}
Logger.LogWarning("Failed to parse color config value. Using default.");
return DefaultValue;
}
/// <summary>
/// Create all the localization data. Returns whether there is any per-culture data set up
/// </summary>
protected virtual bool BuildLocalizationData()
{
bool bHasPerCultureResources = false;
// reset per-culture strings and make sure the default culture entry exists
AppXResources!.ClearStrings();
// add all default strings
if (EngineIni!.GetString(IniSection_PlatformTargetSettings, "CultureStringResources", out string DefaultCultureScratchValue) && ConfigHierarchy.TryParse(DefaultCultureScratchValue, out Dictionary<string, string>? DefaultStrings))
{
AppXResources!.AddDefaultStrings(DefaultStrings);
}
// read StageId overrides
bool bHasStageIdOverrides = false;
if (EngineIni!.GetString(IniSection_PlatformTargetSettings, "StageIdOverrides", out string? StageIdOverridesString) &&
ConfigHierarchy.TryParseAsMap(StageIdOverridesString, out Dictionary<string, string>? StageIdOverrides))
{
bHasStageIdOverrides = true;
UEStageIdToAppXCultureId = StageIdOverrides;
AppXResources!.AddCultures(UEStageIdToAppXCultureId.Values);
}
// add per culture strings
if (EngineIni.GetArray(IniSection_PlatformTargetSettings, "PerCultureResources", out List<string>? PerCultureResources))
{
bHasPerCultureResources = true;
foreach (string CultureResources in PerCultureResources)
{
if (!ConfigHierarchy.TryParse(CultureResources, out Dictionary<string, string>? CultureProperties)
|| !CultureProperties.ContainsKey("CultureStringResources")
|| !CultureProperties.ContainsKey("StageId"))
{
Logger.LogWarning("Invalid per-culture resource value: {Culture}", CultureResources);
continue;
}
string StageId = CultureProperties["StageId"];
if (String.IsNullOrEmpty(StageId))
{
Logger.LogWarning("Missing StageId value: {Culture}", CultureResources);
continue;
}
string CultureId = "";
if (bHasStageIdOverrides)
{
CultureId = UEStageIdToAppXCultureId.ContainsKey(StageId) ? UEStageIdToAppXCultureId[StageId] : StageId;
}
else if (!CultureProperties.ContainsKey("CultureId"))
{
Logger.LogWarning("Invalid per-culture resource value: {Culture}", CultureResources);
}
else
{
CultureId = CultureProperties["CultureId"];
if (String.IsNullOrEmpty(CultureId))
{
Logger.LogWarning("Missing CultureId value: {Culture}", CultureResources);
continue;
}
// PerCultureResources CultureId is deprecated in favor of new StageIdOverrides property
// if the CultureId field contains an overridden property, warn they need to update the data
if (CultureId != StageId && !bHasWarnedAboutDeprecatedCultureId)
{
Logger.LogWarning("PerCultureResources is out of date - please re-save this project's {Platform}Engine.ini in the editor to update StageIdOverrides. This must be done before UE5.5 to avoid losing data to deprecation", Platform);
bHasWarnedAboutDeprecatedCultureId = true;
}
UEStageIdToAppXCultureId[StageId] = CultureId;
}
AppXResources.AddCulture(CultureId);
// read culture strings
if (!ConfigHierarchy.TryParse(CultureProperties["CultureStringResources"], out Dictionary<string, string>? CultureStringResources))
{
Logger.LogError("Invalid culture string resources: \"{Culture}\". Unable to add resource entry.", CultureResources);
continue;
}
AppXResources!.AddCultureStrings(CultureId, CultureStringResources);
}
}
return bHasPerCultureResources;
}
/// <summary>
/// Register the locations where resource binary files can be found
/// </summary>
protected virtual void PrepareResourceBinaryPaths()
{
if (ProjectFile != null)
{
AppXResources!.ProjectBinaryResourceDirectories.Add(DirectoryReference.Combine(ProjectFile.Directory, "Build", Platform.ToString(), BuildResourceSubPath));
AppXResources!.ProjectBinaryResourceDirectories.Add(DirectoryReference.Combine(ProjectFile.Directory, "Platforms", Platform.ToString(), "Build", BuildResourceSubPath));
}
AppXResources!.EngineFallbackBinaryResourceDirectories.Add(DirectoryReference.Combine(Unreal.EngineDirectory, "Build", Platform.ToString(), EngineResourceSubPath));
AppXResources!.EngineFallbackBinaryResourceDirectories.Add(DirectoryReference.Combine(Unreal.EngineDirectory, "Platforms", Platform.ToString(), "Build", EngineResourceSubPath));
}
/// <summary>
/// Get the resources element
/// </summary>
protected XElement GetResources()
{
List<string> ResourceCulturesList = AppXResources!.GetAllCultureIds().ToList();
// Move the default culture to the front of the list
ResourceCulturesList.Remove(DefaultAppXCultureId!);
ResourceCulturesList.Insert(0, DefaultAppXCultureId!);
// Check that we have a valid number of cultures
if (ResourceCulturesList!.Count < 1 || ResourceCulturesList.Count >= MaxResourceEntries)
{
Logger.LogWarning("Incorrect number of cultures to stage. There must be between 1 and {MaxCultures} cultures selected.", MaxResourceEntries);
}
// Create the culture list. This list is unordered except that the default language must be first which we already took care of above.
IEnumerable<XElement> CultureElements = ResourceCulturesList.Select(c =>
new XElement("Resource", new XAttribute("Language", c)));
return new XElement("Resources", CultureElements);
}
/// <summary>
/// Get the package identity name string
/// </summary>
protected virtual string GetIdentityPackageName()
{
// Read the PackageName from config
string DefaultName = (ProjectFile != null) ? ProjectFile.GetFileNameWithoutAnyExtensions() : (TargetName ?? "DefaultUEProject");
string PackageName = Regex.Replace(GetConfigString("PackageName", "ProjectName", DefaultName), "[^-.A-Za-z0-9]", "");
if (String.IsNullOrWhiteSpace(PackageName))
{
Logger.LogError("Invalid package name {Name}. Package names must only contain letters, numbers, dash, and period and must be at least one character long.", PackageName);
Logger.LogError("Consider using the setting [{IniSection}]:PackageName to provide a specific value.", IniSection_PlatformTargetSettings);
}
// If specified in the project settings append the users machine name onto the package name to allow sharing of devkits without stomping of deploys
bool bPackageNameUseMachineName;
if (EngineIni!.GetBool(IniSection_PlatformTargetSettings, "bPackageNameUseMachineName", out bPackageNameUseMachineName) && bPackageNameUseMachineName)
{
string MachineName = Regex.Replace(Unreal.MachineName, "[^-.A-Za-z0-9]", "");
PackageName = PackageName + ".NOT.SHIPPABLE." + MachineName;
}
return PackageName;
}
/// <summary>
/// Get the publisher name string
/// </summary>
protected virtual string GetIdentityPublisherName()
{
string PublisherName = GetConfigString("PublisherName", "CompanyDistinguishedName", "CN=NoPublisher");
return PublisherName;
}
/// <summary>
/// Get the package version string
/// </summary>
protected virtual string? GetIdentityVersionNumber()
{
string? VersionNumber = GetConfigString("PackageVersion", "ProjectVersion", "1.0.0.0");
VersionNumber = ValidatePackageVersion(VersionNumber);
// If specified in the project settings attempt to retrieve the current build number and increment the version number by that amount, accounting for overflows
bool bIncludeEngineVersionInPackageVersion;
if (EngineIni!.GetBool(IniSection_PlatformTargetSettings, "bIncludeEngineVersionInPackageVersion", out bIncludeEngineVersionInPackageVersion) && bIncludeEngineVersionInPackageVersion)
{
VersionNumber = IncludeBuildVersionInPackageVersion(VersionNumber);
}
return VersionNumber;
}
/// <summary>
/// Get the package identity element
/// </summary>
protected XElement GetIdentity(out string IdentityName)
{
string PackageName = GetIdentityPackageName();
string PublisherName = GetIdentityPublisherName();
string? VersionNumber = GetIdentityVersionNumber();
IdentityName = PackageName;
return new XElement("Identity",
new XAttribute("Name", PackageName),
new XAttribute("Publisher", PublisherName),
new XAttribute("Version", VersionNumber!));
}
/// <summary>
/// Updates the given package version to include the engine build version, if requested
/// </summary>
protected virtual string? IncludeBuildVersionInPackageVersion(string? VersionNumber)
{
BuildVersion? BuildVersionForPackage;
if (VersionNumber != null && BuildVersion.TryRead(BuildVersion.GetDefaultFileName(), out BuildVersionForPackage) && BuildVersionForPackage.Changelist != 0)
{
// Break apart the version number into individual elements
string[] SplitVersionString = VersionNumber.Split('.');
VersionNumber = String.Format("{0}.{1}.{2}.{3}",
SplitVersionString[0],
SplitVersionString[1],
BuildVersionForPackage.Changelist / 10000,
BuildVersionForPackage.Changelist % 10000);
}
return VersionNumber;
}
/// <summary>
/// Get the path to the makepri.exe tool
/// </summary>
protected virtual FileReference GetMakePriBinaryPath()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
throw new BuildException("unsupported platform");
}
if (!MicrosoftPlatformSDK.TryGetWindowsSdkDir(null, Logger, out VersionNumber? SdkVersion, out DirectoryReference? SdkDir))
{
throw new BuildException("Cannot get default Windows Sdk directory");
}
FileReference MakePriPath = FileReference.Combine(SdkDir, "bin", SdkVersion.ToString(), "x64", "makepri.exe");
if (!FileReference.Exists(MakePriPath))
{
throw new BuildException($"{MakePriPath} - file not found");
}
return MakePriPath;
}
/// <summary>
/// Get any additional platform-specific parameters for makepri.exe
/// </summary>
protected virtual string GetMakePriExtraCommandLine()
{
return "";
}
/// <summary>
/// Return the entire manifest element
/// </summary>
protected abstract XElement GetManifest(Dictionary<UnrealTargetConfiguration, string> InExecutablePairs, out string IdentityName);
/// <summary>
/// Perform any platform-specific processing on the manifest before it is saved
/// </summary>
protected virtual void ProcessManifest(Dictionary<UnrealTargetConfiguration, string> InExecutablePairs, string ManifestName, string ManifestTargetPath, string ManifestIntermediatePath)
{
}
/// <summary>
/// Perform any additional initialization once all parameters and configuration are ready
/// </summary>
protected virtual void PostConfigurationInit()
{
}
/// <summary>
/// Create a manifest and return the list of modified files
/// </summary>
public List<string>? CreateManifest(string InManifestName, DirectoryReference OutputDirectory, string? InTargetName, FileReference? InProjectFile, Dictionary<UnrealTargetConfiguration, string> InExecutablePairs)
{
FileUtils.CreateDirectoryTree(OutputDirectory);
DirectoryReference ProjectRoot = (InProjectFile != null) ? InProjectFile.Directory : Unreal.EngineDirectory;
DirectoryReference IntermediateDirectory = DirectoryReference.Combine(ProjectRoot, "Intermediate", "Manifest", Platform.ToString());
FileUtils.ForceDeleteDirectory(IntermediateDirectory);
FileUtils.CreateDirectoryTree(IntermediateDirectory);
TargetName = InTargetName;
ProjectFile = InProjectFile;
AppXResources = new(Logger, GetMakePriBinaryPath());
PrepareResourceBinaryPaths();
// Load up INI settings. We'll use engine settings to retrieve the manifest configuration, but these may reference
// values in either game or engine settings, so we'll keep both.
GameIni = ConfigCache.ReadHierarchy(ConfigHierarchyType.Game, DirectoryReference.FromFile(InProjectFile), Platform, CustomConfig);
EngineIni = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, DirectoryReference.FromFile(InProjectFile), Platform, CustomConfig);
PostConfigurationInit();
// Load and verify/clean culture list
List<string>? SelectedUECultureIds;
string DefaultUECultureId;
GameIni.GetArray("/Script/UnrealEd.ProjectPackagingSettings", "CulturesToStage", out SelectedUECultureIds);
GameIni.GetString("/Script/UnrealEd.ProjectPackagingSettings", "DefaultCulture", out DefaultUECultureId);
if (SelectedUECultureIds == null || SelectedUECultureIds.Count < 1)
{
Logger.LogError("At least one culture must be selected to stage.");
return null;
}
SelectedUECultureIds = SelectedUECultureIds.Distinct().ToList();
if (DefaultUECultureId == null || DefaultUECultureId.Length < 1)
{
DefaultUECultureId = SelectedUECultureIds[0];
Logger.LogWarning("A default culture must be selected to stage. Using {DefaultCulture}.", DefaultUECultureId);
}
if (!SelectedUECultureIds.Contains(DefaultUECultureId))
{
DefaultUECultureId = SelectedUECultureIds[0];
Logger.LogWarning("The default culture must be one of the staged cultures. Using {DefaultCulture}.", DefaultUECultureId);
}
BuildLocalizationData();
// generate the list of AppX cultures to stage
foreach (string UEStageId in SelectedUECultureIds)
{
if (!UEStageIdToAppXCultureId.TryGetValue(UEStageId, out string? AppXCultureId) || String.IsNullOrEmpty(AppXCultureId))
{
// use the culture directly - no remapping required
AppXCultureId = UEStageId;
UEStageIdToAppXCultureId[UEStageId] = AppXCultureId;
AppXResources.AddCulture(UEStageId);
}
}
// look up the default AppX culture
if (!UEStageIdToAppXCultureId.TryGetValue(DefaultUECultureId, out DefaultAppXCultureId) || String.IsNullOrEmpty(DefaultAppXCultureId))
{
// use the default culture directly - no remapping required
DefaultAppXCultureId = DefaultUECultureId;
UEStageIdToAppXCultureId[DefaultUECultureId] = DefaultAppXCultureId;
}
// Create the manifest document
string? IdentityName = null;
XDocument ManifestXmlDocument = new XDocument(GetManifest(InExecutablePairs, out IdentityName));
// Export manifest to the intermediate directory and add it to the manifest resources files for copying
FileReference ManifestIntermediateFile = FileReference.Combine(IntermediateDirectory, InManifestName);
FileReference ManifestTargetFile = FileReference.Combine(OutputDirectory, InManifestName);
ManifestXmlDocument.Save(ManifestIntermediateFile.FullName);
AppXResources.AddFileReference(ManifestIntermediateFile, InManifestName);
ProcessManifest(InExecutablePairs, InManifestName, ManifestTargetFile.FullName, ManifestIntermediateFile.FullName);
// Generate the package resource index and copy all resource files to the output
FileReference ManifestTargetPath = FileReference.Combine(OutputDirectory, InManifestName);
List<FileReference> UpdatedFiles = AppXResources.GenerateAppXResources(OutputDirectory, IntermediateDirectory, ManifestTargetFile, DefaultAppXCultureId, IdentityName);
// Clean up and reutrn the list of updated files
FileUtils.ForceDeleteDirectory(IntermediateDirectory);
return UpdatedFiles.ConvertAll(X => X.FullName);
}
}
}