// Copyright Epic Games, Inc. All Rights Reserved.
using EpicGames.Core;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using EpicGames.MsBuild;
using Microsoft.Extensions.Logging;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.FileSystemGlobbing;
namespace UnrealBuildBase
{
public static class CompileScriptModule
{
[Flags]
public enum BuildFlags
{
///
/// No build flags specified
///
None = 0,
///
/// Do not allow compiles of any projects
///
NoCompile = 1 << 0,
///
/// If not compiling, generate error if target does not exist
///
ErrorOnMissingTarget = 1 << 1,
///
/// When NoCompile options are specified, this flag will test to see if the build records
/// are valid. But the results of that test are currently ignored. Only the existence of the
/// target path is considered.
///
/// When NoCompile options are not specified, then the validity of the build records will be used
/// to test to see if project files need building. If ForceCompile is specified, then this flag
/// serves no useful purpose. If this flag is not specified, then ForceCompile is assumed.
///
UseBuildRecords = 1 << 2,
///
/// Force compile when the NoCompile options aren't specified regardless of the
/// state of the build records.
///
ForceCompile = 1 << 3,
}
class Hook : CsProjBuildHook
{
private ILogger Logger;
private Dictionary WriteTimes = new Dictionary();
private string BuildRecordDirectory;
public Hook(ILogger InLogger, Rules.RulesFileType InRulesFileType)
{
Logger = InLogger;
switch (InRulesFileType)
{
case Rules.RulesFileType.AutomationModule:
BuildRecordDirectory = "ScriptModules";
break;
case Rules.RulesFileType.UbtPlugin:
BuildRecordDirectory = "ScriptModules.UBT";
break;
default:
throw new ArgumentException("Unexpected rules file type");
}
}
public DateTime GetLastWriteTime(DirectoryReference BasePath, string RelativeFilePath)
{
return GetLastWriteTime(BasePath.FullName, RelativeFilePath);
}
public DateTime GetLastWriteTime(string BasePath, string RelativeFilePath)
{
string NormalizedPath = Path.GetFullPath(RelativeFilePath, BasePath);
if (!WriteTimes.TryGetValue(NormalizedPath, out DateTime WriteTime))
{
WriteTimes.Add(NormalizedPath, WriteTime = File.GetLastWriteTime(NormalizedPath));
}
return WriteTime;
}
public DirectoryReference GetBuildRecordDirectory(DirectoryReference BasePath)
{
return DirectoryReference.Combine(BasePath, "Intermediate", BuildRecordDirectory);
}
public void ValidateRecursively(Dictionary BuildRecords, FileReference ProjectPath)
{
CompileScriptModule.ValidateBuildRecordRecursively(BuildRecords, ProjectPath, this, Logger);
}
private static readonly char[] s_wildcardCharacters = { '*', '?' };
public bool HasWildcards(string FileSpec)
{
// Perf Note: Doing a [Last]IndexOfAny(...) is much faster than compiling a
// regular expression that does the same thing, regardless of whether
// filespec contains one of the characters.
// Choose LastIndexOfAny instead of IndexOfAny because it seems more likely
// that wildcards will tend to be towards the right side.
return -1 != FileSpec.LastIndexOfAny(s_wildcardCharacters);
}
DirectoryReference CsProjBuildHook.EngineDirectory => Unreal.EngineDirectory;
DirectoryReference CsProjBuildHook.DotnetDirectory => Unreal.DotnetDirectory;
FileReference CsProjBuildHook.DotnetPath => Unreal.DotnetPath;
}
///
/// Return the target paths from the collection of build records
///
/// Input build records
/// Set of target files
public static HashSet GetTargetPaths(IReadOnlyDictionary BuildRecords)
{
return new HashSet(BuildRecords.Select(x => GetTargetPath(x)));
}
///
/// Return the target path for the given build record
///
/// Build record
/// File reference for the target
public static FileReference GetTargetPath(KeyValuePair BuildRecord)
{
return FileReference.Combine(BuildRecord.Key.Directory, BuildRecord.Value.BuildRecord.TargetPath!);
}
///
/// Locates script modules, builds them if necessary, returns set of .dll files
///
///
///
///
///
///
///
///
/// Action to invoke when projects get built
///
/// Collection of all the projects. They will have been compiled.
public static HashSet InitializeScriptModules(Rules.RulesFileType RulesFileType,
string? ScriptsForProjectFileName, List? AdditionalScriptsFolders, bool bForceCompile, bool bNoCompile, bool bUseBuildRecords,
out bool bBuildSuccess, Action OnBuildingProjects, ILogger Logger)
{
List GameDirectories = GetGameDirectories(ScriptsForProjectFileName, Logger);
List AdditionalDirectories = GetAdditionalDirectories(AdditionalScriptsFolders);
List GameBuildDirectories = GetAdditionalBuildDirectories(GameDirectories);
// List of directories used to locate Intermediate/ScriptModules dirs for writing build records
List BaseDirectories = new List(1 + GameDirectories.Count + AdditionalDirectories.Count);
BaseDirectories.Add(Unreal.EngineDirectory);
BaseDirectories.AddRange(GameDirectories);
BaseDirectories.AddRange(AdditionalDirectories);
BaseDirectories = BaseDirectories.Distinct().ToList();
HashSet FoundProjects = new HashSet(
Rules.FindAllRulesSourceFiles(RulesFileType,
// Project scripts require source engine builds
GameFolders: Unreal.IsEngineInstalled() ? GameDirectories : new List(),
ForeignPlugins: null, AdditionalSearchPaths: AdditionalDirectories.Concat(GameBuildDirectories).ToList()));
BuildFlags BuildFlags = BuildFlags.None;
if (bForceCompile)
{
BuildFlags |= BuildFlags.ForceCompile;
}
if (bNoCompile)
{
BuildFlags |= BuildFlags.NoCompile;
}
if (bUseBuildRecords)
{
BuildFlags |= BuildFlags.UseBuildRecords;
}
Dictionary BuildResults;
if (Unreal.IsEngineInstalled())
{
// Warn if not using build records or force compiling
if (BuildFlags.HasFlag(BuildFlags.ForceCompile))
{
Logger.LogWarning("ForceCompile not supported if Unreal.IsEngineInstalled() == true");
}
if (!BuildFlags.HasFlag(BuildFlags.UseBuildRecords))
{
Logger.LogWarning("Disabling UseBuildRecords not supported if Unreal.IsEngineInstalled() == true");
}
BuildFlags EngineBuildFlags = BuildFlags | BuildFlags.UseBuildRecords | BuildFlags.NoCompile | BuildFlags.ErrorOnMissingTarget;
bool bEngineBuildSuccess = false;
Dictionary EngineBuildResults = Build(
RulesFileType,
FoundProjects.Where(x => x.IsUnderDirectory(Unreal.EngineDirectory)).ToHashSet(),
BaseDirectories.Where(x => x.IsUnderDirectory(Unreal.EngineDirectory)).ToList(),
null,
EngineBuildFlags,
out bEngineBuildSuccess,
OnBuildingProjects,
Logger);
bool bProjectBuildSuccess = false;
BuildFlags ProjectBuildFlags = BuildFlags | BuildFlags.UseBuildRecords;
// ForceCompile not supported for installed builds
if (ProjectBuildFlags.HasFlag(BuildFlags.ForceCompile))
{
ProjectBuildFlags &= ~BuildFlags.ForceCompile;
}
Dictionary ProjectBuildResults = Build(
RulesFileType,
FoundProjects.Where(x => !x.IsUnderDirectory(Unreal.EngineDirectory)).ToHashSet(),
BaseDirectories,
null,
ProjectBuildFlags,
out bProjectBuildSuccess,
OnBuildingProjects,
Logger);
bBuildSuccess = bEngineBuildSuccess && bProjectBuildSuccess;
BuildResults = new Dictionary(EngineBuildResults);
foreach (KeyValuePair Item in ProjectBuildResults)
{
if (!BuildResults.ContainsKey(Item.Key))
{
BuildResults.Add(Item.Key, Item.Value);
}
}
}
else
{
BuildResults = Build(RulesFileType, FoundProjects, BaseDirectories, null, BuildFlags, out bBuildSuccess, OnBuildingProjects, Logger);
}
return GetTargetPaths(BuildResults);
}
///
/// Test to see if all the given projects are up-to-date
///
/// Type of script modules being tested
/// Collection of projects to test
/// Base directories of the projects
/// Logger for output
/// True if all of the projects are up to date
public static bool AreScriptModulesUpToDate(Rules.RulesFileType RulesFileType, HashSet FoundProjects, List BaseDirectories, ILogger Logger)
{
CsProjBuildHook Hook = new Hook(Logger, RulesFileType);
// Load existing build records, validating them only if (re)compiling script projects is an option
Dictionary BuildRecords = LoadExistingBuildRecords(Hook, BaseDirectories, Logger);
foreach (FileReference Project in FoundProjects)
{
ValidateBuildRecordRecursively(BuildRecords, Project, Hook, Logger);
}
// If all found records are valid, we can return their targets directly
Dictionary ValidBuildRecords = new(BuildRecords.Where(x => x.Value.Status == CsProjBuildRecordStatus.Valid));
return FoundProjects.All(x => ValidBuildRecords.ContainsKey(x));
}
///
/// Locates script modules, builds them if necessary, returns set of .dll files
///
///
/// Projects to be compiled
/// Base directories for all the projects
/// Collection of constants to be added to the project
/// Collection of flags to customize compilation
///
/// Action to invoke when projects get built
///
/// Collection of all the projects. They will have been compiled.
public static Dictionary Build(Rules.RulesFileType RulesFileType,
HashSet FoundProjects, IEnumerable BaseDirectories, IEnumerable? DefineConstants,
BuildFlags BuildFlags, out bool bBuildSuccess, Action OnBuildingProjects, ILogger Logger)
{
CsProjBuildHook Hook = new Hook(Logger, RulesFileType);
// Load existing build records, validating them only if (re)compiling script projects is an option
Dictionary BuildRecords = LoadExistingBuildRecords(Hook, BaseDirectories, Logger);
// Only validate the build records if requested
if (BuildFlags.HasFlag(BuildFlags.UseBuildRecords))
{
if (Unreal.IsEngineInstalled())
{
// Validate build records only for projects to be compiled
foreach (FileReference Project in FoundProjects)
{
ValidateBuildRecordRecursively(BuildRecords, Project, Hook, Logger);
}
}
else
{
// Validate build records for all existing records
foreach (FileReference Project in BuildRecords.Keys)
{
ValidateBuildRecordRecursively(BuildRecords, Project, Hook, Logger);
}
}
}
else
{
// If not using build records, mark all existing records as invalid and force compile
foreach (CsProjBuildRecordEntry Record in BuildRecords.Values)
{
Record.Status = CsProjBuildRecordStatus.Invalid;
}
BuildFlags |= BuildFlags.ForceCompile;
}
// If we are not compiling, then test to see if the targets exist
if (BuildFlags.HasFlag(BuildFlags.NoCompile))
{
string FilterExtension = String.Empty;
switch (RulesFileType)
{
case Rules.RulesFileType.AutomationModule:
FilterExtension = ".Automation.json";
break;
case Rules.RulesFileType.UbtPlugin:
FilterExtension = ".ubtplugin.json";
break;
default:
throw new Exception("Unsupported rules file type");
}
Dictionary OutRecords = new Dictionary(BuildRecords.Count);
foreach (KeyValuePair Record in BuildRecords.Where(x => x.Value.BuildRecordFile.HasExtension(FilterExtension)))
{
FileReference TargetPath = FileReference.Combine(Record.Key.Directory, Record.Value.BuildRecord.TargetPath!);
if (FileReference.Exists(TargetPath))
{
OutRecords.Add(Record.Key, Record.Value);
}
else
{
if (!BuildFlags.HasFlag(BuildFlags.ErrorOnMissingTarget))
{
// when -NoCompile is on the command line, try to run with whatever is available
Logger.LogWarning("Script module \"{TargetPath}\" not found for record \"{BuildRecordPath}\"", TargetPath, Record.Value.BuildRecordFile);
}
else
{
// when the engine is installed, expect to find a built target assembly for every record that was found
throw new Exception($"Script module \"{TargetPath}\" not found for record \"{Record.Value.BuildRecordFile}\"");
}
}
}
bBuildSuccess = true;
return OutRecords;
}
else
{
// when the engine is not installed, delete any build record .json file that is not valid
foreach (CsProjBuildRecordEntry Entry in BuildRecords.Values.Where(x => x.Status == CsProjBuildRecordStatus.Invalid))
{
if (Entry.BuildRecordFile != null)
{
Logger.LogDebug("Deleting invalid build record \"{BuildRecordPath}\"", Entry.BuildRecordFile);
FileReference.Delete(Entry.BuildRecordFile);
}
}
}
if (!BuildFlags.HasFlag(BuildFlags.ForceCompile))
{
// If all found records are valid, we can return their targets directly
Dictionary ValidBuildRecords = new(BuildRecords.Where(x => x.Value.Status == CsProjBuildRecordStatus.Valid));
if (FoundProjects.All(x => ValidBuildRecords.ContainsKey(x)))
{
bBuildSuccess = true;
return new Dictionary(ValidBuildRecords.Where(x => FoundProjects.Contains(x.Key)));
}
}
// Fall back to the slower approach: use msbuild to load csproj files & build as necessary
return Build(FoundProjects, BuildFlags.HasFlag(BuildFlags.ForceCompile), out bBuildSuccess, Hook, BaseDirectories, DefineConstants, OnBuildingProjects, Logger);
}
///
/// This method exists purely to prevent EpicGames.MsBuild from being loaded until the absolute last moment.
/// If it is placed in the caller directly, then when the caller is invoked, the assembly will be loaded resulting
/// in the possible Microsoft.Build.Framework load issue later on is this method isn't invoked.
///
[MethodImpl(MethodImplOptions.NoInlining)]
static Dictionary Build(HashSet FoundProjects,
bool bForceCompile, out bool bBuildSuccess, CsProjBuildHook Hook, IEnumerable BaseDirectories,
IEnumerable? DefineConstants, Action OnBuildingProjects, ILogger Logger)
{
return CsProjBuilder.Build(FoundProjects, bForceCompile, out bBuildSuccess, Hook, BaseDirectories, DefineConstants ?? (Enumerable.Empty()), OnBuildingProjects, Logger);
}
///
/// Find and load existing build record .json files from any Intermediate/ScriptModules found in the provided lists
///
/// Hook object used to query directory
///
///
///
static Dictionary LoadExistingBuildRecords(CsProjBuildHook Hook, IEnumerable BaseDirectories, ILogger Logger)
{
Dictionary LoadedBuildRecords = new Dictionary();
foreach (DirectoryReference Directory in BaseDirectories)
{
DirectoryReference IntermediateDirectory = Hook.GetBuildRecordDirectory(Directory);
if (!DirectoryReference.Exists(IntermediateDirectory))
{
continue;
}
foreach (FileReference JsonFile in DirectoryReference.EnumerateFiles(IntermediateDirectory, "*.json"))
{
CsProjBuildRecord? BuildRecord = default;
// filesystem errors or json parsing might result in an exception. If that happens, we fall back to the
// slower path - if compiling, buildrecord files will be re-generated; other filesystem errors may persist
try
{
BuildRecord = JsonSerializer.Deserialize(FileReference.ReadAllText(JsonFile));
Logger.LogDebug("Loaded script module build record {JsonFile}", JsonFile);
}
catch(Exception Ex)
{
Logger.LogWarning("[{JsonFile}] Failed to load build record: {Message}", JsonFile, Ex.Message);
}
if (BuildRecord != null && BuildRecord.ProjectPath != null)
{
CsProjBuildRecordEntry Entry = new CsProjBuildRecordEntry(
FileReference.FromString(Path.GetFullPath(BuildRecord.ProjectPath, JsonFile.Directory.FullName)),
JsonFile,
BuildRecord);
LoadedBuildRecords.Add(Entry.ProjectFile, Entry);
}
else
{
// Delete the invalid build record
Logger.LogWarning("Deleting invalid build record {JsonFile}", JsonFile);
}
}
}
return LoadedBuildRecords;
}
private static bool ValidateGlobbedFiles(DirectoryReference ProjectDirectory,
List Globs, HashSet GlobbedDependencies, out string ValidationFailureMessage)
{
// First, evaluate globs
// Files are grouped by ItemType (e.g. Compile, EmbeddedResource) to ensure that Exclude and
// Remove act as expected.
Dictionary> Files = new Dictionary>();
foreach (CsProjBuildRecord.Glob Glob in Globs)
{
HashSet? TypedFiles;
if (!Files.TryGetValue(Glob.ItemType!, out TypedFiles))
{
TypedFiles = new HashSet();
Files.Add(Glob.ItemType!, TypedFiles);
}
Matcher matcher = new Matcher();
if (Glob.Include?.Any() == true)
{
matcher.AddIncludePatterns(Glob.Include);
}
if (Glob.Exclude?.Any() == true)
{
matcher.AddExcludePatterns(Glob.Exclude);
}
TypedFiles.UnionWith(matcher.GetResultsInFullPath(ProjectDirectory.FullName).Select(x => Path.GetRelativePath(ProjectDirectory.FullName, x)));
if (Glob.Remove?.Any() == true)
{
// Matcher.Match() doesn't handle inconsistent path separators correctly - which is why globs
// are normalized when they are added to CsProjBuildRecord
Matcher removeMatcher = new Matcher();
removeMatcher.AddIncludePatterns(Glob.Remove);
TypedFiles.RemoveWhere(F => removeMatcher.Match(F).HasMatches);
}
}
// Then, validation that our evaluation matches what we're comparing against
bool bValid = true;
StringBuilder ValidationFailureText = new StringBuilder();
// Look for extra files that were found
foreach (HashSet TypedFiles in Files.Values)
{
foreach (string File in TypedFiles)
{
if (!GlobbedDependencies.Contains(File))
{
ValidationFailureText.AppendLine($"Found additional file {File}");
bValid = false;
}
}
}
// Look for files that are missing
foreach (string File in GlobbedDependencies)
{
bool bFound = false;
foreach (HashSet TypedFiles in Files.Values)
{
if (TypedFiles.Contains(File))
{
bFound = true;
break;
}
}
if (!bFound)
{
ValidationFailureText.AppendLine($"Did not find {File}");
bValid = false;
}
}
ValidationFailureMessage = ValidationFailureText.ToString();
return bValid;
}
private static bool ValidateBuildRecord(CsProjBuildRecord BuildRecord, DirectoryReference ProjectDirectory, out string ValidationFailureMessage, CsProjBuildHook Hook)
{
string TargetRelativePath =
Path.GetRelativePath(Unreal.EngineDirectory.FullName, BuildRecord.TargetPath!);
if (BuildRecord.Version != CsProjBuildRecord.CurrentVersion)
{
ValidationFailureMessage =
$"version does not match: build record has version {BuildRecord.Version}; current version is {CsProjBuildRecord.CurrentVersion}";
return false;
}
DateTime TargetWriteTime = Hook.GetLastWriteTime(ProjectDirectory, BuildRecord.TargetPath!);
if (BuildRecord.TargetBuildTime != TargetWriteTime)
{
ValidationFailureMessage =
$"recorded target build time ({BuildRecord.TargetBuildTime}) does not match {TargetRelativePath} ({TargetWriteTime})";
return false;
}
foreach (string Dependency in BuildRecord.Dependencies)
{
if (Hook.GetLastWriteTime(ProjectDirectory, Dependency) > TargetWriteTime)
{
ValidationFailureMessage = $"{Dependency} is newer than {TargetRelativePath}";
return false;
}
}
if (!ValidateGlobbedFiles(ProjectDirectory, BuildRecord.Globs, BuildRecord.GlobbedDependencies,
out ValidationFailureMessage))
{
return false;
}
foreach (string Dependency in BuildRecord.GlobbedDependencies)
{
if (Hook.GetLastWriteTime(ProjectDirectory, Dependency) > TargetWriteTime)
{
ValidationFailureMessage = $"{Dependency} is newer than {TargetRelativePath}";
return false;
}
}
return true;
}
static void ValidateBuildRecordRecursively(
Dictionary BuildRecords,
FileReference ProjectPath, CsProjBuildHook Hook, ILogger Logger)
{
// Was a build record loaded for this project path? (relevant when considering referenced projects)
if (!BuildRecords.TryGetValue(ProjectPath, out CsProjBuildRecordEntry? Entry))
{
Logger.LogDebug("Found project {ProjectPath} with no existing build record", ProjectPath);
return;
}
// Ignore if the status is known
if (Entry.Status != CsProjBuildRecordStatus.Unknown)
{
// Project validity has already been determined
return;
}
// If the engine is installed, and the project being validated is under the engine directory, treat it as valid
// so it is not built as a dependency when building an external project.
// It is assumed these projects exist and have already been verified.
if (Unreal.IsEngineInstalled() && ProjectPath.IsUnderDirectory(Unreal.EngineDirectory))
{
Entry.Status = CsProjBuildRecordStatus.Valid;
return;
}
// Is this particular build record valid?
if (!ValidateBuildRecord(Entry.BuildRecord, ProjectPath.Directory, out string ValidationFailureMessage, Hook))
{
string ProjectRelativePath = Path.GetRelativePath(Unreal.EngineDirectory.FullName, ProjectPath.FullName);
Logger.LogDebug("[{ProjectRelativePath}] {ValidationFailureMessage}", ProjectRelativePath, ValidationFailureMessage);
Entry.Status = CsProjBuildRecordStatus.Invalid;
return;
}
// Are all referenced build records valid?
foreach (CsProjBuildRecordRef ReferencedProject in Entry.BuildRecord.ProjectReferencesAndTimes)
{
FileReference FullProjectPath = FileReference.FromString(Path.GetFullPath(ReferencedProject.ProjectPath, ProjectPath.Directory.FullName));
ValidateBuildRecordRecursively(BuildRecords, FullProjectPath, Hook, Logger);
if (!BuildRecords.TryGetValue(FullProjectPath, out CsProjBuildRecordEntry? RefEntry))
{
string ProjectRelativePath = Path.GetRelativePath(Unreal.EngineDirectory.FullName, ProjectPath.FullName);
string DependencyRelativePath = Path.GetRelativePath(Unreal.EngineDirectory.FullName, FullProjectPath.FullName);
Logger.LogDebug("[{ProjectRelativePath}] Existing output is not valid because dependency {DependencyRelativePath} could not be found", ProjectRelativePath, DependencyRelativePath);
Entry.Status = CsProjBuildRecordStatus.Invalid;
return;
}
if (RefEntry.Status != CsProjBuildRecordStatus.Valid)
{
string ProjectRelativePath = Path.GetRelativePath(Unreal.EngineDirectory.FullName, ProjectPath.FullName);
string DependencyRelativePath = Path.GetRelativePath(Unreal.EngineDirectory.FullName, FullProjectPath.FullName);
Logger.LogDebug("[{ProjectRelativePath}] Existing output is not valid because dependency {DependencyRelativePath} is not valid", ProjectRelativePath, DependencyRelativePath);
Entry.Status = CsProjBuildRecordStatus.Invalid;
return;
}
// Ensure that the dependency was not built more recently than the project
if (ReferencedProject.TargetBuildTime == DateTime.MinValue)
{
if (Entry.BuildRecord.TargetBuildTime < RefEntry.BuildRecord.TargetBuildTime)
{
string ProjectRelativePath = Path.GetRelativePath(Unreal.EngineDirectory.FullName, ProjectPath.FullName);
string DependencyRelativePath = Path.GetRelativePath(Unreal.EngineDirectory.FullName, FullProjectPath.FullName);
Logger.LogDebug("[{ProjectRelativePath}] Existing output is not valid because dependency {DependencyRelativePath} is newer", ProjectRelativePath, DependencyRelativePath);
Entry.Status = CsProjBuildRecordStatus.Invalid;
return;
}
}
else
{
if (ReferencedProject.TargetBuildTime != RefEntry.BuildRecord.TargetBuildTime)
{
string ProjectRelativePath = Path.GetRelativePath(Unreal.EngineDirectory.FullName, ProjectPath.FullName);
string DependencyRelativePath = Path.GetRelativePath(Unreal.EngineDirectory.FullName, FullProjectPath.FullName);
Logger.LogDebug("[{ProjectRelativePath}] Existing dependency output time stamp for {DependencyRelativePath} does not match expected value", ProjectRelativePath, DependencyRelativePath);
Entry.Status = CsProjBuildRecordStatus.Invalid;
return;
}
}
}
Entry.Status = CsProjBuildRecordStatus.Valid;
}
static List GetGameDirectories(string? ScriptsForProjectFileName, ILogger Logger)
{
List GameDirectories = new List();
if (String.IsNullOrEmpty(ScriptsForProjectFileName))
{
GameDirectories = NativeProjectsBase.EnumerateProjectFiles(Logger).Select(x => x.Directory).ToList();
}
else
{
DirectoryReference ScriptsDir = new DirectoryReference(Path.GetDirectoryName(ScriptsForProjectFileName)!);
ScriptsDir = DirectoryReference.FindCorrectCase(ScriptsDir);
GameDirectories.Add(ScriptsDir);
}
return GameDirectories;
}
static List GetAdditionalDirectories(List? AdditionalScriptsFolders) =>
AdditionalScriptsFolders == null ? new List() :
AdditionalScriptsFolders.Select(x => DirectoryReference.FindCorrectCase(new DirectoryReference(x))).ToList();
static List GetAdditionalBuildDirectories(List GameDirectories) =>
GameDirectories.Select(x => DirectoryReference.Combine(x, "Build")).Where(x => DirectoryReference.Exists(x)).ToList();
}
}