// 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);
}
}
}