cmake-package 0.1.13

A helper library for Cargo build-scripts to find and link against existing CMake packages.
Documentation
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
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
# SPDX-FileCopyrightText: 2024 Daniel Vrátil <dvratil@kde.org>
#
# SPDX-License-Identifier: MIT

#[===================================================================================[.rst
Find Package Cmake Script
--------------------------

This is a helper CMake script that is used to execute find_package() and collect
the results into a JSON file. The JSON file is then read and interpreted by the
Rust code.

This is basically a regular project CMakeLists.txt script (except it has documentation :)),
so it needs to be compiled as CMakeLists.txt into some temporary directory and then invoked
by running `cmake .` in that directory. Some additional arguments must be passed to cmake
in order to specify the package to find, the output file and optionally the version and
components to find.

Parameters
~~~~~~~~~~

``PACKAGE``
  The package name to find (required)
``OUTPUT_FILE``
  The file to write the JSON output to (required)
``VERSION``
  Minimum version of the package to find (optional)
``COMPONENTS``
  Semicolon-separated list of components to find (optional)
``TARGET``
  The target to resolve (optional)

To invoke the script, first copy it into a temporary directory and then run:

.. code-block:: bash

  cmake -DPACKAGE=foo \
        -DVERSION=1.2.3 \
        -DCOMPONENTS=bar;baz \
        -DOUTPUT_FILE=/path/to/output.json
        -B /path/to/tmp/dir/build
        /path/to/tmp/dir

When `TARGET` is not specified, the script will only call ``find_package()`` and write
a JSON file with the package name, discovered version and components. When ``TARGET``
is set, the script will find all the following properties for the target, and also for
recursively for all nested targets referenced by e.g. ``INTERFACE_LINK_LIBRARIES``
target property:

``NAME``
``LOCATION``
``LOCATION_Release``
``LOCATION_RelWithDebInfo``
``LOCATION_MinSizeRel``
``LOCATION_Debug``
``INTERFACE_COMPILE_DEFINITIONS``
``INTERFACE_COMPILE_OPTIONS``
``INTERFACE_INCLUDE_DIRECTORIES``
``INTERFACE_LINK_DIRECTORIES``
``INTERFACE_LINK_LIBRARIES``
``INTERFACE_LINK_OPTIONS``

Note that due to usage of ``find_package()`` it is not possible to run the script in CMake script
mode. It must be run in the standard "configure" mode.

#]===================================================================================]

cmake_minimum_required(VERSION ${CMAKE_MIN_VERSION})
# TODO: Make it possible to disable check for compilers (by passing `LANGUAGES NONE`)
# so that users do not need to have a C/C++ compiler installed.
# C compiler is required by FindThreads.cmake that is often used inside other package
# scripts.
project(cmake-package)


###################################################################################
# Invokes find_package() and writes the result into a JSON file.
# Parameters:
#   PACKAGE: The package name to find (required)
#   OUTPUT_FILE: The file to write the JSON output to (required)
#   VERSION: The minimum version of the package to find (optional)
#   COMPONENTS: The components to find (optional)
#   NAMES: Alternative package names to search for (optional)
###################################################################################
function(find_package_wrapper)
    cmake_parse_arguments(FP "" "PACKAGE;VERSION;OUTPUT_FILE" "COMPONENTS;NAMES" ${ARGN})
    if (NOT FP_PACKAGE)
        message(FATAL_ERROR "PACKAGE is not set")
    endif()
    if (NOT FP_OUTPUT_FILE)
        message(FATAL_ERROR "OUTPUT_FILE is not set")
    endif()

    set(_extra_args)
    if (FP_COMPONENTS)
        list(APPEND _extra_args COMPONENTS ${FP_COMPONENTS})
    endif()
    if (FP_NAMES)
        list(APPEND _extra_args NAMES ${FP_NAMES})
    endif()

    # Don't specify the version here, even if FP_VERSION is set - we want to find the package
    # even if the version is too old in order to be able to return the found version back to
    # the Rust code.
    find_package(${FP_PACKAGE} ${_extra_args})

    # Package found?
    if (${FP_PACKAGE}_FOUND)
        # Write its name into the JSON
        string(JSON json SET "{ }" "name" "\"${FP_PACKAGE}\"")
        # If we also found a version, write its version
        if (${FP_PACKAGE}_VERSION)
            string(JSON json SET ${json} "version" "\"${${FP_PACKAGE}_VERSION}\"")
        endif()
        if (FP_COMPONENTS)
            string(REPLACE ";" "\",\"" component_array "${FP_COMPONENTS}")
            string(JSON json SET ${json} "components" "[\"${component_array}\"]")
        endif()

        file(WRITE ${FP_OUTPUT_FILE} ${json})
    else()
        # If not found, just output an empty JSON object, the rust code will interpret it as not found
        file(WRITE ${FP_OUTPUT_FILE} "{ }")
    endif()
endfunction()

###################################################################################
# For given target and a target property this function resolves the value of the
# property. It checks each value and if the value is in fact another target, it
# calls `resolve_deps_recursively()` on it to obtain all properties of the target,
# otherwise it just keeps the value. Generator expressions are ignored since they
# cannot be resolved at configuration time.
#
# The result is a list of either strings or JSON objects (as a string). It is stored
# in the provided `OUT_VAR` variable.
#
#
# Parameters:
#   TARGET: The target to resolve (required)
#   PROPERTY: The property to resolve (required)
#   OUT_VAR: The variable to store the result in (required)
###################################################################################
function(resolve_target_prop)
    cmake_parse_arguments(ARG "" "TARGET;PROPERTY;OUT_VAR" "" ${ARGN})
    # Read the property value
    get_target_property(prop_values ${ARG_TARGET} ${ARG_PROPERTY})
    # Check each value
    message(STATUS "${ARG_TARGET}: ${ARG_PROPERTY} = ${prop_values}")
    set(result)
    foreach(value ${prop_values})
        # If the value is actually another imported target, then recursive into it and obtain
        # all properties of the target. Don't recurse into ourselves.
        if (TARGET ${value} AND NOT ("${ARG_TARGET}" STREQUAL "${value}"))
            set(var)
            resolve_deps_recursively(TARGET ${value} OUTPUT_JSON var)
            list(APPEND result ${var})
        elseif(value AND NOT ("${value}" MATCHES "^\\$\\<.*")) # Ignore generator expressions
            # Otherwise just append the value to output the list
            list(APPEND result ${value})
        endif()
    endforeach()
    set(${ARG_OUT_VAR} ${result} PARENT_SCOPE)
endfunction()

###################################################################################
# Recursively resolves all properties of the given target and all targets that may
# be referenced by any of the properties of the target (see `resolve_target_prop()`).
# The result is a JSON object with all properties of the target.
#
# Parameters:
#   TARGET: The target to resolve (required)
#   OUTPUT_JSON: The variable to store the result in (required)
###################################################################################
function(resolve_deps_recursively)
    cmake_parse_arguments(ARG "" "TARGET;OUTPUT_JSON" "" ${ARGN})
    set(single_value_props
        NAME
        LOCATION
        IMPORTED_IMPLIB
        IMPORTED_NO_SONAME
    )
    set(multi_value_props
        INTERFACE_COMPILE_DEFINITIONS
        INTERFACE_COMPILE_OPTIONS
        INTERFACE_INCLUDE_DIRECTORIES
        INTERFACE_LINK_DIRECTORIES
        INTERFACE_LINK_LIBRARIES
        INTERFACE_LINK_LIBRARIES_DIRECT
        INTERFACE_LINK_DEPENDENT_LIBRARIES
        INTERFACE_LINK_OPTIONS
    )
    set(cfg_props
        LOCATION
        IMPORTED_IMPLIB
    )
    set(cfg_types
        Release
        RelWithDebInfo
        MinSizeRel
        Debug
    )
    foreach(cfg_prop ${cfg_props})
        foreach(config ${cfg_types})
            list(APPEND single_value_props "${cfg_prop}_${config}")
        endforeach()
    endforeach()

    set(json "{}")
    foreach(prop ${single_value_props})
        set(value)
        get_target_property(value ${ARG_TARGET} ${prop})
        message(STATUS "${ARG_TARGET}: ${prop} = ${value}")
        if (value)
            string(JSON json SET ${json} ${prop} "\"${value}\"")
        endif()
    endforeach()

    foreach(prop ${multi_value_props})
        set(value)
        resolve_target_prop(TARGET ${ARG_TARGET} PROPERTY ${prop} OUT_VAR value)
        if (value)
            list_to_json(json ${json} ${prop} value)
        endif()
    endforeach()
    set(${ARG_OUTPUT_JSON} ${json} PARENT_SCOPE)

endfunction()

###################################################################################
# Converts a list of strings into a JSON array and stores it in the provided JSON
# object.
#
# Parameters:
#   json_var: Output variable to store the resulting JSON into
#   json: String with a JSON object to append the array to
#   member: The member name of the array in the JSON object
#   list_var: Name of the variable containing the list of strings to convert to JSON
###################################################################################
function(list_to_json json_var json member list_var)
    set(i 0)
    string(JSON json SET ${json} "${member}" "[]")
    foreach(elem ${${list_var}})
        # Super-simple check if "elem" contains a JSON object, otherwise assume string
        if (elem MATCHES "^{.*")
            string(JSON json SET ${json} "${member}" "${i}" "${elem}")
        else()
            string(JSON json SET ${json} "${member}" "${i}" "\"${elem}\"")
        endif()
        math(EXPR i "${i} + 1")
    endforeach()

    set(${json_var} ${json} PARENT_SCOPE)
endfunction()

###################################################################################
# Invokes find_package(), locates the specified target and returns all relevant
# properties of the target and all targets that may be referenced by any of the
# properties (recursively).
#
# Parameters:
#   PACKAGE: The package name to find (required)
#   TARGET: The target to resolve (required)
#   OUTPUT_FILE: The file to write the JSON output to (required)
#   COMPONENTS: The components to find (optional)
#   VERSION: The minimum version of the package to find (optional)
#   NAMES: Alternative package names to search for (optional)
###################################################################################
function (find_package_target)
    cmake_parse_arguments(ARG "" "PACKAGE;VERSION;TARGET;OUTPUT_FILE" "COMPONENTS;NAMES" ${ARGN})
    if (NOT ARG_PACKAGE)
        message(FATAL_ERROR "PACKAGE argument is not set")
    endif()
    if (NOT ARG_TARGET)
        message(FATAL_ERROR "TARGET argument is not set")
    endif()
    if (NOT ARG_OUTPUT_FILE)
        message(FATAL_ERROR "OUTPUT_FILE argument is not set")
    endif()

    set(_extra_args)
    if (ARG_COMPONENTS)
        list(APPEND _extra_args COMPONENTS ${ARG_COMPONENTS})
    endif()
    if (ARG_NAMES)
        list(APPEND _extra_args NAMES ${ARG_NAMES})
    endif()

    # It's safe to require the version here, we already found the package before and established
    # the version is recent enough.
    find_package(${ARG_PACKAGE} ${ARG_VERSION} ${_extra_args})
    if (${ARG_PACKAGE}_FOUND)
        resolve_deps_recursively(
            TARGET ${ARG_TARGET}
            OUTPUT_JSON json
        )
        file(WRITE ${ARG_OUTPUT_FILE} ${json})
        message(STATUS "Target details written to ${ARG_OUTPUT_FILE}")
    else()
        # We found the package before, how come we did not find it this time?!
        message(FATAL_ERROR "Package ${ARG_PACKAGE} not found")
    endif()
endfunction()

###################################################################################
# Invokes find_package(), locates the specified target and returns the value of
# the specified property of the target.
#
# Parameters:
#   PACKAGE: The package name to find (required)
#   TARGET: The target to resolve (required)
#   PROPERTY: The property to read from the target (required)
#   OUTPUT_FILE: The file to write the JSON output to (required)
#   COMPONENTS: The components to find (optional)
#   VERSION: The minimum version of the package to find (optional)
#   NAMES: Alternative package names to search for (optional)
###################################################################################

function (find_target_property)
    cmake_parse_arguments(ARG "" "PACKAGE;VERSION;TARGET;PROPERTY;OUTPUT_FILE" "COMPONENTS;NAMES" ${ARGN})
    if (NOT ARG_PACKAGE)
        message(FATAL_ERROR "PACKAGE argument is not set")
    endif()
    if (NOT ARG_TARGET)
        message(FATAL_ERROR "TARGET argument is not set")
    endif()
    if (NOT ARG_PROPERTY)
        message(FATAL_ERROR "PROPERTY argument is not set")
    endif()
    if (NOT ARG_OUTPUT_FILE)
        message(FATAL_ERROR "OUTPUT_FILE argument is not set")
    endif()

    set(_extra_args)
    if (ARG_COMPONENTS)
        list(APPEND _extra_args COMPONENTS ${ARG_COMPONENTS})
    endif()
    if (ARG_NAMES)
        list(APPEND _extra_args NAMES ${ARG_NAMES})
    endif()

    # It's safe to require the version here, we already found the package before and
    # established version is recent enough.
    find_package(${ARG_PACKAGE} ${ARG_VERSION} ${_extra_args})

    if (${ARG_PACKAGE}_FOUND)
        get_target_property(prop_value ${ARG_TARGET} ${ARG_PROPERTY})
        set(json "{}")
        if (NOT prop_value STREQUAL "prop_value-NOTFOUND")
            string(JSON json SET ${json} "value" "\"${prop_value}\"")
            message(STATUS "Target property ${ARG_PROPERTY} value written to ${ARG_OUTPUT_FILE}")
        else()
            message(STATUS "Target property ${ARG_PROPERTY} not found")
        endif()

        file(WRITE ${ARG_OUTPUT_FILE} ${json})
    else()
        # We found the package before, how come we did not find it this time?!
        message(FATAL_ERROR "Package ${ARG_PACKAGE} not found")
    endif()
endfunction()


if (NOT DEFINED PACKAGE)
    message(FATAL_ERROR "PACKAGE is not set")
endif()

if (NOT DEFINED OUTPUT_FILE)
    message(FATAL_ERROR "OUTPUT_FILE is not set")
endif()

message(STATUS "CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}")

if (DEFINED TARGET AND DEFINED PROPERTY)
    find_target_property(
        PACKAGE ${PACKAGE}
        COMPONENTS "${COMPONENTS}"
        NAMES "${NAMES}"
        VERSION ${VERSION}
        TARGET ${TARGET}
        PROPERTY ${PROPERTY}
        OUTPUT_FILE ${OUTPUT_FILE}
    )
elseif (DEFINED TARGET)
    find_package_target(
        PACKAGE ${PACKAGE}
        COMPONENTS "${COMPONENTS}"
        NAMES "${NAMES}"
        VERSION ${VERSION}
        TARGET ${TARGET}
        OUTPUT_FILE ${OUTPUT_FILE}
    )
else()
    find_package_wrapper(
        PACKAGE ${PACKAGE}
        COMPONENTS "${COMPONENTS}"
        NAMES "${NAMES}"
        VERSION ${VERSION}
        OUTPUT_FILE ${OUTPUT_FILE}
    )
endif()

include(FeatureSummary)

feature_summary(WHAT PACKAGES_FOUND PACKAGES_NOT_FOUND)