2021-08-24 10:39:13 -04:00
// Copyright Epic Games, Inc. All Rights Reserved.
using System ;
using System.Collections.Generic ;
using System.Text ;
using System.IO ;
using AutomationTool ;
using UnrealBuildTool ;
using EpicGames.Core ;
using UnrealBuildBase ;
using System.Linq ;
2023-03-08 12:43:35 -05:00
using Microsoft.Extensions.Logging ;
2021-08-24 10:39:13 -04:00
[Help("Create stub code for platform extension")]
[Help("Source", "Path to source .uplugin, .build.cs or .target.cs, or a source folder to search")]
[Help("Platform", "Platform(s) or Platform Groups to generate for")]
[Help("Project", "Optional path to project (only required if not creating code for Engine modules/plugins")]
[Help("SkipPluginModules", "Do not generate platform extension module files when generating a platform extension plugin")]
2021-10-25 20:05:28 -04:00
[Help("AllowOverwrite", "If target files already exist they'll be overwritten rather than skipped")]
2021-08-24 10:39:13 -04:00
[Help("AllowUnknownPlatforms", "Allow platform & platform groups that are not known, for example when generating code for extensions we do not have access to")]
2022-01-26 11:31:26 -05:00
[Help("AllowPlatformExtensionsAsParents", "When creating a platform extension from another platform extension, use the source platform as the parent")]
2021-08-24 10:39:13 -04:00
[Help("P4", "Create a changelist for the new files")]
2021-10-25 20:05:28 -04:00
[Help("CL", "Override the changelist #")]
2022-01-26 11:31:26 -05:00
public class CreatePlatformExtension : BuildCommand
2021-08-24 10:39:13 -04:00
{
2021-10-12 21:21:22 -04:00
readonly List < ModuleHostType > ModuleTypeDenyList = new List < ModuleHostType >
2021-08-24 10:39:13 -04:00
{
ModuleHostType . Developer ,
ModuleHostType . Editor ,
ModuleHostType . EditorNoCommandlet ,
ModuleHostType . EditorAndProgram ,
ModuleHostType . Program ,
} ;
ConfigHierarchy GameIni ;
DirectoryReference ProjectDir ;
2022-01-26 11:31:26 -05:00
DirectoryReference SourceDir ;
public List < string > NewFiles = new List < string > ( ) ;
public List < string > ModifiedFiles = new List < string > ( ) ;
public List < string > WritableFiles = new List < string > ( ) ; // for testing only
2021-10-25 20:05:28 -04:00
string [ ] Platforms ;
2022-01-26 11:31:26 -05:00
public bool bSkipPluginModules = false ;
public bool bOverwriteExistingFile = false ;
public bool bIsTest = false ;
public bool bAllowPlatformExtensionsAsParents = false ;
public int CL = - 1 ;
2021-08-24 10:39:13 -04:00
public override void ExecuteBuild ( )
{
// Parse the parameters
2022-01-26 11:31:26 -05:00
string [ ] SrcPlatforms = ParseParamValue ( "Platform" , "" ) . Split ( '+' , StringSplitOptions . RemoveEmptyEntries ) ;
string Source = ParseParamValue ( "Source" , "" ) ;
string Project = ParseParamValue ( "Project" , "" ) ;
2021-10-25 20:05:28 -04:00
bSkipPluginModules = ParseParam ( "SkipPluginModules" ) ;
bOverwriteExistingFile = ParseParam ( "AllowOverwrite" ) ;
CL = ParseParamInt ( "CL" , - 1 ) ;
2021-08-24 10:39:13 -04:00
// make sure we have somewhere to look
if ( string . IsNullOrEmpty ( Source ) )
{
2023-03-08 12:43:35 -05:00
Logger . LogError ( "No -Source= directory/file specified" ) ;
2021-08-24 10:39:13 -04:00
return ;
}
// Sanity check platforms list
2022-01-26 11:31:26 -05:00
SrcPlatforms = VerifyPlatforms ( SrcPlatforms ) ;
if ( SrcPlatforms . Length = = 0 )
2021-08-24 10:39:13 -04:00
{
2023-03-08 12:43:35 -05:00
Logger . LogError ( "Please specify at least one platform or platform group" ) ;
2021-08-24 10:39:13 -04:00
return ;
}
2022-01-26 11:31:26 -05:00
// cannot have both -P4 and -DebugTest
bIsTest = ParseParam ( "DebugTest" ) & & System . Diagnostics . Debugger . IsAttached ; //NB. -DebugTest is for debugging this program only
2021-10-25 20:05:28 -04:00
if ( CommandUtils . P4Enabled & & bIsTest )
{
2023-03-08 12:43:35 -05:00
Logger . LogError ( "Cannot specify both -P4 and -DebugTest" ) ;
2021-10-25 20:05:28 -04:00
return ;
}
// cannot have -CL without -P4
if ( ! CommandUtils . P4Enabled & & CL > = 0 )
{
2023-03-08 12:43:35 -05:00
Logger . LogError ( "-CL requires -P4" ) ;
2021-10-25 20:05:28 -04:00
return ;
}
// sanity check changelist
if ( CommandUtils . P4Enabled & & CL > = 0 )
{
bool bPending ;
if ( ! P4 . ChangeExists ( CL , out bPending ) | | ! bPending )
{
2023-03-08 12:43:35 -05:00
Logger . LogError ( "Changelist {CL} cannot be used or is not valid" , CL ) ;
2021-10-25 20:05:28 -04:00
return ;
}
}
2021-08-24 10:39:13 -04:00
// Generate the code
try
{
2022-01-26 11:31:26 -05:00
GeneratePlatformExtension ( Project , Source , SrcPlatforms ) ;
2021-08-24 10:39:13 -04:00
// check the generated files
2021-10-25 20:05:28 -04:00
if ( NewFiles . Count > 0 | | ModifiedFiles . Count > 0 )
2021-08-24 10:39:13 -04:00
{
// display final report
2023-03-08 12:43:35 -05:00
Logger . LogInformation ( "{Text}" , System . Environment . NewLine ) ;
Logger . LogInformation ( "{Text}" , System . Environment . NewLine ) ;
Logger . LogInformation ( "{Text}" , System . Environment . NewLine ) ;
Logger . LogInformation ( "{Text}" , "The following files should have been added or edited" + ( ( CL > 0 ) ? $" in changelist {CL}:" : ":" ) ) ;
2021-08-24 10:39:13 -04:00
foreach ( string NewFile in NewFiles )
{
2023-03-08 12:43:35 -05:00
Logger . LogInformation ( "\t{NewFile} (added)" , NewFile ) ;
2021-10-25 20:05:28 -04:00
}
foreach ( string ModifiedFile in ModifiedFiles )
{
2023-03-08 12:43:35 -05:00
Logger . LogInformation ( "\t{ModifiedFile} (edit)" , ModifiedFile ) ;
2021-08-24 10:39:13 -04:00
}
2023-03-08 12:43:35 -05:00
Logger . LogInformation ( "{Text}" , System . Environment . NewLine ) ;
Logger . LogInformation ( "{Text}" , System . Environment . NewLine ) ;
Logger . LogInformation ( "{Text}" , System . Environment . NewLine ) ;
Logger . LogWarning ( "*** It is strongly recommended that each file is manually verified! ***" ) ;
Logger . LogInformation ( "{Text}" , System . Environment . NewLine ) ;
Logger . LogInformation ( "{Text}" , System . Environment . NewLine ) ;
Logger . LogInformation ( "{Text}" , System . Environment . NewLine ) ;
2021-08-24 10:39:13 -04:00
// remove everything if requested (for debugging etc)
2021-10-25 20:05:28 -04:00
if ( bIsTest )
2021-08-24 10:39:13 -04:00
{
2023-03-08 12:43:35 -05:00
Logger . LogInformation ( "Deleting all the files because this is just a test..." ) ;
2021-08-24 10:39:13 -04:00
foreach ( string NewFile in NewFiles )
{
File . Delete ( NewFile ) ;
}
2021-10-25 20:05:28 -04:00
foreach ( string WritableFile in WritableFiles )
{
File . Delete ( WritableFile ) ;
File . Move ( WritableFile + ".tmp.bak" , WritableFile ) ;
File . SetAttributes ( WritableFile , File . GetAttributes ( WritableFile ) | FileAttributes . ReadOnly ) ;
}
2021-08-24 10:39:13 -04:00
}
}
}
catch ( Exception )
{
// something went wrong - clean up anything we've created so far
foreach ( string NewFile in NewFiles )
{
2023-03-08 12:43:35 -05:00
Logger . LogInformation ( "Removing partial file ${NewFile} due to error" , NewFile ) ;
2021-08-24 10:39:13 -04:00
File . Delete ( NewFile ) ;
}
2021-10-25 20:05:28 -04:00
foreach ( string WritableFile in WritableFiles )
{
2023-03-08 12:43:35 -05:00
Logger . LogInformation ( "Restoring read-only file ${WritableFile} due to error" , WritableFile ) ;
2021-10-25 20:05:28 -04:00
File . Delete ( WritableFile ) ;
File . Move ( WritableFile + ".tmp.bak" , WritableFile ) ;
File . SetAttributes ( WritableFile , File . GetAttributes ( WritableFile ) | FileAttributes . ReadOnly ) ;
}
2021-08-24 10:39:13 -04:00
2021-10-25 20:05:28 -04:00
// try to safely clean up the perforce changelist too, if it was not specified on the command line
2021-08-24 10:39:13 -04:00
try
{
2021-10-25 20:05:28 -04:00
if ( CL > 0 & & CommandUtils . P4Enabled & & ParseParamInt ( "CL" , - 1 ) ! = CL )
2021-08-24 10:39:13 -04:00
{
2023-03-08 12:43:35 -05:00
Logger . LogInformation ( "Removing partial changelist ${CL} due to error" , CL ) ;
2021-08-24 10:39:13 -04:00
P4 . DeleteChange ( CL , true ) ;
}
}
catch ( Exception e )
{
2023-03-08 12:43:35 -05:00
Logger . LogError ( "{Text}" , e . Message ) ;
2021-08-24 10:39:13 -04:00
}
throw ;
}
}
2022-01-26 11:31:26 -05:00
/// <summary>
/// Generates platform extensions from the given source
/// </summary>
public void GeneratePlatformExtension ( string InProject , string InSource , string [ ] InPlatforms )
{
// Prepare values
ProjectDir = string . IsNullOrEmpty ( InProject ) ? null : new FileReference ( InProject ) . Directory ;
GameIni = ConfigCache . ReadHierarchy ( ConfigHierarchyType . Game , ProjectDir , BuildHostPlatform . Current . Platform ) ;
Platforms = InPlatforms ;
if ( Directory . Exists ( InSource ) )
{
SourceDir = new DirectoryReference ( InSource ) ;
// check the directory for plugins first, because the plugins will automatically generate the modules too
List < string > Plugins = Directory . EnumerateFiles ( InSource , "*.uplugin" , SearchOption . AllDirectories ) . ToList ( ) ;
if ( Plugins . Count > 0 )
{
foreach ( string Plugin in Plugins )
{
GeneratePluginPlatformExtension ( new FileReference ( Plugin ) ) ;
}
}
else
{
// there were no plugins found, so search for module & target rules instead
List < string > ModuleRules = Directory . EnumerateFiles ( InSource , "*.build.cs" , SearchOption . AllDirectories ) . ToList ( ) ;
ModuleRules . AddRange ( Directory . EnumerateFiles ( InSource , "*.target.cs" , SearchOption . AllDirectories ) ) ;
if ( ModuleRules . Count > 0 )
{
foreach ( string ModuleRule in ModuleRules )
{
GenerateModulePlatformExtension ( new FileReference ( ModuleRule ) , Platforms ) ;
}
}
else
{
2023-03-08 12:43:35 -05:00
Logger . LogError ( "Cannot find any supported files in {InSource}" , InSource ) ;
2022-01-26 11:31:26 -05:00
}
}
}
else if ( File . Exists ( InSource ) )
{
FileReference Source = new FileReference ( InSource ) ;
SourceDir = Source . Directory ;
GeneratePlatformExtensionFromFile ( Source ) ;
}
else
{
2023-03-08 12:43:35 -05:00
Logger . LogError ( "Invalid path or file name {InSource}" , InSource ) ;
2022-01-26 11:31:26 -05:00
}
}
2021-08-24 10:39:13 -04:00
/// <summary>
/// Create the platform extension plugin files of the given plugin, for the given platforms
/// </summary>
2021-10-25 20:05:28 -04:00
private void GeneratePluginPlatformExtension ( FileReference PluginPath )
2021-08-24 10:39:13 -04:00
{
// sanity check plugin path
if ( ! File . Exists ( PluginPath . FullName ) )
{
2023-03-08 12:43:35 -05:00
Logger . LogError ( "File not found: {PluginPath}" , PluginPath ) ;
2021-08-24 10:39:13 -04:00
return ;
}
DirectoryReference PluginDir = PluginPath . Directory ;
if ( ProjectDir = = null & & ! PluginDir . IsUnderDirectory ( Unreal . EngineDirectory ) )
{
2023-03-08 12:43:35 -05:00
Logger . LogError ( "{PluginPath} is not under the Engine directory, and no -project= has been specified" , PluginPath ) ;
2021-08-24 10:39:13 -04:00
return ;
}
DirectoryReference RootDir = ProjectDir ? ? Unreal . EngineDirectory ;
if ( ! PluginDir . IsUnderDirectory ( RootDir ) )
{
2023-03-08 12:43:35 -05:00
Logger . LogError ( "{PluginPath} is not under {RootDir}" , PluginPath , RootDir ) ;
2021-08-24 10:39:13 -04:00
return ;
}
// load the plugin & find suitable modules, if required
2021-10-12 21:21:22 -04:00
PluginDescriptor ParentPlugin = PluginDescriptor . FromFile ( PluginPath ) ; //NOTE: if the PluginPath is itself a child plugin, not all allow list, deny list & supported platform information will be available.
List < PluginReferenceDescriptor > ParentPluginDescs = new List < PluginReferenceDescriptor > ( ) ;
2021-08-24 10:39:13 -04:00
Dictionary < ModuleDescriptor , FileReference > ParentModuleRules = new Dictionary < ModuleDescriptor , FileReference > ( ) ;
if ( ! bSkipPluginModules & & ParentPlugin . Modules ! = null )
{
// find all module rules that are listed in the plugin
DirectoryReference ModuleRulesPath = DirectoryReference . Combine ( PluginDir , "Source" ) ;
2021-10-25 20:05:28 -04:00
if ( DirectoryReference . Exists ( ModuleRulesPath ) )
2021-08-24 10:39:13 -04:00
{
2021-10-25 20:05:28 -04:00
var ModuleRules = DirectoryReference . EnumerateFiles ( ModuleRulesPath , "*.build.cs" , SearchOption . AllDirectories ) ;
foreach ( FileReference ModuleRule in ModuleRules )
2021-08-24 10:39:13 -04:00
{
2021-10-25 20:05:28 -04:00
string ModuleRuleName = GetPlatformExtensionBaseNameFromPath ( ModuleRule . FullName ) ;
ModuleDescriptor ModuleDesc = ParentPlugin . Modules . Find ( ParentModuleDesc = > ParentModuleDesc . Name . Equals ( ModuleRuleName , StringComparison . InvariantCultureIgnoreCase ) ) ;
if ( ModuleDesc ! = null )
{
ParentModuleRules . Add ( ModuleDesc , ModuleRule ) ;
}
2021-08-24 10:39:13 -04:00
}
}
}
2021-10-25 20:05:28 -04:00
bool bParentPluginDirty = false ;
2021-08-24 10:39:13 -04:00
// generate the platform extension files
string BasePluginName = GetPlatformExtensionBaseNameFromPath ( PluginPath . FullName ) ;
foreach ( string PlatformName in Platforms )
{
// verify final file name
2021-10-25 20:05:28 -04:00
string FinalFileName = MakePlatformExtensionPathFromSource ( RootDir , PluginDir , PlatformName , BasePluginName + "_" + PlatformName + ".uplugin" ) ;
if ( File . Exists ( FinalFileName ) & & ! ( bOverwriteExistingFile & & EditFile ( FinalFileName ) ) )
2021-08-24 10:39:13 -04:00
{
2023-03-08 12:43:35 -05:00
Logger . LogWarning ( "Skipping {FinalFileName} as it already exists" , FinalFileName ) ;
2021-08-24 10:39:13 -04:00
continue ;
}
// create the child plugin
Directory . CreateDirectory ( Path . GetDirectoryName ( FinalFileName ) ) ;
using ( JsonWriter ChildPlugin = new JsonWriter ( FinalFileName ) )
{
UnrealTargetPlatform Platform ;
bool bHasPlatform = UnrealTargetPlatform . TryParse ( PlatformName , out Platform ) ;
// a platform reference is needed if there are already platforms listed in the parent, or the parent requires an explicit platform list
2021-10-12 21:21:22 -04:00
bool NeedsPlatformReference < T > ( List < T > ParentPlatforms , bool bHasExplicitPlatforms )
2021-08-24 10:39:13 -04:00
{
return ( bHasPlatform & & ( ( ParentPlatforms ! = null & & ParentPlatforms . Count > 0 ) | | bHasExplicitPlatforms ) ) ;
}
// create the plugin definition
ChildPlugin . WriteObjectStart ( ) ;
ChildPlugin . WriteValue ( "FileVersion" , ( int ) PluginDescriptorVersion . ProjectPluginUnification ) ; // this is the version that this code has been tested against
ChildPlugin . WriteValue ( "bIsPluginExtension" , true ) ;
if ( NeedsPlatformReference ( ParentPlugin . SupportedTargetPlatforms , ParentPlugin . bHasExplicitPlatforms ) )
{
ChildPlugin . WriteStringArrayField ( "SupportedTargetPlatforms" , new string [ ] { Platform . ToString ( ) } ) ;
}
2021-10-25 20:05:28 -04:00
// select all modules that need child module references for this platform
IEnumerable < ModuleDescriptor > ModuleDescs = ParentPlugin . Modules ? . Where ( ModuleDesc = > ShouldCreateChildReferenceForModule ( ModuleDesc , bHasPlatform , Platform ) ) ;
2021-11-07 23:43:01 -05:00
if ( ModuleDescs ! = null & & ModuleDescs . Any ( ) )
2021-08-24 10:39:13 -04:00
{
ChildPlugin . WriteArrayStart ( "Modules" ) ;
foreach ( ModuleDescriptor ParentModuleDesc in ModuleDescs )
{
// create the child module reference
ChildPlugin . WriteObjectStart ( ) ;
ChildPlugin . WriteValue ( "Name" , ParentModuleDesc . Name ) ;
ChildPlugin . WriteValue ( "Type" , ParentModuleDesc . Type . ToString ( ) ) ;
2021-10-12 21:21:22 -04:00
if ( NeedsPlatformReference ( ParentModuleDesc . PlatformAllowList , ParentModuleDesc . bHasExplicitPlatforms ) )
2021-08-24 10:39:13 -04:00
{
2021-10-12 21:21:22 -04:00
ChildPlugin . WriteStringArrayField ( "PlatformAllowList" , new string [ ] { Platform . ToString ( ) } ) ;
2021-08-24 10:39:13 -04:00
}
2021-11-07 23:43:01 -05:00
else if ( NeedsPlatformReference ( ParentModuleDesc . PlatformDenyList , ParentModuleDesc . bHasExplicitPlatforms ) )
2021-10-25 20:05:28 -04:00
{
ChildPlugin . WriteStringArrayField ( "PlatformDenyList" , new string [ ] { Platform . ToString ( ) } ) ;
}
2021-08-24 10:39:13 -04:00
ChildPlugin . WriteObjectEnd ( ) ;
// see if there is a module rule file too & generate the rules file for this platform
FileReference ParentModuleRule ;
if ( ParentModuleRules . TryGetValue ( ParentModuleDesc , out ParentModuleRule ) )
{
GenerateModulePlatformExtension ( ParentModuleRule , new string [ ] { PlatformName } ) ;
}
2021-10-25 20:05:28 -04:00
// remove platform from parent plugin references
2022-06-07 04:34:07 -04:00
if ( bHasPlatform & & ParentModuleDesc . PlatformAllowList ! = null & & ParentModuleDesc . PlatformAllowList . Contains ( Platform ) )
2021-10-25 20:05:28 -04:00
{
2022-06-07 04:34:07 -04:00
ParentModuleDesc . PlatformAllowList . Remove ( Platform ) ;
ParentModuleDesc . bHasExplicitPlatforms | = ( ParentModuleDesc . PlatformAllowList . Count = = 0 ) ; // an empty list is interpreted as 'all platforms are allowed' otherwise
bParentPluginDirty = true ;
2021-10-25 20:05:28 -04:00
}
if ( bHasPlatform & & ParentModuleDesc . PlatformDenyList ! = null )
{
bParentPluginDirty | = ParentModuleDesc . PlatformDenyList . Remove ( Platform ) ;
}
2021-08-24 10:39:13 -04:00
}
ChildPlugin . WriteArrayEnd ( ) ;
}
2021-10-12 21:21:22 -04:00
2021-10-25 20:05:28 -04:00
// select all plugins that need child plugin references for this platform
IEnumerable < PluginReferenceDescriptor > PluginDescs = ParentPlugin . Plugins ? . Where ( PluginDesc = > ShouldCreateChildReferenceForDependentPlugin ( PluginDesc , bHasPlatform , Platform ) ) ;
2021-11-07 23:43:01 -05:00
if ( PluginDescs ! = null & & PluginDescs . Any ( ) )
2021-10-12 21:21:22 -04:00
{
ChildPlugin . WriteArrayStart ( "Plugins" ) ;
foreach ( PluginReferenceDescriptor ParentPluginDesc in PluginDescs )
{
// create the child plugin reference
ChildPlugin . WriteObjectStart ( ) ;
ChildPlugin . WriteValue ( "Name" , ParentPluginDesc . Name ) ;
ChildPlugin . WriteValue ( "Enabled" , ParentPluginDesc . bEnabled ) ;
2021-10-25 20:05:28 -04:00
if ( NeedsPlatformReference ( ParentPluginDesc . PlatformAllowList ? . ToList ( ) , ParentPluginDesc . bHasExplicitPlatforms ) )
2021-10-12 21:21:22 -04:00
{
ChildPlugin . WriteStringArrayField ( "PlatformAllowList" , new string [ ] { Platform . ToString ( ) } ) ;
}
2021-10-25 20:05:28 -04:00
if ( NeedsPlatformReference ( ParentPluginDesc . PlatformDenyList ? . ToList ( ) , ParentPluginDesc . bHasExplicitPlatforms ) )
{
ChildPlugin . WriteStringArrayField ( "PlatformDenyList" , new string [ ] { Platform . ToString ( ) } ) ;
}
2021-10-12 21:21:22 -04:00
ChildPlugin . WriteObjectEnd ( ) ;
2021-10-25 20:05:28 -04:00
// remove platform from parent plugin references
if ( bHasPlatform & & PlatformArrayContainsPlatform ( ParentPluginDesc . PlatformAllowList , Platform ) )
{
ParentPluginDesc . PlatformAllowList = ParentPluginDesc . PlatformAllowList . Where ( X = > ! X . Equals ( Platform . ToString ( ) ) ) . ToArray ( ) ;
2022-06-07 04:34:07 -04:00
ParentPluginDesc . bHasExplicitPlatforms | = ( ParentPluginDesc . PlatformAllowList . Length = = 0 ) ; // an empty list is interpreted as "all platforms" otherwise
2021-10-25 20:05:28 -04:00
bParentPluginDirty = true ;
}
if ( bHasPlatform & & PlatformArrayContainsPlatform ( ParentPluginDesc . PlatformDenyList , Platform ) )
{
ParentPluginDesc . PlatformDenyList = ParentPluginDesc . PlatformDenyList . Where ( X = > ! X . Equals ( Platform . ToString ( ) ) ) . ToArray ( ) ;
bParentPluginDirty = true ;
}
2021-10-12 21:21:22 -04:00
}
ChildPlugin . WriteArrayEnd ( ) ;
}
2021-08-24 10:39:13 -04:00
ChildPlugin . WriteObjectEnd ( ) ;
2021-10-25 20:05:28 -04:00
// remove platform from parent plugin, if necessary
2022-06-07 04:34:07 -04:00
if ( bHasPlatform & & ParentPlugin . SupportedTargetPlatforms ! = null & & ParentPlugin . SupportedTargetPlatforms . Contains ( Platform ) )
2021-10-25 20:05:28 -04:00
{
2022-06-07 04:34:07 -04:00
ParentPlugin . SupportedTargetPlatforms . Remove ( Platform ) ;
ParentPlugin . bHasExplicitPlatforms | = ( ParentPlugin . SupportedTargetPlatforms . Count = = 0 ) ; // an empty list is interpreted as "all platforms" otherwise
bParentPluginDirty = true ;
2021-10-25 20:05:28 -04:00
}
2021-08-24 10:39:13 -04:00
}
2021-10-25 20:05:28 -04:00
AddNewFile ( FinalFileName ) ;
}
// save parent plugin, if necessary
if ( bParentPluginDirty & & EditFile ( PluginPath . FullName ) )
{
ParentPlugin . Save ( PluginPath . FullName ) ;
2021-08-24 10:39:13 -04:00
}
}
/// <summary>
/// Creates the platform extension child class files of the given module, for the given platforms
/// </summary>
2021-10-25 20:05:28 -04:00
private void GenerateModulePlatformExtension ( FileReference ModulePath , string [ ] PlatformNames )
2021-08-24 10:39:13 -04:00
{
// sanity check module path
if ( ! File . Exists ( ModulePath . FullName ) )
{
2023-03-08 12:43:35 -05:00
Logger . LogError ( "File not found: {ModulePath}" , ModulePath ) ;
2021-08-24 10:39:13 -04:00
return ;
}
DirectoryReference ModuleDir = ModulePath . Directory ;
if ( ProjectDir = = null & & ! ModuleDir . IsUnderDirectory ( Unreal . EngineDirectory ) )
{
2023-03-08 12:43:35 -05:00
Logger . LogError ( "{ModulePath} is not under the Engine directory, and no -project= has been specified" , ModulePath ) ;
2021-08-24 10:39:13 -04:00
return ;
}
DirectoryReference RootDir = ProjectDir ? ? Unreal . EngineDirectory ;
if ( ! ModuleDir . IsUnderDirectory ( RootDir ) )
{
2023-03-08 12:43:35 -05:00
Logger . LogError ( "{ModulePath} is not under {RootDir}" , ModulePath , RootDir ) ;
2021-08-24 10:39:13 -04:00
return ;
}
// sanity check module file name
string ModuleFilename = ModulePath . GetFileName ( ) ;
string ModuleExtension = ModuleFilename . Substring ( ModuleFilename . IndexOf ( '.' ) ) ;
ModuleFilename = ModuleFilename . Substring ( 0 , ModuleFilename . Length - ModuleExtension . Length ) ;
if ( ! ModuleExtension . Equals ( ".build.cs" , System . StringComparison . InvariantCultureIgnoreCase ) & & ! ModuleExtension . Equals ( ".target.cs" , System . StringComparison . InvariantCultureIgnoreCase ) )
{
2023-03-08 12:43:35 -05:00
Logger . LogError ( "{ModulePath} is a module/rules file. Expecting .build.cs or .target.cs" , ModulePath ) ;
2021-08-24 10:39:13 -04:00
return ;
}
// load module file & find module class name, and optional class namespace
2022-03-15 12:25:04 -04:00
char [ ] ClassNameSeparators = new char [ ] { ' ' , '\t' , ':' } ;
2021-08-24 10:39:13 -04:00
const string ClassDeclaration = "public class " ;
const string NamespaceDeclaration = "namespace " ;
string [ ] ModuleContents = File . ReadAllLines ( ModulePath . FullName ) ;
string ModuleClassDeclaration = ModuleContents . FirstOrDefault ( L = > L . Trim ( ) . StartsWith ( ClassDeclaration ) ) ;
string ModuleNamespaceDeclaration = ModuleContents . FirstOrDefault ( L = > L . Trim ( ) . StartsWith ( NamespaceDeclaration ) ) ;
if ( string . IsNullOrEmpty ( ModuleClassDeclaration ) )
{
2023-03-08 12:43:35 -05:00
Logger . LogError ( "Cannot find class declaration in ${ModulePath}" , ModulePath ) ;
2021-08-24 10:39:13 -04:00
return ;
}
2022-03-15 12:25:04 -04:00
string ParentModuleName = ModuleClassDeclaration . Trim ( ) . Remove ( 0 , ClassDeclaration . Length ) . Split ( ClassNameSeparators , StringSplitOptions . None ) . Last ( ) ;
2022-01-26 11:31:26 -05:00
if ( bAllowPlatformExtensionsAsParents | | ParentModuleName . Equals ( "ModuleRules" ) | | ParentModuleName . Equals ( "TargetRules" ) )
{
2022-03-15 12:25:04 -04:00
ParentModuleName = ModuleClassDeclaration . Trim ( ) . Remove ( 0 , ClassDeclaration . Length ) . Split ( ClassNameSeparators , StringSplitOptions . None ) . First ( ) ;
2022-01-26 11:31:26 -05:00
}
2021-08-24 10:39:13 -04:00
if ( string . IsNullOrEmpty ( ParentModuleName ) )
{
2023-03-08 12:43:35 -05:00
Logger . LogError ( "Cannot parse class declaration in ${ModulePath}" , ModulePath ) ;
2021-08-24 10:39:13 -04:00
return ;
}
string ParentNamespace = string . IsNullOrEmpty ( ModuleNamespaceDeclaration ) ? "" : ( ModuleNamespaceDeclaration . Trim ( ) . Remove ( 0 , NamespaceDeclaration . Length ) . Split ( ' ' , StringSplitOptions . None ) . First ( ) + "." ) ;
string BaseModuleName = ParentModuleName ;
int Index = BaseModuleName . IndexOf ( '_' ) ; //trim off _[platform] suffix
if ( Index ! = - 1 )
{
BaseModuleName = BaseModuleName . Substring ( 0 , Index ) ;
}
2021-11-07 23:43:01 -05:00
BaseModuleName = BaseModuleName . TrimEnd ( new char [ ] { ':' } ) . Trim ( ) ; // trim off any : suffix
// ignore if it is the default namespace for build rules
if ( ParentNamespace = = "UnrealBuildTool.Rules." & & ModuleExtension . Equals ( ".build.cs" , System . StringComparison . InvariantCultureIgnoreCase ) )
{
ParentNamespace = "" ;
}
2021-08-24 10:39:13 -04:00
// load template and generate the platform extension files
string BaseModuleFileName = GetPlatformExtensionBaseNameFromPath ( ModulePath . FullName ) ;
string CopyrightLine = MakeCopyrightLine ( ) ;
string Template = LoadTemplate ( $"PlatformExtension{ModuleExtension}.template" ) ;
2021-10-25 20:05:28 -04:00
foreach ( string PlatformName in PlatformNames )
2021-08-24 10:39:13 -04:00
{
// verify the final file name
2021-10-25 20:05:28 -04:00
string FinalFileName = MakePlatformExtensionPathFromSource ( RootDir , ModuleDir , PlatformName , BaseModuleFileName + "_" + PlatformName + ModuleExtension ) ;
if ( File . Exists ( FinalFileName ) & & ! ( bOverwriteExistingFile & & EditFile ( FinalFileName ) ) )
2021-08-24 10:39:13 -04:00
{
2023-03-08 12:43:35 -05:00
Logger . LogWarning ( "Skipping {FinalFileName} as it already exists" , FinalFileName ) ;
2021-08-24 10:39:13 -04:00
continue ;
}
// generate final code from the template
string FinalOutput = Template ;
FinalOutput = FinalOutput . Replace ( "%COPYRIGHT_LINE%" , CopyrightLine , StringComparison . InvariantCultureIgnoreCase ) ;
FinalOutput = FinalOutput . Replace ( "%PARENT_MODULE_NAME%" , ParentNamespace + ParentModuleName , StringComparison . InvariantCultureIgnoreCase ) ;
FinalOutput = FinalOutput . Replace ( "%BASE_MODULE_NAME%" , BaseModuleName , StringComparison . InvariantCultureIgnoreCase ) ;
FinalOutput = FinalOutput . Replace ( "%PLATFORM_NAME%" , PlatformName , StringComparison . InvariantCultureIgnoreCase ) ;
// save the child .cs file
Directory . CreateDirectory ( Path . GetDirectoryName ( FinalFileName ) ) ;
File . WriteAllText ( FinalFileName , FinalOutput ) ;
2021-10-25 20:05:28 -04:00
AddNewFile ( FinalFileName ) ;
2021-08-24 10:39:13 -04:00
}
}
/// <summary>
/// Generates platform extension files based on the given source file name
/// </summary>
2021-10-25 20:05:28 -04:00
private void GeneratePlatformExtensionFromFile ( FileReference Source )
2021-08-24 10:39:13 -04:00
{
if ( Source . FullName . ToLower ( ) . EndsWith ( ".uplugin" ) )
{
2021-10-25 20:05:28 -04:00
GeneratePluginPlatformExtension ( Source ) ;
2021-08-24 10:39:13 -04:00
}
else if ( Source . FullName . ToLower ( ) . EndsWith ( ".build.cs" ) | | Source . FullName . ToLower ( ) . EndsWith ( ".target.cs" ) )
{
GenerateModulePlatformExtension ( Source , Platforms ) ;
}
else
{
2023-03-08 12:43:35 -05:00
Logger . LogError ( "unsupported file type {Source}" , Source ) ;
2021-08-24 10:39:13 -04:00
}
}
#region boilerplate & helpers
/// <summary>
2021-10-25 20:05:28 -04:00
/// Determines whether we should attempt to add a child module reference for the given plugin module
2021-08-24 10:39:13 -04:00
/// </summary>
/// <param name="ModuleDesc"></param>
2024-06-10 21:51:45 -04:00
/// <param name="bHasPlatform"></param>
2021-10-25 20:05:28 -04:00
/// <param name="Platform"></param>
2021-08-24 10:39:13 -04:00
/// <returns></returns>
2021-10-25 20:05:28 -04:00
private bool ShouldCreateChildReferenceForModule ( ModuleDescriptor ModuleDesc , bool bHasPlatform , UnrealTargetPlatform Platform )
2021-08-24 10:39:13 -04:00
{
// make sure it's a type that is usually associated with platform extensions
2021-10-12 21:21:22 -04:00
if ( ModuleTypeDenyList . Contains ( ModuleDesc . Type ) )
2021-08-24 10:39:13 -04:00
{
return false ;
}
// this module must have supported platforms explicitly listed so we must create a child reference
if ( ModuleDesc . bHasExplicitPlatforms )
{
return true ;
}
2021-10-12 21:21:22 -04:00
// the module has a non-empty platform allow list so we must create a child reference
2021-10-25 20:05:28 -04:00
if ( ModuleDesc . PlatformAllowList ! = null & & ModuleDesc . PlatformAllowList . Count > 0 )
{
return true ;
}
// the module has a non-empty platform deny list that explicitly mentions this platform
if ( bHasPlatform & & ModuleDesc . PlatformDenyList ! = null & & ModuleDesc . PlatformDenyList . Contains ( Platform ) )
2021-08-24 10:39:13 -04:00
{
return true ;
}
2021-10-12 21:21:22 -04:00
// the module has an empty platform allow list so no explicit platform reference is needed
return false ;
}
/// <summary>
/// Determines whether we should attempt to add this dependent plugin module to the child plugin references
/// </summary>
/// <param name="PluginDesc"></param>
2024-06-10 21:51:45 -04:00
/// <param name="bHasPlatform"></param>
/// <param name="Platform"></param>
2021-10-12 21:21:22 -04:00
/// <returns></returns>
2021-10-25 20:05:28 -04:00
private bool ShouldCreateChildReferenceForDependentPlugin ( PluginReferenceDescriptor PluginDesc , bool bHasPlatform , UnrealTargetPlatform Platform )
2021-10-12 21:21:22 -04:00
{
// this plugin reference must have supported platforms explicitly listed so we must create a child reference
if ( PluginDesc . bHasExplicitPlatforms )
{
return true ;
}
// the plugin reference has a non-empty platform allow list so we must create a child reference
2021-10-25 20:05:28 -04:00
if ( PluginDesc . PlatformAllowList ! = null & & PluginDesc . PlatformAllowList . Length > 0 )
{
return true ;
}
// the plugin reference has a non-empty platform deny list that explicitly mentions this platform
if ( bHasPlatform & & PlatformArrayContainsPlatform ( PluginDesc . PlatformDenyList , Platform ) )
2021-10-12 21:21:22 -04:00
{
return true ;
}
// the plugin reference has an empty platform allow list so no explicit platform reference is needed
2021-08-24 10:39:13 -04:00
return false ;
}
/// <summary>
2024-06-10 21:51:45 -04:00
/// Generates the final platform extension file path for the given source directory, platform and filename
2021-08-24 10:39:13 -04:00
/// </summary>
2021-10-25 20:05:28 -04:00
private string MakePlatformExtensionPathFromSource ( DirectoryReference RootDir , DirectoryReference SourceDir , string PlatformName , string Filename )
2021-08-24 10:39:13 -04:00
{
2021-10-25 20:05:28 -04:00
string BaseDir = SourceDir . MakeRelativeTo ( RootDir )
. Replace ( Path . AltDirectorySeparatorChar , Path . DirectorySeparatorChar ) ;
// handle Restricted folders first - need to keep the restricted folder at the start of the path
string OptionalRestrictedDir = "" ;
if ( BaseDir . StartsWith ( "Restricted" + Path . DirectorySeparatorChar ) )
2021-08-24 10:39:13 -04:00
{
2021-10-25 20:05:28 -04:00
string [ ] RestrictedFragments = BaseDir . Split ( Path . DirectorySeparatorChar , 3 ) ; // 3 = Restricted/NotForLicensees/<remainder...>
if ( RestrictedFragments . Length > 0 )
{
OptionalRestrictedDir = Path . Combine ( RestrictedFragments . SkipLast ( 1 ) . ToArray ( ) ) ; // Restricted/NotForLicensees
BaseDir = RestrictedFragments . Last ( ) ; // remainder
}
2021-08-24 10:39:13 -04:00
}
2021-10-25 20:05:28 -04:00
// handle Platform folders next - trim off the source platform
if ( BaseDir . StartsWith ( "Platforms" + Path . DirectorySeparatorChar ) )
{
string [ ] PlatformFragments = BaseDir . Split ( Path . DirectorySeparatorChar , 3 ) ; // 3 = Platforms/<platform>/<remainder...>
if ( PlatformFragments . Length > 0 )
{
BaseDir = PlatformFragments . Last ( ) ; //remainder
}
}
// build the final path
return Path . Combine ( RootDir . FullName , OptionalRestrictedDir , "Platforms" , PlatformName , BaseDir , Filename ) ;
2021-08-24 10:39:13 -04:00
}
/// <summary>
/// Given a full path to a plugin or module file, returns the raw file name - trimming off any _[platform] suffix too
/// </summary>
private string GetPlatformExtensionBaseNameFromPath ( string FileName )
{
// trim off path
string BaseName = Path . GetFileName ( FileName ) ;
// trim off any extensions
string Extensions = BaseName . Substring ( BaseName . IndexOf ( '.' ) ) ;
BaseName = BaseName . Substring ( 0 , BaseName . Length - Extensions . Length ) ;
// trim off any platform suffix
int Idx = BaseName . IndexOf ( '_' ) ;
if ( Idx ! = - 1 )
{
BaseName = BaseName . Substring ( 0 , Idx ) ;
}
return BaseName ;
}
/// <summary>
/// Load the given file from the engine templates folder
/// </summary>
private string LoadTemplate ( string FileName )
{
string TemplatePath = Path . Combine ( Unreal . EngineDirectory . FullName , "Content" , "Editor" , "Templates" , FileName ) ;
return File . ReadAllText ( TemplatePath ) ;
}
/// <summary>
/// Look up the project/engine specific copyright string
/// </summary>
private string MakeCopyrightLine ( )
{
string CopyrightNotice = "" ;
GameIni . GetString ( "/Script/EngineSettings.GeneralProjectSettings" , "CopyrightNotice" , out CopyrightNotice ) ;
if ( ! string . IsNullOrEmpty ( CopyrightNotice ) )
{
return "// " + CopyrightNotice ;
}
else
{
return "" ;
}
}
2021-10-25 20:05:28 -04:00
/// <summary>
/// Returns whether there is a valid changelist for adding files to
/// </summary>
/// <returns></returns>
private bool HasChangelist ( )
{
if ( ! CommandUtils . P4Enabled )
{
return false ;
}
if ( CL = = - 1 )
{
// add the files to perforce if that is available
string Description = $"[AUTO-GENERATED] {string.Join('+', Platforms)} platform extension files from {SourceDir.MakeRelativeTo(Unreal.RootDirectory)}\n\n#nocheckin verify the code has been generated successfully before checking in!" ;
CL = P4 . CreateChange ( P4Env . Client , Description ) ;
}
return ( CL ! = - 1 ) ;
}
/// <summary>
/// Adds the given file to the list of new files, optionally adding to the current changelist if applicable
/// </summary>
/// <param name="NewFile"></param>
private void AddNewFile ( string NewFile )
{
if ( ModifiedFiles . Contains ( NewFile ) | | NewFiles . Contains ( NewFile ) )
{
return ;
}
if ( HasChangelist ( ) )
{
P4 . Add ( CL , CommandUtils . MakePathSafeToUseWithCommandLine ( NewFile ) ) ;
}
NewFiles . Add ( NewFile ) ;
}
/// <summary>
/// Attempts to edit the given file, optionally checking it out if applicable. If this is just a test, read-only files are backed up and made writable
/// </summary>
/// <param name="ExistingFile"></param>
/// <returns></returns>
private bool EditFile ( string ExistingFile )
{
if ( ModifiedFiles . Contains ( ExistingFile ) | | NewFiles . Contains ( ExistingFile ) )
{
return true ;
}
if ( HasChangelist ( ) )
{
P4 . Edit ( CL , CommandUtils . MakePathSafeToUseWithCommandLine ( ExistingFile ) ) ;
}
if ( ( File . GetAttributes ( ExistingFile ) & FileAttributes . ReadOnly ) = = FileAttributes . ReadOnly )
{
if ( bIsTest )
{
File . Copy ( ExistingFile , ExistingFile + ".tmp.bak" ) ;
// make the file writable if this is a test
WritableFiles . Add ( ExistingFile ) ;
File . SetAttributes ( ExistingFile , File . GetAttributes ( ExistingFile ) & ~ FileAttributes . ReadOnly ) ;
}
else
{
2023-03-08 12:43:35 -05:00
Logger . LogWarning ( "Cannot edit {ExistingFile} because it is read-only" , ExistingFile ) ;
2021-10-25 20:05:28 -04:00
return false ;
}
}
ModifiedFiles . Add ( ExistingFile ) ;
return true ;
}
/// <summary>
/// Heler function to see if the given platform name array contains the given platform
/// </summary>
/// <param name="PlatformNames"></param>
/// <param name="Platform"></param>
/// <returns></returns>
private bool PlatformArrayContainsPlatform ( string [ ] PlatformNames , UnrealTargetPlatform Platform )
{
return PlatformNames ! = null & & PlatformNames . Any ( PlatformName = > PlatformName . Equals ( Platform . ToString ( ) , StringComparison . InvariantCultureIgnoreCase ) ) ;
}
2021-08-24 10:39:13 -04:00
/// <summary>
/// Returns a list of validated and case-corrected platform and platform groups
/// </summary>
private string [ ] VerifyPlatforms ( string [ ] Platforms )
{
2021-10-25 20:05:28 -04:00
bool bAllowUnknownPlatforms = ParseParam ( "AllowUnknownPlatforms" ) ;
2021-08-24 10:39:13 -04:00
List < string > Result = new List < string > ( ) ;
foreach ( string PlatformName in Platforms )
{
// see if this is a platform
UnrealTargetPlatform Platform ;
if ( UnrealTargetPlatform . TryParse ( PlatformName , out Platform ) )
{
Result . Add ( Platform . ToString ( ) ) ;
continue ;
}
// see if this is a platform group
UnrealPlatformGroup PlatformGroup ;
if ( UnrealPlatformGroup . TryParse ( PlatformName , out PlatformGroup ) )
{
Result . Add ( PlatformGroup . ToString ( ) ) ;
continue ;
}
// this is an unknown item - see if we will accept it anyway...
if ( bAllowUnknownPlatforms )
{
2023-03-08 12:43:35 -05:00
Logger . LogWarning ( "{PlatformName} is not a known Platform or Platform Group. The code will still be generated but you may not be able to test it locally" , PlatformName ) ;
2021-08-24 10:39:13 -04:00
Result . Add ( PlatformName ) ;
}
else
{
2023-03-08 12:43:35 -05:00
Logger . LogWarning ( "{PlatformName} is not a known Platform or Platform Group and so it will be ignored. Specify -AllowUnknownPlatforms to allow it anyway" , PlatformName ) ;
2021-08-24 10:39:13 -04:00
}
}
return Result . ToArray ( ) ;
}
#endregion
}