// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Diagnostics; using System.Threading; using System.Runtime.Serialization; namespace UnrealBuildTool { /// /// Represents a file on disk that is used as an input or output of a build action. /// FileItems are created by calling FileItem.GetItemByFileReference, which creates a single FileItem for each unique file path. /// [Serializable] class FileItem : ISerializable { /// /// Preparation and Assembly (serialized) /// /// /// The action that produces the file. /// public Action ProducingAction = null; /// /// The file reference /// public FileReference Reference; /// /// True if any DLLs produced by this /// public bool bNeedsHotReloadNumbersDLLCleanUp = false; /// /// Whether or not this is a remote file, in which case we can't access it directly /// public bool bIsRemoteFile = false; /// /// Accessor for the absolute path to the file /// public string AbsolutePath { get { return Reference.FullName; } } /// /// For C++ file items, this stores cached information about the include paths needed in order to include header files from these C++ files. This is part of UBT's dependency caching optimizations. /// public CppIncludePaths CachedIncludePaths { get { return CachedIncludePathsValue; } set { if (value != null && CachedIncludePathsValue != null && CachedIncludePathsValue != value) { // Uh oh. We're clobbering our cached CompileEnvironment for this file with a different CompileEnvironment. This means // that the same source file is being compiled into more than one module. throw new BuildException("File '{0}' was included by multiple modules, but with different include paths", this.Info.FullName); } CachedIncludePathsValue = value; } } private CppIncludePaths CachedIncludePathsValue; /// /// Preparation only (not serialized) /// /// /// The PCH file that this file will use /// public FileReference PrecompiledHeaderIncludeFilename; /// /// Transients (not serialized) /// /// /// The information about the file. /// public FileInfo Info; /// /// This is true if this item is actually a directory. Consideration for Mac application bundles. Note that Info will be null if true! /// public bool IsDirectory; /// /// Relative cost of action associated with producing this file. /// public long RelativeCost = 0; /// /// The last write time of the file. /// public DateTimeOffset _LastWriteTime; public DateTimeOffset LastWriteTime { get { if (bIsRemoteFile) { LookupOutstandingFiles(); } return _LastWriteTime; } set { _LastWriteTime = value; } } /// /// Whether the file exists. /// public bool _bExists = false; public bool bExists { get { if (bIsRemoteFile) { LookupOutstandingFiles(); } return _bExists; } set { _bExists = value; } } /// /// Size of the file if it exists, otherwise -1 /// public long _Length = -1; public long Length { get { if (bIsRemoteFile) { LookupOutstandingFiles(); } return _Length; } set { _Length = value; } } /// /// Statics /// /// /// Used for performance debugging /// public static long TotalFileItemCount = 0; public static long MissingFileItemCount = 0; /// /// A case-insensitive dictionary that's used to map each unique file name to a single FileItem object. /// static Dictionary UniqueSourceFileMap = new Dictionary(); /// /// A list of remote file items that have been created but haven't needed the remote info yet, so we can gang up many into one request /// static List DelayedRemoteLookupFiles = new List(); /// /// Clears the FileItem caches. /// public static void ClearCaches() { UniqueSourceFileMap.Clear(); DelayedRemoteLookupFiles.Clear(); } /// /// Clears the cached include paths on every file item /// public static void ClearCachedIncludePaths() { foreach(FileItem Item in UniqueSourceFileMap.Values) { Item.CachedIncludePaths = null; } } /// /// Resolve any outstanding remote file info lookups /// private void LookupOutstandingFiles() { // for remote files, look up any outstanding files if (bIsRemoteFile) { FileItem[] Files = null; lock (DelayedRemoteLookupFiles) { if (DelayedRemoteLookupFiles.Count > 0) { // make an array so we can clear the original array, just in case BatchFileInfo does something that uses // DelayedRemoteLookupFiles, so we don't deadlock Files = DelayedRemoteLookupFiles.ToArray(); DelayedRemoteLookupFiles.Clear(); } } if (Files != null) { RPCUtilHelper.BatchFileInfo(Files); } } } /// The FileItem that represents the given file path. public static FileItem GetItemByPath(string FilePath) { return GetItemByFileReference(new FileReference(FilePath)); } /// The FileItem that represents the given a full file path. public static FileItem GetItemByFileReference(FileReference Reference) { FileItem Result = null; if (UniqueSourceFileMap.TryGetValue(Reference, out Result)) { return Result; } else { return new FileItem(Reference); } } /// The remote FileItem that represents the given file path. public static FileItem GetRemoteItemByPath(string AbsoluteRemotePath, UnrealTargetPlatform Platform) { if (AbsoluteRemotePath.StartsWith(".")) { throw new BuildException("GetRemoteItemByPath must be passed an absolute path, not a relative path '{0}'", AbsoluteRemotePath); } FileReference RemoteFileReference = FileReference.MakeRemote(AbsoluteRemotePath); FileItem Result = null; if (UniqueSourceFileMap.TryGetValue(RemoteFileReference, out Result)) { return Result; } else { return new FileItem(RemoteFileReference, true, Platform); } } /// /// If the given file path identifies a file that already exists, returns the FileItem that represents it. /// public static FileItem GetExistingItemByPath(string FileName) { return GetExistingItemByFileReference(new FileReference(FileName)); } /// /// If the given file path identifies a file that already exists, returns the FileItem that represents it. /// public static FileItem GetExistingItemByFileReference(FileReference FileRef) { FileItem Result = GetItemByFileReference(FileRef); if (Result.bExists) { return Result; } else { return null; } } /// /// Determines the appropriate encoding for a string: either ASCII or UTF-8. /// /// The string to test. /// Either System.Text.Encoding.ASCII or System.Text.Encoding.UTF8, depending on whether or not the string contains non-ASCII characters. private static Encoding GetEncodingForString(string Str) { // If the string length is equivalent to the encoded length, then no non-ASCII characters were present in the string. // Don't write BOM as it messes with clang when loading response files. return (Encoding.UTF8.GetByteCount(Str) == Str.Length) ? Encoding.ASCII : new UTF8Encoding(false); } /// /// Creates a text file with the given contents. If the contents of the text file aren't changed, it won't write the new contents to /// the file to avoid causing an action to be considered outdated. /// public static FileItem CreateIntermediateTextFile(FileReference AbsolutePath, string Contents) { // Create the directory if it doesn't exist. Directory.CreateDirectory(Path.GetDirectoryName(AbsolutePath.FullName)); // Only write the file if its contents have changed. if (!FileReference.Exists(AbsolutePath) || !String.Equals(Utils.ReadAllText(AbsolutePath.FullName), Contents, StringComparison.InvariantCultureIgnoreCase)) { File.WriteAllText(AbsolutePath.FullName, Contents, GetEncodingForString(Contents)); } return GetItemByFileReference(AbsolutePath); } /// /// Deletes the file. /// public void Delete() { Debug.Assert(_bExists); Debug.Assert(!bIsRemoteFile); int MaxRetryCount = 3; int DeleteTryCount = 0; bool bFileDeletedSuccessfully = false; do { // If this isn't the first time through, sleep a little before trying again if (DeleteTryCount > 0) { Thread.Sleep(1000); } DeleteTryCount++; try { // Delete the destination file if it exists FileInfo DeletedFileInfo = new FileInfo(AbsolutePath); if (DeletedFileInfo.Exists) { DeletedFileInfo.IsReadOnly = false; DeletedFileInfo.Delete(); } // Success! bFileDeletedSuccessfully = true; } catch (Exception Ex) { Log.TraceInformation("Failed to delete file '" + AbsolutePath + "'"); Log.TraceInformation(" Exception: " + Ex.Message); if (DeleteTryCount < MaxRetryCount) { Log.TraceInformation("Attempting to retry..."); } else { Log.TraceInformation("ERROR: Exhausted all retries!"); } } } while (!bFileDeletedSuccessfully && (DeleteTryCount < MaxRetryCount)); } /// /// Initialization constructor. /// protected FileItem(FileReference InFile) { Reference = InFile; ResetFileInfo(); ++TotalFileItemCount; if (!_bExists) { ++MissingFileItemCount; // Log.TraceInformation( "Missing: " + FileAbsolutePath ); } UniqueSourceFileMap[Reference] = this; } /// /// ISerializable: Constructor called when this object is deserialized /// protected FileItem(SerializationInfo SerializationInfo, StreamingContext StreamingContext) { ProducingAction = (Action)SerializationInfo.GetValue("pa", typeof(Action)); Reference = (FileReference)SerializationInfo.GetValue("fi", typeof(FileReference)); bIsRemoteFile = SerializationInfo.GetBoolean("rf"); bNeedsHotReloadNumbersDLLCleanUp = SerializationInfo.GetBoolean("hr"); CachedIncludePaths = (CppIncludePaths)SerializationInfo.GetValue("ci", typeof(CppIncludePaths)); // Go ahead and init normally now { ResetFileInfo(); ++TotalFileItemCount; if (!_bExists) { ++MissingFileItemCount; // Log.TraceInformation( "Missing: " + FileAbsolutePath ); } if (bIsRemoteFile) { lock (DelayedRemoteLookupFiles) { DelayedRemoteLookupFiles.Add(this); } } else { UniqueSourceFileMap[Reference] = this; } } } /// /// ISerializable: Called when serialized to report additional properties that should be saved /// public void GetObjectData(SerializationInfo SerializationInfo, StreamingContext StreamingContext) { SerializationInfo.AddValue("pa", ProducingAction); SerializationInfo.AddValue("fi", Reference); SerializationInfo.AddValue("rf", bIsRemoteFile); SerializationInfo.AddValue("hr", bNeedsHotReloadNumbersDLLCleanUp); SerializationInfo.AddValue("ci", CachedIncludePaths); } /// /// (Re-)set file information for this FileItem /// public void ResetFileInfo() { if (Directory.Exists(AbsolutePath)) { // path is actually a directory (such as a Mac app bundle) _bExists = true; LastWriteTime = Directory.GetLastWriteTimeUtc(AbsolutePath); IsDirectory = true; _Length = 0; Info = null; } else { Info = new FileInfo(AbsolutePath); _bExists = Info.Exists; if (_bExists) { _LastWriteTime = Info.LastWriteTimeUtc; _Length = Info.Length; } } } /// /// Reset file information on all cached FileItems. /// public static void ResetInfos() { foreach (KeyValuePair Item in UniqueSourceFileMap) { Item.Value.ResetFileInfo(); } } /// /// Initialization constructor for optionally remote files. /// protected FileItem(FileReference InReference, bool InIsRemoteFile, UnrealTargetPlatform Platform) { bIsRemoteFile = InIsRemoteFile; Reference = InReference; // @todo iosmerge: This doesn't handle remote directories (may be needed for compiling Mac from Windows) if (bIsRemoteFile) { if (Platform == UnrealTargetPlatform.IOS || Platform == UnrealTargetPlatform.Mac) { lock (DelayedRemoteLookupFiles) { DelayedRemoteLookupFiles.Add(this); } } else { throw new BuildException("Only IPhone and Mac support remote FileItems"); } } else { FileInfo Info = new FileInfo(AbsolutePath); _bExists = Info.Exists; if (_bExists) { _LastWriteTime = Info.LastWriteTimeUtc; _Length = Info.Length; } ++TotalFileItemCount; if (!_bExists) { ++MissingFileItemCount; // Log.TraceInformation( "Missing: " + FileAbsolutePath ); } } // @todo iosmerge: This was in UE3, why commented out now? //UniqueSourceFileMap[AbsolutePathUpperInvariant] = this; } public override string ToString() { return Path.GetFileName(AbsolutePath); } } }