This is because you’re confusing generate-time logic with configure-time logic. Here’s a breakdown of what’s happening for a hypothetical target named foo:
# Plain message
set(OUT_TMP "====dump_target_includes begin (foo)====")
# Plain variable settings to strings. They *use* genex syntax, but they're just strings at this point.
set(INCLUDE_DIRS $<TARGET_PROPERTY:foo,INCLUDE_DIRECTORIES>)
set(INTERFACE_INCLUDE_DIRS $<TARGET_PROPERTY:foo,INTERFACE_INCLUDE_DIRECTORIES>)
# You really just want this instead of what you have because otherwise you'll end up duplicating `OUT_TMP` content quadratically:
string(CONCAT OUT_TMP "\n\n----INCLUDE_DIRECTORIES----")
# Loop over `INCLUDE_DIRS` as a list. This list has one element, `$<TARGET_PROPERTY:foo,INCLUDE_DIRECTORIES>`
foreach(PATH ${INCLUDE_DIRS})
# Appends `\n$<TARGET_PROPERTY:foo,INCLUDE_DIRECTORIES>` to `OUT_TMP`
string(CONCAT OUT_TMP "\n${PATH}")
endforeach()
# Same as above
string(CONCAT OUT_TMP "\n\n----INTERFACE_INCLUDE_DIRECTORIES----")
foreach(PATH ${INTERFACE_INCLUDE_DIRS})
string(CONCAT OUT_TMP "\n${PATH}")
endforeach()
string(CONCAT OUT_TMP "\n\n====dump_target_includes end (foo)====")
# Here, `OUT_TMP` contains this content:
[==[
====dump_target_includes begin (foo)====
----INCLUDE_DIRECTORIES----
$<TARGET_PROPERTY:foo,INCLUDE_DIRECTORIES>
----INTERFACE_INCLUDE_DIRECTORIES----
$<TARGET_PROPERTY:foo,INTERFACE_INCLUDE_DIRECTORIES>
====dump_target_includes end (foo)====
]==]
file(GENERATE OUTPUT test_debug_genex.log
CONTENT ${OUT_TMP})
This final result of OUT_TMP doesn’t have anything about the configure-time foreach loops in them because the value of $<TARGET_PROPERTY> is not known at this time. What you want is to use $<JOIN:\n,$<TARGET_PROPERTY:…>> to intersperse \n characters in between each element.