Files
dusklight/cmake/ModSDK.cmake
T
Luke Street 5c9c76cc0b Dusklight mod API core
Co-authored-by: qwertyquerty <qwertytrogi@gmail.com>
Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2026-07-08 01:47:31 -06:00

216 lines
9.3 KiB
CMake

# add_mod(<target> SOURCES <file>... MOD_JSON <mod.json> [RES_DIR <res>] [OUTPUT_DIR <dir>] [BUNDLE])
set(DUSK_MODS_OUTPUT_DIR "${CMAKE_BINARY_DIR}/mods" CACHE PATH "Directory to write mod packages into")
function(_mod_lib_name out_var)
set(_arch "${CMAKE_SYSTEM_PROCESSOR}")
if (APPLE AND CMAKE_OSX_ARCHITECTURES)
list(LENGTH CMAKE_OSX_ARCHITECTURES _count)
if (_count GREATER 1)
message(FATAL_ERROR "add_mod: universal binaries are not supported")
endif ()
set(_arch "${CMAKE_OSX_ARCHITECTURES}")
endif ()
string(TOLOWER "${CMAKE_SYSTEM_NAME}" _platform)
string(TOLOWER "${_arch}" _arch)
if (_arch MATCHES "^(i[3-6]86|x86)$")
set(_arch "x86")
endif ()
if (WIN32)
set(_ext ".dll")
elseif (APPLE)
set(_ext ".dylib")
else ()
set(_ext ".so")
endif ()
set(${out_var} "${_platform}-${_arch}${_ext}" PARENT_SCOPE)
endfunction()
function(_mod_resolve_source_path out_var path)
if (IS_ABSOLUTE "${path}")
set(_path "${path}")
else ()
set(_path "${CMAKE_CURRENT_SOURCE_DIR}/${path}")
endif ()
set(${out_var} "${_path}" PARENT_SCOPE)
endfunction()
function(_mod_collect_assets out_var dir)
if (NOT IS_DIRECTORY "${dir}")
message(FATAL_ERROR "add_mod: asset directory does not exist: ${dir}")
endif ()
file(GLOB_RECURSE _files CONFIGURE_DEPENDS LIST_DIRECTORIES false "${dir}/*")
set(${out_var} ${_files} PARENT_SCOPE)
endfunction()
function(add_mod target_name)
cmake_parse_arguments(ARG "BUNDLE" "MOD_JSON;RES_DIR;OUTPUT_DIR" "SOURCES" ${ARGN})
if (NOT ARG_MOD_JSON)
message(FATAL_ERROR "add_mod: MOD_JSON is required")
endif ()
_mod_resolve_source_path(_mod_json "${ARG_MOD_JSON}")
if (NOT EXISTS "${_mod_json}")
message(FATAL_ERROR "add_mod: MOD_JSON does not exist: ${_mod_json}")
endif ()
add_library(${target_name} SHARED ${ARG_SOURCES})
_mod_lib_name(_lib_name)
set_target_properties(${target_name} PROPERTIES
PREFIX ""
C_VISIBILITY_PRESET hidden
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON
WINDOWS_EXPORT_ALL_SYMBOLS OFF)
target_compile_features(${target_name} PRIVATE cxx_std_20)
target_link_libraries(${target_name} PRIVATE dusklight_game_headers)
if (NOT TARGET dusklight)
# Apply global compile options for out-of-tree mod builds
if (CMAKE_SYSTEM_NAME STREQUAL Linux)
target_compile_options(${target_name} PRIVATE
-Wno-multichar -Wno-trigraphs -Wno-deprecated-declarations)
elseif (APPLE)
target_compile_options(${target_name} PRIVATE
-Wno-declaration-after-statement -Wno-non-pod-varargs)
elseif (MSVC)
target_compile_options(${target_name} PRIVATE
"$<$<COMPILE_LANGUAGE:C,CXX>:/bigobj>"
"$<$<COMPILE_LANGUAGE:C,CXX>:/utf-8>")
endif ()
# Use signed char on ARM to match the original game (and x86)
string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _mod_arch)
if (_mod_arch MATCHES "^(arm|aarch64)" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "GNU")
target_compile_options(${target_name} PRIVATE -fsigned-char)
endif ()
endif ()
set(_output_dir "${DUSK_MODS_OUTPUT_DIR}")
if (ARG_OUTPUT_DIR)
set(_output_dir "${ARG_OUTPUT_DIR}")
endif ()
set(_stage "${CMAKE_CURRENT_BINARY_DIR}/${target_name}_stage")
set(_out "${_output_dir}/${target_name}.dusk")
set(_zip_args "${_lib_name}" mod.json)
set(_package_deps "${_mod_json}")
set(_package_inputs "${_mod_json}")
set(_extra_cmds "")
if (ARG_RES_DIR)
_mod_resolve_source_path(_res_dir "${ARG_RES_DIR}")
_mod_collect_assets(_res_deps "${_res_dir}")
list(APPEND _package_deps ${_res_deps})
list(APPEND _package_inputs "${_res_dir}" ${_res_deps})
list(APPEND _zip_args res)
list(APPEND _extra_cmds COMMAND ${CMAKE_COMMAND} -E copy_directory
"${_res_dir}" "${_stage}/res")
endif ()
set(_bundle_cmds "")
if (ARG_BUNDLE AND TARGET dusklight)
file(READ "${_mod_json}" _mod_json_text)
string(JSON _mod_id GET "${_mod_json_text}" id)
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_TARGETS "${target_name}")
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_IDS "${_mod_id}")
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_STAGES "${_stage}")
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_PACKAGES "${_out}")
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_LIB_NAMES "${_lib_name}")
set(_bundle_cmds
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/bundled_mods"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_out}" "${CMAKE_BINARY_DIR}/bundled_mods/${target_name}.dusk")
endif ()
set(_package_target "${target_name}_package")
set(_package_inputs_file "${CMAKE_CURRENT_BINARY_DIR}/${target_name}_package_inputs.txt")
list(SORT _package_inputs)
set(_package_inputs_text "")
foreach (_package_input IN LISTS _package_inputs)
string(APPEND _package_inputs_text "${_package_input}\n")
endforeach ()
file(GENERATE OUTPUT "${_package_inputs_file}" CONTENT "${_package_inputs_text}")
add_custom_command(OUTPUT "${_out}"
COMMAND ${CMAKE_COMMAND} -E rm -rf "${_stage}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${_stage}" "${_output_dir}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "$<TARGET_FILE:${target_name}>" "${_stage}/${_lib_name}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_mod_json}" "${_stage}/mod.json"
${_extra_cmds}
COMMAND ${CMAKE_COMMAND} -E chdir "${_stage}" ${CMAKE_COMMAND} -E tar cvf "${_out}" --format=zip ${_zip_args}
${_bundle_cmds}
DEPENDS ${target_name} ${_package_deps} "${_package_inputs_file}"
COMMENT "Packaging ${target_name} -> ${_out}"
COMMAND_EXPAND_LISTS
VERBATIM
)
add_custom_target(${_package_target} ALL DEPENDS "${_out}")
if (TARGET dusklight_mods)
add_dependencies(dusklight_mods ${_package_target})
endif ()
endfunction()
# Install rules for BUNDLE mods.
# - Windows: the .dusk archives into <install>/mods (the loader extracts native libs to the
# user cache).
# - Linux: pre-extracted stage dirs into <install>/mods so native libs dlopen in place from
# read-only installs.
# - macOS: pre-extracted stage dirs into the installed app's Contents/Resources/mods, dylibs
# ad-hoc signed in place, then the whole bundle re-signed.
# - iOS/tvOS: assets into <app>/mods/<id> and the dylib into Frameworks/<id>.dylib.
# - Android: nothing here; gradle packs ${CMAKE_BINARY_DIR}/bundled_mods into APK assets.
function(install_bundled_mods)
get_property(_targets GLOBAL PROPERTY DUSK_BUNDLED_MOD_TARGETS)
if (NOT _targets OR ANDROID)
return ()
endif ()
get_property(_ids GLOBAL PROPERTY DUSK_BUNDLED_MOD_IDS)
get_property(_stages GLOBAL PROPERTY DUSK_BUNDLED_MOD_STAGES)
get_property(_lib_names GLOBAL PROPERTY DUSK_BUNDLED_MOD_LIB_NAMES)
list(LENGTH _targets _count)
math(EXPR _last "${_count} - 1")
if (APPLE)
get_target_property(_app_name dusklight OUTPUT_NAME)
if (NOT _app_name)
set(_app_name dusklight)
endif ()
set(_bundle_dir "${CMAKE_INSTALL_PREFIX}/${_app_name}.app")
if (IOS OR TVOS)
foreach (_i RANGE ${_last})
list(GET _targets ${_i} _target)
list(GET _ids ${_i} _id)
list(GET _stages ${_i} _stage)
list(GET _lib_names ${_i} _lib_name)
install(DIRECTORY "${_stage}/" DESTINATION "${_bundle_dir}/mods/${_id}"
PATTERN "${_lib_name}" EXCLUDE)
install(PROGRAMS "$<TARGET_FILE:${_target}>"
DESTINATION "${_bundle_dir}/Frameworks" RENAME "${_id}.dylib")
endforeach ()
else ()
foreach (_i RANGE ${_last})
list(GET _ids ${_i} _id)
list(GET _stages ${_i} _stage)
list(GET _lib_names ${_i} _lib_name)
install(DIRECTORY "${_stage}/" DESTINATION "${_bundle_dir}/Contents/Resources/mods/${_id}")
install(CODE "execute_process(COMMAND /usr/bin/codesign --force --sign - \"${_bundle_dir}/Contents/Resources/mods/${_id}/${_lib_name}\" COMMAND_ERROR_IS_FATAL ANY)")
endforeach ()
install(CODE "execute_process(COMMAND /usr/bin/codesign --force --sign - --entitlements \"${DUSK_ENTITLEMENTS}\" \"${_bundle_dir}\" COMMAND_ERROR_IS_FATAL ANY)")
endif ()
return ()
endif ()
if (DUSK_PACKAGE_INSTALL)
set(_mods_dest "${CMAKE_INSTALL_DATAROOTDIR}/dusklight/mods")
else ()
set(_mods_dest "${CMAKE_INSTALL_PREFIX}/mods")
endif ()
if (WIN32)
foreach (_target IN LISTS _targets)
install(FILES "${CMAKE_BINARY_DIR}/bundled_mods/${_target}.dusk" DESTINATION "${_mods_dest}")
endforeach ()
else ()
foreach (_i RANGE ${_last})
list(GET _ids ${_i} _id)
list(GET _stages ${_i} _stage)
install(DIRECTORY "${_stage}/" DESTINATION "${_mods_dest}/${_id}")
endforeach ()
endif ()
endfunction()