summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlexey Edelev <alexey.edelev@qt.io>2023-01-18 22:06:08 +0100
committerAlexey Edelev <alexey.edelev@qt.io>2023-02-08 12:59:19 +0100
commit214c3a033a4e9191f0082bf6f88624d356a45384 (patch)
tree95f412ee2ab668e1b903ae1d002d32f3decd27ea
parent52150469a837a1eca7e31b8aaea077127db0432f (diff)
Add simple project generation based on existing source files
Introduce the qt-cmake-create script. The script generates the simple CMakeLists.txt based on the source files located in the current or specified directory. The initial version can generate a CMake code for the following file types: - .c .cc .cpp .cxx .h .hh .hxx .hpp - generates the qt_add_executable call with prerequisites. - .qml .js .mjs - generates the qt_add_qml_module call with prerequisites. - .ui - adds the found ui files to the existing executable. Requires C++ files be present in the directory too. - .qrc - generates the qt_add_resources call and adds the resources to the existing executable. Requires C++ files be present in the directory too. - .proto - generates qt_add_protobuf call with prerequisites. The QtInitProject.cmake script contains the 'handle_type' function that allows extending the script capabilities and establish simple relation chains between the file types. Note: The initial implementation doesn't deal with sub-directories, so all files from sub-directories will be added to and handled in the top-level CMakeLists.txt file. This can be extended by user request. Task-number: QTBUG-104388 Change-Id: I5abd9e07da109e867ff95986572ed2bf02ef9d3d Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Alexandru Croitor <alexandru.croitor@qt.io>
-rw-r--r--bin/qt-cmake-create.bat.in18
-rwxr-xr-xbin/qt-cmake-create.in29
-rw-r--r--cmake/QtBaseGlobalTargets.cmake1
-rw-r--r--cmake/QtInitProject.cmake214
-rw-r--r--cmake/QtWrapperScriptHelpers.cmake14
-rw-r--r--cmake/README.md13
-rw-r--r--tests/auto/tools/CMakeLists.txt1
-rw-r--r--tests/auto/tools/qt_cmake_create/CMakeLists.txt14
-rw-r--r--tests/auto/tools/qt_cmake_create/testdata/cpp_project/CMakeLists.txt.expected14
-rw-r--r--tests/auto/tools/qt_cmake_create/testdata/cpp_project/main.cpp7
-rw-r--r--tests/auto/tools/qt_cmake_create/testdata/proto_project/CMakeLists.txt.expected17
-rw-r--r--tests/auto/tools/qt_cmake_create/testdata/proto_project/test.proto5
-rw-r--r--tests/auto/tools/qt_cmake_create/testdata/qml_project/CMakeLists.txt.expected27
-rw-r--r--tests/auto/tools/qt_cmake_create/testdata/qml_project/TestComponent.qml4
-rw-r--r--tests/auto/tools/qt_cmake_create/testdata/qml_project/main.cpp18
-rw-r--r--tests/auto/tools/qt_cmake_create/testdata/qml_project/main.qml16
-rw-r--r--tests/auto/tools/qt_cmake_create/testdata/qrc_project/CMakeLists.txt.expected20
-rw-r--r--tests/auto/tools/qt_cmake_create/testdata/qrc_project/main.cpp13
-rw-r--r--tests/auto/tools/qt_cmake_create/testdata/qrc_project/test.qrc5
-rw-r--r--tests/auto/tools/qt_cmake_create/testdata/qrc_project/test.txt1
-rw-r--r--tests/auto/tools/qt_cmake_create/testdata/ui_only_project/widget.ui32
-rw-r--r--tests/auto/tools/qt_cmake_create/testdata/ui_project/CMakeLists.txt.expected23
-rw-r--r--tests/auto/tools/qt_cmake_create/testdata/ui_project/main.cpp11
-rw-r--r--tests/auto/tools/qt_cmake_create/testdata/ui_project/widget.cpp15
-rw-r--r--tests/auto/tools/qt_cmake_create/testdata/ui_project/widget.h21
-rw-r--r--tests/auto/tools/qt_cmake_create/testdata/ui_project/widget.ui32
-rw-r--r--tests/auto/tools/qt_cmake_create/tst_qt_cmake_create.cpp157
27 files changed, 742 insertions, 0 deletions
diff --git a/bin/qt-cmake-create.bat.in b/bin/qt-cmake-create.bat.in
new file mode 100644
index 0000000000..ff8f7310e9
--- /dev/null
+++ b/bin/qt-cmake-create.bat.in
@@ -0,0 +1,18 @@
+@echo off
+:: The directory of this script is the expanded absolute path of the "$qt_prefix/bin" directory.
+set script_dir_path=%~dp0
+
+:: Try to use original cmake, otherwise to make it relocatable, use any cmake found in PATH.
+set cmake_path=@CMAKE_COMMAND@
+if not exist "%cmake_path%" set cmake_path=cmake
+
+if NOT "%~2" == "" goto :showhelp
+if NOT "%~1" == "" (set PROJECT_DIR=%~1) else (set PROJECT_DIR=%cd%)
+
+"%cmake_path%" -DPROJECT_DIR="%PROJECT_DIR%" -P "%script_dir_path%\@__GlobalConfig_relative_path_from_bin_dir_to_cmake_config_dir@\QtInitProject.cmake"
+exit /b %errorlevel%
+
+:showhelp
+echo Usage
+echo. qt-cmake-create <path/to/project>
+exit /b 1
diff --git a/bin/qt-cmake-create.in b/bin/qt-cmake-create.in
new file mode 100755
index 0000000000..7865d0fe91
--- /dev/null
+++ b/bin/qt-cmake-create.in
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+HELP_MESSAGE="Usage
+ qt-cmake-create <path/to/project>"
+
+# The directory of this script is the expanded absolute path of the "$qt_prefix/bin" directory.
+script_dir_path=`dirname $0`
+script_dir_path=`(cd "$script_dir_path"; /bin/pwd)`
+
+# Try to use original cmake, otherwise to make it relocatable, use any cmake found in PATH.
+original_cmake_path="@CMAKE_COMMAND@"
+cmake_path=$original_cmake_path
+if ! test -f "$cmake_path"; then
+ cmake_path="cmake"
+fi
+
+if [ "$#" -gt 1 ]; then
+ echo "Invalid number of arguments"
+ echo "$HELP_MESSAGE"
+ exit 1
+fi
+
+if [ "$#" -gt 0 ]; then
+ PROJECT_DIR=$1
+else
+ PROJECT_DIR=$PWD
+fi
+exec "$cmake_path" -DPROJECT_DIR="$PROJECT_DIR" -P \
+ "$script_dir_path/@__GlobalConfig_relative_path_from_bin_dir_to_cmake_config_dir@/QtInitProject.cmake"
diff --git a/cmake/QtBaseGlobalTargets.cmake b/cmake/QtBaseGlobalTargets.cmake
index cc551ed08f..b3eea3006f 100644
--- a/cmake/QtBaseGlobalTargets.cmake
+++ b/cmake/QtBaseGlobalTargets.cmake
@@ -330,6 +330,7 @@ set(__public_cmake_helpers
cmake/QtCopyFileIfDifferent.cmake
cmake/QtFeature.cmake
cmake/QtFeatureCommon.cmake
+ cmake/QtInitProject.cmake
cmake/QtPublicAppleHelpers.cmake
cmake/QtPublicCMakeHelpers.cmake
cmake/QtPublicCMakeVersionHelpers.cmake
diff --git a/cmake/QtInitProject.cmake b/cmake/QtInitProject.cmake
new file mode 100644
index 0000000000..3a8d11459f
--- /dev/null
+++ b/cmake/QtInitProject.cmake
@@ -0,0 +1,214 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+cmake_minimum_required(VERSION 3.16)
+
+if(NOT PROJECT_DIR)
+ set(PROJECT_DIR "${CMAKE_SOURCE_DIR}")
+endif()
+
+get_filename_component(project_name "${PROJECT_DIR}" NAME)
+
+get_filename_component(project_abs_dir "${PROJECT_DIR}" ABSOLUTE)
+if(NOT IS_DIRECTORY "${project_abs_dir}")
+ message(FATAL_ERROR "Unable to scan ${project_abs_dir}. The directory doesn't exist.")
+endif()
+
+set(known_extensions "")
+set(types "")
+
+# The function allows extending the capabilities of this script and establishes simple relation
+# chains between the file types.
+# Option Arguments:
+# EXPERIMENTAL
+# Marks that the support of the following files is experimental and the required Qt modules
+# are in Technical preview state.
+# DEPRECATED
+# Marks that the support of the following files will be discontinued soon and the required
+# Qt modules are deprecated.
+# One-value Arguments:
+# TEMPLATE
+# The CMake code template. Use the '@files@' string for the files substitution.
+# Multi-value Arguments:
+# EXTENSIONS
+# List of the file extensions treated as this source 'type'.
+# MODULES
+# List of Qt modules required for these file types.
+# DEPENDS
+# The prerequisite source 'type' needed by this source 'type'
+macro(handle_type type)
+ cmake_parse_arguments(arg
+ "EXPERIMENTAL;DEPRECATED"
+ "TEMPLATE"
+ "EXTENSIONS;MODULES;DEPENDS"
+ ${ARGN}
+ )
+
+ if(NOT arg_EXTENSIONS)
+ message(FATAL_ERROR "Unexpected call handle_type of with no EXTENSIONS specified."
+ " This is the Qt issue, please report a bug at https://bugreports.qt.io.")
+ endif()
+ set(unique_extensions_subset "${known_extensions}")
+ list(REMOVE_ITEM unique_extensions_subset ${arg_EXTENSIONS})
+ if(NOT "${known_extensions}" STREQUAL "${unique_extensions_subset}")
+ message(FATAL_ERROR "${type} contains duplicated extensions, this is not supported."
+ " This is the Qt issue, please report a bug at https://bugreports.qt.io.")
+ endif()
+ set(${type}_file_extensions "${arg_EXTENSIONS}")
+
+ if(NOT arg_TEMPLATE)
+ message(FATAL_ERROR "Unexpected call handle_type of with no TEMPLATE specified."
+ " This is the Qt issue, please report a bug at https://bugreports.qt.io.")
+ endif()
+ set(${type}_template "${arg_TEMPLATE}")
+
+ if(arg_MODULES)
+ set(${type}_required_modules "${arg_MODULES}")
+ endif()
+
+ list(APPEND types ${type})
+
+ if(arg_EXPERIMENTAL)
+ set(${type}_is_experimental TRUE)
+ else()
+ set(${type}_is_experimental FALSE)
+ endif()
+
+ if(arg_DEPRECATED)
+ set(${type}_is_deprecated TRUE)
+ else()
+ set(${type}_is_deprecated FALSE)
+ endif()
+
+ if(arg_DEPENDS)
+ set(${type}_dependencies ${arg_DEPENDS})
+ endif()
+endmacro()
+
+handle_type(cpp EXTENSIONS .c .cc .cpp .cxx .h .hh .hxx .hpp MODULES Core TEMPLATE
+"\n\nqt_add_executable(${project_name}
+ @files@
+)"
+)
+
+handle_type(qml EXTENSIONS .qml .js .mjs MODULES Gui Qml Quick TEMPLATE
+"\n\nqt_add_qml_module(${project_name}
+ URI ${project_name}
+ OUTPUT_DIRECTORY qml
+ VERSION 1.0
+ AUTO_RESOURCE_PREFIX
+ QML_FILES
+ @files@
+)"
+)
+
+handle_type(ui EXTENSIONS .ui MODULES Gui Widgets DEPENDS cpp TEMPLATE
+"\n\ntarget_sources(${project_name}
+ PRIVATE
+ @files@
+)"
+)
+
+handle_type(qrc EXTENSIONS .qrc DEPENDS cpp TEMPLATE
+"\n\nqt_add_resources(${project_name}_resources @files@)
+target_sources(${project_name}
+ PRIVATE
+ \\\${${project_name}_resources}
+)"
+)
+
+handle_type(protobuf EXPERIMENTAL EXTENSIONS .proto MODULES Protobuf Grpc TEMPLATE
+"\n\nqt_add_protobuf(${project_name}
+ GENERATE_PACKAGE_SUBFOLDERS
+ PROTO_FILES
+ @files@
+)"
+)
+
+set(extra_packages "")
+file(GLOB_RECURSE files RELATIVE "${project_abs_dir}" "${project_abs_dir}/*")
+foreach(f IN LISTS files)
+ get_filename_component(file_extension "${f}" LAST_EXT)
+ string(TOLOWER "${file_extension}" file_extension)
+
+ foreach(type IN LISTS types)
+ if(file_extension IN_LIST ${type}_file_extensions)
+ list(APPEND ${type}_sources "${f}")
+ list(APPEND packages ${${type}_required_modules})
+ if(${type}_is_experimental)
+ message("We found files with the following extensions in your directory:"
+ " ${${type}_file_extensions}\n"
+ "Note that the modules ${${type}_required_modules} are"
+ " in the technical preview state.")
+ endif()
+ if(${type}_is_deprecated)
+ message("We found files with the following extensions in your directory:"
+ " ${${type}_file_extensions}\n"
+ "Note that the modules ${${type}_required_modules} are deprecated.")
+ endif()
+ break()
+ endif()
+ endforeach()
+endforeach()
+
+if(packages)
+ list(REMOVE_DUPLICATES packages)
+ list(JOIN packages " " packages_string)
+ list(JOIN packages "\n Qt::" deps_string)
+ set(deps_string "Qt::${deps_string}")
+endif()
+
+set(content
+"cmake_minimum_required(VERSION 3.16)
+project(${project_name} LANGUAGES CXX)
+
+find_package(Qt6 REQUIRED COMPONENTS ${packages_string})
+qt_standard_project_setup()"
+)
+
+set(has_useful_sources FALSE)
+foreach(type IN LISTS types)
+ if(${type}_sources)
+ set(skip FALSE)
+ foreach(dep IN LISTS ${type}_dependencies)
+ if(NOT ${dep}_sources)
+ set(skip TRUE)
+ message("Sources of type ${${type}_file_extensions} cannot live in the project"
+ " without ${${dep}_file_extensions} files. Skipping.")
+ break()
+ endif()
+ endforeach()
+ if(skip)
+ continue()
+ endif()
+
+ set(has_useful_sources TRUE)
+ string(REGEX MATCH "( +)@files@" unused "${${type}_template}")
+ list(JOIN ${type}_sources "\n${CMAKE_MATCH_1}" ${type}_sources)
+ string(REPLACE "@files@" "${${type}_sources}" ${type}_content
+ "${${type}_template}")
+ string(APPEND content "${${type}_content}")
+ endif()
+endforeach()
+
+string(APPEND content "\n\ntarget_link_libraries(${project_name}
+ PRIVATE
+ ${deps_string}
+)\n"
+)
+
+if(EXISTS "${project_abs_dir}/CMakeLists.txt")
+ message(FATAL_ERROR "Project is already initialized in current directory."
+ " Please remove CMakeLists.txt if you want to regenerate the project.")
+endif()
+
+if(NOT has_useful_sources)
+ message(FATAL_ERROR "Could not find any files to generate the project.")
+endif()
+file(WRITE "${project_abs_dir}/CMakeLists.txt" "${content}")
+
+message("The project file is successfully generated. To build the project run:"
+ "\nmkdir build"
+ "\ncd build"
+ "\nqt-cmake ${project_abs_dir}"
+ "\ncmake --build ${project_abs_dir}"
+)
diff --git a/cmake/QtWrapperScriptHelpers.cmake b/cmake/QtWrapperScriptHelpers.cmake
index bfb6d0d7e6..e87d0ad2e5 100644
--- a/cmake/QtWrapperScriptHelpers.cmake
+++ b/cmake/QtWrapperScriptHelpers.cmake
@@ -39,6 +39,20 @@ function(qt_internal_create_wrapper_scripts)
DESTINATION "${INSTALL_BINDIR}")
endif()
+ if(generate_unix)
+ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/bin/qt-cmake-create.in"
+ "${QT_BUILD_DIR}/${INSTALL_BINDIR}/qt-cmake-create" @ONLY
+ NEWLINE_STYLE LF)
+ qt_install(PROGRAMS "${QT_BUILD_DIR}/${INSTALL_BINDIR}/qt-cmake-create"
+ DESTINATION "${INSTALL_BINDIR}")
+ endif()
+ if(generate_non_unix)
+ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/bin/qt-cmake-create.bat.in"
+ "${QT_BUILD_DIR}/${INSTALL_BINDIR}/qt-cmake-create.bat" @ONLY
+ NEWLINE_STYLE CRLF)
+ qt_install(PROGRAMS "${QT_BUILD_DIR}/${INSTALL_BINDIR}/qt-cmake-create.bat"
+ DESTINATION "${INSTALL_BINDIR}")
+ endif()
# Provide a private convenience wrapper with options which should not be propagated via the
# public qt-cmake wrapper e.g. CMAKE_GENERATOR.
# These options can not be set in a toolchain file, but only on the command line.
diff --git a/cmake/README.md b/cmake/README.md
index 9800dae1ce..ac9d1012cf 100644
--- a/cmake/README.md
+++ b/cmake/README.md
@@ -313,3 +313,16 @@ $ cd some/empty/directory
$ ~/Qt/6.0.0/bin/qt-cmake-standalone-test ~/source/of/qtbase/test/auto/corelib/io/qprocess
$ cmake --build .
```
+
+## qt-cmake-create
+
+Generates a simple CMakeLists.txt based on source files in specified project directory.
+
+Example:
+
+```
+$ cd some/source/directory/
+$ qt-cmake-create
+$ qt-cmake -S . -B /build/directory
+$ cmake --build /build/directory
+```
diff --git a/tests/auto/tools/CMakeLists.txt b/tests/auto/tools/CMakeLists.txt
index 9e59771aa0..88c9b85a53 100644
--- a/tests/auto/tools/CMakeLists.txt
+++ b/tests/auto/tools/CMakeLists.txt
@@ -11,6 +11,7 @@ if(NOT ANDROID AND NOT IOS)
endif()
add_subdirectory(moc)
add_subdirectory(rcc)
+ add_subdirectory(qt_cmake_create)
endif()
# QTBUG-88538 # special case
if(TARGET Qt::Widgets AND NOT ANDROID AND NOT IOS)
diff --git a/tests/auto/tools/qt_cmake_create/CMakeLists.txt b/tests/auto/tools/qt_cmake_create/CMakeLists.txt
new file mode 100644
index 0000000000..6611471388
--- /dev/null
+++ b/tests/auto/tools/qt_cmake_create/CMakeLists.txt
@@ -0,0 +1,14 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+# Collect test data
+file(GLOB_RECURSE test_data_glob
+ RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
+ testdata/*)
+list(APPEND test_data ${test_data_glob})
+
+qt_internal_add_test(tst_qt_cmake_create
+ SOURCES
+ tst_qt_cmake_create.cpp
+ TESTDATA ${test_data}
+)
diff --git a/tests/auto/tools/qt_cmake_create/testdata/cpp_project/CMakeLists.txt.expected b/tests/auto/tools/qt_cmake_create/testdata/cpp_project/CMakeLists.txt.expected
new file mode 100644
index 0000000000..7e646b722d
--- /dev/null
+++ b/tests/auto/tools/qt_cmake_create/testdata/cpp_project/CMakeLists.txt.expected
@@ -0,0 +1,14 @@
+cmake_minimum_required(VERSION 3.16)
+project(cpp_project LANGUAGES CXX)
+
+find_package(Qt6 REQUIRED COMPONENTS Core)
+qt_standard_project_setup()
+
+qt_add_executable(cpp_project
+ main.cpp
+)
+
+target_link_libraries(cpp_project
+ PRIVATE
+ Qt::Core
+)
diff --git a/tests/auto/tools/qt_cmake_create/testdata/cpp_project/main.cpp b/tests/auto/tools/qt_cmake_create/testdata/cpp_project/main.cpp
new file mode 100644
index 0000000000..ae659d5ed4
--- /dev/null
+++ b/tests/auto/tools/qt_cmake_create/testdata/cpp_project/main.cpp
@@ -0,0 +1,7 @@
+#include <iostream>
+
+int main(int, char *[])
+{
+ std::cout << "Now I have CMakeLists.txt. Thanks!" << std::endl;
+ return 0;
+}
diff --git a/tests/auto/tools/qt_cmake_create/testdata/proto_project/CMakeLists.txt.expected b/tests/auto/tools/qt_cmake_create/testdata/proto_project/CMakeLists.txt.expected
new file mode 100644
index 0000000000..1b9ae912b2
--- /dev/null
+++ b/tests/auto/tools/qt_cmake_create/testdata/proto_project/CMakeLists.txt.expected
@@ -0,0 +1,17 @@
+cmake_minimum_required(VERSION 3.16)
+project(proto_project LANGUAGES CXX)
+
+find_package(Qt6 REQUIRED COMPONENTS Protobuf Grpc)
+qt_standard_project_setup()
+
+qt_add_protobuf(proto_project
+ GENERATE_PACKAGE_SUBFOLDERS
+ PROTO_FILES
+ test.proto
+)
+
+target_link_libraries(proto_project
+ PRIVATE
+ Qt::Protobuf
+ Qt::Grpc
+)
diff --git a/tests/auto/tools/qt_cmake_create/testdata/proto_project/test.proto b/tests/auto/tools/qt_cmake_create/testdata/proto_project/test.proto
new file mode 100644
index 0000000000..e03a5c0832
--- /dev/null
+++ b/tests/auto/tools/qt_cmake_create/testdata/proto_project/test.proto
@@ -0,0 +1,5 @@
+syntax = "proto3";
+
+package test;
+message Noop {
+}
diff --git a/tests/auto/tools/qt_cmake_create/testdata/qml_project/CMakeLists.txt.expected b/tests/auto/tools/qt_cmake_create/testdata/qml_project/CMakeLists.txt.expected
new file mode 100644
index 0000000000..1befe64ffd
--- /dev/null
+++ b/tests/auto/tools/qt_cmake_create/testdata/qml_project/CMakeLists.txt.expected
@@ -0,0 +1,27 @@
+cmake_minimum_required(VERSION 3.16)
+project(qml_project LANGUAGES CXX)
+
+find_package(Qt6 REQUIRED COMPONENTS Gui Qml Quick Core)
+qt_standard_project_setup()
+
+qt_add_executable(qml_project
+ main.cpp
+)
+
+qt_add_qml_module(qml_project
+ URI qml_project
+ OUTPUT_DIRECTORY qml
+ VERSION 1.0
+ AUTO_RESOURCE_PREFIX
+ QML_FILES
+ TestComponent.qml
+ main.qml
+)
+
+target_link_libraries(qml_project
+ PRIVATE
+ Qt::Gui
+ Qt::Qml
+ Qt::Quick
+ Qt::Core
+)
diff --git a/tests/auto/tools/qt_cmake_create/testdata/qml_project/TestComponent.qml b/tests/auto/tools/qt_cmake_create/testdata/qml_project/TestComponent.qml
new file mode 100644
index 0000000000..f97cbcf115
--- /dev/null
+++ b/tests/auto/tools/qt_cmake_create/testdata/qml_project/TestComponent.qml
@@ -0,0 +1,4 @@
+import QtQuick
+
+Item {
+}
diff --git a/tests/auto/tools/qt_cmake_create/testdata/qml_project/main.cpp b/tests/auto/tools/qt_cmake_create/testdata/qml_project/main.cpp
new file mode 100644
index 0000000000..13c7014d8c
--- /dev/null
+++ b/tests/auto/tools/qt_cmake_create/testdata/qml_project/main.cpp
@@ -0,0 +1,18 @@
+#include <QGuiApplication>
+#include <QQmlApplicationEngine>
+
+int main(int argc, char *argv[])
+{
+ QGuiApplication app(argc, argv);
+
+ QQmlApplicationEngine engine;
+ const QUrl url(QStringLiteral("qrc:/qt/qml/qml_project/main.qml"));
+ QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
+ &app, [url](QObject *obj, const QUrl &objUrl) {
+ if (!obj && url == objUrl)
+ QCoreApplication::exit(-1);
+ }, Qt::QueuedConnection);
+ engine.load(url);
+
+ return app.exec();
+}
diff --git a/tests/auto/tools/qt_cmake_create/testdata/qml_project/main.qml b/tests/auto/tools/qt_cmake_create/testdata/qml_project/main.qml
new file mode 100644
index 0000000000..b42c9e7897
--- /dev/null
+++ b/tests/auto/tools/qt_cmake_create/testdata/qml_project/main.qml
@@ -0,0 +1,16 @@
+import QtQuick
+import QtQuick.Window
+
+Window {
+ width: 640
+ height: 480
+ visible: true
+ title: "Hello World"
+ Text {
+ anchors.centerIn: parent
+ font.pointSize: 16
+ text: "Now I have CMakeLists.txt. Thanks!"
+ }
+ TestComponent {
+ }
+}
diff --git a/tests/auto/tools/qt_cmake_create/testdata/qrc_project/CMakeLists.txt.expected b/tests/auto/tools/qt_cmake_create/testdata/qrc_project/CMakeLists.txt.expected
new file mode 100644
index 0000000000..8a595ac3c2
--- /dev/null
+++ b/tests/auto/tools/qt_cmake_create/testdata/qrc_project/CMakeLists.txt.expected
@@ -0,0 +1,20 @@
+cmake_minimum_required(VERSION 3.16)
+project(qrc_project LANGUAGES CXX)
+
+find_package(Qt6 REQUIRED COMPONENTS Core)
+qt_standard_project_setup()
+
+qt_add_executable(qrc_project
+ main.cpp
+)
+
+qt_add_resources(qrc_project_resources test.qrc)
+target_sources(qrc_project
+ PRIVATE
+ ${qrc_project_resources}
+)
+
+target_link_libraries(qrc_project
+ PRIVATE
+ Qt::Core
+)
diff --git a/tests/auto/tools/qt_cmake_create/testdata/qrc_project/main.cpp b/tests/auto/tools/qt_cmake_create/testdata/qrc_project/main.cpp
new file mode 100644
index 0000000000..cd8ed8f57b
--- /dev/null
+++ b/tests/auto/tools/qt_cmake_create/testdata/qrc_project/main.cpp
@@ -0,0 +1,13 @@
+#include <QFile>
+#include <QDebug>
+
+int main(int, char **)
+{
+ QFile file(":/test.txt");
+ if (!file.open(QFile::ReadOnly))
+ return 1;
+
+ QString data = QString::fromUtf8(file.readAll());
+ qDebug() << data;
+ return 0;
+}
diff --git a/tests/auto/tools/qt_cmake_create/testdata/qrc_project/test.qrc b/tests/auto/tools/qt_cmake_create/testdata/qrc_project/test.qrc
new file mode 100644
index 0000000000..adfe37b52e
--- /dev/null
+++ b/tests/auto/tools/qt_cmake_create/testdata/qrc_project/test.qrc
@@ -0,0 +1,5 @@
+<RCC>
+ <qresource prefix="/">
+ <file>test.txt</file>
+ </qresource>
+</RCC>
diff --git a/tests/auto/tools/qt_cmake_create/testdata/qrc_project/test.txt b/tests/auto/tools/qt_cmake_create/testdata/qrc_project/test.txt
new file mode 100644
index 0000000000..6c5b215ebe
--- /dev/null
+++ b/tests/auto/tools/qt_cmake_create/testdata/qrc_project/test.txt
@@ -0,0 +1 @@
+Now I have CMakeLists.txt. Thanks!
diff --git a/tests/auto/tools/qt_cmake_create/testdata/ui_only_project/widget.ui b/tests/auto/tools/qt_cmake_create/testdata/ui_only_project/widget.ui
new file mode 100644
index 0000000000..d2c4f620b1
--- /dev/null
+++ b/tests/auto/tools/qt_cmake_create/testdata/ui_only_project/widget.ui
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Widget</class>
+ <widget class="QWidget" name="Widget">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>800</width>
+ <height>600</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Widget</string>
+ </property>
+ <widget class="QLabel" name="label">
+ <property name="geometry">
+ <rect>
+ <x>10</x>
+ <y>6</y>
+ <width>781</width>
+ <height>581</height>
+ </rect>
+ </property>
+ <property name="text">
+ <string>Now I have CMakeLists.txt. Thanks!</string>
+ </property>
+ </widget>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/tests/auto/tools/qt_cmake_create/testdata/ui_project/CMakeLists.txt.expected b/tests/auto/tools/qt_cmake_create/testdata/ui_project/CMakeLists.txt.expected
new file mode 100644
index 0000000000..6252b71389
--- /dev/null
+++ b/tests/auto/tools/qt_cmake_create/testdata/ui_project/CMakeLists.txt.expected
@@ -0,0 +1,23 @@
+cmake_minimum_required(VERSION 3.16)
+project(ui_project LANGUAGES CXX)
+
+find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets)
+qt_standard_project_setup()
+
+qt_add_executable(ui_project
+ main.cpp
+ widget.cpp
+ widget.h
+)
+
+target_sources(ui_project
+ PRIVATE
+ widget.ui
+)
+
+target_link_libraries(ui_project
+ PRIVATE
+ Qt::Core
+ Qt::Gui
+ Qt::Widgets
+)
diff --git a/tests/auto/tools/qt_cmake_create/testdata/ui_project/main.cpp b/tests/auto/tools/qt_cmake_create/testdata/ui_project/main.cpp
new file mode 100644
index 0000000000..b0a4ec2647
--- /dev/null
+++ b/tests/auto/tools/qt_cmake_create/testdata/ui_project/main.cpp
@@ -0,0 +1,11 @@
+#include "widget.h"
+
+#include <QApplication>
+
+int main(int argc, char *argv[])
+{
+ QApplication a(argc, argv);
+ Widget w;
+ w.show();
+ return a.exec();
+}
diff --git a/tests/auto/tools/qt_cmake_create/testdata/ui_project/widget.cpp b/tests/auto/tools/qt_cmake_create/testdata/ui_project/widget.cpp
new file mode 100644
index 0000000000..815d5f8c4b
--- /dev/null
+++ b/tests/auto/tools/qt_cmake_create/testdata/ui_project/widget.cpp
@@ -0,0 +1,15 @@
+#include "widget.h"
+#include "ui_widget.h"
+
+Widget::Widget(QWidget *parent)
+ : QWidget(parent)
+ , ui(new Ui::Widget)
+{
+ ui->setupUi(this);
+}
+
+Widget::~Widget()
+{
+ delete ui;
+}
+
diff --git a/tests/auto/tools/qt_cmake_create/testdata/ui_project/widget.h b/tests/auto/tools/qt_cmake_create/testdata/ui_project/widget.h
new file mode 100644
index 0000000000..1fe2322b13
--- /dev/null
+++ b/tests/auto/tools/qt_cmake_create/testdata/ui_project/widget.h
@@ -0,0 +1,21 @@
+#ifndef WIDGET_H
+#define WIDGET_H
+
+#include <QWidget>
+
+QT_BEGIN_NAMESPACE
+namespace Ui { class Widget; }
+QT_END_NAMESPACE
+
+class Widget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ Widget(QWidget *parent = nullptr);
+ ~Widget();
+
+private:
+ Ui::Widget *ui;
+};
+#endif // WIDGET_H
diff --git a/tests/auto/tools/qt_cmake_create/testdata/ui_project/widget.ui b/tests/auto/tools/qt_cmake_create/testdata/ui_project/widget.ui
new file mode 100644
index 0000000000..d2c4f620b1
--- /dev/null
+++ b/tests/auto/tools/qt_cmake_create/testdata/ui_project/widget.ui
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Widget</class>
+ <widget class="QWidget" name="Widget">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>800</width>
+ <height>600</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Widget</string>
+ </property>
+ <widget class="QLabel" name="label">
+ <property name="geometry">
+ <rect>
+ <x>10</x>
+ <y>6</y>
+ <width>781</width>
+ <height>581</height>
+ </rect>
+ </property>
+ <property name="text">
+ <string>Now I have CMakeLists.txt. Thanks!</string>
+ </property>
+ </widget>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/tests/auto/tools/qt_cmake_create/tst_qt_cmake_create.cpp b/tests/auto/tools/qt_cmake_create/tst_qt_cmake_create.cpp
new file mode 100644
index 0000000000..5d358ec6a2
--- /dev/null
+++ b/tests/auto/tools/qt_cmake_create/tst_qt_cmake_create.cpp
@@ -0,0 +1,157 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include <QTest>
+#include <QtTest>
+
+#include <QLibraryInfo>
+#include <QLatin1StringView>
+#include <QDir>
+#include <QFileInfo>
+#include <QProcess>
+#include <QCryptographicHash>
+
+#include <array>
+
+using namespace Qt::Literals::StringLiterals;
+
+class tst_qt_cmake_create : public QObject
+{
+ Q_OBJECT
+
+public:
+ tst_qt_cmake_create();
+
+private slots:
+ void init();
+ void initTestCase();
+ void generatingCMakeLists_data();
+ void generatingCMakeLists();
+
+private:
+ QString m_testWorkDir;
+ QString m_shell;
+ QString m_cmd;
+};
+
+tst_qt_cmake_create::tst_qt_cmake_create() : m_testWorkDir(qApp->applicationDirPath()) { }
+
+void tst_qt_cmake_create::initTestCase()
+{
+ QString binpath = QLibraryInfo::path(QLibraryInfo::BinariesPath);
+#ifdef Q_OS_WINDOWS
+ m_shell = QString("cmd.exe");
+ m_cmd = QString("%1/qt-cmake-create.bat").arg(binpath);
+#else
+ m_shell = QString("/bin/sh");
+ m_cmd = QString("%1/qt-cmake-create").arg(binpath);
+ QVERIFY(QFile::exists(m_shell));
+#endif
+
+ QVERIFY(QFile::exists(m_cmd));
+}
+
+void tst_qt_cmake_create::init()
+{
+ QFETCH(QString, projectDirPath);
+ QDir workDir(m_testWorkDir);
+ QString fullProjectPath = m_testWorkDir + '/' + projectDirPath;
+ if (workDir.exists(fullProjectPath)) {
+ QDir projectDir(projectDirPath);
+ projectDir.removeRecursively();
+ }
+ workDir.mkdir(projectDirPath);
+ auto testDataPath = QFINDTESTDATA("testdata"_L1 + '/' + projectDirPath);
+ QVERIFY(QFile::exists(testDataPath));
+
+ for (const auto &fileInfo : QDir(testDataPath).entryInfoList(QDir::Files)) {
+ QVERIFY(QFile::copy(fileInfo.absoluteFilePath(),
+ fullProjectPath + '/' + fileInfo.fileName()));
+ }
+}
+
+void tst_qt_cmake_create::generatingCMakeLists_data()
+{
+ QTest::addColumn<QString>("projectDirPath");
+ QTest::addColumn<bool>("expectPass");
+ QTest::addColumn<QString>("workDir");
+
+ const std::array<QLatin1StringView, 5> expectPass = {
+ "cpp"_L1, "proto"_L1, "qml"_L1, "qrc"_L1, "ui"_L1,
+ };
+
+ const std::array<QString, 5> workDirs = {
+ m_testWorkDir, ""_L1, m_testWorkDir, ""_L1, m_testWorkDir,
+ };
+
+ static_assert(expectPass.size() == workDirs.size());
+
+ const QLatin1StringView expectFail[] = {
+ "ui_only"_L1,
+ };
+
+ for (size_t i = 0; i < expectPass.size(); ++i) {
+ const auto type = expectPass.at(i);
+ QTest::addRow("tst_qt_cmake_create_%s", type.data())
+ << QString("%1_project").arg(type) << true << workDirs.at(i);
+ }
+
+ for (const auto type : expectFail) {
+ QTest::addRow("tst_qt_cmake_create_%s", type.data())
+ << QString("%1_project").arg(type) << false << QString();
+ }
+}
+
+void tst_qt_cmake_create::generatingCMakeLists()
+{
+ QFETCH(QString, projectDirPath);
+ QFETCH(bool, expectPass);
+ QFETCH(QString, workDir);
+
+ QString fullProjectPath = m_testWorkDir + '/' + projectDirPath;
+ QProcess command;
+ QStringList arguments = {
+#ifdef Q_OS_WINDOWS
+ "/C"_L1,
+#endif
+ m_cmd
+ };
+
+ QString workingDirectory = fullProjectPath;
+ if (!workDir.isEmpty()) {
+ workingDirectory = workDir;
+ arguments.append(fullProjectPath);
+ }
+ command.setProgram(m_shell);
+ command.setArguments(arguments);
+ command.setWorkingDirectory(workingDirectory);
+
+ command.start();
+ QVERIFY(command.waitForFinished());
+ QCOMPARE(command.exitCode() == 0, expectPass);
+
+ QFile actualFile = QFile(fullProjectPath + '/' + "CMakeLists.txt"_L1);
+
+ // Skip the rest if we expect that qt-cmake-create should exit with error
+ if (!expectPass) {
+ QVERIFY(!actualFile.exists());
+ return;
+ }
+
+ QFile expectedFile = QFile(fullProjectPath + '/' + "CMakeLists.txt.expected"_L1);
+ QVERIFY(actualFile.open(QFile::ReadOnly));
+ QVERIFY(expectedFile.open(QFile::ReadOnly));
+
+ auto actualData = actualFile.readAll();
+ actualData.replace(QByteArrayView("\r\n"), QByteArrayView("\n"));
+ auto expectedData = expectedFile.readAll();
+ expectedData.replace(QByteArrayView("\r\n"), QByteArrayView("\n"));
+
+ static auto hash = [](const QByteArray &data) {
+ return QCryptographicHash::hash(data, QCryptographicHash::Sha1).toHex();
+ };
+ QCOMPARE_EQ(hash(actualData), hash(expectedData));
+}
+
+QTEST_MAIN(tst_qt_cmake_create)
+#include "tst_qt_cmake_create.moc"