# ============================================================================ # dotnet.cmake - CMake functions for building .NET projects with Roslyn csc # # Provides add_dotnet_library, add_dotnet_executable, target_link_dotnet_libraries, # target_link_dotnet_system_references, target_link_dotnet_directories, 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) # target_link_dotnet_system_references(MyApp System.Linq System.Net.Http) # target_link_dotnet_directories(MyApp /path/to/extra/libs) # # 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() function(target_link_dotnet_system_references TARGET) cmake_parse_arguments(PARSE_ARGV 1 arg "" "" "${ARGN}") get_property(ALL_TARGETS GLOBAL PROPERTY DOTNET_TARGETS) list(FIND ALL_TARGETS "${TARGET}" _idx) if(_idx EQUAL -1) message(FATAL_ERROR "target_link_dotnet_system_references: '${TARGET}' is not a dotnet target") endif() foreach(ref IN LISTS arg_UNPARSED_ARGUMENTS) get_property(CURRENT_REFS GLOBAL PROPERTY DOTNET_TARGET_${TARGET}_SYSTEM_REFS) if(NOT CURRENT_REFS) set(CURRENT_REFS "") endif() list(APPEND CURRENT_REFS "-reference:${ref}.dll") set_property(GLOBAL PROPERTY DOTNET_TARGET_${TARGET}_SYSTEM_REFS "${CURRENT_REFS}") endforeach() endfunction() function(target_link_dotnet_directories TARGET) cmake_parse_arguments(PARSE_ARGV 1 arg "" "" "${ARGN}") get_property(ALL_TARGETS GLOBAL PROPERTY DOTNET_TARGETS) list(FIND ALL_TARGETS "${TARGET}" _idx) if(_idx EQUAL -1) message(FATAL_ERROR "target_link_dotnet_directories: '${TARGET}' is not a dotnet target") endif() foreach(dir IN LISTS arg_UNPARSED_ARGUMENTS) get_property(CURRENT_DIRS GLOBAL PROPERTY DOTNET_TARGET_${TARGET}_LIB_DIRS) if(NOT CURRENT_DIRS) set(CURRENT_DIRS "") endif() list(APPEND CURRENT_DIRS "-lib:${dir}") set_property(GLOBAL PROPERTY DOTNET_TARGET_${TARGET}_LIB_DIRS "${CURRENT_DIRS}") 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) get_property(SYSTEM_REFS GLOBAL PROPERTY DOTNET_TARGET_${NAME}_SYSTEM_REFS) get_property(LIB_DIRS GLOBAL PROPERTY DOTNET_TARGET_${NAME}_LIB_DIRS) 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} ${SYSTEM_REFS} -out:${OUTPUT_FILE} -lib:${RUNTIME_DIR} ${LIB_DIRS} -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()