build(cmake): add linker script helper
Like preprocessing, CMake offers no official way to apply custom linker
scripts to executables. This commit adds a helper function to apply a
linker script to a given binary target, optionally preprocessing it
beforehand.
Change-Id: I6ed09720a8f768cf5330171365eb10f9a4c5057c
Signed-off-by: Chris Kay <chris.kay@arm.com>
diff --git a/cmake/Modules/ArmTargetLinkerScript.cmake b/cmake/Modules/ArmTargetLinkerScript.cmake
new file mode 100644
index 0000000..6a955c8
--- /dev/null
+++ b/cmake/Modules/ArmTargetLinkerScript.cmake
@@ -0,0 +1,187 @@
+#[=======================================================================[.rst:
+ArmTargetLinkerScript
+---------------------
+
+.. default-domain:: cmake
+
+.. command:: arm_target_linker_script
+
+Set the linker script for a target.
+
+.. code-block:: cmake
+
+ arm_target_linker_script(
+ TARGET <target> SCRIPT <script>
+ [PREPROCESSOR SUBTARGET <subtarget> LANGUAGE <language>])
+
+Sets the linker script for the target ``<target>`` to the script ``<script>``,
+which is optionally first preprocessed with the preprocessor for the language
+given by ``<language>``, which creates the target ``<subtarget>``.
+
+When preprocessing, the following properties are automatically inherited from
+the target ``<target>`` and may also be set on the sub-target ``<subtarget>`` in
+order to pass additional information to the preprocessor:
+
+ - :prop_tgt:`COMPILE_OPTIONS <prop_tgt:COMPILE_OPTIONS>`
+ - :prop_tgt:`COMPILE_DEFINITIONS <prop_tgt:COMPILE_DEFINITIONS>`
+ - :prop_tgt:`INCLUDE_DIRECTORIES <prop_tgt:INCLUDE_DIRECTORIES>`
+
+Additionally, the linker script automatically inherits flags from both
+:variable:`CMAKE_<LANG>_FLAGS <variable:CMAKE_<LANG>_FLAGS>` and
+:variable:`CMAKE_<LANG>_FLAGS_<CONFIG> <variable:CMAKE_<LANG>_FLAGS_<CONFIG>>`.
+
+.. code-block:: cmake
+ :caption: Usage example
+ :linenos:
+
+ add_executable(my-executable "main.c")
+
+ arm_target_linker_script(
+ TARGET my-executable SCRIPT "linker.ld"
+ PREPROCESSOR TARGET my-executable-lds LANGUAGE C)
+
+ set_property(
+ TARGET my-executable-lds APPEND
+ PROPERTY COMPILE_DEFINITIONS "LINKER=1")
+#]=======================================================================]
+
+include_guard()
+
+include(ArmAssert)
+include(ArmPreprocessSource)
+
+function(arm_target_linker_script)
+ set(options "")
+ set(single-args "TARGET;SCRIPT")
+ set(multi-args "PREPROCESSOR")
+
+ cmake_parse_arguments(PARSE_ARGV 0 ARG
+ "${options}" "${single-args}" "${multi-args}")
+
+ arm_assert(
+ CONDITION DEFINED ARG_TARGET
+ MESSAGE "No value was given for the `TARGET` argument.")
+
+ arm_assert(
+ CONDITION DEFINED ARG_SCRIPT
+ MESSAGE "No value was given for the `SCRIPT` argument.")
+
+ cmake_path(ABSOLUTE_PATH ARG_SCRIPT NORMALIZE
+ BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
+
+ if(DEFINED ARG_PREPROCESSOR)
+ _arm_target_linker_script_preprocess(${ARG_PREPROCESSOR}
+ TARGET "${ARG_TARGET}" SCRIPT "${ARG_SCRIPT}"
+ OUTPUT ARG_SCRIPT)
+ endif()
+
+ get_target_property(language "${ARG_TARGET}" LINKER_LANGUAGE)
+
+ if(CMAKE_${language}_COMPILER_ID STREQUAL "ARMClang")
+ target_link_options("${ARG_TARGET}"
+ PUBLIC "LINKER:--scatter" "LINKER:${ARG_SCRIPT}")
+ else()
+ target_link_options("${ARG_TARGET}"
+ PUBLIC "LINKER:-T" "LINKER:${ARG_SCRIPT}")
+ endif()
+endfunction()
+
+function(_arm_target_linker_script_preprocess)
+ set(options "")
+ set(single-args "TARGET;SCRIPT;OUTPUT;SUBTARGET;LANGUAGE")
+
+ cmake_parse_arguments(PARSE_ARGV 0 ARG
+ "${options}" "${single-args}" "${multi-args}")
+
+ arm_assert(
+ CONDITION (DEFINED ARG_SUBTARGET) AND
+ (DEFINED ARG_LANGUAGE)
+ MESSAGE "The preprocessor `SUBTARGET` and `LANGUAGE` arguments must "
+ "both be provided when preprocessing.")
+
+ _arm_target_linker_script_preprocess_path(
+ TARGET "${ARG_SUBTARGET}" SCRIPT "${ARG_SCRIPT}"
+ OUTPUT path)
+
+ arm_preprocess_source(
+ TARGET "${ARG_SUBTARGET}" LANGUAGE "${ARG_LANGUAGE}"
+ SOURCE "${ARG_SCRIPT}" OUTPUT "${path}"
+ INHIBIT_LINEMARKERS)
+
+ set(compile-options "$<TARGET_PROPERTY:${ARG_TARGET},COMPILE_OPTIONS>")
+ set(compile-definitions "$<TARGET_PROPERTY:${ARG_TARGET},COMPILE_DEFINITIONS>")
+ set(include-directories "$<TARGET_PROPERTY:${ARG_TARGET},INCLUDE_DIRECTORIES>")
+
+ foreach(config IN LISTS CMAKE_BUILD_TYPE CMAKE_CONFIGURATION_TYPES)
+ string(TOUPPER "${config}" config)
+
+ separate_arguments(config-compile-options
+ NATIVE_COMMAND "${CMAKE_${preprocessor-language}_FLAGS_${config}}")
+ list(PREPEND compile-options
+ "$<$<CONFIG:${config}>:${config-compile-options}>")
+ endforeach()
+
+ separate_arguments(global-compile-options
+ NATIVE_COMMAND "${CMAKE_${preprocessor-language}_FLAGS}")
+ list(PREPEND compile-options "${global-compile-options}")
+
+ set_target_properties("${ARG_SUBTARGET}"
+ PROPERTIES COMPILE_OPTIONS "${compile-options}"
+ COMPILE_DEFINITIONS "${compile-definitions}"
+ INCLUDE_DIRECTORIES "${include-directories}")
+
+ add_dependencies(${ARG_TARGET} "${ARG_SUBTARGET}")
+
+ set(${ARG_OUTPUT} "${path}" PARENT_SCOPE)
+endfunction()
+
+function(_arm_target_linker_script_preprocess_path result target script)
+ set(options "")
+ set(single-args "OUTPUT;TARGET;SCRIPT")
+ set(multi-args "")
+
+ cmake_parse_arguments(PARSE_ARGV 0 ARG
+ "${options}" "${single-args}" "${multi-args}")
+
+ #
+ # Figure out where we're going to place our preprocessed file. This depends
+ # on whether we're using a multi-config generator or not:
+ #
+ # - Single-config: CMakeFiles/${subtarget}.dir
+ # - Multi-config: CMakeFiles/${subtarget}.dir/$<CONFIG>
+ #
+
+ get_property(multi-config GLOBAL
+ PROPERTY GENERATOR_IS_MULTI_CONFIG)
+
+ set(path "${CMAKE_CURRENT_BINARY_DIR}")
+
+ cmake_path(APPEND_STRING path "${CMAKE_FILES_DIRECTORY}")
+ cmake_path(APPEND path "${ARG_TARGET}.dir")
+
+ if(multi-config)
+ cmake_path(APPEND path "$<CONFIG>")
+ endif()
+
+ #
+ # Try to mirror the behaviour of CMake when deciding the relativized path
+ # for the preprocessed file. If the source file is a child of the current
+ # source directory we use its path relative to that, but otherwise we take
+ # its relative path part. As an example:
+ #
+ # - ${CMAKE_CURRENT_SOURCE_DIR}/foo/bar.c -> foo/bar.c.i
+ # - C:/foo/bar.c -> foo/bar.c.i
+ #
+
+ cmake_path(IS_PREFIX CMAKE_CURRENT_SOURCE_DIR "${ARG_SCRIPT}" is-child)
+
+ if(is-child)
+ cmake_path(RELATIVE_PATH ARG_SCRIPT OUTPUT_VARIABLE relative-script)
+ else()
+ cmake_path(GET ARG_SCRIPT RELATIVE_PART relative-script)
+ endif()
+
+ cmake_path(APPEND path "${relative-script}.i")
+
+ set(${ARG_OUTPUT} "${path}" PARENT_SCOPE)
+endfunction()