cpdocs 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cpdocs/__init__.py +5 -0
- cpdocs/__main__.py +5 -0
- cpdocs/build.py +645 -0
- cpdocs/clang_backend.py +1254 -0
- cpdocs/cli.py +78 -0
- cpdocs/config.py +160 -0
- cpdocs/contract.py +407 -0
- cpdocs/html_renderer.py +1803 -0
- cpdocs/model.py +228 -0
- cpdocs/properdocs_core.py +158 -0
- cpdocs/properdocs_plugin.py +236 -0
- cpdocs/python_backend.py +469 -0
- cpdocs/python_manifest.py +84 -0
- cpdocs/render.py +838 -0
- cpdocs/rust_backend.py +234 -0
- cpdocs/share/cmake/cpdocs.cmake +464 -0
- cpdocs/versioning.py +201 -0
- cpdocs-0.1.0.dist-info/METADATA +48 -0
- cpdocs-0.1.0.dist-info/RECORD +21 -0
- cpdocs-0.1.0.dist-info/WHEEL +4 -0
- cpdocs-0.1.0.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
# --------------------------------------------------------------------------------------------------
|
|
2
|
+
# SPDX-FileCopyrightText: 2026 cpdocs developers
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
# --------------------------------------------------------------------------------------------------
|
|
5
|
+
# CMake implementation of the cpdocs build-system contract.
|
|
6
|
+
#
|
|
7
|
+
# cpdocs injects this file with -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES and passes its request with
|
|
8
|
+
# -DCPDOCS_REQUEST. A project declares what it documents:
|
|
9
|
+
#
|
|
10
|
+
# cpdocs_features(<axis> [VALUES <value>...])
|
|
11
|
+
# cpdocs_variant(<name> [PRIMARY] [<axis> <value>...]...)
|
|
12
|
+
# cpdocs_api(UNIT <id> LANGUAGE <c|cpp|python|rust>
|
|
13
|
+
# [TARGET <target>] [API <dir>...] [GENERATED <dir>...]
|
|
14
|
+
# [STANDARD <std>] [DEFINE <definition>...] [INCLUDE <dir>...]
|
|
15
|
+
# [PACKAGE <python package>] [RUSTDOC_JSON <file>] [DEPENDS <target>...])
|
|
16
|
+
#
|
|
17
|
+
# Feature axes follow the besa_feature convention: the axis variable holds the selected values, the
|
|
18
|
+
# <axis>_ARRAY variable holds the valid values, and "none" / "all" select nothing or everything.
|
|
19
|
+
#
|
|
20
|
+
# At the end of the top-level CMakeLists.txt the adapter answers the request:
|
|
21
|
+
#
|
|
22
|
+
# describe writes the describe manifest directly;
|
|
23
|
+
# variant generates the per-unit description and a `cpdocs-prepare` target that builds the
|
|
24
|
+
# declared dependencies and then converts that description into the variant manifest.
|
|
25
|
+
# --------------------------------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
include_guard(GLOBAL)
|
|
28
|
+
if(CMAKE_VERSION VERSION_LESS 3.24)
|
|
29
|
+
message(FATAL_ERROR "cpdocs.cmake needs CMake 3.24 or newer")
|
|
30
|
+
endif()
|
|
31
|
+
# Everything below, including the functions it defines, uses modern policies regardless of the
|
|
32
|
+
# project's own cmake_minimum_required.
|
|
33
|
+
cmake_policy(PUSH)
|
|
34
|
+
cmake_policy(VERSION 3.24)
|
|
35
|
+
|
|
36
|
+
# --------------------------------------------------------------------------------------------------
|
|
37
|
+
# JSON helpers (shared by configure and script mode)
|
|
38
|
+
# --------------------------------------------------------------------------------------------------
|
|
39
|
+
function(_cpdocs_json_string out value)
|
|
40
|
+
string(REPLACE "\\" "\\\\" value "${value}")
|
|
41
|
+
string(REPLACE "\"" "\\\"" value "${value}")
|
|
42
|
+
string(REPLACE "\t" "\\t" value "${value}")
|
|
43
|
+
string(REPLACE "\n" "\\n" value "${value}")
|
|
44
|
+
set(${out} "\"${value}\"" PARENT_SCOPE)
|
|
45
|
+
endfunction()
|
|
46
|
+
|
|
47
|
+
# Build a JSON array of strings from the remaining arguments.
|
|
48
|
+
function(_cpdocs_json_array out)
|
|
49
|
+
set(items "")
|
|
50
|
+
foreach(value IN LISTS ARGN)
|
|
51
|
+
_cpdocs_json_string(quoted "${value}")
|
|
52
|
+
list(APPEND items "${quoted}")
|
|
53
|
+
endforeach()
|
|
54
|
+
list(JOIN items ", " joined)
|
|
55
|
+
set(${out} "[${joined}]" PARENT_SCOPE)
|
|
56
|
+
endfunction()
|
|
57
|
+
|
|
58
|
+
# --------------------------------------------------------------------------------------------------
|
|
59
|
+
# Script mode: convert the generated unit description into the variant manifest
|
|
60
|
+
# --------------------------------------------------------------------------------------------------
|
|
61
|
+
if(CMAKE_SCRIPT_MODE_FILE)
|
|
62
|
+
if(NOT DEFINED CPDOCS_CONVERT OR NOT DEFINED CPDOCS_OUTPUT)
|
|
63
|
+
message(FATAL_ERROR "cpdocs.cmake: script mode needs CPDOCS_CONVERT and CPDOCS_OUTPUT")
|
|
64
|
+
endif()
|
|
65
|
+
file(STRINGS "${CPDOCS_CONVERT}" lines)
|
|
66
|
+
|
|
67
|
+
set(variant "")
|
|
68
|
+
set(features "")
|
|
69
|
+
set(units "")
|
|
70
|
+
set(unit "")
|
|
71
|
+
|
|
72
|
+
macro(_cpdocs_flush_unit)
|
|
73
|
+
if(NOT unit STREQUAL "")
|
|
74
|
+
_cpdocs_json_string(id "${unit}")
|
|
75
|
+
_cpdocs_json_string(lang "${u_language}")
|
|
76
|
+
set(roots "")
|
|
77
|
+
foreach(path IN LISTS u_api)
|
|
78
|
+
_cpdocs_json_string(quoted "${path}")
|
|
79
|
+
list(APPEND roots "{\"path\": ${quoted}}")
|
|
80
|
+
endforeach()
|
|
81
|
+
foreach(path IN LISTS u_generated)
|
|
82
|
+
_cpdocs_json_string(quoted "${path}")
|
|
83
|
+
list(APPEND roots "{\"path\": ${quoted}, \"generated\": true}")
|
|
84
|
+
endforeach()
|
|
85
|
+
list(JOIN roots ", " roots)
|
|
86
|
+
set(entry "{\"id\": ${id}, \"language\": ${lang}, \"api\": [${roots}]")
|
|
87
|
+
if(u_language STREQUAL "c" OR u_language STREQUAL "cpp")
|
|
88
|
+
list(REMOVE_DUPLICATES u_include)
|
|
89
|
+
list(REMOVE_DUPLICATES u_system)
|
|
90
|
+
list(REMOVE_DUPLICATES u_define)
|
|
91
|
+
_cpdocs_json_string(standard "${u_standard}")
|
|
92
|
+
_cpdocs_json_array(include ${u_include})
|
|
93
|
+
_cpdocs_json_array(system ${u_system})
|
|
94
|
+
_cpdocs_json_array(define ${u_define})
|
|
95
|
+
_cpdocs_json_array(flags ${u_flags})
|
|
96
|
+
string(APPEND entry ", \"parse\": {\"standard\": ${standard}, \"include\": ${include}, "
|
|
97
|
+
"\"system-include\": ${system}, \"define\": ${define}, \"flags\": ${flags}}")
|
|
98
|
+
elseif(u_language STREQUAL "python")
|
|
99
|
+
_cpdocs_json_string(package "${u_package}")
|
|
100
|
+
string(APPEND entry ", \"package\": ${package}")
|
|
101
|
+
elseif(u_language STREQUAL "rust")
|
|
102
|
+
_cpdocs_json_string(rustdoc "${u_rustdoc}")
|
|
103
|
+
string(APPEND entry ", \"rustdoc-json\": ${rustdoc}")
|
|
104
|
+
endif()
|
|
105
|
+
string(APPEND entry "}")
|
|
106
|
+
list(APPEND units "${entry}")
|
|
107
|
+
endif()
|
|
108
|
+
foreach(field language api generated standard include system define flags package rustdoc)
|
|
109
|
+
set(u_${field} "")
|
|
110
|
+
endforeach()
|
|
111
|
+
endmacro()
|
|
112
|
+
|
|
113
|
+
foreach(line IN LISTS lines)
|
|
114
|
+
string(FIND "${line}" "=" split)
|
|
115
|
+
if(split LESS 1)
|
|
116
|
+
continue()
|
|
117
|
+
endif()
|
|
118
|
+
string(SUBSTRING "${line}" 0 ${split} key)
|
|
119
|
+
math(EXPR start "${split} + 1")
|
|
120
|
+
string(SUBSTRING "${line}" ${start} -1 value)
|
|
121
|
+
if(key STREQUAL "variant")
|
|
122
|
+
set(variant "${value}")
|
|
123
|
+
elseif(key STREQUAL "feature")
|
|
124
|
+
string(FIND "${value}" "=" split)
|
|
125
|
+
string(SUBSTRING "${value}" 0 ${split} axis)
|
|
126
|
+
math(EXPR start "${split} + 1")
|
|
127
|
+
string(SUBSTRING "${value}" ${start} -1 selected)
|
|
128
|
+
string(REPLACE "," ";" selected "${selected}")
|
|
129
|
+
_cpdocs_json_string(axis "${axis}")
|
|
130
|
+
_cpdocs_json_array(selected ${selected})
|
|
131
|
+
list(APPEND features "${axis}: ${selected}")
|
|
132
|
+
elseif(key STREQUAL "unit")
|
|
133
|
+
_cpdocs_flush_unit()
|
|
134
|
+
set(unit "${value}")
|
|
135
|
+
elseif(value STREQUAL "")
|
|
136
|
+
continue()
|
|
137
|
+
elseif(key STREQUAL "standard" OR key STREQUAL "language" OR key STREQUAL "package"
|
|
138
|
+
OR key STREQUAL "rustdoc")
|
|
139
|
+
set(u_${key} "${value}")
|
|
140
|
+
else()
|
|
141
|
+
list(APPEND u_${key} "${value}")
|
|
142
|
+
endif()
|
|
143
|
+
endforeach()
|
|
144
|
+
_cpdocs_flush_unit()
|
|
145
|
+
|
|
146
|
+
list(JOIN features ", " features)
|
|
147
|
+
list(JOIN units ", " units)
|
|
148
|
+
_cpdocs_json_string(variant "${variant}")
|
|
149
|
+
file(WRITE "${CPDOCS_OUTPUT}" "{\n \"cpdocs-manifest\": 1,\n \"kind\": \"variant\",\n"
|
|
150
|
+
" \"variant\": ${variant},\n \"features\": {${features}},\n \"units\": [${units}]\n}\n")
|
|
151
|
+
cmake_policy(POP)
|
|
152
|
+
return()
|
|
153
|
+
endif()
|
|
154
|
+
|
|
155
|
+
# --------------------------------------------------------------------------------------------------
|
|
156
|
+
# Configure mode
|
|
157
|
+
# --------------------------------------------------------------------------------------------------
|
|
158
|
+
set(CPDOCS_MODULE "${CMAKE_CURRENT_LIST_FILE}" CACHE INTERNAL "cpdocs CMake adapter")
|
|
159
|
+
set(CPDOCS_CONFIGURE_ARGS "" CACHE STRING
|
|
160
|
+
"Extra cache arguments forwarded to every cpdocs variant configuration")
|
|
161
|
+
|
|
162
|
+
function(cpdocs_features axis)
|
|
163
|
+
cmake_parse_arguments(PARSE_ARGV 1 arg "" "" "VALUES")
|
|
164
|
+
if(arg_VALUES)
|
|
165
|
+
set(values ${arg_VALUES})
|
|
166
|
+
elseif(DEFINED ${axis}_ARRAY)
|
|
167
|
+
set(values ${${axis}_ARRAY})
|
|
168
|
+
else()
|
|
169
|
+
message(FATAL_ERROR "cpdocs_features(${axis}): give VALUES or declare the axis first")
|
|
170
|
+
endif()
|
|
171
|
+
set_property(GLOBAL APPEND PROPERTY CPDOCS_AXES "${axis}")
|
|
172
|
+
set_property(GLOBAL PROPERTY CPDOCS_AXIS_${axis}_VALUES "${values}")
|
|
173
|
+
endfunction()
|
|
174
|
+
|
|
175
|
+
function(cpdocs_variant name)
|
|
176
|
+
get_property(axes GLOBAL PROPERTY CPDOCS_AXES)
|
|
177
|
+
set(primary FALSE)
|
|
178
|
+
set(axis "")
|
|
179
|
+
foreach(token IN LISTS ARGN)
|
|
180
|
+
if(token STREQUAL "PRIMARY")
|
|
181
|
+
set(primary TRUE)
|
|
182
|
+
elseif(token IN_LIST axes)
|
|
183
|
+
set(axis "${token}")
|
|
184
|
+
set(spelling_${axis} "")
|
|
185
|
+
elseif(axis STREQUAL "")
|
|
186
|
+
message(FATAL_ERROR "cpdocs_variant(${name}): ${token} is not a declared feature axis")
|
|
187
|
+
else()
|
|
188
|
+
list(APPEND spelling_${axis} "${token}")
|
|
189
|
+
endif()
|
|
190
|
+
endforeach()
|
|
191
|
+
set_property(GLOBAL APPEND PROPERTY CPDOCS_VARIANTS "${name}")
|
|
192
|
+
set_property(GLOBAL PROPERTY CPDOCS_VARIANT_${name}_PRIMARY ${primary})
|
|
193
|
+
foreach(axis IN LISTS axes)
|
|
194
|
+
if(DEFINED spelling_${axis})
|
|
195
|
+
set_property(GLOBAL PROPERTY CPDOCS_VARIANT_${name}_AXIS_${axis} "${spelling_${axis}}")
|
|
196
|
+
endif()
|
|
197
|
+
endforeach()
|
|
198
|
+
endfunction()
|
|
199
|
+
|
|
200
|
+
function(cpdocs_api)
|
|
201
|
+
cmake_parse_arguments(PARSE_ARGV 0 arg "" "UNIT;LANGUAGE;TARGET;STANDARD;PACKAGE;RUSTDOC_JSON"
|
|
202
|
+
"API;GENERATED;DEFINE;INCLUDE;DEPENDS")
|
|
203
|
+
if(NOT arg_UNIT OR NOT arg_LANGUAGE)
|
|
204
|
+
message(FATAL_ERROR "cpdocs_api needs UNIT and LANGUAGE")
|
|
205
|
+
endif()
|
|
206
|
+
set(unit "${arg_UNIT}")
|
|
207
|
+
set(api "")
|
|
208
|
+
foreach(path IN LISTS arg_API)
|
|
209
|
+
cmake_path(ABSOLUTE_PATH path BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" NORMALIZE)
|
|
210
|
+
list(APPEND api "${path}")
|
|
211
|
+
endforeach()
|
|
212
|
+
set(generated "")
|
|
213
|
+
foreach(path IN LISTS arg_GENERATED)
|
|
214
|
+
cmake_path(ABSOLUTE_PATH path BASE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" NORMALIZE)
|
|
215
|
+
list(APPEND generated "${path}")
|
|
216
|
+
endforeach()
|
|
217
|
+
set(include "")
|
|
218
|
+
foreach(path IN LISTS arg_INCLUDE)
|
|
219
|
+
cmake_path(ABSOLUTE_PATH path BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" NORMALIZE)
|
|
220
|
+
list(APPEND include "${path}")
|
|
221
|
+
endforeach()
|
|
222
|
+
set(rustdoc "")
|
|
223
|
+
if(arg_RUSTDOC_JSON)
|
|
224
|
+
set(rustdoc "${arg_RUSTDOC_JSON}")
|
|
225
|
+
cmake_path(ABSOLUTE_PATH rustdoc BASE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" NORMALIZE)
|
|
226
|
+
endif()
|
|
227
|
+
set_property(GLOBAL APPEND PROPERTY CPDOCS_UNITS "${unit}")
|
|
228
|
+
foreach(field LANGUAGE TARGET STANDARD PACKAGE DEFINE DEPENDS)
|
|
229
|
+
set_property(GLOBAL PROPERTY CPDOCS_UNIT_${unit}_${field} "${arg_${field}}")
|
|
230
|
+
endforeach()
|
|
231
|
+
set_property(GLOBAL PROPERTY CPDOCS_UNIT_${unit}_API "${api}")
|
|
232
|
+
set_property(GLOBAL PROPERTY CPDOCS_UNIT_${unit}_GENERATED "${generated}")
|
|
233
|
+
set_property(GLOBAL PROPERTY CPDOCS_UNIT_${unit}_INCLUDE "${include}")
|
|
234
|
+
set_property(GLOBAL PROPERTY CPDOCS_UNIT_${unit}_RUSTDOC "${rustdoc}")
|
|
235
|
+
endfunction()
|
|
236
|
+
|
|
237
|
+
# Selected values of an axis in the current configuration, with none/all resolved.
|
|
238
|
+
function(_cpdocs_resolve out axis spelling)
|
|
239
|
+
get_property(values GLOBAL PROPERTY CPDOCS_AXIS_${axis}_VALUES)
|
|
240
|
+
if("all" IN_LIST spelling)
|
|
241
|
+
set(${out} "${values}" PARENT_SCOPE)
|
|
242
|
+
elseif("none" IN_LIST spelling)
|
|
243
|
+
set(${out} "" PARENT_SCOPE)
|
|
244
|
+
else()
|
|
245
|
+
set(${out} "${spelling}" PARENT_SCOPE)
|
|
246
|
+
endif()
|
|
247
|
+
endfunction()
|
|
248
|
+
|
|
249
|
+
# The C or C++ standard a target asks its consumers for, from its compile features.
|
|
250
|
+
function(_cpdocs_standard out target language)
|
|
251
|
+
set(prefix "cxx_std_")
|
|
252
|
+
set(flag "c++")
|
|
253
|
+
if(language STREQUAL "c")
|
|
254
|
+
set(prefix "c_std_")
|
|
255
|
+
set(flag "c")
|
|
256
|
+
endif()
|
|
257
|
+
get_target_property(features ${target} INTERFACE_COMPILE_FEATURES)
|
|
258
|
+
set(best "")
|
|
259
|
+
foreach(feature IN LISTS features)
|
|
260
|
+
if(feature MATCHES "^${prefix}([0-9]+)$")
|
|
261
|
+
set(number "${CMAKE_MATCH_1}")
|
|
262
|
+
if(best STREQUAL "" OR number GREATER best)
|
|
263
|
+
set(best "${number}")
|
|
264
|
+
endif()
|
|
265
|
+
endif()
|
|
266
|
+
endforeach()
|
|
267
|
+
if(best STREQUAL "")
|
|
268
|
+
set(${out} "" PARENT_SCOPE)
|
|
269
|
+
else()
|
|
270
|
+
set(${out} "${flag}${best}" PARENT_SCOPE)
|
|
271
|
+
endif()
|
|
272
|
+
endfunction()
|
|
273
|
+
|
|
274
|
+
function(_cpdocs_describe manifest)
|
|
275
|
+
get_property(axes GLOBAL PROPERTY CPDOCS_AXES)
|
|
276
|
+
get_property(units GLOBAL PROPERTY CPDOCS_UNITS)
|
|
277
|
+
get_property(variants GLOBAL PROPERTY CPDOCS_VARIANTS)
|
|
278
|
+
if(NOT units)
|
|
279
|
+
message(FATAL_ERROR "cpdocs: the project declares no cpdocs_api() units")
|
|
280
|
+
endif()
|
|
281
|
+
if(NOT variants)
|
|
282
|
+
set(variants default)
|
|
283
|
+
set_property(GLOBAL PROPERTY CPDOCS_VARIANT_default_PRIMARY TRUE)
|
|
284
|
+
endif()
|
|
285
|
+
set(has_primary FALSE)
|
|
286
|
+
foreach(name IN LISTS variants)
|
|
287
|
+
get_property(primary GLOBAL PROPERTY CPDOCS_VARIANT_${name}_PRIMARY)
|
|
288
|
+
if(primary)
|
|
289
|
+
set(has_primary TRUE)
|
|
290
|
+
endif()
|
|
291
|
+
endforeach()
|
|
292
|
+
if(NOT has_primary)
|
|
293
|
+
list(GET variants 0 first)
|
|
294
|
+
set_property(GLOBAL PROPERTY CPDOCS_VARIANT_${first}_PRIMARY TRUE)
|
|
295
|
+
endif()
|
|
296
|
+
|
|
297
|
+
set(feature_entries "")
|
|
298
|
+
foreach(axis IN LISTS axes)
|
|
299
|
+
get_property(values GLOBAL PROPERTY CPDOCS_AXIS_${axis}_VALUES)
|
|
300
|
+
_cpdocs_json_string(key "${axis}")
|
|
301
|
+
_cpdocs_json_array(values ${values})
|
|
302
|
+
list(APPEND feature_entries "${key}: {\"values\": ${values}}")
|
|
303
|
+
endforeach()
|
|
304
|
+
list(JOIN feature_entries ", " feature_entries)
|
|
305
|
+
|
|
306
|
+
set(unit_entries "")
|
|
307
|
+
foreach(unit IN LISTS units)
|
|
308
|
+
get_property(language GLOBAL PROPERTY CPDOCS_UNIT_${unit}_LANGUAGE)
|
|
309
|
+
_cpdocs_json_string(id "${unit}")
|
|
310
|
+
_cpdocs_json_string(language "${language}")
|
|
311
|
+
list(APPEND unit_entries "{\"id\": ${id}, \"language\": ${language}}")
|
|
312
|
+
endforeach()
|
|
313
|
+
list(JOIN unit_entries ", " unit_entries)
|
|
314
|
+
|
|
315
|
+
set(common "${CMAKE_COMMAND}" -S "{source}" -B "{build}" -G "${CMAKE_GENERATOR}"
|
|
316
|
+
"-DCPDOCS_REQUEST={request}" "-DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=${CPDOCS_MODULE}")
|
|
317
|
+
if(CMAKE_BUILD_TYPE)
|
|
318
|
+
list(APPEND common "-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}")
|
|
319
|
+
endif()
|
|
320
|
+
if(CMAKE_TOOLCHAIN_FILE)
|
|
321
|
+
list(APPEND common "-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}")
|
|
322
|
+
endif()
|
|
323
|
+
list(APPEND common ${CPDOCS_CONFIGURE_ARGS})
|
|
324
|
+
_cpdocs_json_array(build "${CMAKE_COMMAND}" --build "{build}" --target cpdocs-prepare)
|
|
325
|
+
|
|
326
|
+
set(variant_entries "")
|
|
327
|
+
foreach(name IN LISTS variants)
|
|
328
|
+
get_property(primary GLOBAL PROPERTY CPDOCS_VARIANT_${name}_PRIMARY)
|
|
329
|
+
_cpdocs_json_array(configure ${common})
|
|
330
|
+
string(REGEX REPLACE "]$" "" configure "${configure}")
|
|
331
|
+
set(selected_entries "")
|
|
332
|
+
foreach(axis IN LISTS axes)
|
|
333
|
+
get_property(pinned GLOBAL PROPERTY CPDOCS_VARIANT_${name}_AXIS_${axis} SET)
|
|
334
|
+
if(pinned)
|
|
335
|
+
get_property(spelling GLOBAL PROPERTY CPDOCS_VARIANT_${name}_AXIS_${axis})
|
|
336
|
+
else()
|
|
337
|
+
set(spelling "${${axis}}")
|
|
338
|
+
endif()
|
|
339
|
+
if(spelling STREQUAL "")
|
|
340
|
+
set(spelling none)
|
|
341
|
+
endif()
|
|
342
|
+
_cpdocs_resolve(selected "${axis}" "${spelling}")
|
|
343
|
+
_cpdocs_json_string(argument "-D${axis}=${spelling}")
|
|
344
|
+
string(APPEND configure ", ${argument}")
|
|
345
|
+
_cpdocs_json_string(key "${axis}")
|
|
346
|
+
_cpdocs_json_array(selected ${selected})
|
|
347
|
+
list(APPEND selected_entries "${key}: ${selected}")
|
|
348
|
+
endforeach()
|
|
349
|
+
list(JOIN selected_entries ", " selected_entries)
|
|
350
|
+
string(APPEND configure "]")
|
|
351
|
+
_cpdocs_json_string(quoted "${name}")
|
|
352
|
+
if(primary)
|
|
353
|
+
set(primary true)
|
|
354
|
+
else()
|
|
355
|
+
set(primary false)
|
|
356
|
+
endif()
|
|
357
|
+
string(CONCAT entry "{\"name\": ${quoted}, \"primary\": ${primary}, "
|
|
358
|
+
"\"features\": {${selected_entries}}, \"commands\": [${configure}, ${build}]}")
|
|
359
|
+
if(NOT variant_entries STREQUAL "")
|
|
360
|
+
string(APPEND variant_entries ",\n ")
|
|
361
|
+
endif()
|
|
362
|
+
string(APPEND variant_entries "${entry}")
|
|
363
|
+
endforeach()
|
|
364
|
+
|
|
365
|
+
_cpdocs_json_string(project_name "${CMAKE_PROJECT_NAME}")
|
|
366
|
+
_cpdocs_json_string(project_version "${CMAKE_PROJECT_VERSION}")
|
|
367
|
+
_cpdocs_json_string(generator "cpdocs.cmake / CMake ${CMAKE_VERSION}")
|
|
368
|
+
file(WRITE "${manifest}" "{\n \"cpdocs-manifest\": 1,\n \"kind\": \"describe\",\n"
|
|
369
|
+
" \"generator\": ${generator},\n"
|
|
370
|
+
" \"project\": {\"name\": ${project_name}, \"version\": ${project_version}},\n"
|
|
371
|
+
" \"features\": {${feature_entries}},\n \"units\": [${unit_entries}],\n"
|
|
372
|
+
" \"variants\": [\n ${variant_entries}\n ]\n}\n")
|
|
373
|
+
endfunction()
|
|
374
|
+
|
|
375
|
+
function(_cpdocs_variant name manifest)
|
|
376
|
+
get_property(axes GLOBAL PROPERTY CPDOCS_AXES)
|
|
377
|
+
get_property(units GLOBAL PROPERTY CPDOCS_UNITS)
|
|
378
|
+
get_property(variants GLOBAL PROPERTY CPDOCS_VARIANTS)
|
|
379
|
+
if(variants AND NOT name IN_LIST variants)
|
|
380
|
+
message(FATAL_ERROR "cpdocs: the project declares no variant '${name}'")
|
|
381
|
+
endif()
|
|
382
|
+
|
|
383
|
+
# The unit description is written with generator expressions, so target usage requirements are
|
|
384
|
+
# evaluated by CMake itself, and converted to JSON by `cpdocs-prepare` in script mode.
|
|
385
|
+
set(content "variant=${name}\n")
|
|
386
|
+
foreach(axis IN LISTS axes)
|
|
387
|
+
set(spelling "${${axis}}")
|
|
388
|
+
if(spelling STREQUAL "")
|
|
389
|
+
set(spelling none)
|
|
390
|
+
endif()
|
|
391
|
+
_cpdocs_resolve(selected "${axis}" "${spelling}")
|
|
392
|
+
list(JOIN selected "," selected)
|
|
393
|
+
string(APPEND content "feature=${axis}=${selected}\n")
|
|
394
|
+
endforeach()
|
|
395
|
+
|
|
396
|
+
set(dependencies "")
|
|
397
|
+
foreach(unit IN LISTS units)
|
|
398
|
+
foreach(field LANGUAGE TARGET STANDARD PACKAGE DEFINE DEPENDS API GENERATED INCLUDE RUSTDOC)
|
|
399
|
+
get_property(u_${field} GLOBAL PROPERTY CPDOCS_UNIT_${unit}_${field})
|
|
400
|
+
endforeach()
|
|
401
|
+
list(APPEND dependencies ${u_DEPENDS})
|
|
402
|
+
string(APPEND content "unit=${unit}\nlanguage=${u_LANGUAGE}\n")
|
|
403
|
+
foreach(path IN LISTS u_API)
|
|
404
|
+
string(APPEND content "api=${path}\n")
|
|
405
|
+
endforeach()
|
|
406
|
+
foreach(path IN LISTS u_GENERATED)
|
|
407
|
+
string(APPEND content "generated=${path}\n")
|
|
408
|
+
endforeach()
|
|
409
|
+
if(u_LANGUAGE STREQUAL "python")
|
|
410
|
+
string(APPEND content "package=${u_PACKAGE}\n")
|
|
411
|
+
elseif(u_LANGUAGE STREQUAL "rust")
|
|
412
|
+
string(APPEND content "rustdoc=${u_RUSTDOC}\n")
|
|
413
|
+
else()
|
|
414
|
+
if(u_STANDARD STREQUAL "" AND NOT u_TARGET STREQUAL "")
|
|
415
|
+
_cpdocs_standard(u_STANDARD "${u_TARGET}" "${u_LANGUAGE}")
|
|
416
|
+
endif()
|
|
417
|
+
string(APPEND content "standard=${u_STANDARD}\n")
|
|
418
|
+
foreach(path IN LISTS u_INCLUDE)
|
|
419
|
+
string(APPEND content "include=${path}\n")
|
|
420
|
+
endforeach()
|
|
421
|
+
foreach(definition IN LISTS u_DEFINE)
|
|
422
|
+
string(APPEND content "define=${definition}\n")
|
|
423
|
+
endforeach()
|
|
424
|
+
if(NOT u_TARGET STREQUAL "")
|
|
425
|
+
foreach(pair "include;INTERFACE_INCLUDE_DIRECTORIES"
|
|
426
|
+
"system;INTERFACE_SYSTEM_INCLUDE_DIRECTORIES"
|
|
427
|
+
"define;INTERFACE_COMPILE_DEFINITIONS" "flags;INTERFACE_COMPILE_OPTIONS")
|
|
428
|
+
list(GET pair 0 key)
|
|
429
|
+
list(GET pair 1 name)
|
|
430
|
+
set(property "$<TARGET_PROPERTY:${u_TARGET},${name}>")
|
|
431
|
+
string(APPEND content "${key}=$<JOIN:${property},\n${key}=>\n")
|
|
432
|
+
endforeach()
|
|
433
|
+
endif()
|
|
434
|
+
endif()
|
|
435
|
+
endforeach()
|
|
436
|
+
|
|
437
|
+
set(description "${CMAKE_BINARY_DIR}/cpdocs/units-$<CONFIG>.txt")
|
|
438
|
+
file(GENERATE OUTPUT "${description}" CONTENT "${content}")
|
|
439
|
+
add_custom_target(cpdocs-prepare
|
|
440
|
+
COMMAND "${CMAKE_COMMAND}" "-DCPDOCS_CONVERT=${description}" "-DCPDOCS_OUTPUT=${manifest}"
|
|
441
|
+
-P "${CPDOCS_MODULE}"
|
|
442
|
+
VERBATIM)
|
|
443
|
+
if(dependencies)
|
|
444
|
+
add_dependencies(cpdocs-prepare ${dependencies})
|
|
445
|
+
endif()
|
|
446
|
+
endfunction()
|
|
447
|
+
|
|
448
|
+
function(_cpdocs_finalize)
|
|
449
|
+
file(READ "${CPDOCS_REQUEST}" request)
|
|
450
|
+
string(JSON phase GET "${request}" phase)
|
|
451
|
+
string(JSON manifest GET "${request}" manifest)
|
|
452
|
+
if(phase STREQUAL "describe")
|
|
453
|
+
_cpdocs_describe("${manifest}")
|
|
454
|
+
else()
|
|
455
|
+
string(JSON variant GET "${request}" variant)
|
|
456
|
+
_cpdocs_variant("${variant}" "${manifest}")
|
|
457
|
+
endif()
|
|
458
|
+
endfunction()
|
|
459
|
+
|
|
460
|
+
if(DEFINED CPDOCS_REQUEST AND NOT CPDOCS_REQUEST STREQUAL "")
|
|
461
|
+
cmake_language(DEFER DIRECTORY "${CMAKE_SOURCE_DIR}" CALL _cpdocs_finalize)
|
|
462
|
+
endif()
|
|
463
|
+
|
|
464
|
+
cmake_policy(POP)
|
cpdocs/versioning.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 cpdocs developers
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Git-ref selection for cpdocs' versioned API site.
|
|
4
|
+
|
|
5
|
+
Branches are named explicitly (glob patterns allowed) and are always built. Tags are chosen by a
|
|
6
|
+
selector. Branch names are matched against local branches and, for CI clones that only carry the
|
|
7
|
+
checked-out branch locally, against remote-tracking branches as well.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import fnmatch
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import subprocess
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from packaging.version import InvalidVersion, Version
|
|
20
|
+
|
|
21
|
+
_RANGE_PART = re.compile(r"^(>=|<=|==|!=|>|<)\s*(.+)$")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _git_ref_names(project_root: Path, namespace: str) -> list[str]:
|
|
25
|
+
result = subprocess.run(
|
|
26
|
+
["git", "for-each-ref", "--format=%(refname:short)", f"refs/{namespace}/"],
|
|
27
|
+
cwd=project_root,
|
|
28
|
+
check=False,
|
|
29
|
+
text=True,
|
|
30
|
+
stdout=subprocess.PIPE,
|
|
31
|
+
stderr=subprocess.PIPE,
|
|
32
|
+
)
|
|
33
|
+
if result.returncode != 0:
|
|
34
|
+
raise RuntimeError("cpdocs version selection requires a readable Git repository")
|
|
35
|
+
return [line for line in result.stdout.splitlines() if line]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _versioned_tags(project_root: Path) -> list[tuple[Version, str]]:
|
|
39
|
+
values: list[tuple[Version, str]] = []
|
|
40
|
+
for tag in _git_ref_names(project_root, "tags"):
|
|
41
|
+
try:
|
|
42
|
+
parsed = Version(tag)
|
|
43
|
+
except InvalidVersion:
|
|
44
|
+
continue
|
|
45
|
+
values.append((parsed, tag))
|
|
46
|
+
return sorted(values, key=lambda item: (item[0], item[1]))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _version_in_range(version_value: Version, expression: str) -> bool:
|
|
50
|
+
comparators = [part.strip() for part in expression.split(",") if part.strip()]
|
|
51
|
+
if not comparators:
|
|
52
|
+
raise RuntimeError("cpdocs version range must contain at least one comparator")
|
|
53
|
+
for comparator in comparators:
|
|
54
|
+
match = _RANGE_PART.fullmatch(comparator)
|
|
55
|
+
if match is None:
|
|
56
|
+
raise RuntimeError(
|
|
57
|
+
f"invalid cpdocs version range comparator {comparator!r}; use forms such as >=0.2,<0.6"
|
|
58
|
+
)
|
|
59
|
+
operator, raw_version = match.groups()
|
|
60
|
+
try:
|
|
61
|
+
boundary = Version(raw_version.strip())
|
|
62
|
+
except InvalidVersion as error:
|
|
63
|
+
raise RuntimeError(f"invalid cpdocs version {raw_version!r} in range") from error
|
|
64
|
+
relation = {
|
|
65
|
+
">=": version_value >= boundary,
|
|
66
|
+
"<=": version_value <= boundary,
|
|
67
|
+
"==": version_value == boundary,
|
|
68
|
+
"!=": version_value != boundary,
|
|
69
|
+
">": version_value > boundary,
|
|
70
|
+
"<": version_value < boundary,
|
|
71
|
+
}[operator]
|
|
72
|
+
if not relation:
|
|
73
|
+
return False
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class SelectedRef:
|
|
79
|
+
"""One version to build: its label, its kind and the Git object to check out.
|
|
80
|
+
|
|
81
|
+
``checkout`` is ``None`` when the label is the branch currently checked out, in which case the
|
|
82
|
+
working tree itself is documented (uncommitted changes included).
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
name: str
|
|
86
|
+
kind: str
|
|
87
|
+
checkout: str | None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def select_tags(project_root: Path, value: str) -> list[str]:
|
|
91
|
+
"""Return tag names chosen by a selector, newest first."""
|
|
92
|
+
|
|
93
|
+
value = value.strip()
|
|
94
|
+
if value == "none":
|
|
95
|
+
return []
|
|
96
|
+
tags = _versioned_tags(project_root)
|
|
97
|
+
if value == "all":
|
|
98
|
+
return [tag for _version, tag in reversed(tags)]
|
|
99
|
+
if value.startswith("latest:"):
|
|
100
|
+
try:
|
|
101
|
+
count = int(value.removeprefix("latest:").strip())
|
|
102
|
+
except ValueError as error:
|
|
103
|
+
raise RuntimeError(f"invalid cpdocs latest selector {value!r}") from error
|
|
104
|
+
if count < 1:
|
|
105
|
+
raise RuntimeError("cpdocs latest selector must request at least one version")
|
|
106
|
+
return [tag for _version, tag in reversed(tags[-count:])]
|
|
107
|
+
if value.startswith("range:"):
|
|
108
|
+
expression = value.removeprefix("range:").strip()
|
|
109
|
+
return [tag for parsed, tag in reversed(tags) if _version_in_range(parsed, expression)]
|
|
110
|
+
if value.startswith("refs:"):
|
|
111
|
+
requested = [
|
|
112
|
+
item.strip() for item in value.removeprefix("refs:").split(",") if item.strip()
|
|
113
|
+
]
|
|
114
|
+
if not requested:
|
|
115
|
+
raise RuntimeError("cpdocs refs selector must contain at least one tag")
|
|
116
|
+
known = set(_git_ref_names(project_root, "tags"))
|
|
117
|
+
unknown = [name for name in requested if name not in known]
|
|
118
|
+
if unknown:
|
|
119
|
+
raise RuntimeError("unknown cpdocs Git tags: " + ", ".join(unknown))
|
|
120
|
+
return requested
|
|
121
|
+
raise RuntimeError(
|
|
122
|
+
f"unsupported cpdocs tag selector {value!r}; expected all, none, latest:N, "
|
|
123
|
+
"range:<comparators>, or refs:<tag,...>"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _branches(project_root: Path) -> dict[str, str]:
|
|
128
|
+
"""Map branch names to the ref that can be checked out, preferring local branches."""
|
|
129
|
+
|
|
130
|
+
values: dict[str, str] = {}
|
|
131
|
+
for remote_ref in _git_ref_names(project_root, "remotes"):
|
|
132
|
+
remote, _, name = remote_ref.partition("/")
|
|
133
|
+
if name and name != "HEAD":
|
|
134
|
+
values.setdefault(name, f"{remote}/{name}")
|
|
135
|
+
for name in _git_ref_names(project_root, "heads"):
|
|
136
|
+
values[name] = name
|
|
137
|
+
return values
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def current_branch(project_root: Path) -> str | None:
|
|
141
|
+
"""Name of the checked-out branch, also in detached CI checkouts."""
|
|
142
|
+
|
|
143
|
+
for variable in ("CPDOCS_CURRENT_BRANCH", "GITHUB_HEAD_REF", "CI_COMMIT_BRANCH"):
|
|
144
|
+
value = os.environ.get(variable, "").strip()
|
|
145
|
+
if value:
|
|
146
|
+
return value
|
|
147
|
+
if os.environ.get("GITHUB_REF_TYPE") == "branch" and os.environ.get("GITHUB_REF_NAME"):
|
|
148
|
+
return os.environ["GITHUB_REF_NAME"]
|
|
149
|
+
result = subprocess.run(
|
|
150
|
+
["git", "branch", "--show-current"],
|
|
151
|
+
cwd=project_root,
|
|
152
|
+
check=False,
|
|
153
|
+
text=True,
|
|
154
|
+
stdout=subprocess.PIPE,
|
|
155
|
+
stderr=subprocess.DEVNULL,
|
|
156
|
+
)
|
|
157
|
+
value = result.stdout.strip()
|
|
158
|
+
return value or None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def select_refs(project_root: Path, *, branches: tuple[str, ...], tags: str) -> list[SelectedRef]:
|
|
162
|
+
"""Select every version to build: the configured branches first, then the selected tags."""
|
|
163
|
+
|
|
164
|
+
available = _branches(project_root)
|
|
165
|
+
current = current_branch(project_root)
|
|
166
|
+
selected: list[SelectedRef] = []
|
|
167
|
+
seen: set[str] = set()
|
|
168
|
+
for pattern in branches:
|
|
169
|
+
if any(character in pattern for character in "*?["):
|
|
170
|
+
matches = sorted(name for name in available if fnmatch.fnmatchcase(name, pattern))
|
|
171
|
+
elif pattern in available or pattern == current:
|
|
172
|
+
matches = [pattern]
|
|
173
|
+
else:
|
|
174
|
+
raise RuntimeError(
|
|
175
|
+
f"configured branch {pattern!r} does not exist locally or on a remote; "
|
|
176
|
+
"fetch it before building versions"
|
|
177
|
+
)
|
|
178
|
+
for name in matches:
|
|
179
|
+
if name in seen:
|
|
180
|
+
continue
|
|
181
|
+
seen.add(name)
|
|
182
|
+
checkout = None if name == current else available[name]
|
|
183
|
+
selected.append(SelectedRef(name=name, kind="branch", checkout=checkout))
|
|
184
|
+
for tag in select_tags(project_root, tags):
|
|
185
|
+
if tag in seen:
|
|
186
|
+
continue
|
|
187
|
+
seen.add(tag)
|
|
188
|
+
selected.append(SelectedRef(name=tag, kind="tag", checkout=f"refs/tags/{tag}"))
|
|
189
|
+
return selected
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def latest_tag(names: list[str]) -> str | None:
|
|
193
|
+
"""Return the highest semantic version among ``names``."""
|
|
194
|
+
|
|
195
|
+
versions: list[tuple[Version, str]] = []
|
|
196
|
+
for name in names:
|
|
197
|
+
try:
|
|
198
|
+
versions.append((Version(name), name))
|
|
199
|
+
except InvalidVersion:
|
|
200
|
+
continue
|
|
201
|
+
return max(versions)[1] if versions else None
|