diff --git a/src/plugins/qmlprojectmanager/buildsystem/projectitem/converters.cpp b/src/plugins/qmlprojectmanager/buildsystem/projectitem/converters.cpp index 82ba1fc2e41..82653dba841 100644 --- a/src/plugins/qmlprojectmanager/buildsystem/projectitem/converters.cpp +++ b/src/plugins/qmlprojectmanager/buildsystem/projectitem/converters.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 #include "converters.h" +#include "utils/algorithm.h" #include @@ -224,6 +225,103 @@ QString jsonToQmlProject(const QJsonObject &rootObject) return qmlProjectString; } +QStringList qmlprojectsFromFilesNodes(const QJsonArray &fileGroups, + const Utils::FilePath &projectRootPath) +{ + QStringList qmlProjectFiles; + for (const QJsonValue &fileGroup : fileGroups) { + if (fileGroup["type"].toString() != "Module") { + continue; + } + // In Qul, paths are relative to the project root directory, not the "directory" entry + qmlProjectFiles.append(fileGroup["files"].toVariant().toStringList()); + + // If the "directory" property is set, all qmlproject files in the directory are also added + // as relative paths from the project root directory, in addition to explicitly added files + const QString directoryProp = fileGroup["directory"].toString(""); + if (directoryProp.isEmpty()) { + continue; + } + const Utils::FilePath dir = projectRootPath / directoryProp; + qmlProjectFiles.append(Utils::transform( + dir.dirEntries(Utils::FileFilter({"*.qmlproject"}, QDir::Files)), + [&projectRootPath](Utils::FilePath file) { + return file.absoluteFilePath().relativePathFrom(projectRootPath).toFSPathString(); + })); + } + + return qmlProjectFiles; +} + +QString moduleUriFromQmlProject(const QString &qmlProjectFilePath) +{ + QmlJS::SimpleReader simpleReader; + const auto rootNode = simpleReader.readFile(qmlProjectFilePath); + // Since the file wasn't explicitly added, skip qmlproject files with errors + if (!rootNode || !simpleReader.errors().isEmpty()) { + return QString(); + } + + for (const auto &child : rootNode->children()) { + if (child->name() == "MCU.Module") { + const auto prop = child->property("uri").isValid() ? child->property("uri") + : child->property("MCU.uri"); + if (prop.isValid()) { + return prop.value.toString(); + } + break; + } + } + + return QString(); +} + +// Returns a list of qmlproject files in currentSearchPath which are valid modules, +// with URIs matching the relative path from importPathBase. +QStringList getModuleQmlProjectFiles(const Utils::FilePath &importPath, + const Utils::FilePath &projectRootPath) +{ + QStringList qmlProjectFiles; + + QDirIterator it(importPath.toFSPathString(), + QDir::NoDotAndDotDot | QDir::Files, + QDirIterator::Subdirectories); + while (it.hasNext()) { + const QString file = it.next(); + if (!file.endsWith(".qmlproject")) { + continue; + } + + // Add if matching + QString uri = moduleUriFromQmlProject(file); + if (uri.isEmpty()) { + // If the qmlproject file is not a valid module, skip it + continue; + } + + const auto filePath = Utils::FilePath::fromUserInput(file); + const bool isBaseImportPath = filePath.parentDir() == importPath; + + // Check the URI against the original import path before adding + // If we look directly in the search path, the URI doesn't matter + const QString relativePath = filePath.parentDir().relativePathFrom(importPath).path(); + if (isBaseImportPath || uri.replace(".", "/") == relativePath) { + // If the URI matches the path or the file is directly in the import path, add it + qmlProjectFiles.emplace_back(filePath.relativePathFrom(projectRootPath).toFSPathString()); + } + } + return qmlProjectFiles; +} + +QStringList qmlprojectsFromImportPaths(const QStringList &importPaths, + const Utils::FilePath &projectRootPath) +{ + return Utils::transform(importPaths, [&projectRootPath](const QString &importPath) { + const auto importDir = projectRootPath / importPath; + return getModuleQmlProjectFiles(importDir, projectRootPath); + }); +} + QJsonObject qmlProjectTojson(const Utils::FilePath &projectFile) { QmlJS::SimpleReader simpleQmlJSReader; @@ -265,6 +363,9 @@ QJsonObject qmlProjectTojson(const Utils::FilePath &projectFile) bool qtForMCUs = false; + QStringList importPaths; + Utils::FilePath projectRootPath = projectFile.parentDir(); + // convert the non-object props for (const QString &propName : rootNode->propertyNames()) { QJsonObject *currentObj = &rootObject; @@ -300,6 +401,7 @@ QJsonObject qmlProjectTojson(const Utils::FilePath &projectFile) value = rootNode->property(propName).value.toBool() ? "6" : "5"; } else if (propName.contains("importpaths", Qt::CaseInsensitive)) { objKey = "importPaths"; + importPaths = value.toVariant().toStringList(); } else { currentObj = &otherProperties; objKey = propName; // With prefix @@ -413,6 +515,12 @@ QJsonObject qmlProjectTojson(const Utils::FilePath &projectFile) } } + QStringList qmlProjectDependencies; + qmlProjectDependencies.append(qmlprojectsFromImportPaths(importPaths, projectRootPath)); + qmlProjectDependencies.append(qmlprojectsFromFilesNodes(fileGroupsObject, projectRootPath)); + qmlProjectDependencies.sort(); + rootObject.insert("qmlprojectDependencies", QJsonArray::fromStringList(qmlProjectDependencies)); + mcuObject.insert("config", mcuConfigObject); mcuObject.insert("module", mcuModuleObject); qtForMCUs |= !(mcuModuleObject.isEmpty() && mcuConfigObject.isEmpty()); diff --git a/src/plugins/qmlprojectmanager/buildsystem/projectitem/qmlprojectitem.cpp b/src/plugins/qmlprojectmanager/buildsystem/projectitem/qmlprojectitem.cpp index 5229d486ce3..067ada48f03 100644 --- a/src/plugins/qmlprojectmanager/buildsystem/projectitem/qmlprojectitem.cpp +++ b/src/plugins/qmlprojectmanager/buildsystem/projectitem/qmlprojectitem.cpp @@ -209,6 +209,11 @@ void QmlProjectItem::addImportPath(const QString &importPath) insertAndUpdateProjectFile("importPaths", importPaths); } +QStringList QmlProjectItem::qmlProjectModules() const +{ + return m_project["qmlprojectDependencies"].toVariant().toStringList(); +} + QStringList QmlProjectItem::fileSelectors() const { return m_project["runConfig"].toObject()["fileSelectors"].toVariant().toStringList(); diff --git a/src/plugins/qmlprojectmanager/buildsystem/projectitem/qmlprojectitem.h b/src/plugins/qmlprojectmanager/buildsystem/projectitem/qmlprojectitem.h index 5d0b520f144..bcbc3ddadc5 100644 --- a/src/plugins/qmlprojectmanager/buildsystem/projectitem/qmlprojectitem.h +++ b/src/plugins/qmlprojectmanager/buildsystem/projectitem/qmlprojectitem.h @@ -46,6 +46,8 @@ public: void setImportPaths(const QStringList &paths); void addImportPath(const QString &importPath); + QStringList qmlProjectModules() const; + QStringList fileSelectors() const; void setFileSelectors(const QStringList &selectors); void addFileSelector(const QString &selector); diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/README.md b/tests/unit/tests/unittests/qmlprojectmanager/data/README.md index c73ef285904..afb4db417f5 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/data/README.md +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/README.md @@ -38,6 +38,9 @@ that QDS and QUL are aligned on the qmlproject format and that new features in Q qmlproject files for MCU projects. The test set will be tested in the Qt for MCUs repositories to make sure both the original and the converted qmlprojects build correctly. +The test set also includes some dummy qmlproject files to test that the converter correctly picks up +qmlproject modules in the same way Qt for MCUs does. + The qmlproject files in the test set aim to cover all the possible contents of a Qt for MCUs qmlproject, but since new features are added with every release, it is not guaranteed to be exhaustive. diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-1/testfile.qmltojson b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-1/testfile.qmltojson index e362cdf8860..c5e94ef04ea 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-1/testfile.qmltojson +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-1/testfile.qmltojson @@ -260,6 +260,8 @@ }, "otherProperties": { }, + "qmlprojectDependencies": [ + ], "runConfig": { "fileSelectors": [ ], diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-2/testfile.qmltojson b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-2/testfile.qmltojson index 80eaf6fefa3..46f0b34973a 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-2/testfile.qmltojson +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-2/testfile.qmltojson @@ -113,6 +113,8 @@ }, "otherProperties": { }, + "qmlprojectDependencies": [ + ], "runConfig": { "fileSelectors": [ "WXGA", diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-3/testfile.qmltojson b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-3/testfile.qmltojson index e362cdf8860..c5e94ef04ea 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-3/testfile.qmltojson +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-3/testfile.qmltojson @@ -260,6 +260,8 @@ }, "otherProperties": { }, + "qmlprojectDependencies": [ + ], "runConfig": { "fileSelectors": [ ], diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/mcu-modules/from_importpath/imported_module.qmlproject b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/mcu-modules/from_importpath/imported_module.qmlproject new file mode 100644 index 00000000000..237aa1dbc5d --- /dev/null +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/mcu-modules/from_importpath/imported_module.qmlproject @@ -0,0 +1,7 @@ +import QmlProject 1.3 + +Project { + MCU.Module { + uri: "from_importpath" + } +} diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/mcu-modules/from_importpath/mismatched_uri.qmlproject b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/mcu-modules/from_importpath/mismatched_uri.qmlproject new file mode 100644 index 00000000000..abd5f88245b --- /dev/null +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/mcu-modules/from_importpath/mismatched_uri.qmlproject @@ -0,0 +1,9 @@ +import QmlProject 1.3 + +// This qmlproject file will not be picked up from the import path "mcu-modules" +// since the URI doesn't match a relative path to the importPath +Project { + MCU.Module { + uri: "SomeModule" + } +} diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/mcu-modules/module.qmlproject b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/mcu-modules/module.qmlproject new file mode 100644 index 00000000000..fc9bcde9038 --- /dev/null +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/mcu-modules/module.qmlproject @@ -0,0 +1,8 @@ +import QmlProject 1.3 + +// This file will be picked up as an MCU module from importPaths +Project { + MCU.Module { + uri: "MyModule" + } +} diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/mcu-modules/no_module.qmlproject b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/mcu-modules/no_module.qmlproject new file mode 100644 index 00000000000..4b766c067a0 --- /dev/null +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/mcu-modules/no_module.qmlproject @@ -0,0 +1,5 @@ +import QmlProject 1.3 + +// This file will NOT be picked up as an MCU module from importPaths +Project { +} diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/testfile.jsontoqml b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/testfile.jsontoqml index 04911c40f89..0b5989a7719 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/testfile.jsontoqml +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/testfile.jsontoqml @@ -8,7 +8,7 @@ Project { targetDirectory: "/opt/UntitledProject13" enableCMakeGeneration: false widgetApp: true - importPaths: [ "imports","asset_imports" ] + importPaths: [ "imports","asset_imports","mcu-modules" ] qdsVersion: "4.0" quickVersion: "6.2" @@ -81,6 +81,10 @@ Project { MCU.qulModules: ["Controls","Timeline"] } + ModuleFiles { + directory: "../test-set-mcu-2" + } + FontFiles { files: [ "fonts/RobotoFonts.fmp" diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/testfile.qmlproject b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/testfile.qmlproject index 1b5b10ec18b..a6ea8113db8 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/testfile.qmlproject +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/testfile.qmlproject @@ -11,7 +11,7 @@ Project { mainFile: "Main.qml" supportedLanguages: ["no"] primaryLanguage: "en" - importPaths: [ "imports", "asset_imports" ] + importPaths: [ "imports", "asset_imports", "mcu-modules"] idBasedTranslations: true projectRootPath: ".." // END of common properties @@ -84,6 +84,10 @@ Project { ] } + ModuleFiles { + directory: "../test-set-mcu-2" + } + FontFiles { files: [ "fonts/RobotoFonts.fmp" diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/testfile.qmltojson b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/testfile.qmltojson index 634623c9cf2..c7736561b27 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/testfile.qmltojson +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/testfile.qmltojson @@ -108,6 +108,18 @@ }, "type": "Module" }, + { + "directory": "../test-set-mcu-2", + "files": [ + ], + "filters": [ + ], + "mcuProperties": { + }, + "otherProperties": { + }, + "type": "Module" + }, { "directory": "", "files": [ @@ -180,7 +192,8 @@ "fileVersion": 1, "importPaths": [ "imports", - "asset_imports" + "asset_imports", + "mcu-modules" ], "language": { "multiLanguageSupport": true, @@ -213,6 +226,12 @@ "idBasedTranslations": true, "projectRootPath": ".." }, + "qmlprojectDependencies": [ + "../test-set-mcu-2/testfile.qmlproject", + "mcu-modules/from_importpath/imported_module.qmlproject", + "mcu-modules/module.qmlproject", + "qmlproject/module/module.qmlproject" + ], "runConfig": { "fileSelectors": [ ], diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-2/testfile.qmltojson b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-2/testfile.qmltojson index 0ef011b615c..693a2a989e9 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-2/testfile.qmltojson +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-2/testfile.qmltojson @@ -46,6 +46,8 @@ "otherProperties": { "projectRootPath": "../.." }, + "qmlprojectDependencies": [ + ], "runConfig": { "fileSelectors": [ ] diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/getter-setter/mcu_project_with_modules.qmlproject b/tests/unit/tests/unittests/qmlprojectmanager/data/getter-setter/mcu_project_with_modules.qmlproject new file mode 100644 index 00000000000..08f99b98e80 --- /dev/null +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/getter-setter/mcu_project_with_modules.qmlproject @@ -0,0 +1,15 @@ +import QmlProject 1.3 + +Project { + importPaths: [ "../converter/test-set-mcu-1/mcu-modules" ] + + mainFile: "Main.qml" + + ModuleFiles { + files: [ + "file1.qmlproject", + "file2.qmlproject" + ] + directory: "../converter/test-set-mcu-2" + } +} diff --git a/tests/unit/tests/unittests/qmlprojectmanager/projectitem-test.cpp b/tests/unit/tests/unittests/qmlprojectmanager/projectitem-test.cpp index b610aee466b..f3088a56bee 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/projectitem-test.cpp +++ b/tests/unit/tests/unittests/qmlprojectmanager/projectitem-test.cpp @@ -35,6 +35,11 @@ protected: Utils::FilePath::fromString(localTestDataDir + "/file-filters/MaterialBundle.qmlproject"), true); + + projectItemMcuWithModules = std::make_unique( + Utils::FilePath::fromString(localTestDataDir + + "/getter-setter/mcu_project_with_modules.qmlproject"), + true); } static void TearDownTestSuite() @@ -43,6 +48,7 @@ protected: projectItemWithQdsPrefix.reset(); projectItemWithoutQdsPrefix.reset(); projectItemFileFilters.reset(); + projectItemMcuWithModules.reset(); } protected: @@ -54,6 +60,7 @@ protected: localTestDataDir + "/getter-setter/empty.qmlproject"), true); inline static std::unique_ptr projectItemFileFilters; + inline static std::unique_ptr projectItemMcuWithModules; }; auto createAbsoluteFilePaths(const QStringList &fileList) @@ -742,4 +749,24 @@ TEST_F(QmlProjectItem, not_matches_file) ASSERT_FALSE(fileFound); } +TEST_F(QmlProjectItem, qmlproject_modules) +{ + auto qmlProjectModules = projectItemMcuWithModules->qmlProjectModules(); + + ASSERT_THAT( + qmlProjectModules, + UnorderedElementsAre( + "file1.qmlproject", + "file2.qmlproject", + "../converter/test-set-mcu-1/mcu-modules/from_importpath/imported_module.qmlproject", + "../converter/test-set-mcu-2/testfile.qmlproject")); +} + +TEST_F(QmlProjectItem, no_qmlproject_modules) +{ + auto qmlProjectModules = projectItemEmpty->qmlProjectModules(); + + ASSERT_THAT(qmlProjectModules, IsEmpty()); +} + } // namespace