Revision control

Copy as Markdown

# Copyright (C) 2025 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
set(__qt_openapi_macros_module_base_dir "${CMAKE_CURRENT_LIST_DIR}" CACHE INTERNAL "")
function(_qt_internal_openapi_detect_target_type target
out_is_shared out_is_static out_is_executable)
cmake_parse_arguments(PARSE_ARGV 1 arg
"${options}" "${oneValueArgs}" "${multiValueArgs}"
)
set(is_shared FALSE)
set(is_static FALSE)
set(is_executable FALSE)
get_target_property(target_type ${target} TYPE)
if(target_type STREQUAL "SHARED_LIBRARY"
OR target_type STREQUAL "MODULE_LIBRARY")
set(is_shared TRUE)
elseif(target_type STREQUAL "STATIC_LIBRARY")
set(is_static TRUE)
elseif(target_type STREQUAL "EXECUTABLE")
set(is_executable TRUE)
else()
message(FATAL_ERROR "Unsupported target type '${target_type}'.")
endif()
set(${out_is_shared} "${is_shared}" PARENT_SCOPE)
set(${out_is_static} "${is_static}" PARENT_SCOPE)
set(${out_is_executable} "${is_executable}" PARENT_SCOPE)
endfunction()
function(qt6_add_openapi_client target)
set(options
GENERATE_DOCUMENTATION
__QT_INTERNAL_GENERATE_COMMON_LIBRARY_TARGET
)
set(oneValueArgs
SPEC_FILE
OUTPUT_DIRECTORY
DOCUMENTATION_OUTPUT_DIRECTORY
OUTPUT_PUBLIC_HEADERS_DIR
OUTPUT_PRIVATE_HEADERS_DIR
)
set(multiValueArgs
ADDITIONAL_PROPERTIES
GENERATE_OPTIONS
JAVA_OPTIONS
)
cmake_parse_arguments(PARSE_ARGV 1 arg
"${options}" "${oneValueArgs}" "${multiValueArgs}"
)
_qt_internal_validate_all_args_are_parsed(arg)
if(NOT arg_SPEC_FILE)
message(FATAL_ERROR "qt6_add_openapi_client: SPEC_FILE is required.")
endif()
if(NOT arg_OUTPUT_DIRECTORY)
set(arg_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}")
endif()
if(NOT TARGET "${target}")
message(FATAL_ERROR "qt6_add_openapi_client: "
"Target '${target}' does not exist. "
"Please create it before calling qt6_add_openapi_client. "
"You can use qt_add_library or qt_add_executable to create the target.")
endif()
if(arg___QT_INTERNAL_GENERATE_COMMON_LIBRARY_TARGET AND NOT QT_BUILDING_QT)
message(FATAL_ERROR "qt6_add_openapi_client: "
"__QT_INTERNAL_GENERATE_COMMON_LIBRARY_TARGET is an internal option, and shouldn't be "
"used outside of the Qt build.")
endif()
if (NOT TARGET "${QT_CMAKE_EXPORT_NAMESPACE}::QtOpenAPIGeneratorJar")
message(FATAL_ERROR
"Can't find ${QT_CMAKE_EXPORT_NAMESPACE}::QtOpenAPIGeneratorJar target, "
"please report an issue on the Qt bugtracker")
return()
endif()
get_target_property(openapi_generator_cli_jar_file
WrapOpenAPIGenerator::WrapOpenAPIGenerator INTERFACE_OPENAPI_GENERATOR_CLI_JAR)
if(NOT openapi_generator_cli_jar_file)
get_target_property(openapi_generator_cli_exec_file
WrapOpenAPIGenerator::WrapOpenAPIGenerator INTERFACE_OPENAPI_GENERATOR_CLI_EXECUTABLE)
if (NOT openapi_generator_cli_exec_file)
message(FATAL_ERROR "qt6_add_openapi_client: "
"Java OpenAPI generator is not installed or not added in PATH.")
endif()
endif()
file(MAKE_DIRECTORY "${arg_OUTPUT_DIRECTORY}")
if(NOT arg_DOCUMENTATION_OUTPUT_DIRECTORY)
set(arg_DOCUMENTATION_OUTPUT_DIRECTORY "${arg_OUTPUT_DIRECTORY}")
endif()
set(generator_name ${QT_OPENAPI_GENERATOR_NAME})
get_target_property(generator_jar_path
"${QT_CMAKE_EXPORT_NAMESPACE}::QtOpenAPIGeneratorJar" IMPORTED_LOCATION)
# Qt pre-generated common library always uses QOAI
# prefix, for generating different common library,
# please call the generator manually.
set(qt_commonlib_prefix "QOAI")
set(qt_common_file_prefix "qoai")
# Qt pre-generated common library always uses QtOpenApiCommon namespace
set(cpp_common_namespace "QtOpenApiCommon")
# The default commonLibGenerationType is defined in CppQt6ClientGenerator.java:
# String commonLibrary = GENERATION_TYPE.COMMON_LIB.value;
if(arg___QT_INTERNAL_GENERATE_COMMON_LIBRARY_TARGET)
set(common_lib_generation_type "Gen-Common-Lib")
else()
set(common_lib_generation_type "Gen-Client-Lib")
endif()
set(common_lib_name "CommonLibrary")
# The common library always needs compression support.
set(compression_required "true")
string(JOIN "," fixed_additional_properties
"--additional-properties=useCmakeMacro=true"
"packageName=${target}"
"cppCommonNamespace=${cpp_common_namespace}"
"prefix=${qt_commonlib_prefix}"
"commonLibrary=${common_lib_generation_type}"
"commonLibraryName=${common_lib_name}"
"contentCompression=${compression_required}"
)
if(arg_ADDITIONAL_PROPERTIES)
string(JOIN "," users_additional_properties ${arg_ADDITIONAL_PROPERTIES})
string(PREPEND users_additional_properties "--additional-properties=")
else()
set(users_additional_properties "")
endif()
set(generating_sources "")
set(common_sources "")
set(client_sources "")
set(common_dir "common")
set(client_dir "client")
set(target_lower_case ${target})
string(TOLOWER ${target} target_lower_case)
set(client_dir_path "${arg_OUTPUT_DIRECTORY}/${client_dir}/")
set(common_dir_path "${arg_OUTPUT_DIRECTORY}/${common_dir}/")
set(extra_dependencies
DEPENDS
"${generator_jar_path}"
"${arg_SPEC_FILE}"
)
if(TARGET QtOpenAPIGenerator)
# otherwise add_custom_command tries to run generator, that is not created yet
list(APPEND extra_dependencies
QtOpenAPIGenerator
)
endif()
# if target is already created, no need to use package_name option for generator
if(TARGET ${target})
set(target_lib_name "")
else()
set(target_lib_name "--package-name=${target}")
endif()
if(arg___QT_INTERNAL_GENERATE_COMMON_LIBRARY_TARGET)
set(comment "Generating the Qt6 Common Library code with the generator: ${generator_name}")
else()
string(CONCAT comment
"Generating the Qt6 Client code for ${target} with the generator: ${generator_name}."
"\nThe specification file is: ${arg_SPEC_FILE}"
)
endif()
if(openapi_generator_cli_jar_file)
if(CMAKE_HOST_WIN32)
set(path_separator "\\;")
else()
set(path_separator ":")
endif()
set(openapi_cli_entrypoint_class "org.openapitools.codegen.OpenAPIGenerator")
set(run_client_cmd
"${generator_jar_path}${path_separator}${openapi_generator_cli_jar_file}")
set(generation_command ${Java_JAVA_EXECUTABLE} ${arg_JAVA_OPTIONS} -cp "${run_client_cmd}" "${openapi_cli_entrypoint_class}")
list(APPEND extra_dependencies "${openapi_generator_cli_jar_file}")
elseif(openapi_generator_cli_exec_file)
# When using the jar, logback.xml is included in it and picked up automatically.
# When using the cli executable, there is no jar on the classpath, so we pass
# the logback config explicitly via JAVA_OPTS. The user's JAVA_OPTIONS come
# after, so they can still override it.
get_filename_component(generator_logback_config
"${__qt_openapi_macros_module_base_dir}/../tools/qtopenapi-generator/src/main/resources/logback.xml"
REALPATH)
set(backup_java_opts "$ENV{JAVA_OPTS}")
list(PREPEND arg_JAVA_OPTIONS "-Dlogback.configurationFile=${generator_logback_config}")
list(JOIN arg_JAVA_OPTIONS " " java_options_str)
set(ENV{JAVA_OPTS}
"${backup_java_opts} ${java_options_str}")
set(generation_command
"${openapi_generator_cli_exec_file}" --custom-generator="${generator_jar_path}")
list(APPEND extra_dependencies "${openapi_generator_cli_exec_file}")
endif()
# We *do not* want to regenerate the sources if the user changes something
# manually in the generated files, but we want to regenerate the sources if
# the files got removed.
set(need_call_generator FALSE)
# The generator writes the list of generated files into
# ${arg_OUTPUT_DIRECTORY}/.openapi-generator/FILES
# Check if this file exists, and if it does, read its
# content and verify that all the listed files exist.
set(generated_files_file "${arg_OUTPUT_DIRECTORY}/.openapi-generator/FILES")
if(NOT EXISTS "${generated_files_file}")
message(DEBUG "File does not exist: ${generated_files_file}")
# the file does not exist
set(need_call_generator TRUE)
else()
# add this file to the dependencies, so that CMake automatically
# reconfigures if it's gone
set_property(DIRECTORY PROPERTY CMAKE_CONFIGURE_DEPENDS "${generated_files_file}")
# get the timestamp of the file - we'll use it later
file(TIMESTAMP "${generated_files_file}" generated_files_file_timestamp)
# read the file and get the list of generated files
file(STRINGS "${generated_files_file}" generated_file_list)
foreach(generated_file IN LISTS generated_file_list)
if(NOT EXISTS "${arg_OUTPUT_DIRECTORY}/${generated_file}")
message(DEBUG "File not found: ${arg_OUTPUT_DIRECTORY}/${generated_file}")
set(need_call_generator TRUE)
break()
endif()
endforeach()
endif()
# We depend on the following things:
# * the yaml file;
# * the Qt generator plugin (because newer version may do smth differently).
# * the upstream OpenAPI generator
# Handle changes in the yaml file. We need to check both the path
# and the timestamp.
if(NOT DEFINED QT_INTERNAL_OPENAPI_${target}_YAML_PATH)
set(QT_INTERNAL_OPENAPI_${target}_YAML_PATH ""
CACHE INTERNAL "Path to the yaml file for ${target}")
endif()
set(yaml_depfile "${arg_OUTPUT_DIRECTORY}/.openapi-generator/yaml_deps.txt")
if(NOT ("${arg_SPEC_FILE}" STREQUAL "${QT_INTERNAL_OPENAPI_${target}_YAML_PATH}"))
# Yaml file path has changed
message(DEBUG "Yaml file path has changed"
"\nOld path: ${QT_INTERNAL_OPENAPI_${target}_YAML_PATH}"
"\nNew path: ${arg_SPEC_FILE}")
set(need_call_generator TRUE)
set(QT_INTERNAL_OPENAPI_${target}_YAML_PATH "${arg_SPEC_FILE}"
CACHE INTERNAL "Path to the yaml file for ${target}" FORCE)
elseif(generated_files_file_timestamp)
# read the file with yaml deps. It also includes ${arg_SPEC_FILE}
message(DEBUG "Reading ${yaml_depfile} to get the list of all yaml dependencies.")
if(EXISTS "${yaml_depfile}")
file(STRINGS "${yaml_depfile}" yaml_depfile_entries)
foreach(yaml_file IN LISTS yaml_depfile_entries)
if(EXISTS "${yaml_file}")
file(TIMESTAMP "${yaml_file}" curr_yaml_timestamp)
if("${generated_files_file_timestamp}" STRLESS "${curr_yaml_timestamp}")
message(DEBUG "${yaml_file} was modified after last check"
"\nModification time is: ${curr_yaml_timestamp}")
set(need_call_generator TRUE)
break() # we do not need to check all
endif()
endif()
endforeach()
else()
# At least check the timestamp of the ${arg_SPEC_FILE} yaml file
message(DEBUG "Could not find ${yaml_depfile}."
"\nChecking the timestamp of ${arg_SPEC_FILE}")
file(TIMESTAMP "${arg_SPEC_FILE}" curr_yaml_timestamp)
if("${generated_files_file_timestamp}" STRLESS "${curr_yaml_timestamp}")
message(DEBUG "Yaml file was modified after last check"
"\nModification time is: ${curr_yaml_timestamp}")
set(need_call_generator TRUE)
endif()
endif()
endif()
# For the Qt6 generator, only check its timestamp.
if(generated_files_file_timestamp)
file(TIMESTAMP "${generator_jar_path}" qt_generator_timestamp)
if("${generated_files_file_timestamp}" STRLESS "${qt_generator_timestamp}")
message(DEBUG "Qt6 Generator was modified after last check")
set(need_call_generator TRUE)
endif()
endif()
# For the upstream generator, also check its timestamp only.
# This covers the case when the upstream generator is updated
# by some package manager.
set(upstream_generator_path "${openapi_generator_cli_jar_file}")
if(NOT upstream_generator_path)
set(upstream_generator_path "${openapi_generator_cli_exec_file}")
endif()
if(generated_files_file_timestamp)
file(TIMESTAMP "${upstream_generator_path}" upstream_generator_timestamp)
if("${generated_files_file_timestamp}" STRLESS "${upstream_generator_timestamp}")
message(DEBUG "Upstream OpenAPI generator was modified after last check")
set(need_call_generator TRUE)
endif()
endif()
set_property(DIRECTORY PROPERTY CMAKE_CONFIGURE_DEPENDS "${upstream_generator_path}")
if(need_call_generator)
message(STATUS "${comment}")
set(extra_args "")
if(NOT QT_FEATURE_developer_build)
list(APPEND extra_args
ERROR_VARIABLE generate_error
OUTPUT_VARIABLE generate_output
)
endif()
execute_process(
COMMAND ${generation_command}
generate -g ${generator_name}
${target_lib_name}
${users_additional_properties}
${arg_GENERATE_OPTIONS}
-i "${arg_SPEC_FILE}"
-o "${arg_OUTPUT_DIRECTORY}"
${fixed_additional_properties}
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
RESULT_VARIABLE generate_result
${extra_args}
)
if(openapi_generator_cli_exec_file)
set(ENV{JAVA_OPTS} ${backup_java_opts})
endif()
if(NOT generate_result)
# now we have a list of all yaml files - add them as dependencies
if(EXISTS "${yaml_depfile}")
# ${arg_SPEC_FILE} is also in the list
file(STRINGS "${yaml_depfile}" yaml_depfile_entries)
foreach(yaml_file IN LISTS yaml_depfile_entries)
set_property(DIRECTORY PROPERTY CMAKE_CONFIGURE_DEPENDS "${yaml_file}")
endforeach()
else()
# at least add ${arg_SPEC_FILE} as a dependency
set_property(DIRECTORY PROPERTY CMAKE_CONFIGURE_DEPENDS "${arg_SPEC_FILE}")
endif()
message(STATUS "Generation completed successfully for ${target}.")
else()
set(error_message "Generation failed. Exit code: ${generate_result}.")
if(generate_output OR generate_error)
string(APPEND error_message "\n stdout: '${generate_output}' "
"\n stderr: '${generate_error}'")
endif()
message(FATAL_ERROR "${error_message}")
endif()
else()
message(STATUS "Skipping the generation step for ${target}, because nothing has changed.")
endif() # need_call_generator
if(arg___QT_INTERNAL_GENERATE_COMMON_LIBRARY_TARGET)
list(APPEND common_sources "${common_dir_path}${qt_common_file_prefix}${target_lower_case}commonexports.h")
list(APPEND common_sources "${common_dir_path}${qt_common_file_prefix}baseapi.h")
list(APPEND common_sources "${common_dir_path}${qt_common_file_prefix}baseapi.cpp")
list(APPEND common_sources "${common_dir_path}${qt_common_file_prefix}commonglobal.h")
list(APPEND common_sources "${common_dir_path}${qt_common_file_prefix}helpers.h")
list(APPEND common_sources "${common_dir_path}${qt_common_file_prefix}helpers.cpp")
list(APPEND common_sources "${common_dir_path}${qt_common_file_prefix}httprequest.h")
list(APPEND common_sources "${common_dir_path}${qt_common_file_prefix}httprequest.cpp")
list(APPEND common_sources "${common_dir_path}${qt_common_file_prefix}httpfileelement.h")
list(APPEND common_sources "${common_dir_path}${qt_common_file_prefix}httpfileelement.cpp")
list(APPEND common_sources "${common_dir_path}${qt_common_file_prefix}object.h")
list(APPEND common_sources "${common_dir_path}${qt_common_file_prefix}object.cpp")
list(APPEND common_sources "${common_dir_path}${qt_common_file_prefix}enum.h")
list(APPEND common_sources "${common_dir_path}${qt_common_file_prefix}enum.cpp")
list(APPEND common_sources "${common_dir_path}${qt_common_file_prefix}serverconfiguration.h")
list(APPEND common_sources "${common_dir_path}${qt_common_file_prefix}serverconfiguration.cpp")
list(APPEND common_sources "${common_dir_path}${qt_common_file_prefix}servervariable.h")
list(APPEND common_sources "${common_dir_path}${qt_common_file_prefix}servervariable.cpp")
list(APPEND generating_sources ${common_sources})
else()
# Re-read the full list of generated files from .openapi-generator/FILES
file(STRINGS "${generated_files_file}" generated_file_list)
list(TRANSFORM generated_file_list PREPEND "${arg_OUTPUT_DIRECTORY}/"
OUTPUT_VARIABLE client_sources)
list(APPEND generating_sources ${client_sources})
endif()
set(is_shared FALSE)
set(is_static FALSE)
set(is_executable FALSE)
if(NOT arg___QT_INTERNAL_GENERATE_COMMON_LIBRARY_TARGET)
set_property(TARGET ${target} PROPERTY AUTOMOC "ON")
target_link_libraries(${target} PRIVATE
${QT_CMAKE_EXPORT_NAMESPACE}::Core
${QT_CMAKE_EXPORT_NAMESPACE}::Network
)
# This is PUBLIC, because the consumer of the client library
# needs to get also the common library includes.
target_link_libraries(${target} PUBLIC Qt6::OpenApiCommon)
endif()
_qt_internal_openapi_detect_target_type(${target}
is_shared is_static is_executable)
set(define_infix "")
if(arg___QT_INTERNAL_GENERATE_COMMON_LIBRARY_TARGET)
set(define_infix "_COMMON")
endif()
set(target_export ${target})
string(TOUPPER ${target} target_export)
if(is_shared)
target_compile_definitions(${target} PRIVATE
${target_export}${define_infix}_LIB_SHARED)
elseif(is_static OR is_executable)
target_compile_definitions(${target} PRIVATE
${target_export}${define_infix}_LIB_STATIC)
endif()
if(NOT is_executable)
target_compile_definitions(${target} PRIVATE
${target_export}_BUILD${define_infix}_LIB)
endif()
target_include_directories(${target} PUBLIC
"$<BUILD_INTERFACE:${arg_OUTPUT_DIRECTORY}>")
target_include_directories(${target} PUBLIC
"$<BUILD_INTERFACE:${arg_OUTPUT_DIRECTORY}/${client_dir}>")
if(arg___QT_INTERNAL_GENERATE_COMMON_LIBRARY_TARGET)
target_include_directories(${target} PRIVATE
"$<BUILD_INTERFACE:${arg_OUTPUT_DIRECTORY}/${common_dir}>")
endif()
if(TARGET QtOpenAPIGenerator)
add_dependencies(${target} QtOpenAPIGenerator)
endif()
if(arg___QT_INTERNAL_GENERATE_COMMON_LIBRARY_TARGET)
target_sources(${target} PRIVATE ${common_sources})
_qt_internal_set_source_file_generated(
SOURCES ${common_sources}
)
else()
target_sources(${target} PRIVATE ${client_sources})
_qt_internal_set_source_file_generated(
SOURCES ${client_sources}
)
endif()
# Generate Doxygen documentation for the Client.
if(arg_GENERATE_DOCUMENTATION)
# If Doxygen is not installed, then just skipping.
if(TARGET Doxygen::doxygen)
file(MAKE_DIRECTORY "${arg_DOCUMENTATION_OUTPUT_DIRECTORY}")
set(configure_doxygen_script
"${__qt_openapi_macros_module_base_dir}/Qt6OpenApiConfigureDoxygenScript.cmake")
set(MAIN_GEN_DOC_FILE
"${arg_DOCUMENTATION_OUTPUT_DIRECTORY}/doc/html/index.html")
set(DOXYGEN_FILE "${client_dir_path}doc/Doxyfile")
add_custom_command(
OUTPUT "${MAIN_GEN_DOC_FILE}"
COMMAND "${CMAKE_COMMAND}"
"-DDOXYGEN_IN_FILE_PATH=${DOXYGEN_FILE}.in"
"-DDOXYGEN_OUT_FILE_PATH=${DOXYGEN_FILE}"
"-DINPUT_DIR=${client_dir_path}"
"-DOUTPUT_DIR=${arg_DOCUMENTATION_OUTPUT_DIRECTORY}/doc"
-P "${configure_doxygen_script}"
COMMAND $<TARGET_FILE:Doxygen::doxygen> "${DOXYGEN_FILE}"
${extra_dependencies}
${generating_sources}
COMMENT "Generating doxygen documentation for target '${target}'"
VERBATIM
COMMAND_EXPAND_LISTS
)
add_custom_target(${target}_openapi_docs ALL
DEPENDS "${MAIN_GEN_DOC_FILE}"
)
else()
message(WARNING "Could not find doxygen to generate documentation. "
"Either install it or make sure it's added to PATH and then call "
"find_package(Doxygen) in your project."
"Skipping documentation generation.")
endif()
endif()
if(arg_OUTPUT_PUBLIC_HEADERS_DIR OR arg_OUTPUT_PRIVATE_HEADERS_DIR)
set(client_public_headers_dir "${arg_OUTPUT_DIRECTORY}/${client_dir}_public_headers")
set(client_private_headers_dir "${arg_OUTPUT_DIRECTORY}/${client_dir}_private_headers")
set(copied_headers_timestamp_file
"${arg_OUTPUT_DIRECTORY}/openapi_client_copied_headers_timestamp.txt")
set(copy_headers_script
"${__qt_openapi_macros_module_base_dir}/Qt6OpenApiToolsCopyHeadersScript.cmake")
add_custom_command(
OUTPUT "${copied_headers_timestamp_file}"
DEPENDS
"${copy_headers_script}"
${generating_sources}
COMMAND "${CMAKE_COMMAND}"
"-DTIMESTAMP_PATH=${copied_headers_timestamp_file}"
"-DCLIENT_DIR=${client_dir_path}"
"-DOUTPUT_PUBLIC_HEADERS_DIR=${client_public_headers_dir}"
"-DOUTPUT_PRIVATE_HEADERS_DIR=${client_private_headers_dir}"
-P "${copy_headers_script}"
COMMENT "Copying generated OpenApi headers for target '${target}'"
VERBATIM
COMMAND_EXPAND_LISTS
)
target_sources(${target} PRIVATE "${copied_headers_timestamp_file}")
endif()
if(arg_OUTPUT_PUBLIC_HEADERS_DIR)
set(${arg_OUTPUT_PUBLIC_HEADERS_DIR} "${client_public_headers_dir}" PARENT_SCOPE)
endif()
if(arg_OUTPUT_PRIVATE_HEADERS_DIR)
set(${arg_OUTPUT_PRIVATE_HEADERS_DIR} "${client_private_headers_dir}" PARENT_SCOPE)
endif()
endfunction()
if(NOT QT_NO_CREATE_VERSIONLESS_FUNCTIONS)
function(qt_add_openapi_client)
qt6_add_openapi_client(${ARGV})
endfunction()
endif()