2023-03-08 12:43:35 -05:00
// Copyright Epic Games, Inc. All Rights Reserved.
2022-05-31 14:55:54 -04:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Text ;
using System.Text.RegularExpressions ;
using System.IO ;
using AutomationTool ;
using UnrealBuildTool ;
2022-09-20 12:35:25 -04:00
using UnrealBuildBase ;
2022-05-31 14:55:54 -04:00
using EpicGames.Core ;
2023-03-08 12:43:35 -05:00
using Microsoft.Extensions.Logging ;
2023-04-03 12:39:27 -04:00
using System.Security.Cryptography ;
2022-05-31 14:55:54 -04:00
2022-10-20 12:22:07 -04:00
public class MacPlatform : ApplePlatform
2022-05-31 14:55:54 -04:00
{
/// <summary>
/// Default architecture to build projects for. Defaults to Intel
/// </summary>
UnrealArch/UnrealArchitectures changes
- Creates the UnrealArchitectures class, which wraps a list of UnrealArch objects
- UnrealArch is a single architecture, expandable enum-like struct
- There is no more concept of "no/default architecture", there is always a valid active architecture when building
- Most uses of "string Architecture" are replaced with one of the two above, depending if multiple architectures are supported or not
- UnrealArch has some platform-extensions for platform-specific naming (like Linux adds in LinuxName that turns, for instance, Arm64 -> aarch64-unknown-linux-gnueabi, which is used in folder names, etc)
- UnrealArch has bIsX64 which can be used determine intel instruction set (as opposed to arm)
- TargetRules class has an "Architecture" accessor that will return a single architecture if the active architectures is a single architecture, or throw an exception if multiple. This is useful in a majority of the cases where a paltform can only have a single architecture active in TargetRules (microsoft platforms, for instance, will create separate targets when compiling multiple architectures at once)
- Added UnrealArchitectureConfig class, which contains all the architecture information for a platform (what architectures are supported, what ones are currently active for given project, etc)
#preflight 63c81fb5b065224750a1759e
#rb mike.fricker,roman.dzieciol,joe.kirchoff,dmytro.vovk,brandon.schaefer [various parts]
#p4v-preflight-copy 23562471
[CL 23829977 by josh adams in ue5-main branch]
2023-01-24 09:30:28 -05:00
protected UnrealArchitectures ProjectTargetArchitectures = new ( UnrealArch . X64 ) ;
2022-05-31 14:55:54 -04:00
public MacPlatform ( )
: base ( UnrealTargetPlatform . Mac )
{
}
public override void PlatformSetupParams ( ref ProjectParams ProjParams )
{
base . PlatformSetupParams ( ref ProjParams ) ;
string ConfigTargetArchicture = "" ;
ConfigHierarchy PlatformEngineConfig = null ;
if ( ProjParams . EngineConfigs . TryGetValue ( PlatformType , out PlatformEngineConfig ) )
{
PlatformEngineConfig . GetString ( "/Script/MacTargetPlatform.MacTargetSettings" , "TargetArchitecture" , out ConfigTargetArchicture ) ;
if ( ConfigTargetArchicture . ToLower ( ) . Contains ( "intel" ) )
{
UnrealArch/UnrealArchitectures changes
- Creates the UnrealArchitectures class, which wraps a list of UnrealArch objects
- UnrealArch is a single architecture, expandable enum-like struct
- There is no more concept of "no/default architecture", there is always a valid active architecture when building
- Most uses of "string Architecture" are replaced with one of the two above, depending if multiple architectures are supported or not
- UnrealArch has some platform-extensions for platform-specific naming (like Linux adds in LinuxName that turns, for instance, Arm64 -> aarch64-unknown-linux-gnueabi, which is used in folder names, etc)
- UnrealArch has bIsX64 which can be used determine intel instruction set (as opposed to arm)
- TargetRules class has an "Architecture" accessor that will return a single architecture if the active architectures is a single architecture, or throw an exception if multiple. This is useful in a majority of the cases where a paltform can only have a single architecture active in TargetRules (microsoft platforms, for instance, will create separate targets when compiling multiple architectures at once)
- Added UnrealArchitectureConfig class, which contains all the architecture information for a platform (what architectures are supported, what ones are currently active for given project, etc)
#preflight 63c81fb5b065224750a1759e
#rb mike.fricker,roman.dzieciol,joe.kirchoff,dmytro.vovk,brandon.schaefer [various parts]
#p4v-preflight-copy 23562471
[CL 23829977 by josh adams in ue5-main branch]
2023-01-24 09:30:28 -05:00
ProjectTargetArchitectures = new UnrealArchitectures ( UnrealArch . X64 ) ;
2022-05-31 14:55:54 -04:00
}
else if ( ConfigTargetArchicture . ToLower ( ) . Contains ( "apple" ) )
{
UnrealArch/UnrealArchitectures changes
- Creates the UnrealArchitectures class, which wraps a list of UnrealArch objects
- UnrealArch is a single architecture, expandable enum-like struct
- There is no more concept of "no/default architecture", there is always a valid active architecture when building
- Most uses of "string Architecture" are replaced with one of the two above, depending if multiple architectures are supported or not
- UnrealArch has some platform-extensions for platform-specific naming (like Linux adds in LinuxName that turns, for instance, Arm64 -> aarch64-unknown-linux-gnueabi, which is used in folder names, etc)
- UnrealArch has bIsX64 which can be used determine intel instruction set (as opposed to arm)
- TargetRules class has an "Architecture" accessor that will return a single architecture if the active architectures is a single architecture, or throw an exception if multiple. This is useful in a majority of the cases where a paltform can only have a single architecture active in TargetRules (microsoft platforms, for instance, will create separate targets when compiling multiple architectures at once)
- Added UnrealArchitectureConfig class, which contains all the architecture information for a platform (what architectures are supported, what ones are currently active for given project, etc)
#preflight 63c81fb5b065224750a1759e
#rb mike.fricker,roman.dzieciol,joe.kirchoff,dmytro.vovk,brandon.schaefer [various parts]
#p4v-preflight-copy 23562471
[CL 23829977 by josh adams in ue5-main branch]
2023-01-24 09:30:28 -05:00
ProjectTargetArchitectures = new UnrealArchitectures ( UnrealArch . Arm64 ) ;
2022-05-31 14:55:54 -04:00
}
else if ( ConfigTargetArchicture . ToLower ( ) . Contains ( "universal" ) )
{
UnrealArch/UnrealArchitectures changes
- Creates the UnrealArchitectures class, which wraps a list of UnrealArch objects
- UnrealArch is a single architecture, expandable enum-like struct
- There is no more concept of "no/default architecture", there is always a valid active architecture when building
- Most uses of "string Architecture" are replaced with one of the two above, depending if multiple architectures are supported or not
- UnrealArch has some platform-extensions for platform-specific naming (like Linux adds in LinuxName that turns, for instance, Arm64 -> aarch64-unknown-linux-gnueabi, which is used in folder names, etc)
- UnrealArch has bIsX64 which can be used determine intel instruction set (as opposed to arm)
- TargetRules class has an "Architecture" accessor that will return a single architecture if the active architectures is a single architecture, or throw an exception if multiple. This is useful in a majority of the cases where a paltform can only have a single architecture active in TargetRules (microsoft platforms, for instance, will create separate targets when compiling multiple architectures at once)
- Added UnrealArchitectureConfig class, which contains all the architecture information for a platform (what architectures are supported, what ones are currently active for given project, etc)
#preflight 63c81fb5b065224750a1759e
#rb mike.fricker,roman.dzieciol,joe.kirchoff,dmytro.vovk,brandon.schaefer [various parts]
#p4v-preflight-copy 23562471
[CL 23829977 by josh adams in ue5-main branch]
2023-01-24 09:30:28 -05:00
ProjectTargetArchitectures = new UnrealArchitectures ( new [ ] { UnrealArch . X64 , UnrealArch . Arm64 } ) ;
2022-05-31 14:55:54 -04:00
}
}
}
public override DeviceInfo [ ] GetDevices ( )
{
List < DeviceInfo > Devices = new List < DeviceInfo > ( ) ;
if ( HostPlatform . Current . HostEditorPlatform = = TargetPlatformType )
{
2023-11-14 09:40:24 -05:00
DeviceInfo LocalMachine = new DeviceInfo ( TargetPlatformType , Unreal . MachineName , Unreal . MachineName ,
2022-05-31 14:55:54 -04:00
Environment . OSVersion . Version . ToString ( ) , "Computer" , true , true ) ;
Devices . Add ( LocalMachine ) ;
}
return Devices . ToArray ( ) ;
}
public override string GetCookPlatform ( bool bDedicatedServer , bool bIsClientOnly )
{
const string NoEditorCookPlatform = "Mac" ;
const string ServerCookPlatform = "MacServer" ;
const string ClientCookPlatform = "MacClient" ;
if ( bDedicatedServer )
{
return ServerCookPlatform ;
}
else if ( bIsClientOnly )
{
return ClientCookPlatform ;
}
else
{
return NoEditorCookPlatform ;
}
}
public override string GetEditorCookPlatform ( )
{
return "MacEditor" ;
}
/// <summary>
/// Override PreBuildAgenda so we can control the architecture that targets are built for based on
/// project settings and the current user environment
/// </summary>
/// <param name="Build"></param>
/// <param name="Agenda"></param>
/// <param name="Params"></param>
public override void PreBuildAgenda ( UnrealBuild Build , UnrealBuild . BuildAgenda Agenda , ProjectParams Params )
{
base . PreBuildAgenda ( Build , Agenda , Params ) ;
2022-12-07 09:42:34 -05:00
// Go through the agenda for all targets and set the architecture if needed
2022-05-31 14:55:54 -04:00
foreach ( UnrealBuild . BuildTarget Target in Agenda . Targets )
{
2022-12-07 09:42:34 -05:00
// if building for Distribution, and no arch is already specified, then get the distro architectures and use that for this build
2023-10-10 10:52:53 -04:00
// editors aren't usually distributed, so if we are doing distribution, it's probably not for the editor target, and we don't want to make universal
// editors just to make a distribution client
if ( Params . Distribution & & ! Target . TargetName . Contains ( "Editor" ) & & ! Target . UBTArgs . ToLower ( ) . Contains ( "-architecture=" ) )
2022-05-31 14:55:54 -04:00
{
UnrealArch/UnrealArchitectures changes
- Creates the UnrealArchitectures class, which wraps a list of UnrealArch objects
- UnrealArch is a single architecture, expandable enum-like struct
- There is no more concept of "no/default architecture", there is always a valid active architecture when building
- Most uses of "string Architecture" are replaced with one of the two above, depending if multiple architectures are supported or not
- UnrealArch has some platform-extensions for platform-specific naming (like Linux adds in LinuxName that turns, for instance, Arm64 -> aarch64-unknown-linux-gnueabi, which is used in folder names, etc)
- UnrealArch has bIsX64 which can be used determine intel instruction set (as opposed to arm)
- TargetRules class has an "Architecture" accessor that will return a single architecture if the active architectures is a single architecture, or throw an exception if multiple. This is useful in a majority of the cases where a paltform can only have a single architecture active in TargetRules (microsoft platforms, for instance, will create separate targets when compiling multiple architectures at once)
- Added UnrealArchitectureConfig class, which contains all the architecture information for a platform (what architectures are supported, what ones are currently active for given project, etc)
#preflight 63c81fb5b065224750a1759e
#rb mike.fricker,roman.dzieciol,joe.kirchoff,dmytro.vovk,brandon.schaefer [various parts]
#p4v-preflight-copy 23562471
[CL 23829977 by josh adams in ue5-main branch]
2023-01-24 09:30:28 -05:00
UnrealArchitectures DistroArches = UnrealArchitectureConfig . ForPlatform ( UnrealTargetPlatform . Mac ) . DistributionArchitectures ( Params . RawProjectPath , Target . TargetName ) ;
Target . UBTArgs + = " -architecture=" + DistroArches . ToString ( ) ;
2022-09-20 12:35:25 -04:00
}
2022-05-31 14:55:54 -04:00
}
}
private void StageAppBundle ( DeploymentContext SC , DirectoryReference InPath , StagedDirectoryReference NewName )
{
// Files with DebugFileExtensions should always be DebugNonUFS
List < string > DebugExtensions = GetDebugFileExtensions ( ) ;
if ( DirectoryExists ( InPath . FullName ) )
{
foreach ( FileReference InputFile in DirectoryReference . EnumerateFiles ( InPath , "*" , SearchOption . AllDirectories ) )
{
StagedFileReference OutputFile = StagedFileReference . Combine ( NewName , InputFile . MakeRelativeTo ( InPath ) ) ;
StagedFileType FileType = DebugExtensions . Any ( x = > InputFile . HasExtension ( x ) ) ? StagedFileType . DebugNonUFS : StagedFileType . NonUFS ;
SC . StageFile ( FileType , InputFile , OutputFile ) ;
}
}
}
public override void GetFilesToDeployOrStage ( ProjectParams Params , DeploymentContext SC )
{
// Stage all the build products
foreach ( StageTarget Target in SC . StageTargets )
{
SC . StageBuildProductsFromReceipt ( Target . Receipt , Target . RequireFilesExist , Params . bTreatNonShippingBinariesAsDebugFiles ) ;
}
if ( SC . bStageCrashReporter )
{
StagedDirectoryReference CrashReportClientPath = StagedDirectoryReference . Combine ( "Engine/Binaries" , SC . PlatformDir , "CrashReportClient.app" ) ;
StageAppBundle ( SC , DirectoryReference . Combine ( SC . LocalRoot , "Engine/Binaries" , SC . PlatformDir , "CrashReportClient.app" ) , CrashReportClientPath ) ;
}
// Find the app bundle path
List < FileReference > Exes = GetExecutableNames ( SC ) ;
foreach ( var Exe in Exes )
{
StagedDirectoryReference AppBundlePath = null ;
if ( Exe . IsUnderDirectory ( DirectoryReference . Combine ( SC . RuntimeProjectRootDir , "Binaries" , SC . PlatformDir ) ) )
{
AppBundlePath = StagedDirectoryReference . Combine ( SC . ShortProjectName , "Binaries" , SC . PlatformDir , Path . GetFileNameWithoutExtension ( Exe . FullName ) + ".app" ) ;
}
else if ( Exe . IsUnderDirectory ( DirectoryReference . Combine ( SC . RuntimeRootDir , "Engine/Binaries" , SC . PlatformDir ) ) )
{
AppBundlePath = StagedDirectoryReference . Combine ( "Engine/Binaries" , SC . PlatformDir , Path . GetFileNameWithoutExtension ( Exe . FullName ) + ".app" ) ;
}
// Copy the custom icon and Steam dylib, if needed
if ( AppBundlePath ! = null )
{
FileReference AppIconsFile = FileReference . Combine ( SC . ProjectRoot , "Build" , "Mac" , "Application.icns" ) ;
if ( FileReference . Exists ( AppIconsFile ) )
{
SC . StageFile ( StagedFileType . NonUFS , AppIconsFile , StagedFileReference . Combine ( AppBundlePath , "Contents" , "Resources" , "Application.icns" ) ) ;
}
}
}
// Copy the splash screen, Mac specific
FileReference SplashImage = FileReference . Combine ( SC . ProjectRoot , "Content" , "Splash" , "Splash.bmp" ) ;
if ( FileReference . Exists ( SplashImage ) )
{
SC . StageFile ( StagedFileType . NonUFS , SplashImage ) ;
}
2023-03-22 07:24:29 -04:00
// Stage the bootstrap executable (modern doesn't need it with full .apps)
2023-05-26 12:51:21 -04:00
if ( ! Params . NoBootstrapExe & & ! AppleExports . UseModernXcode ( Params . RawProjectPath ) )
2022-05-31 14:55:54 -04:00
{
foreach ( StageTarget Target in SC . StageTargets )
{
BuildProduct Executable = Target . Receipt . BuildProducts . FirstOrDefault ( x = > x . Type = = BuildProductType . Executable ) ;
if ( Executable ! = null )
{
// only create bootstraps for executables
List < StagedFileReference > StagedFiles = SC . FilesToStage . NonUFSFiles . Where ( x = > x . Value = = Executable . Path ) . Select ( x = > x . Key ) . ToList ( ) ;
if ( StagedFiles . Count > 0 & & Executable . Path . FullName . Replace ( "\\" , "/" ) . Contains ( "/" + TargetPlatformType . ToString ( ) + "/" ) )
{
string BootstrapArguments = "" ;
if ( ! ShouldStageCommandLine ( Params , SC ) )
{
if ( ! SC . IsCodeBasedProject )
{
BootstrapArguments = String . Format ( "../../../{0}/{0}.uproject" , SC . ShortProjectName ) ;
}
else
{
BootstrapArguments = SC . ShortProjectName ;
}
}
string BootstrapExeName ;
if ( SC . StageTargetConfigurations . Count > 1 )
{
BootstrapExeName = Path . GetFileName ( Executable . Path . FullName ) + ".app" ;
}
else if ( Params . IsCodeBasedProject )
{
// We want Mac-Shipping etc in the bundle name
BootstrapExeName = Path . GetFileName ( Executable . Path . FullName ) + ".app" ;
}
else
{
BootstrapExeName = SC . ShortProjectName + ".app" ;
}
string AppSuffix = ".app" + Path . DirectorySeparatorChar ;
string AppPath = Executable . Path . FullName . Substring ( 0 , Executable . Path . FullName . LastIndexOf ( AppSuffix ) + AppSuffix . Length ) ;
foreach ( var DestPath in StagedFiles )
{
string AppRelativePath = DestPath . Name . Substring ( 0 , DestPath . Name . LastIndexOf ( AppSuffix ) + AppSuffix . Length ) ;
StageBootstrapExecutable ( SC , BootstrapExeName , AppPath , AppRelativePath , BootstrapArguments ) ;
}
}
}
}
}
// Copy the ShaderCache files, if they exist
FileReference DrawCacheFile = FileReference . Combine ( SC . ProjectRoot , "Content" , "DrawCache.ushadercache" ) ;
if ( FileReference . Exists ( DrawCacheFile ) )
{
SC . StageFile ( StagedFileType . UFS , DrawCacheFile ) ;
}
FileReference ByteCodeCacheFile = FileReference . Combine ( SC . ProjectRoot , "Content" , "ByteCodeCache.ushadercode" ) ;
if ( FileReference . Exists ( ByteCodeCacheFile ) )
{
SC . StageFile ( StagedFileType . UFS , ByteCodeCacheFile ) ;
}
{
// Stage any *.metallib files as NonUFS.
// Get the final output directory for cooked data
DirectoryReference CookOutputDir ;
if ( ! String . IsNullOrEmpty ( Params . CookOutputDir ) )
{
CookOutputDir = DirectoryReference . Combine ( new DirectoryReference ( Params . CookOutputDir ) , SC . CookPlatform ) ;
}
else if ( Params . CookInEditor )
{
CookOutputDir = DirectoryReference . Combine ( SC . ProjectRoot , "Saved" , "EditorCooked" , SC . CookPlatform ) ;
}
else
{
CookOutputDir = DirectoryReference . Combine ( SC . ProjectRoot , "Saved" , "Cooked" , SC . CookPlatform ) ;
}
if ( DirectoryReference . Exists ( CookOutputDir ) )
{
List < FileReference > CookedFiles = DirectoryReference . EnumerateFiles ( CookOutputDir , "*.metallib" , SearchOption . AllDirectories ) . ToList ( ) ;
foreach ( FileReference CookedFile in CookedFiles )
{
SC . StageFile ( StagedFileType . NonUFS , CookedFile , new StagedFileReference ( CookedFile . MakeRelativeTo ( CookOutputDir ) ) ) ;
}
}
}
}
string GetValueFromInfoPlist ( string InfoPlist , string Key , string DefaultValue = "" )
{
string Value = DefaultValue ;
string KeyString = "<key>" + Key + "</key>" ;
int KeyIndex = InfoPlist . IndexOf ( KeyString ) ;
if ( KeyIndex > 0 )
{
int ValueStartIndex = InfoPlist . IndexOf ( "<string>" , KeyIndex + KeyString . Length ) + "<string>" . Length ;
int ValueEndIndex = InfoPlist . IndexOf ( "</string>" , ValueStartIndex ) ;
if ( ValueStartIndex > 0 & & ValueEndIndex > ValueStartIndex )
{
Value = InfoPlist . Substring ( ValueStartIndex , ValueEndIndex - ValueStartIndex ) ;
}
}
return Value ;
}
void StageBootstrapExecutable ( DeploymentContext SC , string ExeName , string TargetFile , string StagedRelativeTargetPath , string StagedArguments )
{
DirectoryReference InputApp = DirectoryReference . Combine ( SC . LocalRoot , "Engine" , "Binaries" , SC . PlatformDir , "BootstrapPackagedGame.app" ) ;
if ( InternalUtils . SafeDirectoryExists ( InputApp . FullName ) )
{
// Create the new bootstrap program
DirectoryReference IntermediateDir = DirectoryReference . Combine ( SC . ProjectRoot , "Intermediate" , "Staging" ) ;
InternalUtils . SafeCreateDirectory ( IntermediateDir . FullName ) ;
DirectoryReference IntermediateApp = DirectoryReference . Combine ( IntermediateDir , ExeName ) ;
if ( DirectoryReference . Exists ( IntermediateApp ) )
{
DirectoryReference . Delete ( IntermediateApp , true ) ;
}
CloneDirectory ( InputApp . FullName , IntermediateApp . FullName ) ;
// Rename the executable
string GameName = Path . GetFileNameWithoutExtension ( ExeName ) ;
FileReference . Move ( FileReference . Combine ( IntermediateApp , "Contents" , "MacOS" , "BootstrapPackagedGame" ) , FileReference . Combine ( IntermediateApp , "Contents" , "MacOS" , GameName ) ) ;
// Copy the icon
string SrcInfoPlistPath = CombinePaths ( TargetFile , "Contents" , "Info.plist" ) ;
string SrcInfoPlist = File . ReadAllText ( SrcInfoPlistPath ) ;
string IconName = GetValueFromInfoPlist ( SrcInfoPlist , "CFBundleIconFile" ) ;
if ( ! string . IsNullOrEmpty ( IconName ) )
{
string IconPath = CombinePaths ( TargetFile , "Contents" , "Resources" , IconName + ".icns" ) ;
InternalUtils . SafeCreateDirectory ( CombinePaths ( IntermediateApp . FullName , "Contents" , "Resources" ) ) ;
File . Copy ( IconPath , CombinePaths ( IntermediateApp . FullName , "Contents" , "Resources" , IconName + ".icns" ) ) ;
}
// Update Info.plist contents
string DestInfoPlistPath = CombinePaths ( IntermediateApp . FullName , "Contents" , "Info.plist" ) ;
string DestInfoPlist = File . ReadAllText ( DestInfoPlistPath ) ;
string AppIdentifier = GetValueFromInfoPlist ( SrcInfoPlist , "CFBundleIdentifier" ) ;
if ( AppIdentifier = = "com.epicgames.UnrealGame" )
{
AppIdentifier = "" ;
}
string Copyright = GetValueFromInfoPlist ( SrcInfoPlist , "NSHumanReadableCopyright" ) ;
string BundleVersion = GetValueFromInfoPlist ( SrcInfoPlist , "CFBundleVersion" , "1" ) ;
string ShortVersion = GetValueFromInfoPlist ( SrcInfoPlist , "CFBundleShortVersionString" , "1.0" ) ;
DestInfoPlist = DestInfoPlist . Replace ( "com.epicgames.BootstrapPackagedGame" , string . IsNullOrEmpty ( AppIdentifier ) ? "com.epicgames." + GameName + "_bootstrap" : AppIdentifier + "_bootstrap" ) ;
DestInfoPlist = DestInfoPlist . Replace ( "BootstrapPackagedGame" , GameName ) ;
DestInfoPlist = DestInfoPlist . Replace ( "__UE4_ICON_FILE__" , IconName ) ;
DestInfoPlist = DestInfoPlist . Replace ( "__UE4_APP_TO_LAUNCH__" , StagedRelativeTargetPath ) ;
DestInfoPlist = DestInfoPlist . Replace ( "__UE4_COMMANDLINE__" , StagedArguments ) ;
DestInfoPlist = DestInfoPlist . Replace ( "__UE4_COPYRIGHT__" , Copyright ) ;
DestInfoPlist = DestInfoPlist . Replace ( "__UE4_BUNDLE_VERSION__" , BundleVersion ) ;
DestInfoPlist = DestInfoPlist . Replace ( "__UE4_SHORT_VERSION__" , ShortVersion ) ;
File . WriteAllText ( DestInfoPlistPath , DestInfoPlist ) ;
StageAppBundle ( SC , IntermediateApp , new StagedDirectoryReference ( ExeName ) ) ;
}
}
private void RemoveExtraRPaths ( ProjectParams Params , DeploymentContext SC )
{
// When we link the executable we add RPATH entries for all possible places where dylibs can be loaded from, so that the same executable can be used from Binaries/Mac
// as well as in a packaged, self-contained application. In recent versions of macOS, Gatekeeper doesn't allow RPATHs pointing to folders that don't exist,
// so we remove these based on the type of packaging (Params.CreateAppBundle).
List < FileReference > Exes = GetExecutableNames ( SC ) ;
foreach ( var ExePath in Exes )
{
IProcessResult CommandResult = Run ( "otool" , "-l \"" + ExePath + "\"" , null , ERunOptions . None ) ;
if ( CommandResult . ExitCode = = 0 )
{
StringReader Reader = new StringReader ( CommandResult . Output ) ;
Regex RPathPattern = new Regex ( @"^\s+path (?<rpath>.+)\s\(offset" ) ;
string ToRemovePattern = Params . CreateAppBundle ? "/../../../" : "@loader_path/../UE4/" ;
string OutputLine ;
while ( ( OutputLine = Reader . ReadLine ( ) ) ! = null )
{
if ( OutputLine . EndsWith ( "cmd LC_RPATH" ) )
{
OutputLine = Reader . ReadLine ( ) ;
OutputLine = Reader . ReadLine ( ) ;
Match RPathMatch = RPathPattern . Match ( OutputLine ) ;
if ( RPathMatch . Success )
{
string RPath = RPathMatch . Groups [ "rpath" ] . Value ;
if ( RPath . Contains ( ToRemovePattern ) )
{
Run ( "xcrun" , "install_name_tool -delete_rpath \"" + RPath + "\" \"" + ExePath + "\"" , null , ERunOptions . NoStdOutCapture ) ;
}
}
}
}
}
}
}
2023-04-03 12:39:27 -04:00
private void FixupFrameworks ( string TargetPath )
{
DirectoryReference TargetCEFDir = DirectoryReference . Combine ( new DirectoryReference ( TargetPath ) , "Engine/Binaries/ThirdParty/CEF3/Mac" ) ;
DirectoryReference X86Framework = DirectoryReference . Combine ( TargetCEFDir , "Chromium Embedded Framework x86.framework" ) ;
DirectoryReference X86Versions = DirectoryReference . Combine ( X86Framework , "Versions" ) ;
DirectoryReference Arm64Framework = DirectoryReference . Combine ( TargetCEFDir , "Chromium Embedded Framework arm64.framework" ) ;
DirectoryReference Arm64Versions = DirectoryReference . Combine ( Arm64Framework , "Versions" ) ;
DirectoryReference EngineCEFDir = DirectoryReference . Combine ( Unreal . EngineDirectory , "Binaries/ThirdParty/CEF3/Mac" ) ;
FileReference X86Zip = FileReference . Combine ( EngineCEFDir , "Chromium Embedded Framework x86.framework.zip" ) ;
FileReference Arm64Zip = FileReference . Combine ( EngineCEFDir , "Chromium Embedded Framework arm64.framework.zip" ) ;
// if the archive has a framework without Versions directory, it won't be allowed for App Store submission, so replace it with the zipped version
// that has the proper symlinks
if ( DirectoryReference . Exists ( X86Framework ) & & ! DirectoryReference . Exists ( X86Versions ) )
{
Logger . LogInformation ( $"Replacing {X86Framework} with {X86Zip}..." ) ;
DirectoryReference . Delete ( X86Framework , true ) ;
Utils . RunLocalProcessAndLogOutput ( "/usr/bin/unzip" , $"-q -o \" { X86Zip } \ " -d \"{TargetCEFDir}\" -x \"__MACOSX/*\" \"*.DS_Store\"" , Logger ) ;
}
if ( DirectoryReference . Exists ( Arm64Framework ) & & ! DirectoryReference . Exists ( Arm64Versions ) )
{
Logger . LogInformation ( $"Replacing {Arm64Framework} with {Arm64Zip}..." ) ;
DirectoryReference . Delete ( Arm64Framework , true ) ;
Utils . RunLocalProcessAndLogOutput ( "/usr/bin/unzip" , $"-q -o \" { Arm64Zip } \ " -d \"{TargetCEFDir}\" -x \"__MACOSX/*\" \"*.DS_Store\"" , Logger ) ;
}
}
2022-05-31 14:55:54 -04:00
public override void ProcessArchivedProject ( ProjectParams Params , DeploymentContext SC )
{
2023-04-27 14:22:37 -04:00
// nothing to do with modern
2023-05-26 12:51:21 -04:00
if ( AppleExports . UseModernXcode ( Params . RawProjectPath ) )
2023-04-27 14:22:37 -04:00
{
return ;
}
2022-05-31 14:55:54 -04:00
if ( Params . CreateAppBundle )
{
string ExeName = SC . StageExecutables [ 0 ] ;
2023-03-22 07:24:29 -04:00
string BundlePath = SC . IsCodeBasedProject ? CombinePaths ( SC . ArchiveDirectory . FullName , ExeName + ".app" ) : CombinePaths ( SC . ArchiveDirectory . FullName , SC . ShortProjectName + ".app" ) ;
2022-05-31 14:55:54 -04:00
if ( SC . bIsCombiningMultiplePlatforms )
{
// when combining multiple platforms, don't merge the content into the .app, use the one in the Binaries directory
BundlePath = CombinePaths ( SC . ArchiveDirectory . FullName , SC . ShortProjectName , "Binaries" , "Mac" , ExeName + ".app" ) ;
2022-07-01 18:52:43 -04:00
if ( ! DirectoryExists ( BundlePath ) )
2022-05-31 14:55:54 -04:00
{
// if the .app wasn't there, just skip out (we don't require executables when combining)
return ;
}
}
string TargetPath = CombinePaths ( BundlePath , "Contents" , "UE" ) ;
if ( ! SC . bIsCombiningMultiplePlatforms )
{
2022-07-01 18:52:43 -04:00
DeleteDirectory ( true , BundlePath ) ;
2022-05-31 14:55:54 -04:00
string SourceBundlePath = CombinePaths ( SC . ArchiveDirectory . FullName , SC . ShortProjectName , "Binaries" , "Mac" , ExeName + ".app" ) ;
2022-07-01 18:52:43 -04:00
if ( ! DirectoryExists ( SourceBundlePath ) )
2022-05-31 14:55:54 -04:00
{
SourceBundlePath = CombinePaths ( SC . ArchiveDirectory . FullName , "Engine" , "Binaries" , "Mac" , ExeName + ".app" ) ;
2022-07-01 18:52:43 -04:00
if ( ! DirectoryExists ( SourceBundlePath ) )
2022-05-31 14:55:54 -04:00
{
SourceBundlePath = CombinePaths ( SC . ArchiveDirectory . FullName , "Engine" , "Binaries" , "Mac" , "UE4.app" ) ;
}
}
2022-07-01 18:52:43 -04:00
RenameDirectory ( SourceBundlePath , BundlePath , true ) ;
2022-05-31 14:55:54 -04:00
2022-07-01 18:52:43 -04:00
DeleteDirectory ( true , TargetPath ) ;
2022-05-31 14:55:54 -04:00
string [ ] StagedFiles = Directory . GetFiles ( SC . ArchiveDirectory . FullName , "*" , SearchOption . TopDirectoryOnly ) ;
foreach ( string FilePath in StagedFiles )
{
string TargetFilePath = CombinePaths ( TargetPath , Path . GetFileName ( FilePath ) ) ;
2022-07-01 18:52:43 -04:00
CreateDirectory ( Path . GetDirectoryName ( TargetFilePath ) ) ;
RenameFile ( FilePath , TargetFilePath , true ) ;
2022-05-31 14:55:54 -04:00
}
string [ ] StagedDirectories = Directory . GetDirectories ( SC . ArchiveDirectory . FullName , "*" , SearchOption . TopDirectoryOnly ) ;
foreach ( string DirPath in StagedDirectories )
{
string DirName = Path . GetFileName ( DirPath ) ;
if ( ! DirName . EndsWith ( ".app" ) )
{
string TargetDirPath = CombinePaths ( TargetPath , DirName ) ;
2022-07-01 18:52:43 -04:00
CreateDirectory ( Path . GetDirectoryName ( TargetDirPath ) ) ;
RenameDirectory ( DirPath , TargetDirPath , true ) ;
2022-05-31 14:55:54 -04:00
}
}
2023-04-03 12:39:27 -04:00
FixupFrameworks ( TargetPath ) ;
2022-05-31 14:55:54 -04:00
}
// Update executable name, icon and entry in Info.plist
string UE4GamePath = CombinePaths ( BundlePath , "Contents" , "MacOS" , ExeName ) ;
2022-07-01 18:52:43 -04:00
if ( ! SC . IsCodeBasedProject & & ExeName ! = SC . ShortProjectName & & FileExists ( UE4GamePath ) )
2022-05-31 14:55:54 -04:00
{
string GameExePath = CombinePaths ( BundlePath , "Contents" , "MacOS" , SC . ShortProjectName ) ;
2022-07-01 18:52:43 -04:00
DeleteFile ( GameExePath ) ;
RenameFile ( UE4GamePath , GameExePath ) ;
2022-05-31 14:55:54 -04:00
string DefaultIconPath = CombinePaths ( BundlePath , "Contents" , "Resources" , "UnrealGame.icns" ) ;
string CustomIconSrcPath = CombinePaths ( BundlePath , "Contents" , "Resources" , "Application.icns" ) ;
string CustomIconDestPath = CombinePaths ( BundlePath , "Contents" , "Resources" , SC . ShortProjectName + ".icns" ) ;
2022-07-01 18:52:43 -04:00
if ( FileExists ( CustomIconSrcPath ) )
2022-05-31 14:55:54 -04:00
{
2022-07-01 18:52:43 -04:00
DeleteFile ( DefaultIconPath ) ;
DeleteFile ( CustomIconDestPath ) ;
RenameFile ( CustomIconSrcPath , CustomIconDestPath , true ) ;
2022-05-31 14:55:54 -04:00
}
2022-07-01 18:52:43 -04:00
else if ( FileExists ( DefaultIconPath ) )
2022-05-31 14:55:54 -04:00
{
2022-07-01 18:52:43 -04:00
DeleteFile ( CustomIconDestPath ) ;
RenameFile ( DefaultIconPath , CustomIconDestPath , true ) ;
2022-05-31 14:55:54 -04:00
}
string InfoPlistPath = CombinePaths ( BundlePath , "Contents" , "Info.plist" ) ;
string InfoPlistContents = File . ReadAllText ( InfoPlistPath ) ;
InfoPlistContents = InfoPlistContents . Replace ( ExeName , SC . ShortProjectName ) ;
InfoPlistContents = InfoPlistContents . Replace ( "<string>UnrealGame</string>" , "<string>" + SC . ShortProjectName + "</string>" ) ;
2022-07-01 18:52:43 -04:00
DeleteFile ( InfoPlistPath ) ;
WriteAllText ( InfoPlistPath , InfoPlistContents ) ;
2023-03-31 15:17:27 -04:00
// we now need to re-sign the .app because we modified the .plist
// we codesign with ad-hoc, and if the Developer ID Application cert exists, attempt to use it, ignore any errors
Utils . RunLocalProcessAndReturnStdOut ( "/usr/bin/codesign" , $"-f -s - \" { BundlePath } \ "" , null ) ;
Utils . RunLocalProcessAndReturnStdOut ( "/usr/bin/codesign" , $"-f -s \" Developer ID Application \ " \"{BundlePath}\"" , null ) ;
2022-05-31 14:55:54 -04:00
}
2023-04-03 12:39:27 -04:00
// we now need to re-sign the .app because we modified the .plist
// we codesign with ad-hoc, and if the Developer ID Application cert exists, attempt to use it, ignore any errors
Utils . RunLocalProcessAndReturnStdOut ( "/usr/bin/codesign" , $"-f -s - \" { BundlePath } \ "" , null ) ;
Utils . RunLocalProcessAndReturnStdOut ( "/usr/bin/codesign" , $"-f -s \" Developer ID Application \ " \"{BundlePath}\"" , null ) ;
2022-05-31 14:55:54 -04:00
if ( ! SC . bIsCombiningMultiplePlatforms )
{
// creating these directories when the content isn't moved into the application causes it
// to fail to load, and isn't needed
2022-07-01 18:52:43 -04:00
CreateDirectory ( CombinePaths ( TargetPath , "Engine" , "Binaries" , "Mac" ) ) ;
CreateDirectory ( CombinePaths ( TargetPath , SC . ShortProjectName , "Binaries" , "Mac" ) ) ;
2022-05-31 14:55:54 -04:00
}
// Find any dSYM files in the Manifest_DebugFiles_Mac file, move them to the archive directory, and remove them from the manifest.
2022-07-01 18:52:43 -04:00
string [ ] DebugManifests = FindFiles ( "Manifest_DebugFiles_Mac.txt" , true , SC . ArchiveDirectory . FullName ) ;
2022-05-31 14:55:54 -04:00
if ( DebugManifests . Count ( ) > 0 )
{
string DebugManifest = DebugManifests [ 0 ] ;
List < string > ManifestLines = new List < string > ( File . ReadAllLines ( DebugManifest ) ) ;
bool ModifyManifest = false ;
for ( int ManifestLineIndex = ManifestLines . Count - 1 ; ManifestLineIndex > = 0 ; ManifestLineIndex - - )
{
string ManifestLine = ManifestLines [ ManifestLineIndex ] ;
int TabIndex = ManifestLine . IndexOf ( '\t' ) ;
if ( TabIndex > 0 )
{
string FoundDebugFile = ManifestLine . Substring ( 0 , TabIndex ) ;
if ( FoundDebugFile . Contains ( ".dSYM" ) )
{
2022-07-01 18:52:43 -04:00
FoundDebugFile = CombinePaths ( TargetPath , FoundDebugFile ) ;
2022-05-31 14:55:54 -04:00
string MovedDebugFile = CombinePaths ( SC . ArchiveDirectory . FullName , Path . GetFileName ( FoundDebugFile ) ) ;
2022-07-01 18:52:43 -04:00
RenameFile ( FoundDebugFile , MovedDebugFile ) ;
2022-05-31 14:55:54 -04:00
2023-03-08 12:43:35 -05:00
Logger . LogInformation ( "Moving debug file: '{FoundDebugFile}')" , FoundDebugFile ) ;
2022-05-31 14:55:54 -04:00
ManifestLines . RemoveAt ( ManifestLineIndex ) ;
ModifyManifest = true ;
}
}
}
if ( ModifyManifest )
{
File . WriteAllLines ( DebugManifest , ManifestLines . ToArray ( ) ) ;
}
}
// If there is a dSYM matching the exe name rename it so it matches the project name
string ExeDSYMName = CombinePaths ( SC . ArchiveDirectory . FullName , ExeName + ".dSYM" ) ;
string ProjectDSYMName = CombinePaths ( SC . ArchiveDirectory . FullName , SC . ShortProjectName + ".dSYM" ) ;
if ( ExeDSYMName ! = ProjectDSYMName )
{
2022-07-01 18:52:43 -04:00
if ( FileExists ( ExeDSYMName ) )
2022-05-31 14:55:54 -04:00
{
2022-07-01 18:52:43 -04:00
RenameFile ( ExeDSYMName , ProjectDSYMName ) ;
2022-05-31 14:55:54 -04:00
}
}
Run ( "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister" , "-f " + BundlePath , null , ERunOptions . Default ) ;
}
}
public override IProcessResult RunClient ( ERunOptions ClientRunFlags , string ClientApp , string ClientCmdLine , ProjectParams Params )
{
2023-08-11 19:55:40 -04:00
if ( AppleExports . UseModernXcode ( Params . RawProjectPath ) )
{
// moden creates a full .app in the root of the Staged dir, but ClientApp as passed in is in Staged/Project/Binaries/Mac, which exists, but is not a full app
string ExeName = Path . GetFileNameWithoutExtension ( ClientApp ) ;
Int32 BaseDirLen = Params . BaseStageDirectory . Length ;
string StageSubDir = ClientApp . Substring ( BaseDirLen , ClientApp . IndexOf ( "/" , BaseDirLen + 1 ) - BaseDirLen ) ;
ClientApp = CombinePaths ( Params . BaseStageDirectory , StageSubDir , $"{ExeName}.app/Contents/MacOS/{ExeName}" ) ;
}
else if ( ! File . Exists ( ClientApp ) )
2022-05-31 14:55:54 -04:00
{
if ( Directory . Exists ( ClientApp + ".app" ) )
{
ClientApp + = ".app/Contents/MacOS/" + Path . GetFileName ( ClientApp ) ;
}
else
{
Int32 BaseDirLen = Params . BaseStageDirectory . Length ;
string StageSubDir = ClientApp . Substring ( BaseDirLen , ClientApp . IndexOf ( "/" , BaseDirLen + 1 ) - BaseDirLen ) ;
ClientApp = CombinePaths ( Params . BaseStageDirectory , StageSubDir , Params . ShortProjectName + ".app/Contents/MacOS/" + Params . ShortProjectName ) ;
}
}
PushDir ( Path . GetDirectoryName ( ClientApp ) ) ;
// Always start client process and don't wait for exit.
IProcessResult ClientProcess = Run ( ClientApp , ClientCmdLine , null , ClientRunFlags | ERunOptions . NoWaitForExit ) ;
PopDir ( ) ;
return ClientProcess ;
}
public override bool IsSupported { get { return true ; } }
public override List < string > GetDebugFileExtensions ( )
{
return new List < string > { ".dSYM" } ;
}
public override bool CanHostPlatform ( UnrealTargetPlatform Platform )
{
if ( Platform = = UnrealTargetPlatform . IOS | | Platform = = UnrealTargetPlatform . Mac | | Platform = = UnrealTargetPlatform . TVOS )
{
return true ;
}
return false ;
}
public override bool ShouldStageCommandLine ( ProjectParams Params , DeploymentContext SC )
{
2023-05-25 13:04:27 -04:00
// modern mode doesn't use the Bootstrap wrapper app, so we always insert the commandline file into the .app so double-clicking the .app works
2023-05-26 12:51:21 -04:00
return AppleExports . UseModernXcode ( Params . RawProjectPath ) ; // !String.IsNullOrEmpty(Params.StageCommandline) || !String.IsNullOrEmpty(Params.RunCommandline) || (!Params.IsCodeBasedProject && Params.NoBootstrapExe);
2022-05-31 14:55:54 -04:00
}
public override bool SignExecutables ( DeploymentContext SC , ProjectParams Params )
{
if ( UnrealBuildTool . BuildHostPlatform . Current . Platform = = UnrealTargetPlatform . Mac )
{
if ( Params . Archive )
{
// Remove extra RPATHs if we will be archiving the project
2023-03-08 12:43:35 -05:00
Logger . LogInformation ( "Removing extraneous rpath entries" ) ;
2022-05-31 14:55:54 -04:00
RemoveExtraRPaths ( Params , SC ) ;
}
2023-10-10 10:52:53 -04:00
// with modern, the .app we'd want to sign is in the root of the staging directory, so this doesn't do anything,
// and that one is already signed. note this is only done when Staging, so it doesn't affect codesigning the editor
if ( ! AppleExports . UseModernXcode ( Params . RawProjectPath ) )
2022-05-31 14:55:54 -04:00
{
2023-10-10 10:52:53 -04:00
// Sign everything we built
List < FileReference > FilesToSign = GetExecutableNames ( SC ) ;
Logger . LogInformation ( "{Text}" , "RuntimeProjectRootDir: " + SC . RuntimeProjectRootDir ) ;
foreach ( var Exe in FilesToSign )
2022-05-31 14:55:54 -04:00
{
2023-10-10 10:52:53 -04:00
Logger . LogInformation ( "{Text}" , "Signing: " + Exe ) ;
string AppBundlePath = "" ;
if ( Exe . IsUnderDirectory ( DirectoryReference . Combine ( SC . RuntimeProjectRootDir , "Binaries" , SC . PlatformDir ) ) )
{
Logger . LogInformation ( "Starts with Binaries" ) ;
AppBundlePath = CombinePaths ( SC . RuntimeProjectRootDir . FullName , "Binaries" , SC . PlatformDir , Path . GetFileNameWithoutExtension ( Exe . FullName ) + ".app" ) ;
}
else if ( Exe . IsUnderDirectory ( DirectoryReference . Combine ( SC . RuntimeRootDir , "Engine/Binaries" , SC . PlatformDir ) ) )
{
Logger . LogInformation ( "Starts with Engine/Binaries" ) ;
AppBundlePath = CombinePaths ( "Engine/Binaries" , SC . PlatformDir , Path . GetFileNameWithoutExtension ( Exe . FullName ) + ".app" ) ;
}
2022-05-31 14:55:54 -04:00
2023-10-10 10:52:53 -04:00
Logger . LogInformation ( "{Text}" , "Signing: " + AppBundlePath ) ;
CodeSign . SignMacFileOrFolder ( AppBundlePath ) ;
}
2022-05-31 14:55:54 -04:00
}
}
return true ;
}
public override void StripSymbols ( FileReference SourceFile , FileReference TargetFile )
{
MacExports . StripSymbols ( SourceFile , TargetFile , Log . Logger ) ;
}
}