2023-05-10 16:53:20 -04:00
// Copyright Epic Games, Inc. All Rights Reserved.
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Text ;
using EpicGames.Core ;
using Microsoft.CodeAnalysis ;
2023-05-30 18:38:07 -04:00
using Microsoft.Extensions.Logging ;
using UnrealBuildBase ;
2023-05-10 16:53:20 -04:00
/ * * * * *
Here ' s how this works :
* An XcodeProjectFile ( subclass of generic ProjectFile class ) is created , along with it - UnrealData and XcodeFileCollection objects are made
* High level code calls AddModule ( ) which this code will use to cache information about the Modules in the project ( including build settings , etc )
* These are used to determine what source files can be indexed together ( we use native xcode code compilation for indexing , so we make compiling succesful for best index )
* A few # defines are removed or modified ( FOO_API , etc ) which would otherwise make every module a separate target
* High level code then calls WriteProjectFile ( ) which is the meat of all this
* This code then creates a hierarchy / reference - chain of xcode project nodes ( an xcode project is basically a series of Guid / object pairs that reference each other )
* Then each node writes itself into the project file that is saved to disk
. xcconfig files :
* We also now use Xcconfig files for all of the build settings , instead of jamming them into the xcode project file itself
* This makes it easier to see the various settings ( you can also see them in the Xcode UI as before , but now with more read - only columns - they must be set via editing the file )
* The files are in the Xcode file browsing pane , in the an Xcconfigs folder under each project
* The files are :
* _Project - settings apply to all targets in the projectapplies to all targets ) , one for each configuration ( Debug , Development Editor , etc ) which applies to all targets
* _Debug / _DevelopmentEditor , etc - applies to all targets when building in that configuration
* _Run - applies to the native run project , which does no compiling
* _Index / SubIndex0 , etc - applies when Indexing , which uses native Xcode compiling ( these will include # defines from UBT , etc )
* There is currently no _Build xcconfig file since it ' s a makefile project and has no build setting needs
Known issues :
* No PBXFrameworksBuildPhase nodes are made
Future ideas :
* I have started working on a Template project that we merge Build / Index targets into , which will allow a licensee to setup codesigning , add extensions , etc .
* Always make a final build xcodeproj from UBT for handling the codesigning / extensions / frameworks
* Allow for non - conflicting # defines to share SubIndex targets , hopefully will greatly reduce the sub targets in UE5
* * /
namespace UnrealBuildTool.XcodeProjectXcconfig
{
class UnrealBuildConfig
{
2023-06-14 03:54:47 -04:00
public UnrealBuildConfig ( string InDisplayName , string InBuildTarget , string InExeName , ProjectTarget ? InProjectTarget , UnrealTargetConfiguration InBuildConfig , DirectoryReference InRootDirectory )
2023-05-10 16:53:20 -04:00
{
DisplayName = InDisplayName ;
BuildTarget = InBuildTarget ;
2023-05-25 13:04:27 -04:00
ExeName = InExeName ;
2023-05-10 16:53:20 -04:00
ProjectTarget = InProjectTarget ;
BuildConfig = InBuildConfig ;
2023-06-14 03:54:47 -04:00
RootDirectory = InRootDirectory ;
2023-05-10 16:53:20 -04:00
if ( BuildTarget ! = ProjectTarget ? . Name )
{
throw new BuildException ( $"Name exepcted to match - {BuildTarget} != {ProjectTarget?.Name}" ) ;
}
}
public string DisplayName ;
public string BuildTarget ;
2023-05-25 13:04:27 -04:00
public string ExeName ;
2023-05-10 16:53:20 -04:00
public ProjectTarget ? ProjectTarget ;
public UnrealTargetConfiguration BuildConfig ;
2023-06-14 03:54:47 -04:00
public DirectoryReference RootDirectory ;
2023-05-10 16:53:20 -04:00
public bool bSupportsMac = > Supports ( UnrealTargetPlatform . Mac ) ;
public bool bSupportsIOS = > Supports ( UnrealTargetPlatform . IOS ) ;
public bool bSupportsTVOS = > Supports ( UnrealTargetPlatform . TVOS ) ;
public bool Supports ( UnrealTargetPlatform ? Platform )
{
return UnrealData . Supports ( Platform ) & & ( ProjectTarget = = null | | Platform = = null | | ProjectTarget . SupportedPlatforms . Contains ( ( UnrealTargetPlatform ) Platform ) ) ;
}
} ;
class UnrealBatchedFiles
{
// build settings that cause uniqueness
public IEnumerable < String > ? ForceIncludeFiles = null ;
// @todo can we actually use this effectively with indexing other than fotced include?
public FileReference ? PCHFile = null ;
public bool bEnableRTTI = false ;
// union of settings for all modules
public HashSet < string > AllDefines = new ( ) { "__INTELLISENSE__" , "MONOLITHIC_BUILD=1" } ;
public HashSet < DirectoryReference > SystemIncludePaths = new ( ) ;
public HashSet < DirectoryReference > UserIncludePaths = new ( ) ;
public List < XcodeSourceFile > Files = new ( ) ;
public UEBuildModuleCPP Module ;
public FileReference ResponseFile ;
public UnrealBatchedFiles ( UnrealData UnrealData , int Index , UEBuildModuleCPP Module )
{
this . Module = Module ;
ResponseFile = FileReference . Combine ( UnrealData . XcodeProjectFileLocation . ParentDirectory ! , "ResponseFiles" , $"{UnrealData.ProductName}{Index}.response" ) ;
}
public void GenerateResponseFile ( )
{
StringBuilder ResponseFileContents = new ( ) ;
ResponseFileContents . Append ( "-isystem" ) ;
ResponseFileContents . AppendJoin ( " -isystem" , SystemIncludePaths . Select ( x = > x . FullName . Contains ( ' ' ) ? $"\" { x . FullName } \ "" : x . FullName ) ) ;
ResponseFileContents . Append ( " -I" ) ;
ResponseFileContents . AppendJoin ( " -I" , UserIncludePaths . Select ( x = > x . FullName . Contains ( ' ' ) ? $"\" { x . FullName } \ "" : x . FullName ) ) ;
if ( ForceIncludeFiles ! = null )
{
ResponseFileContents . Append ( " -include " ) ;
ResponseFileContents . AppendJoin ( " -include " , ForceIncludeFiles . Select ( x = > x . Contains ( ' ' ) ? $"\" { x } \ "" : x ) ) ;
}
ResponseFileContents . Append ( " -D" ) ;
ResponseFileContents . AppendJoin ( " -D" , AllDefines ) ;
if ( PCHFile ! = null )
{
ResponseFileContents . Append ( $" -include {PCHFile.FullName}" ) ;
}
ResponseFileContents . Append ( bEnableRTTI ? " -frtti" : " -fno-rtti" ) ;
DirectoryReference . CreateDirectory ( ResponseFile . Directory ) ;
FileReference . WriteAllText ( ResponseFile , ResponseFileContents . ToString ( ) ) ;
}
}
enum MetadataPlatform
{
MacEditor ,
Mac ,
IOS ,
}
enum MetadataMode
{
Unset = - 1 ,
UsePremade ,
UpdateTemplate ,
}
class MetadataItem
{
public MetadataMode Mode = MetadataMode . Unset ;
public FileReference ? File = null ;
public string? XcodeProjectRelative = null ;
//public Metadata(MetadataMode Mode, FileReference File, DirectoryReference XcodeProjectFile)
//{
// this.Mode = Mode;
// this.File = File;
// XcodeProjectRelative = File.MakeRelativeTo(XcodeProjectFile.ParentDirectory!);
//}
// Location: Can be key of setting entry in .ini, or the full file path
// CopyFromFolderIfNotFound: If the file at "Location" does not exist, try to copy the same named file from this folder
public MetadataItem ( DirectoryReference ProductDirectory , DirectoryReference XcodeProject , ConfigHierarchy Ini , string Location , MetadataMode InMode , DirectoryReference CopyFromFolderIfNotFound )
{
// no extension means it's a .ini entry
2023-06-20 16:40:51 -04:00
if ( Path . GetExtension ( Location ) . Length = = 0 )
2023-05-10 16:53:20 -04:00
{
string? FileLocation ;
2023-06-20 16:40:51 -04:00
if ( Ini . TryGetValue ( "/Script/MacTargetPlatform.XcodeProjectSettings" , Location , out FileLocation ) & & FileLocation . Length > 0 )
2023-05-10 16:53:20 -04:00
{
2023-06-20 16:40:51 -04:00
File = AppleExports . ConvertFilePath ( ProductDirectory , FileLocation ) ;
2023-05-10 16:53:20 -04:00
}
}
else
{
File = new FileReference ( Location ) ;
}
if ( File ! = null )
{
// Copy from source location if no such file exist
// Except UBTGenerated, they will be generated during UBT runs
if ( ! FileReference . Exists ( File ) & & ! File . ContainsName ( "UBTGenerated" , 0 ) )
{
FileReference SourceFile = FileReference . Combine ( CopyFromFolderIfNotFound , File . GetFileName ( ) ) ;
if ( FileReference . Exists ( SourceFile ) )
{
DirectoryReference . CreateDirectory ( File . Directory ) ;
try
{
FileReference . Copy ( SourceFile , File , false ) ;
}
catch ( System . IO . IOException ex )
{
if ( ex . Message . Contains ( "already exists" ) )
{
// Some other thread is probably copying the same file, ignore
}
else
{
throw ex ;
}
}
}
}
}
if ( File ! = null & & ( File . ContainsName ( "UBTGenerated" , 0 ) | | FileReference . Exists ( File ) ) )
{
// We found a valid metadata file
Mode = InMode ;
XcodeProjectRelative = File . MakeRelativeTo ( XcodeProject . ParentDirectory ! ) ;
}
else
{
// Either key is missing, or file is missing
Mode = MetadataMode . Unset ;
}
}
}
class Metadata
{
public Dictionary < MetadataPlatform , MetadataItem > PlistFiles = new ( ) ;
public Dictionary < MetadataPlatform , MetadataItem > EntitlementsFiles = new ( ) ;
public Dictionary < MetadataPlatform , MetadataItem > ShippingEntitlementsFiles = new ( ) ;
public Metadata ( DirectoryReference ProductDirectory , DirectoryReference XcodeProject , ConfigHierarchy Ini , bool bSupportsMac , bool bSupportsIOSOrTVOS , ILogger Logger )
{
DirectoryReference ResourceFolder = DirectoryReference . Combine ( Unreal . EngineDirectory , "Build/Mac/Resources/" ) ;
if ( bSupportsMac )
{
// All editor use template plist which is just a copy of default info.plist, user should not need to modify or change this
// TEMP: currently cooked editor (e.g. QAGCookedEditor) uses this hardcoded file too, we should find a way to expose this in Game/Config/DefaultEngine.ini
PlistFiles [ MetadataPlatform . MacEditor ] = new MetadataItem ( ProductDirectory , XcodeProject , Ini ,
Unreal . EngineDirectory . FullName + "/Intermediate/Build/Mac/Resources/Info-Editor.Template.plist" ,
MetadataMode . UpdateTemplate ,
ResourceFolder ) ;
if ( ProductDirectory = = Unreal . EngineDirectory )
{
// Engine dir is the same as Product dir, meaning no uproject (UnrealGame.app)
// Don't use ini value in this case, since ini value points at /Game/, use this hardcoded location instead
PlistFiles [ MetadataPlatform . Mac ] = new MetadataItem ( ProductDirectory , XcodeProject , Ini ,
Unreal . EngineDirectory . FullName + "/Intermediate/Build/Mac/Resources/Info.Template.plist" ,
MetadataMode . UpdateTemplate ,
ResourceFolder ) ;
}
else
{
// E.g. QAGame.app
// Try Premade first
PlistFiles [ MetadataPlatform . Mac ] = new MetadataItem ( ProductDirectory , XcodeProject , Ini ,
"PremadeMacPlist" ,
MetadataMode . UsePremade ,
ResourceFolder ) ;
if ( PlistFiles [ MetadataPlatform . Mac ] . Mode = = MetadataMode . Unset )
{
// Premade not found, try Template
PlistFiles [ MetadataPlatform . Mac ] = new MetadataItem ( ProductDirectory , XcodeProject , Ini ,
"TemplateMacPlist" ,
MetadataMode . UpdateTemplate ,
ResourceFolder ) ;
}
}
EntitlementsFiles [ MetadataPlatform . MacEditor ] = new MetadataItem ( ProductDirectory , XcodeProject , Ini ,
"PremadeMacEditorEntitlements" ,
MetadataMode . UsePremade ,
ResourceFolder ) ;
EntitlementsFiles [ MetadataPlatform . Mac ] = new MetadataItem ( ProductDirectory , XcodeProject , Ini ,
"PremadeMacEntitlements" ,
MetadataMode . UsePremade ,
ResourceFolder ) ;
ShippingEntitlementsFiles [ MetadataPlatform . MacEditor ] = new MetadataItem ( ProductDirectory , XcodeProject , Ini ,
"ShippingSpecificMacEditorEntitlements" ,
MetadataMode . UsePremade ,
ResourceFolder ) ;
ShippingEntitlementsFiles [ MetadataPlatform . Mac ] = new MetadataItem ( ProductDirectory , XcodeProject , Ini ,
"ShippingSpecificMacEntitlements" ,
MetadataMode . UsePremade ,
ResourceFolder ) ;
}
if ( bSupportsIOSOrTVOS )
{
// Try Premade first
PlistFiles [ MetadataPlatform . IOS ] = new MetadataItem ( ProductDirectory , XcodeProject , Ini ,
"PremadeIOSPlist" ,
MetadataMode . UsePremade ,
ResourceFolder ) ;
if ( PlistFiles [ MetadataPlatform . IOS ] . Mode = = MetadataMode . Unset )
{
// Premade not found, try Template
PlistFiles [ MetadataPlatform . IOS ] = new MetadataItem ( ProductDirectory , XcodeProject , Ini ,
"TemplateIOSPlist" ,
MetadataMode . UpdateTemplate ,
ResourceFolder ) ;
}
EntitlementsFiles [ MetadataPlatform . IOS ] = new MetadataItem ( ProductDirectory , XcodeProject , Ini ,
"PremadeIOSEntitlements" ,
MetadataMode . UsePremade ,
ResourceFolder ) ;
ShippingEntitlementsFiles [ MetadataPlatform . IOS ] = new MetadataItem ( ProductDirectory , XcodeProject , Ini ,
"ShippingSpecificIOSEntitlements" ,
MetadataMode . UsePremade ,
ResourceFolder ) ;
}
}
}
class UnrealData
{
public bool bIsStubProject ;
public bool bIsForeignProject ;
public bool bMakeProjectPerTarget ;
2023-05-25 13:04:27 -04:00
public bool bIsContentOnlyProject ;
2023-05-10 16:53:20 -04:00
public TargetRules TargetRules = > _TargetRules ! ;
2023-06-14 03:54:47 -04:00
bool bIsAppBundle ;
2023-05-10 16:53:20 -04:00
public bool bUseAutomaticSigning = false ;
public bool bIsMergingProjects = false ;
public bool bWriteCodeSigningSettings = true ;
public Metadata ? Metadata ;
public List < UnrealBuildConfig > AllConfigs = new ( ) ;
public List < UnrealBatchedFiles > BatchedFiles = new ( ) ;
public List < string > ExtraPreBuildScriptLines = new ( ) ;
public FileReference ? UProjectFileLocation = null ;
public DirectoryReference XcodeProjectFileLocation ;
public DirectoryReference ProductDirectory ;
// settings read from project configs
public IOSProjectSettings ? IOSProjectSettings ;
public TVOSProjectSettings ? TVOSProjectSettings ;
// Name of the product (usually the project name, but UE5.xcodeproj is actually UnrealGame product)
public string ProductName ;
// Name of the xcode project
public string XcodeProjectName ;
// Display name, can be overridden from commandline
public string DisplayName ;
/// <summary>
/// Used to mark the project for distribution (some platforms require this)
/// </summary>
public bool bForDistribution = false ;
/// <summary>
/// Override for bundle identifier
/// </summary>
public string BundleIdentifier = "" ;
/// <summary>
/// Override AppName
/// </summary>
public string AppName = "" ;
/// <summary>
/// Architectures supported for iOS
/// </summary>
public string [ ] SupportedIOSArchitectures = { "arm64" } ;
/// <summary>
/// UBT logger object
/// </summary>
public ILogger ? Logger ;
private XcodeProjectFile ? ProjectFile ;
private TargetRules ? _TargetRules ;
public static bool bSupportsMac = > Supports ( UnrealTargetPlatform . Mac ) ;
public static bool bSupportsIOS = > Supports ( UnrealTargetPlatform . IOS ) ;
public static bool bSupportsTVOS = > Supports ( UnrealTargetPlatform . TVOS ) ;
public static bool Supports ( UnrealTargetPlatform ? Platform )
{
return Platform = = null | | XcodeProjectFileGenerator . XcodePlatforms . Contains ( ( UnrealTargetPlatform ) Platform ) ;
}
2023-06-14 03:54:47 -04:00
public bool IsAppBundle ( UnrealTargetPlatform Platform )
{
2023-06-23 20:29:27 -04:00
if ( Platform = = UnrealTargetPlatform . IOS | | Platform = = UnrealTargetPlatform . TVOS )
2023-06-14 03:54:47 -04:00
{
2023-06-23 20:29:27 -04:00
// iOS and TvOS always need app bundles
2023-06-14 03:54:47 -04:00
return true ;
}
return bIsAppBundle ;
}
2023-05-25 13:04:27 -04:00
public UnrealData ( FileReference XcodeProjectFileLocation , bool bIsForDistribution , string BundleID , string AppName , bool bMakeProjectPerTarget )
2023-05-10 16:53:20 -04:00
{
// the .xcodeproj is actually a directory
this . XcodeProjectFileLocation = new DirectoryReference ( XcodeProjectFileLocation . FullName ) ;
// default to engine director, will be fixed in Initialize if needed
ProductDirectory = Unreal . EngineDirectory ;
XcodeProjectName = ProductName = XcodeProjectFileLocation . GetFileNameWithoutAnyExtensions ( ) ;
if ( ProductName = = "UE5" )
{
ProductName = "UnrealGame" ;
}
this . bMakeProjectPerTarget = bMakeProjectPerTarget ;
2023-05-30 18:59:32 -04:00
bForDistribution = bIsForDistribution ;
BundleIdentifier = BundleID ;
DisplayName = String . IsNullOrEmpty ( AppName ) ? "$(UE_PRODUCT_NAME)" : AppName ;
2023-05-10 16:53:20 -04:00
}
2023-05-25 13:04:27 -04:00
public void InitializeUProjectFileLocation ( XcodeProjectFile ProjectFile )
2023-05-10 16:53:20 -04:00
{
2023-05-25 13:04:27 -04:00
if ( UProjectFileLocation ! = null )
{
return ;
}
2023-05-10 16:53:20 -04:00
// find a uproject file (UE5 target won't have one)
foreach ( Project Target in ProjectFile . ProjectTargets )
{
if ( Target . UnrealProjectFilePath ! = null )
{
UProjectFileLocation = Target . UnrealProjectFilePath ;
break ;
}
}
2023-05-30 18:38:07 -04:00
if ( ProjectFile . IsContentOnlyProject & & XcodeProjectFileGenerator . Current ? . OnlyGameProject ! = null & &
2023-05-25 13:04:27 -04:00
XcodeProjectFileGenerator . Current . OnlyGameProject . IsUnderDirectory ( ProjectFile . BaseDir ) )
{
UProjectFileLocation = XcodeProjectFileGenerator . Current . OnlyGameProject ;
}
2023-05-10 16:53:20 -04:00
// now that we have a UProject file (or not), update the FileCollection RootDirectory to point to it
ProjectFile . FileCollection . SetUProjectLocation ( UProjectFileLocation ) ;
2023-05-25 13:04:27 -04:00
return ;
2023-05-10 16:53:20 -04:00
}
public bool Initialize ( XcodeProjectFile ProjectFile , List < UnrealTargetConfiguration > Configurations , ILogger Logger )
{
this . ProjectFile = ProjectFile ;
2023-05-30 18:59:32 -04:00
bIsForeignProject = ProjectFile . IsForeignProject ;
bIsStubProject = ProjectFile . IsStubProject ;
bIsContentOnlyProject = ProjectFile . IsContentOnlyProject ;
2023-05-10 16:53:20 -04:00
this . Logger = Logger ;
2023-05-25 13:04:27 -04:00
InitializeUProjectFileLocation ( ProjectFile ) ;
2023-05-10 16:53:20 -04:00
// setup BundleIdentifier from ini file (if there's a specified plist file with one, that will override this)
2023-05-30 18:59:32 -04:00
if ( String . IsNullOrEmpty ( BundleIdentifier ) )
2023-05-10 16:53:20 -04:00
{
ConfigHierarchy Ini = ConfigCache . ReadHierarchy ( ConfigHierarchyType . Engine , UProjectFileLocation ? . Directory , UnrealTargetPlatform . Mac ) ;
2023-06-20 16:40:51 -04:00
Ini . GetString ( $"/Script/MacTargetPlatform.XcodeProjectSettings" , "BundleIdentifier" , out BundleIdentifier ) ;
2023-05-10 16:53:20 -04:00
}
2023-05-30 18:59:32 -04:00
if ( String . IsNullOrEmpty ( BundleIdentifier ) )
2023-05-10 16:53:20 -04:00
{
BundleIdentifier = "$(UE_SIGNING_PREFIX).$(UE_PRODUCT_NAME_STRIPPED)" ;
}
// make sure ProjectDir is something good
if ( UProjectFileLocation ! = null )
{
ProductDirectory = UProjectFileLocation . Directory ;
}
else if ( ProjectFile . ProjectTargets [ 0 ] . TargetRules ! . Type = = TargetType . Program )
{
DirectoryReference ? ProgramFinder = DirectoryReference . Combine ( ProjectFile . BaseDir ) ;
2023-05-30 18:59:32 -04:00
while ( ProgramFinder ! = null & & String . Compare ( ProgramFinder . GetDirectoryName ( ) , "Source" , true ) ! = 0 )
2023-05-10 16:53:20 -04:00
{
ProgramFinder = ProgramFinder . ParentDirectory ;
}
// we are now at Source directory, go up one more, then into Programs, and finally the "project" directory
if ( ProgramFinder ! = null )
{
ProgramFinder = DirectoryReference . Combine ( ProgramFinder , "../Programs" , ProductName ) ;
// if it exists, we have a ProductDir we can use for plists, icons, etc
if ( DirectoryReference . Exists ( ProgramFinder ) )
{
ProductDirectory = ProgramFinder ;
}
}
}
InitializeMetadata ( Logger ) ;
// Figure out all the desired configurations on the unreal side
AllConfigs = GetSupportedBuildConfigs ( XcodeProjectFileGenerator . XcodePlatforms , Configurations , Logger ) ;
// if we can't find any configs, we will fail to create a project
if ( AllConfigs . Count = = 0 )
{
return false ;
}
// verify all configs share the same TargetRules
if ( ! AllConfigs . All ( x = > x . ProjectTarget ! . TargetRules = = AllConfigs [ 0 ] . ProjectTarget ! . TargetRules ) )
{
throw new BuildException ( "All Configs must share a TargetRules. This indicates bMakeProjectPerTarget is returning false" ) ;
}
_TargetRules = AllConfigs [ 0 ] . ProjectTarget ! . TargetRules ;
2023-05-30 18:38:07 -04:00
2023-05-10 16:53:20 -04:00
// this project makes an app bundle (.app directory instead of a raw executable or dylib) if none of the fings make a non-appbundle
bIsAppBundle = ! AllConfigs . Any ( x = > x . ProjectTarget ! . TargetRules ! . bIsBuildingConsoleApplication | | x . ProjectTarget . TargetRules . bShouldCompileAsDLL ) ;
// read config settings
if ( AllConfigs . Any ( x = > x . bSupportsIOS ) )
{
IOSPlatform IOSPlatform = ( ( IOSPlatform ) UEBuildPlatform . GetBuildPlatform ( UnrealTargetPlatform . IOS ) ) ;
IOSProjectSettings = IOSPlatform . ReadProjectSettings ( UProjectFileLocation ) ;
}
if ( AllConfigs . Any ( x = > x . bSupportsTVOS ) )
{
TVOSPlatform TVOSPlatform = ( ( TVOSPlatform ) UEBuildPlatform . GetBuildPlatform ( UnrealTargetPlatform . TVOS ) ) ;
TVOSProjectSettings = TVOSPlatform . ReadProjectSettings ( UProjectFileLocation ) ;
}
return true ;
}
private void InitializeMetadata ( ILogger Logger )
{
// read setings from the configs, now that we have a project
ConfigHierarchy SharedPlatformIni = ConfigCache . ReadHierarchy ( ConfigHierarchyType . Engine , UProjectFileLocation ? . Directory , UnrealTargetPlatform . Mac ) ;
2023-06-20 16:40:51 -04:00
SharedPlatformIni . TryGetValue ( "/Script/MacTargetPlatform.XcodeProjectSettings" , "bUseAutomaticCodeSigning" , out bUseAutomaticSigning ) ;
2023-05-10 16:53:20 -04:00
2023-06-23 20:29:27 -04:00
Metadata = new Metadata ( ProductDirectory , XcodeProjectFileLocation , SharedPlatformIni , bSupportsMac , bSupportsIOS | | bSupportsTVOS , Logger ) ;
2023-05-10 16:53:20 -04:00
}
public string? FindFile ( List < string > Paths , UnrealTargetPlatform Platform , bool bMakeRelative )
{
foreach ( string Entry in Paths )
{
string FinalPath = Entry . Replace ( "$(Engine)" , Unreal . EngineDirectory . FullName ) ;
FinalPath = FinalPath . Replace ( "$(Project)" , ProductDirectory . FullName ) ;
FinalPath = FinalPath . Replace ( "$(Platform)" , Platform . ToString ( ) ) ;
2023-05-30 18:38:07 -04:00
// Console.WriteLine($"Looking for {FinalPath}");
2023-05-10 16:53:20 -04:00
if ( File . Exists ( FinalPath ) | | Directory . Exists ( FinalPath ) )
{
2023-05-30 18:38:07 -04:00
// Console.WriteLine($" Found it!");
2023-05-10 16:53:20 -04:00
if ( bMakeRelative )
{
FinalPath = new FileReference ( FinalPath ) . MakeRelativeTo ( XcodeProjectFileLocation . ParentDirectory ! ) ;
}
return FinalPath ;
}
}
return null ;
}
public string ProjectOrEnginePath ( string SubPath , bool bMakeRelative )
{
string? FinalPath = null ;
if ( UProjectFileLocation ! = null )
{
string PathToCheck = Path . Combine ( ProductDirectory . FullName , SubPath ) ;
if ( File . Exists ( PathToCheck ) | | Directory . Exists ( PathToCheck ) )
{
FinalPath = PathToCheck ;
}
}
if ( FinalPath = = null )
{
FinalPath = Path . Combine ( Unreal . EngineDirectory . FullName , SubPath ) ;
}
if ( bMakeRelative )
{
FinalPath = new FileReference ( FinalPath ) . MakeRelativeTo ( XcodeProjectFileLocation . ParentDirectory ! ) ;
}
return FinalPath ;
}
public void AddModule ( UEBuildModuleCPP Module , CppCompileEnvironment CompileEnvironment )
{
// one batched files per module
UnrealBatchedFiles FileBatch = new UnrealBatchedFiles ( this , BatchedFiles . Count + 1 , Module ) ;
BatchedFiles . Add ( FileBatch ) ;
if ( CompileEnvironment . ForceIncludeFiles . Count = = 0 )
{
// if there are no ForceInclude files, then that means it's a module that forces the includes to come from a generated PCH file
// and so we will use this for definitions and uniqueness
if ( CompileEnvironment . PrecompiledHeaderIncludeFilename ! = null )
{
FileBatch . PCHFile = FileReference . Combine ( XcodeProjectFileLocation . ParentDirectory ! , "PCHFiles" , CompileEnvironment . PrecompiledHeaderIncludeFilename . GetFileName ( ) ) ;
DirectoryReference . CreateDirectory ( FileBatch . PCHFile . Directory ) ;
FileReference . Copy ( CompileEnvironment . PrecompiledHeaderIncludeFilename , FileBatch . PCHFile , true ) ;
}
}
else
{
FileBatch . ForceIncludeFiles = CompileEnvironment . ForceIncludeFiles . Select ( x = > x . FullName ) ;
}
FileBatch . bEnableRTTI = CompileEnvironment . bUseRTTI ;
FileBatch . SystemIncludePaths . UnionWith ( CompileEnvironment . SystemIncludePaths ) ;
FileBatch . UserIncludePaths . UnionWith ( CompileEnvironment . UserIncludePaths ) ;
}
private List < UnrealBuildConfig > GetSupportedBuildConfigs ( List < UnrealTargetPlatform > Platforms , List < UnrealTargetConfiguration > Configurations , ILogger Logger )
{
List < UnrealBuildConfig > BuildConfigs = new List < UnrealBuildConfig > ( ) ;
//string ProjectName = ProjectFilePath.GetFileNameWithoutExtension();
foreach ( UnrealTargetConfiguration Configuration in Configurations )
{
if ( InstalledPlatformInfo . IsValidConfiguration ( Configuration , EProjectType . Code ) )
{
foreach ( UnrealTargetPlatform Platform in Platforms )
{
2023-06-23 20:29:27 -04:00
if ( InstalledPlatformInfo . IsValidPlatform ( Platform , EProjectType . Code ) & & ( Platform = = UnrealTargetPlatform . Mac | | Platform = = UnrealTargetPlatform . IOS | | Platform = = UnrealTargetPlatform . TVOS ) ) // @todo support other platforms
2023-05-10 16:53:20 -04:00
{
UEBuildPlatform ? BuildPlatform ;
if ( UEBuildPlatform . TryGetBuildPlatform ( Platform , out BuildPlatform ) & & ( BuildPlatform . HasRequiredSDKsInstalled ( ) = = SDKStatus . Valid ) )
{
// Check we have targets (Expected to be no Engine targets when generating for a single .uproject)
if ( ProjectFile ! . ProjectTargets . Count = = 0 & & ProjectFile ! . BaseDir ! = Unreal . EngineDirectory )
{
throw new BuildException ( $"Expecting at least one ProjectTarget to be associated with project '{XcodeProjectFileLocation}' in the TargetProjects list " ) ;
}
// Now go through all of the target types for this project
foreach ( ProjectTarget ProjectTarget in ProjectFile . ProjectTargets . OfType < ProjectTarget > ( ) )
{
if ( MSBuildProjectFile . IsValidProjectPlatformAndConfiguration ( ProjectTarget , Platform , Configuration , Logger ) )
{
// Figure out if this is a monolithic build
bool bShouldCompileMonolithic = BuildPlatform . ShouldCompileMonolithicBinary ( Platform ) ;
2023-05-17 16:22:48 -04:00
try
{
bShouldCompileMonolithic | = ( ProjectTarget . CreateRulesDelegate ( Platform , Configuration ) . LinkType = = TargetLinkType . Monolithic ) ;
}
catch ( BuildException )
{
}
2023-05-10 16:53:20 -04:00
string ConfigName = Configuration . ToString ( ) ;
if ( ! bMakeProjectPerTarget )
{
if ( ProjectTarget . TargetRules ! . Type ! = TargetType . Game & & ProjectTarget . TargetRules . Type ! = TargetType . Program )
{
ConfigName + = " " + ProjectTarget . TargetRules . Type . ToString ( ) ;
}
}
if ( BuildConfigs . Where ( Config = > Config . DisplayName = = ConfigName ) . ToList ( ) . Count = = 0 )
{
string TargetName = ProjectTarget . TargetFilePath . GetFileNameWithoutAnyExtensions ( ) ;
// Get the .uproject directory
DirectoryReference ? UProjectDirectory = DirectoryReference . FromFile ( ProjectTarget . UnrealProjectFilePath ) ;
// Get the output directory
DirectoryReference RootDirectory ;
2023-05-30 18:38:07 -04:00
if ( UProjectDirectory ! = null & &
( bShouldCompileMonolithic | | ProjectTarget . TargetRules ! . BuildEnvironment = = TargetBuildEnvironment . Unique ) & &
2023-05-10 16:53:20 -04:00
ProjectTarget . TargetRules ! . File ! . IsUnderDirectory ( UProjectDirectory ) )
{
RootDirectory = UEBuildTarget . GetOutputDirectoryForExecutable ( UProjectDirectory , ProjectTarget . TargetRules . File ! ) ;
}
else
{
RootDirectory = UEBuildTarget . GetOutputDirectoryForExecutable ( Unreal . EngineDirectory , ProjectTarget . TargetRules ! . File ! ) ;
}
string ExeName = TargetName ;
if ( ! bShouldCompileMonolithic & & ProjectTarget . TargetRules . Type ! = TargetType . Program )
{
// Figure out what the compiled binary will be called so that we can point the IDE to the correct file
if ( ProjectTarget . TargetRules . Type ! = TargetType . Game )
{
// Only if shared - unique retains the Target Name
if ( ProjectTarget . TargetRules . BuildEnvironment = = TargetBuildEnvironment . Shared )
{
ExeName = "Unreal" + ProjectTarget . TargetRules . Type . ToString ( ) ;
}
}
}
2023-06-14 03:54:47 -04:00
BuildConfigs . Add ( new UnrealBuildConfig ( ConfigName , TargetName , ExeName , ProjectTarget , Configuration , RootDirectory ) ) ;
2023-05-10 16:53:20 -04:00
}
}
}
}
}
}
}
}
return BuildConfigs ;
}
}
abstract class XcodeProjectNode
{
// keeps a list of other node this node references, which is used when writing out the whole xcode project file
public List < XcodeProjectNode > References = new ( ) ;
// optional Xcconfig file
public XcconfigFile ? Xcconfig = null ;
/// <summary>
/// Abstract function the individual node classes must override to write out the node to the project file
/// </summary>
/// <param name="Content"></param>
public abstract void Write ( StringBuilder Content ) ;
/// <summary>
/// Walks the references of the given node to find all nodes of the given type.
/// </summary>
/// <typeparam name="T">Parent class of the nodes to return</typeparam>
/// <param name="Node">Root node to start with</param>
/// <returns>Set of matching nodes</returns>
public static IEnumerable < T > GetNodesOfType < T > ( XcodeProjectNode Node ) where T : XcodeProjectNode
{
// gather the nodes without recursion
LinkedList < XcodeProjectNode > Nodes = new ( ) ;
Nodes . AddLast ( Node ) ;
// pull off the front of the "deque" amd add its references to the back, gather
List < XcodeProjectNode > Return = new ( ) ;
while ( Nodes . Count ( ) > 0 )
{
XcodeProjectNode Head = Nodes . First ( ) ;
Nodes . RemoveFirst ( ) ;
Head . References . ForEach ( x = > Nodes . AddLast ( x ) ) ;
// remember them all
Return . AddRange ( Head . References ) ;
}
// filter down
return Return . OfType < T > ( ) ;
}
public void CreateXcconfigFile ( XcodeProject Project , UnrealTargetPlatform ? Platform , string Name )
{
DirectoryReference XcodeProjectDirectory = Project . UnrealData . XcodeProjectFileLocation . ParentDirectory ! ;
Xcconfig = new XcconfigFile ( XcodeProjectDirectory , Platform , Name ) ;
Project . FileCollection . AddFileReference ( Xcconfig . Guid , Xcconfig . FileRef . MakeRelativeTo ( XcodeProjectDirectory ) , "explicitFileType" , "test.xcconfig" , "\"<group>\"" , "Xcconfigs" ) ;
}
2023-06-20 16:40:51 -04:00
public virtual void WriteXcconfigFile ( ILogger Logger )
2023-05-10 16:53:20 -04:00
{
2023-05-30 18:38:07 -04:00
2023-05-10 16:53:20 -04:00
}
/// <summary>
/// THhis will walk the node reference tree and call WRite on each node to add all needed nodes to the xcode poject file
/// </summary>
/// <param name="Content"></param>
/// <param name="Node"></param>
2023-06-20 16:40:51 -04:00
/// <param name="Logger"></param>
2023-05-10 16:53:20 -04:00
/// <param name="WrittenNodes"></param>
2023-06-20 16:40:51 -04:00
public static void WriteNodeAndReferences ( StringBuilder Content , XcodeProjectNode Node , ILogger Logger , HashSet < XcodeProjectNode > ? WrittenNodes = null )
2023-05-10 16:53:20 -04:00
{
if ( WrittenNodes = = null )
{
WrittenNodes = new ( ) ;
}
// write the node into the xcode project file
Node . Write ( Content ) ;
2023-06-20 16:40:51 -04:00
Node . WriteXcconfigFile ( Logger ) ;
2023-05-10 16:53:20 -04:00
foreach ( XcodeProjectNode Reference in Node . References )
{
if ( ! WrittenNodes . Contains ( Reference ) )
{
WrittenNodes . Add ( Reference ) ;
2023-06-20 16:40:51 -04:00
WriteNodeAndReferences ( Content , Reference , Logger , WrittenNodes ) ;
2023-05-10 16:53:20 -04:00
}
2023-05-30 18:38:07 -04:00
}
2023-05-10 16:53:20 -04:00
}
}
class XcodeDependency : XcodeProjectNode
{
public XcodeTarget Target ;
public string Guid = XcodeProjectFileGenerator . MakeXcodeGuid ( ) ;
public string ProxyGuid = XcodeProjectFileGenerator . MakeXcodeGuid ( ) ;
public string ProjectGuid ;
public XcodeDependency ( XcodeTarget Target , string ProjectGuid )
{
this . Target = Target ;
this . ProjectGuid = ProjectGuid ;
References . Add ( Target ) ;
}
public override void Write ( StringBuilder Content )
{
Content . WriteLine ( "/* Begin PBXContainerItemProxy section */" ) ;
Content . WriteLine ( $"\t\t{ProxyGuid} /* PBXContainerItemProxy */ = {{" ) ;
Content . WriteLine ( "\t\t\tisa = PBXContainerItemProxy;" ) ;
Content . WriteLine ( $"\t\t\tcontainerPortal = {ProjectGuid} /* Project object */;" ) ;
Content . WriteLine ( "\t\t\tproxyType = 1;" ) ;
Content . WriteLine ( $"\t\t\tremoteGlobalIDString = {Target.Guid};" ) ;
Content . WriteLine ( $"\t\t\tremoteInfo = \" { Target . Name } \ ";" ) ;
Content . WriteLine ( "\t\t};" ) ;
Content . WriteLine ( "/* End PBXContainerItemProxy section */" ) ;
Content . WriteLine ( "" ) ;
Content . WriteLine ( "/* Begin PBXTargetDependency section */" ) ;
Content . WriteLine ( $"\t\t{Guid} /* PBXTargetDependency */ = {{" ) ;
Content . WriteLine ( "\t\t\tisa = PBXTargetDependency;" ) ;
Content . WriteLine ( $"\t\t\ttarget = {Target.Guid} /* {Target.Name} */;" ) ;
Content . WriteLine ( $"\t\t\ttargetProxy = {ProxyGuid} /* PBXContainerItemProxy */;" ) ;
Content . WriteLine ( "\t\t};" ) ;
Content . WriteLine ( "/* End PBXTargetDependency section */" ) ;
}
}
abstract class XcodeBuildPhase : XcodeProjectNode
{
public string Name ;
public string IsAType ;
public string Guid = XcodeProjectFileGenerator . MakeXcodeGuid ( ) ;
protected List < XcodeSourceFile > FileItems = new ( ) ;
protected List < string > MiscItems = new ( ) ;
public XcodeBuildPhase ( string Name , string IsAType )
{
this . Name = Name ;
this . IsAType = IsAType ;
}
public override void Write ( StringBuilder Content )
{
Content . WriteLine ( $"/* Begin {IsAType} section */" ) ;
Content . WriteLine ( 2 , $"{Guid} = {{" ) ;
2023-05-30 18:38:07 -04:00
Content . WriteLine ( 3 , $"isa = {IsAType};" ) ;
Content . WriteLine ( 3 , "buildActionMask = 2147483647;" ) ;
Content . WriteLine ( 3 , "files = (" ) ;
2023-05-10 16:53:20 -04:00
foreach ( XcodeSourceFile File in FileItems )
{
2023-05-30 18:38:07 -04:00
Content . WriteLine ( 4 , $"{File.FileGuid} /* {File.Reference.GetFileName()} in {Name} */," ) ;
2023-05-10 16:53:20 -04:00
}
Content . WriteLine ( 3 , ");" ) ;
foreach ( string Line in MiscItems )
{
Content . WriteLine ( 3 , Line ) ;
}
Content . WriteLine ( 2 , "};" ) ;
Content . WriteLine ( $"/* End {IsAType} section */" ) ;
}
}
class XcodeSourcesBuildPhase : XcodeBuildPhase
{
public XcodeSourcesBuildPhase ( )
: base ( "Sources" , "PBXSourcesBuildPhase" )
{
}
public void AddFile ( XcodeSourceFile File )
{
FileItems . Add ( File ) ;
}
}
class XcodeResourcesBuildPhase : XcodeBuildPhase
{
private XcodeFileCollection FileCollection ;
public XcodeResourcesBuildPhase ( XcodeFileCollection FileCollection )
: base ( "Resources" , "PBXResourcesBuildPhase" )
{
this . FileCollection = FileCollection ;
}
public void AddResource ( FileReference Resource )
{
XcodeSourceFile ResourceSource = new XcodeSourceFile ( Resource , null ) ;
FileCollection . ProcessFile ( ResourceSource , true , false , "Resources" ) ;
FileItems . Add ( ResourceSource ) ;
}
public void AddFolderResource ( DirectoryReference Resource , string GroupName )
{
XcodeSourceFile ResourceSource = new XcodeSourceFile ( new FileReference ( Resource . FullName ) , null ) ;
FileCollection . ProcessFile ( ResourceSource , true , false , GroupName ) ;
//Project.FileCollection.AddFolderReference(CookedData.MakeRelativeTo(UnrealData.XcodeProjectFileLocation.ParentDirectory!), "CookedData_Game");
FileItems . Add ( ResourceSource ) ;
}
}
class XcodeFrameworkBuildPhase : XcodeBuildPhase
{
private XcodeFileCollection FileCollection ;
public XcodeFrameworkBuildPhase ( XcodeFileCollection FileCollection )
: base ( "Frameworks" , "PBXFrameworksBuildPhase" )
{
this . FileCollection = FileCollection ;
}
public void AddFramework ( DirectoryReference Framework , string FileRefGuid )
{
XcodeSourceFile FrameworkSource = new XcodeSourceFile ( new FileReference ( Framework . FullName ) , null , FileRefGuid ) ;
FileCollection . ProcessFile ( FrameworkSource , true , false , "Frameworks" , "" ) ; ;
FileItems . Add ( FrameworkSource ) ;
}
}
class XcodeCopyFilesBuildPhase : XcodeBuildPhase
{
private XcodeFileCollection FileCollection ;
public XcodeCopyFilesBuildPhase ( XcodeFileCollection FileCollection )
: base ( "Embed Frameworks" , "PBXCopyFilesBuildPhase" )
{
this . FileCollection = FileCollection ;
MiscItems . Add ( $"dstPath = \" \ ";" ) ;
MiscItems . Add ( $"dstSubfolderSpec = 10;" ) ;
MiscItems . Add ( $"name = \" { Name } \ ";" ) ;
}
public void AddFramework ( DirectoryReference Framework , string FileRefGuid )
{
XcodeSourceFile FrameworkSource = new XcodeSourceFile ( new FileReference ( Framework . FullName ) , null , FileRefGuid ) ;
FileCollection . ProcessFile ( FrameworkSource , true , false , "" , "settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); };" ) ;
FileItems . Add ( FrameworkSource ) ;
}
}
class XcodeShellScriptBuildPhase : XcodeBuildPhase
{
2023-05-30 18:38:07 -04:00
public XcodeShellScriptBuildPhase ( string Name , IEnumerable < string > ScriptLines , IEnumerable < string > Inputs , IEnumerable < string > Outputs , bool bInstallOnly = false )
2023-05-10 16:53:20 -04:00
: base ( Name , "PBXShellScriptBuildPhase" )
{
MiscItems . Add ( $"name = \" { Name } \ ";" ) ;
MiscItems . Add ( $"inputPaths = (" ) ;
foreach ( string Input in Inputs )
{
MiscItems . Add ( $"\t\" { Input } \ "" ) ;
}
MiscItems . Add ( $");" ) ;
MiscItems . Add ( $"outputPaths = (" ) ;
foreach ( string Output in Outputs )
{
MiscItems . Add ( $"\t\" { Output } \ "" ) ;
}
MiscItems . Add ( $");" ) ;
// string Script = string.Join("
", ScriptLines);
2023-05-30 18:59:32 -04:00
string Script = String . Join ( "\\n" , ScriptLines ) ;
2023-05-10 16:53:20 -04:00
MiscItems . Add ( $"shellPath = /bin/sh;" ) ;
MiscItems . Add ( $"shellScript = \" { Script } \ ";" ) ;
if ( bInstallOnly )
{
MiscItems . Add ( "runOnlyForDeploymentPostprocessing = 1;" ) ;
}
}
}
class XcodeBuildConfig : XcodeProjectNode
{
public string Guid = XcodeProjectFileGenerator . MakeXcodeGuid ( ) ;
public UnrealBuildConfig Info ;
// Because we don't make project-wide .xcconfig files, we need to specify at the projet level that all of the platforms are supported,
// so that the _Build targets will have any platform as a supported platform, otherwise it can only compile for Mac (default Xcode platform)
private bool bIncludeAllPlatforms ;
public XcodeBuildConfig ( UnrealBuildConfig Info , bool bIncludeAllPlatforms )
{
this . Info = Info ;
this . bIncludeAllPlatforms = bIncludeAllPlatforms ;
}
public override void Write ( StringBuilder Content )
{
Content . WriteLine ( 2 , $"{Guid} /* {Info.DisplayName} */ = {{" ) ;
2023-05-30 18:38:07 -04:00
Content . WriteLine ( 3 , "isa = XCBuildConfiguration;" ) ;
2023-05-10 16:53:20 -04:00
if ( Xcconfig ! = null )
{
2023-05-30 18:38:07 -04:00
Content . WriteLine ( 3 , $"baseConfigurationReference = {Xcconfig.Guid} /* {Xcconfig.Name}.xcconfig */;" ) ;
2023-05-10 16:53:20 -04:00
}
2023-05-30 18:38:07 -04:00
Content . WriteLine ( 3 , "buildSettings = {" ) ;
2023-05-10 16:53:20 -04:00
if ( bIncludeAllPlatforms )
{
2023-06-23 20:29:27 -04:00
Content . WriteLine ( 4 , $"SUPPORTED_PLATFORMS = \" macosx iphonesimulator iphoneos appletvsimulator appletvos \ ";" ) ;
2023-05-30 18:38:07 -04:00
Content . WriteLine ( 4 , $"ONLY_ACTIVE_ARCH = YES;" ) ;
2023-05-10 16:53:20 -04:00
}
2023-05-30 18:38:07 -04:00
Content . WriteLine ( 3 , "};" ) ;
Content . WriteLine ( 3 , $"name = \" { Info . DisplayName } \ ";" ) ;
2023-05-10 16:53:20 -04:00
Content . WriteLine ( 2 , "};" ) ;
}
}
2023-05-30 18:38:07 -04:00
2023-05-10 16:53:20 -04:00
class XcodeBuildConfigList : XcodeProjectNode
2023-05-30 18:38:07 -04:00
{
2023-05-10 16:53:20 -04:00
public string Guid = XcodeProjectFileGenerator . MakeXcodeGuid ( ) ;
public string TargetName ;
private UnrealData UnrealData ;
public List < XcodeBuildConfig > BuildConfigs = new ( ) ;
public bool bSupportsMac = > Supports ( UnrealTargetPlatform . Mac ) ;
public bool bSupportsIOS = > Supports ( UnrealTargetPlatform . IOS ) ;
public bool bSupportsTVOS = > Supports ( UnrealTargetPlatform . TVOS ) ;
public bool Supports ( UnrealTargetPlatform ? Platform )
{
return this . Platform = = Platform | | ( this . Platform = = null & & BuildConfigs . Any ( x = > x . Info . Supports ( Platform ) ) ) ;
}
public UnrealTargetPlatform ? Platform ;
public XcodeBuildConfigList ( UnrealTargetPlatform ? Platform , string TargetName , UnrealData UnrealData , bool bIncludeAllPlatformsInConfig )
{
this . Platform = Platform ;
this . UnrealData = UnrealData ;
if ( UnrealData . AllConfigs . Count = = 0 )
{
throw new BuildException ( "Created a XcodeBuildConfigList with no BuildConfigs. This likely means a target was created too early" ) ;
}
this . TargetName = TargetName ;
// create build config objects for each info passed in, and them as references
IEnumerable < XcodeBuildConfig > Configs = UnrealData . AllConfigs . Select ( x = > new XcodeBuildConfig ( x , bIncludeAllPlatformsInConfig ) ) ;
// filter out configs that dont match a platform if we are single-platform mode
Configs = Configs . Where ( x = > Platform = = null | | x . Info . Supports ( ( UnrealTargetPlatform ) Platform ) ) ;
BuildConfigs = Configs . ToList ( ) ;
References . AddRange ( BuildConfigs ) ;
}
public override void Write ( StringBuilder Content )
{
// figure out the default configuration to use
string Default = "Development" ;
if ( ! UnrealData . bMakeProjectPerTarget & & BuildConfigs . Any ( x = > x . Info . DisplayName . Contains ( " Editor" ) ) )
{
Default = "Development Editor" ;
}
Content . WriteLine ( 2 , $"{Guid} /* Build configuration list for target {TargetName} */ = {{" ) ;
2023-05-30 18:38:07 -04:00
Content . WriteLine ( 3 , "isa = XCConfigurationList;" ) ;
Content . WriteLine ( 3 , "buildConfigurations = (" ) ;
2023-05-10 16:53:20 -04:00
foreach ( XcodeBuildConfig Config in BuildConfigs )
{
2023-05-30 18:38:07 -04:00
Content . WriteLine ( 4 , $"{Config.Guid} /* {Config.Info.DisplayName} */," ) ;
2023-05-10 16:53:20 -04:00
}
2023-05-30 18:38:07 -04:00
Content . WriteLine ( 3 , ");" ) ;
Content . WriteLine ( 3 , "defaultConfigurationIsVisible = 0;" ) ;
Content . WriteLine ( 3 , $"defaultConfigurationName = \" { Default } \ ";" ) ;
2023-05-10 16:53:20 -04:00
Content . WriteLine ( 2 , "};" ) ;
}
}
class XcodeTarget : XcodeProjectNode
{
public enum Type
{
Run_App ,
Run_Tool ,
Build ,
Index ,
}
// Guid for this target
public string Guid = XcodeProjectFileGenerator . MakeXcodeGuid ( ) ;
2023-05-30 18:38:07 -04:00
// string TargetAppGuid = XcodeProjectFileGenerator.MakeXcodeGuid();
2023-05-10 16:53:20 -04:00
// com.apple.product-type.application, etc
string ProductType ;
// xcode target type name
string TargetTypeName ;
Type TargetType ;
// UnrealEngine_Build, etc
public string Name ;
// QAGame, QAGameEditor, etc
private string UnrealTargetName ;
// list of build configs this target supports (for instance, the Index target only indexes a Development config)
public XcodeBuildConfigList ? BuildConfigList ;
// dependencies for this target
public List < XcodeDependency > Dependencies = new List < XcodeDependency > ( ) ;
// build phases for this target (source, resource copying, etc)
public List < XcodeBuildPhase > BuildPhases = new List < XcodeBuildPhase > ( ) ;
private FileReference ? GameProject ;
2023-05-30 18:38:07 -04:00
public XcodeTarget ( Type Type , UnrealData UnrealData , string? OverrideName = null )
2023-05-10 16:53:20 -04:00
{
2023-05-25 13:04:27 -04:00
// when we are content only, we do not want to build with the uproject on the commandline, we will be building UnrealGame
GameProject = UnrealData . bIsContentOnlyProject ? null : UnrealData . UProjectFileLocation ;
2023-05-10 16:53:20 -04:00
string ConfigName ;
TargetType = Type ;
switch ( Type )
{
case Type . Run_App :
ProductType = "com.apple.product-type.application" ;
TargetTypeName = "PBXNativeTarget" ;
ConfigName = "_Run" ;
break ;
case Type . Run_Tool :
ProductType = "com.apple.product-type.tool" ;
TargetTypeName = "PBXNativeTarget" ;
ConfigName = "_Run" ;
break ;
case Type . Build :
ProductType = "com.apple.product-type.library.static" ;
TargetTypeName = "PBXLegacyTarget" ;
ConfigName = "_Build" ;
break ;
case Type . Index :
ProductType = "com.apple.product-type.library.static" ;
TargetTypeName = "PBXNativeTarget" ;
ConfigName = "_Index" ;
break ;
default :
throw new BuildException ( $"Unhandled target type {Type}" ) ;
}
// set up names
2023-05-25 13:04:27 -04:00
UnrealTargetName = UnrealData . TargetRules . Name ;
2023-05-10 16:53:20 -04:00
Name = OverrideName ? ? ( UnrealData . XcodeProjectName + ConfigName ) ;
}
public void AddDependency ( XcodeTarget Target , XcodeProject Project )
{
XcodeDependency Dependency = new XcodeDependency ( Target , Project . Guid ) ;
Dependencies . Add ( Dependency ) ;
References . Add ( Dependency ) ;
}
public override void Write ( StringBuilder Content )
{
Content . WriteLine ( $"/* Begin {TargetType} section */" ) ;
Content . WriteLine ( 2 , $"{Guid} /* {Name} */ = {{" ) ;
2023-05-30 18:38:07 -04:00
Content . WriteLine ( 3 , $"isa = {TargetTypeName};" ) ;
Content . WriteLine ( 3 , $"buildConfigurationList = {BuildConfigList!.Guid} /* Build configuration list for {TargetTypeName} \" { Name } \ " */;" ) ;
2023-05-10 16:53:20 -04:00
if ( TargetType = = Type . Build )
{
// get paths to Unreal bits to be able ro tun UBT
string UProjectParam = GameProject = = null ? "" : $"{GameProject.FullName.Replace(" ", " \ \ ")}" ;
string UEDir = XcodeFileCollection . ConvertPath ( Path . GetFullPath ( Directory . GetCurrentDirectory ( ) + "../../.." ) ) ;
string BuildToolPath = UEDir + "/Engine/Build/BatchFiles/Mac/XcodeBuild.sh" ;
// insert elements to call UBT when building
2023-05-30 18:38:07 -04:00
Content . WriteLine ( 3 , $"buildArgumentsString = \" $ ( ACTION ) { UnrealTargetName } $ ( PLATFORM_NAME ) $ ( CONFIGURATION ) { UProjectParam } \ ";" ) ;
Content . WriteLine ( 3 , $"buildToolPath = \" { BuildToolPath } \ ";" ) ;
Content . WriteLine ( 3 , $"buildWorkingDirectory = \" { UEDir } \ ";" ) ;
2023-05-10 16:53:20 -04:00
}
2023-05-30 18:38:07 -04:00
Content . WriteLine ( 3 , "buildPhases = (" ) ;
2023-05-10 16:53:20 -04:00
foreach ( XcodeBuildPhase BuildPhase in BuildPhases )
{
2023-05-30 18:38:07 -04:00
Content . WriteLine ( 4 , $"{BuildPhase.Guid} /* {BuildPhase.Name} */," ) ;
2023-05-10 16:53:20 -04:00
}
2023-05-30 18:38:07 -04:00
Content . WriteLine ( 3 , ");" ) ;
Content . WriteLine ( 3 , "dependencies = (" ) ;
2023-05-10 16:53:20 -04:00
foreach ( XcodeDependency Dependency in Dependencies )
{
2023-05-30 18:38:07 -04:00
Content . WriteLine ( 4 , $"{Dependency.Guid} /* {Dependency.Target.Name} */," ) ;
2023-05-10 16:53:20 -04:00
}
2023-05-30 18:38:07 -04:00
Content . WriteLine ( 3 , ");" ) ;
Content . WriteLine ( 3 , $"name = \" { Name } \ ";" ) ;
Content . WriteLine ( 3 , "passBuildSettingsInEnvironment = 1;" ) ;
Content . WriteLine ( 3 , $"productType = \" { ProductType } \ ";" ) ;
2023-05-10 16:53:20 -04:00
WriteExtraTargetProperties ( Content ) ;
Content . WriteLine ( 2 , "};" ) ;
Content . WriteLine ( $"/* End {TargetType} section */" ) ;
}
/// <summary>
/// Let subclasses add extra properties into this target section
/// </summary>
protected virtual void WriteExtraTargetProperties ( StringBuilder Content )
{
// nothing by default
}
}
class XcodeRunTarget : XcodeTarget
{
private string ProductGuid = XcodeProjectFileGenerator . MakeXcodeGuid ( ) ;
private UnrealData UnrealData ;
public UnrealTargetPlatform Platform ;
public TargetType TargetType ;
public XcodeRunTarget ( XcodeProject Project , string TargetName , TargetType TargetType , UnrealTargetPlatform Platform , XcodeBuildTarget ? BuildTarget , XcodeProjectFile ProjectFile , ILogger Logger )
2023-06-14 03:54:47 -04:00
: base ( Project . UnrealData . IsAppBundle ( Platform ) ? XcodeTarget . Type . Run_App : XcodeTarget . Type . Run_Tool , Project . UnrealData ,
2023-05-10 16:53:20 -04:00
TargetName + ( XcodeProjectFileGenerator . PerPlatformMode = = XcodePerPlatformMode . OneWorkspacePerPlatform ? "" : $"_{Platform}" ) )
{
this . TargetType = TargetType ;
this . Platform = Platform ;
2023-05-30 18:59:32 -04:00
UnrealData = Project . UnrealData ;
2023-05-10 16:53:20 -04:00
2023-05-30 18:38:07 -04:00
BuildConfigList = new XcodeBuildConfigList ( Platform , Name , Project . UnrealData , bIncludeAllPlatformsInConfig : false ) ;
2023-05-10 16:53:20 -04:00
References . Add ( BuildConfigList ) ;
// add the Product item to the project to be visible in left pane
2023-06-14 03:54:47 -04:00
Project . FileCollection . AddFileReference ( ProductGuid , UnrealData . ProductName , "explicitFileType" , Project . UnrealData . IsAppBundle ( Platform ) ? "wrapper.application" : "\"compiled.mach-o.executable\"" , "BUILT_PRODUCTS_DIR" , "Products" ) ;
2023-05-10 16:53:20 -04:00
2023-06-14 03:54:47 -04:00
if ( Project . UnrealData . IsAppBundle ( Platform ) )
2023-05-10 16:53:20 -04:00
{
XcodeResourcesBuildPhase ResourcesBuildPhase = new XcodeResourcesBuildPhase ( Project . FileCollection ) ;
BuildPhases . Add ( ResourcesBuildPhase ) ;
References . Add ( ResourcesBuildPhase ) ;
ProcessFrameworks ( ResourcesBuildPhase , Project , ProjectFile , Logger ) ;
ProcessScripts ( ResourcesBuildPhase , Project ) ;
ProcessAssets ( ResourcesBuildPhase ) ;
}
if ( BuildTarget ! = null )
{
AddDependency ( BuildTarget , Project ) ;
}
CreateXcconfigFile ( Project , Platform , $"{Name}" ) ;
// create per-config Xcconfig files
foreach ( XcodeBuildConfig Config in BuildConfigList . BuildConfigs )
{
Config . CreateXcconfigFile ( Project , Platform , $"{Name}_{Config.Info.DisplayName.Replace(" ", " ")}" ) ;
}
}
/// <summary>
/// Write some scripts to do some fixup with how UBT links files. There is currently a difference between Mac and IOS/TVOS:
/// All:
/// - The staged files gets pulled into the .app for a self-contained app, created at Xcode build time, unless an envvar is set
/// that tells this script to skip the copy. This allows UAT to potentially skip copying staged files before we stage
/// ("BuildCookRun -build -cook -stage -package" can skip the copying until the -package step).
/// Also we don't copy anything if this is the engine, no-project, build, and an
/// Mac:
2023-06-12 14:56:21 -04:00
/// - UBT will link executable next to .app
/// - We will copy it into .app here, similar to iOS
2023-05-10 16:53:20 -04:00
/// - However, during Archiving, the .app is created in a intermediate location, so then here we copy from Binaries/Mac to the intermediate location
/// - Trying to have UBT link directly to the intermeidate location causes various issues, so we copy it like IOS does
/// IOS/TVOS:
/// - IOS will link to Binaries/IOS/Foo
/// - During normal operation, here we copy from Binaries/IOS/Foo to Binaries/IOS/Foo.app/Foo
/// - Note that IOS and Mac have different internal directory structures (which EXECUTABLE_PATH expresses)
/// - When Archiving, we copy from Binaries/IOS/Foo to the intermediate location's .app
/// All:
/// - At this point, the executable is in the correct spot, and so CONFIGURATION_BUILD_DIR/EXECUTABLE_PATH points to it
/// - So here we gneerate a dSYM from the executable, copying it to where Xcode wants it (DWARF_DSYM_FOLDER_PATH/DWARF_DSYM_FILE_NAME), and
/// then we strip the executable in place
/// </summary>
/// <param name="ResourcesBuildPhase"></param>
/// <param name="Project"></param>
protected void ProcessScripts ( XcodeResourcesBuildPhase ResourcesBuildPhase , XcodeProject Project )
{
List < string > CopyScript = new ( ) ;
// UBT no longer copies the executable into the .app directory in PostBuild, so we do it here
// EXECUTABLE_NAME is Foo, EXECUTABLE_PATH is Foo.app/Foo
// NOTE: We read from hardcoded location where UBT writes to, but we write to CONFIGURATION_BUILD_DIR because
// when Archiving, the .app is somewhere else
CopyScript . AddRange ( new string [ ]
{
2023-05-25 13:04:27 -04:00
"set -eo pipefail" ,
2023-05-10 16:53:20 -04:00
"# Copy the executable into .app if needed (-ef checks if two files/paths are equivalent)" ,
2023-05-25 13:04:27 -04:00
"SRC_EXE=\\\"${UE_BINARIES_DIR}/${UE_UBT_BINARY_SUBPATH}\\\"" ,
"DEST_EXE=\\\"${CONFIGURATION_BUILD_DIR}/${EXECUTABLE_PATH}\\\"" ,
2023-05-10 16:53:20 -04:00
"" ,
"echo ${SRC_EXE} /// ${DEST_EXE}" ,
"if [[ ! ${SRC_EXE} -ef ${DEST_EXE} ]]; then" ,
" echo Copying executable..." ,
" mkdir -p `dirname ${DEST_EXE}`" ,
2023-05-25 13:04:27 -04:00
" ditto \\\"${SRC_EXE}\\\" \\\"${DEST_EXE}\\\"" ,
2023-05-10 16:53:20 -04:00
"fi" ,
""
} ) ;
2023-06-12 14:56:21 -04:00
// Editor just need the above script to copy executable into .app
if ( Project . UnrealData . TargetRules . Type = = TargetType . Editor )
{
XcodeShellScriptBuildPhase EditorCopyScriptPhase = new ( "Copy Executable into .app" , CopyScript , new string [ ] { } , new string [ ] { $"/dev/null" } ) ;
BuildPhases . Add ( EditorCopyScriptPhase ) ;
References . Add ( EditorCopyScriptPhase ) ;
return ;
}
2023-05-10 16:53:20 -04:00
// rsync the Staged build into the .app, unless the UE_SKIP_STAGEDDATA_SYNC var is set to 1
2023-05-25 13:04:27 -04:00
// editor builds don't need staged content in them
if ( TargetType ! = TargetType . Editor )
2023-05-10 16:53:20 -04:00
{
bool bIsEngineBuild = Project . UnrealData . UProjectFileLocation = = null ;
2023-05-25 13:04:27 -04:00
string DefaultStageDir = bIsEngineBuild ? "${UE_OVERRIDE_STAGE_DIR}" : "${UE_PROJECT_DIR}/Saved/StagedBuilds/${UE_TARGET_PLATFORM_NAME}" ;
2023-05-26 17:18:47 -04:00
string SyncSourceSubdir = ( Platform = = UnrealTargetPlatform . Mac ) ? "" : "/cookeddata" ;
string SyncDestSubdir = ( Platform = = UnrealTargetPlatform . Mac ) ? "/UE" : "/cookeddata" ;
2023-05-10 16:53:20 -04:00
CopyScript . AddRange ( new string [ ]
{
"# Skip syncing if desired" ,
"if [[ ${UE_SKIP_STAGEDDATA_SYNC} -eq 1 ]]; then exit 0; fi" ,
"" ,
"# When building engine projects, like UnrealGame, we don't have data to stage unless something has specified UE_OVERRIDE_STAGE_DIR" ,
2023-05-25 13:04:27 -04:00
$"STAGED_DIR=\\\" { DefaultStageDir } \ \ \ "" ,
2023-05-10 16:53:20 -04:00
"if [[ -z ${STAGED_DIR} ]]; then exit 0; fi" ,
2023-05-25 13:04:27 -04:00
} ) ;
// Programs have an optional Staged dir - usually they aren't staged, but allow it to be staged if it was
if ( TargetType ! = TargetType . Program )
{
CopyScript . AddRange ( new string [ ]
{
"# Make sure the staged directory exists and has files in it" ,
"if [[ ! -e \\\"${STAGED_DIR}\\\" || ! $(ls -A \\\"${STAGED_DIR}\\\") ]]; then " ,
" echo =========================================================================================" ,
" echo \\\"WARNING: To run, you must have a valid staged build directory. The Staged location is:\\\"" ,
" echo \\\" ${STAGED_DIR}\\\"" ,
" echo \\\"Use the editor's Platforms menu, or run a command like::\\\"" ,
$" echo \\\" . / RunUAT . sh BuildCookRun - platform = { Platform } - project = < project > - build - cook - stage - pak \ \ \ "" ,
" echo =========================================================================================" ,
" exit -0 " ,
"fi" ,
} ) ;
}
else
{
CopyScript . AddRange ( new string [ ]
{
"# Make sure the staged directory exists and has files in it" ,
"if [[ ! -e ${STAGED_DIR} ]]; then exit 0; fi" ,
} ) ;
}
CopyScript . AddRange ( new string [ ]
{
2023-05-10 16:53:20 -04:00
"" ,
2023-05-26 17:18:47 -04:00
$"echo \\\" Syncing $ { { STAGED_DIR } } { SyncSourceSubdir } to $ { { CONFIGURATION_BUILD_DIR } } / $ { { CONTENTS_FOLDER_PATH } } { SyncDestSubdir } \ \ \ "" ,
$"rsync -a --delete --exclude=Info.plist --exclude=/Manifest_* --exclude=*.app \\\" $ { { STAGED_DIR } } { SyncSourceSubdir } / \ \ \ " \\\"${{CONFIGURATION_BUILD_DIR}}/${{CONTENTS_FOLDER_PATH}}{SyncDestSubdir}\\\"" ,
2023-05-10 16:53:20 -04:00
} ) ;
// copy any dylibs from the staged stub .app into the real one
if ( Platform = = UnrealTargetPlatform . Mac )
{
2023-05-25 13:04:27 -04:00
// when doing a Content only project the Binaries are under Engine, not the project
string EngineOrProject = ( Project . UnrealData . bIsContentOnlyProject | | Project . UnrealData . UProjectFileLocation = = null ) ? "Engine" : Project . UnrealData . ProductName ;
2023-05-10 16:53:20 -04:00
CopyScript . AddRange ( new string [ ]
{
// copy from whatever .app is in Binaries/Mac - this is so we can make a Shipping app from a Development staged directory
2023-05-25 13:04:27 -04:00
"pushd \\\"${STAGED_DIR}/${UE_STAGED_BINARIES_DIR_BASE}/Binaries/Mac/\\\" > /dev/null" ,
"APPS=($(echo *.app))" ,
"echo Syncing $PWD/${APPS[0]}/${BUNDLE_CONTENTS_FOLDER_PATH} to ${CONFIGURATION_BUILD_DIR}/${CONTENTS_FOLDER_PATH}/\\\"" ,
2023-05-26 17:18:47 -04:00
"rsync -a --include='*/' --include='*.dylib' --exclude='*' ${APPS[0]}/${BUNDLE_CONTENTS_FOLDER_PATH}MacOS/\\\" \\\"${CONFIGURATION_BUILD_DIR}/${CONTENTS_FOLDER_PATH}/MacOS\\\"" ,
2023-05-25 13:04:27 -04:00
"popd > /dev/null" ,
2023-05-10 16:53:20 -04:00
} ) ;
}
}
// run this script every time, but xcode will show a warning if there isn't _some_ output
string ScriptOutput = $"/dev/null" ;
XcodeShellScriptBuildPhase CopyScriptPhase = new ( "Copy Executable and Staged Data into .app" , CopyScript , new string [ ] { } , new string [ ] { ScriptOutput } ) ;
BuildPhases . Add ( CopyScriptPhase ) ;
References . Add ( CopyScriptPhase ) ;
// always generate a dsym file when we archive, and by having Xcode do it, it will be put into the archive properly
// (note bInstallOnly which will make this onle run when archiving)
List < string > DsymScript = new ( ) ;
DsymScript . AddRange ( new string [ ]
{
"set -e" ,
"" ,
"# Run the wrapper dsym generator" ,
"\\\"${UE_ENGINE_DIR}/Build/BatchFiles/Mac/GenerateUniversalDSYM.sh\\\" \\\"${CONFIGURATION_BUILD_DIR}/${EXECUTABLE_PATH}\\\" \\\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}\\\"" ,
"strip -no_code_signature_warning -D \\\"${CONFIGURATION_BUILD_DIR}/${EXECUTABLE_PATH}\\\"" ,
"" ,
"# Remove any unused architectures from dylibs in the .app (param1) that don't match the executable (param2). Also error if a dylib is missing arches" ,
"\\\"${UE_ENGINE_DIR}/Build/BatchFiles/Mac/ThinApp.sh\\\" \\\"${CONFIGURATION_BUILD_DIR}/${CONTENTS_FOLDER_PATH}\\\" \\\"${CONFIGURATION_BUILD_DIR}/${EXECUTABLE_PATH}\\\"" ,
} ) ;
string DsymScriptInput = $"\\\" $ ( CONFIGURATION_BUILD_DIR ) / $ ( EXECUTABLE_PATH ) \ \ \ "" ;
string DsymScriptOutput = $"\\\" $ ( DWARF_DSYM_FOLDER_PATH ) / $ ( DWARF_DSYM_FILE_NAME ) \ \ \ "" ;
2023-05-30 18:38:07 -04:00
XcodeShellScriptBuildPhase DsymScriptPhase = new ( "Generate dsym for archive, and strip" , DsymScript , new string [ ] { DsymScriptInput } , new string [ ] { DsymScriptOutput } , bInstallOnly : true ) ;
2023-05-10 16:53:20 -04:00
BuildPhases . Add ( DsymScriptPhase ) ;
References . Add ( DsymScriptPhase ) ;
}
private void ProcessAssets ( XcodeResourcesBuildPhase ResourcesBuildPhase )
{
List < string > StoryboardPaths = new List < string > ( )
{
"$(Project)/Build/$(Platform)/Resources/Interface/LaunchScreen.storyboardc" ,
"$(Project)/Build/$(Platform)/Resources/Interface/LaunchScreen.storyboard" ,
"$(Project)/Build/Apple/Resources/Interface/LaunchScreen.storyboardc" ,
"$(Project)/Build/Apple/Resources/Interface/LaunchScreen.storyboard" ,
"$(Engine)/Build/$(Platform)/Resources/Interface/LaunchScreen.storyboardc" ,
"$(Engine)/Build/$(Platform)/Resources/Interface/LaunchScreen.storyboard" ,
"$(Engine)/Build/Apple/Resources/Interface/LaunchScreen.storyboardc" ,
"$(Engine)/Build/Apple/Resources/Interface/LaunchScreen.storyboard" ,
} ;
// look for Assets
string? StoryboardPath ;
if ( Platform = = UnrealTargetPlatform . Mac )
{
string AssetsPath = UnrealData . ProjectOrEnginePath ( "Build/Mac/Resources/Assets.xcassets" , false ) ;
ResourcesBuildPhase . AddResource ( new FileReference ( AssetsPath ) ) ;
StoryboardPath = UnrealData . FindFile ( StoryboardPaths , UnrealTargetPlatform . Mac , false ) ;
}
else
{
string AssetsPath = UnrealData . ProjectOrEnginePath ( "Build/IOS/Resources/Assets.xcassets" , false ) ;
ResourcesBuildPhase . AddResource ( new FileReference ( AssetsPath ) ) ;
StoryboardPath = UnrealData . FindFile ( StoryboardPaths , UnrealTargetPlatform . IOS , false ) ;
}
if ( StoryboardPath ! = null )
{
ResourcesBuildPhase . AddResource ( new FileReference ( StoryboardPath ) ) ;
}
}
protected void ProcessFrameworks ( XcodeResourcesBuildPhase ResourcesBuildPhase , XcodeProject Project , XcodeProjectFile ProjectFile , ILogger Logger )
{
// look up to see if we had cached any Frameworks
Tuple < ProjectFile , UnrealTargetPlatform > FrameworkKey = Tuple . Create ( ( ProjectFile ) ProjectFile , Platform ) ;
IEnumerable < UEBuildFramework > ? Frameworks ;
if ( XcodeProjectFileGenerator . TargetFrameworks . TryGetValue ( FrameworkKey , out Frameworks ) )
{
XcodeCopyFilesBuildPhase EmbedFrameworks = new XcodeCopyFilesBuildPhase ( Project . FileCollection ) ;
XcodeFrameworkBuildPhase FrameworkPhase = new XcodeFrameworkBuildPhase ( Project . FileCollection ) ;
// filter frameworks that need to installed into the .app (either the framework or a bundle inside a .zip)
2023-05-30 18:59:32 -04:00
IEnumerable < UEBuildFramework > InstalledFrameworks = Frameworks . Where ( x = > x . bCopyFramework | | ! String . IsNullOrEmpty ( x . CopyBundledAssets ) ) ;
2023-05-10 16:53:20 -04:00
// filter frameworks that need to be unzipped before we compile
IEnumerable < UEBuildFramework > ZippedFrameworks = Frameworks . Where ( x = > x . ZipFile ! = null ) ;
bool bHasEmbeddedFrameworks = false ;
// only look at frameworks that need anything copied into
foreach ( UEBuildFramework Framework in InstalledFrameworks )
{
DirectoryReference ? FinalFrameworkDir = Framework . GetFrameworkDirectory ( null , null , Logger ) ;
if ( FinalFrameworkDir = = null )
{
continue ;
}
// the framework may come with FrameworkDir being parent of a .framework with name of Framework.Name
if ( ! FinalFrameworkDir . HasExtension ( ".framework" ) & & ! FinalFrameworkDir . HasExtension ( ".xcframework" ) )
{
FinalFrameworkDir = DirectoryReference . Combine ( FinalFrameworkDir , Framework . Name + ".framework" ) ;
}
DirectoryReference BundleRootDir = Framework . GetFrameworkDirectory ( null , null , Logger ) ! ;
if ( Framework . ZipFile ! = null )
{
if ( Framework . ZipFile . FullName . EndsWith ( ".embeddedframework.zip" ) )
{
// foo.embeddedframework.zip would have foo.framework inside it, which is what we want to install
FinalFrameworkDir = DirectoryReference . Combine ( Framework . ZipOutputDirectory ! , Framework . ZipFile . GetFileNameWithoutAnyExtensions ( ) + ".framework" ) ;
BundleRootDir = Framework . ZipOutputDirectory ! ;
}
else
{
FinalFrameworkDir = Framework . ZipOutputDirectory ! ;
}
}
// set up the CopyBundle to be copied
2023-05-30 18:59:32 -04:00
if ( ! String . IsNullOrEmpty ( Framework . CopyBundledAssets ) )
2023-05-10 16:53:20 -04:00
{
ResourcesBuildPhase . AddFolderResource ( DirectoryReference . Combine ( BundleRootDir , Framework . CopyBundledAssets ) , "Resources" ) ;
}
if ( Framework . bCopyFramework )
{
string FileRefGuid = XcodeProjectFileGenerator . MakeXcodeGuid ( ) ;
EmbedFrameworks . AddFramework ( FinalFrameworkDir , FileRefGuid ) ;
FrameworkPhase . AddFramework ( FinalFrameworkDir , FileRefGuid ) ;
bHasEmbeddedFrameworks = true ;
}
}
if ( bHasEmbeddedFrameworks )
{
BuildPhases . Add ( EmbedFrameworks ) ;
References . Add ( EmbedFrameworks ) ;
BuildPhases . Add ( FrameworkPhase ) ;
References . Add ( FrameworkPhase ) ;
}
// each zipped framework needs to be unzipped in case C++ code needs to compile/link against it - this will add unzip commands
// to the PreBuild script that is run before anything else happens - note that the ZipDependToken is shared with UBT, so that
// if a new framework is unzipped, it will dirty any source files in modules that use this framework
foreach ( UEBuildFramework Framework in ZippedFrameworks )
{
string ZipIn = Utils . MakePathSafeToUseWithCommandLine ( Framework . ZipFile ! . FullName ) ;
string ZipOut = Utils . MakePathSafeToUseWithCommandLine ( Framework . ZipOutputDirectory ! . FullName ) ;
// Zip contains folder with the same name, hence ParentDirectory
string ZipOutParent = Utils . MakePathSafeToUseWithCommandLine ( Framework . ZipOutputDirectory . ParentDirectory ! . FullName ) ;
string ZipDependToken = Utils . MakePathSafeToUseWithCommandLine ( Framework . ExtractedTokenFile ! . FullName ) ;
UnrealData . ExtraPreBuildScriptLines . AddRange ( new [ ]
{
// delete any output and make sure parent dir exists
$"if [ {ZipIn} -nt {ZipDependToken} ] " ,
$"then" ,
$" [ -d {ZipOut} ] && rm -rf {ZipOut}" ,
$" mkdir -p {ZipOutParent}" ,
// unzip the framework and maybe extra data
$" unzip -q -o {ZipIn} -d {ZipOutParent}" ,
$" touch {ZipDependToken}" ,
$"fi" ,
$"" ,
} ) ;
}
}
}
protected override void WriteExtraTargetProperties ( StringBuilder Content )
{
Content . WriteLine ( $"\t\t\tproductReference = {ProductGuid};" ) ;
Content . WriteLine ( $"\t\t\tproductName = \" { UnrealData . ProductName } \ ";" ) ;
}
2023-06-20 16:40:51 -04:00
private static Dictionary < string , string > PlistFileMap = new ( StringComparer . OrdinalIgnoreCase ) ;
private string GetPlistSigningName ( string ProvisionSetting , ILogger Logger )
{
string? SigningName ;
lock ( PlistFileMap )
{
if ( ! PlistFileMap . TryGetValue ( ProvisionSetting , out SigningName ) )
{
FileReference ProfileFile = AppleExports . ConvertFilePath ( UnrealData . UProjectFileLocation ? . Directory , ProvisionSetting ) ;
// get mobile provision UUID, either directly or from the probision
if ( ProfileFile . GetExtension ( ) = = ".mobileprovision" )
{
// security will read the provision and dump out the text (plist) bits of the profile, and plutil will extract the UUID to stdout
string UUID = Utils . RunLocalProcessAndReturnStdOut ( "/bin/sh" , $"-c 'security cms -D -i {ProfileFile.FullName.Replace(" ", " \ \ ")} | plutil -extract UUID raw -'" ) ;
// make sure it's living in user's library
// (note: i couldn't find a way to install it without opening Xcode, so copying it works well enough)
DirectoryReference UserDir = new DirectoryReference ( Environment . GetFolderPath ( Environment . SpecialFolder . UserProfile ) ) ;
FileReference InstalledProfileFile = FileReference . Combine ( UserDir , "Library" , "MobileDevice" , "Provisioning Profiles" , $"{UUID}.mobileprovision" ) ;
// copy if needed
if ( ! FileReference . Exists ( InstalledProfileFile ) )
{
DirectoryReference . CreateDirectory ( InstalledProfileFile . Directory ) ;
FileReference . Copy ( ProfileFile , InstalledProfileFile ) ;
Logger . LogInformation ( "Copying project's provision '{SourceProvision}' to your libary: '{TargetProvision}'" , ProfileFile , InstalledProfileFile ) ;
}
SigningName = UUID ;
}
else
{
SigningName = ProvisionSetting ;
}
PlistFileMap [ ProvisionSetting ] = SigningName ;
}
}
return SigningName ! ;
}
public override void WriteXcconfigFile ( ILogger Logger )
2023-05-10 16:53:20 -04:00
{
// gather general, all-platform, data we are doing to put into the configs
UnrealBuildConfig BuildConfig = BuildConfigList ! . BuildConfigs [ 0 ] . Info ;
TargetRules TargetRules = BuildConfig . ProjectTarget ! . TargetRules ! ;
2023-05-25 13:04:27 -04:00
DirectoryReference ProjectOrEngineDir = UnrealData . UProjectFileLocation ? . Directory ? ? Unreal . EngineDirectory ;
2023-05-30 18:38:07 -04:00
2023-05-25 13:04:27 -04:00
// point to the shader Engine/Binaries for content only project (sadly, the TargetRules.OutputFile is not filled out)
DirectoryReference ConfigBuildDir = ( TargetRules . Type = = TargetType . Editor | | UnrealData . bIsContentOnlyProject ) ? Unreal . EngineDirectory : ProjectOrEngineDir ;
if ( TargetRules . Type = = TargetType . Program & & TargetRules . File ! . IsUnderDirectory ( Unreal . EngineDirectory ) )
{
2023-06-14 03:54:47 -04:00
ConfigBuildDir = BuildConfig . RootDirectory ;
2023-05-25 13:04:27 -04:00
}
string BinariesBaseDir = ConfigBuildDir . GetDirectoryName ( ) ;
2023-05-10 16:53:20 -04:00
2023-05-25 13:04:27 -04:00
ConfigBuildDir = DirectoryReference . Combine ( ConfigBuildDir , "Binaries" , Platform . ToString ( ) , TargetRules . ExeBinariesSubFolder ) ;
2023-05-30 18:38:07 -04:00
2023-05-10 16:53:20 -04:00
MetadataPlatform MetadataPlatform ;
string TargetPlatformName = Platform . ToString ( ) ;
2023-05-25 13:04:27 -04:00
if ( TargetRules . Type ! = TargetType . Game & & TargetRules . Type ! = TargetType . Program )
2023-05-10 16:53:20 -04:00
{
TargetPlatformName + = TargetRules . Type . ToString ( ) ;
}
// get ini file for the platform
ConfigHierarchy PlatformIni = ConfigCache . ReadHierarchy ( ConfigHierarchyType . Engine , UnrealData . UProjectFileLocation ? . Directory , Platform ) ;
// settings for all platforms
2023-06-20 16:40:51 -04:00
bool bAutomaticSigning ;
bool bMacSignToRunLocally ;
2023-05-10 16:53:20 -04:00
string? SigningTeam ;
string? SigningPrefix ;
string? AppCategory ;
string SupportedPlatforms ;
string SDKRoot ;
string DeploymentTarget ;
string DeploymentTargetKey ;
2023-06-20 16:40:51 -04:00
string? SigningIdentity = null ;
2023-05-10 16:53:20 -04:00
string? ProvisioningProfile = null ;
string? SupportedDevices = null ;
string? MarketingVersion = null ;
string? BundleIdentifier ;
List < string > ExtraConfigLines = new ( ) ;
2023-06-20 16:40:51 -04:00
// get signing settings
PlatformIni . TryGetValue ( "/Script/MacTargetPlatform.XcodeProjectSettings" , "bUseAutomaticCodeSigning" , out bAutomaticSigning ) ;
PlatformIni . TryGetValue ( "/Script/MacTargetPlatform.XcodeProjectSettings" , "bMacSignToRunLocally" , out bMacSignToRunLocally ) ;
PlatformIni . TryGetValue ( "/Script/MacTargetPlatform.XcodeProjectSettings" , "CodeSigningTeam" , out SigningTeam ) ;
PlatformIni . TryGetValue ( "/Script/MacTargetPlatform.XcodeProjectSettings" , "CodeSigningPrefix" , out SigningPrefix ) ;
PlatformIni . TryGetValue ( "/Script/MacTargetPlatform.XcodeProjectSettings" , $"{Platform}ProvisioningProfile" , out ProvisioningProfile ) ;
PlatformIni . TryGetValue ( "/Script/MacTargetPlatform.XcodeProjectSettings" , $"{Platform}SigningIdentity" , out SigningIdentity ) ;
2023-05-10 16:53:20 -04:00
PlatformIni . TryGetValue ( "/Script/MacTargetPlatform.XcodeProjectSettings" , "AppCategory" , out AppCategory ) ;
2023-06-20 16:40:51 -04:00
2023-05-10 16:53:20 -04:00
if ( Platform = = UnrealTargetPlatform . Mac )
{
// editor vs game metadata
bool bIsEditor = UnrealData . TargetRules . Type = = TargetType . Editor ;
MetadataPlatform = bIsEditor ? MetadataPlatform . MacEditor : MetadataPlatform . Mac ;
SDKRoot = "macosx" ;
SupportedPlatforms = "macosx" ;
DeploymentTargetKey = "MACOSX_DEPLOYMENT_TARGET" ;
DeploymentTarget = MacToolChain . Settings . MacOSVersion ;
BundleIdentifier = bIsEditor ? "com.epicgames.UnrealEditor" : UnrealData . BundleIdentifier ;
// @todo: get a version for games, like IOS has
MarketingVersion = MacToolChain . LoadEngineDisplayVersion ( ) ;
2023-05-30 18:59:32 -04:00
string SupportedMacArchitectures = String . Join ( " " , XcodeUtils . GetSupportedMacArchitectures ( BuildConfig . BuildTarget , UnrealData . UProjectFileLocation ) . Architectures . Select ( x = > x . AppleName ) ) ;
2023-05-10 16:53:20 -04:00
ExtraConfigLines . Add ( $"VALID_ARCHS = {SupportedMacArchitectures}" ) ;
}
else
{
MetadataPlatform = MetadataPlatform . IOS ;
// get IOS (same as TVOS) BundleID, and if there's a specified plist with a bundleID, use it, as Xcode would warn if they don't match
BundleIdentifier = UnrealData . BundleIdentifier ;
// short version string
PlatformIni . GetString ( $"/Script/IOSRuntimeSettings.IOSRuntimeSettings" , "VersionInfo" , out MarketingVersion ) ;
if ( Platform = = UnrealTargetPlatform . IOS )
{
SDKRoot = "iphoneos" ;
SupportedPlatforms = "iphoneos" ; // iphonesimulator
DeploymentTargetKey = "IPHONEOS_DEPLOYMENT_TARGET" ;
SupportedDevices = UnrealData . IOSProjectSettings ! . RuntimeDevices ;
DeploymentTarget = UnrealData . IOSProjectSettings . RuntimeVersion ;
// only iphone deals with orientation
List < string > SupportedOrientations = XcodeUtils . GetSupportedOrientations ( PlatformIni ) ;
2023-05-30 18:59:32 -04:00
ExtraConfigLines . Add ( $"INFOPLIST_KEY_UISupportedInterfaceOrientations = \" { String . Join ( " " , SupportedOrientations ) } \ "" ) ;
2023-06-16 10:28:50 -04:00
// iPhone is always Fullscreen, however, an iPad can support SplitView mode (dynamic view resizeing), so check if that's enabled and then add iPad UISupportedInterfaceOrientations
bool bEnableSplitView = false ;
PlatformIni . GetBool ( "/Script/IOSRuntimeSettings.IOSRuntimeSettings" , "bEnableSplitView" , out bEnableSplitView ) ;
if ( bEnableSplitView )
{
ExtraConfigLines . Add ( $"INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown" ) ;
ExtraConfigLines . Add ( $"INFOPLIST_KEY_UIRequiresFullScreen_iPad = false" ) ;
}
2023-05-10 16:53:20 -04:00
}
2023-06-23 20:29:27 -04:00
else // tvos
2023-05-10 16:53:20 -04:00
{
SDKRoot = "appletvos" ;
SupportedPlatforms = "appletvos" ; // appletvsimulator
DeploymentTargetKey = "TVOS_DEPLOYMENT_TARGET" ;
SupportedDevices = UnrealData . TVOSProjectSettings ! . RuntimeDevices ;
DeploymentTarget = UnrealData . TVOSProjectSettings . RuntimeVersion ;
}
}
// get metadata for the platform set above
MetadataItem PlistMetadata = UnrealData . Metadata ! . PlistFiles [ MetadataPlatform ] ;
// now pull the bundle id's out, as xcode will warn if they don't match (this has to happen after each platform set bundle id above)
XcodeUtils . FindPlistId ( PlistMetadata , "CFBundleIdentifier" , ref BundleIdentifier ) ;
// if the user had a ini setting, but also set it in the template plist, the plist one should win (this is only used if we are updating a template plist)
XcodeUtils . FindPlistId ( PlistMetadata , "LSApplicationCategoryType" , ref AppCategory ) ;
// include another xcconfig for versions that UBT writes out
Xcconfig ! . AppendLine ( $"#include? \" { ProjectOrEngineDir } / Intermediate / Build / Versions . xcconfig \ "" ) ;
2023-05-25 13:04:27 -04:00
if ( UnrealData . UProjectFileLocation = = null )
{
Xcconfig ! . AppendLine ( $"#include? \" { XcodeProjectFileGenerator . ContentOnlySettingsFile } \ "" ) ;
}
2023-05-10 16:53:20 -04:00
// write out some UE variables that can be used in premade .plist files, etc
Xcconfig . AppendLine ( "" ) ;
Xcconfig . AppendLine ( "// Unreal project-wide variables" ) ;
2023-05-25 13:04:27 -04:00
Xcconfig . AppendLine ( $"UE_XCODE_BUILD_MODE = Default" ) ;
2023-05-10 16:53:20 -04:00
Xcconfig . AppendLine ( $"UE_PRODUCT_NAME = {UnrealData.ProductName}" ) ;
Xcconfig . AppendLine ( $"UE_PRODUCT_NAME_STRIPPED = {UnrealData.ProductName.Replace(" _ ", " ").Replace(" ", " ")}" ) ;
Xcconfig . AppendLine ( $"UE_DISPLAY_NAME = {UnrealData.DisplayName}" ) ;
Xcconfig . AppendLine ( $"UE_SIGNING_PREFIX = {SigningPrefix}" ) ;
Xcconfig . AppendLine ( $"UE_PLATFORM_NAME = {Platform}" ) ;
2023-05-24 14:31:06 -04:00
Xcconfig . AppendLine ( $"UE_TARGET_NAME = {UnrealData.TargetRules.OriginalName}" ) ;
2023-05-10 16:53:20 -04:00
Xcconfig . AppendLine ( $"UE_TARGET_PLATFORM_NAME = {TargetPlatformName}" ) ;
Xcconfig . AppendLine ( $"UE_ENGINE_DIR = {Unreal.EngineDirectory}" ) ;
2023-05-25 13:04:27 -04:00
Xcconfig . AppendLine ( $"UE_BINARIES_DIR = {ConfigBuildDir}" ) ;
Xcconfig . AppendLine ( $"UE_STAGED_BINARIES_DIR_BASE = {BinariesBaseDir}" ) ;
2023-05-10 16:53:20 -04:00
if ( UnrealData . UProjectFileLocation ! = null )
{
Xcconfig . AppendLine ( $"UE_PROJECT_DIR = {UnrealData.UProjectFileLocation.Directory}" ) ;
}
Xcconfig . AppendLine ( "" ) ;
Xcconfig . AppendLine ( "// Constant settings (same for all platforms and targets)" ) ;
Xcconfig . AppendLine ( "INFOPLIST_OUTPUT_FORMAT = xml" ) ;
Xcconfig . AppendLine ( "COMBINE_HIDPI_IMAGES = YES" ) ;
Xcconfig . AppendLine ( "USE_HEADERMAP = NO" ) ;
Xcconfig . AppendLine ( "ONLY_ACTIVE_ARCH = YES" ) ;
Xcconfig . AppendLine ( "" ) ;
Xcconfig . AppendLine ( "// Platform settings" ) ;
Xcconfig . AppendLine ( $"SUPPORTED_PLATFORMS = {SupportedPlatforms}" ) ;
Xcconfig . AppendLine ( $"SDKROOT = {SDKRoot}" ) ;
2023-06-09 10:41:06 -04:00
// Xcode creates the Build Dir (where the .app is) by combining {SYMROOT}/{CONFIGURATION}{EFFECTIVE_PLATFORM_NAME}, so we set SYMROOT
// to the Parent directory of the diectory the binary is in, CONFIGURATION to nothing, and EFFECTIVE_PLATFORM_NAME to the directory
// the binary is in
2023-05-10 16:53:20 -04:00
Xcconfig . AppendLine ( "" ) ;
Xcconfig . AppendLine ( $"// These settings combined will tell Xcode to write to Binaries/{Platform} (instead of something like Binaries/Development-iphoneos)" ) ;
Xcconfig . AppendLine ( $"SYMROOT = {ConfigBuildDir.ParentDirectory}" ) ;
Xcconfig . AppendLine ( $"CONFIGURATION = " ) ;
2023-06-09 10:41:06 -04:00
Xcconfig . AppendLine ( $"EFFECTIVE_PLATFORM_NAME = {ConfigBuildDir.GetDirectoryName()}" ) ;
2023-05-10 16:53:20 -04:00
if ( ExtraConfigLines . Count > 0 )
{
Xcconfig . AppendLine ( "" ) ;
Xcconfig . AppendLine ( "// Misc settings" ) ;
foreach ( string Line in ExtraConfigLines )
{
Xcconfig . AppendLine ( Line ) ;
}
}
Xcconfig . AppendLine ( "" ) ;
Xcconfig . AppendLine ( "// Project settings" ) ;
Xcconfig . AppendLine ( $"TARGETED_DEVICE_FAMILY = {SupportedDevices}" ) ;
Xcconfig . AppendLine ( $"PRODUCT_BUNDLE_IDENTIFIER = {BundleIdentifier}" ) ;
Xcconfig . AppendLine ( $"{DeploymentTargetKey} = {DeploymentTarget}" ) ;
Xcconfig . AppendLine ( "" ) ;
Xcconfig . AppendLine ( "// Plist settings" ) ;
Xcconfig . AppendLine ( $"INFOPLIST_FILE = {PlistMetadata.XcodeProjectRelative}" ) ;
if ( PlistMetadata . Mode = = MetadataMode . UpdateTemplate )
{
// allow Xcode to generate the final plist file from our input, some INFOPLIST settings and other settings
Xcconfig . AppendLine ( "GENERATE_INFOPLIST_FILE = YES" ) ;
Xcconfig . AppendLine ( $"ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon" ) ;
Xcconfig . AppendLine ( $"CURRENT_PROJECT_VERSION = $(UE_{Platform.ToString().ToUpper()}_BUILD_VERSION)" ) ;
Xcconfig . AppendLine ( $"MARKETING_VERSION = {MarketingVersion}" ) ;
Xcconfig . AppendLine ( $"INFOPLIST_KEY_LSApplicationCategoryType = {AppCategory}" ) ;
}
2023-06-20 16:40:51 -04:00
// always use defualt codesigning for Editor, which is for running locally
if ( UnrealData . bWriteCodeSigningSettings & & UnrealData . TargetRules . Type ! = TargetType . Editor )
2023-05-10 16:53:20 -04:00
{
Xcconfig . AppendLine ( "" ) ;
Xcconfig . AppendLine ( "// Code-signing settings" ) ;
Xcconfig . AppendLine ( "CODE_SIGN_STYLE = " + ( bAutomaticSigning ? "Automatic" : "Manual" ) ) ;
2023-06-20 16:40:51 -04:00
Xcconfig . AppendLine ( $"DEVELOPMENT_TEAM = {SigningTeam}" ) ;
// Mac has Sign to Run Locally to deal with
if ( Platform = = UnrealTargetPlatform . Mac )
2023-05-10 16:53:20 -04:00
{
2023-06-20 16:40:51 -04:00
// when set, always use the - identity, that's how Xcode selects "Sign to Run Locally"
// if it is not set, and Automatic is on, then it will use it's default of "Apple Development", but we don't specify it in case default changes
if ( bMacSignToRunLocally )
{
Xcconfig . AppendLine ( $"CODE_SIGN_IDENTITY = -" ) ;
}
else if ( ! bAutomaticSigning & & SigningIdentity ! = null )
{
Xcconfig . AppendLine ( $"CODE_SIGN_IDENTITY = \" { SigningIdentity } \ "" ) ;
}
2023-05-10 16:53:20 -04:00
}
2023-06-20 16:40:51 -04:00
else
2023-05-10 16:53:20 -04:00
{
2023-06-20 16:40:51 -04:00
if ( ! bAutomaticSigning )
{
// only use profile on IOS
if ( ! String . IsNullOrEmpty ( ProvisioningProfile ) )
{
string SigningName = GetPlistSigningName ( ProvisioningProfile , Logger ) ;
Xcconfig . AppendLine ( $"PROVISIONING_PROFILE_SPECIFIER = {SigningName}" ) ;
}
if ( SigningIdentity ! = null )
{
Xcconfig . AppendLine ( $"CODE_SIGN_IDENTITY = \" { SigningIdentity } \ "" ) ;
}
}
2023-05-10 16:53:20 -04:00
}
}
Xcconfig . Write ( ) ;
// Now for each config write out the specific settings
DirectoryReference ? GameDir = UnrealData . UProjectFileLocation ? . Directory ;
string? GamePath = GameDir ! = null ? XcodeFileCollection . ConvertPath ( GameDir . FullName ) : null ;
foreach ( UnrealBuildConfig Config in UnrealData . AllConfigs )
{
XcodeBuildConfig ? MatchedConfig = BuildConfigList ! . BuildConfigs . FirstOrDefault ( x = > x . Info = = Config ) ;
if ( MatchedConfig = = null | | ! Config . Supports ( Platform ) )
{
continue ;
}
2023-05-25 13:04:27 -04:00
// hook up the Buildconfig that matches this info to this xcconfig file
XcconfigFile ConfigXcconfig = MatchedConfig . Xcconfig ! ;
string ExecutableName = XcodeUtils . MakeExecutableFileName ( Config . ExeName , Platform , Config . BuildConfig , TargetRules . Architectures , TargetRules . UndecoratedConfiguration ) ;
2023-06-12 14:56:21 -04:00
string ExetuableSubPath = FileReference . Combine ( ConfigBuildDir , ExecutableName ) . MakeRelativeTo ( ConfigBuildDir ) ;
2023-05-25 13:04:27 -04:00
string ProductName = ExecutableName ;
string ExecutableKey = $"UE_{Platform.ToString().ToUpper()}_EXECUTABLE_NAME" ;
2023-05-10 16:53:20 -04:00
MetadataItem EntitlementsMetadata = UnrealData . Metadata ! . EntitlementsFiles [ MetadataPlatform ] ;
2023-05-30 18:38:07 -04:00
2023-05-10 16:53:20 -04:00
// if we have shipping specified, use it instead
if ( Config . BuildConfig = = UnrealTargetConfiguration . Shipping )
{
MetadataItem ShippingEntitlementsMetadata = UnrealData . Metadata ! . ShippingEntitlementsFiles [ MetadataPlatform ] ;
if ( ShippingEntitlementsMetadata . Mode = = MetadataMode . UsePremade )
{
EntitlementsMetadata = ShippingEntitlementsMetadata ;
}
}
ConfigXcconfig . AppendLine ( "// pull in the shared settings for all configs for this target" ) ;
ConfigXcconfig . AppendLine ( $"#include \" { Xcconfig . Name } . xcconfig \ "" ) ;
ConfigXcconfig . AppendLine ( "" ) ;
ConfigXcconfig . AppendLine ( "// Unreal per-config variables" ) ;
ConfigXcconfig . AppendLine ( $"UE_TARGET_CONFIG = {Config.BuildConfig}" ) ;
2023-05-25 13:04:27 -04:00
ConfigXcconfig . AppendLine ( $"UE_UBT_BINARY_SUBPATH = {ExetuableSubPath}" ) ;
2023-05-10 16:53:20 -04:00
ConfigXcconfig . AppendLine ( $"{ExecutableKey} = {ExecutableName}" ) ;
ConfigXcconfig . AppendLine ( $"PRODUCT_NAME = {ProductName}" ) ;
if ( EntitlementsMetadata . Mode = = MetadataMode . UsePremade )
{
ConfigXcconfig . AppendLine ( $"CODE_SIGN_ENTITLEMENTS = {EntitlementsMetadata.XcodeProjectRelative}" ) ;
}
// debug settings
if ( Config . BuildConfig = = UnrealTargetConfiguration . Debug )
{
ConfigXcconfig . AppendLine ( "ENABLE_TESTABILITY = YES" ) ;
}
ConfigXcconfig . Write ( ) ;
}
}
}
class XcodeBuildTarget : XcodeTarget
{
public XcodeBuildTarget ( XcodeProject Project )
: base ( XcodeTarget . Type . Build , Project . UnrealData )
{
BuildConfigList = new XcodeBuildConfigList ( Project . Platform , Name , Project . UnrealData , bIncludeAllPlatformsInConfig : false ) ;
References . Add ( BuildConfigList ) ;
}
}
class XcodeIndexTarget : XcodeTarget
{
private UnrealData UnrealData ;
// just take the Project since it has everything we need, and is needed when adding target dependencies
public XcodeIndexTarget ( XcodeProject Project )
: base ( XcodeTarget . Type . Index , Project . UnrealData )
{
UnrealData = Project . UnrealData ;
BuildConfigList = new XcodeBuildConfigList ( Project . Platform , Name , UnrealData , bIncludeAllPlatformsInConfig : false ) ;
References . Add ( BuildConfigList ) ;
CreateXcconfigFile ( Project , Project . Platform , Name ) ;
// hook up each buildconfig to this Xcconfig
BuildConfigList . BuildConfigs . ForEach ( x = > x . Xcconfig = Xcconfig ) ;
// add all of the files to be natively compiled by this target
XcodeSourcesBuildPhase SourcesBuildPhase = new XcodeSourcesBuildPhase ( ) ;
BuildPhases . Add ( SourcesBuildPhase ) ;
References . Add ( SourcesBuildPhase ) ;
foreach ( KeyValuePair < XcodeSourceFile , FileReference ? > Pair in Project . FileCollection . BuildableFilesToResponseFile )
{
// only add files that found a moduleto be part of (since we can't build without the build settings that come from a module)
if ( Pair . Value ! = null )
{
SourcesBuildPhase . AddFile ( Pair . Key ) ;
}
}
}
2023-06-20 16:40:51 -04:00
public override void WriteXcconfigFile ( ILogger Logger )
2023-05-10 16:53:20 -04:00
{
// write out settings that for compiling natively
Xcconfig ! . AppendLine ( "CLANG_CXX_LANGUAGE_STANDARD = c++17" ) ;
Xcconfig . AppendLine ( "GCC_WARN_INHIBIT_ALL_WARNINGS = YES" ) ;
Xcconfig . AppendLine ( "GCC_PRECOMPILE_PREFIX_HEADER = YES" ) ;
Xcconfig . AppendLine ( "GCC_OPTIMIZATION_LEVEL = 0" ) ;
Xcconfig . AppendLine ( $"PRODUCT_NAME = {Name}" ) ;
Xcconfig . AppendLine ( "SYMROOT = build" ) ;
Xcconfig . AppendLine ( "USE_HEADERMAP = NO" ) ;
Xcconfig . AppendLine ( "WARNING_CFLAGS = -Wno-c++11-narrowing" ) ;
Xcconfig . Write ( ) ;
}
}
class XcodeProject : XcodeProjectNode
{
// a null platform here means all platforms like the old way
public UnrealTargetPlatform ? Platform ;
// the blob of data coming from unreal that we can pass around
public UnrealData UnrealData ;
// container for all files and groups
public XcodeFileCollection FileCollection ;
// Guid for the project node
public string Guid = XcodeProjectFileGenerator . MakeXcodeGuid ( ) ;
private string ProvisioningStyle ;
public List < XcodeRunTarget > RunTargets = new ( ) ;
public XcodeBuildConfigList ProjectBuildConfigs ;
public XcodeProject ( UnrealTargetPlatform ? Platform , UnrealData UnrealData , XcodeFileCollection FileCollection , XcodeProjectFile ProjectFile , bool bIsStubEditor , ILogger Logger )
{
this . Platform = Platform ;
this . UnrealData = UnrealData ;
this . FileCollection = FileCollection ;
ProvisioningStyle = UnrealData . bUseAutomaticSigning ? "Automatic" : "Manual" ;
// if we are run-only, then we don't need a build target (this is shared between platforms if we are doing multi-target)
XcodeBuildTarget ? BuildTarget = null ;
if ( ! XcodeProjectFileGenerator . bGenerateRunOnlyProject & & ! UnrealData . bIsStubProject & & ! bIsStubEditor )
{
BuildTarget = new XcodeBuildTarget ( this ) ;
}
if ( ! bIsStubEditor )
{
// create one run target for each platform if our platform is null (ie XcodeProjectGenerator.PerPlatformMode is RunTargetPerPlatform)
List < UnrealTargetPlatform > TargetPlatforms = Platform = = null ? XcodeProjectFileGenerator . XcodePlatforms : new ( ) { Platform . Value } ;
foreach ( UnrealTargetPlatform TargetPlatform in TargetPlatforms )
{
XcodeRunTarget RunTarget = new XcodeRunTarget ( this , UnrealData . ProductName , UnrealData . AllConfigs [ 0 ] . ProjectTarget ! . TargetRules ! . Type , TargetPlatform , BuildTarget , ProjectFile , Logger ) ;
RunTargets . Add ( RunTarget ) ;
References . Add ( RunTarget ) ;
}
}
ProjectBuildConfigs = new XcodeBuildConfigList ( bIsStubEditor ? null : Platform , UnrealData . ProductName , UnrealData , bIncludeAllPlatformsInConfig : true ) ;
References . Add ( ProjectBuildConfigs ) ;
2023-05-30 18:38:07 -04:00
2023-05-10 16:53:20 -04:00
// make an indexing target if we aren't just a run-only project, and it has buildable source files
if ( ! XcodeProjectFileGenerator . bGenerateRunOnlyProject & & UnrealData . BatchedFiles . Count ! = 0 )
2023-05-30 18:38:07 -04:00
{
2023-05-10 16:53:20 -04:00
// index isn't a dependency of run, it's simply a target that xcode will find to index from
XcodeIndexTarget IndexTarget = new XcodeIndexTarget ( this ) ;
References . Add ( IndexTarget ) ;
}
}
public override void Write ( StringBuilder Content )
{
Content . WriteLine ( "/* Begin PBXProject section */" ) ;
Content . WriteLine ( 2 , $"{Guid} /* Project object */ = {{" ) ;
2023-05-30 18:38:07 -04:00
Content . WriteLine ( 3 , "isa = PBXProject;" ) ;
Content . WriteLine ( 3 , "attributes = {" ) ;
Content . WriteLine ( 4 , "LastUpgradeCheck = 2000;" ) ;
Content . WriteLine ( 4 , "ORGANIZATIONNAME = \"Epic Games, Inc.\";" ) ;
Content . WriteLine ( 4 , "TargetAttributes = {" ) ;
2023-05-10 16:53:20 -04:00
foreach ( XcodeRunTarget RunTarget in RunTargets )
2023-05-30 18:38:07 -04:00
{
Content . WriteLine ( 5 , $"{RunTarget.Guid} = {{" ) ;
Content . WriteLine ( 6 , $"ProvisioningStyle = {ProvisioningStyle};" ) ;
Content . WriteLine ( 5 , "};" ) ;
2023-05-10 16:53:20 -04:00
}
2023-05-30 18:38:07 -04:00
Content . WriteLine ( 4 , "};" ) ;
Content . WriteLine ( 3 , "};" ) ;
Content . WriteLine ( 3 , $"buildConfigurationList = {ProjectBuildConfigs.Guid} /* Build configuration list for PBXProject \" { ProjectBuildConfigs . TargetName } \ " */;" ) ;
Content . WriteLine ( 3 , "compatibilityVersion = \"Xcode 8.0\";" ) ;
Content . WriteLine ( 3 , "developmentRegion = English;" ) ;
Content . WriteLine ( 3 , "hasScannedForEncodings = 0;" ) ;
Content . WriteLine ( 3 , "knownRegions = (" ) ;
Content . WriteLine ( 4 , "en" ) ;
Content . WriteLine ( 3 , ");" ) ;
Content . WriteLine ( 3 , $"mainGroup = {FileCollection.MainGroupGuid};" ) ;
2023-05-10 16:53:20 -04:00
// for stub editor projects, we don't have a run target, so we don't have a product folder
if ( RunTargets . Count > 0 )
{
Content . WriteLine ( 3 , $"productRefGroup = {FileCollection.GetProductGroupGuid()};" ) ;
}
2023-05-30 18:38:07 -04:00
Content . WriteLine ( 3 , "projectDirPath = \"\";" ) ;
Content . WriteLine ( 3 , "projectRoot = \"\";" ) ;
Content . WriteLine ( 3 , "targets = (" ) ;
2023-05-10 16:53:20 -04:00
foreach ( XcodeTarget Target in XcodeProjectNode . GetNodesOfType < XcodeTarget > ( this ) )
{
Content . WriteLine ( 4 , $"{Target.Guid} /* {Target.Name} */," ) ;
}
Content . WriteLine ( 3 , ");" ) ;
Content . WriteLine ( 2 , "};" ) ;
Content . WriteLine ( "/* End PBXProject section */" ) ;
}
}
class XcodeProjectFile : ProjectFile
{
/// <summary>
/// Constructs a new project file object
/// </summary>
/// <param name="InitFilePath">The path to the project file on disk</param>
/// <param name="BaseDir">The base directory for files within this project</param>
/// <param name="bIsForDistribution">True for distribution builds</param>
/// <param name="BundleID">Override option for bundle identifier</param>
/// <param name="AppName"></param>
/// <param name="bMakeProjectPerTarget"></param>
/// <param name="SingleTargetName"></param>
public XcodeProjectFile ( FileReference InitFilePath , DirectoryReference BaseDir , bool bIsForDistribution , string BundleID , string AppName , bool bMakeProjectPerTarget , string? SingleTargetName )
: base ( InitFilePath , BaseDir )
{
2023-05-25 13:04:27 -04:00
UnrealData = new UnrealData ( InitFilePath , bIsForDistribution , BundleID , AppName , bMakeProjectPerTarget ) ;
2023-05-10 16:53:20 -04:00
// create the container for all the files that will
SharedFileCollection = new XcodeFileCollection ( this ) ;
FileCollection = SharedFileCollection ;
this . SingleTargetName = SingleTargetName ;
}
public UnrealData UnrealData ;
/// <summary>
/// The PBXPRoject node, root of everything
/// </summary>
public Dictionary < XcodeProject , UnrealTargetPlatform ? > RootProjects = new ( ) ;
private XcodeProjectLegacy . XcodeProjectFile ? LegacyProjectFile = null ;
private bool bHasCheckedForLegacy = false ;
public bool bHasLegacyProject = > LegacyProjectFile ! = null ;
/// <summary>
/// Gathers the files and generates project sections
/// </summary>
private XcodeFileCollection SharedFileCollection ;
public XcodeFileCollection FileCollection ;
// if set, only this will be written
private string? SingleTargetName ;
/// <summary>
/// Allocates a generator-specific source file object
/// </summary>
/// <param name="InitFilePath">Path to the source file on disk</param>
/// <param name="InitProjectSubFolder">Optional sub-folder to put the file in. If empty, this will be determined automatically from the file's path relative to the project file</param>
/// <returns>The newly allocated source file object</returns>
public override SourceFile ? AllocSourceFile ( FileReference InitFilePath , DirectoryReference ? InitProjectSubFolder )
{
if ( InitFilePath . GetFileName ( ) . StartsWith ( "." ) )
{
return null ;
}
return new XcodeSourceFile ( InitFilePath , InitProjectSubFolder ) ;
}
public void ConditionalCreateLegacyProject ( )
{
if ( ! bHasCheckedForLegacy )
{
bHasCheckedForLegacy = true ;
if ( ProjectTargets . Count = = 0 )
{
throw new BuildException ( "Expected to have a target before AddModule is called" ) ;
}
2023-05-25 13:04:27 -04:00
UnrealData . InitializeUProjectFileLocation ( this ) ;
2023-05-26 12:51:21 -04:00
if ( ! AppleExports . UseModernXcode ( ProjectFilePath ) )
2023-05-10 16:53:20 -04:00
{
LegacyProjectFile = new XcodeProjectLegacy . XcodeProjectFile ( ProjectFilePath , BaseDir , UnrealData . bForDistribution , UnrealData . BundleIdentifier , UnrealData . AppName , UnrealData . bMakeProjectPerTarget ) ;
LegacyProjectFile . ProjectTargets . AddRange ( ProjectTargets ) ;
LegacyProjectFile . SourceFiles . AddRange ( SourceFiles ) ;
LegacyProjectFile . IsGeneratedProject = IsGeneratedProject ;
LegacyProjectFile . IsStubProject = IsStubProject ;
LegacyProjectFile . IsForeignProject = IsForeignProject ;
}
}
}
public override void AddModule ( UEBuildModuleCPP Module , CppCompileEnvironment CompileEnvironment )
{
ConditionalCreateLegacyProject ( ) ;
if ( LegacyProjectFile ! = null )
{
LegacyProjectFile . AddModule ( Module , CompileEnvironment ) ;
return ;
}
UnrealData . AddModule ( Module , CompileEnvironment ) ;
}
/// <summary>
/// Generates bodies of all sections that contain a list of source files plus a dictionary of project navigator groups.
/// </summary>
private void ProcessSourceFiles ( )
{
// process the files that came from UE/cross-platform land
SourceFiles . SortBy ( x = > x . Reference . FullName ) ;
Dictionary < DirectoryReference , int > BuildFileMap = new ( ) ;
foreach ( XcodeSourceFile SourceFile in SourceFiles . OfType < XcodeSourceFile > ( ) )
{
2023-05-30 18:38:07 -04:00
SharedFileCollection . ProcessFile ( SourceFile , bIsForBuild : IsGeneratedProject , bIsFolder : false , SourceToBuildFileMap : BuildFileMap ) ;
2023-05-10 16:53:20 -04:00
}
// cache the main group
2023-06-20 16:40:51 -04:00
SharedFileCollection . MainGroupGuid = XcodeFileCollection . GetRootGroupGuid ( SharedFileCollection . Groups , UnrealData . XcodeProjectFileLocation ) ;
2023-05-10 16:53:20 -04:00
// filter each file into the appropriate batch
foreach ( XcodeSourceFile File in SharedFileCollection . BuildableFilesToResponseFile . Keys )
{
AddFileToBatch ( File , SharedFileCollection ) ;
}
// write out the response files for each batch now that everything is done
foreach ( UnrealBatchedFiles Batch in UnrealData . BatchedFiles )
{
Batch . GenerateResponseFile ( ) ;
}
}
private void AddFileToBatch ( XcodeSourceFile File , XcodeFileCollection FileCollection )
{
foreach ( UnrealBatchedFiles Batch in UnrealData . BatchedFiles )
{
if ( Batch . Module . ContainsFile ( File . Reference ) )
{
Batch . Files . Add ( File ) ;
FileCollection . BuildableFilesToResponseFile [ File ] = Batch . ResponseFile ;
return ;
}
}
}
public FileReference ProjectFilePathForPlatform ( UnrealTargetPlatform ? Platform )
{
return new FileReference ( XcodeUtils . ProjectDirPathForPlatform ( UnrealData . XcodeProjectFileLocation , Platform ) . FullName ) ;
}
public FileReference PBXFilePathForPlatform ( UnrealTargetPlatform ? Platform )
{
return FileReference . Combine ( XcodeUtils . ProjectDirPathForPlatform ( UnrealData . XcodeProjectFileLocation , Platform ) , "project.pbxproj" ) ;
}
/// Implements Project interface
public override bool WriteProjectFile ( List < UnrealTargetPlatform > InPlatforms , List < UnrealTargetConfiguration > InConfigurations , PlatformProjectGeneratorCollection PlatformProjectGenerators , ILogger Logger )
{
// if we don't want this one, just skip
if ( SingleTargetName ! = null & & ! ProjectFilePath . GetFileNameWithoutAnyExtensions ( ) . Equals ( SingleTargetName , StringComparison . InvariantCultureIgnoreCase ) )
{
return true ;
}
ConditionalCreateLegacyProject ( ) ;
if ( LegacyProjectFile ! = null )
{
return LegacyProjectFile . WriteProjectFile ( InPlatforms , InConfigurations , PlatformProjectGenerators , Logger ) ;
}
if ( UnrealData . Initialize ( this , InConfigurations , Logger ) = = false )
{
// if we failed to initialize, we silently return to move on (it's not an error, it's a project with nothing to do)
return true ;
}
// look for an existing project to use as a template (if none found, create one from scratch)
DirectoryReference BuildDirLocation = UnrealData . UProjectFileLocation = = null ? Unreal . EngineDirectory : UnrealData . UProjectFileLocation . Directory ;
string ExistingProjectName = UnrealData . ProductName ;
FileReference TemplateProject = FileReference . Combine ( BuildDirLocation , $"Build/IOS/{UnrealData.ProductName}.xcodeproj/project.pbxproj" ) ;
// @todo this for per-platform!
UnrealData . bIsMergingProjects = FileReference . Exists ( TemplateProject ) ;
UnrealData . bWriteCodeSigningSettings = ! UnrealData . bIsMergingProjects ;
// turn all UE files into internal representation
ProcessSourceFiles ( ) ;
bool bSuccess = true ;
foreach ( UnrealTargetPlatform ? Platform in XcodeProjectFileGenerator . WorkspacePlatforms )
{
bool bAddStubEditor = Platform ! = UnrealTargetPlatform . Mac & & UnrealData . ProductName = = "UnrealEditor" ;
// skip the platform if the project has no configurations for it
if ( ! bAddStubEditor & & ! UnrealData . AllConfigs . Any ( x = > x . Supports ( Platform ) ) )
{
continue ;
}
FileReference PBXFilePath = PBXFilePathForPlatform ( Platform ) ;
// now create the xcodeproject elements (project -> target -> buildconfigs, etc)
FileCollection = new XcodeFileCollection ( SharedFileCollection ) ;
XcodeProject RootProject = new XcodeProject ( Platform , UnrealData , FileCollection , this , bAddStubEditor , Logger ) ;
RootProjects [ RootProject ] = Platform ;
if ( FileReference . Exists ( TemplateProject ) )
{
// @todo hahahaah
continue ;
2023-06-20 16:40:51 -04:00
//bSuccess = MergeIntoTemplateProject(PBXFilePath, RootProject, TemplateProject, Logger);
2023-05-10 16:53:20 -04:00
}
else
{
// write metadata now so we can add them to the FileCollection
ConditionalWriteMetadataFiles ( UnrealTargetPlatform . IOS ) ;
StringBuilder Content = new StringBuilder ( ) ;
Content . WriteLine ( 0 , "// !$*UTF8*$!" ) ;
Content . WriteLine ( 0 , "{" ) ;
Content . WriteLine ( 1 , "archiveVersion = 1;" ) ;
Content . WriteLine ( 1 , "classes = {" ) ;
Content . WriteLine ( 1 , "};" ) ;
Content . WriteLine ( 1 , "objectVersion = 46;" ) ;
Content . WriteLine ( 1 , "objects = {" ) ;
// write out the list of files and groups
FileCollection . Write ( Content ) ;
// now write out the project node and its recursive dependent nodes
2023-06-20 16:40:51 -04:00
XcodeProjectNode . WriteNodeAndReferences ( Content , RootProject , Logger ) ;
2023-05-10 16:53:20 -04:00
Content . WriteLine ( 1 , "};" ) ;
Content . WriteLine ( 1 , $"rootObject = {RootProject.Guid} /* Project object */;" ) ;
Content . WriteLine ( 0 , "}" ) ;
// finally write out the pbxproj file!
bSuccess = ProjectFileGenerator . WriteFileIfChanged ( PBXFilePath . FullName , Content . ToString ( ) , Logger , new UTF8Encoding ( ) ) & & bSuccess ;
}
bool bNeedScheme = ! bAddStubEditor & & XcodeUtils . ShouldIncludeProjectInWorkspace ( this , Logger ) ;
if ( bNeedScheme )
{
if ( bSuccess )
{
string ProjectName = ProjectFilePathForPlatform ( Platform ) . GetFileNameWithoutAnyExtensions ( ) ;
string? BuildTargetGuid = XcodeProjectNode . GetNodesOfType < XcodeBuildTarget > ( RootProject ) . FirstOrDefault ( ) ? . Guid ;
string? IndexTargetGuid = XcodeProjectNode . GetNodesOfType < XcodeIndexTarget > ( RootProject ) . FirstOrDefault ( ) ? . Guid ;
XcodeSchemeFile . WriteSchemeFile ( UnrealData , Platform , ProjectName , RootProject . RunTargets , BuildTargetGuid , IndexTargetGuid ) ;
}
}
else
{
XcodeSchemeFile . CleanSchemeFile ( UnrealData , Platform ) ;
}
}
return bSuccess ;
}
private UnrealTargetPlatform CurrentPlistPlatform ;
private void ConditionalWriteMetadataFiles ( UnrealTargetPlatform Platform )
{
CurrentPlistPlatform = Platform ;
// we now use templates or premade, no writing out here
foreach ( MetadataItem Data in UnrealData . Metadata ! . PlistFiles . Values )
{
if ( Data . XcodeProjectRelative ! = null )
{
FileCollection . AddFileReference ( XcodeProjectFileGenerator . MakeXcodeGuid ( ) , Data . XcodeProjectRelative , "explicitFileType" , "text.plist" , "\"<group>\"" , "Metadata" ) ;
}
}
foreach ( MetadataItem Data in UnrealData . Metadata . EntitlementsFiles . Values )
{
if ( Data . XcodeProjectRelative ! = null & & Data . Mode = = MetadataMode . UsePremade )
{
FileCollection . AddFileReference ( XcodeProjectFileGenerator . MakeXcodeGuid ( ) , Data . XcodeProjectRelative , "explicitFileType" , "text.plist" , "\"<group>\"" , "Metadata" ) ;
}
}
}
private string Plist ( string Command )
{
return XcodeUtils . Plist ( Command ) ;
}
2023-06-20 16:40:51 -04:00
bool MergeIntoTemplateProject ( FileReference PBXProjFilePath , XcodeProject RootProject , FileReference TemplateProject , ILogger Logger )
2023-05-10 16:53:20 -04:00
{
// activate a file for plist reading/writing here
XcodeUtils . SetActivePlistFile ( PBXFilePathForPlatform ( CurrentPlistPlatform ) . FullName ) ;
// copy existing template project to final location
if ( FileReference . Exists ( PBXProjFilePath ) )
{
FileReference . Delete ( PBXProjFilePath ) ;
}
DirectoryReference . CreateDirectory ( PBXProjFilePath . Directory ) ;
FileReference . Copy ( TemplateProject , PBXProjFilePath ) ;
// write the nodes we need to add (Build/Index targets)
XcodeRunTarget RunTarget = XcodeProjectNode . GetNodesOfType < XcodeRunTarget > ( RootProject ) . First ( ) ;
XcodeBuildTarget BuildTarget = XcodeProjectNode . GetNodesOfType < XcodeBuildTarget > ( RunTarget ) . First ( ) ;
XcodeIndexTarget IndexTarget = XcodeProjectNode . GetNodesOfType < XcodeIndexTarget > ( RootProject ) . First ( ) ;
XcodeDependency BuildDependency = XcodeProjectNode . GetNodesOfType < XcodeDependency > ( RunTarget ) . First ( ) ;
// the runtarget and project need to write out so all of their xcconfigs get written as well,
// so write everything to a temp string that is tossed, but all xcconfigs will be done at least
StringBuilder Temp = new StringBuilder ( ) ;
2023-06-20 16:40:51 -04:00
XcodeProjectNode . WriteNodeAndReferences ( Temp , RootProject ! , Logger ) ;
2023-05-10 16:53:20 -04:00
StringBuilder Content = new StringBuilder ( ) ;
Content . WriteLine ( 0 , "{" ) ;
FileCollection . Write ( Content ) ;
2023-06-20 16:40:51 -04:00
XcodeProjectNode . WriteNodeAndReferences ( Content , BuildTarget , Logger ) ;
XcodeProjectNode . WriteNodeAndReferences ( Content , IndexTarget , Logger ) ;
XcodeProjectNode . WriteNodeAndReferences ( Content , BuildDependency , Logger ) ;
2023-05-10 16:53:20 -04:00
Content . WriteLine ( 0 , "}" ) ;
// write to disk
FileReference ImportFile = FileReference . Combine ( PBXProjFilePath . Directory , "import.plist" ) ;
File . WriteAllText ( ImportFile . FullName , Content . ToString ( ) ) ;
// cache some standard guids from the template project
string ProjectGuid = Plist ( $"Print :rootObject" ) ;
string TemplateMainGroupGuid = Plist ( $"Print :objects:{ProjectGuid}:mainGroup" ) ;
// fixup paths that were relative to original project to be relative to merged project
2023-05-30 18:38:07 -04:00
// List<string> ObjectGuids = PlistObjects();
2023-05-10 16:53:20 -04:00
IEnumerable < string > MainGroupChildrenGuids = XcodeUtils . PlistArray ( $":objects:{TemplateMainGroupGuid}:children" ) ;
string RelativeFromMergedToTemplate = TemplateProject . Directory . ParentDirectory ! . MakeRelativeTo ( PBXProjFilePath . Directory . ParentDirectory ! ) ;
// look for groups with a 'path' element that is in the main group, so that it and everything will get redirected to new location
string? FixedPath ;
foreach ( string ChildGuid in MainGroupChildrenGuids )
{
string IsA = Plist ( $"Print :objects:{ChildGuid}:isa" ) ;
// if a Group has a path
if ( IsA = = "PBXGroup" )
{
if ( ( FixedPath = XcodeUtils . PlistFixPath ( $":objects:{ChildGuid}:path" , RelativeFromMergedToTemplate ) ) ! = null )
{
// if there wasn't a name before, it will now have a nasty path as the name, so add it now
XcodeUtils . PlistSetAdd ( $":objects:{ChildGuid}:name" , Path . GetFileName ( FixedPath ) ) ;
}
}
}
// and import it into the template
Plist ( $"Merge \" { ImportFile . FullName } \ " :objects" ) ;
// get all the targets in the template that are application types
IEnumerable < string > AppTargetGuids = XcodeUtils . PlistArray ( $":objects:{ProjectGuid}:targets" )
. Where ( TargetGuid = > ( Plist ( $"Print :objects:{TargetGuid}:productType" ) = = "com.apple.product-type.application" ) ) ;
// add a dependency on the build target from the app target(s)
foreach ( string AppTargetGuid in AppTargetGuids )
{
Plist ( $"Add :objects:{AppTargetGuid}:dependencies:0 string {BuildDependency.Guid}" ) ;
}
// the BuildDependency object was in the "container" of the generated project, not the merged one, so fix it up now
Plist ( $"Set :objects:{BuildDependency.ProxyGuid}:containerPortal {ProjectGuid}" ) ;
// now add all the non-run targets from the generated
foreach ( XcodeTarget Target in XcodeProjectNode . GetNodesOfType < XcodeTarget > ( RootProject ! ) . Where ( x = > x . GetType ( ) ! = typeof ( XcodeRunTarget ) ) )
{
Plist ( $"Add :objects:{ProjectGuid}:targets:0 string {Target.Guid}" ) ;
}
// hook up Xcconfig files to the project and the project configs
// @todo how to manage with conflicts already present...
//PlistSetAdd($":objects:{ProjectGuid}:baseConfigurationReference", RootProject.Xcconfig!.Guid, "string");
// re-get the list of targets now that we merged in the other file
IEnumerable < string > AllTargetGuids = XcodeUtils . PlistArray ( $":objects:{ProjectGuid}:targets" ) ;
List < string > NodesToFix = new ( ) { ProjectGuid } ;
NodesToFix . AddRange ( AllTargetGuids ) ;
bool bIsProject = true ;
foreach ( string NodeGuid in NodesToFix )
{
bool bIsAppTarget = AppTargetGuids . Contains ( NodeGuid ) ;
// get the config list, and from there we can get the configs
string ProjectBuildConfigListGuid = Plist ( $"Print :objects:{NodeGuid}:buildConfigurationList" ) ;
IEnumerable < string > ConfigGuids = XcodeUtils . PlistArray ( $":objects:{ProjectBuildConfigListGuid}:buildConfigurations" ) ;
foreach ( string ConfigGuid in ConfigGuids )
{
// find the matching unreal generated project build config to hook up to
// for now we assume Release is Development [Editor], but we should make sure the template project has good configs
// we have to rename the template config from Release because it won't find the matching config in the build target
string ConfigName = Plist ( $"Print :objects:{ConfigGuid}:name" ) ;
if ( ConfigName = = "Release" )
{
ConfigName = "Development" ;
if ( UnrealData . bMakeProjectPerTarget & & UnrealData . TargetRules . Type = = TargetType . Editor )
{
ConfigName = "Development Editor" ;
}
Plist ( $"Set :objects:{ConfigGuid}:name \" { ConfigName } \ "" ) ;
}
// if there's a plist path, then it will need to be fixed up
XcodeUtils . PlistFixPath ( $":objects:{ConfigGuid}:buildSettings:INFOPLIST_FILE" , RelativeFromMergedToTemplate ) ;
if ( bIsProject )
{
//Console.WriteLine("Looking for " + ConfigName);
XcodeBuildConfig Config = RootProject ! . ProjectBuildConfigs . BuildConfigs . First ( x = > x . Info . DisplayName = = ConfigName ) ;
XcodeUtils . PlistSetAdd ( $":objects:{ConfigGuid}:baseConfigurationReference" , Config . Xcconfig ! . Guid , "string" ) ;
}
// the Build target used some ini settings to compile, and Run target must match, so we override a few settings, at
// whatever level they were already specified at (Projet and/or Target)
XcodeUtils . PlistSetUpdate ( $":objects:{ConfigGuid}:buildSettings:MACOSX_DEPLOYMENT_TARGET" , MacToolChain . Settings . MacOSVersion ) ;
if ( UnrealData . IOSProjectSettings ! = null )
{
XcodeUtils . PlistSetUpdate ( $":objects:{ConfigGuid}:buildSettings:IPHONEOS_DEPLOYMENT_TARGET" , UnrealData . IOSProjectSettings . RuntimeVersion ) ;
}
if ( UnrealData . TVOSProjectSettings ! = null )
{
XcodeUtils . PlistSetUpdate ( $":objects:{ConfigGuid}:buildSettings:TVOS_DEPLOYMENT_TARGET" , UnrealData . TVOSProjectSettings . RuntimeVersion ) ;
}
}
bIsProject = false ;
}
// now we need to merge the main groups together
string GeneratedMainGroupGuid = FileCollection . MainGroupGuid ;
int Index = 0 ;
while ( true )
{
// we copy to a high index to put the copied entries at the end in the same order
string Output = Plist ( $"Copy :objects:{GeneratedMainGroupGuid}:children:{Index} :objects:{TemplateMainGroupGuid}:children:100000000" ) ;
// loop until error
2023-05-31 13:37:21 -04:00
if ( ! String . IsNullOrEmpty ( Output ) )
2023-05-10 16:53:20 -04:00
{
break ;
}
Index + + ;
}
// and remove the one we copied from
Plist ( $"Delete :objects:{GeneratedMainGroupGuid}" ) ;
return true ;
}
}
}