aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/reference/module-providers/qbspkgconfig-module-provider.qdoc14
-rw-r--r--share/qbs/module-providers/qbspkgconfig.qbs3
-rw-r--r--src/lib/corelib/jsextensions/pkgconfigjs.cpp2
-rw-r--r--src/lib/pkgconfig/pcpackage.h20
-rw-r--r--src/lib/pkgconfig/pkgconfig.cpp208
-rw-r--r--src/lib/pkgconfig/pkgconfig.h3
-rw-r--r--tests/auto/pkgconfig/testdata/private-dep.pc6
-rw-r--r--tests/auto/pkgconfig/testdata/public-dep.pc6
-rw-r--r--tests/auto/pkgconfig/testdata/requires-test-merged-static.json22
-rw-r--r--tests/auto/pkgconfig/testdata/requires-test-merged.json19
-rw-r--r--tests/auto/pkgconfig/tst_pkgconfig.cpp58
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()