Swift package (PackageDescription)

Is it possible to create a Swift package project (i.e. as can be created by swift package init ...) using CMake?

The example repository by swiftlang on GitHub doesn’t contain this as an example. And there is no issue that addresses this either (one issue that mentions packages, seems to be about using the binary in an external package project, not creating a package project from CMake).
https://github.com/swiftlang/swift-cmake-examples

I tried compiling a .swift file that requires dependencies specified in a Package.swift file that is in the source folder next to the CMakeLists.txt. As I added the CMakeLists.txt to an existing Swift package project. This didn’t work. Errors about the import statements in the .swift source files are output, that those packages cannot be found.

I also tried adding the Package.swift to the add_executable command.

add_executable(Example Package.swift ${sources})

… but that didn’t work either. The PackageDescription module can also not be found. I was also not sure what adding that file might even do, if it would try to build the project twice (once via CMake and once via the Package.swift file). But it didn’t work.

It of course works to use add_custom_target. And in my case the .build folder is also the CMAKE_CURRENT_BINARY_DIR. Which seems to not conflict with CMake. But not sure if it might. And it is also error-prone. As ${sources} is merely for telling CMake when to run. So in this case a file(GLOB ...) would be required.

add_custom_target(Example ALL
    COMMAND swift build
    DEPENDS ${sources}
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})

The build folder for swift can be modified using --scratch-path. So it no longer conflicts.

Update:

I misunderstood that add_custom_target doesn’t require DEPENDS to trigger, it always triggers.

I cannot use add_custom_command, as it would require file(REAL_PATH ...) for a generator expression path I think?: $<IF:$<CONFIG:Debug>,debug,release> (release and debug are symlinks created by swift).

So instead, I now have

add_custom_target(_Example ALL
    COMMAND swift build --scratch-path "${CMAKE_CURRENT_BINARY_DIR}/swift"
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
    VERBATIM
)

add_executable(Example IMPORTED)
set_target_properties(Example
    PROPERTIES
    IMPORTED_LOCATION_Debug "${CMAKE_CURRENT_BINARY_DIR}/swift/debug"
    IMPORTED_LOCATION_Release "${CMAKE_CURRENT_BINARY_DIR}/swift/release"
)

Using generator Ninja Multi-Config

I happened to notice inside of one of the repositories swift copies to .build/checkouts, that CMakeLists.txt files are present. Which seem to also use external packages?

This one from swift-subprocess:

find_package(SwiftSystem QUIET)
if(NOT SwiftSystem_FOUND)
  message("-- Vendoring swift-system")
  FetchContent_Declare(SwiftSystem
    GIT_REPOSITORY https://github.com/apple/swift-system.git
    GIT_TAG a34201439c74b53f0fd71ef11741af7e7caf01e1 # 1.4.2
    GIT_SHALLOW YES)
  list(APPEND VendoredDependencies SwiftSystem)
endif()

Is that how to use Swift packages?

Update: Some dependencies don’t have CMakeLists.txt files. So this approach won’t suffice.

Some more progress, discovered instead of using a generator expression that relies on swift’s generated symbolic links. You can also use --show-bin-path to get the absolute path:

execute_process(
    COMMAND swift build --scratch-path "${CMAKE_CURRENT_BINARY_DIR}/swift" -c release --show-bin-path
    OUTPUT_VARIABLE swiftBinaryPath
)

I did not know that set_target_properties requires the specified config to be capitalized1. So the above did not work. I now have the default IMPORTED_LOCATION set to the release folder, and otherwise IMPORTED_LOCATION_DEBUG set to the debug folder. And also added the file name at the end of the IMPORTED_LOCATION[_...] values, e.g. example${CMAKE_EXECUTABLE_SUFFIX}.

I have also added USES_TERMINAL to add_custom_target, to see the output of swift. As swift (with several dependencies specific in Package.swift) seems to always take a long time to compile, even if nothing has changed.

I’ve also added some install code:

install(CODE "
file(COPY \"\$<TARGET_FILE:${executableTgt}>\" DESTINATION \"\${CMAKE_CURRENT_SOURCE_DIR}/.artifacts\")
# https://cmake.org/cmake/help/latest/variable/CMAKE_SYSTEM_NAME.html
# Swift also generates one folder ending with .resources
if(${CMAKE_SYSTEM_NAME} STREQUAL \"Windows\")
    file(
        GLOB sharedLibraries
        LIST_DIRECTORIES false
        \"\$<TARGET_FILE_DIR:${executableTgt}>/*.dll\"
    )
    file(
        GLOB sharedLibraryFolders
        LIST_DIRECTORIES true
        \"\$<TARGET_FILE_DIR:${executableTgt}>/*.resources/\"
    )
    #file(COPY \"\${sharedLibraries}\" DESTINATION \"\${CMAKE_CURRENT_SOURCE_DIR}/.artifacts\")
    foreach(sharedLibraryItem \${sharedLibraries} \${sharedLibraryFolders})
        file(COPY \"\${sharedLibraryItem}\" DESTINATION \"\${CMAKE_CURRENT_SOURCE_DIR}/.artifacts\")
    endforeach()
endif()
")

I tried CMAKE_SYSTEM_NAME without ${} at first, but it seems that at install time this variable is empty.

There seem to be a few documentation issues. One of which being the capitalization rules of IMPORTED_LOCAITON_ not being documented on add_target_properties.

Another one is that file(COPY ...) according to the documentation takes a list of files.

file(<COPY|INSTALL> <files>...

Source

But I got an error about a single file containing ; characters. So it seems this doesn’t work. Hence the foreach.