Complete iOS IPA packaging flow

This commit is contained in:
aperezro 2026-06-05 14:01:26 -06:00
parent 4f9e6cf2ff
commit ed6cd3cdab
13 changed files with 295 additions and 23 deletions

View file

@ -177,6 +177,8 @@
"CMAKE_OSX_SYSROOT": "iphonesimulator",
"CMAKE_OSX_ARCHITECTURES": "arm64",
"CMAKE_BUILD_TYPE": "Debug",
"UNLEASHED_RECOMP_SKIP_TARGET_TOOL_EXECUTABLES": true,
"UNLEASHED_RECOMP_HOST_TOOLS_DIR": "${sourceDir}/out/build/macos-release",
"CMAKE_TOOLCHAIN_FILE": {
"value": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"type": "FILEPATH"
@ -207,6 +209,8 @@
"CMAKE_OSX_ARCHITECTURES": "arm64",
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_INTERPROCEDURAL_OPTIMIZATION": true,
"UNLEASHED_RECOMP_SKIP_TARGET_TOOL_EXECUTABLES": true,
"UNLEASHED_RECOMP_HOST_TOOLS_DIR": "${sourceDir}/out/build/macos-release",
"CMAKE_TOOLCHAIN_FILE": {
"value": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"type": "FILEPATH"

View file

@ -1,4 +1,18 @@
project("UnleashedRecomp")
project("UnleashedRecomp")
set(UNLEASHED_RECOMP_HOST_TOOLS_DIR "" CACHE PATH "Directory containing host-built recompilation tools.")
function(unleashed_recomp_resolve_tool OUT_VAR TARGET_NAME RELATIVE_PATH)
if (UNLEASHED_RECOMP_HOST_TOOLS_DIR)
set(${OUT_VAR} "${UNLEASHED_RECOMP_HOST_TOOLS_DIR}/${RELATIVE_PATH}" PARENT_SCOPE)
elseif (TARGET ${TARGET_NAME})
set(${OUT_VAR} "$<TARGET_FILE:${TARGET_NAME}>" PARENT_SCOPE)
else()
message(FATAL_ERROR "Tool ${TARGET_NAME} is not available. Set UNLEASHED_RECOMP_HOST_TOOLS_DIR.")
endif()
endfunction()
unleashed_recomp_resolve_tool(UNLEASHED_RECOMP_FILE_TO_C_TOOL file_to_c "tools/file_to_c/file_to_c")
if (WIN32)
option(UNLEASHED_RECOMP_D3D12 "Add D3D12 support for rendering" ON)
@ -27,8 +41,8 @@ function(BIN2C)
set(BIN2C_ARGS_COMPRESSION_TYPE "none")
endif()
add_custom_command(OUTPUT "${BIN2C_ARGS_DEST_FILE}.c"
COMMAND $<TARGET_FILE:file_to_c> "${BIN2C_ARGS_SOURCE_FILE}" "${BIN2C_ARGS_ARRAY_NAME}" "${BIN2C_ARGS_COMPRESSION_TYPE}" "${BIN2C_ARGS_DEST_FILE}.c" "${BIN2C_ARGS_DEST_FILE}.h"
add_custom_command(OUTPUT "${BIN2C_ARGS_DEST_FILE}.c"
COMMAND "${UNLEASHED_RECOMP_FILE_TO_C_TOOL}" "${BIN2C_ARGS_SOURCE_FILE}" "${BIN2C_ARGS_ARRAY_NAME}" "${BIN2C_ARGS_COMPRESSION_TYPE}" "${BIN2C_ARGS_DEST_FILE}.c" "${BIN2C_ARGS_DEST_FILE}.h"
DEPENDS "${BIN2C_ARGS_SOURCE_FILE}"
BYPRODUCTS "${BIN2C_ARGS_DEST_FILE}.h"
COMMENT "Generating binary header for ${BIN2C_ARGS_SOURCE_FILE}..."

View file

@ -166,9 +166,11 @@ void UpdateChecker::visitWebsite()
#elif defined(__linux__)
std::string command = "xdg-open " + std::string(VISIT_URL) + " &";
std::system(command.c_str());
#elif defined(__APPLE__)
#elif defined(__APPLE__) && !defined(__ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__)
std::string command = "open " + std::string(VISIT_URL) + " &";
std::system(command.c_str());
#elif defined(__APPLE__)
// iOS apps cannot spawn external commands; leave website navigation to platform UI.
#else
static_assert(false, "Visit website not implemented for this platform.");
#endif

View file

@ -13,8 +13,10 @@
#include "xcontent_file_system.h"
#include <bit>
#include <fstream>
#include <set>
#include <stack>
#include <xbox.h>
enum class XContentPackageType
{

View file

@ -10,9 +10,27 @@ else()
add_compile_options(-ffp-model=strict)
endif()
target_compile_definitions(XenonRecomp PRIVATE
XENON_RECOMP_CONFIG_FILE_PATH=\"${CMAKE_CURRENT_SOURCE_DIR}/config/SWA.toml\"
XENON_RECOMP_HEADER_FILE_PATH=\"${UNLEASHED_RECOMP_TOOLS_ROOT}/XenonRecomp/XenonUtils/ppc_context.h\")
set(UNLEASHED_RECOMP_HOST_TOOLS_DIR "" CACHE PATH "Directory containing host-built recompilation tools.")
function(unleashed_recomp_resolve_tool OUT_VAR TARGET_NAME RELATIVE_PATH)
if (UNLEASHED_RECOMP_HOST_TOOLS_DIR)
set(${OUT_VAR} "${UNLEASHED_RECOMP_HOST_TOOLS_DIR}/${RELATIVE_PATH}" PARENT_SCOPE)
elseif (TARGET ${TARGET_NAME})
set(${OUT_VAR} "$<TARGET_FILE:${TARGET_NAME}>" PARENT_SCOPE)
else()
message(FATAL_ERROR "Tool ${TARGET_NAME} is not available. Set UNLEASHED_RECOMP_HOST_TOOLS_DIR.")
endif()
endfunction()
unleashed_recomp_resolve_tool(UNLEASHED_RECOMP_XENON_RECOMP_TOOL XenonRecomp "tools/XenonRecomp/XenonRecomp/XenonRecomp")
unleashed_recomp_resolve_tool(UNLEASHED_RECOMP_X_DECOMPRESS_TOOL x_decompress "tools/x_decompress/x_decompress")
unleashed_recomp_resolve_tool(UNLEASHED_RECOMP_XENOS_RECOMP_TOOL XenosRecomp "tools/XenosRecomp/XenosRecomp/XenosRecomp")
if (TARGET XenonRecomp)
target_compile_definitions(XenonRecomp PRIVATE
XENON_RECOMP_CONFIG_FILE_PATH=\"${CMAKE_CURRENT_SOURCE_DIR}/config/SWA.toml\"
XENON_RECOMP_HEADER_FILE_PATH=\"${UNLEASHED_RECOMP_TOOLS_ROOT}/XenonRecomp/XenonUtils/ppc_context.h\")
endif()
set(UNLEASHED_RECOMP_PPC_RECOMPILED_SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/ppc/ppc_config.h"
@ -30,7 +48,7 @@ add_custom_command(
"${CMAKE_CURRENT_SOURCE_DIR}/private/default_patched.xex"
${UNLEASHED_RECOMP_PPC_RECOMPILED_SOURCES}
COMMAND
$<TARGET_FILE:XenonRecomp>
"${UNLEASHED_RECOMP_XENON_RECOMP_TOOL}"
DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/private/default.xex"
"${CMAKE_CURRENT_SOURCE_DIR}/private/default.xexp"
@ -42,7 +60,7 @@ add_custom_command(
OUTPUT
"${CMAKE_CURRENT_SOURCE_DIR}/private/shader_decompressed.ar"
COMMAND
$<TARGET_FILE:x_decompress> "${CMAKE_CURRENT_SOURCE_DIR}/private/shader.ar" "${CMAKE_CURRENT_SOURCE_DIR}/private/shader_decompressed.ar"
"${UNLEASHED_RECOMP_X_DECOMPRESS_TOOL}" "${CMAKE_CURRENT_SOURCE_DIR}/private/shader.ar" "${CMAKE_CURRENT_SOURCE_DIR}/private/shader_decompressed.ar"
DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/private/shader.ar"
)
@ -50,12 +68,14 @@ add_custom_command(
set(XENOS_RECOMP_ROOT "${UNLEASHED_RECOMP_TOOLS_ROOT}/XenosRecomp/XenosRecomp")
set(XENOS_RECOMP_INCLUDE "${XENOS_RECOMP_ROOT}/shader_common.h")
target_compile_definitions(XenosRecomp PRIVATE
XENOS_RECOMP_INPUT=\"${CMAKE_CURRENT_SOURCE_DIR}/private\"
XENOS_RECOMP_OUTPUT=\"${CMAKE_CURRENT_SOURCE_DIR}/shader/shader_cache.cpp\"
XENOS_RECOMP_INCLUDE_INPUT=\"${XENOS_RECOMP_INCLUDE}\"
UNLEASHED_RECOMP
)
if (TARGET XenosRecomp)
target_compile_definitions(XenosRecomp PRIVATE
XENOS_RECOMP_INPUT=\"${CMAKE_CURRENT_SOURCE_DIR}/private\"
XENOS_RECOMP_OUTPUT=\"${CMAKE_CURRENT_SOURCE_DIR}/shader/shader_cache.cpp\"
XENOS_RECOMP_INCLUDE_INPUT=\"${XENOS_RECOMP_INCLUDE}\"
UNLEASHED_RECOMP
)
endif()
file(GLOB XENOS_RECOMP_SOURCES
"${XENOS_RECOMP_ROOT}/*.h"
@ -66,7 +86,7 @@ add_custom_command(
OUTPUT
"${CMAKE_CURRENT_SOURCE_DIR}/shader/shader_cache.cpp"
COMMAND
$<TARGET_FILE:XenosRecomp>
"${UNLEASHED_RECOMP_XENOS_RECOMP_TOOL}"
DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/private/default_patched.xex"
"${CMAKE_CURRENT_SOURCE_DIR}/private/shader_decompressed.ar"

View file

@ -6,6 +6,7 @@ static const char* g_nfd_ios_error = "Native file dialogs are not implemented fo
nfdresult_t NFD_Init(void) { return NFD_OKAY; }
void NFD_Quit(void) {}
const char* NFD_GetError(void) { return g_nfd_ios_error; }
void NFD_ClearError(void) {}
void NFD_FreePathN(nfdnchar_t* filePath) { free(filePath); }
void NFD_FreePathU8(nfdu8char_t* filePath) { free(filePath); }
@ -37,6 +38,6 @@ void NFD_PathSet_FreePathN(const nfdnchar_t* filePath) { free((void*)filePath);
void NFD_PathSet_FreePathU8(const nfdu8char_t* filePath) { free((void*)filePath); }
void NFD_PathSet_Free(const nfdpathset_t* pathSet) { (void)pathSet; }
void NFD_PathSet_FreeEnum(nfdpathsetenum_t* enumerator) { (void)enumerator; }
nfdresult_t NFD_PathSet_GetEnum(const nfdpathset_t* pathSet, nfdpathsetenum_t** outEnumerator) { (void)pathSet; (void)outEnumerator; return NFD_ERROR; }
nfdresult_t NFD_PathSet_GetEnum(const nfdpathset_t* pathSet, nfdpathsetenum_t* outEnumerator) { (void)pathSet; (void)outEnumerator; return NFD_ERROR; }
nfdresult_t NFD_PathSet_EnumNextN(nfdpathsetenum_t* enumerator, nfdnchar_t** outPath) { (void)enumerator; (void)outPath; return NFD_ERROR; }
nfdresult_t NFD_PathSet_EnumNextU8(nfdpathsetenum_t* enumerator, nfdu8char_t** outPath) { (void)enumerator; (void)outPath; return NFD_ERROR; }

View file

@ -1,6 +1,20 @@
add_subdirectory(${UNLEASHED_RECOMP_TOOLS_ROOT}/bc_diff)
add_subdirectory(${UNLEASHED_RECOMP_TOOLS_ROOT}/file_to_c)
add_subdirectory(${UNLEASHED_RECOMP_TOOLS_ROOT}/fshasher)
add_subdirectory(${UNLEASHED_RECOMP_TOOLS_ROOT}/x_decompress)
add_subdirectory(${UNLEASHED_RECOMP_TOOLS_ROOT}/XenonRecomp)
add_subdirectory(${UNLEASHED_RECOMP_TOOLS_ROOT}/XenosRecomp)
if (NOT UNLEASHED_RECOMP_SKIP_TARGET_TOOL_EXECUTABLES)
add_subdirectory(${UNLEASHED_RECOMP_TOOLS_ROOT}/file_to_c)
add_subdirectory(${UNLEASHED_RECOMP_TOOLS_ROOT}/x_decompress)
add_subdirectory(${UNLEASHED_RECOMP_TOOLS_ROOT}/XenonRecomp)
add_subdirectory(${UNLEASHED_RECOMP_TOOLS_ROOT}/XenosRecomp)
else()
set(THIRDPARTY_ROOT ${UNLEASHED_RECOMP_TOOLS_ROOT}/XenonRecomp/thirdparty)
add_subdirectory(${UNLEASHED_RECOMP_TOOLS_ROOT}/XenonRecomp/thirdparty)
add_subdirectory(${UNLEASHED_RECOMP_TOOLS_ROOT}/XenonRecomp/XenonUtils)
if (NOT TARGET libzstd)
add_subdirectory(${UNLEASHED_RECOMP_TOOLS_ROOT}/XenosRecomp/thirdparty/zstd/build/cmake)
endif()
endif()
if (NOT UNLEASHED_RECOMP_SKIP_TARGET_TOOL_EXECUTABLES)
add_subdirectory(${UNLEASHED_RECOMP_TOOLS_ROOT}/iso_extract)
add_subdirectory(${UNLEASHED_RECOMP_TOOLS_ROOT}/xcontent_extract)
endif()

View file

@ -0,0 +1,13 @@
project("iso_extract")
add_executable(iso_extract
"iso_extract.cpp"
"${CMAKE_SOURCE_DIR}/UnleashedRecomp/install/iso_file_system.cpp"
)
target_include_directories(iso_extract PRIVATE
"${CMAKE_SOURCE_DIR}/UnleashedRecomp/install"
"${UNLEASHED_RECOMP_TOOLS_ROOT}/XenonRecomp/XenonUtils"
)
target_link_libraries(iso_extract PRIVATE XenonUtils)

View file

@ -0,0 +1,74 @@
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
#include <iso_file_system.h>
static bool ExtractFile(const ISOFileSystem& iso, const std::string& sourcePath, const std::filesystem::path& outputPath)
{
std::vector<uint8_t> data;
size_t dataSize = iso.getSize(sourcePath);
if (dataSize == 0)
{
std::cerr << "File not found in ISO: " << sourcePath << "\n";
return false;
}
data.resize(dataSize);
if (!iso.load(sourcePath, data.data(), data.size()))
{
std::cerr << "File not found in ISO: " << sourcePath << "\n";
return false;
}
std::error_code ec;
std::filesystem::create_directories(outputPath.parent_path(), ec);
if (ec)
{
std::cerr << "Failed to create output directory: " << outputPath.parent_path() << "\n";
return false;
}
std::ofstream output(outputPath, std::ios::binary);
if (!output)
{
std::cerr << "Failed to open output file: " << outputPath << "\n";
return false;
}
output.write(reinterpret_cast<const char*>(data.data()), static_cast<std::streamsize>(data.size()));
if (!output)
{
std::cerr << "Failed to write output file: " << outputPath << "\n";
return false;
}
return true;
}
int main(int argc, char** argv)
{
if (argc < 4 || ((argc - 2) % 2) != 0)
{
std::cerr << "Usage: iso_extract <game.iso> <iso-path> <output-path> [<iso-path> <output-path> ...]\n";
return 2;
}
auto iso = ISOFileSystem::create(argv[1]);
if (!iso)
{
std::cerr << "Failed to open Xbox 360 ISO: " << argv[1] << "\n";
return 1;
}
bool ok = true;
for (int i = 2; i < argc; i += 2)
{
ok = ExtractFile(*iso, argv[i], argv[i + 1]) && ok;
}
return ok ? 0 : 1;
}

View file

@ -3,14 +3,35 @@ set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BUILD_DIR="$ROOT/out/build/ios-device-release"
HOST_BUILD_DIR="$ROOT/out/build/macos-release"
IPA_DIR="$ROOT/out/ipa"
PAYLOAD_DIR="$IPA_DIR/Payload"
APP_PATH="$BUILD_DIR/Unleashed Recompiled.app"
APP_PATH="$BUILD_DIR/UnleashedRecomp/Unleashed Recompiled.app"
IPA_PATH="$IPA_DIR/UnleashedRecompiled.ipa"
DEFAULT_SIGNING_IDENTITY="Apple Development: aroblesalago@gmail.com (MK28YRUCCG)"
"$ROOT/tools/apply_ios_submodule_patches.sh"
if [[ -n "${ISO_PATH:-}" ]]; then
cmake --preset macos-release
cmake --build "$HOST_BUILD_DIR" --target iso_extract -j "${JOBS:-8}"
"$HOST_BUILD_DIR/tools/iso_extract/iso_extract" "$ISO_PATH" \
default.xex "$ROOT/UnleashedRecompLib/private/default.xex" \
shader.ar "$ROOT/UnleashedRecompLib/private/shader.ar"
fi
if [[ -n "${XEXP_PATH:-}" ]]; then
mkdir -p "$ROOT/UnleashedRecompLib/private"
cp "$XEXP_PATH" "$ROOT/UnleashedRecompLib/private/default.xexp"
fi
if [[ -n "${UPDATE_PATH:-}" ]]; then
cmake --preset macos-release
cmake --build "$HOST_BUILD_DIR" --target xcontent_extract -j "${JOBS:-8}"
"$HOST_BUILD_DIR/tools/xcontent_extract/xcontent_extract" "$UPDATE_PATH" \
default.xexp "$ROOT/UnleashedRecompLib/private/default.xexp"
fi
missing=0
for file in \
"$ROOT/UnleashedRecompLib/private/default.xex" \
@ -27,6 +48,9 @@ if [[ "$missing" -ne 0 ]]; then
exit 1
fi
cmake --preset macos-release
cmake --build "$HOST_BUILD_DIR" --target XenonRecomp XenosRecomp x_decompress file_to_c -j "${JOBS:-8}"
cmake --preset ios-device-release
cmake --build "$BUILD_DIR" --target UnleashedRecomp -j "${JOBS:-8}"

View file

@ -1,5 +1,5 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6a7645a..bf336b9 100644
index 6a7645a..6e6e887 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -8,10 +8,16 @@ if(APPLE)
@ -20,6 +20,24 @@ index 6a7645a..bf336b9 100644
cmake_dependent_option(D3D12_AGILITY_SDK_ENABLED "Enable D3D12 Agility SDK" OFF WIN32 OFF)
option(PLUME_BUILD_EXAMPLES "Build example applications" OFF)
@@ -50,7 +56,7 @@ set(PLUME_SOURCES
)
# Platform-specific files
-if(APPLE)
+if(APPLE AND NOT IS_IOS)
list(APPEND PLUME_SOURCES
plume_metal.cpp
plume_metal.h
@@ -94,7 +100,7 @@ if(D3D12_AGILITY_SDK_ENABLED)
endif()
# Platform-specific includes
-if(APPLE)
+if(APPLE AND NOT IS_IOS)
target_include_directories(plume PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/contrib/metal-cpp
)
diff --git a/plume_vulkan.cpp b/plume_vulkan.cpp
index 9103ca8..02c3591 100644
--- a/plume_vulkan.cpp

View file

@ -0,0 +1,13 @@
project("xcontent_extract")
add_executable(xcontent_extract
"xcontent_extract.cpp"
"${CMAKE_SOURCE_DIR}/UnleashedRecomp/install/xcontent_file_system.cpp"
)
target_include_directories(xcontent_extract PRIVATE
"${CMAKE_SOURCE_DIR}/UnleashedRecomp/install"
"${UNLEASHED_RECOMP_TOOLS_ROOT}/XenonRecomp/XenonUtils"
)
target_link_libraries(xcontent_extract PRIVATE XenonUtils)

View file

@ -0,0 +1,73 @@
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
#include <xcontent_file_system.h>
static bool ExtractFile(const XContentFileSystem& content, const std::string& sourcePath, const std::filesystem::path& outputPath)
{
size_t dataSize = content.getSize(sourcePath);
if (dataSize == 0)
{
std::cerr << "File not found in XContent package: " << sourcePath << "\n";
return false;
}
std::vector<uint8_t> data(dataSize);
if (!content.load(sourcePath, data.data(), data.size()))
{
std::cerr << "Failed to read XContent file: " << sourcePath << "\n";
return false;
}
std::error_code ec;
std::filesystem::create_directories(outputPath.parent_path(), ec);
if (ec)
{
std::cerr << "Failed to create output directory: " << outputPath.parent_path() << "\n";
return false;
}
std::ofstream output(outputPath, std::ios::binary);
if (!output)
{
std::cerr << "Failed to open output file: " << outputPath << "\n";
return false;
}
output.write(reinterpret_cast<const char*>(data.data()), static_cast<std::streamsize>(data.size()));
if (!output)
{
std::cerr << "Failed to write output file: " << outputPath << "\n";
return false;
}
return true;
}
int main(int argc, char** argv)
{
if (argc < 4 || ((argc - 2) % 2) != 0)
{
std::cerr << "Usage: xcontent_extract <package> <package-path> <output-path> [<package-path> <output-path> ...]\n";
return 2;
}
auto content = XContentFileSystem::create(argv[1]);
if (!content)
{
std::cerr << "Failed to open XContent package: " << argv[1] << "\n";
return 1;
}
bool ok = true;
for (int i = 2; i < argc; i += 2)
{
ok = ExtractFile(*content, argv[i], argv[i + 1]) && ok;
}
return ok ? 0 : 1;
}