// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading.Tasks;
using EpicGames.Core;
using Microsoft.Extensions.Logging;
using UnrealBuildBase;
namespace UnrealBuildTool
{
///
/// Parameters for the WriteMetadata mode
///
[Serializable]
class WriteMetadataTargetInfo
{
///
/// The project file
///
public FileReference? ProjectFile;
///
/// Output location for the version file
///
public FileReference? VersionFile;
///
/// The new version to write. This should only be set on the engine step.
///
public BuildVersion? Version;
///
/// Output location for the target file
///
public FileReference? ReceiptFile;
///
/// The new receipt to write. This should only be set on the target step.
///
public TargetReceipt? Receipt;
///
/// Map of module manifest filenames to their location on disk.
///
public Dictionary FileToManifest;
///
/// Map of load order manifest filenames to their locations on disk (generally, at most one load oder manifest is expected).
///
public Dictionary FileToLoadOrderManifest;
///
/// Constructor
///
///
///
///
///
///
///
///
public WriteMetadataTargetInfo(FileReference? ProjectFile, FileReference? VersionFile, BuildVersion? Version, FileReference? ReceiptFile, TargetReceipt? Receipt, Dictionary FileToManifest, Dictionary? FileToLoadOrderManifest)
{
this.ProjectFile = ProjectFile;
this.VersionFile = VersionFile;
this.Version = Version;
this.ReceiptFile = ReceiptFile;
this.Receipt = Receipt;
this.FileToManifest = FileToManifest;
this.FileToLoadOrderManifest = FileToLoadOrderManifest ?? new Dictionary();
}
}
///
/// Writes all metadata files at the end of a build (receipts, version files, etc...). This is implemented as a separate mode to allow it to be done as part of the action graph.
///
[ToolMode("WriteMetadata", ToolModeOptions.None)]
class WriteMetadataMode : ToolMode
{
///
/// Version number for output files. This is not used directly, but can be appended to command-line invocations of the tool to ensure that actions to generate metadata are updated if the output format changes.
/// The action graph is regenerated whenever UBT is rebuilt, so this should always match.
///
public const int CurrentVersionNumber = 2;
///
/// Execute the command
///
/// Command line arguments
/// Exit code
///
public override Task ExecuteAsync(CommandLineArguments Arguments, ILogger Logger)
{
// Acquire a different mutex to the regular UBT instance, since this mode will be called as part of a build. We need the mutex to ensure that building two modular configurations
// in parallel don't clash over writing shared *.modules files (eg. DebugGame and Development editors).
string MutexName = GlobalSingleInstanceMutex.GetUniqueMutexForPath("UnrealBuildTool_WriteMetadata", Unreal.RootDirectory);
using (new GlobalSingleInstanceMutex(MutexName, true))
{
return ExecuteInternal(Arguments, Logger);
}
}
///
/// Execute the command, having obtained the appropriate mutex
///
/// Command line arguments
/// Logger for output
/// Exit code
private Task ExecuteInternal(CommandLineArguments Arguments, ILogger Logger)
{
// Read the target info
WriteMetadataTargetInfo TargetInfo = BinaryFormatterUtils.Load(Arguments.GetFileReference("-Input="));
bool bNoManifestChanges = Arguments.HasOption("-NoManifestChanges");
int VersionNumber = Arguments.GetInteger("-Version=");
Arguments.CheckAllArgumentsUsed();
// Make sure the version number is correct
if (VersionNumber != CurrentVersionNumber)
{
throw new BuildException("Version number to WriteMetadataMode is incorrect (expected {0}, got {1})", CurrentVersionNumber, VersionNumber);
}
// Get the build id to use
string? BuildId;
if (TargetInfo.Version != null && !String.IsNullOrEmpty(TargetInfo.Version.BuildId))
{
BuildId = TargetInfo.Version.BuildId;
}
else if (TargetInfo.Receipt != null && !String.IsNullOrEmpty(TargetInfo.Receipt.Version.BuildId))
{
BuildId = TargetInfo.Receipt.Version.BuildId;
}
else if (TargetInfo.VersionFile != null && BuildVersion.TryRead(TargetInfo.VersionFile, out BuildVersion? PrevVersion) && CanRecycleBuildId(PrevVersion.BuildId, TargetInfo.FileToManifest, Logger))
{
BuildId = PrevVersion.BuildId;
}
else
{
BuildId = Guid.NewGuid().ToString();
}
// Read all the existing manifests and merge them into the new ones if they have the same build id
foreach (KeyValuePair Pair in TargetInfo.FileToManifest)
{
ModuleManifest? SourceManifest;
if (TryReadManifest(Pair.Key, Logger, out SourceManifest) && SourceManifest.BuildId == BuildId)
{
MergeManifests(SourceManifest, Pair.Value);
}
}
// Update the build id in all the manifests, and write them out
foreach (KeyValuePair Pair in TargetInfo.FileToManifest)
{
FileReference ManifestFile = Pair.Key;
if (!Unreal.IsFileInstalled(ManifestFile))
{
ModuleManifest Manifest = Pair.Value;
Manifest.BuildId = BuildId ?? String.Empty;
if (!FileReference.Exists(ManifestFile))
{
// If the file doesn't already exist, just write it out
DirectoryReference.CreateDirectory(ManifestFile.Directory);
Manifest.Write(ManifestFile);
}
else
{
// Otherwise write it to a buffer first
string OutputText;
using (StringWriter Writer = new StringWriter())
{
Manifest.Write(Writer);
OutputText = Writer.ToString();
}
// Check if the manifest has changed. Note that if a manifest is out of date, we should have generated a new build id causing the contents to differ.
if (bNoManifestChanges)
{
string CurrentText = FileReference.ReadAllText(ManifestFile);
if (CurrentText != OutputText)
{
Logger.LogError("Build modifies {File}. This is not permitted. Before:\n {OldFile}\nAfter:\n {NewFile}", ManifestFile, CurrentText.Replace("\n", "\n "), OutputText.Replace("\n", "\n "));
}
}
// Write it to disk
FileReference.WriteAllText(ManifestFile, OutputText);
}
}
}
// Write load order manifests out.
foreach (KeyValuePair Pair in TargetInfo.FileToLoadOrderManifest)
{
FileReference ManifestFile = Pair.Key;
if (!Unreal.IsFileInstalled(ManifestFile))
{
LoadOrderManifest Manifest = Pair.Value;
if (!FileReference.Exists(ManifestFile))
{
// If the file doesn't already exist, just write it out
DirectoryReference.CreateDirectory(ManifestFile.Directory);
Manifest.Write(ManifestFile);
}
else
{
// Otherwise write it to a buffer first
string OutputText;
using (StringWriter Writer = new StringWriter())
{
Manifest.Write(Writer);
OutputText = Writer.ToString();
}
// Check if the manifest has changed. Note that if a manifest is out of date, we should have generated a new build id causing the contents to differ.
if (bNoManifestChanges)
{
string CurrentText = FileReference.ReadAllText(ManifestFile);
if (CurrentText != OutputText)
{
Logger.LogError("Build modifies {File}. This is not permitted. Before:\n {OldFile}\nAfter:\n {NewFile}", ManifestFile, CurrentText.Replace("\n", "\n "), OutputText.Replace("\n", "\n "));
}
}
// Write it to disk
FileReference.WriteAllText(ManifestFile, OutputText);
}
}
}
// Write out the version file
if (TargetInfo.Version != null && TargetInfo.VersionFile != null)
{
DirectoryReference.CreateDirectory(TargetInfo.VersionFile.Directory);
TargetInfo.Version.BuildId = BuildId;
TargetInfo.Version.Write(TargetInfo.VersionFile);
}
// Write out the receipt
if (TargetInfo.Receipt != null && TargetInfo.ReceiptFile != null)
{
DirectoryReference.CreateDirectory(TargetInfo.ReceiptFile.Directory);
TargetInfo.Receipt.Version.BuildId = BuildId;
TargetInfo.Receipt.Write(TargetInfo.ReceiptFile);
}
return Task.FromResult(0);
}
///
/// Checks if this
///
///
///
///
///
bool CanRecycleBuildId(string? BuildId, Dictionary FileToManifest, ILogger Logger)
{
foreach (FileReference ManifestFileName in FileToManifest.Keys)
{
ModuleManifest? Manifest;
if (ManifestFileName.IsUnderDirectory(Unreal.EngineDirectory) && TryReadManifest(ManifestFileName, Logger, out Manifest) && Manifest.BuildId == BuildId)
{
DateTime ManifestTime = FileReference.GetLastWriteTimeUtc(ManifestFileName);
foreach (string FileName in Manifest.ModuleNameToFileName.Values)
{
FileInfo ModuleInfo = new FileInfo(FileReference.Combine(ManifestFileName.Directory, FileName).FullName);
if (!ModuleInfo.Exists || ModuleInfo.LastWriteTimeUtc > ManifestTime)
{
return false;
}
}
}
}
return true;
}
///
/// Attempts to read a manifest from the given location
///
/// Path to the manifest
///
/// If successful, receives the manifest that was read
/// True if the manifest was read correctly, false otherwise
public static bool TryReadManifest(FileReference ManifestFileName, ILogger Logger, [NotNullWhen(true)] out ModuleManifest? Manifest)
{
if (FileReference.Exists(ManifestFileName))
{
try
{
Manifest = ModuleManifest.Read(ManifestFileName);
return true;
}
catch (Exception Ex)
{
Logger.LogWarning("Unable to read '{ManifestFileName}'; ignoring.", ManifestFileName);
Logger.LogInformation("{Ex}", ExceptionUtils.FormatExceptionDetails(Ex));
}
}
Manifest = null;
return false;
}
///
/// Merge a manifest into another manifest
///
/// The source manifest
/// The target manifest to merge into
static void MergeManifests(ModuleManifest SourceManifest, ModuleManifest TargetManifest)
{
foreach (KeyValuePair ModulePair in SourceManifest.ModuleNameToFileName)
{
if (!TargetManifest.ModuleNameToFileName.ContainsKey(ModulePair.Key))
{
TargetManifest.ModuleNameToFileName.Add(ModulePair.Key, ModulePair.Value);
}
}
}
}
}