summaryrefslogtreecommitdiffstats
path: root/cmake/dotnet.cmake
blob: 8782659ad540e96f6462f96c417959f85a03804f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# ============================================================================
# dotnet.cmake - CMake functions for building .NET projects with Roslyn csc
#
# Provides add_dotnet_library, add_dotnet_executable, target_link_dotnet_libraries,
# and dotnet_finalize_targets for building .NET assemblies using the Roslyn
# compiler bundled with the .NET SDK.
#
# Usage pattern:
#
#   add_dotnet_library(MyLib SOURCES MyLib.cs)
#
#   add_dotnet_executable(MyApp MAIN Program SOURCES Program.cs)
#   target_link_dotnet_libraries(MyApp PRIVATE MyLib)
#
#   dotnet_finalize_targets()   # Must be called after all targets are defined
#
# WHY dotnet_finalize_targets()?
# --------------------------------
# CMake's native target_link_libraries works because the build system generator
# (Ninja, Make, VS) handles dependency tracking at a lower level — it
# automatically adds a linked library's output file as an input to the
# dependent target's link step, so Ninja/Make know to rebuild when the
# dependency changes.
#
# We use add_custom_command(OUTPUT ...) to compile .NET assemblies with csc.
# For Ninja to rebuild a target when its dependency's .dll changes, that .dll
# must appear in the DEPENDS list of the custom command. But DEPENDS is fixed
# at creation time — we cannot append to it later.
#
# Since target_link_dotnet_libraries is called AFTER add_dotnet_library/
# add_dotnet_executable, we don't know all dependencies when the custom
# command is created. The solution: defer custom command creation to
# dotnet_finalize_targets(), which runs after all linking is configured and
# knows the full dependency graph.
# ============================================================================

function(add_dotnet_library NAME)
    set(multiValueArgs SOURCES REFERENCES)

    cmake_parse_arguments(PARSE_ARGV 1 arg
        "" "" "${multiValueArgs}"
    )

    if(NOT arg_SOURCES)
        message(FATAL_ERROR "add_dotnet_library: SOURCES is required")
    endif()

    set_property(GLOBAL APPEND PROPERTY DOTNET_TARGETS "${NAME}")

    set(CSC_FLAGS -target:library)
    foreach(ref IN LISTS arg_REFERENCES)
        list(APPEND CSC_FLAGS "-reference:${ref}")
    endforeach()

    set_property(GLOBAL PROPERTY DOTNET_TARGET_${NAME}_TYPE "library")
    set_property(GLOBAL PROPERTY DOTNET_TARGET_${NAME}_SOURCES "${arg_SOURCES}")
    set_property(GLOBAL PROPERTY DOTNET_TARGET_${NAME}_CSC_FLAGS "${CSC_FLAGS}")
endfunction()


function(add_dotnet_executable NAME)
    set(oneValueArgs MAIN)
    set(multiValueArgs SOURCES REFERENCES)

    cmake_parse_arguments(PARSE_ARGV 1 arg
        "" "${oneValueArgs}" "${multiValueArgs}"
    )

    if(NOT arg_SOURCES)
        message(FATAL_ERROR "add_dotnet_executable: SOURCES is required")
    endif()

    set_property(GLOBAL APPEND PROPERTY DOTNET_TARGETS "${NAME}")

    if(NOT arg_MAIN)
        set(arg_MAIN "Main")
    endif()

    set(CSC_FLAGS -target:exe -main:${arg_MAIN})
    foreach(ref IN LISTS arg_REFERENCES)
        list(APPEND CSC_FLAGS "-reference:${ref}")
    endforeach()

    configure_file(
        ${CMAKE_SOURCE_DIR}/cmake/runtimeconfig.json.in
        ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${NAME}.runtimeconfig.json
        COPYONLY
    )

    set_property(GLOBAL PROPERTY DOTNET_TARGET_${NAME}_TYPE "executable")
    set_property(GLOBAL PROPERTY DOTNET_TARGET_${NAME}_SOURCES "${arg_SOURCES}")
    set_property(GLOBAL PROPERTY DOTNET_TARGET_${NAME}_CSC_FLAGS "${CSC_FLAGS}")
endfunction()


function(target_link_dotnet_libraries TARGET)
    set(multiValueArgs PUBLIC PRIVATE INTERFACE NATIVE_DEPS)

    cmake_parse_arguments(PARSE_ARGV 1 arg
        "" "" "${multiValueArgs}"
    )

    get_property(ALL_TARGETS GLOBAL PROPERTY DOTNET_TARGETS)
    list(FIND ALL_TARGETS "${TARGET}" _idx)
    if(_idx EQUAL -1)
        message(FATAL_ERROR "target_link_dotnet_libraries: '${TARGET}' is not a dotnet target")
    endif()

    foreach(scope PUBLIC PRIVATE INTERFACE)
        foreach(lib IN LISTS arg_${scope})
            list(FIND ALL_TARGETS "${lib}" _idx)
            if(_idx EQUAL -1)
                message(FATAL_ERROR "target_link_dotnet_libraries: '${lib}' is not a dotnet target")
            endif()
        endforeach()
    endforeach()

    foreach(dep IN LISTS arg_NATIVE_DEPS)
        get_property(CURRENT_NDEPS GLOBAL PROPERTY DOTNET_TARGET_${TARGET}_NATIVE_DEPS)
        if(NOT CURRENT_NDEPS)
            set(CURRENT_NDEPS "")
        endif()
        list(APPEND CURRENT_NDEPS ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/lib${dep}.so)
        set_property(GLOBAL PROPERTY DOTNET_TARGET_${TARGET}_NATIVE_DEPS "${CURRENT_NDEPS}")

        get_property(CURRENT_DEP_TARGETS GLOBAL PROPERTY DOTNET_TARGET_${TARGET}_DEP_TARGETS)
        if(NOT CURRENT_DEP_TARGETS)
            set(CURRENT_DEP_TARGETS "")
        endif()
        list(APPEND CURRENT_DEP_TARGETS ${dep})
        set_property(GLOBAL PROPERTY DOTNET_TARGET_${TARGET}_DEP_TARGETS "${CURRENT_DEP_TARGETS}")
    endforeach()

    foreach(scope PUBLIC PRIVATE INTERFACE)
        set(libs "${arg_${scope}}")
        if(NOT libs)
            continue()
        endif()

        foreach(lib IN LISTS libs)
            get_property(CURRENT_DEPS GLOBAL PROPERTY DOTNET_TARGET_${TARGET}_DOTNET_DEPS)
            if(NOT CURRENT_DEPS)
                set(CURRENT_DEPS "")
            endif()
            list(APPEND CURRENT_DEPS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${lib}.dll)
            set_property(GLOBAL PROPERTY DOTNET_TARGET_${TARGET}_DOTNET_DEPS "${CURRENT_DEPS}")

            get_property(CURRENT_DEP_TARGETS GLOBAL PROPERTY DOTNET_TARGET_${TARGET}_DEP_TARGETS)
            if(NOT CURRENT_DEP_TARGETS)
                set(CURRENT_DEP_TARGETS "")
            endif()
            list(APPEND CURRENT_DEP_TARGETS ${lib})
            set_property(GLOBAL PROPERTY DOTNET_TARGET_${TARGET}_DEP_TARGETS "${CURRENT_DEP_TARGETS}")

            get_property(CURRENT_ALL_REFS GLOBAL PROPERTY DOTNET_TARGET_${TARGET}_ALL_REFS)
            if(NOT CURRENT_ALL_REFS)
                set(CURRENT_ALL_REFS "")
            endif()
            list(APPEND CURRENT_ALL_REFS "-reference:${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${lib}.dll")

            get_property(LIB_PUB_REFS GLOBAL PROPERTY DOTNET_TARGET_${lib}_PUB_REFS)
            if(NOT LIB_PUB_REFS)
                set(LIB_PUB_REFS "")
            endif()
            list(APPEND CURRENT_ALL_REFS ${LIB_PUB_REFS})
            set_property(GLOBAL PROPERTY DOTNET_TARGET_${TARGET}_ALL_REFS "${CURRENT_ALL_REFS}")

            if("${scope}" STREQUAL "PUBLIC" OR "${scope}" STREQUAL "INTERFACE")
                get_property(CURRENT_PUB GLOBAL PROPERTY DOTNET_TARGET_${TARGET}_PUB_REFS)
                if(NOT CURRENT_PUB)
                    set(CURRENT_PUB "")
                endif()
                list(APPEND CURRENT_PUB "-reference:${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${lib}.dll")
                list(APPEND CURRENT_PUB ${LIB_PUB_REFS})
                list(REMOVE_DUPLICATES CURRENT_PUB)
                set_property(GLOBAL PROPERTY DOTNET_TARGET_${TARGET}_PUB_REFS "${CURRENT_PUB}")
            endif()
        endforeach()
    endforeach()
endfunction()


# Creates the actual custom commands and targets with full dependency knowledge.
# Must be called after all add_dotnet_library/add_dotnet_executable and
# target_link_dotnet_libraries calls.
function(dotnet_finalize_targets)
    get_property(ALL_TARGETS GLOBAL PROPERTY DOTNET_TARGETS)
    foreach(NAME IN LISTS ALL_TARGETS)
        get_property(TARGET_TYPE GLOBAL PROPERTY DOTNET_TARGET_${NAME}_TYPE)
        get_property(SOURCES GLOBAL PROPERTY DOTNET_TARGET_${NAME}_SOURCES)
        get_property(CSC_FLAGS GLOBAL PROPERTY DOTNET_TARGET_${NAME}_CSC_FLAGS)
        get_property(NATIVE_DEPS GLOBAL PROPERTY DOTNET_TARGET_${NAME}_NATIVE_DEPS)
        get_property(DOTNET_DEPS GLOBAL PROPERTY DOTNET_TARGET_${NAME}_DOTNET_DEPS)
        get_property(DEP_TARGETS GLOBAL PROPERTY DOTNET_TARGET_${NAME}_DEP_TARGETS)
        get_property(ALL_REFS GLOBAL PROPERTY DOTNET_TARGET_${NAME}_ALL_REFS)
        get_property(PUB_REFS GLOBAL PROPERTY DOTNET_TARGET_${NAME}_PUB_REFS)

        set(OUTPUT_FILE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${NAME}.dll)

        set(ALL_DEPS "")
        if(TARGET_TYPE STREQUAL "executable")
            list(APPEND ALL_DEPS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${NAME}.runtimeconfig.json)
        endif()
        if(NATIVE_DEPS)
            list(APPEND ALL_DEPS ${NATIVE_DEPS})
        endif()
        if(DOTNET_DEPS)
            list(APPEND ALL_DEPS ${DOTNET_DEPS})
        endif()

        add_custom_command(
            OUTPUT ${OUTPUT_FILE}
            COMMAND dotnet ${CSC_DLL}
                ${CSC_FLAGS}
                ${ALL_REFS}
                -out:${OUTPUT_FILE}
                -lib:${RUNTIME_DIR}
                -reference:System.Private.CoreLib.dll
                -reference:System.Runtime.dll
                -reference:System.Console.dll
                ${SOURCES}
            DEPENDS ${SOURCES} ${ALL_DEPS}
            COMMENT "Compiling ${NAME} with Roslyn csc..."
        )

        add_custom_target(${NAME} ALL DEPENDS ${OUTPUT_FILE})

        foreach(dep IN LISTS DEP_TARGETS)
            add_dependencies(${NAME} ${dep})
        endforeach()

        set_target_properties(${NAME} PROPERTIES
            DOTNET_ALL_REFS "${ALL_REFS}"
            DOTNET_PUBLIC_REFS "${PUB_REFS}"
        )
    endforeach()
endfunction()