Remote building is a very convenient way to package iOS in UE, allowing code compilation only on a Mac without COOKing resources, which greatly reduces the hardware requirements for Mac.
Moreover, Macs are quite expensive. In a traditional setup, if multiple independent iOS package build processes are needed, it usually requires purchasing several separate Mac machines for deployment, leading to high hardware costs.
Additionally, the iOS development environment often needs to be updated with iOS system iterations. Every year at WWDC, a new version of iOS is released, requiring support from the latest XCode, which in turn depends on the latest MacOS—a nested chain of dependencies. If the number of Mac build environments increases, updating the build environments of these machines also becomes a cumbersome repetitive task.
Remote building for iOS can effectively solve this issue. A single independent Mac can simultaneously support multiple iOS build processes, and if an update is needed, it only needs to be handled on one machine. Furthermore, the time it takes to compile code is relatively fixed, and incremental compilation can be performed. In theory, as long as not all machines execute a full compilation at the same time, the performance pressure on the Mac won’t be that great.
There are also articles on enabling remote building for iOS in the blog: UE Development Notes: Mac/iOS Edition, which can be referred to for basic configuration.
However, the default implementation of iOS remote building in UE also presents several issues in actual use, failing to achieve a fully automated process from deployment to building. This article aims to optimize the pain points of remote building in real project applications.
RemoteServer Specified Port Number
This is a problem that exists across all UE4 versions. The engine sets the default SSHD port number for RemoteServer to 22, which is indeed the case in most situations. However, for various reasons, the company’s internal network policy does not allow port 22 to be open. If SSH connections are needed, the SSHD must be changed to another port.
In this case, if the RemoteServerName is configured in IP:PORT
format in ProjectSettings, it will cause SSHKey lookup failures because UE by default directly concatenates the RemoteServerName to the path.
Therefore, engine support needs to be modified so that the IP:PORT
format configuration does not impact SSHKey lookup.
The changed file is the PostInitProperties
function in Runtime/IOS/IOSRuntimeSettings/Private/IOSRuntimeSettings.cpp
:
This issue has been resolved in UE5. My submitted PR (EpicGames/UnrealEngine/pull/7737) has been merged into UE5, but it still exists in UE4.
Automated Certificate and Provision Import
By default, whether packaging iOS on Windows or Mac, both remote building and direct exporting from Mac require importing the corresponding certificate and provision for the specific bundle ID in the engine. A popup to input the certificate password will also appear:
The entire process is purely manual and very cumbersome. For each machine, the configuration needs to be imported once. If the certificate expires or the provision is updated, all machines must undergo the manual process again, which is torturous.
To address this problem, I investigated the implementation of certificate and provision import in the engine to find a method for complete automation.
The engine contains a standalone program, iPhonePackager, which can import certificates and provisions, as well as install IPA on devices, and perform re-signing of IPA, among other functions. Although it provides a command line, it cannot specify the certificate password, resulting in a popup during execution, similar to the situation when imported in the editor.
So, I modified the code in iPhonePackager to support specifying the certificate password through the command line parameter -cerpassword
, avoiding the popup issue.
This allows for silent certificate import through the following command:
1 | # Import certificate |
However, by default, when compiling the engine, this program is not compiled, making it impossible to use the latest modifications. To avoid this situation, a precompiled iPhonePackager was placed into the project, executing the import through this precompiled version.
So far, silent underlying support for updating certificates and provisions has been implemented, but the timing for importing certificates still needs to be determined.
I chose the timing to write a logic segment in the project’s target.cs to detect the current compilation environment, the path of iPhonePackager, the certificate path, etc., constructing an import command and calling iPhonePackager for execution.
This way, imports can be executed during compilation, and the UE compilation process also conveniently allows passing compilation parameters to UBT, which can be detected in Target.cs and executed as needed.
Additionally, the automatic import feature is best disabled by default and can be enabled via a command line parameter for UBT, allowing automatic certificate import in the pipeline.
The code in Target.cs for parameter detection for certificate import is:
1 | bool bExportCer = IsContainInCmd("-importcer"); |
If the -importcer
parameter is passed to UBT during compilation:
1 | Build\BatchFiles\Build.bat -Target="GameEditor Win64 Development" -Project="G:\Client\Game.uproject" -WaitMutex -importcer |
The compilation log will display logs for importing the certificate and provision:
1 | 2023-08-15 20:13:09:055 : AssemblyDir: G:\UE4_27\Engine |
New Machine SSH Connection Fingerprint Verification
Since remote building for iOS is essentially initiated from Windows, commands are sent via SSH and rsync, transferring engine and code files, and then pulling back compilation results through rsync.
Key point: SSH Connection.
When an SSH Client connects to a new IP for the first time, it triggers a security mechanism that gives a prompt and waits for the user to input yes
:
When you connect to a new server, it asks whether you trust this new server and wish to add it to the known_hosts file. If you input yes
, SSH will add the server’s public key to your local machine’s known_hosts file. Thus, on subsequent connections to that server, SSH can compare the public key provided by the server with the public key in the known_hosts file to ensure you’re connecting to the same server, preventing man-in-the-middle attacks.
However, this issue creates a configuration nightmare when using iOS remote building:
- If the RemoteServerIP in the project is modified, every PC involved in the connection must manually execute an SSH connection and allow it.
- When a new PC machine is deployed and needs to use remote building, the same manual SSH connection must be performed.
If no configuration is made, the engine uses -BatchMode=yes
by default for SSH connections, and if user input is required, it will directly lead to build failures! I have previously analyzed this issue; see: Remote Building SSH Connection Error
The entire process is exceptionally cumbersome, contradicting our goal of achieving fully automated builds.
Similarly, to resolve this issue, modifications to the engine’s implementation are needed to allow it to ignore fingerprint verification when using SSH and rsync connections.
The code change is in the ToolChain/RemoteMac.cs
, modifying the SSH connection parameters:
Once changes are made, finally, peace returns to the world…
UPL Overwrites plist Modifications
When using remote building for iOS, stub files are generated on the Mac for synthesizing the final IPA on Windows.
The logic we write in UPL will be reflected in the stub generated on the Mac, but a Info.plist
file will also be generated in StagedBuilds/IOS
on Windows, which contains template information:
1 |
|
When using iPhonePackager to package the stub generated on the Mac with resources into the final IPA, the Info.plist in the stub is merged with StagedBuilds/IOS/Info.plist
.
The command executed by iPhonePackager is:
1 | D:\UE4.27\Engine\Binaries\DotNET\IOS\IPhonePackager.exe RepackageFromStage "D:\Client\GWorld.uproject" -config Development -schemename GWorld -schemeconfig "Development" -targetname GWorld -sign -codebased -stagedir "D:\Client\Saved\StagedBuilds\IOS" -project "D:\Client\GWorld.uproject" -provision "com.imzlp.gworld_Development.mobileprovision" -certificate "iPhone Developer: Created via API (XXXX)" |
In the Programs/IOS/iPhonePackager/CookTime.cs
‘s RepackageIPAFromStub
function, the merging process is implemented: iPhonePackager/CookTime.cs#L226-L258
During merging, the following LOG appears:
1 | ---------- |
The engine’s implementation of plist merging occurs in such a way that the plist from StagedBuilds is merged into the stub’s plist (note this sequence).
If elements in the StagedBuilds plist already exist in the stub’s plist, the elements in the stub’s plist will be removed first, and then elements from the StagedBuilds plist will be added to the resulting stub’s plist.
The implementation of MergePlistIn
is found here: iPhonePackager/Utilities.cs#L399
If we modify the values of existing elements in the StagedBuilds/IOS/Info.plist
via UPL, under this logic, our modified values will always be overridden by the default values, leading to inconsistencies between the final IPA and the values in the stub’s plist.
This should be regarded as a bug in the engine; the fix is to let the plist from StagedBuilds merge into the stub’s plist, effectively reversing the aforementioned process.
1 | // Merge the two plists, using the staged one as the authority when they conflict |
Also, compiling the engine separately via UBT will not include the iPhonePackager program, so it’s necessary to execute a separate compilation for it or create a script to compile all tools each time the engine is compiled, which is more secure. Ultimately, a precompiled iPhonePackager can also be submitted to the engine.
Modifying Remote Build Upload Path
In UE4.26 and earlier versions, uploads are defaulted to the ~
directory, which is hardcoded in RemoteMac.cs and cannot be configured:
1 | // 4.27 or before |
The output of echo ~
is the absolute path of the current ~
.
In UE4.27, the engine introduced a configuration option that allows the RemoteServerOverrideBuildPath
to be set in IOSRuntimeSettings
:
1 | // 4.27 or later |
If configured, it will prioritize that; if not, it will retrieve it using echo ~
.
If there is insufficient disk space on the default upload path, this method can be used to change the remote build upload path to another disk.