How to run extra install code, after ninja install installs all its configurations in a multi-config setup

Given the following project:

cmake_minimum_required(VERSION 3.18)

set(CMAKE_CROSS_CONFIGS "all" CACHE STRING "" FORCE)
set(CMAKE_DEFAULT_CONFIGS "all" CACHE STRING "" FORCE)
set(CMAKE_INSTALL_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/installed" CACHE STRING "" FORCE)
project(test_nmc_install)

set(source_file "${CMAKE_CURRENT_BINARY_DIR}/foo-$<CONFIG>.txt")
file(GENERATE OUTPUT "${source_file}" CONTENT "$<CONFIG>")

install(FILES "${source_file}" DESTINATION ".")

install(CODE "message(STATUS \"I should run after all configs of foo.txt are installed\")")

configured with

cmake .. -DCMAKE_INSTALL_PREFIX=$PWD/installed '-GNinja Multi-Config'

I’d like to run the install(CODE) after ninja installs all configurations of the foo.txt file.
Currently it is run once for each config.

$ ninja install                                                                                                                                                                                                                                                       130 ↵
[0/1] Re-running CMake...
-- Configuring done (0.0s)
-- Generating done (0.0s)
-- Build files have been written to: /Users/alex/Dev/projects/cmake/general/multi_ninja_install_all_after_code/build
[0/3] Install the project...
-- Install configuration: "Debug"
-- Up-to-date: /Users/alex/Dev/projects/cmake/general/multi_ninja_install_all_after_code/build/installed/./foo-Debug.txt
-- I should run after all configs of foo.txt are installed
[1/3] Install the project...
-- Install configuration: "Release"
-- Up-to-date: /Users/alex/Dev/projects/cmake/general/multi_ninja_install_all_after_code/build/installed/./foo-Release.txt
-- I should run after all configs of foo.txt are installed
[2/3] Install the project...
-- Install configuration: "RelWithDebInfo"
-- Up-to-date: /Users/alex/Dev/projects/cmake/general/multi_ninja_install_all_after_code/build/installed/./foo-RelWithDebInfo.txt
-- I should run after all configs of foo.txt are installed

The build.ninja file rule for ‘install’ is

build install: phony install$:Debug install$:RelWithDebInfo install$:Release

So the order in which the installation will happen will be arbitrary.

Detecting that some config is the last one to be installed is probably also not feasible, due to installation possibly happening in parallel.

Is there no way then to achieve what I’m aiming for?

Taking a step back, what do you need to do after all other per-config install things have run? Perhaps there’s an alternative way of achieving that goal. I’m thinking here whether something like defining a workflow preset that sequences a predictable set of install steps, which you follow by whatever it is you need to do after those (might need install components to get involved, could be complex, but let’s see what your underlying use case is first).

I want to generate a single spdx sbom file. It will contain things like installed project libraries, copyrights, versions, etc.

In a multi-config build, the spdx file needs to contain an entry for each installed libfoo-$< CONFIG >.so, specifically its install path and sha1 checksum (amont other things)

To compute the checksum, the file needs to be installed first, because it might have its rpath modified, so computing it from the build folder doesn’t make sense.

I can create intermediate install scripts for each config to compute the checksums.
But i still need a final “assemble” install step to actually put that into a single file.

file(APPEND ing) or using configure_file() to replace certain parts of the file incrementally during each config install doesn’t really work, because ninja install can run each config installation in parallel (from what i’ve seen).
At least i’ve noticed file system write racing issues in my trials when doing file(APPEND) to the same file.

Given the constraint of using Ninja Multi-Config only, and that cmake by default puts the install ninja targets into the console pool, which disables parallel installation, i was able to solve this by creating a marker file for each install configuration, and only running the main code after confirming that the full set of ${CMAKE_CONFIGURATION_TYPES} marker files are present.
After the main code is run, the marker files are deleted.

Some bits of code:

    set(install_markers_dir "${some_dir}/install_markers")
    set(install_marker_path "${install_markers_dir}/finished_install-$<CONFIG>.cmake")
    install(CODE "file(WRITE \"${install_marker_path}\" \"\")")

    get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG)
    if(is_multi_config)
        set(configs ${CMAKE_CONFIGURATION_TYPES})
    else()
        set(configs "${CMAKE_BUILD_TYPE}")
    endif()

    set(install_markers "")
    foreach(config IN LISTS configs)
        list(APPEND install_markers "${install_markers_dir}/finished_install-${config}.cmake")
    endforeach()

    set(assemble_sbom_install "
        set(INSTALLED_ALL_CONFIGS TRUE)
        set(INSTALL_MARKERS \"${install_markers}\")
        foreach(INSTALL_MARKER IN LISTS INSTALL_MARKERS)
            if(NOT EXISTS \"\${INSTALL_MARKER}\")
                set(INSTALLED_ALL_CONFIGS FALSE)
            endif()
        endforeach()
        if(INSTALLED_ALL_CONFIGS)
            # main code goes here
            # ...
            foreach(INSTALL_MARKER IN LISTS INSTALL_MARKERS)
                file(REMOVE \"\${INSTALL_MARKER}\")
            endforeach()
        else()
            message(STATUS \"Skipping because not all configs installed yet.\")
        endif()
")

    install(CODE "${assemble_sbom_install}")