Hi all. So, here follows the quiet elaborate write up of what I had to do, to get this to work with an UE5.8 binary build of the engine. First and foremost you should be familiar with Terminal on Mac to get through this. I did this on my Mac, therefore the paths are Mac specific, but you should be able to apply this to CMD on Windows as well, at least for the patching part.
Then as a disclaimer: I was able to work around this issue by using Claude Code. So, decide for yourself if you want to trust this or not. I can only affirm you that it has worked for me.
Now let’s get going:
Symptoms
- Packaging for iOS produces a working
.app that installs fine on-device via Xcode, but never produces a valid, submittable .xcarchive
- Enabling “Generate Xcode Archive” + “For Distribution” in Project Settings doesn’t help
- Manually forcing
-archive -distribution via RunUAT.sh BuildCookRun fails with:
cp: .../BuildProductsPath/IOS/<YourApp>.app: Not a directoryCommand PhaseScriptExecution failed with a nonzero exit code
** ARCHIVE FAILED **
- If UAT still reports “success” afterward, check the resulting
.xcarchive closely — UAT’s archive step silently falls back to copying an old, stale archive from ~/Library/Developer/Xcode/Archives/ when the real xcodebuild archive invocation fails, so it can look deceptively fine at a glance.
- This happens on Xcode 15+ (confirmed on 16.1 and 26.1), independent of engine hotfix version (reproduced on 5.8.2 and 5.8.3).
Root cause
During an Xcode Archive (“install”) build, Xcode redirects CONFIGURATION_BUILD_DIR through a compatibility symlink pointing to the real product location (InstallationBuildProductsLocation). This is normal Xcode behavior, not an engine bug by itself.
The bug is in the “Copy Executable and Staged Data into .app” script phase that UnrealBuildTool generates for iOS targets. One specific line:
cp -r "${UPL_BUILD_DIR}" "${CONFIGURATION_BUILD_DIR}/${CONTENTS_FOLDER_PATH}"
copies your plugins’ UPL build output (e.g. GoogleService-Info.plist) using cp -r against that symlinked destination. macOS’s cp -R/-r does not treat a symlink-to-directory as a valid “copy into” target the way a plain cp or rsync does, and fails with Not a directory. This never surfaces on a normal device Build (no symlink involved there), only on Archive — which is why it’s so easy to miss.
Fixing XcodeProject.cs
You need to adjust and add some lines of code to the XcodeProject.cs file, which can be found here:
File: Engine/Source/Programs/UnrealBuildTool/ProjectFiles/Xcode/XcodeProject.cs, method ProcessScripts.
- Insert a new line right after line 1556 (the rsync that copies the executable), inside the first CopyScript.AddRange block:
"rsync -au \\\"${SRC_EXE}\\\" \\\"${DEST_EXE}\\\"",
Add: "CONTENTS_DIR=$(cd -P \\\"${CONFIGURATION_BUILD_DIR}/${CONTENTS_FOLDER_PATH}\\\" && pwd)",
- Line 1638 (Program branch’s rm -rf) — interpolated string, change:
Replace: $" rm -rf \\\"${{CONFIGURATION_BUILD_DIR}}/${{CONTENTS_FOLDER_PATH}}{SyncDestSubdir}\\\"",
With: $" rm -rf \\\"${{CONTENTS_DIR}}{SyncDestSubdir}\\\"",
- Line 1663 (the uecommandline.txt copy):
Replace: " cp \\\"${STAGED_DIR}/uecommandline.txt\\\" \\\"${CONFIGURATION_BUILD_DIR}/${CONTENTS_FOLDER_PATH}\\\"",
With: " cp \\\"${STAGED_DIR}/uecommandline.txt\\\" \\\"${CONTENTS_DIR}\\\"",
- Line 1667 — this is the actual bug, the cp -r for the UPL build output:
Replace: "cp -r \\\"${UPL_BUILD_DIR}\\\" \\\"${CONFIGURATION_BUILD_DIR}/${CONTENTS_FOLDER_PATH}\\\"",
With: "cp -r \\\"${UPL_BUILD_DIR}\\\" \\\"${CONTENTS_DIR}\\\"",
- Line 1677 (the cookeddata rsync) — interpolated string, change:
Replace: $"rsync -a --delete {ExcludeString} \\\"${{STAGED_DIR}}{SyncSourceSubdir}/\\\" \\\"${{CONFIGURATION_BUILD_DIR}}/${{CONTENTS_FOLDER_PATH}}{SyncDestSubdir}\\\"",
With: $"rsync -a --delete {ExcludeString} \\\"${{STAGED_DIR}}{SyncSourceSubdir}/\\\" \\\"${{CONTENTS_DIR}}{SyncDestSubdir}\\\"",
Applying the fix
- Edit
XcodeProject.cs as above.
- Rebuild UnrealBuildTool itself — editing the .cs file alone does nothing until it’s recompiled. You can do this via this command in Terminal:
"<EngineDir>/Binaries/ThirdParty/DotNet/<version>/<platform>/dotnet" build \ "<EngineDir>/Source/Programs/UnrealBuildTool/UnrealBuildTool.csproj" -c Development
Then find where it actually built to (it does not land in Engine/Binaries/DotNET/... automatically):
find "<EngineDir>/Source/Programs/UnrealBuildTool" -name "UnrealBuildTool.dll"
and sync the real bin/Development/<tfm>/ output folder into place:
rsync -a "<found bin folder>/" "<EngineDir>/Binaries/DotNET/UnrealBuildTool/"
Essentially we want to copy the newly built DLLs and other files for the UnrealBuildTool from .../UE_5.8/Engine/Source/Programs/UnrealBuildTool/bin/Development/net10.0/... to .../UE_5.8/Engine/Binaries/DotNET/UnrealBuildTool/.... And yes, make sure to override those. You can always use the verify option via the Launcher to revert those, or just copy the directory beforehand as backup.
- If you’re on a Launcher/binary-installed engine (not a source build), you must also rebuild
AutomationTool — this is the step that’s easy to miss and was quiet a doozy to find out. AutomationTool (which drives RunUAT/BuildCookRun’s -archive step, and is what the Editor’s “Package Project” button uses) ships precompiled and is not automatically rebuilt when you patch UnrealBuildTool. Source builds work “by default” simply because they recompile everything, including AutomationTool, during normal engine setup. Now, we want to actually rebuild AutomationTool the same way we did with UnrealBuild tool. Here are some Terminal commands to do it:
# 1. Find the path for the AutomationTool.csproj
find "<EngineDir>/Source/Programs/AutomationTool" -maxdepth 1 -name "*.csproj""<dotnet path>"
# 2. Rebuild the AutomationTool
build "<AutomationTool.csproj path>" -c Development
# 3. Get the path where the newly built AutomationTool DLL and files are
find "<EngineDir>/Source/Programs/AutomationTool" -name "AutomationTool.dll"
# 4. Copy them to the actual AutomationTool DLL path (binaries)
rsync -a "<found bin folder>/" "<EngineDir>/Binaries/DotNET/AutomationTool/"
So, again: You need to use the first two commands to actually build the AutomationTool. Step 3. & 4. are optional and you can just manually copy those files over if you prefer to do it manually.
These are the paths if you wish to do so: .../UE_5.8/Engine/Source/Programs/AutomationTool/bin/Development/net10.0/... to .../UE_5.8/Engine/Binaries/DotNET/AutomationTool/...
- Delete any stale generated project files/caches so everything regenerates from the patched tools:
# 1. Delete ProjectFilesIOS located in your projects intermediate folder
rm -rf "<ProjectDir>/Intermediate/ProjectFiles" "<ProjectDir>/Intermediate/ProjectFilesIOS"
# 2. Delete derived data from your Macs developer folder
rm -rf ~/Library/Developer/Xcode/DerivedData/<YourProject>*
- Try packaging again — via Terminal
RunUAT.sh BuildCookRun ... -distribution -archive, or the Editor’s “Package Project”, or a manual Xcode Archive. All three now work identically.
Make sure to verify
- Check the resulting
.xcarchive: Products/Applications/<App>.app should be several hundred MB (includes your actual cooked content), not just tens of MB (executable only).
- Run Window → Organizer → (select archive) → Validate App in Xcode — it should pass App Store submission checks.
And yes, that’s it. Quiet a process as promised, but the figuring it out part is what took a long time. But I hope, that this helps in preventing a severe headache for anyone else.