// 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.Text; using System.Threading.Tasks; using EpicGames.Core; using Microsoft.Extensions.Logging; namespace UnrealBuildBase { /// /// Utility functions for querying native projects (ie. those found via a .uprojectdirs query) /// public class NativeProjectsBase { /// /// Object used for synchronizing access to static fields /// protected static object LockObject = new object(); /// /// The native project base directories /// static HashSet? CachedBaseDirectories; /// /// Cached list of project files within all the base directories /// static HashSet? CachedProjectFiles; /// /// Clear our cached properties. Generally only needed if your script has modified local files... /// public static void ClearCacheBase() { CachedBaseDirectories = null; CachedProjectFiles = null; } /// /// Retrieve the list of base directories for native projects /// public static IEnumerable EnumerateBaseDirectories(ILogger Logger) { if(CachedBaseDirectories == null) { lock(LockObject) { if(CachedBaseDirectories == null) { HashSet BaseDirs = new HashSet(); foreach (FileReference RootFile in DirectoryLookupCache.EnumerateFiles(Unreal.RootDirectory)) { if(RootFile.HasExtension(".uprojectdirs")) { foreach(string Line in File.ReadAllLines(RootFile.FullName)) { string TrimLine = Line.Trim(); if(!TrimLine.StartsWith(";")) { DirectoryReference BaseProjectDir = DirectoryReference.Combine(Unreal.RootDirectory, TrimLine); if(BaseProjectDir.IsUnderDirectory(Unreal.RootDirectory)) { BaseDirs.Add(BaseProjectDir); } else { Logger.LogWarning("Project search path '{SearchPath}' referenced by '{ProjectDirFile}' is not under '{RootDir}', ignoring.", TrimLine, RootFile, Unreal.RootDirectory); } } } } } CachedBaseDirectories = BaseDirs; } } } return CachedBaseDirectories; } /// /// Returns a list of all the projects /// /// List of projects public static IEnumerable EnumerateProjectFiles(ILogger Logger) { if(CachedProjectFiles == null) { lock(LockObject) { if(CachedProjectFiles == null) { HashSet ProjectFiles = new HashSet(); foreach(DirectoryReference BaseDirectory in EnumerateBaseDirectories(Logger)) { if(DirectoryLookupCache.DirectoryExists(BaseDirectory)) { foreach(DirectoryReference SubDirectory in DirectoryLookupCache.EnumerateDirectories(BaseDirectory)) { // Ignore system directories (specifically for Mac, since temporary folders are created here) string DirectoryName = SubDirectory.GetDirectoryName(); if (DirectoryName.StartsWith(".", StringComparison.Ordinal)) { continue; } foreach(FileReference File in DirectoryLookupCache.EnumerateFiles(SubDirectory)) { if(File.HasExtension(".uproject")) { ProjectFiles.Add(File); } } } } } CachedProjectFiles = ProjectFiles; } } } return CachedProjectFiles; } /// /// Searches base directories for an existing relative pathed file /// /// File to search for /// /// A FileReference for the existing file, otherwise null public static FileReference? FindRelativeFileReference(string File, ILogger Logger) { return EnumerateBaseDirectories(Logger) .Select(x => FileReference.Combine(x, File)) .FirstOrDefault(x => FileReference.Exists(x)); } /// /// Searches base directories for an existing relative pathed directory /// /// Directory to search for /// /// A DirectoryReference for the existing directory, otherwise null public static DirectoryReference? FindRelativeDirectoryReference(string Directory, ILogger Logger) { return EnumerateBaseDirectories(Logger) .Select(x => DirectoryReference.Combine(x, Directory)) .FirstOrDefault(x => DirectoryReference.Exists(x)); } /// /// Takes a project name (e.g "ShooterGame") or path and attempt to find the existing uproject file in the base directories /// /// Project to search, either a name or a .uproject path /// /// A FileReference to an existing .uproject file, otherwise null public static FileReference? FindProjectFile(string Project, ILogger Logger) { // Handle absolute paths, or relative paths from the current working directory if (File.Exists(Project)) { return new FileReference(Project); } if (Path.IsPathFullyQualified(Project)) { // Absolute path not found, return null instead of searching return null; } string ProjectName = Path.GetFileNameWithoutExtension(Project); // Search known .uprojects by name, then as relative path, then relative path for Project/Project.uproject return EnumerateProjectFiles(Logger).FirstOrDefault(x => String.Equals(x.GetFileNameWithoutExtension(), ProjectName, StringComparison.OrdinalIgnoreCase)) ?? FindRelativeFileReference(Project, Logger) ?? FindRelativeFileReference(Path.Combine(ProjectName, $"{ProjectName}.uproject"), Logger); } /// /// Finds all target files under a given folder, and add them to the target name to project file map /// /// Directory to search /// Map from target name to project file /// The project file for this directory protected static void FindTargetFiles(DirectoryReference Directory, Dictionary TargetNameToProjectFile, FileReference ProjectFile) { // Search for all target files within this directory bool bSearchSubFolders = true; foreach (FileReference File in DirectoryLookupCache.EnumerateFiles(Directory)) { if (File.HasExtension(".target.cs")) { string TargetName = Path.GetFileNameWithoutExtension(File.GetFileNameWithoutExtension()); TargetNameToProjectFile[TargetName] = ProjectFile; bSearchSubFolders = false; } } // If we didn't find anything, recurse through the subfolders if(bSearchSubFolders) { foreach(DirectoryReference SubDirectory in DirectoryLookupCache.EnumerateDirectories(Directory)) { FindTargetFiles(SubDirectory, TargetNameToProjectFile, ProjectFile); } } } /// /// Checks if a given project is a native project /// /// The project file to check /// /// True if the given project is a native project public static bool IsNativeProject(FileReference ProjectFile, ILogger Logger) { EnumerateProjectFiles(Logger); return CachedProjectFiles!.Contains(ProjectFile); } } }