diff options
-rw-r--r-- | doc/reference/module-providers/qbspkgconfig-module-provider.qdoc | 14 | ||||
-rw-r--r-- | share/qbs/module-providers/qbspkgconfig.qbs | 3 | ||||
-rw-r--r-- | src/lib/corelib/jsextensions/pkgconfigjs.cpp | 2 | ||||
-rw-r--r-- | src/lib/pkgconfig/pcpackage.h | 20 | ||||
-rw-r--r-- | src/lib/pkgconfig/pkgconfig.cpp | 208 | ||||
-rw-r--r-- | src/lib/pkgconfig/pkgconfig.h | 3 | ||||
-rw-r--r-- | tests/auto/pkgconfig/testdata/private-dep.pc | 6 | ||||
-rw-r--r-- | tests/auto/pkgconfig/testdata/public-dep.pc | 6 | ||||
-rw-r--r-- | tests/auto/pkgconfig/testdata/requires-test-merged-static.json | 22 | ||||
-rw-r--r-- | tests/auto/pkgconfig/testdata/requires-test-merged.json | 19 | ||||
-rw-r--r-- | tests/auto/pkgconfig/tst_pkgconfig.cpp | 58 |
11 files changed, 345 insertions, 16 deletions
diff --git a/doc/reference/module-providers/qbspkgconfig-module-provider.qdoc b/doc/reference/module-providers/qbspkgconfig-module-provider.qdoc index 80afe3667..4ba176794 100644 --- a/doc/reference/module-providers/qbspkgconfig-module-provider.qdoc +++ b/doc/reference/module-providers/qbspkgconfig-module-provider.qdoc @@ -97,3 +97,17 @@ \defaultvalue \c "" on macOS, \c qbs.sysroot on other platforms */ + +/*! + \qmlproperty bool qbspkgconfig::mergeDependencies + + Holds whether dependencies should be merged by pkg-config or \QBS. + + If set to true, dependencies are merged by pkg-config meaning each generated module + is self-contained and does not depend on other modules. If set to false, generated modules + may depend on other modules and property merging is done by \QBS. The latter approach gives + \QBS more information about dependencies, but may have performance implications during resolve + phase, e.g. when using ABSEIL library. + + \defaultvalue \c true +*/ diff --git a/share/qbs/module-providers/qbspkgconfig.qbs b/share/qbs/module-providers/qbspkgconfig.qbs index 40f0526ed..5309610e1 100644 --- a/share/qbs/module-providers/qbspkgconfig.qbs +++ b/share/qbs/module-providers/qbspkgconfig.qbs @@ -54,6 +54,7 @@ ModuleProvider { return ""; return qbs.sysroot; } + property bool mergeDependencies: true relativeSearchPaths: { @@ -143,6 +144,8 @@ ModuleProvider { var options = {}; options.searchPaths = libDirs; options.sysroot = sysroot; + options.staticMode = staticMode; + options.mergeDependencies = mergeDependencies; if (options.sysroot && !options.searchPaths) { options.searchPaths = [ sysroot + "/usr/lib/pkgconfig", diff --git a/src/lib/corelib/jsextensions/pkgconfigjs.cpp b/src/lib/corelib/jsextensions/pkgconfigjs.cpp index f4b7b08d2..2bd7b5d17 100644 --- a/src/lib/corelib/jsextensions/pkgconfigjs.cpp +++ b/src/lib/corelib/jsextensions/pkgconfigjs.cpp @@ -232,6 +232,8 @@ PkgConfig::Options PkgConfigJs::convertOptions(const QProcessEnvironment &env, c std::back_inserter(result.systemLibraryPaths), [](const QString &str){ return str.toStdString(); }); result.disableUninstalled = map.value(QStringLiteral("disableUninstalled"), true).toBool(); + result.staticMode = map.value(QStringLiteral("staticMode"), false).toBool(); + result.mergeDependencies = map.value(QStringLiteral("mergeDependencies"), true).toBool(); result.globalVariables = variablesFromQVariantMap(map.value(QStringLiteral("globalVariables")).toMap()); result.systemVariables = envToVariablesMap(env); diff --git a/src/lib/pkgconfig/pcpackage.h b/src/lib/pkgconfig/pcpackage.h index 9edd56c60..340a4698f 100644 --- a/src/lib/pkgconfig/pcpackage.h +++ b/src/lib/pkgconfig/pcpackage.h @@ -171,6 +171,26 @@ public: explicit PcException(const std::string &message) : std::runtime_error(message) {} }; +inline bool operator==(const PcPackage::Flag &lhs, const PcPackage::Flag &rhs) +{ + return lhs.type == rhs.type && lhs.value == rhs.value; +} + +inline bool operator!=(const PcPackage::Flag &lhs, const PcPackage::Flag &rhs) +{ + return !(lhs == rhs); +} + } // namespace qbs +namespace std { +template<> struct hash<qbs::PcPackage::Flag> +{ + size_t operator()(const qbs::PcPackage::Flag &v) const noexcept + { + return hash<std::string>()(v.value) + size_t(v.type); + } +}; +} // namespace std + #endif // PC_PACKAGE_H diff --git a/src/lib/pkgconfig/pkgconfig.cpp b/src/lib/pkgconfig/pkgconfig.cpp index 2b9c1edea..8a3c81c64 100644 --- a/src/lib/pkgconfig/pkgconfig.cpp +++ b/src/lib/pkgconfig/pkgconfig.cpp @@ -106,6 +106,50 @@ constexpr inline char listSeparator() noexcept #endif } +// based on https://stackoverflow.com/a/33135699/295518 +int compareVersions(std::string_view v1, std::string_view v2) +{ + for (size_t i = 0, j = 0; i < v1.length() || j < v2.length(); ) { + size_t acc1 = 0; + size_t acc2 = 0; + + while (i < v1.length() && v1[i] != '.') { + acc1 = acc1 * 10 + (v1[i] - '0'); + i++; + } + while (j < v2.length() && v2[j] != '.') { + acc2 = acc2 * 10 + (v2[j] - '0'); + j++; + } + + if (acc1 < acc2) + return -1; + if (acc1 > acc2) + return +1; + + ++i; + ++j; + } + return 0; +} + +using ComparisonType = PcPackage::RequiredVersion::ComparisonType; + +bool versionTest(ComparisonType comparison, std::string_view a, std::string_view b) +{ + switch (comparison) { + case ComparisonType::LessThan: return compareVersions(a, b) < 0; + case ComparisonType::GreaterThan: return compareVersions(a, b) > 0; + case ComparisonType::LessThanEqual: return compareVersions(a, b) <= 0; + case ComparisonType::GreaterThanEqual: return compareVersions(a, b) >= 0; + case ComparisonType::Equal: return compareVersions(a, b) == 0; + case ComparisonType::NotEqual: return compareVersions(a, b) != 0; + case ComparisonType::AlwaysMatch: return true; + } + + return false; +} + [[noreturn]] void raizeUnknownPackageException(std::string_view package) { std::string message; @@ -115,6 +159,13 @@ constexpr inline char listSeparator() noexcept throw PcException(message); } +template <class C> +C &operator<<(C &container, const C &other) +{ + container.insert(container.end(), other.cbegin(), other.cend()); + return container; +} + } // namespace PkgConfig::PkgConfig() @@ -237,6 +288,160 @@ std::vector<std::string> getPcFilePaths(const std::vector<std::string> &searchPa } #endif +PcBrokenPackage makeMissingDependency( + const PcPackage &package, const PcPackage::RequiredVersion &depVersion) +{ + std::string message; + message += "Package "; + message += package.name; + message += " requires package "; + message += depVersion.name; + message += " but it is not found"; + return PcBrokenPackage{ + package.filePath, package.baseFileName, std::move(message)}; +} + +PcBrokenPackage makeBrokenDependency( + const PcPackage &package, const PcPackage::RequiredVersion &depVersion) +{ + std::string message; + message += "Package "; + message += package.name; + message += " requires package "; + message += depVersion.name; + message += " but it is broken"; + return PcBrokenPackage{ + package.filePath, package.baseFileName, std::move(message)}; +} + +PcBrokenPackage makeVersionMismatchDependency( + const PcPackage &package, + const PcPackage &depPackage, + const PcPackage::RequiredVersion &depVersion) +{ + std::string message; + message += "Package "; + message += package.name; + message += " requires version "; + message += PcPackage::RequiredVersion::comparisonToString( + depVersion.comparison); + message += depVersion.version; + message += " but "; + message += depPackage.version; + message += " is present"; + return PcBrokenPackage{ + package.filePath, package.baseFileName, std::move(message)}; +} + +PkgConfig::Packages PkgConfig::mergeDependencies(const PkgConfig::Packages &packages) const +{ + std::unordered_map<std::string_view, const PcPackageVariant *> packageHash; + + struct MergedHashEntry + { + PcPackageVariant package; // merged package or broken package + std::vector<const PcPackage *> deps; // unmerged transitive deps, including Package itself + }; + std::unordered_map<std::string, MergedHashEntry> mergedHash; + + for (const auto &package: packages) + packageHash[package.getBaseFileName()] = &package; + + auto func = [&](const PcPackageVariant &package, auto &f) -> const MergedHashEntry & + { + const auto it = mergedHash.find(package.getBaseFileName()); + if (it != mergedHash.end()) + return it->second; + + auto &entry = mergedHash[package.getBaseFileName()]; + + auto visitor = [&](auto &&package) -> PcPackageVariant { + + using T = std::decay_t<decltype(package)>; + if constexpr (std::is_same_v<T, PcPackage>) { // NOLINT + + using Flags = std::vector<PcPackage::Flag>; + + // returns true if multiple copies of the flag can present in the same package + // we can't properly merge flags that have multiple parameters except for + // -framework which we handle correctly. + auto canHaveDuplicates = [](const PcPackage::Flag::Type &type) { + return type == PcPackage::Flag::Type::LinkerFlag + || type == PcPackage::Flag::Type::CompilerFlag; + }; + + std::unordered_set<PcPackage::Flag> visitedFlags; + // appends only those flags to the target that were not seen before (except for + // ones that can have duplicates) + auto mergeFlags = [&](Flags &target, const Flags &source) + { + for (const auto &flag: source) { + if (canHaveDuplicates(flag.type) || visitedFlags.insert(flag).second) + target.push_back(flag); + } + }; + + std::unordered_set<const PcPackage *> visitedDeps; + + PcPackage result; + // copy only meta info for now + result.filePath = package.filePath; + result.baseFileName = package.baseFileName; + result.name = package.name; + result.version = package.version; + result.description = package.description; + result.url = package.url; + + auto allDependencies = package.requiresPublic; + if (m_options.staticMode) + allDependencies << package.requiresPrivate; + + for (const auto &dependency: allDependencies) { + const auto it = packageHash.find(dependency.name); + if (it == packageHash.end()) + return makeMissingDependency(result, dependency); + + const auto childEntry = f(*it->second, f); + if (childEntry.package.isBroken()) + return makeBrokenDependency(result, dependency); + + const auto &mergedPackage = childEntry.package.asPackage(); + const bool versionOk = versionTest( + dependency.comparison, mergedPackage.version, dependency.version); + if (!versionOk) + return makeVersionMismatchDependency(result, mergedPackage, dependency); + + for (const auto *dep: childEntry.deps) { + if (visitedDeps.insert(dep).second) + entry.deps.push_back(dep); + } + } + + entry.deps.push_back(&package); + + for (const auto *dep: entry.deps) { + mergeFlags(result.libs, dep->libs); + mergeFlags(result.cflags, dep->cflags); + } + + return result; + } + return package; + }; + entry.package = package.visit(visitor); + + return entry; + }; + + for (auto &package: packages) + func(package, func); + + Packages result; + for (auto &[key, value]: mergedHash) + result.push_back(std::move(value.package)); + return result; +} + PkgConfig::Packages PkgConfig::findPackages() const { Packages result; @@ -270,6 +475,9 @@ PkgConfig::Packages PkgConfig::findPackages() const result.emplace_back(std::move(pkg)); } + if (m_options.mergeDependencies) + result = mergeDependencies(result); + const auto lessThanPackage = [](const PcPackageVariant &lhs, const PcPackageVariant &rhs) { return lhs.getBaseFileName() < rhs.getBaseFileName(); diff --git a/src/lib/pkgconfig/pkgconfig.h b/src/lib/pkgconfig/pkgconfig.h index d66d58985..76e4a3ac3 100644 --- a/src/lib/pkgconfig/pkgconfig.h +++ b/src/lib/pkgconfig/pkgconfig.h @@ -56,6 +56,8 @@ public: bool allowSystemLibraryPaths{false}; // PKG_CONFIG_ALLOW_SYSTEM_LIBS std::vector<std::string> systemLibraryPaths; // PKG_CONFIG_SYSTEM_LIBRARY_PATH bool disableUninstalled{true}; // PKG_CONFIG_DISABLE_UNINSTALLED + bool staticMode{false}; + bool mergeDependencies{true}; VariablesMap globalVariables; VariablesMap systemVariables; }; @@ -73,6 +75,7 @@ public: private: Packages findPackages() const; + Packages mergeDependencies(const Packages &packages) const; private: Options m_options; diff --git a/tests/auto/pkgconfig/testdata/private-dep.pc b/tests/auto/pkgconfig/testdata/private-dep.pc new file mode 100644 index 000000000..cb401391d --- /dev/null +++ b/tests/auto/pkgconfig/testdata/private-dep.pc @@ -0,0 +1,6 @@ +Name: Requires test package +Description: Dummy pkgconfig test package for testing Requires/Requires.private +Version: 1.0.0 +Libs: -L/private-dep/lib -lprivate-dep +Cflags: -I/private-dep/include + diff --git a/tests/auto/pkgconfig/testdata/public-dep.pc b/tests/auto/pkgconfig/testdata/public-dep.pc new file mode 100644 index 000000000..e450e46f1 --- /dev/null +++ b/tests/auto/pkgconfig/testdata/public-dep.pc @@ -0,0 +1,6 @@ +Name: Requires test package +Description: Dummy pkgconfig test package for testing Requires/Requires.private +Version: 1.0.0 +Requires.private: +Libs: -L/public-dep/lib -lpublic-dep +Cflags: -I/public-dep/include diff --git a/tests/auto/pkgconfig/testdata/requires-test-merged-static.json b/tests/auto/pkgconfig/testdata/requires-test-merged-static.json new file mode 100644 index 000000000..2c43b2d40 --- /dev/null +++ b/tests/auto/pkgconfig/testdata/requires-test-merged-static.json @@ -0,0 +1,22 @@ +{ + "Name": "Requires test package", + "Description": "Dummy pkgconfig test package for testing Requires/Requires.private", + "Version": "1.0.0", + "Libs": [ + {"Type": "LibraryPath", "Value": "/public-dep/lib"}, + {"Type": "LibraryName", "Value": "public-dep"}, + {"Type": "LibraryPath", "Value": "/private-dep/lib"}, + {"Type": "LibraryName", "Value": "private-dep"}, + {"Type": "LibraryPath", "Value": "/requires-test/lib"}, + {"Type": "LibraryName", "Value": "requires-test"} + ], + "Cflags": [ + {"Type": "IncludePath", "Value": "/public-dep/include"}, + {"Type": "IncludePath", "Value": "/private-dep/include"}, + {"Type": "IncludePath", "Value": "/requires-test/include"} + ], + "Requires": [ + ], + "RequiresPrivate": [ + ] +} diff --git a/tests/auto/pkgconfig/testdata/requires-test-merged.json b/tests/auto/pkgconfig/testdata/requires-test-merged.json new file mode 100644 index 000000000..88114ba30 --- /dev/null +++ b/tests/auto/pkgconfig/testdata/requires-test-merged.json @@ -0,0 +1,19 @@ +{ + "Name": "Requires test package", + "Description": "Dummy pkgconfig test package for testing Requires/Requires.private", + "Version": "1.0.0", + "Libs": [ + {"Type": "LibraryPath", "Value": "/public-dep/lib"}, + {"Type": "LibraryName", "Value": "public-dep"}, + {"Type": "LibraryPath", "Value": "/requires-test/lib"}, + {"Type": "LibraryName", "Value": "requires-test"} + ], + "Cflags": [ + {"Type": "IncludePath", "Value": "/public-dep/include"}, + {"Type": "IncludePath", "Value": "/requires-test/include"} + ], + "Requires": [ + ], + "RequiresPrivate": [ + ] +} diff --git a/tests/auto/pkgconfig/tst_pkgconfig.cpp b/tests/auto/pkgconfig/tst_pkgconfig.cpp index 96a70d199..1b04d231b 100644 --- a/tests/auto/pkgconfig/tst_pkgconfig.cpp +++ b/tests/auto/pkgconfig/tst_pkgconfig.cpp @@ -60,24 +60,32 @@ void TestPkgConfig::initTestCase() void TestPkgConfig::pkgConfig() { - QFETCH(QString, fileName); + QFETCH(QString, pcFileName); + QFETCH(QString, jsonFileName); QFETCH(QVariantMap, optionsMap); - Options options = qbs::Internal::PkgConfigJs::convertOptions(QProcessEnvironment::systemEnvironment(), optionsMap); + if (jsonFileName.isEmpty()) + jsonFileName = pcFileName; + + if (!optionsMap.contains("mergeDependencies")) + optionsMap["mergeDependencies"] = false; + + Options options = qbs::Internal::PkgConfigJs::convertOptions( + QProcessEnvironment::systemEnvironment(), optionsMap); options.searchPaths.push_back(m_workingDataDir.toStdString()); PkgConfig pkgConfig(std::move(options)); - QFile jsonFile(m_workingDataDir + "/" + fileName + ".json"); + QFile jsonFile(m_workingDataDir + "/" + jsonFileName + ".json"); QVERIFY(jsonFile.open(QIODevice::ReadOnly)); QJsonParseError error{}; const auto json = QJsonDocument::fromJson(jsonFile.readAll(), &error).toVariant().toMap(); QCOMPARE(error.error, QJsonParseError::NoError); - const auto &packageOr = pkgConfig.getPackage(fileName.toStdString()); + const auto &packageOr = pkgConfig.getPackage(pcFileName.toStdString()); QVERIFY(packageOr.isValid()); const auto &package = packageOr.asPackage(); - QCOMPARE(QString::fromStdString(package.baseFileName), fileName); + QCOMPARE(QString::fromStdString(package.baseFileName), pcFileName); QCOMPARE(QString::fromStdString(package.name), json.value("Name").toString()); QCOMPARE(QString::fromStdString(package.description), json.value("Description").toString()); QCOMPARE(QString::fromStdString(package.version), json.value("Version").toString()); @@ -155,20 +163,38 @@ void TestPkgConfig::pkgConfig() void TestPkgConfig::pkgConfig_data() { - QTest::addColumn<QString>("fileName"); + QTest::addColumn<QString>("pcFileName"); + QTest::addColumn<QString>("jsonFileName"); QTest::addColumn<QVariantMap>("optionsMap"); - QTest::newRow("non-l-required") << QStringLiteral("non-l-required") << QVariantMap(); - QTest::newRow("simple") << QStringLiteral("simple") << QVariantMap(); - QTest::newRow("requires-test") << QStringLiteral("requires-test") << QVariantMap(); - QTest::newRow("special-flags") << QStringLiteral("special-flags") << QVariantMap(); - QTest::newRow("system") << QStringLiteral("system") << QVariantMap(); + QTest::newRow("non-l-required") + << QStringLiteral("non-l-required") << QString() << QVariantMap(); + QTest::newRow("simple") + << QStringLiteral("simple") << QString() << QVariantMap(); + QTest::newRow("requires-test") + << QStringLiteral("requires-test") << QString() << QVariantMap(); + QTest::newRow("requires-test-merged") + << QStringLiteral("requires-test") + << QStringLiteral("requires-test-merged") + << QVariantMap({{"mergeDependencies", true}}); + QTest::newRow("requires-test-merged-static") + << QStringLiteral("requires-test") + << QStringLiteral("requires-test-merged-static") + << QVariantMap({{"mergeDependencies", true}, {"staticMode", true}}); + QTest::newRow("special-flags") + << QStringLiteral("special-flags") << QString() << QVariantMap(); + QTest::newRow("system") + << QStringLiteral("system") << QString() << QVariantMap(); QTest::newRow("sysroot") - << QStringLiteral("sysroot") << QVariantMap({{"sysroot", "/newroot"}}); - QTest::newRow("tilde") << QStringLiteral("tilde") << QVariantMap(); - QTest::newRow("variables") << QStringLiteral("variables") << QVariantMap(); - QTest::newRow("whitespace") << QStringLiteral("whitespace") << QVariantMap(); - QTest::newRow("base.name") << QStringLiteral("base.name") << QVariantMap(); + << QStringLiteral("sysroot") << QString() << QVariantMap({{"sysroot", "/newroot"}}); + QTest::newRow("tilde") + << QStringLiteral("tilde") << QString() << QVariantMap(); + QTest::newRow("variables") + << QStringLiteral("variables") << QString() << QVariantMap(); + QTest::newRow("whitespace") + << QStringLiteral("whitespace") << QString() << QVariantMap(); + QTest::newRow("base.name") + << QStringLiteral("base.name") << QString() << QVariantMap(); } void TestPkgConfig::benchSystem() |