cmake: add option to generate .qmlls.ini files

Add a CMake bool variable QT_QML_GENERATE_QMLLS_INI to indicate
to qt_add_qml_module that .qmlls.ini files have to be generated for
each directory where qt_add_qml_module is invoked. The .qmlls.ini
files are generated in the source directory.
This option needs to be set explicitly by the user, either as normal
or as cache variable.

The .qmlls.ini files are required for qmlls to work properly in any
editor, without needing custom qmlls-client implementations for every
client that passes the build folder on to qmlls.

Each source directory with a CMakeLists.txt that does the
qt_add_qml_module command gets a .qmlls.ini that contains all the build
folders of all qt_add_qml_module-commands of the current source
directory. This mimics how qmlls searches for the .qmlls.ini (starting
at the source file directory and going up until it finds a .qmlls.ini),
and avoids having to save a map from source folders to build folders in
the .ini file.

Warn the user when using CMake versions <= 3.19:
QT_QML_GENERATE_QMLLS_INI requires deferring the calls to write the
.qmlls.ini files to make sure that all build paths of all
qt_add_qml_module calls of the current directory can be written
inside the .qmlls.ini file.

For multi-config build, this just makes use of the last generated
config and overwrites the .qmlls.ini files for the other files.
This is similar to what CMake does for compile_commands.json on
the ninja multi-config generator, see
https://gitlab.kitware.com/cmake/cmake/-/merge_requests/7477 for
example.

Added some documentation about the option, and also a test.

Fixes: QTBUG-115225
Task-number: QTCREATORBUG-29419
Task-number: QTCREATORBUG-29407
Change-Id: I4a463ff7af534de266359176188fb016d48cf2c4
Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
Reviewed-by: Alexandru Croitor <alexandru.croitor@qt.io>
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
This commit is contained in:
Sami Shalayel 2023-08-10 16:54:05 +02:00
parent 4fcadf6a36
commit cc46882322
10 changed files with 215 additions and 0 deletions

3
.gitignore vendored
View File

@ -369,3 +369,6 @@ cmake_install.cmake
*_autogen
tst_*.xml
CMakeLists.txt.user
# QML Language Server ini-files
.qmlls.ini

View File

@ -712,6 +712,51 @@ Check https://doc.qt.io/qt-6/qt-cmake-policy-qtp0001.html for policy details."
endif()
endforeach()
if(${QT_QML_GENERATE_QMLLS_INI})
if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.19.0")
# collect all build dirs obtained from all the qt_add_qml_module calls and
# write the .qmlls.ini file in a deferred call
if(NOT "${arg_OUTPUT_DIRECTORY}" STREQUAL "")
set(output_folder "${arg_OUTPUT_DIRECTORY}")
else()
set(output_folder "${CMAKE_CURRENT_BINARY_DIR}")
endif()
get_filename_component(build_folder "${output_folder}" DIRECTORY)
get_directory_property(_qmlls_ini_build_folders _qmlls_ini_build_folders)
list(APPEND _qmlls_ini_build_folders "${build_folder}")
set_directory_properties(PROPERTIES _qmlls_ini_build_folders "${_qmlls_ini_build_folders}")
# if no call with id 'qmlls_ini_generation_id' was deferred for this directory, do it now
cmake_language(DEFER GET_CALL qmlls_ini_generation_id call)
if("${call}" STREQUAL "")
cmake_language(EVAL CODE
"cmake_language(DEFER ID qmlls_ini_generation_id CALL _qt_internal_write_deferred_qmlls_ini_file)"
)
endif()
else()
get_property(__qt_internal_generate_qmlls_ini_warning GLOBAL PROPERTY __qt_internal_generate_qmlls_ini_warning)
if (NOT "${__qt_internal_generate_qmlls_ini_warning}")
message(WARNING "QT_QML_GENERATE_QMLLS_INI is not supported on CMake versions < 3.19, disabling...")
set_property(GLOBAL PROPERTY __qt_internal_generate_qmlls_ini_warning ON)
endif()
endif()
endif()
endfunction()
function(_qt_internal_write_deferred_qmlls_ini_file)
set(qmlls_ini_file "${CMAKE_CURRENT_SOURCE_DIR}/.qmlls.ini")
get_directory_property(_qmlls_ini_build_folders _qmlls_ini_build_folders)
list(REMOVE_DUPLICATES _qmlls_ini_build_folders)
if(NOT CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
# replace cmake list separator ';' with unix path separator ':'
string(REPLACE ";" ":" concatenated_build_dirs "${_qmlls_ini_build_folders}")
else()
# cmake list separator and windows path separator are both ';', so no replacement needed
set(concatenated_build_dirs "${_qmlls_ini_build_folders}")
endif()
set(file_content "[General]\nbuildDir=${concatenated_build_dirs}\n")
file(CONFIGURE OUTPUT "${qmlls_ini_file}" CONTENT "${file_content}")
endfunction()
if(NOT QT_NO_CREATE_VERSIONLESS_FUNCTIONS)

View File

@ -1,6 +1,15 @@
// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
/*!
\group cmake-variables-qtqml
\title CMake Global Variables in Qt6 Qml
\l{CMake Command Reference#Qt6::Qml}{CMake Commands} know about the following
global CMake variables:
*/
/*!
\page cmake-variable-qt-qml-output-directory.html
\ingroup cmake-variables-qtqml
@ -26,6 +35,30 @@ The \c QT_QML_OUTPUT_DIRECTORY will also be added to the import path of the
modules under the same base location. This allows the project to use a source
directory structure that doesn't exactly match the URI structure of the QML
modules, or to merge sets of QML modules under a common base point.
*/
/*!
\page cmake-variable-qt-qml-generate-qmlls-ini.html
\ingroup cmake-variables-qtqml
\title QT_QML_GENERATE_QMLLS_INI
\brief Enables autogeneration of .qmlls.ini files for QML Language Server
\c QT_QML_GENERATE_QMLLS_INI is a boolean that describes whether
\l{qt6_add_qml_module}{qt6_add_qml_module()} calls generate \c{.qmlls.ini} files inside
the \b{source folder}. If \c{.qmlls.ini} files already exists in the source folder,
then they are overwritten.
\note Using \c QT_QML_GENERATE_QMLLS_INI requires a CMake version >= 3.19.
These \c{.qmlls.ini} files contain the path to the last configured build directory,
and is needed by \l{QML Language Server} to find user defined modules. See also
\l{QML Language Server} about the other ways of passing build folder to QML Language Server.
\note The files generated by \c QT_QML_GENERATE_QMLLS_INI are only valid for the current
configuration and should be ignored by your version control system. For git, this can be
done by adding \c{.qmlls.ini} to your \c{.gitignore}, for example.
*/

View File

@ -82,6 +82,9 @@ QML Language Server can be configured via a configuration file \c{.qmlls.ini}.
This file should be in the root source directory of the project.
It should be a text file in the ini-format.
\note \c{.qmlls.ini} files can be generated automatically via
\l{QT_QML_GENERATE_QMLLS_INI}.
The configuration file should look like this:
\code
// .qmlls.ini

View File

@ -122,6 +122,9 @@ if(TARGET Qt::Quick)
BINARY cmake_test
)
endif()
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.19")
_qt_internal_test_expect_pass(test_generate_qmlls_ini BINARY tst_generate_qmlls_ini)
endif()
endif()
if(NOT QT6_IS_SHARED_LIBS_BUILD)
_qt_internal_test_expect_pass(test_import_static_shapes_plugin_resources

View File

@ -0,0 +1,42 @@
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
cmake_minimum_required(VERSION 3.19)
project(tst_generate_qmlls_ini)
find_package(Qt6 REQUIRED COMPONENTS Core Qml Test)
qt_standard_project_setup()
qt_add_executable(tst_generate_qmlls_ini main.cpp)
target_link_libraries(tst_generate_qmlls_ini PRIVATE Qt6::Test)
set(QT_QML_GENERATE_QMLLS_INI ON CACHE BOOL "" FORCE)
add_subdirectory(SomeSubfolder)
qt_add_qml_module(tst_generate_qmlls_ini
URI MainModule
VERSION 1.0
NO_RESOURCE_TARGET_PATH
SOURCES
main.cpp
QML_FILES
Main.qml
)
target_compile_definitions(tst_generate_qmlls_ini
PRIVATE
"SOURCE_DIRECTORY=u\"${CMAKE_CURRENT_SOURCE_DIR}\"_s"
"BUILD_DIRECTORY=u\"${CMAKE_CURRENT_BINARY_DIR}\"_s"
)
qt_add_qml_module(Module
URI Module
VERSION 1.0
QML_FILES
Main.qml
OUTPUT_DIRECTORY ./qml/hello/subfolders/Module
)
# Ensure linting runs when building the default "all" target
set_target_properties(all_qmllint PROPERTIES EXCLUDE_FROM_ALL FALSE)

View File

@ -0,0 +1,5 @@
import QtQuick 2.15
Item {
}

View File

@ -0,0 +1,13 @@
qt_add_qml_module(ModuleA
URI ModuleA
VERSION 1.0
QML_FILES Main.qml
OUTPUT_DIRECTORY ./qml/Some/Sub/Folder/ModuleA
)
qt_add_qml_module(ModuleB
URI ModuleB
VERSION 1.0
QML_FILES Main.qml
OUTPUT_DIRECTORY ./qml/Some/Sub/Folder/ModuleB
)

View File

@ -0,0 +1,5 @@
import QtQuick 2.15
Item {
}

View File

@ -0,0 +1,63 @@
// 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 <QtCore/qobject.h>
#include <QtCore/qstring.h>
#include <QtCore/qdir.h>
#include <QtCore/qfile.h>
#include <QtQml/qqml.h>
#include <QtTest/qtest.h>
class tst_generate_qmlls_ini : public QObject
{
Q_OBJECT
private slots:
void qmllsIniAreCorrect();
};
using namespace Qt::StringLiterals;
#ifndef SOURCE_DIRECTORY
# define SOURCE_DIRECTORY u"invalid_source_directory"_s
#endif
#ifndef BUILD_DIRECTORY
# define BUILD_DIRECTORY u"invalid_build_directory"_s
#endif
void tst_generate_qmlls_ini::qmllsIniAreCorrect()
{
const QString qmllsIniName = u".qmlls.ini"_s;
QDir source(SOURCE_DIRECTORY);
QDir build(BUILD_DIRECTORY);
if (!source.exists())
QSKIP(u"Cannot find source directory '%1', skipping test..."_s.arg(SOURCE_DIRECTORY)
.toLatin1());
{
auto file = QFile(source.absoluteFilePath(qmllsIniName));
QVERIFY(file.exists());
QVERIFY(file.open(QFile::ReadOnly | QFile::Text));
const auto fileContent = QString::fromUtf8(file.readAll());
auto secondFolder = QDir(build.absolutePath().append(u"/qml/hello/subfolders"_s));
QVERIFY(secondFolder.exists());
QCOMPARE(fileContent,
u"[General]\nbuildDir=%1%2%3\n"_s.arg(build.absolutePath(), QDir::listSeparator(),
secondFolder.absolutePath()));
}
QDir sourceSubfolder = source;
QVERIFY(sourceSubfolder.cd(u"SomeSubfolder"_s));
QDir buildSubfolder(build.absolutePath().append(u"/SomeSubfolder/qml/Some/Sub/Folder"_s));
{
auto file = QFile(sourceSubfolder.absoluteFilePath(qmllsIniName));
QVERIFY(file.exists());
QVERIFY(file.open(QFile::ReadOnly | QFile::Text));
const auto fileContent = QString::fromUtf8(file.readAll());
QCOMPARE(fileContent,
u"[General]\nbuildDir=%1\n"_s.arg(buildSubfolder.absolutePath()));
}
}
QTEST_MAIN(tst_generate_qmlls_ini)
#include "main.moc"