2022-10-15 02:12:12 -04:00
// Copyright Epic Games, Inc. All Rights Reserved.
using System ;
using System.Collections.Generic ;
2023-05-30 18:38:07 -04:00
using System.Diagnostics ;
2022-10-15 02:12:12 -04:00
using System.Linq ;
using System.Text.RegularExpressions ;
2023-04-19 21:44:13 -04:00
using System.Threading.Tasks ;
2023-05-30 18:38:07 -04:00
using EpicGames.Core ;
using Microsoft.Extensions.Logging ;
using UnrealBuildBase ;
2022-10-15 02:12:12 -04:00
namespace UnrealBuildTool
{
/// <summary>
/// Fixes the include paths found in a header and source file
/// </summary>
[ToolMode("FixIncludePaths", ToolModeOptions.XmlConfig | ToolModeOptions.BuildPlatforms | ToolModeOptions.SingleInstance | ToolModeOptions.StartPrefetchingEngine | ToolModeOptions.ShowExecutionTime)]
class FixIncludePathsMode : ToolMode
{
/// <summary>
/// Regex that matches #include statements.
/// </summary>
2022-10-26 17:48:47 -04:00
static readonly Regex IncludeRegex = new Regex ( "^[ \t]*#[ \t]*include[ \t]*[<\"](?<HeaderFile>[^\">]*)[\">]" , RegexOptions . Compiled | RegexOptions . Singleline | RegexOptions . ExplicitCapture ) ;
2022-10-15 02:12:12 -04:00
static readonly string UnrealRootDirectory = Unreal . RootDirectory . FullName . Replace ( '\\' , '/' ) ;
2022-10-19 22:00:55 -04:00
static readonly string [ ] PreferredPaths = { "/Public/" , "/Private/" , "/Classes/" , "/Internal/" , "/UHT/" , "/VNI/" } ;
2022-10-15 02:12:12 -04:00
static readonly string [ ] PublicDirectories = { "Public" , "Classes" , } ;
[CommandLine("-Filter=", Description = "Set of filters for files to include in the database. Relative to the root directory, or to the project file.")]
List < string > FilterRules = new List < string > ( ) ;
2022-11-14 18:26:52 -05:00
[CommandLine("-IncludeFilter=", Description = "Set of filters for #include'd lines to allow updating. Relative to the root directory, or to the project file.")]
List < string > IncludeFilterRules = new List < string > ( ) ;
2022-10-15 02:12:12 -04:00
[CommandLine("-CheckoutWithP4", Description = "Flags that this task should use p4 to check out the file before updating it.")]
public bool bCheckoutWithP4 = false ;
[CommandLine("-NoOutput", Description = "Flags that the updated files shouldn't be saved.")]
public bool bNoOutput = false ;
2022-10-18 01:22:38 -04:00
[CommandLine("-NoIncludeSorting", Description = "Flags that includes should not be sorted.")]
public bool bNoIncludeSorting = false ;
2022-10-15 02:12:12 -04:00
/// <summary>
/// Execute the command
/// </summary>
/// <param name="Arguments">Command line arguments</param>
/// <returns>Exit code</returns>
/// <param name="Logger"></param>
2023-04-19 21:44:13 -04:00
public override Task < int > ExecuteAsync ( CommandLineArguments Arguments , ILogger Logger )
2022-10-15 02:12:12 -04:00
{
Arguments . ApplyTo ( this ) ;
// Create the build configuration object, and read the settings
BuildConfiguration BuildConfiguration = new BuildConfiguration ( ) ;
XmlConfig . ApplyTo ( BuildConfiguration ) ;
Arguments . ApplyTo ( BuildConfiguration ) ;
// Parse the filter argument
FileFilter ? FileFilter = null ;
if ( FilterRules . Count > 0 )
{
FileFilter = new FileFilter ( FileFilterType . Exclude ) ;
foreach ( string FilterRule in FilterRules )
{
FileFilter . AddRules ( FilterRule . Split ( ';' ) ) ;
}
}
2022-11-14 18:26:52 -05:00
// Parse the include filter argument
FileFilter ? IncludeFilter = null ;
if ( IncludeFilterRules . Count > 0 )
{
IncludeFilter = new FileFilter ( FileFilterType . Exclude ) ;
foreach ( string FilterRule in IncludeFilterRules )
{
IncludeFilter . AddRules ( FilterRule . Split ( ';' ) ) ;
}
}
2022-10-15 02:12:12 -04:00
// Force C++ modules to always include their generated code directories
UEBuildModuleCPP . bForceAddGeneratedCodeIncludePath = true ;
// Parse all the target descriptors
2023-07-21 16:57:04 -04:00
List < TargetDescriptor > TargetDescriptors = TargetDescriptor . ParseCommandLine ( Arguments , BuildConfiguration , Logger ) ;
2022-10-15 02:12:12 -04:00
// Generate the compile DB for each target
using ( ISourceFileWorkingSet WorkingSet = new EmptySourceFileWorkingSet ( ) )
{
HashSet < UEBuildModule > ScannedModules = new ( ) ;
// Find the compile commands for each file in the target
Dictionary < FileReference , string > FileToCommand = new Dictionary < FileReference , string > ( ) ;
foreach ( TargetDescriptor TargetDescriptor in TargetDescriptors )
{
// Create a makefile for the target
2023-07-21 16:57:04 -04:00
UEBuildTarget Target = UEBuildTarget . Create ( TargetDescriptor , BuildConfiguration , Logger ) ;
2022-10-15 02:12:12 -04:00
2022-11-14 18:26:52 -05:00
// Get InputLists and build Include Filter if necessary
HashSet < FileReference > IncludeFileList = new HashSet < FileReference > ( ) ;
Dictionary < UEBuildModuleCPP , List < FileReference > > ModuleToFiles = new Dictionary < UEBuildModuleCPP , List < FileReference > > ( ) ;
2022-10-15 02:12:12 -04:00
foreach ( UEBuildBinary Binary in Target . Binaries )
{
foreach ( UEBuildModuleCPP Module in Binary . Modules . OfType < UEBuildModuleCPP > ( ) )
{
2023-03-06 11:07:00 -05:00
UEBuildModuleCPP . InputFileCollection InputFileCollection = Module . FindInputFiles ( Target . Platform , new Dictionary < DirectoryItem , FileItem [ ] > ( ) , Logger ) ;
2022-10-15 02:12:12 -04:00
List < FileItem > InputFiles = new List < FileItem > ( ) ;
InputFiles . AddRange ( InputFileCollection . HeaderFiles ) ;
InputFiles . AddRange ( InputFileCollection . CPPFiles ) ;
InputFiles . AddRange ( InputFileCollection . CCFiles ) ;
InputFiles . AddRange ( InputFileCollection . CFiles ) ;
2023-05-31 13:37:21 -04:00
List < FileReference > FileList = new List < FileReference > ( ) ;
2022-10-15 02:12:12 -04:00
foreach ( FileItem InputFile in InputFiles )
{
if ( FileFilter = = null | | FileFilter . Matches ( InputFile . Location . MakeRelativeTo ( Unreal . RootDirectory ) ) )
{
2023-05-31 13:37:21 -04:00
FileReference fileRef = new FileReference ( InputFile . AbsolutePath ) ;
2022-10-15 02:12:12 -04:00
FileList . Add ( fileRef ) ;
}
}
2022-11-14 18:26:52 -05:00
ModuleToFiles [ Module ] = FileList ;
2022-10-15 02:12:12 -04:00
2022-11-14 18:26:52 -05:00
foreach ( FileItem InputFile in InputFiles )
2022-10-15 02:12:12 -04:00
{
2022-11-14 18:26:52 -05:00
if ( IncludeFilter = = null | | IncludeFilter . Matches ( InputFile . Location . MakeRelativeTo ( Unreal . RootDirectory ) ) )
2022-10-15 02:12:12 -04:00
{
2023-05-31 13:37:21 -04:00
FileReference fileRef = new FileReference ( InputFile . AbsolutePath ) ;
2022-11-14 18:26:52 -05:00
IncludeFileList . Add ( fileRef ) ;
}
}
}
}
2022-10-15 02:12:12 -04:00
2022-11-14 18:26:52 -05:00
// List of modules that are allowed to be processed (not ThirdParty and have unfiltered files)
HashSet < UEBuildModuleCPP > AllModules = ModuleToFiles . Where ( Item = > ! Item . Key . RulesFile . ContainsName ( "ThirdParty" , Unreal . RootDirectory ) & & Item . Value . Any ( ) ) . Select ( Item = > Item . Key ) . ToHashSet ( ) ;
// Keep track of progress
int Index = 0 ;
int Total = AllModules . Count ( ) ;
// Create the global compile environment for this target
CppCompileEnvironment GlobalCompileEnvironment = Target . CreateCompileEnvironmentForProjectFiles ( Logger ) ;
// Create all the binaries and modules
foreach ( UEBuildBinary Binary in Target . Binaries )
{
CppCompileEnvironment BinaryCompileEnvironment = Binary . CreateBinaryCompileEnvironment ( GlobalCompileEnvironment ) ;
foreach ( UEBuildModuleCPP Module in Binary . Modules . OfType < UEBuildModuleCPP > ( ) . Where ( Module = > AllModules . Contains ( Module ) ) )
{
List < FileReference > FileList = ModuleToFiles [ Module ] ;
Dictionary < string , string? > PreferredPathCache = new ( ) ;
CppCompileEnvironment env = Module . CreateCompileEnvironmentForIntellisense ( Target . Rules , BinaryCompileEnvironment , Logger ) ;
2023-05-31 13:37:21 -04:00
foreach ( FileReference InputFile in FileList )
2022-11-14 18:26:52 -05:00
{
List < int > LinesUpdated = new ( ) ;
string [ ] Text = FileReference . ReadAllLines ( InputFile ) ;
bool UpdatedText = false ;
for ( int i = 0 ; i < Text . Length ; i + + )
{
2023-05-31 13:37:21 -04:00
string Line = Text [ i ] ;
2022-11-14 18:26:52 -05:00
int LineNumber = i + 1 ;
Match IncludeMatch = IncludeRegex . Match ( Line ) ;
if ( IncludeMatch . Success )
2022-10-15 02:12:12 -04:00
{
2022-11-14 18:26:52 -05:00
string Include = IncludeMatch . Groups [ 1 ] . Value ;
2022-10-15 02:12:12 -04:00
2022-11-14 18:26:52 -05:00
if ( Include . Contains ( "/Private/" ) & & PublicDirectories . Any ( dir = > InputFile . FullName . Contains ( System . IO . Path . DirectorySeparatorChar + dir + System . IO . Path . DirectorySeparatorChar ) ) )
{
Logger . LogError ( "{FileName}({LineNumber}): Can not update #include '{Include}' in the public file because it may break external code that uses it." , InputFile . FullName , LineNumber , Include ) ;
continue ;
}
if ( Include . Contains ( ".." ) )
{
Logger . LogError ( "{FileName}({LineNumber}): Can not update #include '{Include}', relative pathing is not currently handled." , InputFile . FullName , LineNumber , Include ) ;
continue ;
}
2023-02-14 18:12:00 -05:00
//Debugger.Launch();
2022-11-14 18:26:52 -05:00
string? PreferredInclude = null ;
if ( ! PreferredPathCache . TryGetValue ( Include , out PreferredInclude ) )
{
List < DirectoryReference > IncludePaths = new ( ) ;
IncludePaths . Add ( new DirectoryReference ( System . IO . Directory . GetParent ( InputFile . FullName ) ! ) ) ;
IncludePaths . AddRange ( env . UserIncludePaths ) ;
IncludePaths . AddRange ( env . SystemIncludePaths ) ;
// search include paths
FileReference ? FoundIncludeFile = null ;
DirectoryReference ? FoundIncludePath = null ;
2023-05-31 13:37:21 -04:00
foreach ( DirectoryReference IncludePath in IncludePaths )
2022-10-15 02:12:12 -04:00
{
2022-11-14 18:26:52 -05:00
string Path = System . IO . Path . GetFullPath ( System . IO . Path . Combine ( IncludePath . FullName , Include ) ) ;
if ( System . IO . File . Exists ( Path ) )
{
FoundIncludeFile = FileReference . FromString ( Path ) ;
FoundIncludePath = IncludePath ;
break ;
}
}
if ( FoundIncludeFile ! = null & & ! IncludeFileList . Contains ( FoundIncludeFile ) )
{
2023-02-14 18:12:00 -05:00
Logger . LogInformation ( "{FileName}({LineNumber}): Skipping '{Include}' because it is filtered out." , InputFile . FullName , LineNumber , Include ) ;
2022-11-14 18:26:52 -05:00
PreferredInclude = Include ;
PreferredPathCache [ Include ] = PreferredInclude ;
2022-10-15 02:12:12 -04:00
continue ;
}
2022-11-14 18:26:52 -05:00
if ( FoundIncludeFile ! = null )
2022-10-15 02:12:12 -04:00
{
2022-11-14 18:26:52 -05:00
string FullPath = FoundIncludeFile . FullName . Replace ( '\\' , '/' ) ;
if ( FullPath . Contains ( "ThirdParty" ) )
2022-10-18 01:22:50 -04:00
{
2023-02-14 18:12:00 -05:00
Logger . LogInformation ( "{FileName}({LineNumber}): Skipping '{Include}' because it is a third party header." , InputFile . FullName , LineNumber , Include ) ;
2022-11-14 18:26:52 -05:00
PreferredInclude = Include ;
PreferredPathCache [ Include ] = PreferredInclude ;
continue ;
2022-10-18 01:22:50 -04:00
}
2022-11-14 18:26:52 -05:00
// if the include and the source file live in the same directory then it is OK to be relative
2023-05-30 18:59:32 -04:00
if ( String . Equals ( System . IO . Directory . GetParent ( FullPath ) ? . FullName , System . IO . Directory . GetParent ( InputFile . FullName ) ? . FullName , StringComparison . CurrentCultureIgnoreCase ) & &
String . Equals ( Include , System . IO . Path . GetFileName ( FullPath ) , StringComparison . CurrentCultureIgnoreCase ) )
2022-10-15 02:12:12 -04:00
{
2023-02-14 18:12:00 -05:00
Logger . LogInformation ( "{FileName}({LineNumber}): Using '{Include}' because it is in the same directory." , InputFile . FullName , LineNumber , Include ) ;
2022-11-14 18:26:52 -05:00
PreferredInclude = Include ;
}
else
{
if ( ! FullPath . Contains ( UnrealRootDirectory ) )
2022-10-19 22:00:55 -04:00
{
2023-02-14 18:12:00 -05:00
Logger . LogInformation ( "{FileName}({LineNumber}): Skipping '{Include}' because it isn't under the Unreal root directory." , InputFile . FullName , LineNumber , Include ) ;
2022-10-15 02:12:12 -04:00
}
else
{
2022-11-14 18:26:52 -05:00
string? FoundPreferredPath = PreferredPaths . FirstOrDefault ( path = > FullPath . Contains ( path ) ) ;
if ( FoundPreferredPath ! = null )
2022-10-18 01:22:50 -04:00
{
2022-11-14 18:26:52 -05:00
int end = FullPath . LastIndexOf ( FoundPreferredPath ) + FoundPreferredPath . Length ;
PreferredInclude = FullPath . Substring ( end ) ;
// Is the current include a shortened version of the preferred include path?
if ( PreferredInclude ! = Include & & PreferredInclude . Contains ( Include ) )
{
2023-02-14 18:12:00 -05:00
Logger . LogInformation ( "{FileName}({LineNumber}): Using '{Include}' because it is shorter than '{PreferredInclude}'." , InputFile . FullName , LineNumber , Include , PreferredInclude ) ;
2022-11-14 18:26:52 -05:00
PreferredInclude = Include ;
}
2022-10-18 01:22:50 -04:00
}
else
{
2022-11-14 18:26:52 -05:00
PreferredInclude = null ;
2022-10-20 16:41:47 -04:00
2022-11-14 18:26:52 -05:00
string ModulePath = FullPath ;
FileReference IncludeFileReference = FileReference . FromString ( FullPath ) ;
DirectoryReference ? TempDirectory = IncludeFileReference . Directory ;
DirectoryReference ? FoundDirectory = null ;
// find the module this include is part of
while ( TempDirectory ! = null )
{
if ( DirectoryReference . EnumerateFiles ( TempDirectory , $"*.build.cs" ) . Any ( ) )
2022-10-20 16:41:47 -04:00
{
2022-11-14 18:26:52 -05:00
FoundDirectory = TempDirectory ;
break ;
2022-10-20 16:41:47 -04:00
}
2022-11-14 18:26:52 -05:00
TempDirectory = TempDirectory . ParentDirectory ;
2022-10-20 16:41:47 -04:00
}
2022-11-14 18:26:52 -05:00
if ( FoundDirectory ! = null )
2022-10-20 16:41:47 -04:00
{
2022-11-14 18:26:52 -05:00
PreferredInclude = FullPath . Substring ( FoundDirectory . FullName . Length + 1 ) ;
2022-10-19 22:00:55 -04:00
}
2022-10-18 01:22:50 -04:00
}
2022-10-15 02:12:12 -04:00
}
2022-11-14 18:26:52 -05:00
PreferredPathCache [ Include ] = PreferredInclude ;
}
2022-10-15 02:12:12 -04:00
}
2023-05-30 18:38:07 -04:00
2022-11-14 18:26:52 -05:00
if ( PreferredInclude = = null )
{
2023-02-14 18:12:00 -05:00
Logger . LogInformation ( "{FileName}({LineNumber}): Could not find path to '{IncludePath}'" , InputFile . FullName , LineNumber , Include ) ;
2022-11-14 18:26:52 -05:00
}
}
if ( PreferredInclude ! = null & & Include ! = PreferredInclude )
{
Logger . LogInformation ( "{FileName}({LineNumber}): Updated '{OldInclude}' -> '{NewInclude}'" , InputFile . FullName , LineNumber , Include , PreferredInclude ) ;
Text [ i ] = Line . Replace ( Include , PreferredInclude ) ;
UpdatedText = true ;
LinesUpdated . Add ( i ) ;
2022-10-15 02:12:12 -04:00
}
}
2022-11-14 18:26:52 -05:00
}
2022-10-15 02:12:12 -04:00
2022-11-14 18:26:52 -05:00
if ( UpdatedText )
{
if ( ! bNoIncludeSorting )
2022-10-15 02:12:12 -04:00
{
2022-11-14 18:26:52 -05:00
SortIncludes ( InputFile , LinesUpdated , Text ) ;
}
2022-10-15 02:12:12 -04:00
2022-11-14 18:26:52 -05:00
if ( ! bNoOutput )
{
Logger . LogInformation ( "Updating {IncludePath}" , InputFile . FullName ) ;
try
2022-10-15 02:12:12 -04:00
{
2022-11-14 18:26:52 -05:00
if ( bCheckoutWithP4 )
2022-10-15 02:12:12 -04:00
{
2022-11-14 18:26:52 -05:00
System . Diagnostics . Process Process = new System . Diagnostics . Process ( ) ;
System . Diagnostics . ProcessStartInfo StartInfo = new System . Diagnostics . ProcessStartInfo ( ) ;
Process . StartInfo . WindowStyle = System . Diagnostics . ProcessWindowStyle . Hidden ;
Process . StartInfo . FileName = "p4.exe" ;
Process . StartInfo . Arguments = $"edit {InputFile.FullName}" ;
Process . Start ( ) ;
Process . WaitForExit ( ) ;
2022-10-15 02:12:12 -04:00
}
2022-11-14 18:26:52 -05:00
System . IO . File . WriteAllLines ( InputFile . FullName , Text ) ;
}
catch ( Exception ex )
{
Logger . LogWarning ( "Failed to write to file: {Exception}" , ex ) ;
2022-10-15 02:12:12 -04:00
}
}
}
}
2022-11-14 18:26:52 -05:00
Logger . LogInformation ( "[{Index}/{Total}] Processed Module {Name} ({Files} files)" , + + Index , Total , Module . Name , FileList . Count ) ;
2022-10-15 02:12:12 -04:00
ScannedModules . Add ( Module ) ;
}
}
}
}
2023-04-19 21:44:13 -04:00
return Task . FromResult ( 0 ) ;
2022-10-15 02:12:12 -04:00
}
2022-10-18 01:22:38 -04:00
class HeaderSortComparison : IComparer < string >
{
private string IWYUFileName ;
public HeaderSortComparison ( string IWYUFileName )
{
this . IWYUFileName = IWYUFileName ;
}
public int Compare ( string? x , string? y )
{
if ( String . IsNullOrEmpty ( x ) & & String . IsNullOrEmpty ( y ) )
{
return 0 ;
}
if ( String . IsNullOrEmpty ( y ) )
{
return - 1 ;
}
if ( String . IsNullOrEmpty ( x ) )
{
return 1 ;
}
// IWYU header
if ( x . Contains ( IWYUFileName ) )
{
return - 1 ;
}
if ( y . Contains ( IWYUFileName ) )
{
return 1 ;
}
2022-10-26 17:48:47 -04:00
// system includes
if ( x . Contains ( '<' ) )
{
return 1 ;
}
if ( y . Contains ( '<' ) )
{
return - 1 ;
}
2022-10-18 01:22:38 -04:00
// generated header
if ( x . Contains ( ".generated.h" ) )
{
return 1 ;
}
if ( y . Contains ( ".generated.h" ) )
{
return - 1 ;
}
2022-10-26 17:48:47 -04:00
2023-05-30 18:59:32 -04:00
return String . Compare ( x , y ) ;
2022-10-18 01:22:38 -04:00
}
}
private void SortIncludes ( FileReference File , List < int > LinesUpdated , string [ ] Text )
{
2023-05-31 13:37:21 -04:00
HeaderSortComparison HeaderSort = new HeaderSortComparison ( File . GetFileNameWithoutExtension ( ) + ".h" ) ;
foreach ( int LineIndex in LinesUpdated )
2022-10-18 01:22:38 -04:00
{
2023-05-31 13:37:21 -04:00
int FirstIncludeIndex = LineIndex ;
2022-10-18 01:22:38 -04:00
for ( int i = LineIndex - 1 ; i > = 0 ; i - - )
{
Match IncludeMatch = IncludeRegex . Match ( Text [ i ] ) ;
if ( IncludeMatch . Success )
{
FirstIncludeIndex = i ;
}
else
{
break ;
}
}
2023-05-31 13:37:21 -04:00
int LastIncludeIndex = LineIndex ;
2022-10-18 01:22:38 -04:00
for ( int i = LineIndex + 1 ; i < Text . Length ; i + + )
{
Match IncludeMatch = IncludeRegex . Match ( Text [ i ] ) ;
if ( IncludeMatch . Success )
{
LastIncludeIndex = i ;
}
else
{
break ;
}
}
Array . Sort ( Text , FirstIncludeIndex , LastIncludeIndex - FirstIncludeIndex + 1 , HeaderSort ) ;
}
}
2022-10-15 02:12:12 -04:00
}
}