diff options
author | Alexey Edelev <[email protected]> | 2022-08-15 18:29:41 +0200 |
---|---|---|
committer | Alexey Edelev <[email protected]> | 2022-09-27 13:12:11 +0200 |
commit | b89d63515bb352cecfd87e709320a2db5b6a1906 (patch) | |
tree | 4f6e40d6e14991af1c9082ab7508a49a6360f25a | |
parent | 458ec4cb5442315c2c923ba78faf45fdd729a109 (diff) |
Replace the syncqt.pl script with syncqt tool
syncqt.pl adds an extra dependency on perl when building Qt. Modern C++
provides the convenient cross-platform way to access a filesystem and
to use regular expressions, so we may replace the perl script with C++
application. The syncqt executable is built at configure time and
installed as QtCore tool. It's running at configure time to deliver the
required header files for IDE to build a consistent code model and at
the build time to keep tracking changes in header files and generate
the missing aliases without reconfiguring. 'syncqt' only parses header
files from a CMake build tree, so the resulting Qt installation only
contains interfacing headers that belong to the platform that Qt is
built for. 'sync.profile' files are not used as the 'source of truth'
for sync qt procedure anymore, all the necessary information is taken
from either CMake files at configure time or from the module header
files while parsing them.
syncqt.pl is still in place since it's required as fallback solution
for a smooth transition to the new syncqt implementation for all qt
repositories.
This patchset only enables the C++ based syncqt for 'qtbase'
repository.
From the performance perspective C++ version works faster then perl
script, also the configure time is reduced significally on subsequent
reconfigurations - up x2 times faster when re-configuring repository,
but it also takes time to compile the tool itself the first time.
Numbers for qtbase:
syncqt.pl syncqt.cpp
initial: 0m16,035s 0m20,413s
reconfig: 0m6,819s 0m3,725s
The syncing procedure can be run separately for each module using
<ModuleName>_sync_headers targets. The 'sync_headers' target can be
used to sync all the modules at once.
Task-number: QTBUG-87480
Task-number: QTBUG-103196
Change-Id: I8c938bcaf88a8713b39bbfd66d9e7ef12b2c3523
Reviewed-by: Alexandru Croitor <[email protected]>
31 files changed, 2214 insertions, 182 deletions
diff --git a/.cmake.conf b/.cmake.conf index fce7707bcdb..4f4c0a20174 100644 --- a/.cmake.conf +++ b/.cmake.conf @@ -31,3 +31,6 @@ set(QT_SUPPORTED_MIN_CMAKE_VERSION_FOR_USING_QT_STATIC "3.21") # in sync. set(QT_MIN_NEW_POLICY_CMAKE_VERSION "3.16") set(QT_MAX_NEW_POLICY_CMAKE_VERSION "3.21") + +# Use cpp-based syncqt +set(QT_USE_SYNCQT_CPP TRUE) diff --git a/cmake/QtBaseGlobalTargets.cmake b/cmake/QtBaseGlobalTargets.cmake index ba56ea17041..ac3b63a8746 100644 --- a/cmake/QtBaseGlobalTargets.cmake +++ b/cmake/QtBaseGlobalTargets.cmake @@ -247,6 +247,7 @@ qt_copy_or_install(FILES cmake/QtLalrHelpers.cmake cmake/QtModuleConfig.cmake.in cmake/QtModuleDependencies.cmake.in + cmake/QtModuleHeadersCheck.cmake cmake/QtModuleHelpers.cmake cmake/QtModuleToolsConfig.cmake.in cmake/QtModuleToolsDependencies.cmake.in diff --git a/cmake/QtBuild.cmake b/cmake/QtBuild.cmake index a99bb20c4a8..2db677bef84 100644 --- a/cmake/QtBuild.cmake +++ b/cmake/QtBuild.cmake @@ -228,6 +228,15 @@ if(NOT QT_MKSPECS_DIR) set(QT_MKSPECS_DIR "${QT_MKSPECS_DIR}" CACHE INTERNAL "") endif() +# macOS versions 10.14 and less don't have the implementation of std::filesystem API. +if(CMAKE_HOST_APPLE AND CMAKE_HOST_SYSTEM_VERSION VERSION_LESS "19.0.0") + message(FATAL_ERROR "macOS versions less than 10.15 are not supported for building Qt.") +endif() + +if(NOT DEFINED QT_USE_SYNCQT_CPP) + set(QT_USE_SYNCQT_CPP FALSE) +endif() + # the default RPATH to be used when installing, but only if it's not a system directory list(FIND CMAKE_PLATFORM_IMPLICIT_LINK_DIRECTORIES "${CMAKE_INSTALL_PREFIX}/${INSTALL_LIBDIR}" isSystemDir) if("${isSystemDir}" STREQUAL "-1") diff --git a/cmake/QtBuildInternals/QtBuildInternalsConfig.cmake b/cmake/QtBuildInternals/QtBuildInternalsConfig.cmake index 161c0bf5f11..fc978525a37 100644 --- a/cmake/QtBuildInternals/QtBuildInternalsConfig.cmake +++ b/cmake/QtBuildInternals/QtBuildInternalsConfig.cmake @@ -419,6 +419,10 @@ macro(qt_build_repo_begin) add_dependencies(install_docs install_html_docs install_qch_docs) endif() + if(NOT TARGET sync_headers) + add_custom_target(sync_headers) + endif() + # Add global qt_plugins, qpa_plugins and qpa_default_plugins convenience custom targets. # Internal executables will add a dependency on the qpa_default_plugins target, # so that building and running a test ensures it won't fail at runtime due to a missing qpa @@ -474,6 +478,10 @@ macro(qt_build_repo_begin) if(NOT TARGET benchmark) add_custom_target(benchmark) endif() + + if(QT_INTERNAL_SYNCED_MODULES) + set_property(GLOBAL PROPERTY _qt_synced_modules ${QT_INTERNAL_SYNCED_MODULES}) + endif() endmacro() macro(qt_build_repo_end) @@ -510,6 +518,12 @@ macro(qt_build_repo_end) if(NOT QT_SUPERBUILD) qt_print_build_instructions() endif() + + get_property(synced_modules GLOBAL PROPERTY _qt_synced_modules) + if(synced_modules) + set(QT_INTERNAL_SYNCED_MODULES ${synced_modules} CACHE INTERNAL + "List of the synced modules. Prevents running syncqt.cpp after the first configuring.") + endif() endmacro() macro(qt_build_repo) diff --git a/cmake/QtDocsHelpers.cmake b/cmake/QtDocsHelpers.cmake index 53d1ab4be16..04114cb9219 100644 --- a/cmake/QtDocsHelpers.cmake +++ b/cmake/QtDocsHelpers.cmake @@ -140,6 +140,12 @@ function(qt_internal_add_docs) ) add_dependencies(prepare_docs_${target} qattributionsscanner_${target}) + if(QT_USE_SYNCQT_CPP) + if(NOT TARGET sync_all_public_headers) + add_custom_target(sync_all_public_headers) + endif() + add_dependencies(prepare_docs_${target} sync_all_public_headers) + endif() # generate docs target set(generate_qdoc_args diff --git a/cmake/QtExecutableHelpers.cmake b/cmake/QtExecutableHelpers.cmake index b8bccdbb07d..4bd03c8f8cf 100644 --- a/cmake/QtExecutableHelpers.cmake +++ b/cmake/QtExecutableHelpers.cmake @@ -429,7 +429,7 @@ function(qt_internal_add_configure_time_executable target) set(cmake_flags_arg) if(arg_CMAKE_FLAGS) - set(cmake_flags_arg CMAKE_FLAGS ${arg_CMAKE_FLAGS}) + set(cmake_flags_arg CMAKE_FLAGS "${arg_CMAKE_FLAGS}") endif() configure_file("${template}" "${target_binary_dir}/CMakeLists.txt" @ONLY) try_compile(result diff --git a/cmake/QtFrameworkHelpers.cmake b/cmake/QtFrameworkHelpers.cmake index 7effc579f6e..3b4cb012230 100644 --- a/cmake/QtFrameworkHelpers.cmake +++ b/cmake/QtFrameworkHelpers.cmake @@ -97,6 +97,19 @@ function(qt_copy_framework_headers target) QT_COPIED_FRAMEWORK_HEADERS "${out_files}") endfunction() +function(qt_internal_generate_fake_framework_header target) + # Hack to create the "Headers" symlink in the framework: + # Create a fake header file and copy it into the framework by marking it as PUBLIC_HEADER. + # CMake now takes care of creating the symlink. + set(fake_header "${CMAKE_CURRENT_BINARY_DIR}/${target}_fake_header.h") + qt_internal_get_main_cmake_configuration(main_config) + file(GENERATE OUTPUT "${fake_header}" CONTENT "// ignore this file\n" + CONDITION "$<CONFIG:${main_config}>") + target_sources(${target} PRIVATE "${fake_header}") + set_source_files_properties("${fake_header}" PROPERTIES GENERATED ON) + set_property(TARGET ${target} APPEND PROPERTY PUBLIC_HEADER "${fake_header}") +endfunction() + function(qt_finalize_framework_headers_copy target) get_target_property(target_type ${target} TYPE) if(${target_type} STREQUAL "INTERFACE_LIBRARY") @@ -108,17 +121,7 @@ function(qt_finalize_framework_headers_copy target) endif() get_target_property(headers ${target} QT_COPIED_FRAMEWORK_HEADERS) if(headers) - # Hack to create the "Headers" symlink in the framework: - # Create a fake header file and copy it into the framework by marking it as PUBLIC_HEADER. - # CMake now takes care of creating the symlink. - set(fake_header ${target}_fake_header.h) - qt_internal_get_main_cmake_configuration(main_config) - file(GENERATE OUTPUT ${fake_header} CONTENT "// ignore this file\n" - CONDITION "$<CONFIG:${main_config}>") - string(PREPEND fake_header "${CMAKE_CURRENT_BINARY_DIR}/") - target_sources(${target} PRIVATE ${fake_header}) - set_source_files_properties(${fake_header} PROPERTIES GENERATED ON) - set_property(TARGET ${target} APPEND PROPERTY PUBLIC_HEADER ${fake_header}) + qt_internal_generate_fake_framework_header(${target}) # Add a target, e.g. Core_framework_headers, that triggers the header copy. add_custom_target(${target}_framework_headers DEPENDS ${headers}) diff --git a/cmake/QtHeadersClean.cmake b/cmake/QtHeadersClean.cmake index 29385de5fd8..6300a818321 100644 --- a/cmake/QtHeadersClean.cmake +++ b/cmake/QtHeadersClean.cmake @@ -4,26 +4,18 @@ # Add a custom ${module_target}_headersclean_check target that builds each header in # ${module_headers} with a custom set of defines. This makes sure our public headers # are self-contained, and also compile with more strict compiler options. -function(qt_internal_add_headersclean_target - module_target - module_include_name - module_headers) - # module_headers is a list of strings of the form - # <headerfile>[:feature] +function(qt_internal_add_headersclean_target module_target module_headers) + get_target_property(has_headers ${module_target} _qt_module_has_headers) + if(NOT has_headers) + return() + endif() + set(hclean_headers "") - foreach(entry ${module_headers}) - string(REPLACE ":" ";" entry_list ${entry}) - list(LENGTH entry_list entry_list_length) - list(GET entry_list 0 entry_path) - - if (${entry_list_length} EQUAL 2) - list(GET entry_list 1 entry_feature) - if (NOT QT_FEATURE_${entry_feature}) - message(STATUS "headersclean: Ignoring header ${entry_path} because of missing feature ${entry_feature}") - continue() - endif() + foreach(header IN LISTS module_headers) + get_filename_component(header_name "${header}" NAME) + if(header_name MATCHES "^q[^_]+\\.h$" AND NOT header_name MATCHES ".*(global|exports)\\.h") + list(APPEND hclean_headers "${header}") endif() - list(APPEND hclean_headers ${entry_path}) endforeach() # Make sure that the header compiles with our strict options @@ -53,6 +45,12 @@ function(qt_internal_add_headersclean_target set(target_includes_joined_genex "$<${includes_exist_genex}:-I$<JOIN:${target_includes_genex},;-I>>") + get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) + if(is_multi_config) + list(GET CMAKE_CONFIGURATION_TYPES 0 first_config_type) + set(config_suffix "$<$<NOT:$<CONFIG:${first_config_type}>>:-$<CONFIG>>") + endif() + # qmake doesn't seem to add the defines that are set by the header_only_module when checking the # the cleanliness of the module's header files. # This allows us to bypass an error with CMake 3.18 and lower when trying to evaluate @@ -165,35 +163,23 @@ function(qt_internal_add_headersclean_target endforeach() endif() - foreach(header ${hclean_headers}) - get_filename_component(input_path "${header}" ABSOLUTE) - set(artifact_path "header_check/${header}.o") - get_filename_component(artifact_directory "${artifact_path}" DIRECTORY) - set(comment_header_path "${CMAKE_CURRENT_SOURCE_DIR}/${header}") - file(RELATIVE_PATH comment_header_path "${PROJECT_SOURCE_DIR}" "${comment_header_path}") - - add_custom_command( - OUTPUT "${artifact_path}" - COMMENT "headersclean: Checking header ${comment_header_path}" - COMMAND ${CMAKE_COMMAND} -E make_directory "${artifact_directory}" - COMMAND - ${compiler_to_run} -c ${cxx_flags} - "${target_compile_flags_joined_genex}" - "${target_defines_joined_genex}" - ${hcleanFLAGS} - "${target_includes_joined_genex}" - ${framework_includes} - ${hcleanDEFS} - -xc++ "${input_path}" - -o${artifact_path} - IMPLICIT_DEPENDS CXX - VERBATIM - COMMAND_EXPAND_LISTS - DEPENDS "${input_path}" - ) - list(APPEND hclean_artifacts "${artifact_path}") - endforeach() + set(compiler_command_line + "${compiler_to_run}" "-c" "${cxx_flags}" + "${target_compile_flags_joined_genex}" + "${target_defines_joined_genex}" + "${hcleanFLAGS}" + "${target_includes_joined_genex}" + "${framework_includes}" + "${hcleanDEFS}" + ) + string(JOIN " " compiler_command_line_variables + "-xc++" + "\${INPUT_HEADER_FILE}" + "-o" + "\${OUTPUT_ARTIFACT}" + ) + set(input_header_path_type ABSOLUTE) elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") # -Za would enable strict standards behavior, but we can't add it because # <windows.h> and <GL.h> violate the standards. @@ -202,37 +188,87 @@ function(qt_internal_add_headersclean_target # cl.exe needs a source path get_filename_component(source_path "${QT_MKSPECS_DIR}/features/data/dummy.cpp" REALPATH) - foreach(header ${hclean_headers}) - # We need realpath here to make sure path starts with drive letter - get_filename_component(input_path "${header}" REALPATH) - set(artifact_path "header_${header}.o") - set(comment_header_path "${CMAKE_CURRENT_SOURCE_DIR}/${header}") - file(RELATIVE_PATH comment_header_path "${PROJECT_SOURCE_DIR}" "${comment_header_path}") - - add_custom_command( - OUTPUT "${artifact_path}" - COMMENT "headersclean: Checking header ${comment_header_path}" - COMMAND - ${compiler_to_run} -nologo -c ${CMAKE_CXX_FLAGS} - "${target_compile_flags_joined_genex}" - "${target_defines_joined_genex}" - ${hcleanFLAGS} - "${target_includes_joined_genex}" - ${hcleanDEFS} - -FI "${input_path}" - -Fo${artifact_path} "${source_path}" - IMPLICIT_DEPENDS CXX - VERBATIM - COMMAND_EXPAND_LISTS - DEPENDS "${input_path}" - ) - list(APPEND hclean_artifacts "${artifact_path}") - endforeach() + set(compiler_command_line + "${compiler_to_run}" "-nologo" "-c" "${CMAKE_CXX_FLAGS}" + "${target_compile_flags_joined_genex}" + "${target_defines_joined_genex}" + "${hcleanFLAGS}" + "${target_includes_joined_genex}" + "${hcleanDEFS}" + ) + string(JOIN " " compiler_command_line_variables + "-FI" + "\${INPUT_HEADER_FILE}" + "-Fo\${OUTPUT_ARTIFACT}" + "${source_path}" + ) + + set(input_header_path_type REALPATH) else() message(FATAL_ERROR "CMAKE_CXX_COMPILER_ID \"${CMAKE_CXX_COMPILER_ID}\" is not supported" " for the headersclean check.") endif() + get_target_property(module_include_name ${target} _qt_module_include_name) + + unset(header_check_exceptions) + if(QT_USE_SYNCQT_CPP) + set(header_check_exceptions + "${CMAKE_CURRENT_BINARY_DIR}/${module_include_name}_header_check_exceptions") + endif() + set(headers_check_parameters + "${CMAKE_CURRENT_BINARY_DIR}/${module_target}HeadersCheckParameters${config_suffix}.cmake") + string(JOIN "\n" headers_check_parameters_content + "set(HEADER_CHECK_EXCEPTIONS" + " \"${header_check_exceptions}\")" + "set(HEADER_CHECK_COMPILER_COMMAND_LINE" + " \[\[$<JOIN:${compiler_command_line},\]\]\n \[\[>\]\]\n" + " ${compiler_command_line_variables}" + ")" + ) + file(GENERATE OUTPUT "${headers_check_parameters}" + CONTENT "${headers_check_parameters_content}") + + set(sync_headers_dep "") + if(QT_USE_SYNCQT_CPP) + set(sync_headers_dep "sync_headers") + endif() + + foreach(header ${hclean_headers}) + # We need realpath here to make sure path starts with drive letter + get_filename_component(input_path "${header}" ${input_header_path_type}) + + get_filename_component(input_file_name ${input_path} NAME) + set(artifact_path "${CMAKE_CURRENT_BINARY_DIR}/header_check/${input_file_name}.o") + + if(input_path MATCHES "${CMAKE_BINARY_DIR}") + set(input_base_dir "${CMAKE_BINARY_DIR}") + elseif(input_path MATCHES "${CMAKE_SOURCE_DIR}") + set(input_base_dir "${CMAKE_SOURCE_DIR}") + endif() + file(RELATIVE_PATH comment_header_path "${input_base_dir}" "${input_path}") + + add_custom_command( + OUTPUT "${artifact_path}" + COMMENT "headersclean: Checking header ${comment_header_path}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/header_check" + COMMAND ${CMAKE_COMMAND} + -DINPUT_HEADER_FILE=${input_path} + -DOUTPUT_ARTIFACT=${artifact_path} + -DPARAMETERS=${headers_check_parameters} + -P "${QT_CMAKE_DIR}/QtModuleHeadersCheck.cmake" + IMPLICIT_DEPENDS CXX + VERBATIM + COMMAND_EXPAND_LISTS + DEPENDS + ${headers_check_parameters} + ${sync_headers_dep} + ${input_path} + ${header_check_exceptions} + ) + list(APPEND hclean_artifacts "${artifact_path}") + endforeach() + add_custom_target(${module_target}_headersclean_check COMMENT "headersclean: Checking headers in ${module_include_name}" DEPENDS ${hclean_artifacts} diff --git a/cmake/QtModuleHeadersCheck.cmake b/cmake/QtModuleHeadersCheck.cmake new file mode 100644 index 00000000000..d241f5bb554 --- /dev/null +++ b/cmake/QtModuleHeadersCheck.cmake @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 3.16) +# The PARAMETERS file should specify the following variables for the correct work of +# this script: +# HEADER_CHECK_EXCEPTIONS - path to file that contains exceptions. +# The file is created by syncqt. +# +# HEADER_CHECK_COMPILER_COMMAND_LINE - compiler command line +include("${PARAMETERS}") + +if(EXISTS ${HEADER_CHECK_EXCEPTIONS}) + file(READ ${HEADER_CHECK_EXCEPTIONS} header_check_exception_list) +endif() + +file(TO_CMAKE_PATH "${INPUT_HEADER_FILE}" header) +foreach(exception IN LISTS header_check_exception_list) + file(TO_CMAKE_PATH "${exception}" exception) + if(exception STREQUAL header) + file(WRITE "${OUTPUT_ARTIFACT}" "skipped") + return() + endif() +endforeach() + +execute_process(COMMAND ${HEADER_CHECK_COMPILER_COMMAND_LINE} + RESULT_VARIABLE result + OUTPUT_VARIABLE output + ERROR_VARIABLE output +) + +if(NOT result EQUAL 0) + message(FATAL_ERROR "${INPUT_HEADER_FILE} header check" + " failed: ${HEADER_CHECK_COMPILER_COMMAND_LINE}\n" + " ${output}") +endif() diff --git a/cmake/QtModuleHelpers.cmake b/cmake/QtModuleHelpers.cmake index cf64e485e81..a5ff37d5309 100644 --- a/cmake/QtModuleHelpers.cmake +++ b/cmake/QtModuleHelpers.cmake @@ -29,6 +29,7 @@ macro(qt_internal_get_internal_add_module_keywords option_args single_args multi EXTERNAL_HEADERS_DIR PRIVATE_HEADER_FILTERS QPA_HEADER_FILTERS + HEADER_SYNC_SOURCE_DIRECTORY ${__default_target_info_args} ) set(${multi_args} @@ -111,6 +112,12 @@ endfunction() # QPA_HEADER_FILTERS # The regular expressions that filter QPA header files out of target sources. # The value must use the following format 'regex1|regex2|regex3'. +# +# HEADER_SYNC_SOURCE_DIRECTORY +# The source directory for header sync procedure. Header files outside this directory will be +# ignored by syncqt. The specifying this directory allows to skip the parsing of the whole +# CMAKE_CURRENT_SOURCE_DIR for the header files that needs to be synced and only parse the +# single subdirectory, that meanwhile can be outside the CMAKE_CURRENT_SOURCE_DIR tree. function(qt_internal_add_module target) qt_internal_get_internal_add_module_keywords( module_option_args @@ -382,13 +389,18 @@ function(qt_internal_add_module target) else() set_property(TARGET ${target} APPEND PROPERTY EXPORT_PROPERTIES _qt_module_include_name) set_target_properties("${target}" PROPERTIES - _qt_module_include_name "${module_include_name}") + _qt_module_include_name "${module_include_name}" + _qt_module_has_headers ON + ) - # Use QT_BUILD_DIR for the syncqt call. - # So we either write the generated files into the qtbase non-prefix build root, or the - # module specific build root. + # Need to call qt_ensure_sync_qt to install syncqt.pl script. qt_ensure_sync_qt() - set(syncqt_full_command "${HOST_PERL}" -w "${QT_SYNCQT}" + # Repo uses old perl script to sync files. + if(NOT QT_USE_SYNCQT_CPP) + # Use QT_BUILD_DIR for the syncqt call. + # So we either write the generated files into the qtbase non-prefix build root, or the + # module specific build root. + set(syncqt_full_command "${HOST_PERL}" -w "${QT_SYNCQT}" -quiet -check-includes -module "${module_include_name}" @@ -396,18 +408,24 @@ function(qt_internal_add_module target) -outdir "${QT_BUILD_DIR}" -builddir "${PROJECT_BINARY_DIR}" "${PROJECT_SOURCE_DIR}") - message(STATUS "Running syncqt for module: '${module_include_name}' ") - execute_process(COMMAND ${syncqt_full_command} RESULT_VARIABLE syncqt_ret) - if(NOT syncqt_ret EQUAL 0) - message(FATAL_ERROR "Failed to run syncqt, return code: ${syncqt_ret}") - endif() - - set_target_properties("${target}" PROPERTIES - _qt_module_has_headers ON) - - ### FIXME: Can we replace headers.pri? - qt_read_headers_pri("${module_build_interface_include_dir}" "module_headers") + message(STATUS "Running syncqt for module: '${module_include_name}' ") + execute_process(COMMAND ${syncqt_full_command} RESULT_VARIABLE syncqt_ret) + if(NOT syncqt_ret EQUAL 0) + message(FATAL_ERROR "Failed to run syncqt, return code: ${syncqt_ret}") + endif() + ### FIXME: Can we replace headers.pri? + qt_read_headers_pri("${module_build_interface_include_dir}" "module_headers") + set_property(TARGET ${target} APPEND PROPERTY + _qt_module_timestamp_dependencies "${module_headers_generated}") + else() + set(sync_source_directory "${CMAKE_CURRENT_SOURCE_DIR}") + if(arg_HEADER_SYNC_SOURCE_DIRECTORY) + set(sync_source_directory "${arg_HEADER_SYNC_SOURCE_DIRECTORY}") + endif() + set_target_properties(${target} PROPERTIES + _qt_sync_source_directory "${sync_source_directory}") + endif() # We should not generate export headers if module is defined as pure STATIC. # Static libraries don't need to export their symbols, and corner cases when sources are # also used in shared libraries, should be handled manually. @@ -428,6 +446,9 @@ function(qt_internal_add_module target) set(module_depends_header "${module_build_interface_include_dir}/${module_include_name}Depends") + set_source_files_properties("${module_depends_header}" PROPERTIES GENERATED TRUE) + set_target_properties(${target} PROPERTIES _qt_module_depends_header + "${module_depends_header}") if(NOT ${arg_HEADER_MODULE}) set(module_header "${module_build_interface_include_dir}/${module_include_name}") set_property(TARGET "${target}" PROPERTY MODULE_HEADER @@ -455,15 +476,17 @@ function(qt_internal_add_module target) DESTINATION "${module_install_interface_include_dir}" ) else() - if(arg_EXTERNAL_HEADERS) - set(module_headers_public "${arg_EXTERNAL_HEADERS}") + if(NOT QT_USE_SYNCQT_CPP) + if(arg_EXTERNAL_HEADERS) + set(module_headers_public "${arg_EXTERNAL_HEADERS}") + endif() + qt_internal_install_module_headers(${target} + PUBLIC + ${module_headers_public} + "${module_depends_header}" + "${module_header}" + ) endif() - qt_internal_install_module_headers(${target} - PUBLIC - ${module_headers_public} - "${module_depends_header}" - "${module_header}" - ) endif() endif() @@ -650,7 +673,7 @@ function(qt_internal_add_module target) ) endif() - if(NOT arg_HEADER_MODULE) + if(NOT arg_HEADER_MODULE AND NOT QT_USE_SYNCQT_CPP) if(DEFINED module_headers_private) qt_internal_add_linker_version_script("${target}" PRIVATE_HEADERS ${module_headers_private} ${module_headers_qpa}) else() @@ -671,7 +694,7 @@ function(qt_internal_add_module target) string(APPEND final_injections "${extra_library_injections} ") endif() - if(final_injections) + if(final_injections AND NOT QT_USE_SYNCQT_CPP) qt_install_injections(${target} "${QT_BUILD_DIR}" "${QT_INSTALL_DIR}" ${final_injections}) endif() @@ -853,10 +876,9 @@ set(QT_LIBINFIX \"${QT_LIBINFIX}\")") endif() endif() - if(QT_FEATURE_headersclean AND NOT arg_NO_MODULE_HEADERS) + if(QT_FEATURE_headersclean AND NOT arg_NO_MODULE_HEADERS AND NOT QT_USE_SYNCQT_CPP) qt_internal_add_headersclean_target( ${target} - "${module_include_name}" "${module_headers_clean}") endif() @@ -890,16 +912,29 @@ endfunction() function(qt_finalize_module target) qt_internal_collect_module_headers(module_headers ${target}) - set_property(TARGET ${target} APPEND PROPERTY - _qt_module_timestamp_dependencies "${module_headers_public}") # qt_internal_install_module_headers needs to be called before # qt_finalize_framework_headers_copy, because the last uses the QT_COPIED_FRAMEWORK_HEADERS - # property which supposed to be updated inside every qt_internal_install_module_headers call. - qt_internal_install_module_headers(${target} - PRIVATE ${module_headers_private} - QPA ${module_headers_qpa} - ) + # property which supposed to be updated inside every qt_internal_install_module_headers + # call. + if(QT_USE_SYNCQT_CPP) + if(QT_FEATURE_headersclean) + qt_internal_add_headersclean_target(${target} "${module_headers_public}") + endif() + qt_internal_target_sync_headers(${target} "${module_headers_all}" + "${module_headers_generated}") + get_target_property(module_depends_header ${target} _qt_module_depends_header) + qt_internal_install_module_headers(${target} + PUBLIC ${module_headers_public} "${module_depends_header}" + PRIVATE ${module_headers_private} + QPA ${module_headers_qpa} + ) + else() + qt_internal_install_module_headers(${target} + PRIVATE ${module_headers_private} + QPA ${module_headers_qpa} + ) + endif() qt_finalize_framework_headers_copy(${target}) qt_generate_prl_file(${target} "${INSTALL_LIBDIR}") @@ -1098,6 +1133,13 @@ function(qt_internal_generate_cpp_global_exports target module_define_infix) set(${out_public_header} "${generated_header_path}" PARENT_SCOPE) target_sources(${target} PRIVATE "${generated_header_path}") + set_source_files_properties("${generated_header_path}" PROPERTIES GENERATED TRUE) + if(NOT QT_USE_SYNCQT_CPP) + qt_internal_install_module_headers(${target} + PUBLIC + "${generated_header_path}" + ) + endif() if(arg_GENERATE_PRIVATE_CPP_EXPORTS) set(generated_private_header_path @@ -1110,35 +1152,7 @@ function(qt_internal_generate_cpp_global_exports target module_define_infix) set(${out_private_header} "${generated_private_header_path}" PARENT_SCOPE) target_sources(${target} PRIVATE "${generated_private_header_path}") - endif() - - get_target_property(is_framework ${target} FRAMEWORK) - - get_target_property(target_type ${target} TYPE) - set(is_interface_lib 0) - if(target_type STREQUAL "INTERFACE_LIBRARY") - set(is_interface_lib 1) - endif() - - set_property(TARGET ${target} APPEND PROPERTY - _qt_module_timestamp_dependencies "${generated_header_path}") - - if(is_framework) - if(NOT is_interface_lib) - qt_copy_framework_headers(${target} PUBLIC "${generated_header_path}") - - if(arg_GENERATE_PRIVATE_CPP_EXPORTS) - qt_copy_framework_headers(${target} PRIVATE "${generated_private_header_path}") - endif() - endif() - else() - qt_install(FILES "${generated_header_path}" - DESTINATION "${module_install_interface_include_dir}") - - if(arg_GENERATE_PRIVATE_CPP_EXPORTS) - qt_install(FILES "${generated_private_header_path}" - DESTINATION "${module_install_interface_private_include_dir}") - endif() + set_source_files_properties("${generated_private_header_path}" PROPERTIES GENERATED TRUE) endif() endfunction() @@ -1180,10 +1194,9 @@ function(qt_internal_install_module_headers target) qt_install(FILES ${arg_PRIVATE} DESTINATION "${module_install_interface_private_include_dir}") endif() - endif() - - if(arg_QPA) - qt_install(FILES ${arg_QPA} DESTINATION "${module_install_interface_qpa_include_dir}") + if(arg_QPA) + qt_install(FILES ${arg_QPA} DESTINATION "${module_install_interface_qpa_include_dir}") + endif() endif() endfunction() @@ -1191,6 +1204,7 @@ function(qt_internal_collect_module_headers out_var target) set(${out_var}_public "") set(${out_var}_private "") set(${out_var}_qpa "") + set(${out_var}_all "") qt_internal_get_target_sources(sources ${target}) @@ -1203,6 +1217,7 @@ function(qt_internal_collect_module_headers out_var target) if(NOT file_name MATCHES ".+\\.h$") continue() endif() + get_source_file_property(is_generated "${file_path}" GENERATED) get_filename_component(file_path "${file_path}" ABSOLUTE) get_filename_component(file_path "${file_path}" REALPATH) list(APPEND ${out_var}_all "${file_path}") @@ -1213,6 +1228,9 @@ function(qt_internal_collect_module_headers out_var target) elseif(NOT public_filter OR file_name MATCHES "${public_filter}") list(APPEND ${out_var}_public "${file_path}") endif() + if(is_generated) + list(APPEND ${out_var}_generated "${file_path}") + endif() endforeach() set(header_types public private qpa) @@ -1226,6 +1244,8 @@ function(qt_internal_collect_module_headers out_var target) set(${out_var}_${header_type} "${${out_var}_${header_type}}" PARENT_SCOPE) endforeach() + set(${out_var}_all "${${out_var}_all}" PARENT_SCOPE) + set(${out_var}_generated "${${out_var}_generated}" PARENT_SCOPE) if(has_header_types_properties) set_target_properties(${target} PROPERTIES ${has_header_types_properties}) diff --git a/cmake/QtPluginHelpers.cmake b/cmake/QtPluginHelpers.cmake index 7b4f1ae669f..aca5421221d 100644 --- a/cmake/QtPluginHelpers.cmake +++ b/cmake/QtPluginHelpers.cmake @@ -276,6 +276,8 @@ function(qt_internal_add_plugin target) ) endif() endif() + + qt_internal_add_autogen_sync_header_dependencies(${target} ${qt_module_target}) endif() # Change the configuration file install location for qml plugins into the Qml package location. diff --git a/cmake/QtPostProcessHelpers.cmake b/cmake/QtPostProcessHelpers.cmake index 967413265c6..0ca6bdbfa92 100644 --- a/cmake/QtPostProcessHelpers.cmake +++ b/cmake/QtPostProcessHelpers.cmake @@ -1,8 +1,9 @@ # Copyright (C) 2022 The Qt Company Ltd. # SPDX-License-Identifier: BSD-3-Clause -function(qt_internal_write_depends_file target module_include_name) - set(outfile "${QT_BUILD_DIR}/include/${module_include_name}/${module_include_name}Depends") +function(qt_internal_write_depends_file target) + get_target_property(module_depends_header ${target} _qt_module_depends_header) + set(outfile "${module_depends_header}") set(contents "/* This file was generated by cmake with the info from ${target} target. */\n") string(APPEND contents "#ifdef __cplusplus /* create empty PCH in C mode */\n") foreach (m ${ARGN}) @@ -249,8 +250,7 @@ function(qt_internal_create_module_depends_file target) get_target_property(hasModuleHeaders "${target}" _qt_module_has_headers) if (${hasModuleHeaders}) - get_target_property(module_include_name "${target}" _qt_module_include_name) - qt_internal_write_depends_file(${target} ${module_include_name} ${qtdeps}) + qt_internal_write_depends_file(${target} ${qtdeps}) endif() if(third_party_deps OR main_module_tool_deps OR target_deps) diff --git a/cmake/QtSyncQtHelpers.cmake b/cmake/QtSyncQtHelpers.cmake index 40f9cf8e253..fa7e2bf066c 100644 --- a/cmake/QtSyncQtHelpers.cmake +++ b/cmake/QtSyncQtHelpers.cmake @@ -206,3 +206,269 @@ function(qt_compute_injection_forwarding_header target) string(APPEND ${arg_OUT_VAR} " ${relpath}:${fwd}") set(${arg_OUT_VAR} ${${arg_OUT_VAR}} PARENT_SCOPE) endfunction() + +# The function generates the Qt module header structure in build directory and creates install +# rules. Apart the lists of header files the function takes into account +# QT_REPO_PUBLIC_NAMESPACE_REGEX cache variable, that can be set by repository in .cmake.conf file. +# The variable tells the syncqt program, what namespaces are treated as public. Symbols in public +# namespaces are considered when generating CaMeL case header files. +function(qt_internal_target_sync_headers target module_headers module_headers_generated) + if(NOT TARGET ${QT_CMAKE_EXPORT_NAMESPACE}::syncqt) + message(FATAL_ERROR "${QT_CMAKE_EXPORT_NAMESPACE}::syncqt is not a target.") + endif() + get_target_property(has_headers ${target} _qt_module_has_headers) + if(NOT has_headers) + return() + endif() + + qt_internal_module_info(module "${target}") + + get_target_property(sync_source_directory ${target} _qt_sync_source_directory) + set(syncqt_timestamp "${CMAKE_CURRENT_BINARY_DIR}/${target}_syncqt_timestamp") + set(syncqt_outputs "${syncqt_timestamp}") + + set(is_interface_lib FALSE) + get_target_property(type ${target} TYPE) + if(type STREQUAL "INTERFACE_LIBRARY") + set(is_interface_lib TRUE) + endif() + + set(version_script_private_content_file "") + if(NOT is_interface_lib) + list(APPEND syncqt_outputs + "${module_build_interface_include_dir}/${module}Version" + "${module_build_interface_include_dir}/qt${module_lower}version.h") + if(TEST_ld_version_script) + set(version_script_private_content_file + "${CMAKE_CURRENT_BINARY_DIR}/${target}.version.private_content") + set(version_script_args + "-versionScript" "${version_script_private_content_file}") + list(APPEND syncqt_outputs "${version_script_private_content_file}") + qt_internal_add_linker_version_script(${target} + PRIVATE_CONTENT_FILE "${version_script_private_content_file}") + endif() + endif() + + # Check for _qt_module_is_3rdparty_header_library flag to detect non-Qt modules and + # indicate this to syncqt. + get_target_property(is_3rd_party_library ${target} _qt_module_is_3rdparty_header_library) + set(non_qt_module_argument "") + if(is_3rd_party_library) + set(non_qt_module_argument "-nonQt") + else() + list(APPEND syncqt_outputs "${module_build_interface_include_dir}/${module}") + if(QT_FEATURE_headersclean) + list(APPEND syncqt_outputs + "${CMAKE_CURRENT_BINARY_DIR}/${module}_header_check_exceptions") + endif() + endif() + + set(is_framework FALSE) + if(NOT is_interface_lib) + get_target_property(is_framework ${target} FRAMEWORK) + if(is_framework) + qt_internal_get_framework_info(fw ${target}) + get_target_property(fw_output_base_dir ${target} LIBRARY_OUTPUT_DIRECTORY) + set(framework_args "-framework" + "-frameworkIncludeDir" "${fw_output_base_dir}/${fw_versioned_header_dir}" + ) + endif() + endif() + + qt_internal_get_qt_all_known_modules(known_modules) + + get_target_property(is_internal_module ${target} _qt_is_internal_module) + set(internal_module_argument "") + if(is_internal_module) + set(internal_module_argument "-internal") + endif() + + get_target_property(qpa_filter_regex ${target} _qt_module_qpa_headers_filter_regex) + get_target_property(private_filter_regex ${target} _qt_module_private_headers_filter_regex) + + # We need to use the real paths since otherwise it may lead to the invalid work of the + # std::filesystem API + get_filename_component(source_dir_real "${sync_source_directory}" REALPATH) + get_filename_component(binary_dir_real "${CMAKE_CURRENT_BINARY_DIR}" REALPATH) + + if(QT_REPO_PUBLIC_NAMESPACE_REGEX) + set(public_namespaces_filter -publicNamespaceFilter "${QT_REPO_PUBLIC_NAMESPACE_REGEX}") + endif() + + if(qpa_filter_regex) + set(qpa_filter_argument + -qpaHeadersFilter "${qpa_filter_regex}" + ) + endif() + + set(common_syncqt_arguments + -module "${module}" + -sourceDir "${source_dir_real}" + -binaryDir "${binary_dir_real}" + -privateHeadersFilter "${private_filter_regex}" + -includeDir "${module_build_interface_include_dir}" + -privateIncludeDir "${module_build_interface_private_include_dir}" + -qpaIncludeDir "${module_build_interface_qpa_include_dir}" + ${qpa_filter_argument} + ${public_namespaces_filter} + ${non_qt_module_argument} + ${internal_module_argument} + ) + + if(QT_INTERNAL_ENABLE_SYNCQT_DEBUG_OUTPUT) + list(APPEND common_syncqt_arguments -debug) + endif() + + + if(is_framework) + list(REMOVE_ITEM module_headers "${CMAKE_CURRENT_BINARY_DIR}/${target}_fake_header.h") + endif() + + # Filter the generated ui_ header files and header files located in the 'doc/' subdirectory. + list(FILTER module_headers EXCLUDE REGEX + "(.+/(ui_)[^/]+\\.h|${CMAKE_CURRENT_SOURCE_DIR}(/.+)?/doc/+\\.h)") + + set(module_headers_rsp "${binary_dir_real}/module_headers") + list(JOIN module_headers "\n" module_headers_string) + qt_configure_file_v2(OUTPUT "${module_headers_rsp}" CONTENT "${module_headers_string}") + + set(module_headers_generated_rsp "${binary_dir_real}/module_headers_generated") + list(JOIN module_headers_generated "\n" module_headers_generated_string) + qt_configure_file_v2(OUTPUT "${module_headers_generated_rsp}" CONTENT + "${module_headers_generated_string}") + + set(syncqt_staging_dir "${module_build_interface_include_dir}/.syncqt_staging") + add_custom_command( + OUTPUT + ${syncqt_outputs} + COMMAND + ${QT_CMAKE_EXPORT_NAMESPACE}::syncqt + ${common_syncqt_arguments} + -headers "@${module_headers_rsp}" + -generatedHeaders "@${module_headers_generated_rsp}" + -stagingDir "${syncqt_staging_dir}" + -knownModules ${known_modules} + ${framework_args} + ${version_script_args} + COMMAND + ${CMAKE_COMMAND} -E touch "${syncqt_timestamp}" + DEPENDS + ${module_headers_rsp} + ${module_headers_generated_rsp} + ${module_headers} + ${QT_CMAKE_EXPORT_NAMESPACE}::syncqt + COMMENT + "Running syncqt.cpp for module: ${module}" + VERBATIM + ) + add_custom_target(${target}_sync_headers + DEPENDS + ${syncqt_outputs} + ) + add_dependencies(sync_headers ${target}_sync_headers) + + # This target is required when building docs, to make all header files and their aliases + # available for qdoc. + # ${target}_sync_headers is added as dependency to make sure that + # ${target}_sync_all_public_headers is running after ${target}_sync_headers, when building docs. + add_custom_target(${target}_sync_all_public_headers + COMMAND + ${QT_CMAKE_EXPORT_NAMESPACE}::syncqt + ${common_syncqt_arguments} + -all + DEPENDS + ${module_headers} + ${QT_CMAKE_EXPORT_NAMESPACE}::syncqt + ${target}_sync_headers + VERBATIM + ) + + if(NOT TARGET sync_all_public_headers) + add_custom_target(sync_all_public_headers) + endif() + add_dependencies(sync_all_public_headers ${target}_sync_all_public_headers) + + if(NOT is_3rd_party_library AND NOT is_framework) + # Install all the CaMeL style aliases of header files from the staging directory in one rule + qt_install(DIRECTORY "${syncqt_staging_dir}/" + DESTINATION "${module_install_interface_include_dir}" + ) + endif() + + if(NOT is_interface_lib) + set_property(TARGET ${target} + APPEND PROPERTY AUTOGEN_TARGET_DEPENDS "${target}_sync_headers") + endif() + add_dependencies(${target} "${target}_sync_headers") + + + get_target_property(private_module_target ${target} _qt_private_module_target_name) + if(private_module_target) + add_dependencies(${private_module_target} "${target}_sync_headers") + endif() + + # Run sync Qt first time at configure step to make all header files available for the code model + # of IDEs. + get_property(synced_modules GLOBAL PROPERTY _qt_synced_modules) + if(NOT "${module}" IN_LIST synced_modules) + message(STATUS "Running syncqt.cpp for module: ${module}") + get_target_property(syncqt_location ${QT_CMAKE_EXPORT_NAMESPACE}::syncqt LOCATION) + execute_process( + COMMAND + ${syncqt_location} + ${common_syncqt_arguments} + -headers "@${module_headers_rsp}" + -generatedHeaders "@${module_headers_generated_rsp}" + -stagingDir "${syncqt_staging_dir}" + -knownModules ${known_modules} + ${framework_args} + RESULT_VARIABLE syncqt_result + OUTPUT_VARIABLE syncqt_output + ERROR_VARIABLE syncqt_output + ) + if(NOT syncqt_result EQUAL 0) + message(FATAL_ERROR + "Unable to execute syncqt.cpp for module ${target}: ${syncqt_output}") + endif() + set_property(GLOBAL APPEND PROPERTY _qt_synced_modules ${module}) + endif() +endfunction() + +function(qt_internal_collect_sync_header_dependencies out_var skip_non_existing) + if(NOT QT_USE_SYNCQT_CPP) + set(${out_var} "" PARENT_SCOPE) + return() + endif() + + list(LENGTH ARGN sync_headers_target_count) + if(sync_headers_target_count EQUAL 0) + message(FATAL_ERROR "Invalid use of qt_internal_collect_sync_header_dependencies," + " dependencies are not specified") + endif() + + set(${out_var} "") + foreach(sync_headers_target IN LISTS ARGN) + set(sync_headers_target "${sync_headers_target}_sync_headers") + if(NOT skip_non_existing OR TARGET ${sync_headers_target}) + list(APPEND ${out_var} ${sync_headers_target}) + endif() + endforeach() + list(REMOVE_DUPLICATES ${out_var}) + + set(${out_var} "${${out_var}}" PARENT_SCOPE) +endfunction() + +function(qt_internal_add_sync_header_dependencies target) + qt_internal_collect_sync_header_dependencies(sync_headers_targets FALSE ${ARGN}) + if(sync_headers_targets) + add_dependencies(${target} ${sync_headers_targets}) + endif() +endfunction() + +function(qt_internal_add_autogen_sync_header_dependencies target) + qt_internal_collect_sync_header_dependencies(sync_headers_targets TRUE ${ARGN}) + foreach(sync_headers_target IN LISTS sync_headers_targets) + set_property(TARGET ${target} APPEND PROPERTY AUTOGEN_TARGET_DEPENDS + "${sync_headers_target}") + endforeach() +endfunction() diff --git a/cmake/QtToolHelpers.cmake b/cmake/QtToolHelpers.cmake index 4184610ecd3..8af9e50d685 100644 --- a/cmake/QtToolHelpers.cmake +++ b/cmake/QtToolHelpers.cmake @@ -634,10 +634,11 @@ function(qt_internal_add_configure_time_tool target_name) set(extra_args "INSTALL_DIRECTORY" "${install_dir}") endif() + string(REPLACE "\\\;" "\\\\\\\;" unparsed_arguments "${arg_UNPARSED_ARGUMENTS}") qt_internal_add_configure_time_executable(${target_name} OUTPUT_NAME ${name} ${extra_args} - ${arg_UNPARSED_ARGUMENTS} + ${unparsed_arguments} ) if(NOT arg_NO_INSTALL AND arg_TOOLS_TARGET) diff --git a/qmake/CMakeLists.txt b/qmake/CMakeLists.txt index bb455d28df2..8b74b85c495 100644 --- a/qmake/CMakeLists.txt +++ b/qmake/CMakeLists.txt @@ -15,6 +15,9 @@ qt_add_library(QtLibraryInfo OBJECT propertyprinter.cpp propertyprinter.h qmakelibraryinfo.cpp qmakelibraryinfo.h ) + +qt_internal_add_sync_header_dependencies(QtLibraryInfo Core) + set_target_properties(QtLibraryInfo PROPERTIES COMPILE_OPTIONS $<TARGET_PROPERTY:Qt::Core,INTERFACE_COMPILE_OPTIONS> COMPILE_DEFINITIONS $<TARGET_PROPERTY:Qt::Core,INTERFACE_COMPILE_DEFINITIONS> diff --git a/src/3rdparty/harfbuzz-ng/CMakeLists.txt b/src/3rdparty/harfbuzz-ng/CMakeLists.txt index d52a5b7c5b9..b03ad1279c3 100644 --- a/src/3rdparty/harfbuzz-ng/CMakeLists.txt +++ b/src/3rdparty/harfbuzz-ng/CMakeLists.txt @@ -70,6 +70,8 @@ qt_internal_add_3rdparty_library(BundledHarfbuzz $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/harfbuzz> ) +qt_internal_add_sync_header_dependencies(BundledHarfbuzz Core) + # GHS compiler doesn't support the __restrict keyword if(INTEGRITY) target_compile_definitions(BundledHarfbuzz PRIVATE __restrict=) diff --git a/src/3rdparty/zlib/CMakeLists.txt b/src/3rdparty/zlib/CMakeLists.txt index 701dab7dd30..ee9ece80fc3 100644 --- a/src/3rdparty/zlib/CMakeLists.txt +++ b/src/3rdparty/zlib/CMakeLists.txt @@ -34,6 +34,8 @@ qt_internal_add_3rdparty_library(BundledZLIB $<TARGET_PROPERTY:Core,INCLUDE_DIRECTORIES> ) +qt_internal_add_sync_header_dependencies(BundledZLIB Core) + qt_disable_warnings(BundledZLIB) qt_set_symbol_visibility_hidden(BundledZLIB) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index df235433a22..0a2c955ffe0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -15,6 +15,8 @@ if(QT_FEATURE_gui) qt_feature_evaluate_features("${CMAKE_CURRENT_SOURCE_DIR}/gui/configure.cmake") endif() +add_subdirectory(tools/syncqt) + function(find_or_build_bootstrap_names) if (QT_WILL_BUILD_TOOLS) add_subdirectory(tools/bootstrap) # bootstrap library diff --git a/src/corelib/CMakeLists.txt b/src/corelib/CMakeLists.txt index cd698a57efa..1b41c659dc9 100644 --- a/src/corelib/CMakeLists.txt +++ b/src/corelib/CMakeLists.txt @@ -377,10 +377,15 @@ endif() # additional json files. qt6_extract_metatypes(Core ${core_metatype_args}) -set_property(TARGET Core APPEND PROPERTY - PUBLIC_HEADER "${CMAKE_CURRENT_BINARY_DIR}/global/qconfig.h") -set_property(TARGET Core APPEND PROPERTY - PRIVATE_HEADER "${CMAKE_CURRENT_BINARY_DIR}/global/qconfig_p.h") +target_sources(Core PRIVATE + "${CMAKE_CURRENT_BINARY_DIR}/global/qconfig.h" + "${CMAKE_CURRENT_BINARY_DIR}/global/qconfig_p.h" +) +set_source_files_properties( + "${CMAKE_CURRENT_BINARY_DIR}/global/qconfig.h" + "${CMAKE_CURRENT_BINARY_DIR}/global/qconfig_p.h" + PROPERTIES GENERATED TRUE +) # Find ELF interpreter and define a macro for that: if ((LINUX OR HURD) AND NOT CMAKE_CROSSCOMPILING AND BUILD_SHARED_LIBS) diff --git a/src/corelib/global/qtconfigmacros.h b/src/corelib/global/qtconfigmacros.h index 0ff6e2955ae..51f6e651bc1 100644 --- a/src/corelib/global/qtconfigmacros.h +++ b/src/corelib/global/qtconfigmacros.h @@ -5,7 +5,10 @@ #define QTCONFIGMACROS_H #ifdef QT_BOOTSTRAPPED -#include <QtCore/qconfig-bootstrapped.h> +// qconfig-bootstrapped.h is not supposed to be a part of the synced header files. So we find it by +// the include path specified for Bootstrap library in the source tree instead of the build tree as +// it's done for regular header files. +#include "qconfig-bootstrapped.h" #else #include <QtCore/qconfig.h> #include <QtCore/qtcore-config.h> diff --git a/src/corelib/global/qtversionchecks.h b/src/corelib/global/qtversionchecks.h index d6fad1ed6ce..8f3bd8b3713 100644 --- a/src/corelib/global/qtversionchecks.h +++ b/src/corelib/global/qtversionchecks.h @@ -10,7 +10,10 @@ #endif #ifdef QT_BOOTSTRAPPED -#include <QtCore/qconfig-bootstrapped.h> +// qconfig-bootstrapped.h is not supposed to be a part of the synced header files. So we find it by +// the include path specified for Bootstrap library in the source tree instead of the build tree as +// it's done for regular header files. +#include "qconfig-bootstrapped.h" #else #include <QtCore/qconfig.h> #include <QtCore/qtcore-config.h> diff --git a/src/entrypoint/CMakeLists.txt b/src/entrypoint/CMakeLists.txt index 81a68aff04e..845ce419ea3 100644 --- a/src/entrypoint/CMakeLists.txt +++ b/src/entrypoint/CMakeLists.txt @@ -109,6 +109,8 @@ if(WIN32) target_compile_definitions(EntryPointPrivate INTERFACE QT_NEEDS_QMAIN) qt_internal_extend_target(EntryPointImplementation DEFINES QT_NEEDS_QMAIN) endif() + + qt_internal_add_sync_header_dependencies(EntryPointImplementation Core) endif() if(CMAKE_SYSTEM_NAME STREQUAL "iOS") diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index f6c2b2979a6..b6117248353 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -887,7 +887,6 @@ qt_internal_extend_target(Gui CONDITION QT_FEATURE_filesystemmodel qt_internal_extend_target(Gui CONDITION QT_FEATURE_vulkan SOURCES rhi/qrhivulkan.cpp rhi/qrhivulkan_p.h - rhi/qrhivulkan_p_p.h rhi/qrhivulkanext_p.h vulkan/qbasicvulkanplatforminstance.cpp vulkan/qbasicvulkanplatforminstance_p.h vulkan/qplatformvulkaninstance.cpp vulkan/qplatformvulkaninstance.h diff --git a/src/plugins/platforms/eglfs/CMakeLists.txt b/src/plugins/platforms/eglfs/CMakeLists.txt index 8972e7fa26b..108900518c4 100644 --- a/src/plugins/platforms/eglfs/CMakeLists.txt +++ b/src/plugins/platforms/eglfs/CMakeLists.txt @@ -46,6 +46,8 @@ qt_internal_add_module(EglFSDeviceIntegrationPrivate Qt::FbSupportPrivate Qt::GuiPrivate EGL::EGL # special case + HEADER_SYNC_SOURCE_DIRECTORY + "${CMAKE_CURRENT_SOURCE_DIR}/api" ) #### Keys ignored in scope 2:.:.:eglfsdeviceintegration.pro:<TRUE>: diff --git a/src/tools/bootstrap/CMakeLists.txt b/src/tools/bootstrap/CMakeLists.txt index 8964e27fdaa..66d16781aa2 100644 --- a/src/tools/bootstrap/CMakeLists.txt +++ b/src/tools/bootstrap/CMakeLists.txt @@ -11,6 +11,9 @@ # The bootstrap library has a few manual tweaks compared to other # libraries. qt_add_library(Bootstrap STATIC) + +qt_internal_add_sync_header_dependencies(Bootstrap Core) + # special case end qt_internal_extend_target(Bootstrap SOURCES @@ -104,6 +107,7 @@ qt_internal_extend_target(Bootstrap ../../3rdparty/tinycbor/src PUBLIC_INCLUDE_DIRECTORIES # special case $<TARGET_PROPERTY:Core,INCLUDE_DIRECTORIES> # special case + ../../corelib/global PUBLIC_LIBRARIES # special case Qt::Platform # special case ) diff --git a/src/tools/syncqt/CMakeLists.txt b/src/tools/syncqt/CMakeLists.txt new file mode 100644 index 00000000000..0152c534506 --- /dev/null +++ b/src/tools/syncqt/CMakeLists.txt @@ -0,0 +1,30 @@ +# The tool should be optimized for maximum performance when working. +qt_internal_get_optimize_full_flags(optimize_full_flags) + +set(compile_definitions + QT_VERSION_STR="${PROJECT_VERSION}" + QT_VERSION_MAJOR=${PROJECT_VERSION_MAJOR} + QT_VERSION_MINOR=${PROJECT_VERSION_MINOR} + QT_VERSION_PATCH=${PROJECT_VERSION_PATCH} + QT_NAMESPACE="${QT_NAMESPACE}" +) + +if(CMAKE_OSX_ARCHITECTURES) + set(osx_architectures "-DCMAKE_OSX_ARCHITECTURES:STRING=${CMAKE_OSX_ARCHITECTURES}") +endif() +qt_get_tool_target_name(target_name syncqt) +qt_internal_add_configure_time_tool(${target_name} + DEFINES ${compile_definitions} + COMPILE_OPTIONS ${optimize_full_flags} + TOOLS_TARGET Core + INSTALL_DIRECTORY "${INSTALL_LIBEXECDIR}" + CMAKE_FLAGS + -DCMAKE_CXX_STANDARD_REQUIRED:BOOL=TRUE + -DCMAKE_CXX_STANDARD:STRING=${CMAKE_CXX_STANDARD} + # std::filesystem API is only available in macOS 10.15+ + -DCMAKE_OSX_DEPLOYMENT_TARGET:STRING=10.15 + "${osx_architectures}" + SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp" + CONFIG RelWithDebInfo +) diff --git a/src/tools/syncqt/main.cpp b/src/tools/syncqt/main.cpp new file mode 100644 index 00000000000..03a80ef9732 --- /dev/null +++ b/src/tools/syncqt/main.cpp @@ -0,0 +1,1567 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +/* + * The tool generates deployment artifacts for the Qt builds such as: + * - CaMeL case header files named by public C++ symbols located in public module header files + * - Header file that contains the module version information, and named as <module>Vesion + * - LD version script if applicable + * - Aliases or copies of the header files sorted by the generic Qt-types: public/private/qpa + * and stored in the corresponding directories. Also copies the aliases to the framework-specific + * directories. + * Also the tool executes conformity checks on each header file if applicable, to make sure they + * follow rules that are relevant for their header type. + * The tool can be run in two modes: with either '-all' or '-headers' options specified. Depending + * on the selected mode, the tool either scans the filesystem to find header files or use the + * pre-defined list of header files. + */ + +#include <iostream> +#include <fstream> +#include <string> +#include <sstream> +#include <filesystem> +#include <unordered_map> +#include <vector> +#include <regex> +#include <map> +#include <set> +#include <array> + +enum ErrorCodes { + NoError = 0, + InvalidArguments, + SyncFailed, +}; + +// Enum contains the list of checks that can be executed on header files. +enum HeaderChecks { + NoChecks = 0, + NamespaceChecks = 1, /* Checks if header file is wrapped with QT_<BEGIN|END>_NAMESPACE macros */ + PrivateHeaderChecks = 2, /* Checks if the public header includes a private header */ + IncludeChecks = 4, /* Checks if the real header file but not an alias is included */ + WeMeantItChecks = 8, /* Checks if private header files contains 'We meant it' disclaimer */ + CriticalChecks = PrivateHeaderChecks, /* Checks that lead to the fatal error of the sync + process */ + AllChecks = NamespaceChecks | PrivateHeaderChecks | IncludeChecks | WeMeantItChecks, +}; + +constexpr int LinkerScriptCommentAlignment = 55; + +static const std::regex GlobalHeaderRegex("^q(.*)global\\.h$"); + +// This comparator is used to sort include records in master header. +// It's used to put q.*global.h file to the top of the list and sort all other files alphabetically. +bool MasterHeaderIncludeComparator(const std::string &a, const std::string &b) +{ + std::smatch amatch; + std::smatch bmatch; + + if (std::regex_match(a, amatch, GlobalHeaderRegex)) { + if (std::regex_match(b, bmatch, GlobalHeaderRegex)) { + return amatch[1].str().empty() + || (!bmatch[1].str().empty() && amatch[1].str() < bmatch[1].str()); + } + return true; + } else if (std::regex_match(b, bmatch, GlobalHeaderRegex)) { + return false; + } + + return a < b; +}; + +namespace utils { +std::string asciiToLower(std::string s) +{ + std::transform(s.begin(), s.end(), s.begin(), + [](unsigned char c) { return (c >= 'A' && c <= 'Z') ? c | 0x20 : c; }); + return s; +} + +std::string asciiToUpper(std::string s) +{ + std::transform(s.begin(), s.end(), s.begin(), + [](unsigned char c) { return (c >= 'a' && c <= 'z') ? c & 0xdf : c; }); + return s; +} + +class DummyOutputStream : public std::ostream +{ + struct : public std::streambuf + { + int overflow(int c) override { return c; } + } buff; + +public: + DummyOutputStream() : std::ostream(&buff) { } +} DummyOutput; + +void printInternalError() +{ + std::cerr << "Internal error. Please create bugreport at https://bugreports.qt.io " + "using 'Build tools: Other component.'" + << std::endl; +} + +std::filesystem::path normilizedPath(const std::string &path) +{ + return std::filesystem::path(std::filesystem::absolute(path).generic_string()); +} +} + +using FileStamp = std::filesystem::file_time_type; + +class CommandLineOptions +{ + template<typename T> + struct CommandLineOption + { + CommandLineOption(T *_value, bool _isOptional = false) + : value(_value), isOptional(_isOptional) + { + } + + T *value; + bool isOptional; + }; + +public: + CommandLineOptions(int argc, char *argv[]) : m_isValid(parseArguments(argc, argv)) { } + + bool isValid() const { return m_isValid; } + + const std::string &moduleName() const { return m_moduleName; } + + const std::string &sourceDir() const { return m_sourceDir; } + + const std::string &binaryDir() const { return m_binaryDir; } + + const std::string &includeDir() const { return m_includeDir; } + + const std::string &privateIncludeDir() const { return m_privateIncludeDir; } + + const std::string &frameworkIncludeDir() const { return m_frameworkIncludeDir; } + + const std::string &qpaIncludeDir() const { return m_qpaIncludeDir; } + + const std::string &stagingDir() const { return m_stagingDir; } + + const std::string &versionScriptFile() const { return m_versionScriptFile; } + + const std::set<std::string> &knownModules() const { return m_knownModules; } + + const std::regex &qpaHeadersRegex() const { return m_qpaHeadersRegex; } + + const std::regex &privateHeadersRegex() const { return m_privateHeadersRegex; } + + const std::regex &publicNamespaceRegex() const { return m_publicNamespaceRegex; } + + const std::set<std::string> &headers() const { return m_headers; } + + const std::set<std::string> &generatedHeaders() const { return m_generatedHeaders; } + + bool scanAllMode() const { return m_scanAllMode; } + + bool isFramework() const { return m_isFramework; } + + bool isInternal() const { return m_isInternal; } + + bool isNonQtModule() const { return m_isNonQtModule; } + + bool printHelpOnly() const { return m_printHelpOnly; } + + bool debug() const { return m_debug; } + + bool copy() const { return m_copy; } + + bool minimal() const { return m_minimal; } + + bool showOnly() const { return m_showOnly; } + + void printHelp() const + { + std::cout << "Usage: syncqt -sourceDir <dir> -binaryDir <dir> -module <module name>" + " -includeDir <dir> -privateIncludeDir <dir> -qpaIncludeDir <dir>" + " -stagingDir <dir> <-headers <header list>|-all> [-debug]" + " [-versionScript <path>] [-qpaHeadersFilter <regex>]" + " [-framework [-frameworkIncludeDir <dir>]]" + " [-knownModules <module1> <module2>... <moduleN>]" + " [-nonQt] [-internal] [-copy]\n" + "" + "Mandatory arguments:\n" + " -module Module name.\n" + " -headers List of header files.\n" + " -all In 'all' mode syncqt scans source\n" + " directory for public qt headers and\n" + " artifacts not considering CMake source\n" + " tree. The main use cases are the \n" + " generating of documentation and creating\n" + " API review changes.\n" + " -sourceDir Module source directory.\n" + " -binaryDir Module build directory.\n" + " -includeDir Module include directory where the\n" + " generated header files will be located.\n" + " -privateIncludeDir Module include directory for the\n" + " generated private header files.\n" + " -qpaIncludeDir Module include directory for the \n" + " generated QPA header files.\n" + " -stagingDir Temporary staging directory to collect\n" + " artifacts that need to be installed.\n" + " -knownModules list of known modules. syncqt uses the\n" + " list to check the #include macros\n" + " consistency.\n" + "Optional arguments:\n" + " -internal Indicates that the module is internal.\n" + " -nonQt Indicates that the module is not a Qt\n" + " module.\n" + " -privateHeadersFilter Regex that filters private header files\n" + " from the list of 'headers'.\n" + " -qpaHeadersFilter Regex that filters qpa header files from.\n" + " the list of 'headers'.\n" + " -publicNamespaceFilter Symbols that are in the specified\n" + " namespace.\n" + " are treated as public symbols.\n" + " -versionScript Generate linker version script by\n" + " provided path.\n" + " -debug Enable debug output.\n" + " -framework Indicates that module is framework.\n" + " -frameworkIncludeDir The directory to store the framework\n" + " header files.\n" + " E.g. QtCore.framework/Versions/A/Headers\n" + " -copy Copy header files instead of creating\n" + " aliases.\n" + " -minimal Do not create CaMeL case headers for the\n" + " public C++ symbols.\n" + " -showonly Show actions, but not perform them.\n" + " -help Print this help.\n"; + } + +private: + template<typename T> + [[nodiscard]] bool checkRequiredArguments(const std::unordered_map<std::string, T> &arguments) + { + bool ret = true; + for (const auto &argument : arguments) { + if (!argument.second.isOptional + && (!argument.second.value || argument.second.value->size()) == 0) { + std::cerr << "Missing argument: " << argument.first << std::endl; + ret = false; + } + } + return ret; + } + + [[nodiscard]] bool parseArguments(int argc, char *argv[]) + { + std::string qpaHeadersFilter; + std::string privateHeadersFilter; + std::string publicNamespaceFilter; + std::set<std::string> generatedHeaders; + static std::unordered_map<std::string, CommandLineOption<std::string>> stringArgumentMap = { + { "-module", { &m_moduleName } }, + { "-sourceDir", { &m_sourceDir } }, + { "-binaryDir", { &m_binaryDir } }, + { "-privateHeadersFilter", { &privateHeadersFilter, true } }, + { "-qpaHeadersFilter", { &qpaHeadersFilter, true } }, + { "-includeDir", { &m_includeDir } }, + { "-privateIncludeDir", { &m_privateIncludeDir } }, + { "-qpaIncludeDir", { &m_qpaIncludeDir } }, + { "-stagingDir", { &m_stagingDir, true } }, + { "-versionScript", { &m_versionScriptFile, true } }, + { "-frameworkIncludeDir", { &m_frameworkIncludeDir, true } }, + { "-publicNamespaceFilter", { &publicNamespaceFilter, true } }, + }; + + static const std::unordered_map<std::string, CommandLineOption<std::set<std::string>>> + listArgumentMap = { + { "-headers", { &m_headers, true } }, + { "-generatedHeaders", { &generatedHeaders, true } }, + { "-knownModules", { &m_knownModules, true } }, + }; + + static const std::unordered_map<std::string, CommandLineOption<bool>> boolArgumentMap = { + { "-nonQt", { &m_isNonQtModule, true } }, { "-debug", { &m_debug, true } }, + { "-help", { &m_printHelpOnly, true } }, { "-framework", { &m_isFramework, true } }, + { "-internal", { &m_isInternal, true } }, { "-all", { &m_scanAllMode, true } }, + { "-copy", { &m_copy, true } }, { "-minimal", { &m_minimal, true } }, + { "-showonly", { &m_showOnly, true } }, { "-showOnly", { &m_showOnly, true } }, + }; + + std::string *currentValue = nullptr; + std::set<std::string> *currentListValue = nullptr; + for (int i = 1; i < argc; ++i) { + std::string arg(argv[i]); + if (arg.size() == 0) + continue; + + if (arg.size() > 0 && arg[0] == '-') { + currentValue = nullptr; + currentListValue = nullptr; + { + auto it = stringArgumentMap.find(arg); + if (it != stringArgumentMap.end()) { + if (it->second.value == nullptr) { + utils::printInternalError(); + return false; + } + currentValue = it->second.value; + continue; + } + } + + { + auto it = boolArgumentMap.find(arg); + if (it != boolArgumentMap.end()) { + if (it->second.value == nullptr) { + utils::printInternalError(); + return false; + } + *(it->second.value) = true; + continue; + } + } + + { + auto it = listArgumentMap.find(arg); + if (it != listArgumentMap.end()) { + if (it->second.value == nullptr) { + utils::printInternalError(); + return false; + } + currentListValue = it->second.value; + continue; + } + } + + std::cerr << "Unknown argument: " << arg << std::endl; + return false; + } else { + if (currentValue != nullptr) { + *currentValue = arg; + currentValue = nullptr; + } else if (currentListValue != nullptr) { + currentListValue->insert(arg); + } else { + std::cerr << "Unknown argument: " << arg << std::endl; + } + } + } + + if (!qpaHeadersFilter.empty()) + m_qpaHeadersRegex = std::regex(qpaHeadersFilter); + + if (!privateHeadersFilter.empty()) + m_privateHeadersRegex = std::regex(privateHeadersFilter); + + if (!publicNamespaceFilter.empty()) + m_publicNamespaceRegex = std::regex(publicNamespaceFilter); + + if (m_headers.empty() && !m_scanAllMode) { + std::cerr << "You need to specify either -headers or -all option."; + return false; + } + + if (!m_headers.empty() && m_scanAllMode) { + std::cerr << "Both -headers and -all are specified. Need to choose only one" + "operational mode."; + return false; + } + + for (auto header : generatedHeaders) { + if (header.size() == 0) + continue; + if (header[0] == '@') { + std::ifstream ifs(header.substr(1), std::ifstream::in); + if (ifs.is_open()) { + std::string headerFromFile; + while (std::getline(ifs, headerFromFile)) { + if (!headerFromFile.empty()) + m_generatedHeaders.insert(headerFromFile); + } + } + } else { + m_generatedHeaders.insert(header); + } + } + + bool ret = true; + ret &= checkRequiredArguments(stringArgumentMap); + ret &= checkRequiredArguments(listArgumentMap); + + normilizePaths(); + + return ret; + } + + // Convert all paths from command line to a generic one. + void normilizePaths() + { + static std::array<std::string *, 8> paths = { + &m_sourceDir, &m_binaryDir, &m_includeDir, &m_privateIncludeDir, + &m_qpaIncludeDir, &m_stagingDir, &m_versionScriptFile, &m_frameworkIncludeDir + }; + for (auto path : paths) { + if (!path->empty()) + *path = utils::normilizedPath(*path).generic_string(); + } + } + + std::string m_moduleName; + std::string m_sourceDir; + std::string m_binaryDir; + std::string m_includeDir; + std::string m_privateIncludeDir; + std::string m_qpaIncludeDir; + std::string m_stagingDir; + std::string m_versionScriptFile; + std::string m_frameworkIncludeDir; + std::set<std::string> m_knownModules; + std::set<std::string> m_headers; + std::set<std::string> m_generatedHeaders; + bool m_scanAllMode = false; + bool m_copy = false; + bool m_isFramework = false; + bool m_isNonQtModule = false; + bool m_isInternal = false; + bool m_printHelpOnly = false; + bool m_debug = false; + bool m_minimal = false; + bool m_showOnly = false; + std::regex m_qpaHeadersRegex; + std::regex m_privateHeadersRegex; + std::regex m_publicNamespaceRegex; + + bool m_isValid; +}; + +class SyncScanner +{ + class SymbolDescriptor + { + public: + // Where the symbol comes from + enum SourceType { + Pragma = 0, // pragma qt_class is mentioned a header file + Declaration, // The symbol declaration inside a header file + MaxSourceType + }; + + void update(const std::string &file, SourceType type) + { + if (type < m_type) { + m_file = file; + m_type = type; + } + } + + // The file that contains a symbol. + const std::string &file() const { return m_file; } + + private: + SourceType m_type = MaxSourceType; + std::string m_file; + }; + using SymbolContainer = std::unordered_map<std::string, SymbolDescriptor>; + + struct ParsingResult + { + std::vector<std::string> versionScriptContent; + std::string requireConfig; + bool masterInclude = true; + }; + + CommandLineOptions *m_commandLineArgs = nullptr; + + std::map<std::string /* header file name */, std::string /* header feature guard name */, + decltype(MasterHeaderIncludeComparator) *> + m_masterHeaderContents; + + std::unordered_map<std::string /* the deprecated header name*/, + std::string /* the replacement */> + m_deprecatedHeaders; + std::vector<std::string> m_versionScriptContents; + std::set<std::string> m_producedHeaders; + std::vector<std::string> m_headerCheckExceptions; + SymbolContainer m_symbols; + std::ostream &scannerDebug() const + { + if (m_commandLineArgs->debug()) + return std::cout; + return utils::DummyOutput; + } + + enum { Active, Stopped, IgnoreNext, Ignore } m_versionScriptGeneratorState = Active; + + std::filesystem::path m_currentFile; + std::string m_currentFilename; + std::string m_currentFileString; + size_t m_currentFileLineNumber = 0; + bool m_currentFileInSourceDir = false; + + enum FileType { PublicHeader = 0, PrivateHeader = 1, QpaHeader = 2, ExportHeader = 4 }; + unsigned int m_currentFileType = PublicHeader; + +public: + SyncScanner(CommandLineOptions *commandLineArgs) + : m_commandLineArgs(commandLineArgs), m_masterHeaderContents(MasterHeaderIncludeComparator) + { + } + + // The function converts the relative path to a header files to the absolute. It also makes the + // path canonical(removes '..' and '.' parts of the path). The source directory passed in + // '-sourceDir' command line argument is used as base path for relative paths to create the + // absolute path. + [[nodiscard]] std::filesystem::path makeHeaderAbsolute(const std::string &filename) const; + + ErrorCodes sync() + { + m_versionScriptGeneratorState = + m_commandLineArgs->versionScriptFile().empty() ? Stopped : Active; + auto error = NoError; + + // In the scan all mode we ingore the list of header files that is specified in the + // '-headers' argument, and collect header files from the source directory tree. + if (m_commandLineArgs->scanAllMode()) { + for (auto const &entry : + std::filesystem::recursive_directory_iterator(m_commandLineArgs->sourceDir())) { + if (entry.is_regular_file() && isHeader(entry) + && !isDocFileHeuristic(entry.path().generic_string())) { + const std::string filePath = entry.path().generic_string(); + const std::string fileName = entry.path().filename().generic_string(); + scannerDebug() << "Checking: " << filePath << std::endl; + if (!processHeader(makeHeaderAbsolute(filePath))) + error = SyncFailed; + } + } + } else { + // Since the list of header file is quite big syncqt supports response files to avoid + // the issues with long command lines. + std::set<std::string> rspHeaders; + const auto &headers = m_commandLineArgs->headers(); + for (auto it = headers.begin(); it != headers.end(); ++it) { + const auto &header = *it; + if (header.size() > 0 && header[0] == '@') { + std::ifstream ifs(header.substr(1), std::ifstream::in); + if (ifs.is_open()) { + std::string headerFromFile; + while (std::getline(ifs, headerFromFile)) { + if (!headerFromFile.empty()) + rspHeaders.insert(headerFromFile); + } + } + } else if (!processHeader(makeHeaderAbsolute(header))) { + error = SyncFailed; + } + } + for (const auto &header : rspHeaders) { + if (!processHeader(makeHeaderAbsolute(header))) + error = SyncFailed; + } + } + + // No further processing in minimal mode. + if (m_commandLineArgs->minimal()) + return error; + + // Generate aliases for all unique symbols collected during the header files parsing. + for (auto it = m_symbols.begin(); it != m_symbols.end(); ++it) { + const std::string &filename = it->second.file(); + if (!filename.empty()) { + if (generateQtCamelCaseFileIfContentChanged( + m_commandLineArgs->includeDir() + '/' + it->first, filename)) { + m_producedHeaders.insert(it->first); + } else { + error = SyncFailed; + } + } + } + + // Generate the header file containing version information. + if (!m_commandLineArgs->isNonQtModule()) { + std::string moduleNameLower = utils::asciiToLower(m_commandLineArgs->moduleName()); + std::string versionHeaderFilename(moduleNameLower + "version.h"); + std::string versionHeaderCamel(m_commandLineArgs->moduleName() + "Version"); + std::string versionFile = m_commandLineArgs->includeDir() + '/' + versionHeaderFilename; + + std::error_code ec; + FileStamp originalStamp = std::filesystem::last_write_time(versionFile, ec); + if (ec) + originalStamp = FileStamp::clock::now(); + + if (generateVersionHeader(versionFile)) { + if (!generateAliasedHeaderFileIfTimestampChanged( + m_commandLineArgs->includeDir() + '/' + versionHeaderCamel, + versionHeaderFilename, originalStamp)) { + error = SyncFailed; + } + m_masterHeaderContents[versionHeaderFilename] = {}; + m_producedHeaders.insert(versionHeaderFilename); + m_producedHeaders.insert(versionHeaderCamel); + } else { + error = SyncFailed; + } + } + + if (!m_commandLineArgs->scanAllMode()) { + if (!m_commandLineArgs->isNonQtModule()) { + if (!generateDeprecatedHeaders()) + error = SyncFailed; + + if (!generateHeaderCheckExceptions()) + error = SyncFailed; + } + + if (!m_commandLineArgs->versionScriptFile().empty()) { + if (!generateLinkerVersionScript()) + error = SyncFailed; + } + } + + if (!m_commandLineArgs->isNonQtModule()) { + if (!generateMasterHeader()) + error = SyncFailed; + } + + if (!m_commandLineArgs->scanAllMode()) { + // Copy the generated files to a spearate staging directory to make the installation + // process eaiser. + if (!copyGeneratedHeadersToStagingDirectory(m_commandLineArgs->stagingDir())) + error = SyncFailed; + // We also need to have a copy of the generated header files in framework include + // directories when building with '-framework'. + if (m_commandLineArgs->isFramework()) { + if (!copyGeneratedHeadersToStagingDirectory( + m_commandLineArgs->frameworkIncludeDir(), true)) + error = SyncFailed; + } + } + return error; + } + + // The function copies files, that were generated while the sync procedure to a staging + // directory. This is necessary to simplify the installation of the generated files. + [[nodiscard]] bool copyGeneratedHeadersToStagingDirectory(const std::string &outputDirectory, + bool skipCleanup = false) + { + bool result = true; + if (!std::filesystem::exists(outputDirectory)) { + std::filesystem::create_directories(outputDirectory); + } else if (!skipCleanup) { + for (const auto &entry : + std::filesystem::recursive_directory_iterator(outputDirectory)) { + if (m_producedHeaders.find(entry.path().filename().generic_string()) + == m_producedHeaders.end()) { + std::filesystem::remove(entry.path()); + } + } + } + + for (const auto &header : m_producedHeaders) { + std::filesystem::path src(m_commandLineArgs->includeDir() + '/' + header); + std::filesystem::path dst(outputDirectory + '/' + header); + if (!m_commandLineArgs->showOnly()) + result &= updateOrCopy(src, dst); + } + return result; + } + + void resetCurrentFileInfoData(const std::filesystem::path &headerFile) + { + // This regex filters the generated '*exports.h' and '*exports_p.h' header files. + static const std::regex ExportsHeaderRegex("^q(.*)exports(_p)?\\.h$"); + + m_currentFile = headerFile; + m_currentFileLineNumber = 0; + m_currentFilename = m_currentFile.filename().generic_string(); + m_currentFileType = PublicHeader; + m_currentFileString = m_currentFile.generic_string(); + m_currentFileInSourceDir = m_currentFileString.find(m_commandLineArgs->sourceDir()) == 0; + + if (isHeaderPrivate(m_currentFilename)) + m_currentFileType = PrivateHeader; + + if (isHeaderQpa(m_currentFilename)) + m_currentFileType = QpaHeader | PrivateHeader; + + if (std::regex_match(m_currentFilename, ExportsHeaderRegex)) + m_currentFileType |= ExportHeader; + } + + [[nodiscard]] bool processHeader(const std::filesystem::path &headerFile) + { + // This regex filters any paths that contain the '3rdparty' directory. + static const std::regex ThirdPartyFolderRegex(".+/3rdparty/.+"); + + // This regex filters '-config.h' and '-config_p.h' header files. + static const std::regex ConfigHeaderRegex("^(q|.+-)config(_p)?\\.h"); + + resetCurrentFileInfoData(headerFile); + // We assume that header files ouside of the module source or build directories do not + // belong to the module. Skip any processing. + if (!m_currentFileInSourceDir + && m_currentFileString.find(m_commandLineArgs->binaryDir()) != 0) { + scannerDebug() << "Header file: " << headerFile + << " is outside the sync directories. Skipping." << std::endl; + m_headerCheckExceptions.push_back(m_currentFileString); + return true; + } + + // Check if a directory is passed as argument. That shouldn't happen, print error and exit. + if (m_currentFilename.empty()) { + std::cerr << "Header file name of " << m_currentFileString << "is empty"; + return false; + } + + std::error_code ec; + FileStamp originalStamp = std::filesystem::last_write_time(headerFile, ec); + if (ec) + originalStamp = FileStamp::clock::now(); + + bool isPrivate = m_currentFileType & PrivateHeader; + bool isQpa = m_currentFileType & QpaHeader; + bool isExport = m_currentFileType & ExportHeader; + scannerDebug() << headerFile << " m_currentFilename: " << m_currentFilename + << " isPrivate: " << isPrivate << " isQpa: " << isQpa << std::endl; + + // Chose the directory where to generate the header aliases or to copy header file if + // the '-copy' argument is passed. + std::string outputDir = m_commandLineArgs->includeDir(); + if (isQpa) + outputDir = m_commandLineArgs->qpaIncludeDir(); + else if (isPrivate) + outputDir = m_commandLineArgs->privateIncludeDir(); + + if (!std::filesystem::exists(outputDir)) + std::filesystem::create_directories(outputDir); + + bool headerFileExists = std::filesystem::exists(headerFile); + std::string aliasedFilepath = + std::filesystem::relative(headerFile, outputDir).generic_string(); + std::string aliasPath = outputDir + '/' + m_currentFilename; + + // If the '-copy' argument is passed, we copy the original file to a corresponding output + // directory otherwise we only create a header file alias that contains relative path to + // the original header file in the module source or build tree. + if (m_commandLineArgs->copy() && headerFileExists) { + if (!updateOrCopy(headerFile, aliasPath)) + return false; + } else { + if (!generateAliasedHeaderFileIfTimestampChanged(aliasPath, aliasedFilepath, + originalStamp)) + return false; + } + + // No further processing in minimal mode. + if (m_commandLineArgs->minimal()) + return true; + + // Stop processing if header files doesn't exist. This happens at configure time, since + // either header files are generated later than syncqt is running or header files only + // generated at build time. These files will be processed at build time, if CMake files + // contain the correct dependencies between the missing header files and the module + // 'sync_headers' targets. + if (!headerFileExists) { + scannerDebug() << "Header file: " << headerFile + << " doesn't exist, but is added to syncqt scanning. Skipping."; + return true; + } + + bool isGenerated = isHeaderGenerated(m_currentFileString); + bool is3rdParty = std::regex_match(m_currentFileString, ThirdPartyFolderRegex); + // No processing of generated Qt config header files. + if (!std::regex_match(m_currentFilename, ConfigHeaderRegex)) { + unsigned int skipChecks = m_commandLineArgs->scanAllMode() ? AllChecks : NoChecks; + + // Collect checks that should skipped for the header file. + if (m_commandLineArgs->isNonQtModule() || is3rdParty || isQpa + || !m_currentFileInSourceDir || isGenerated) { + skipChecks = AllChecks; + } else { + if (std::regex_match(m_currentFilename, GlobalHeaderRegex) || isExport) + skipChecks |= NamespaceChecks; + + if (isHeaderPCH(m_currentFilename)) + skipChecks |= WeMeantItChecks; + + if (isPrivate) { + skipChecks |= NamespaceChecks; + skipChecks |= PrivateHeaderChecks; + skipChecks |= IncludeChecks; + } else { + skipChecks |= WeMeantItChecks; + } + } + + ParsingResult parsingResult; + parsingResult.masterInclude = m_currentFileInSourceDir && !isExport && !is3rdParty + && !isQpa && !isPrivate && !isGenerated; + if (!parseHeader(headerFile, originalStamp, parsingResult, skipChecks)) + return false; + + // Record the private header file inside the version script content. + if (isPrivate && !m_commandLineArgs->versionScriptFile().empty() + && parsingResult.versionScriptContent.size() > 0) { + m_versionScriptContents.insert(m_versionScriptContents.end(), + parsingResult.versionScriptContent.begin(), + parsingResult.versionScriptContent.end()); + } + + // Add the '#if QT_CONFIG(<feature>)' check for header files that supposed to be + // included into the module master header only if corresponding feature is enabled. + if (!isQpa && !isPrivate) { + if (m_currentFilename.find('_') == std::string::npos + && parsingResult.masterInclude) { + m_masterHeaderContents[m_currentFilename] = parsingResult.requireConfig; + } + } + } else if (m_currentFilename == "qconfig.h") { + // Hardcode generating of QtConfig alias + updateSymbolDescriptor("QtConfig", "qconfig.h", SyncScanner::SymbolDescriptor::Pragma); + } + + return true; + } + + void parseVersionScriptContent(const std::string buffer, ParsingResult &result) + { + // This regex looks for the symbols that needs to be placed into linker version script. + static const std::regex VersionScriptSymbolRegex( + "^(?:struct|class)(?:\\s+Q_\\w*_EXPORT)?\\s+([\\w:]+)[^;]*(;$)?"); + + // This regex looks for the namespaces that needs to be placed into linker version script. + static const std::regex VersionScriptNamespaceRegex( + "^namespace\\s+Q_\\w+_EXPORT\\s+([\\w:]+).*"); + + // This regex filters the tailing colon from the symbol name. + static const std::regex TrailingColonRegex("([\\w]+):$"); + + switch (m_versionScriptGeneratorState) { + case Ignore: + m_versionScriptGeneratorState = Active; + return; + case Stopped: + return; + case IgnoreNext: + m_versionScriptGeneratorState = Ignore; + break; + case Active: + break; + } + + std::smatch match; + std::string symbol; + if (std::regex_match(buffer, match, VersionScriptSymbolRegex) && match[2].str().empty()) + symbol = match[1].str(); + else if (std::regex_match(buffer, match, VersionScriptNamespaceRegex)) + symbol = match[1].str(); + + if (std::regex_match(symbol, match, TrailingColonRegex)) + symbol = match[1].str(); + + // checkLineForSymbols(buffer, symbol); + if (symbol.size() > 0 && symbol[symbol.size() - 1] != ';') { + std::string relPath = m_currentFileInSourceDir + ? std::filesystem::relative(m_currentFile, m_commandLineArgs->sourceDir()) + .string() + : std::filesystem::relative(m_currentFile, m_commandLineArgs->binaryDir()) + .string(); + + std::string versionStringRecord = " *"; + size_t startPos = 0; + size_t endPos = 0; + while (endPos != std::string::npos) { + endPos = symbol.find("::", startPos); + size_t length = endPos != std::string::npos ? (endPos - startPos) + : (symbol.size() - startPos); + if (length > 0) { + std::string symbolPart = symbol.substr(startPos, length); + versionStringRecord += std::to_string(symbolPart.size()); + versionStringRecord += symbolPart; + } + startPos = endPos + 2; + } + versionStringRecord += "*;"; + if (versionStringRecord.size() < LinkerScriptCommentAlignment) + versionStringRecord += + std::string(LinkerScriptCommentAlignment - versionStringRecord.size(), ' '); + versionStringRecord += " # "; + versionStringRecord += relPath; + versionStringRecord += ":"; + versionStringRecord += std::to_string(m_currentFileLineNumber); + versionStringRecord += "\n"; + result.versionScriptContent.push_back(versionStringRecord); + } + } + + // The function parses 'headerFile' and collect artifacts that are used at generating step. + // 'timeStamp' is saved in internal structures to compare it when generating files. + // 'result' the function output value that stores the result of parsing. + // 'skipChecks' checks that are not applicable for the header file. + [[nodiscard]] bool parseHeader(const std::filesystem::path &headerFile, + const FileStamp &timeStamp, ParsingResult &result, + unsigned int skipChecks) + { + if (m_commandLineArgs->showOnly()) + std::cout << headerFile << " [" << m_commandLineArgs->moduleName() << "]" << std::endl; + // This regex checks if line contains a macro. + static const std::regex MacroRegex("^\\s*#.*"); + + // The regex's bellow check line for known pragmas: + // - 'qt_sync_skip_header_check' avoid any header checks. + // + // - 'qt_sync_stop_processing' stops the header proccesing from a moment when pragma is + // found. Important note: All the parsing artifacts were found before this point are + // stored for further processing. + // + // - 'qt_sync_suspend_processing' pauses processing and skip lines inside a header until + // 'qt_sync_resume_processing' is found. 'qt_sync_stop_processing' stops processing if + // it's found before the 'qt_sync_resume_processing'. + // + // - 'qt_sync_resume_processing' resumes processing after 'qt_sync_suspend_processing'. + // + // - 'qt_class(<symbol>)' manually declares the 'symbol' that should be used to generate + // the CaMeL case header alias. + // + // - 'qt_deprecates(<deprecated header file>)' indicates that this header file replaces + // the 'deprecated header file'. syncqt will create the deprecated header file' with + // the special deprecation content. See the 'generateDeprecatedHeaders' function + // for details. + // + // - 'qt_no_master_include' indicates that syncqt should avoid including this header + // files into the module master header file. + static const std::regex SkipHeaderCheckRegex("^#\\s*pragma qt_sync_skip_header_check$"); + static const std::regex StopProcessingRegex("^#\\s*pragma qt_sync_stop_processing$"); + static const std::regex SuspendProcessingRegex("^#\\s*pragma qt_sync_suspend_processing$"); + static const std::regex ResumeProcessingRegex("^#\\s*pragma qt_sync_resume_processing$"); + static const std::regex ExplixitClassPragmaRegex("^#\\s*pragma qt_class\\(([^\\)]+)\\)$"); + static const std::regex DeprecatesPragmaRegex("^#\\s*pragma qt_deprecates\\(([^\\)]+)\\)$"); + static const std::regex NoMasterIncludePragmaRegex("^#\\s*pragma qt_no_master_include$"); + + // This regex checks if header contains 'We mean it' disclaimer. All private headers should + // contain them. + static const std::regex WeMeantItRegex("\\s*// We mean it\\."); + + // The regex's check if the content of header files is wrapped with the Qt namespace macros. + static const std::regex BeginNamespaceRegex("^QT_BEGIN_NAMESPACE(_[A-Z_]+)?$"); + static const std::regex EndNamespaceRegex("^QT_END_NAMESPACE(_[A-Z_]+)?$"); + + // This regex checks if line contains the include macro of the following formats: + // - #include <file> + // - #include "file" + // - # include <file> + static const std::regex IncludeRegex("^#\\s*include\\s*[<\"](.+)[>\"]"); + + // This regex checks if line contains namespace definition. + static const std::regex NamespaceRegex("\\s*namespace ([^ ]*)\\s+"); + + // This regex checks if line contains the Qt iterator declaration, that need to have + // CaMel case header alias. + static const std::regex DeclareIteratorRegex("^ *Q_DECLARE_\\w*ITERATOR\\((\\w+)\\);?$"); + + // This regex checks if header file contains the QT_REQUIRE_CONFIG call. + // The macro argument is used to wrap an include of the header file inside the module master + // header file with the '#if QT_CONFIG(<feature>)' guard. + static const std::regex RequireConfigRegex("^ *QT_REQUIRE_CONFIG\\((\\w+)\\);?$"); + + // This regex looks for the ELFVERSION tag this is control key-word for the version script + // content processing. + // ELFVERSION tag accepts the following values: + // - stop - stops the symbols lookup for a versino script starting from this line. + // - ignore-next - ignores the line followed but the current one. + // - ignore - ignores the current line. + static const std::regex ElfVersionTagRegex(".*ELFVERSION:(stop|ignore-next|ignore).*"); + + std::ifstream input(headerFile, std::ifstream::in); + if (!input.is_open()) { + std::cerr << "Unable to open " << headerFile << std::endl; + return false; + } + + bool hasQtBeginNamespace = false; + std::string qtBeginNamespace; + std::string qtEndNamespace; + bool hasWeMeantIt = false; + bool isSuspended = false; + bool isMultiLineComment = false; + std::size_t bracesDepth = 0; + std::size_t namespaceCount = 0; + std::string namespaceString; + + std::smatch match; + + std::string buffer; + std::string line; + std::string tmpLine; + std::size_t linesProcessed = 0; + int faults = NoChecks; + + // Read file line by line + while (std::getline(input, tmpLine)) { + ++m_currentFileLineNumber; + line.append(tmpLine); + if (line.empty() || line.at(line.size() - 1) == '\\') { + continue; + } + buffer.clear(); + buffer.reserve(line.size()); + // Optimize processing by looking for a special sequences such as: + // - start-end of comments + // - start-end of class/structures + // And avoid processing of the the data inside these blocks. + for (std::size_t i = 0; i < line.size(); ++i) { + if (bracesDepth == namespaceCount) { + if (line[i] == '/') { + if ((i + 1) < line.size()) { + if (line[i + 1] == '*') { + isMultiLineComment = true; + continue; + } else if (line[i + 1] == '/') { // Single line comment + if (!(skipChecks & WeMeantItChecks) + && std::regex_match(line, WeMeantItRegex)) { + hasWeMeantIt = true; + continue; + } + if (m_versionScriptGeneratorState != Stopped + && std::regex_match(line, match, ElfVersionTagRegex)) { + if (match[1].str() == "ignore") + m_versionScriptGeneratorState = Ignore; + else if (match[1].str() == "ignore-next") + m_versionScriptGeneratorState = IgnoreNext; + else if (match[1].str() == "stop") + m_versionScriptGeneratorState = Stopped; + } + break; + } + } + } else if (line[i] == '*' && (i + 1) < line.size() && line[i + 1] == '/') { + ++i; + isMultiLineComment = false; + continue; + } + } + + if (isMultiLineComment) { + if (!(skipChecks & WeMeantItChecks) && std::regex_match(line, WeMeantItRegex)) { + hasWeMeantIt = true; + continue; + } + continue; + } + + if (line[i] == '{') { + if (std::regex_match(buffer, match, NamespaceRegex)) { + ++namespaceCount; + namespaceString += "::"; + namespaceString += match[1].str(); + } + ++bracesDepth; + continue; + } else if (line[i] == '}') { + if (namespaceCount > 0 && bracesDepth == namespaceCount) { + namespaceString.resize(namespaceString.rfind("::")); + --namespaceCount; + } + --bracesDepth; + } else if (bracesDepth == namespaceCount) { + buffer += line[i]; + } + } + line.clear(); + + if (buffer.size() == 0) + continue; + scannerDebug() << m_currentFilename << ": " << buffer << std::endl; + + if (m_currentFileType & PrivateHeader) { + parseVersionScriptContent(buffer, result); + } + + ++linesProcessed; + + bool skipSymbols = + (m_currentFileType & PrivateHeader) || (m_currentFileType & QpaHeader); + + // Parse pragmas + if (std::regex_match(buffer, MacroRegex)) { + if (std::regex_match(buffer, SkipHeaderCheckRegex)) { + skipChecks = AllChecks; + faults = NoChecks; + } else if (std::regex_match(buffer, StopProcessingRegex)) { + if (skipChecks == AllChecks) + m_headerCheckExceptions.push_back(m_currentFileString); + return true; + } else if (std::regex_match(buffer, SuspendProcessingRegex)) { + isSuspended = true; + } else if (std::regex_match(buffer, ResumeProcessingRegex)) { + isSuspended = false; + } else if (std::regex_match(buffer, match, ExplixitClassPragmaRegex)) { + if (!skipSymbols) { + updateSymbolDescriptor(match[1].str(), m_currentFilename, + SymbolDescriptor::Pragma); + } else { + // TODO: warn about skipping symbols that are defined explicitly + } + } else if (std::regex_match(buffer, NoMasterIncludePragmaRegex)) { + result.masterInclude = false; + } else if (std::regex_match(buffer, match, DeprecatesPragmaRegex)) { + m_deprecatedHeaders[match[1].str()] = + m_commandLineArgs->moduleName() + '/' + m_currentFilename; + } else if (std::regex_match(buffer, match, IncludeRegex) && !isSuspended) { + if (!(skipChecks & IncludeChecks)) { + std::string includedHeader = match[1].str(); + if (!(skipChecks & PrivateHeaderChecks) + && isHeaderPrivate(std::filesystem::path(includedHeader) + .filename() + .generic_string())) { + faults |= PrivateHeaderChecks; + std::cerr << m_commandLineArgs->moduleName() + << ": ERROR: " << m_currentFilename + << " includes private header " << includedHeader << std::endl; + } + for (const auto &module : m_commandLineArgs->knownModules()) { + faults |= IncludeChecks; + std::string suggestedHeader = "Qt" + module + '/' + includedHeader; + if (std::filesystem::exists(m_commandLineArgs->includeDir() + "/../" + + suggestedHeader)) { + std::cerr << m_commandLineArgs->moduleName() + << ": WARNING: " << m_currentFilename << " includes " + << includedHeader << " when it should include " + << suggestedHeader << std::endl; + } + } + } + } + continue; + } + + // Logic below this line is affected by the 'qt_sync_suspend_processing' and + // 'qt_sync_resume_processing' pragmas. + if (isSuspended) + continue; + + // Look for the symbols in header file. + if (!skipSymbols) { + std::string symbol; + if (checkLineForSymbols(buffer, symbol)) { + if (namespaceCount == 0 + || std::regex_match(namespaceString, + m_commandLineArgs->publicNamespaceRegex())) { + updateSymbolDescriptor(symbol, m_currentFilename, + SymbolDescriptor::Declaration); + } + continue; + } else if (std::regex_match(buffer, match, DeclareIteratorRegex)) { + std::string iteratorSymbol = match[1].str() + "Iterator"; + updateSymbolDescriptor(std::string("Q") + iteratorSymbol, m_currentFilename, + SymbolDescriptor::Declaration); + updateSymbolDescriptor(std::string("QMutable") + iteratorSymbol, + m_currentFilename, SymbolDescriptor::Declaration); + continue; + } else if (std::regex_match(buffer, match, RequireConfigRegex)) { + result.requireConfig = match[1].str(); + continue; + } + } + + // Check for both QT_BEGIN_NAMESPACE and QT_END_NAMESPACE macros are present in the + // header file. + if (!(skipChecks & NamespaceChecks)) { + if (std::regex_match(buffer, match, BeginNamespaceRegex)) { + qtBeginNamespace = match[1].str(); + hasQtBeginNamespace = true; + } else if (std::regex_match(buffer, match, EndNamespaceRegex)) { + qtEndNamespace = match[1].str(); + } + } + } + + // Error out if namespace checks are failed. + if (!(skipChecks & NamespaceChecks)) { + if (hasQtBeginNamespace) { + if (qtBeginNamespace != qtEndNamespace) { + faults |= NamespaceChecks; + std::cerr << m_commandLineArgs->moduleName() + << ":WARNING: " << m_currentFilename + << " the begin namespace macro QT_BEGIN_NAMESPACE" << qtBeginNamespace + << " doesn't match the end namespace macro QT_END_NAMESPACE" + << qtEndNamespace << std::endl; + } + } else { + faults |= NamespaceChecks; + std::cerr << m_commandLineArgs->moduleName() << ": WARNING: " << m_currentFilename + << " does not include QT_BEGIN_NAMESPACE" << std::endl; + } + } + + if (!(skipChecks & WeMeantItChecks) && !hasWeMeantIt) { + faults |= WeMeantItChecks; + std::cerr << m_commandLineArgs->moduleName() << ": WARNING: " << m_currentFilename + << " does not have the \"We mean it.\" warning\n" + << std::endl; + } + + scannerDebug() << "linesTotal: " << m_currentFileLineNumber + << " linesProcessed: " << linesProcessed << std::endl; + + if (skipChecks == AllChecks) + m_headerCheckExceptions.push_back(m_currentFileString); + + // Exit with an error if any of critical checks are present. + return !(faults & CriticalChecks); + } + + // The function checks if line contains the symbol that needs to have a CaMeL-style alias. + [[nodiscard]] bool checkLineForSymbols(const std::string &line, std::string &symbol) + { + scannerDebug() << "checkLineForSymbols: " << line << std::endl; + + // This regex checks if line contains class or structure declaration like: + // - <class|stuct> StructName + // - template <> class ClassName + // - class ClassName : [public|protected|private] BaseClassName + // - class ClassName [final|Q_DECL_FINAL|sealed] + // And possible combinations of the above variants. + static const std::regex ClassRegex( + "^ *(template *<.*> *)?(class|struct) +([^ <>]* " + "+)?((?!Q_DECL_FINAL|final|sealed)[^<\\s\\:]+) ?(<[^>\\:]*> " + "?)?\\s*(?:Q_DECL_FINAL|final|sealed)?\\s*((,|:)\\s*(public|protected|private)? " + "*.*)? *$"); + + // This regex checks if line contains function pointer typedef declaration like: + // - typedef void (* QFunctionPointerType)(int, char); + static const std::regex FunctionPointerRegex( + "^ *typedef *.*\\(\\*(Q[^\\)]+)\\)\\(.*\\); *"); + + // This regex checks if line contains class or structure typedef declaration like: + // - typedef AnySymbol<char> QAnySymbolType; + static const std::regex TypedefRegex("^ *typedef\\s+(.*)\\s+(Q\\w+); *$"); + + // This regex checks if symbols is the Qt public symbol. Assume that Qt public symbols start + // with the capital 'Q'. + static const std::regex QtClassRegex("^Q\\w+$"); + + std::smatch match; + if (std::regex_match(line, match, FunctionPointerRegex)) { + symbol = match[1].str(); + } else if (std::regex_match(line, match, TypedefRegex)) { + symbol = match[2].str(); + } else if (std::regex_match(line, match, ClassRegex)) { + symbol = match[4].str(); + if (!std::regex_match(symbol, QtClassRegex)) + symbol.clear(); + } else { + return false; + } + return !symbol.empty(); + } + + [[nodiscard]] bool isHeaderQpa(const std::string &headerFileName) + { + return std::regex_match(headerFileName, m_commandLineArgs->qpaHeadersRegex()); + } + + [[nodiscard]] bool isHeaderPrivate(const std::string &headerFile) + { + return std::regex_match(headerFile, m_commandLineArgs->privateHeadersRegex()); + } + + [[nodiscard]] bool isHeaderPCH(const std::string &headerFilename) + { + static const std::string pchSuffix("_pch.h"); + return headerFilename.find(pchSuffix, headerFilename.size() - pchSuffix.size()) + != std::string::npos; + } + + [[nodiscard]] bool isHeader(const std::filesystem::path &path) + { + return path.extension().string() == ".h"; + } + + [[nodiscard]] bool isDocFileHeuristic(const std::string &headerFilePath) + { + return headerFilePath.find("/doc/") != std::string::npos; + } + + [[nodiscard]] bool isHeaderGenerated(const std::string &header) + { + return m_commandLineArgs->generatedHeaders().find(header) + != m_commandLineArgs->generatedHeaders().end(); + } + + [[nodiscard]] bool generateQtCamelCaseFileIfContentChanged(const std::string &outputFilePath, + const std::string &aliasedFilePath); + + [[nodiscard]] bool generateAliasedHeaderFileIfTimestampChanged( + const std::string &outputFilePath, const std::string &aliasedFilePath, + const FileStamp &originalStamp = FileStamp::clock::now()); + + bool writeIfDifferent(const std::string &outputFile, const std::string &buffer); + + [[nodiscard]] bool generateMasterHeader() + { + if (m_masterHeaderContents.empty()) + return true; + + std::string outputFile = + m_commandLineArgs->includeDir() + '/' + m_commandLineArgs->moduleName(); + + std::string moduleUpper = utils::asciiToUpper(m_commandLineArgs->moduleName()); + std::stringstream buffer; + buffer << "#ifndef QT_" << moduleUpper << "_MODULE_H\n" + << "#define QT_" << moduleUpper << "_MODULE_H\n" + << "#include <" << m_commandLineArgs->moduleName() << "/" + << m_commandLineArgs->moduleName() << "Depends>\n"; + for (const auto &headerContents : m_masterHeaderContents) { + if (!headerContents.second.empty()) { + buffer << "#if QT_CONFIG(" << headerContents.second << ")\n" + << "#include \"" << headerContents.first << "\"\n" + << "#endif\n"; + } else { + buffer << "#include \"" << headerContents.first << "\"\n"; + } + } + buffer << "#endif\n"; + + m_producedHeaders.insert(m_commandLineArgs->moduleName()); + return writeIfDifferent(outputFile, buffer.str()); + } + + [[nodiscard]] bool generateVersionHeader(const std::string &outputFile) + { + std::string moduleNameUpper = utils::asciiToUpper( m_commandLineArgs->moduleName()); + + std::stringstream buffer; + buffer << "/* This file was generated by syncqt. */\n" + << "#ifndef QT_" << moduleNameUpper << "_VERSION_H\n" + << "#define QT_" << moduleNameUpper << "_VERSION_H\n\n" + << "#define " << moduleNameUpper << "_VERSION_STR \"" << QT_VERSION_STR << "\"\n\n" + << "#define " << moduleNameUpper << "_VERSION " + << "0x0" << QT_VERSION_MAJOR << "0" << QT_VERSION_MINOR << "0" << QT_VERSION_PATCH + << "\n\n" + << "#endif // QT_" << moduleNameUpper << "_VERSION_H\n"; + + return writeIfDifferent(outputFile, buffer.str()); + } + + [[nodiscard]] bool generateDeprecatedHeaders() + { + static std::regex cIdentifierSymbolsRegex("[^a-zA-Z0-9_]"); + static std::string guard_base = "DEPRECATED_HEADER_" + m_commandLineArgs->moduleName(); + for (auto it = m_deprecatedHeaders.begin(); it != m_deprecatedHeaders.end(); ++it) { + std::string &replacement = it->second; + std::string qualifiedHeaderName = + std::regex_replace(it->first, cIdentifierSymbolsRegex, "_"); + std::string guard = guard_base + "_" + qualifiedHeaderName; + std::string warningText = "Header <" + m_commandLineArgs->moduleName() + "/" + it->first + + "> is deprecated. Please include <" + replacement + "> instead."; + std::stringstream buffer; + buffer << "#ifndef " << guard << "\n" + << "#define " << guard << "\n" + << "#if defined(__GNUC__)\n" + << "# warning " << warningText << "\n" + << "#elif defined(_MSC_VER)\n" + << "# pragma message (\"" << warningText << "\")\n" + << "#endif\n" + << "#include <" << replacement << ">\n" + << "#if 0\n" + // TODO: Looks like qt_no_master_include is useless since deprecated headers are + // generated by syncqt but are never scanned. + << "#pragma qt_no_master_include\n" + << "#endif\n" + << "#endif\n"; + writeIfDifferent(m_commandLineArgs->includeDir() + '/' + it->first, buffer.str()); + } + return true; + } + + [[nodiscard]] bool generateHeaderCheckExceptions() + { + std::stringstream buffer; + for (const auto &header : m_headerCheckExceptions) + buffer << header << ";"; + return writeIfDifferent(m_commandLineArgs->binaryDir() + '/' + + m_commandLineArgs->moduleName() + + "_header_check_exceptions", + buffer.str()); + } + + [[nodiscard]] bool generateLinkerVersionScript() + { + std::stringstream buffer; + for (const auto &content : m_versionScriptContents) + buffer << content; + return writeIfDifferent(m_commandLineArgs->versionScriptFile(), buffer.str()); + } + + bool updateOrCopy(const std::filesystem::path &src, const std::filesystem::path &dst); + void updateSymbolDescriptor(const std::string &symbol, const std::string &file, + SymbolDescriptor::SourceType type); +}; + +// The function updates information about the symbol: +// - The path and modification time of the file where the symbol was found. +// - The source of finding +// Also displays a short info about a symbol in show only mode. +void SyncScanner::updateSymbolDescriptor(const std::string &symbol, const std::string &file, + SymbolDescriptor::SourceType type) +{ + if (m_commandLineArgs->showOnly()) + std::cout << " SYMBOL: " << symbol << std::endl; + m_symbols[symbol].update(file, type); +} + +[[nodiscard]] std::filesystem::path +SyncScanner::makeHeaderAbsolute(const std::string &filename) const +{ + if (std::filesystem::path(filename).is_relative()) + return utils::normilizedPath(m_commandLineArgs->sourceDir() + '/' + filename); + + return utils::normilizedPath(filename); +} + +bool SyncScanner::updateOrCopy(const std::filesystem::path &src, const std::filesystem::path &dst) +{ + if (m_commandLineArgs->showOnly()) + return true; + + if (src == dst) { + std::cout << "Source and destination paths are same when copying " << src.string() + << ". Skipping." << std::endl; + return true; + } + + std::error_code ec; + std::filesystem::copy(src, dst, std::filesystem::copy_options::update_existing, ec); + if (ec) { + ec.clear(); + std::filesystem::remove(dst, ec); + if (ec) { + // On some file systems(e.g. vboxfs) the std::filesystem::copy doesn't support + // std::filesystem::copy_options::overwrite_existing remove file first and then copy. + std::cerr << "Unable to remove file: " << src << " to " << dst << " error: (" + << ec.value() << ")" << ec.message() << std::endl; + return false; + } + + std::filesystem::copy(src, dst, std::filesystem::copy_options::overwrite_existing, ec); + if (ec) { + std::cerr << "Unable to copy file: " << src << " to " << dst << " error: (" + << ec.value() << ")" << ec.message() << std::endl; + return false; + } + } + return true; +} + +// The function generates Qt CaMeL-case files. +// CaMeL-case files can have timestamp that is the same as or newer than timestamp of file that +// supposed to included there. It's not an issue when we generate regular aliases because the +// content of aliases is always the same, but we only want to "touch" them when content of original +// is changed. +bool SyncScanner::generateQtCamelCaseFileIfContentChanged(const std::string &outputFilePath, + const std::string &aliasedFilePath) +{ + if (m_commandLineArgs->showOnly()) + return true; + + std::string buffer = "#include \""; + buffer += aliasedFilePath; + buffer += "\"\n"; + + return writeIfDifferent(outputFilePath, buffer); +} + +// The function generates aliases for files in source tree. Since the content of these aliases is +// always same, it's ok to check only timestamp and touch files in case if stamp of original is +// newer than the timestamp of an alias. +bool SyncScanner::generateAliasedHeaderFileIfTimestampChanged(const std::string &outputFilePath, + const std::string &aliasedFilePath, + const FileStamp &originalStamp) +{ + if (m_commandLineArgs->showOnly()) + return true; + + if (std::filesystem::exists({ outputFilePath }) + && std::filesystem::last_write_time({ outputFilePath }) >= originalStamp) { + return true; + } + scannerDebug() << "Rewrite " << outputFilePath << std::endl; + + std::ofstream ofs; + ofs.open(outputFilePath, std::ofstream::out | std::ofstream::trunc); + if (!ofs.is_open()) { + std::cerr << "Unable to write header file alias: " << outputFilePath << std::endl; + return false; + } + ofs << "#include \"" << aliasedFilePath << "\"\n"; + ofs.close(); + return true; +} + +bool SyncScanner::writeIfDifferent(const std::string &outputFile, const std::string &buffer) +{ + if (m_commandLineArgs->showOnly()) + return true; + + static const std::streamsize bufferSize = 1025; + bool differs = false; + std::filesystem::path outputFilePath(outputFile); + + std::string outputDirectory = outputFilePath.parent_path().string(); + if (!std::filesystem::exists(outputDirectory)) + std::filesystem::create_directories(outputDirectory); + + if (std::filesystem::exists(outputFilePath) + && buffer.size() == std::filesystem::file_size(outputFilePath)) { + char rdBuffer[bufferSize]; + memset(rdBuffer, 0, bufferSize); + + std::ifstream ifs(outputFile, std::fstream::in); + std::streamsize currentPos = 0; + + std::size_t bytesRead = 0; + do { + ifs.read(rdBuffer, bufferSize - 1); // Read by 1K + bytesRead = ifs.gcount(); + if (buffer.compare(currentPos, bytesRead, rdBuffer) != 0) { + differs = true; + break; + } + currentPos += bytesRead; + memset(rdBuffer, 0, bufferSize); + } while (bytesRead > 0); + + ifs.close(); + } else { + differs = true; + } + + scannerDebug() << "Update: " << outputFile << " " << differs << std::endl; + if (differs) { + std::ofstream ofs; + ofs.open(outputFilePath, std::fstream::out | std::ofstream::trunc); + if (!ofs.is_open()) { + std::cerr << "Unable to write header content to " << outputFilePath << std::endl; + return false; + } + ofs << buffer; + + ofs.close(); + } + return true; +} + +int main(int argc, char *argv[]) +{ + CommandLineOptions options(argc, argv); + if (!options.isValid()) + return InvalidArguments; + + if (options.printHelpOnly()) { + options.printHelp(); + return NoError; + } + + SyncScanner scanner = SyncScanner(&options); + return scanner.sync(); +} diff --git a/tests/auto/cmake/CMakeLists.txt b/tests/auto/cmake/CMakeLists.txt index 5153350eb69..30cd340f16f 100644 --- a/tests/auto/cmake/CMakeLists.txt +++ b/tests/auto/cmake/CMakeLists.txt @@ -145,6 +145,12 @@ if(IOS) return() endif() +set(is_qt_build_platform TRUE) +# macOS versions less than 10.15 are not supported for building Qt. +if(CMAKE_HOST_APPLE AND CMAKE_HOST_SYSTEM_VERSION VERSION_LESS "19.0.0") + set(is_qt_build_platform FALSE) +endif() + _qt_internal_test_expect_pass(test_umbrella_config) _qt_internal_test_expect_pass(test_wrap_cpp_and_resources) if (NOT NO_WIDGETS) @@ -274,21 +280,23 @@ elseif(QT6_INSTALL_BINS) endif() # Test building and installing a few dummy Qt modules and plugins. -set(mockplugins_test_args "") -if(NOT QT_FEATURE_no_prefix) - list(APPEND mockplugins_test_args - BINARY "${CMAKE_COMMAND}" - BINARY_ARGS - "-DQT_BUILD_DIR=${CMAKE_CURRENT_BINARY_DIR}/mockplugins" - -P "${qt_install_prefix}/${qt_install_bin_dir}/qt-cmake-private-install.cmake" - ) -endif() -_qt_internal_test_expect_pass(mockplugins ${mockplugins_test_args}) -set_tests_properties(mockplugins PROPERTIES FIXTURES_SETUP build_mockplugins) +if(is_qt_build_platform) + set(mockplugins_test_args "") + if(NOT QT_FEATURE_no_prefix) + list(APPEND mockplugins_test_args + BINARY "${CMAKE_COMMAND}" + BINARY_ARGS + "-DQT_BUILD_DIR=${CMAKE_CURRENT_BINARY_DIR}/mockplugins" + -P "${qt_install_prefix}/${qt_install_bin_dir}/qt-cmake-private-install.cmake" + ) + endif() + _qt_internal_test_expect_pass(mockplugins ${mockplugins_test_args}) + set_tests_properties(mockplugins PROPERTIES FIXTURES_SETUP build_mockplugins) -# Test importing the plugins built in the project above. -_qt_internal_test_expect_pass(test_import_plugins BINARY ${CMAKE_CTEST_COMMAND} BINARY_ARGS -V) -set_tests_properties(test_import_plugins PROPERTIES FIXTURES_REQUIRED build_mockplugins) + # Test importing the plugins built in the project above. + _qt_internal_test_expect_pass(test_import_plugins BINARY ${CMAKE_CTEST_COMMAND} BINARY_ARGS -V) + set_tests_properties(test_import_plugins PROPERTIES FIXTURES_REQUIRED build_mockplugins) +endif() _qt_internal_test_expect_pass(test_versionless_targets) @@ -307,11 +315,13 @@ include(test_plugin_shared_static_flavor.cmake) _qt_internal_test_expect_pass(tst_qaddpreroutine BINARY tst_qaddpreroutine) -_qt_internal_test_expect_pass(test_static_resources - BINARY "${CMAKE_CTEST_COMMAND}" - BINARY_ARGS "-V") +if(is_qt_build_platform) + _qt_internal_test_expect_pass(test_static_resources + BINARY "${CMAKE_CTEST_COMMAND}" + BINARY_ARGS "-V") -_qt_internal_test_expect_pass(test_generating_cpp_exports) + _qt_internal_test_expect_pass(test_generating_cpp_exports) +endif() _qt_internal_test_expect_pass(test_qt_extract_metatypes) diff --git a/tests/auto/cmake/mockplugins/.cmake.conf b/tests/auto/cmake/mockplugins/.cmake.conf index 377be0059e6..edb49ceeb2e 100644 --- a/tests/auto/cmake/mockplugins/.cmake.conf +++ b/tests/auto/cmake/mockplugins/.cmake.conf @@ -1 +1,3 @@ set(QT_REPO_MODULE_VERSION "6.5.0") + +set(QT_USE_SYNCQT_CPP TRUE) diff --git a/tests/auto/cmake/mockplugins/mockplugins1/CMakeLists.txt b/tests/auto/cmake/mockplugins/mockplugins1/CMakeLists.txt index 1d672c231f1..933f6dde5da 100644 --- a/tests/auto/cmake/mockplugins/mockplugins1/CMakeLists.txt +++ b/tests/auto/cmake/mockplugins/mockplugins1/CMakeLists.txt @@ -4,6 +4,7 @@ qt_internal_add_module(MockPlugins1 PLUGIN_TYPES mockplugin SOURCES + qmockplugin.h fake.cpp LIBRARIES Qt::CorePrivate diff --git a/tests/auto/cmake/mockplugins/mockplugins3/CMakeLists.txt b/tests/auto/cmake/mockplugins/mockplugins3/CMakeLists.txt index ead48007986..5df9c1b6856 100644 --- a/tests/auto/cmake/mockplugins/mockplugins3/CMakeLists.txt +++ b/tests/auto/cmake/mockplugins/mockplugins3/CMakeLists.txt @@ -4,6 +4,7 @@ qt_internal_add_module(MockPlugins3 PLUGIN_TYPES mockauxplugin SOURCES + qmockauxplugin.h fake.cpp LIBRARIES Qt::CorePrivate |