// Copyright Epic Games, Inc. All Rights Reserved.
using EpicGames.Core;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using UnrealBuildBase;
namespace AutomationTool
{
///
/// Commandlet to remove duplicate files in the UAT script directories
///
[Help("Removes duplicate binaries in UAT script directories that exist in AutomationTool or AutomationUtils")]
class DedupeAutomationScripts : BuildCommand
{
///
public override void ExecuteBuild()
{
DirectoryReference scriptModuleDir = DirectoryReference.Combine(Unreal.EngineDirectory, "Intermediate", "ScriptModules");
Log.Logger.LogInformation("Scanning for files shared with AutomationTool or AutomationUtils...");
ParallelQuery records = DirectoryReference.EnumerateFiles(scriptModuleDir, "*.Automation.json")
.AsParallel()
.Select(async x => JsonSerializer.Deserialize(await FileReference.ReadAllTextAsync(x)))
.Select(x => x.Result);
DirectoryReference uatDir = DirectoryReference.Combine(Unreal.EngineDirectory, "Binaries", "DotNET", "AutomationTool");
HashSet uatFiles = [
.. DirectoryReference.EnumerateFiles(uatDir).Select(x => x.MakeRelativeTo(uatDir)),
.. DirectoryReference.EnumerateFiles(DirectoryReference.Combine(uatDir, "runtimes"), "*", SearchOption.AllDirectories).Select(x => x.MakeRelativeTo(uatDir)),
];
DirectoryReference utilsDir = DirectoryReference.Combine(Unreal.EngineDirectory, "Binaries", "DotNET", "AutomationTool", "AutomationUtils");
HashSet utilsFiles = [.. DirectoryReference.EnumerateFiles(utilsDir).Select(x => x.MakeRelativeTo(utilsDir))];
if (DirectoryReference.Exists(DirectoryReference.Combine(utilsDir, "runtimes")))
{
utilsFiles.UnionWith(DirectoryReference.EnumerateFiles(DirectoryReference.Combine(utilsDir, "runtimes"), "*", SearchOption.AllDirectories).Select(x => x.MakeRelativeTo(utilsDir)));
}
ParallelQuery fileToDelete = records.AsParallel().SelectMany(record =>
{
FileReference projectPath = FileReference.Combine(scriptModuleDir, record.ProjectPath);
FileReference targetPath = FileReference.Combine(projectPath.Directory, record.TargetPath);
IEnumerable targetFiles = DirectoryReference.EnumerateFiles(targetPath.Directory, "*", SearchOption.AllDirectories);
if (targetPath.Directory != utilsDir)
{
return [
.. targetFiles.Where(x => uatFiles.Contains(x.MakeRelativeTo(targetPath.Directory))),
.. targetFiles.Where(x => utilsFiles.Contains(x.MakeRelativeTo(targetPath.Directory)))
];
}
return targetFiles.Where(x => uatFiles.Contains(x.MakeRelativeTo(targetPath.Directory)));
}).Distinct();
ParallelQuery possiblyEmptyFolders = records.AsParallel().SelectMany(record =>
{
FileReference projectPath = FileReference.Combine(scriptModuleDir, record.ProjectPath);
FileReference targetPath = FileReference.Combine(projectPath.Directory, record.TargetPath);
return DirectoryReference.EnumerateDirectories(targetPath.Directory, "*", SearchOption.AllDirectories);
}).Distinct();
int deletedFiles = 0;
int deletedFolders = 0;
long totalBytes = 0;
foreach (FileReference file in fileToDelete.Order())
{
long bytes = file.ToFileInfo().Length;
Interlocked.Add(ref totalBytes, bytes);
Interlocked.Increment(ref deletedFiles);
FileReference.Delete(file);
Log.Logger.LogDebug("Deleted {File} ({Size})", file, StringUtils.FormatBytesString(bytes));
}
foreach (DirectoryReference folder in possiblyEmptyFolders.Order().Reverse())
{
IEnumerable entries = [
.. DirectoryReference.EnumerateFiles(folder),
.. DirectoryReference.EnumerateDirectories(folder)
];
if (!entries.Any())
{
Interlocked.Increment(ref deletedFolders);
DirectoryReference.Delete(folder);
Log.Logger.LogDebug("Deleted empty directory {Directory}", folder);
}
}
Log.Logger.LogInformation("Deleted {FileCount} files(s) + {FolderCount} empty folder(s) ({TotalSize})", deletedFiles, deletedFolders, StringUtils.FormatBytesString(totalBytes));
}
}
}