From bdaf1e4151148d003699a66f1022686610af4e76 Mon Sep 17 00:00:00 2001 From: David Mentler Date: Mon, 20 May 2024 12:12:57 +0200 Subject: [PATCH] build: Xcode accomodating CMake setup (#1688) ### Problem description This PR implements some rudimentary Xcode support for building and editing ImHex. ### Implementation description #### Problem 1: Xcode is a multi-configuration buildsystem The project is already rather CMake generator independent, thus it did not need to change much to support Xcode's multi-configuration paradigm: By default, CMake generates a `.xcodeproj` in which targets build their artifacts into the specified `<>_OUTPUT_DIRECTORY`, postfixed by the currently active configuration. To better fit the existing paradigm, I instead opted ot introduce `IMHEX_MAIN_OUTPUT_DIRECTORY`. This variable is equal to the previously used `RUNTIME_OUTPUT_DIRECTORY` when using other generators, and is changed to include a configuration specific _prefix_ when used with Xcode. The result is different output directories when using Xcode, and no changes when using any other generator. #### Problem 2: ImHex does not support AppleClang To allow building the codebase with Xcode, I have introduced `IMHEX_IDE_HELPERS_OVERRIDE_XCODE_COMPILER`. Specifying this option to `ON` will force CMake to honor the user specified compiler settings, even when using the Xcode generator. In practice this can be used together with the new "xcode" CMakePreset to build the project with mainline clang using `xcodebuild`, or Xcode itself by generating a buildsystem like so: ``` cmake --preset xcode -DCMAKE_PREFIX_PATH=/opt/homebrew/opt/llvm@17 ``` This solution is of course not without flaws. The inner workings are a particularly ugly hack, and mainline clang does not implement the necessary extensions to allow Xcode to index the code. Regardless this option is useful to enable future work in terms of bundling/signing macOS applications in the "intended" way using Xcode without additional source modifications. #### Problem 3: Vanilla CMake + Xcode = Bad developer UX By default, the CMake generated `.xcodeproj` is a mess. Tons of targets are scattered about, and source files are not organized beyond grouping them into a "Source Files" and "Header Files" group. Even "Header Files" is missing, because the ImHex build system does not regard private header files of libraries as sources of a target, and Xcode does not try to guess this information. The solution is twofold: * Additional code has been added which organizes the targets into a neat folder structure * Additional code was added behind a configuration flag `IMHEX_IDE_HELPERS_INTRUSIVE_IDE_TWEAKS` which automatically creates source file trees in Xcode targets, and discovers the non-declared header files via the folder convention. ### Screenshots N/A ### Additional things As a bonus: `IMHEX_OFFLINE_BUILD` assumes that ImHex-Patterns is cloned into the source tree. I have added an additional fallback that tries to locate it as a sibling folder of `${CMAKE_SOURCE_DIR}`, as this meshes better with my filesystem setup. The setup was tested with `CMake 3.29.2`, `Xcode 15.2`, and `llvm@17` from homebrew. --- .gitignore | 2 +- CMakeLists.txt | 13 ++- CMakePresets.json | 17 ++++ cmake/build_helpers.cmake | 56 ++++++++++-- cmake/ide_helpers.cmake | 149 ++++++++++++++++++++++++++++++++ cmake/modules/ImHexPlugin.cmake | 2 +- main/gui/CMakeLists.txt | 7 +- 7 files changed, 230 insertions(+), 16 deletions(-) create mode 100644 cmake/ide_helpers.cmake diff --git a/.gitignore b/.gitignore index 7e798286b..be9e97d76 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,5 @@ venv/ *.kdev4 imgui.ini .DS_Store -./CMakeUserPresets.json +CMakeUserPresets.json Brewfile.lock.json diff --git a/CMakeLists.txt b/CMakeLists.txt index acb545df0..1197251dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,23 +22,29 @@ option(IMHEX_ENABLE_STD_ASSERTS "Enable debug asserts in the C++ std lib option(IMHEX_ENABLE_UNIT_TESTS "Enable building unit tests" OFF) option(IMHEX_ENABLE_PRECOMPILED_HEADERS "Enable precompiled headers" OFF) +set(IMHEX_BASE_FOLDER "${CMAKE_CURRENT_SOURCE_DIR}") +set(CMAKE_MODULE_PATH "${IMHEX_BASE_FOLDER}/cmake/modules") + +# Optional IDE support +include("${IMHEX_BASE_FOLDER}/cmake/ide_helpers.cmake") + # Basic compiler and cmake configurations set(CMAKE_CXX_STANDARD 23) set(CMAKE_INCLUDE_DIRECTORIES_BEFORE ON) -set(IMHEX_BASE_FOLDER "${CMAKE_CURRENT_SOURCE_DIR}") -set(CMAKE_MODULE_PATH "${IMHEX_BASE_FOLDER}/cmake/modules") include("${IMHEX_BASE_FOLDER}/cmake/build_helpers.cmake") # Setup project loadVersion(IMHEX_VERSION) setVariableInParent(IMHEX_VERSION ${IMHEX_VERSION}) configureCMake() + project(imhex LANGUAGES C CXX VERSION ${IMHEX_VERSION} DESCRIPTION "The ImHex Hex Editor" HOMEPAGE_URL "https://imhex.werwolv.net" ) +configureProject() # Add ImHex sources add_custom_target(imhex_all ALL) @@ -75,3 +81,6 @@ generateSDKDirectory() # Handle package generation createPackage() + +# Accomodate IDEs with FOLDER support +tweakTargetsForIDESupport() diff --git a/CMakePresets.json b/CMakePresets.json index 9b45394d9..83c9e5342 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -28,6 +28,23 @@ "displayName": "x86_64 Build", "description": "x86_64 build", "inherits": [ "base" ] + }, + { + "name": "xcode", + "inherits": [ "base" ], + + "displayName": "Xcode", + "description": "Xcode with external compiler override", + "generator": "Xcode", + + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + + "CMAKE_CXX_COMPILER": "clang++", + "CMAKE_CXX_FLAGS": "-fexperimental-library -Wno-shorten-64-to-32 -Wno-deprecated-declarations", + + "IMHEX_IDE_HELPERS_OVERRIDE_XCODE_COMPILER": "ON" + } } ], "buildPresets": [ diff --git a/cmake/build_helpers.cmake b/cmake/build_helpers.cmake index dd1dc0a5a..b19d69261 100644 --- a/cmake/build_helpers.cmake +++ b/cmake/build_helpers.cmake @@ -152,14 +152,12 @@ macro(addPluginDirectories) foreach (plugin IN LISTS PLUGINS) add_subdirectory("plugins/${plugin}") if (TARGET ${plugin}) - set_target_properties(${plugin} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins) - set_target_properties(${plugin} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins) + set_target_properties(${plugin} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${IMHEX_MAIN_OUTPUT_DIRECTORY}/plugins") + set_target_properties(${plugin} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${IMHEX_MAIN_OUTPUT_DIRECTORY}/plugins") if (APPLE) if (IMHEX_GENERATE_PACKAGE) set_target_properties(${plugin} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${PLUGINS_INSTALL_LOCATION}) - else () - set_target_properties(${plugin} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins) endif () else () if (WIN32) @@ -275,6 +273,8 @@ macro(createPackage) endif() install(CODE [[ message(STATUS "MacOS Bundle finalized. DO NOT TOUCH IT ANYMORE! ANY MODIFICATIONS WILL BREAK IT FROM NOW ON!") ]]) + else() + downloadImHexPatternsFiles("${IMHEX_MAIN_OUTPUT_DIRECTORY}") endif() else() install(TARGETS main RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) @@ -342,8 +342,11 @@ macro(configureCMake) if (LD_LLD_PATH) set(CMAKE_LINKER ${LD_LLD_PATH}) - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fuse-ld=lld") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fuse-ld=lld") + + if (NOT XCODE) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fuse-ld=lld") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fuse-ld=lld") + endif() else () message(WARNING "lld not found, using default linker!") endif () @@ -378,6 +381,15 @@ macro(configureCMake) set(CMAKE_WARN_DEPRECATED OFF CACHE BOOL "Disable deprecated warnings" FORCE) endmacro() +function(configureProject) + if (XCODE) + # Support Xcode's multi configuration paradigm by placing built artifacts into separate directories + set(IMHEX_MAIN_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Configs/$" PARENT_SCOPE) + else() + set(IMHEX_MAIN_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" PARENT_SCOPE) + endif() +endfunction() + macro(setDefaultBuiltTypeIfUnset) if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "RelWithDebInfo" CACHE STRING "Using RelWithDebInfo build type as it was left unset" FORCE) @@ -480,14 +492,40 @@ function(downloadImHexPatternsFiles dest) message(STATUS "Finished downloading ImHex-Patterns") else () + set(imhex_patterns_SOURCE_DIR "") + # Maybe patterns are cloned to a subdirectory - set(imhex_patterns_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ImHex-Patterns") + if (NOT EXISTS ${imhex_patterns_SOURCE_DIR}) + set(imhex_patterns_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ImHex-Patterns") + endif() + + # Or a sibling directory + if (NOT EXISTS ${imhex_patterns_SOURCE_DIR}) + set(imhex_patterns_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../ImHex-Patterns") + endif() endif () - if (EXISTS ${imhex_patterns_SOURCE_DIR}) + if (NOT EXISTS ${imhex_patterns_SOURCE_DIR}) + message(WARNING "Failed to locate ImHex-Patterns repository, some resources will be missing during install!") + elseif(XCODE) + # The Xcode build has multiple configurations, which each need a copy of these files + file(GLOB_RECURSE sourceFilePaths LIST_DIRECTORIES NO CONFIGURE_DEPENDS RELATIVE "${imhex_patterns_SOURCE_DIR}" + "${imhex_patterns_SOURCE_DIR}/constants/*" + "${imhex_patterns_SOURCE_DIR}/encodings/*" + "${imhex_patterns_SOURCE_DIR}/includes/*" + "${imhex_patterns_SOURCE_DIR}/patterns/*" + "${imhex_patterns_SOURCE_DIR}/magic/*" + "${imhex_patterns_SOURCE_DIR}/nodes/*" + ) + list(FILTER sourceFilePaths EXCLUDE REGEX "_schema.json$") + + foreach(relativePath IN LISTS sourceFilePaths) + file(GENERATE OUTPUT "${dest}/${relativePath}" INPUT "${imhex_patterns_SOURCE_DIR}/${relativePath}") + endforeach() + else() set(PATTERNS_FOLDERS_TO_INSTALL constants encodings includes patterns magic nodes) foreach (FOLDER ${PATTERNS_FOLDERS_TO_INSTALL}) - install(DIRECTORY "${imhex_patterns_SOURCE_DIR}/${FOLDER}" DESTINATION ${dest} PATTERN "**/_schema.json" EXCLUDE) + install(DIRECTORY "${imhex_patterns_SOURCE_DIR}/${FOLDER}" DESTINATION "${dest}" PATTERN "**/_schema.json" EXCLUDE) endforeach () endif () diff --git a/cmake/ide_helpers.cmake b/cmake/ide_helpers.cmake new file mode 100644 index 000000000..f908dddfe --- /dev/null +++ b/cmake/ide_helpers.cmake @@ -0,0 +1,149 @@ + +option(IMHEX_IDE_HELPERS_OVERRIDE_XCODE_COMPILER "Enable choice of compiler for Xcode builds, despite CMake's best efforts" OFF) +option(IMHEX_IDE_HELPERS_INTRUSIVE_IDE_TWEAKS "Enable intrusive CMake tweaks to better support IDEs with folder support" OFF) + +# The CMake infrastructure silently ignores the CMAKE_<>_COMPILER settings when +# using the `Xcode` generator. +# +# A particularly nasty (and potentially only) way of getting around this is to +# temporarily lie about the generator being used, while CMake determines and +# locks in the compiler to use. +# +# Needless to say, this is hacky and fragile. Use at your own risk! +if (IMHEX_IDE_HELPERS_OVERRIDE_XCODE_COMPILER AND CMAKE_GENERATOR STREQUAL "Xcode") + set(CMAKE_GENERATOR "Unknown") + enable_language(C CXX) + + set(CMAKE_GENERATOR "Xcode") + + set(CMAKE_XCODE_ATTRIBUTE_CC "${CMAKE_C_COMPILER}") + set(CMAKE_XCODE_ATTRIBUTE_CXX "${CMAKE_CXX_COMPILER}") + + if (CLANG) + set(CMAKE_XCODE_ATTRIBUTE_LD "${CMAKE_C_COMPILER}") + set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS "${CMAKE_CXX_COMPILER}") + endif() + + # By default Xcode passes a `-index-store-path=<...>` parameter to the compiler + # during builds to build code completion indexes. This is not supported by + # anything other than AppleClang + set(CMAKE_XCODE_ATTRIBUTE_COMPILER_INDEX_STORE_ENABLE "NO") +endif() + +# Generate a launch/build scheme for all targets +set(CMAKE_XCODE_GENERATE_SCHEME YES) + +# Utility function that helps avoid messing with non-standard targets +macro(returnIfTargetIsNonTweakable target) + get_target_property(targetIsAliased ${target} ALIASED_TARGET) + get_target_property(targetIsImported ${target} IMPORTED) + + if (targetIsAliased OR targetIsImported) + return() + endif() + + get_target_property(targetType ${target} TYPE) + if (targetType MATCHES "INTERFACE_LIBRARY|UNKNOWN_LIBRARY") + return() + endif() +endmacro() + +# Targets usually don't specify their private headers, nor group their source files +# which results in very spotty coverage by IDEs with folders support +# +# Unfortunately, CMake does not have a `target_source_group` like construct yet, therefore +# we have to play by the limitations of `source_group`. +# +# A particularly problematic part is that the function must be called within the directoryies +# scope for the grouping to take effect. +# +# See: https://discourse.cmake.org/t/topic/7388 +function(tweakTargetForIDESupport target) + returnIfTargetIsNonTweakable(${target}) + + # Don't assume directory structure of third parties + get_target_property(targetSourceDir ${target} SOURCE_DIR) + if (targetSourceDir MATCHES "third_party") + return() + endif() + + # Add headers to target + get_target_property(targetSourceDir ${target} SOURCE_DIR) + if (targetSourceDir) + file(GLOB_RECURSE targetPrivateHeaders CONFIGURE_DEPENDS "${targetSourceDir}/include/*.hpp") + + target_sources(${target} PRIVATE "${targetPrivateHeaders}") + endif() + + # Organize target sources into directory tree + get_target_property(sources ${target} SOURCES) + foreach(file IN LISTS sources) + get_filename_component(path "${file}" ABSOLUTE) + + if (NOT path MATCHES "^${targetSourceDir}") + continue() + endif() + + source_group(TREE "${targetSourceDir}" PREFIX "Source Tree" FILES "${file}") + endforeach() +endfunction() + +if (IMHEX_IDE_HELPERS_INTRUSIVE_IDE_TWEAKS) + # See tweakTargetForIDESupport for rationale + + function(add_library target) + _add_library(${target} ${ARGN}) + + tweakTargetForIDESupport(${target}) + endfunction() + + function(add_executable target) + _add_executable(${target} ${ARGN}) + + tweakTargetForIDESupport(${target}) + endfunction() +endif() + +# Adjust target's FOLDER property, which is an IDE only preference +function(_tweakTarget target path) + get_target_property(targetType ${target} TYPE) + + if (TARGET generator-${target}) + set_target_properties(generator-${target} PROPERTIES FOLDER "romfs/${target}") + endif() + if (TARGET romfs_file_packer-${target}) + set_target_properties(romfs_file_packer-${target} PROPERTIES FOLDER "romfs/${target}") + endif() + if (TARGET libromfs-${target}) + set_target_properties(libromfs-${target} PROPERTIES FOLDER "romfs/${target}") + endif() + + if (${targetType} MATCHES "EXECUTABLE|LIBRARY") + set_target_properties(${target} PROPERTIES FOLDER "${path}") + endif() +endfunction() + +macro(_tweakTargetsRecursive dir) + get_property(subdirectories DIRECTORY ${dir} PROPERTY SUBDIRECTORIES) + foreach(subdir IN LISTS subdirectories) + _tweakTargetsRecursive("${subdir}") + endforeach() + + if(${dir} STREQUAL ${CMAKE_SOURCE_DIR}) + return() + endif() + + get_property(targets DIRECTORY "${dir}" PROPERTY BUILDSYSTEM_TARGETS) + file(RELATIVE_PATH rdir ${CMAKE_SOURCE_DIR} "${dir}/..") + + foreach(target ${targets}) + _tweakTarget(${target} "${rdir}") + endforeach() +endmacro() + +# Tweak all targets this CMake build is aware about +function(tweakTargetsForIDESupport) + set_property(GLOBAL PROPERTY USE_FOLDERS ON) + + _tweakTargetsRecursive("${CMAKE_SOURCE_DIR}") +endfunction() diff --git a/cmake/modules/ImHexPlugin.cmake b/cmake/modules/ImHexPlugin.cmake index 62b688cde..f176c6e70 100644 --- a/cmake/modules/ImHexPlugin.cmake +++ b/cmake/modules/ImHexPlugin.cmake @@ -55,7 +55,7 @@ macro(add_imhex_plugin) # Configure build properties set_target_properties(${IMHEX_PLUGIN_NAME} PROPERTIES - RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins + RUNTIME_OUTPUT_DIRECTORY "${IMHEX_MAIN_OUTPUT_DIRECTORY}/plugins" CXX_STANDARD 23 PREFIX "" SUFFIX ${IMHEX_PLUGIN_SUFFIX} diff --git a/main/gui/CMakeLists.txt b/main/gui/CMakeLists.txt index 35c0876e3..65a6d26b6 100644 --- a/main/gui/CMakeLists.txt +++ b/main/gui/CMakeLists.txt @@ -51,8 +51,9 @@ if (EMSCRIPTEN) endif () set_target_properties(main PROPERTIES - OUTPUT_NAME ${IMHEX_APPLICATION_NAME} - RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/../..) + OUTPUT_NAME "${IMHEX_APPLICATION_NAME}" + RUNTIME_OUTPUT_DIRECTORY "${IMHEX_MAIN_OUTPUT_DIRECTORY}" +) target_compile_definitions(main PRIVATE IMHEX_PROJECT_NAME="${PROJECT_NAME}") @@ -67,4 +68,4 @@ precompileHeaders(main ${CMAKE_CURRENT_SOURCE_DIR}/include) if (APPLE) add_compile_definitions(GL_SILENCE_DEPRECATION) -endif () \ No newline at end of file +endif ()