Files
xeghosted--OrbisForge/src/OrbisForge.Core/Templates/OrbisTargetsTemplate.cs
T
Claude CodeandClaude Fable 5 d863dd462d Add a GHPLUGIN target type for building GoldHEN plugins
A GoldHEN plugin is not a plain PRX. The loader enters the module at _init,
which the GoldHEN SDK's crtprx.o forwards to module_start/module_stop. Built
the stock way it links without complaint and then silently never loads on
console, so add_orbis_target(TYPE GHPLUGIN) applies all three differences:
crtprx.o instead of crtlib.o, -e _init, and SceLibcInternal/SceSysmodule
instead of c/c++. It also checks for the SDK up front and fails with install
instructions rather than producing an unloadable artifact.

The scaffolded starter plugin shows a PS4 notification from module_start. It
is emitted as C++ with extern "C" entry points -- generated projects are
project(<name> CXX), and without that the mangled names leave crtprx.o's _init
unresolved.

Reachable as --type ghplugin (aliases: goldhen, plugin) and as a fourth entry
in the GUI wizard. CI installs the SDK and builds one, since a plugin that
links but cannot load is exactly the failure unit tests cannot catch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 17:57:14 +02:00

258 lines
13 KiB
C#

namespace OrbisForge.Core.Templates;
internal static class OrbisTargetsTemplate
{
public const string OrbisTargetsCMake = """
# add_orbis_target() / add_orbis_package() - generated by OrbisForge.
#
# add_orbis_target(<name>
# TYPE EXE|PRX|SPRX|GHPLUGIN
# SOURCES <files...>
# [LIBS <names...>] # default: c kernel c++
# # (GHPLUGIN: SceLibcInternal kernel SceSysmodule GoldHEN_Hook)
# [PTYPE <ptype>] # default: "fake", or "system_dynlib" for SPRX
# [PAID <hex>] # default: 0x3800000000000011
# [LIBNAME <name>] # library types only, passed to create-fself -libname
# )
#
# Links <name> as a PS4 ELF via the OpenOrbis toolchain, then runs
# create-fself as a POST_BUILD step to produce the final artifact:
# EXE -> eboot.bin
# PRX -> <name>.prx
# SPRX -> <name>.sprx
# GHPLUGIN -> <name>.prx
#
# GHPLUGIN builds a GoldHEN plugin. It differs from PRX in three ways that
# all have to be right or the plugin builds cleanly and never loads: it
# links the GoldHEN SDK's crtprx.o instead of crtlib.o, enters at _init,
# and links SceLibcInternal/SceSysmodule rather than c/c++. Requires the
# GoldHEN Plugin SDK installed into the toolchain. Entry points are
# module_start/module_stop, which must have C linkage.
# readoelf then runs over the packaged .oelf as a build-time sanity check.
# A plain, unsigned <name>.elf (copy of that same validated .oelf) is
# produced alongside it -- raw-ELF loader tools inspect a file's magic
# bytes rather than its extension, so they can't load eboot.bin/*.prx/
# *.sprx (which are SELF-wrapped, not plain ELF) no matter what it's
# renamed to; point those tools at the .elf instead.
function(add_orbis_target name)
set(options "")
set(oneValueArgs TYPE PTYPE PAID LIBNAME)
set(multiValueArgs SOURCES LIBS)
cmake_parse_arguments(ORBIS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if(NOT ORBIS_SOURCES)
message(FATAL_ERROR "add_orbis_target(${name}): SOURCES is required")
endif()
if(NOT ORBIS_TYPE)
set(ORBIS_TYPE "EXE")
endif()
string(TOUPPER "${ORBIS_TYPE}" ORBIS_TYPE)
add_executable(${name} ${ORBIS_SOURCES})
set(_default_libs c kernel c++)
set(_entry_args "")
if(ORBIS_TYPE STREQUAL "EXE")
set(_crt_object "${OO_PS4_TOOLCHAIN}/lib/crt1.o")
set(_default_ptype "fake")
set(_out_name "eboot.bin")
set(_pkg_flag "--eboot")
elseif(ORBIS_TYPE STREQUAL "PRX")
set(_crt_object "${OO_PS4_TOOLCHAIN}/lib/crtlib.o")
set(_default_ptype "fake")
set(_out_name "${name}.prx")
set(_pkg_flag "--lib")
elseif(ORBIS_TYPE STREQUAL "SPRX")
set(_crt_object "${OO_PS4_TOOLCHAIN}/lib/crtlib.o")
set(_default_ptype "system_dynlib")
set(_out_name "${name}.sprx")
set(_pkg_flag "--lib")
elseif(ORBIS_TYPE STREQUAL "GHPLUGIN")
# GoldHEN enters a plugin at _init, which the SDK's crtprx.o forwards
# to module_start/module_stop. Built with the stock crtlib.o instead,
# a plugin links without complaint and then never loads on console --
# so the SDK is checked for here rather than failing silently later.
set(_crt_object "${OO_PS4_TOOLCHAIN}/lib/crtprx.o")
if(NOT EXISTS "${_crt_object}")
message(FATAL_ERROR
"add_orbis_target(${name}): TYPE GHPLUGIN needs the GoldHEN Plugin SDK "
"installed into the toolchain, but ${_crt_object} is missing. "
"Build the SDK (build_static.bat on Windows, make on Linux), then copy "
"crtprx.o and libGoldHEN_Hook.a into ${OO_PS4_TOOLCHAIN}/lib and the SDK's "
"include/* into ${OO_PS4_TOOLCHAIN}/include/GoldHEN.")
endif()
set(_default_ptype "fake")
set(_out_name "${name}.prx")
set(_pkg_flag "--lib")
set(_default_libs SceLibcInternal kernel SceSysmodule GoldHEN_Hook)
set(_entry_args "LINKER:-e,_init")
else()
message(FATAL_ERROR "add_orbis_target(${name}): TYPE must be EXE, PRX, SPRX, or GHPLUGIN (got '${ORBIS_TYPE}')")
endif()
if(NOT ORBIS_LIBS)
set(ORBIS_LIBS ${_default_libs})
endif()
if(_entry_args)
target_link_options(${name} PRIVATE "${_entry_args}")
endif()
# Order matters here and mirrors the toolchain's own sample Makefiles:
# object files, then -l libs, then the CRT object last, so crt1.o's own
# references into libkernel resolve against a linker that has already
# seen -lkernel.
target_link_libraries(${name} PRIVATE ${ORBIS_LIBS} "${_crt_object}")
if(ORBIS_PTYPE)
set(_ptype "${ORBIS_PTYPE}")
else()
set(_ptype "${_default_ptype}")
endif()
if(NOT ORBIS_PAID)
set(ORBIS_PAID "0x3800000000000011")
endif()
set(_libname_args "")
if(ORBIS_LIBNAME)
set(_libname_args -libname "${ORBIS_LIBNAME}")
endif()
set(_out_path "${CMAKE_CURRENT_BINARY_DIR}/${_out_name}")
set(_elf_path "${CMAKE_CURRENT_BINARY_DIR}/${name}.elf")
add_custom_command(TARGET ${name} POST_BUILD
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/create-fself"
-in=$<TARGET_FILE:${name}>
-out=$<TARGET_FILE:${name}>.oelf
${_pkg_flag} "${_out_path}"
-ptype ${_ptype}
--paid ${ORBIS_PAID}
${_libname_args}
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/readoelf" "$<TARGET_FILE:${name}>.oelf"
COMMAND "${CMAKE_COMMAND}" -E copy "$<TARGET_FILE:${name}>.oelf" "${_elf_path}"
BYPRODUCTS "${_out_path}" "${_elf_path}"
COMMENT "orbis: packaging ${name} -> ${_out_name} + ${name}.elf (type=${ORBIS_TYPE}, ptype=${_ptype})"
VERBATIM
)
set_target_properties(${name} PROPERTIES
ORBIS_OUTPUT_PATH "${_out_path}"
ORBIS_OUTPUT_NAME "${_out_name}"
ORBIS_TYPE "${ORBIS_TYPE}"
ORBIS_ELF_PATH "${_elf_path}"
)
endfunction()
# add_orbis_package(<target>
# TITLE_ID <id> CONTENT_ID <id> TITLE <string> [VERSION <string>]
# [ASSETS_DIR <dir>] [SCE_MODULE_DIR <dir>]
# )
#
# Builds a full retail-style .pkg installer from an EXE target created by
# add_orbis_target(): param.sfo -> pkg.gp4 -> <content-id>.pkg, exactly
# mirroring the toolchain's own sample Makefiles. Adds a "<target>_pkg"
# target you build explicitly (it is not part of the default `all`).
function(add_orbis_package target)
set(oneValueArgs TITLE_ID CONTENT_ID TITLE VERSION ASSETS_DIR SCE_MODULE_DIR)
cmake_parse_arguments(PKG "" "${oneValueArgs}" "" ${ARGN})
if(NOT PKG_TITLE_ID OR NOT PKG_CONTENT_ID OR NOT PKG_TITLE)
message(FATAL_ERROR "add_orbis_package(${target}): TITLE_ID, CONTENT_ID, and TITLE are required")
endif()
if(NOT PKG_VERSION)
set(PKG_VERSION "1.00")
endif()
if(NOT PKG_ASSETS_DIR)
set(PKG_ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/assets")
endif()
if(NOT PKG_SCE_MODULE_DIR)
set(PKG_SCE_MODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/sce_module")
endif()
get_target_property(_eboot ${target} ORBIS_OUTPUT_PATH)
if(NOT _eboot)
message(FATAL_ERROR "add_orbis_package(${target}): target has no ORBIS_OUTPUT_PATH; call add_orbis_target(${target} TYPE EXE ...) first")
endif()
set(_sce_sys "${CMAKE_CURRENT_SOURCE_DIR}/sce_sys")
# create-gp4's --files list has no separate source-vs-target-path syntax:
# each entry is a path relative to the working directory it runs from,
# and that SAME relative path becomes the file's location inside the
# package. eboot.bin and param.sfo are built elsewhere (under
# CMAKE_CURRENT_BINARY_DIR), so they need to be staged into a directory
# tree that already looks like the final package layout before create-gp4
# runs from it -- otherwise the package ends up with eboot.bin nested
# under a stray "build/" path instead of at its root, where the PS4
# loader requires it.
set(_stage "${CMAKE_CURRENT_BINARY_DIR}/pkg_root")
set(_sfo "${_stage}/sce_sys/param.sfo")
set(_gp4 "${_stage}/pkg.gp4")
# PkgTool.Core's pkg_build takes the gp4 path plus a "project directory"
# it resolves the gp4's relative file entries against AND writes the
# final .pkg into -- that has to be the same staging directory too.
set(_pkg_out "${_stage}/${PKG_CONTENT_ID}.pkg")
add_custom_command(
OUTPUT "${_sfo}"
COMMAND "${CMAKE_COMMAND}" -E make_directory "${_stage}/sce_sys/about" "${_stage}/sce_module" "${_stage}/assets"
COMMAND "${CMAKE_COMMAND}" -E copy "${_eboot}" "${_stage}/eboot.bin"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${_sce_sys}/icon0.png" "${_stage}/sce_sys/icon0.png"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${_sce_sys}/about/right.sprx" "${_stage}/sce_sys/about/right.sprx"
COMMAND "${CMAKE_COMMAND}" -E copy_directory "${PKG_SCE_MODULE_DIR}" "${_stage}/sce_module"
COMMAND "${CMAKE_COMMAND}" -E copy_directory "${PKG_ASSETS_DIR}" "${_stage}/assets"
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_new "${_sfo}"
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" APP_TYPE --type Integer --maxsize 4 --value 1
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" APP_VER --type Utf8 --maxsize 8 --value "${PKG_VERSION}"
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" ATTRIBUTE --type Integer --maxsize 4 --value 0
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" CATEGORY --type Utf8 --maxsize 4 --value gd
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" CONTENT_ID --type Utf8 --maxsize 48 --value "${PKG_CONTENT_ID}"
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" DOWNLOAD_DATA_SIZE --type Integer --maxsize 4 --value 0
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" SYSTEM_VER --type Integer --maxsize 4 --value 0
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" TITLE --type Utf8 --maxsize 128 --value "${PKG_TITLE}"
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" TITLE_ID --type Utf8 --maxsize 12 --value "${PKG_TITLE_ID}"
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" VERSION --type Utf8 --maxsize 8 --value "${PKG_VERSION}"
DEPENDS "${target}"
COMMENT "orbis: staging package contents + generating param.sfo for ${target}"
VERBATIM
)
file(GLOB_RECURSE _assets RELATIVE "${PKG_ASSETS_DIR}" CONFIGURE_DEPENDS "${PKG_ASSETS_DIR}/*")
file(GLOB _modules RELATIVE "${PKG_SCE_MODULE_DIR}" CONFIGURE_DEPENDS "${PKG_SCE_MODULE_DIR}/*")
list(TRANSFORM _assets PREPEND "assets/")
list(TRANSFORM _modules PREPEND "sce_module/")
set(_all_files eboot.bin sce_sys/about/right.sprx sce_sys/param.sfo sce_sys/icon0.png ${_modules} ${_assets})
list(JOIN _all_files " " _files_arg)
add_custom_command(
OUTPUT "${_gp4}"
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/create-gp4"
-out "${_gp4}"
--content-id=${PKG_CONTENT_ID}
--files "${_files_arg}"
WORKING_DIRECTORY "${_stage}"
DEPENDS "${_sfo}"
COMMENT "orbis: generating pkg.gp4 for ${target}"
VERBATIM
)
add_custom_command(
OUTPUT "${_pkg_out}"
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" pkg_build "${_gp4}" "${_stage}"
WORKING_DIRECTORY "${_stage}"
DEPENDS "${_gp4}"
COMMENT "orbis: building ${PKG_CONTENT_ID}.pkg"
VERBATIM
)
add_custom_target(${target}_pkg DEPENDS "${_pkg_out}")
set_target_properties(${target}_pkg PROPERTIES ORBIS_PKG_PATH "${_pkg_out}")
endfunction()
""";
}