aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--qbs.pro5
-rw-r--r--src/lib/CMakeLists.txt1
-rw-r--r--src/lib/corelib/CMakeLists.txt3
-rw-r--r--src/lib/corelib/corelib.pro2
-rw-r--r--src/lib/corelib/corelib.qbs3
-rw-r--r--src/lib/corelib/jsextensions/jsextensions.cpp1
-rw-r--r--src/lib/corelib/jsextensions/jsextensions.pri4
-rw-r--r--src/lib/corelib/jsextensions/pkgconfigjs.cpp211
-rw-r--r--src/lib/corelib/jsextensions/pkgconfigjs.h106
-rw-r--r--src/lib/corelib/tools/stlutils.h6
-rw-r--r--src/lib/libs.qbs1
-rw-r--r--src/lib/pkgconfig/CMakeLists.txt27
-rw-r--r--src/lib/pkgconfig/pcpackage.cpp172
-rw-r--r--src/lib/pkgconfig/pcpackage.h137
-rw-r--r--src/lib/pkgconfig/pcparser.cpp762
-rw-r--r--src/lib/pkgconfig/pcparser.h84
-rw-r--r--src/lib/pkgconfig/pkgconfig.cpp265
-rw-r--r--src/lib/pkgconfig/pkgconfig.h88
-rw-r--r--src/lib/pkgconfig/pkgconfig.pro24
-rw-r--r--src/lib/pkgconfig/pkgconfig.qbs61
-rw-r--r--src/lib/pkgconfig/use_pkgconfig.pri41
-rw-r--r--tests/auto/CMakeLists.txt1
-rw-r--r--tests/auto/auto.qbs1
-rw-r--r--tests/auto/pkgconfig/CMakeLists.txt8
-rw-r--r--tests/auto/pkgconfig/pkgconfig.qbs19
-rw-r--r--tests/auto/pkgconfig/testdata/non-l-required.json12
-rw-r--r--tests/auto/pkgconfig/testdata/non-l-required.pc5
-rw-r--r--tests/auto/pkgconfig/testdata/requires-test.json18
-rw-r--r--tests/auto/pkgconfig/testdata/requires-test.pc8
-rw-r--r--tests/auto/pkgconfig/testdata/simple.json20
-rw-r--r--tests/auto/pkgconfig/testdata/simple.pc12
-rw-r--r--tests/auto/pkgconfig/testdata/special-flags.json30
-rw-r--r--tests/auto/pkgconfig/testdata/special-flags.pc11
-rw-r--r--tests/auto/pkgconfig/testdata/sysroot.json21
-rw-r--r--tests/auto/pkgconfig/testdata/sysroot.pc12
-rw-r--r--tests/auto/pkgconfig/testdata/system.json17
-rw-r--r--tests/auto/pkgconfig/testdata/system.pc10
-rw-r--r--tests/auto/pkgconfig/testdata/tilde.json11
-rw-r--r--tests/auto/pkgconfig/testdata/tilde.pc5
-rw-r--r--tests/auto/pkgconfig/testdata/variables.json17
-rw-r--r--tests/auto/pkgconfig/testdata/variables.pc11
-rw-r--r--tests/auto/pkgconfig/testdata/whitespace.json24
-rw-r--r--tests/auto/pkgconfig/testdata/whitespace.pc11
-rw-r--r--tests/auto/pkgconfig/tst_pkgconfig.cpp181
-rw-r--r--tests/auto/pkgconfig/tst_pkgconfig.h53
-rw-r--r--tests/auto/shared.h2
46 files changed, 2521 insertions, 3 deletions
diff --git a/qbs.pro b/qbs.pro
index 0ec8d07b9..9777615f4 100644
--- a/qbs.pro
+++ b/qbs.pro
@@ -30,7 +30,9 @@ defineTest(minQtVersion) {
}
TEMPLATE = subdirs
+pkgconfig.file = src/lib/pkgconfig/pkgconfig.pro
corelib.file = src/lib/corelib/corelib.pro
+corelib.depends = pkgconfig
msbuildlib.subdir = src/lib/msbuild
msbuildlib.depends = corelib
src_app.subdir = src/app
@@ -45,10 +47,11 @@ static_res.file = static-res.pro
static_res.depends = src_app src_libexec src_plugins static.pro
qbs_use_bundled_qtscript {
scriptenginelib.file = src/lib/scriptengine/scriptengine.pro
- corelib.depends = scriptenginelib
+ corelib.depends += scriptenginelib
SUBDIRS += scriptenginelib
}
SUBDIRS += \
+ pkgconfig \
corelib\
msbuildlib\
src_app\
diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt
index a463d6464..48eee2608 100644
--- a/src/lib/CMakeLists.txt
+++ b/src/lib/CMakeLists.txt
@@ -2,5 +2,6 @@ if (QBS_USE_BUNDLED_QT_SCRIPT OR NOT Qt5Script_FOUND)
add_subdirectory(scriptengine)
endif()
+add_subdirectory(pkgconfig)
add_subdirectory(corelib)
add_subdirectory(msbuild)
diff --git a/src/lib/corelib/CMakeLists.txt b/src/lib/corelib/CMakeLists.txt
index 2a38a4943..d4b2d8d38 100644
--- a/src/lib/corelib/CMakeLists.txt
+++ b/src/lib/corelib/CMakeLists.txt
@@ -158,6 +158,8 @@ set(JS_EXTENSIONS_SOURCES
jsextensions.h
moduleproperties.cpp
moduleproperties.h
+ pkgconfigjs.cpp
+ pkgconfigjs.h
process.cpp
temporarydir.cpp
textfile.cpp
@@ -426,6 +428,7 @@ add_qbs_library(qbscore
Qt${QT_VERSION_MAJOR}::Network
Qt${QT_VERSION_MAJOR}::Xml
Qt6Core5Compat
+ qbspkgconfig
qbsscriptengine
PUBLIC_DEPENDS
Qt${QT_VERSION_MAJOR}::Core
diff --git a/src/lib/corelib/corelib.pro b/src/lib/corelib/corelib.pro
index 799aebda1..afe07f48f 100644
--- a/src/lib/corelib/corelib.pro
+++ b/src/lib/corelib/corelib.pro
@@ -8,6 +8,8 @@ qbs_use_bundled_qtscript {
QT += script
}
+include(../pkgconfig/use_pkgconfig.pri)
+
isEmpty(QBS_RELATIVE_LIBEXEC_PATH) {
win32:QBS_RELATIVE_LIBEXEC_PATH=../bin
else:QBS_RELATIVE_LIBEXEC_PATH=../libexec/qbs
diff --git a/src/lib/corelib/corelib.qbs b/src/lib/corelib/corelib.qbs
index 0648a051f..6656d638b 100644
--- a/src/lib/corelib/corelib.qbs
+++ b/src/lib/corelib/corelib.qbs
@@ -16,6 +16,7 @@ QbsLibrary {
name: "qbsscriptengine"
condition: qbsbuildconfig.useBundledQtScript || !Qt.script.present
}
+ Depends { name: "qbspkgconfig" }
name: "qbscore"
property stringList bundledQtScriptIncludes: qbsbuildconfig.useBundledQtScript
|| !Qt.script.present ? qbsscriptengine.includePaths : []
@@ -235,6 +236,8 @@ QbsLibrary {
"jsextensions.h",
"moduleproperties.cpp",
"moduleproperties.h",
+ "pkgconfigjs.cpp",
+ "pkgconfigjs.h",
"process.cpp",
"temporarydir.cpp",
"textfile.cpp",
diff --git a/src/lib/corelib/jsextensions/jsextensions.cpp b/src/lib/corelib/jsextensions/jsextensions.cpp
index 052fb79e4..fc464b44d 100644
--- a/src/lib/corelib/jsextensions/jsextensions.cpp
+++ b/src/lib/corelib/jsextensions/jsextensions.cpp
@@ -57,6 +57,7 @@ static InitializerMap setupMap()
ADD_JS_EXTENSION(Environment);
ADD_JS_EXTENSION(File);
ADD_JS_EXTENSION(FileInfo);
+ ADD_JS_EXTENSION(PkgConfig);
ADD_JS_EXTENSION(Process);
ADD_JS_EXTENSION(PropertyList);
ADD_JS_EXTENSION(TemporaryDir);
diff --git a/src/lib/corelib/jsextensions/jsextensions.pri b/src/lib/corelib/jsextensions/jsextensions.pri
index 004a3e42a..d77f5a687 100644
--- a/src/lib/corelib/jsextensions/jsextensions.pri
+++ b/src/lib/corelib/jsextensions/jsextensions.pri
@@ -2,7 +2,8 @@ QT += xml
HEADERS += \
$$PWD/moduleproperties.h \
- $$PWD/jsextensions.h
+ $$PWD/jsextensions.h \
+ $$PWD/pkgconfigjs.h
SOURCES += \
$$PWD/environmentextension.cpp \
@@ -11,6 +12,7 @@ SOURCES += \
$$PWD/temporarydir.cpp \
$$PWD/textfile.cpp \
$$PWD/binaryfile.cpp \
+ $$PWD/pkgconfigjs.cpp \
$$PWD/process.cpp \
$$PWD/moduleproperties.cpp \
$$PWD/domxml.cpp \
diff --git a/src/lib/corelib/jsextensions/pkgconfigjs.cpp b/src/lib/corelib/jsextensions/pkgconfigjs.cpp
new file mode 100644
index 000000000..4490a14a7
--- /dev/null
+++ b/src/lib/corelib/jsextensions/pkgconfigjs.cpp
@@ -0,0 +1,211 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 Ivan Komissarov (abbapoh@gmail.com)
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qbs.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "pkgconfigjs.h"
+
+#include <language/scriptengine.h>
+
+#include <QtScript/qscriptengine.h>
+#include <QtScript/qscriptvalue.h>
+
+#include <QtCore/QProcessEnvironment>
+
+#include <stdexcept>
+
+namespace qbs {
+namespace Internal {
+
+namespace {
+
+template<typename C, typename F> QVariantList convert(const C &c, F &&f)
+{
+ QVariantList result;
+ result.reserve(c.size());
+ std::transform(c.begin(), c.end(), std::back_inserter(result), f);
+ return result;
+}
+
+QVariantMap packageToMap(const PcPackage &package)
+{
+ QVariantMap result;
+ result[QStringLiteral("filePath")] = QString::fromStdString(package.filePath);
+ result[QStringLiteral("baseFileName")] = QString::fromStdString(package.baseFileName);
+ result[QStringLiteral("name")] = QString::fromStdString(package.name);
+ result[QStringLiteral("version")] = QString::fromStdString(package.version);
+ result[QStringLiteral("description")] = QString::fromStdString(package.description);
+ result[QStringLiteral("url")] = QString::fromStdString(package.url);
+
+ const auto flagToMap = [](const PcPackage::Flag &flag)
+ {
+ QVariantMap result;
+ const auto value = QString::fromStdString(flag.value);
+ result[QStringLiteral("type")] = QVariant::fromValue(qint32(flag.type));
+ result[QStringLiteral("value")] = value;
+ return result;
+ };
+
+ const auto requiredVersionToMap = [](const PcPackage::RequiredVersion &version)
+ {
+ QVariantMap result;
+ result[QStringLiteral("name")] = QString::fromStdString(version.name);
+ result[QStringLiteral("version")] = QString::fromStdString(version.version);
+ result[QStringLiteral("comparison")] = QVariant::fromValue(qint32(version.comparison));
+ return result;
+ };
+
+ result[QStringLiteral("libs")] = convert(package.libs, flagToMap);
+ result[QStringLiteral("libsPrivate")] = convert(package.libsPrivate, flagToMap);
+ result[QStringLiteral("cflags")] = convert(package.cflags, flagToMap);
+ result[QStringLiteral("requires")] = convert(package.requiresPublic, requiredVersionToMap);
+ result[QStringLiteral("requiresPrivate")] =
+ convert(package.requiresPrivate, requiredVersionToMap);
+ result[QStringLiteral("conflicts")] = convert(package.conflicts, requiredVersionToMap);
+
+ return result;
+};
+
+QVariantMap brokenPackageToMap(const PcBrokenPackage &package)
+{
+ QVariantMap result;
+ result[QStringLiteral("filePath")] = QString::fromStdString(package.filePath);
+ result[QStringLiteral("errorText")] = QString::fromStdString(package.errorText);
+ return result;
+}
+
+PcPackage::VariablesMap envToVariablesMap(const QProcessEnvironment &env)
+{
+ PcPackage::VariablesMap result;
+ const auto keys = env.keys();
+ for (const auto &key : keys)
+ result[key.toStdString()] = env.value(key).toStdString();
+ return result;
+}
+
+PcPackage::VariablesMap variablesFromQVariantMap(const QVariantMap &map)
+{
+ PcPackage::VariablesMap result;
+ for (auto it = map.cbegin(), end = map.cend(); it != end; ++it)
+ result[it.key().toStdString()] = it.value().toString().toStdString();
+ return result;
+}
+
+std::vector<std::string> stringListToStdVector(const QStringList &list)
+{
+ std::vector<std::string> result;
+ result.reserve(list.size());
+ for (const auto &string : list)
+ result.push_back(string.toStdString());
+ return result;
+}
+
+} // namespace
+
+QScriptValue PkgConfigJs::ctor(QScriptContext *context, QScriptEngine *engine)
+{
+ try {
+ PkgConfigJs *e = nullptr;
+ switch (context->argumentCount()) {
+ case 0:
+ e = new PkgConfigJs(context, engine);
+ break;
+ case 1:
+ e = new PkgConfigJs(context, engine, context->argument(0).toVariant().toMap());
+ break;
+
+ default:
+ return context->throwError(
+ QStringLiteral("TextFile constructor takes at most three parameters."));
+ }
+
+ return engine->newQObject(e, QScriptEngine::ScriptOwnership);
+ } catch (const PcException &e) {
+ return context->throwError(QString::fromUtf8(e.what()));
+ }
+}
+
+PkgConfigJs::PkgConfigJs(
+ QScriptContext *context, QScriptEngine *engine, const QVariantMap &options) :
+ m_pkgConfig(std::make_unique<PkgConfig>(
+ convertOptions(static_cast<ScriptEngine *>(engine)->environment(), options)))
+{
+ Q_UNUSED(context);
+ for (const auto &package : m_pkgConfig->packages())
+ m_packages.insert(QString::fromStdString(package.baseFileName), packageToMap(package));
+
+ for (const auto &package : m_pkgConfig->brokenPackages())
+ m_brokenPackages.push_back(brokenPackageToMap(package));
+}
+
+PkgConfig::Options PkgConfigJs::convertOptions(const QProcessEnvironment &env, const QVariantMap &map)
+{
+ PkgConfig::Options result;
+ result.searchPaths =
+ stringListToStdVector(map.value(QStringLiteral("searchPaths")).toStringList());
+ result.sysroot = map.value(QStringLiteral("sysroot")).toString().toStdString();
+ result.topBuildDir = map.value(QStringLiteral("topBuildDir")).toString().toStdString();
+ result.allowSystemLibraryPaths =
+ map.value(QStringLiteral("allowSystemLibraryPaths"), false).toBool();
+ const auto systemLibraryPaths = map.value(QStringLiteral("systemLibraryPaths")).toStringList();
+ result.systemLibraryPaths.reserve(systemLibraryPaths.size());
+ std::transform(
+ systemLibraryPaths.begin(),
+ systemLibraryPaths.end(),
+ std::back_inserter(result.systemLibraryPaths),
+ [](const QString &str){ return str.toStdString(); });
+ result.disableUninstalled = map.value(QStringLiteral("disableUninstalled"), true).toBool();
+ result.globalVariables =
+ variablesFromQVariantMap(map.value(QStringLiteral("globalVariables")).toMap());
+ result.systemVariables = envToVariablesMap(env);
+
+ return result;
+}
+
+} // namespace Internal
+} // namespace qbs
+
+void initializeJsExtensionPkgConfig(QScriptValue extensionObject)
+{
+ using namespace qbs::Internal;
+ QScriptEngine *engine = extensionObject.engine();
+ QScriptValue obj = engine->newQMetaObject(
+ &PkgConfigJs::staticMetaObject, engine->newFunction(&PkgConfigJs::ctor));
+ extensionObject.setProperty(QStringLiteral("PkgConfig"), obj);
+}
+
+Q_DECLARE_METATYPE(qbs::Internal::PkgConfigJs *)
diff --git a/src/lib/corelib/jsextensions/pkgconfigjs.h b/src/lib/corelib/jsextensions/pkgconfigjs.h
new file mode 100644
index 000000000..66575d8f3
--- /dev/null
+++ b/src/lib/corelib/jsextensions/pkgconfigjs.h
@@ -0,0 +1,106 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 Ivan Komissarov (abbapoh@gmail.com)
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qbs.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "tools/qbs_export.h"
+#include <tools/stlutils.h>
+
+#include <pkgconfig.h>
+
+#include <QtCore/qobject.h>
+#include <QtCore/qvariant.h>
+
+#include <QtScript/qscriptable.h>
+
+#include <memory>
+
+class QProcessEnvironment;
+
+namespace qbs {
+namespace Internal {
+
+class QBS_AUTOTEST_EXPORT PkgConfigJs : public QObject, QScriptable
+{
+ Q_OBJECT
+public:
+
+ // can we trick moc here to avoid duplication?
+ enum class FlagType {
+ LibraryName = toUnderlying(PcPackage::Flag::Type::LibraryName),
+ LibraryPath = toUnderlying(PcPackage::Flag::Type::LibraryPath),
+ StaticLibraryName = toUnderlying(PcPackage::Flag::Type::StaticLibraryName),
+ Framework = toUnderlying(PcPackage::Flag::Type::Framework),
+ FrameworkPath = toUnderlying(PcPackage::Flag::Type::FrameworkPath),
+ LinkerFlags = toUnderlying(PcPackage::Flag::Type::LinkerFlag),
+ IncludePath = toUnderlying(PcPackage::Flag::Type::IncludePath),
+ SystemIncludePath = toUnderlying(PcPackage::Flag::Type::SystemIncludePath),
+ Define = toUnderlying(PcPackage::Flag::Type::Define),
+ CompilerFlags = toUnderlying(PcPackage::Flag::Type::CompilerFlag),
+ };
+ Q_ENUM(FlagType);
+
+ enum class ComparisonType {
+ LessThan,
+ GreaterThan,
+ LessThanEqual,
+ GreaterThanEqual,
+ Equal,
+ NotEqual,
+ AlwaysMatch
+ };
+ Q_ENUM(ComparisonType);
+
+ static QScriptValue ctor(QScriptContext *context, QScriptEngine *engine);
+
+ explicit PkgConfigJs(
+ QScriptContext *context, QScriptEngine *engine, const QVariantMap &options = {});
+
+ Q_INVOKABLE QVariantMap packages() const { return m_packages; }
+ Q_INVOKABLE QVariantList brokenPackages() const { return m_brokenPackages; }
+
+ // also used in tests
+ static PkgConfig::Options convertOptions(const QProcessEnvironment &env, const QVariantMap &map);
+
+private:
+ std::unique_ptr<PkgConfig> m_pkgConfig;
+ QVariantMap m_packages;
+ QVariantList m_brokenPackages;
+};
+
+} // namespace Internal
+} // namespace qbs
diff --git a/src/lib/corelib/tools/stlutils.h b/src/lib/corelib/tools/stlutils.h
index 2a069cbe1..5aff5cc54 100644
--- a/src/lib/corelib/tools/stlutils.h
+++ b/src/lib/corelib/tools/stlutils.h
@@ -219,6 +219,12 @@ C rangeTo(R &&r)
return C(std::begin(r), std::end(r));
}
+template<class Enum>
+constexpr std::underlying_type_t<Enum> toUnderlying(Enum e) noexcept
+{
+ return static_cast<std::underlying_type_t<Enum>>(e);
+}
+
} // namespace Internal
} // namespace qbs
diff --git a/src/lib/libs.qbs b/src/lib/libs.qbs
index 264036e1a..10890bb46 100644
--- a/src/lib/libs.qbs
+++ b/src/lib/libs.qbs
@@ -2,6 +2,7 @@ Project {
references: [
"corelib/corelib.qbs",
"msbuild/msbuild.qbs",
+ "pkgconfig/pkgconfig.qbs",
"scriptengine/scriptengine.qbs",
]
}
diff --git a/src/lib/pkgconfig/CMakeLists.txt b/src/lib/pkgconfig/CMakeLists.txt
new file mode 100644
index 000000000..a39ee5564
--- /dev/null
+++ b/src/lib/pkgconfig/CMakeLists.txt
@@ -0,0 +1,27 @@
+set(SOURCES
+ pcpackage.cpp
+ pcpackage.h
+ pcparser.cpp
+ pcparser.h
+ pkgconfig.cpp
+ pkgconfig.h
+)
+list_transform_prepend(SOLUTION_SOURCES solution/)
+
+if(APPLE)
+ set(HAS_STD_FILESYSTEM "0")
+else()
+ set(HAS_STD_FILESYSTEM "1")
+endif()
+
+add_qbs_library(qbspkgconfig
+ STATIC
+ DEFINES
+ "PKG_CONFIG_PC_PATH=\"${CMAKE_INSTALL_PREFIX}/${QBS_LIBDIR_NAME}/pkgconfig:${CMAKE_INSTALL_PREFIX}/share/pkgconfig:/usr/${QBS_LIBDIR_NAME}/pkgconfig/:/usr/share/pkgconfig/\""
+ "PKG_CONFIG_SYSTEM_LIBRARY_PATH=\"/usr/${QBS_LIBDIR_NAME}\""
+ "HAS_STD_FILESYSTEM=${HAS_STD_FILESYSTEM}"
+ PUBLIC_DEFINES
+ "QBS_PC_WITH_QT_SUPPORT=1"
+ PUBLIC_DEPENDS Qt${QT_VERSION_MAJOR}::Core
+ SOURCES ${SOURCES}
+)
diff --git a/src/lib/pkgconfig/pcpackage.cpp b/src/lib/pkgconfig/pcpackage.cpp
new file mode 100644
index 000000000..cba783708
--- /dev/null
+++ b/src/lib/pkgconfig/pcpackage.cpp
@@ -0,0 +1,172 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 Ivan Komissarov (abbapoh@gmail.com)
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qbs.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "pcpackage.h"
+
+#include <algorithm>
+
+namespace qbs {
+
+using ComparisonType = PcPackage::RequiredVersion::ComparisonType;
+
+std::string_view PcPackage::Flag::typeToString(Type t)
+{
+ switch (t) {
+ case Type::LibraryName: return "LibraryName";
+ case Type::StaticLibraryName: return "StaticLibraryName";
+ case Type::LibraryPath: return "LibraryPath";
+ case Type::Framework: return "Framework";
+ case Type::FrameworkPath: return "FrameworkPath";
+ case Type::LinkerFlag: return "LinkerFlag";
+ case Type::IncludePath: return "IncludePath";
+ case Type::SystemIncludePath: return "SystemIncludePath";
+ case Type::DirAfterIncludePath: return "DirAfterIncludePath";
+ case Type::Define: return "Define";
+ case Type::CompilerFlag: return "CompilerFlag";
+ }
+ return {};
+}
+
+std::optional<PcPackage::Flag::Type> PcPackage::Flag::typeFromString(std::string_view s)
+{
+ if (s == "LibraryName")
+ return Type::LibraryName;
+ else if (s == "StaticLibraryName")
+ return Type::StaticLibraryName;
+ else if (s == "LibraryPath")
+ return Type::LibraryPath;
+ else if (s == "Framework")
+ return Type::Framework;
+ else if (s == "FrameworkPath")
+ return Type::FrameworkPath;
+ else if (s == "LinkerFlag")
+ return Type::LinkerFlag;
+ else if (s == "IncludePath")
+ return Type::IncludePath;
+ else if (s == "SystemIncludePath")
+ return Type::SystemIncludePath;
+ else if (s == "DirAfterIncludePath")
+ return Type::DirAfterIncludePath;
+ else if (s == "Define")
+ return Type::Define;
+ else if (s == "CompilerFlag")
+ return Type::CompilerFlag;
+ return std::nullopt;
+}
+
+std::string_view PcPackage::RequiredVersion::comparisonToString(ComparisonType t)
+{
+ switch (t) {
+ case ComparisonType::LessThan: return "LessThan";
+ case ComparisonType::GreaterThan: return "GreaterThan";
+ case ComparisonType::LessThanEqual: return "LessThanEqual";
+ case ComparisonType::GreaterThanEqual: return "GreaterThanEqual";
+ case ComparisonType::Equal: return "Equal";
+ case ComparisonType::NotEqual: return "NotEqual";
+ case ComparisonType::AlwaysMatch: return "AlwaysMatch";
+ }
+ return {};
+}
+
+std::optional<ComparisonType> PcPackage::RequiredVersion::comparisonFromString(std::string_view s)
+{
+ if (s == "LessThan")
+ return ComparisonType::LessThan;
+ else if (s == "GreaterThan")
+ return ComparisonType::GreaterThan;
+ else if (s == "LessThanEqual")
+ return ComparisonType::LessThanEqual;
+ else if (s == "GreaterThanEqual")
+ return ComparisonType::GreaterThanEqual;
+ else if (s == "Equal")
+ return ComparisonType::Equal;
+ else if (s == "NotEqual")
+ return ComparisonType::NotEqual;
+ else if (s == "AlwaysMatch")
+ return ComparisonType::AlwaysMatch;
+ return std::nullopt;
+}
+
+PcPackage PcPackage::prependSysroot(std::string_view sysroot) &&
+{
+ PcPackage package(std::move(*this));
+
+ const auto doAppend = [](std::vector<Flag> flags, std::string_view sysroot)
+ {
+ if (sysroot.empty())
+ return flags;
+ for (auto &flag : flags) {
+ if (flag.type == Flag::Type::IncludePath
+ || flag.type == Flag::Type::SystemIncludePath
+ || flag.type == Flag::Type::DirAfterIncludePath
+ || flag.type == Flag::Type::LibraryPath) {
+ flag.value = std::string(sysroot) + std::move(flag.value);
+ }
+ }
+ return flags;
+ };
+
+ package.libs = doAppend(std::move(package.libs), sysroot);
+ package.libsPrivate = doAppend(std::move(package.libsPrivate), sysroot);
+ package.cflags = doAppend(std::move(package.cflags), sysroot);
+ return package;
+}
+
+PcPackage PcPackage::removeSystemLibraryPaths(
+ const std::unordered_set<std::string> &libraryPaths) &&
+{
+ PcPackage package(std::move(*this));
+ if (libraryPaths.empty())
+ return package;
+
+ const auto doRemove = [&libraryPaths](std::vector<Flag> flags)
+ {
+ const auto predicate = [&libraryPaths](const Flag &flag)
+ {
+ return flag.type == Flag::Type::LibraryPath && libraryPaths.count(flag.value);
+ };
+ flags.erase(std::remove_if(flags.begin(), flags.end(), predicate), flags.end());
+ return flags;
+ };
+ package.libs = doRemove(package.libs);
+ package.libsPrivate = doRemove(package.libsPrivate);
+ return package;
+}
+
+} // namespace qbs
diff --git a/src/lib/pkgconfig/pcpackage.h b/src/lib/pkgconfig/pcpackage.h
new file mode 100644
index 000000000..df6905185
--- /dev/null
+++ b/src/lib/pkgconfig/pcpackage.h
@@ -0,0 +1,137 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 Ivan Komissarov (abbapoh@gmail.com)
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qbs.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef PC_PACKAGE_H
+#define PC_PACKAGE_H
+
+#include <map>
+#include <optional>
+#include <stdexcept>
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+namespace qbs {
+
+class PcPackage
+{
+public:
+ struct Flag
+ {
+ enum class Type {
+ LibraryName = (1 << 0),
+ StaticLibraryName = (1 << 1),
+ LibraryPath = (1 << 2),
+ Framework = (1 << 3),
+ FrameworkPath = (1 << 4),
+ LinkerFlag = (1 << 5), // this is a lie, this is DriverLinkerFlags
+ IncludePath = (1 << 6),
+ SystemIncludePath = (1 << 7),
+ DirAfterIncludePath = (1 << 8),
+ Define = (1 << 9),
+ CompilerFlag = (1 << 10),
+ };
+ Type type{Type::CompilerFlag};
+ std::string value;
+
+ static std::string_view typeToString(Type t);
+ static std::optional<Type> typeFromString(std::string_view s);
+ };
+
+ struct RequiredVersion
+ {
+ enum class ComparisonType {
+ LessThan,
+ GreaterThan,
+ LessThanEqual,
+ GreaterThanEqual,
+ Equal,
+ NotEqual,
+ AlwaysMatch
+ };
+
+ std::string name;
+ ComparisonType comparison{ComparisonType::GreaterThanEqual};
+ std::string version;
+
+ static std::string_view comparisonToString(ComparisonType t);
+ static std::optional<ComparisonType> comparisonFromString(std::string_view s);
+ };
+
+ std::string filePath;
+ std::string baseFileName;
+ std::string name;
+ std::string version;
+ std::string description;
+ std::string url;
+
+ std::vector<Flag> libs;
+ std::vector<Flag> libsPrivate;
+ std::vector<Flag> cflags;
+
+ std::vector<RequiredVersion> requiresPublic;
+ std::vector<RequiredVersion> requiresPrivate;
+ std::vector<RequiredVersion> conflicts;
+
+ using VariablesMap = std::map<std::string, std::string, std::less<>>;
+ VariablesMap vars;
+
+ bool uninstalled{false};
+
+ PcPackage prependSysroot(std::string_view sysroot) &&;
+ PcPackage removeSystemLibraryPaths(const std::unordered_set<std::string> &libraryPaths) &&;
+};
+
+class PcBrokenPackage
+{
+public:
+ std::string filePath;
+ std::string errorText;
+};
+
+class PcException: public std::runtime_error
+{
+public:
+ explicit PcException(const std::string &message) : std::runtime_error(message) {}
+};
+
+} // namespace qbs
+
+#endif // PC_PACKAGE_H
diff --git a/src/lib/pkgconfig/pcparser.cpp b/src/lib/pkgconfig/pcparser.cpp
new file mode 100644
index 000000000..7ce77618a
--- /dev/null
+++ b/src/lib/pkgconfig/pcparser.cpp
@@ -0,0 +1,762 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 Ivan Komissarov (abbapoh@gmail.com)
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qbs.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "pcparser.h"
+
+#include "pkgconfig.h"
+
+#if HAS_STD_FILESYSTEM
+#include <filesystem>
+#else
+#include <QFileInfo>
+#endif
+
+#include <algorithm>
+#include <fstream>
+#include <stdexcept>
+
+namespace qbs {
+
+namespace {
+
+bool readOneLine(std::ifstream &file, std::string &line)
+{
+ bool quoted = false;
+ bool comment = false;
+ int n_read = 0;
+
+ line = {};
+
+ while (true) {
+ char c;
+ file.get(c);
+ const bool ok = file.good();
+
+ if (!ok) {
+ if (quoted)
+ line += '\\';
+
+ return n_read > 0;
+ } else {
+ n_read++;
+ }
+
+ if (c == '\r') {
+ n_read--;
+ continue;
+ }
+
+ if (quoted) {
+ quoted = false;
+
+ switch (c) {
+ case '#':
+ line += '#';
+ break;
+ case '\n':
+ break;
+ default:
+ line += '\\';
+ line += c;
+ break;
+ }
+ } else {
+ switch (c) {
+ case '#':
+ comment = true;
+ break;
+ case '\\':
+ if (!comment)
+ quoted = true;
+ break;
+ case '\n':
+ return n_read > 0;
+ default:
+ if (!comment)
+ line += c;
+ break;
+ }
+ }
+ }
+
+ return n_read > 0;
+}
+
+std::string_view trimmed(std::string_view str)
+{
+ const auto predicate = [](int c){ return std::isspace(c); };
+ const auto left = std::find_if_not(str.begin(), str.end(), predicate);
+ const auto right = std::find_if_not(str.rbegin(), str.rend(), predicate).base();
+ if (right <= left)
+ return {};
+ return std::string_view(&*left, std::distance(left, right));
+}
+
+// based on https://opensource.apple.com/source/distcc/distcc-31.0.81/popt/poptparse.c.auto.html
+std::optional<std::vector<std::string>> splitCommand(std::string_view s)
+{
+ std::vector<std::string> result;
+ std::string arg;
+
+ char quote = '\0';
+
+ for (auto it = s.begin(), end = s.end(); it != end; ++it) {
+ if (quote == *it) {
+ quote = '\0';
+ } else if (quote != '\0') {
+ if (*it == '\\') {
+ ++it;
+ if (it == s.end())
+ return std::nullopt;
+
+ if (*it != quote)
+ arg += '\\';
+ }
+ arg += *it;
+ } else if (isspace(*it)) {
+ if (!arg.empty()) {
+ result.push_back(arg);
+ arg.clear();
+ }
+ } else {
+ switch (*it) {
+ case '"':
+ case '\'':
+ quote = *it;
+ break;
+ case '\\':
+ ++it;
+ if (it == s.end())
+ return std::nullopt;
+ [[fallthrough]];
+ default:
+ arg += *it;
+ break;
+ }
+ }
+ }
+
+ if (!arg.empty())
+ result.push_back(arg);
+
+ return result;
+}
+
+bool startsWith(std::string_view haystack, std::string_view needle)
+{
+ return haystack.size() >= needle.size() && haystack.compare(0, needle.size(), needle) == 0;
+}
+
+bool endsWith(std::string_view haystack, std::string_view needle)
+{
+ return haystack.size() >= needle.size()
+ && haystack.compare(haystack.size() - needle.size(), needle.size(), needle) == 0;
+}
+
+[[noreturn]] void raizeUnknownComparisonException(const PcPackage &pkg, std::string_view verName, std::string_view comp)
+{
+ std::string message;
+ message += "Unknown version comparison operator '";
+ message += comp;
+ message += "' after package name '";
+ message += verName;
+ message += "' in file '";
+ message += pkg.filePath;
+ message += "'";
+ throw PcException(message);
+}
+
+[[noreturn]] void raiseDuplicateFieldException(std::string_view fieldName, std::string_view path)
+{
+ std::string message;
+ message += fieldName;
+ message += " field occurs twice in '";
+ message += path;
+ message += "'";
+ throw PcException(message);
+}
+
+[[noreturn]] void raizeEmptyPackageNameException(const PcPackage &pkg)
+{
+ std::string message;
+ message += "Empty package name in Requires or Conflicts in file '";
+ message += pkg.filePath;
+ message += "'";
+ throw PcException(message);
+}
+
+[[noreturn]] void raizeNoVersionException(const PcPackage &pkg, std::string_view verName)
+{
+ std::string message;
+ message += "Comparison operator but no version after package name '";
+ message += verName;
+ message += "' in file '";
+ message += pkg.filePath;
+ message += "'";
+ throw PcException(message);
+}
+
+[[noreturn]] void raizeDuplicateVariableException(const PcPackage &pkg, std::string_view variable)
+{
+ std::string message;
+ message += "Duplicate definition of variable '";
+ message += variable;
+ message += "' in '";
+ message += pkg.filePath;
+ throw PcException(message);
+}
+
+[[noreturn]] void raizeUndefinedVariableException(const PcPackage &pkg, std::string_view variable)
+{
+ std::string message;
+ message += "Variable '";
+ message += variable;
+ message += "' not defined in '";
+ message += pkg.filePath;
+ throw PcException(message);
+}
+
+bool isModuleSeparator(char c) { return c == ',' || std::isspace(c); }
+bool isModuleOperator(char c) { return c == '<' || c == '>' || c == '!' || c == '='; }
+
+// A module list is a list of modules with optional version specification,
+// separated by commas and/or spaces. Commas are treated just like whitespace,
+// in order to allow stuff like: Requires: @FRIBIDI_PC@, glib, gmodule
+// where @FRIBIDI_PC@ gets substituted to nothing or to 'fribidi'
+
+std::vector<std::string> splitModuleList(std::string_view str)
+{
+ enum class State {
+ // put numbers to help interpret lame debug spew ;-)
+ OutsideModule = 0,
+ InModuleName = 1,
+ BeforeOperator = 2,
+ InOperator = 3,
+ AfterOperator = 4,
+ InModuleVersion = 5
+ };
+
+ std::vector<std::string> result;
+ State state = State::OutsideModule;
+ State last_state = State::OutsideModule;
+
+ auto start = str.begin();
+ const auto end = str.end();
+ auto p = start;
+
+ while (p != end) {
+
+ switch (state) {
+ case State::OutsideModule:
+ if (!isModuleSeparator(*p))
+ state = State::InModuleName;
+ break;
+
+ case State::InModuleName:
+ if (std::isspace(*p)) {
+ // Need to look ahead to determine next state
+ auto s = p;
+ while (s != end && std::isspace (*s))
+ ++s;
+
+ state = State::OutsideModule;
+ if (s != end && isModuleOperator(*s))
+ state = State::BeforeOperator;
+ }
+ else if (isModuleSeparator(*p))
+ state = State::OutsideModule; // comma precludes any operators
+ break;
+
+ case State::BeforeOperator:
+ // We know an operator is coming up here due to lookahead from
+ // IN_MODULE_NAME
+ if (std::isspace(*p))
+ ; // no change
+ else if (isModuleOperator(*p))
+ state = State::InOperator;
+ break;
+
+ case State::InOperator:
+ if (!isModuleOperator(*p))
+ state = State::AfterOperator;
+ break;
+
+ case State::AfterOperator:
+ if (!std::isspace(*p))
+ state = State::InModuleVersion;
+ break;
+
+ case State::InModuleVersion:
+ if (isModuleSeparator(*p))
+ state = State::OutsideModule;
+ break;
+
+ default:
+ break;
+ }
+
+ if (state == State::OutsideModule && last_state != State::OutsideModule) {
+ // We left a module
+ while (start != end && isModuleSeparator(*start))
+ ++start;
+
+ std::string module(&*start, p - start);
+ result.push_back(module);
+
+ // reset start
+ start = p;
+ }
+
+ last_state = state;
+ ++p;
+ }
+
+ if (p != start) {
+ // get the last module
+ while (start != end && isModuleSeparator(*start))
+ ++start;
+ std::string module(&*start, p - start);
+ result.push_back(module);
+ }
+
+ return result;
+}
+
+PcPackage::RequiredVersion::ComparisonType comparisonFromString(
+ const PcPackage &pkg, std::string_view verName, std::string_view comp)
+{
+ using ComparisonType = PcPackage::RequiredVersion::ComparisonType;
+ if (comp.empty())
+ return ComparisonType::AlwaysMatch;
+ if (comp == "=")
+ return ComparisonType::Equal;
+ if (comp == ">=")
+ return ComparisonType::GreaterThanEqual;
+ if (comp == "<=")
+ return ComparisonType::LessThanEqual;
+ if (comp == ">")
+ return ComparisonType::GreaterThan;
+ if (comp == "<")
+ return ComparisonType::LessThan;
+ if (comp == "!=")
+ return ComparisonType::NotEqual;
+
+ raizeUnknownComparisonException(pkg, verName, comp);
+}
+
+std::string baseName(const std::string_view &filePath)
+{
+ auto pos = filePath.rfind('/');
+ const auto fileName =
+ pos == std::string_view::npos ? std::string_view() : filePath.substr(pos + 1);
+ pos = fileName.find('.');
+ return std::string(pos == std::string_view::npos
+ ? std::string_view()
+ : fileName.substr(0, pos));
+}
+
+} // namespace
+
+PcParser::PcParser(const PkgConfig &pkgConfig)
+ : m_pkgConfig(pkgConfig)
+{
+
+}
+
+PcPackage PcParser::parsePackageFile(const std::string &path)
+{
+ PcPackage package;
+
+ if (path.empty())
+ return package;
+
+ std::ifstream file(path);
+
+ if (!file.is_open())
+ throw PcException(std::string("Can't open file ") + path);
+
+ package.baseFileName = baseName(path);
+#if HAS_STD_FILESYSTEM
+ const auto fsPath = std::filesystem::path(path);
+ package.filePath = fsPath.generic_string();
+ package.vars["pcfiledir"] = fsPath.parent_path().generic_string();
+#else
+ QFileInfo fileInfo(QString::fromStdString(path));
+ package.filePath = fileInfo.absoluteFilePath().toStdString();
+ package.vars["pcfiledir"] = fileInfo.absolutePath().toStdString();
+#endif
+
+ std::string line;
+ while (readOneLine(file, line))
+ parseLine(package, line);
+ return package;
+}
+
+std::string PcParser::trimAndSubstitute(const PcPackage &pkg, std::string_view str) const
+{
+ str = trimmed(str);
+
+ std::string result;
+
+ while (!str.empty()) {
+ if (startsWith(str, "$$")) {
+ // escaped $
+ result += '$';
+ str.remove_prefix(2); // cut "$$"
+ } else if (startsWith(str, "${")) {
+ // variable
+ str.remove_prefix(2); // cut "${"
+ const auto it = std::find(str.begin(), str.end(), '}');
+ // funny, original pkg-config simply reads all available memory here
+ if (it == str.end())
+ throw PcException("Missing closing '}'");
+
+ const std::string_view varname = str.substr(0, std::distance(str.begin(), it));
+
+ // past brace
+ str.remove_prefix(varname.size());
+ str.remove_prefix(1);
+
+ const auto varval = m_pkgConfig.packageGetVariable(pkg, varname);
+
+ if (varval.empty())
+ raizeUndefinedVariableException(pkg, varname);
+
+ result += varval;
+ } else {
+ result += str.front();
+ str.remove_prefix(1);
+ }
+ }
+
+ return result;
+}
+
+void PcParser::parseStringField(
+ PcPackage &pkg,
+ std::string &field,
+ std::string_view fieldName,
+ std::string_view str)
+{
+ if (!field.empty())
+ raiseDuplicateFieldException(fieldName, pkg.filePath);
+
+ field = trimAndSubstitute(pkg, str);
+}
+
+void PcParser::parseLibs(
+ PcPackage &pkg,
+ std::vector<PcPackage::Flag> &libs,
+ std::string_view fieldName,
+ std::string_view str)
+{
+ // Strip out -l and -L flags, put them in a separate list.
+
+ if (!libs.empty())
+ raiseDuplicateFieldException(fieldName, pkg.filePath);
+
+ const auto trimmed = trimAndSubstitute(pkg, str);
+
+ const auto argv = splitCommand(trimmed);
+ if (!trimmed.empty() && !argv)
+ throw PcException("Couldn't parse Libs field into an argument vector");
+
+ libs = doParseLibs(*argv);
+}
+
+std::vector<PcPackage::Flag> PcParser::doParseLibs(const std::vector<std::string> &argv)
+{
+ std::vector<PcPackage::Flag> libs;
+ libs.reserve(argv.size());
+
+ for (auto it = argv.begin(), end = argv.end(); it != end; ++it) {
+ PcPackage::Flag flag;
+ const auto escapedArgument = trimmed(*it);
+ std::string_view arg(escapedArgument);
+
+ // -lib: is used by the C# compiler for libs; it's not an -l flag.
+ if (startsWith(arg, "-l") && !startsWith(arg, "-lib:")) {
+ arg.remove_prefix(2);
+ arg = trimmed(arg);
+
+ flag.type = PcPackage::Flag::Type::LibraryName;
+ flag.value += arg;
+ } else if (startsWith(arg, "-L")) {
+ arg.remove_prefix(2);
+ arg = trimmed(arg);
+
+ flag.type = PcPackage::Flag::Type::LibraryPath;
+ flag.value += arg;
+ } else if ((arg == "-framework" /*|| arg == "-Wl,-framework"*/) && it + 1 != end) {
+ // macOS has a -framework Foo which is really one option,
+ // so we join those to avoid having -framework Foo
+ // -framework Bar being changed into -framework Foo Bar
+ // later
+ const auto framework = trimmed(*(it + 1));
+ flag.type = PcPackage::Flag::Type::Framework;
+ flag.value += framework;
+ ++it;
+ } else if (startsWith(arg, "-F")) {
+ arg.remove_prefix(2);
+ arg = trimmed(arg);
+
+ flag.type = PcPackage::Flag::Type::FrameworkPath;
+ flag.value += arg;
+
+ } else if (!startsWith(arg, "-") && (endsWith(arg, ".a") || endsWith(arg, ".lib"))) {
+ flag.type = PcPackage::Flag::Type::StaticLibraryName;
+ flag.value += arg;
+ } else if (!arg.empty()) {
+ flag.type = PcPackage::Flag::Type::LinkerFlag;
+ flag.value += arg;
+ } else {
+ continue;
+ }
+ libs.push_back(flag);
+ }
+ return libs;
+}
+
+void PcParser::parseCFlags(PcPackage &pkg, std::string_view str)
+{
+ // Strip out -I, -D, -isystem and idirafter flags, put them in a separate lists.
+
+ if (!pkg.cflags.empty())
+ raiseDuplicateFieldException("Cflags", pkg.filePath);
+
+ const auto command = trimAndSubstitute(pkg, str);
+
+ const auto argv = splitCommand(command);
+ if (!command.empty() && !argv)
+ throw PcException("Couldn't parse Cflags field into an argument vector");
+
+ std::vector<PcPackage::Flag> cflags;
+ cflags.reserve(argv->size());
+
+ for (auto it = argv->begin(), end = argv->end(); it != end; ++it) {
+ PcPackage::Flag flag;
+ const auto escapedArgument = trimmed(*it);
+ std::string_view arg(escapedArgument);
+
+ if (startsWith(arg, "-I")) {
+ arg.remove_prefix(2);
+ arg = trimmed(arg);
+
+ flag.type = PcPackage::Flag::Type::IncludePath;
+ flag.value += arg;
+ } else if (startsWith(arg, "-D")) {
+ arg.remove_prefix(2);
+ arg = trimmed(arg);
+
+ flag.type = PcPackage::Flag::Type::Define;
+ flag.value += arg;
+ } else if (arg == "-isystem" && it + 1 != end) {
+ flag.type = PcPackage::Flag::Type::SystemIncludePath;
+ flag.value = trimmed(*(it + 1));
+ ++it;
+ } else if (arg == "-idirafter" && it + 1 != end) {
+ flag.type = PcPackage::Flag::Type::DirAfterIncludePath;
+ flag.value = trimmed(*(it + 1));
+ ++it;
+ } else if (!arg.empty()) {
+ flag.type = PcPackage::Flag::Type::CompilerFlag;
+ flag.value += arg;
+ } else {
+ continue;
+ }
+ cflags.push_back(flag);
+ }
+ pkg.cflags = std::move(cflags);
+}
+
+std::vector<PcPackage::RequiredVersion> PcParser::parseModuleList(PcPackage &pkg, std::string_view str)
+{
+ using ComparisonType = PcPackage::RequiredVersion::ComparisonType;
+
+ std::vector<PcPackage::RequiredVersion> result;
+ auto split = splitModuleList(str);
+
+ for (auto &module: split) {
+ PcPackage::RequiredVersion ver;
+
+ auto p = module.begin();
+ const auto end = module.end();
+
+ ver.comparison = ComparisonType::AlwaysMatch;
+
+ auto start = p;
+
+ while (*p && !std::isspace(*p))
+ ++p;
+
+ const auto name = std::string_view(&*start, std::distance(start, p));
+
+ if (name.empty())
+ raizeEmptyPackageNameException(pkg);
+
+ ver.name = std::string(name);
+
+ while (p != end && std::isspace(*p))
+ ++p;
+
+ start = p;
+
+ while (p != end && !std::isspace(*p))
+ ++p;
+
+ const auto comp = std::string_view(&*start, std::distance(start, p));
+ ver.comparison = comparisonFromString(pkg, ver.name, comp);
+
+ while (p != end && std::isspace(*p))
+ ++p;
+
+ start = p;
+
+ while (p != end && !std::isspace(*p))
+ ++p;
+
+ const auto version = std::string_view(&*start, std::distance(start, p));
+
+ while (p != end && std::isspace(*p))
+ ++p;
+
+ if (ver.comparison != ComparisonType::AlwaysMatch && version.empty())
+ raizeNoVersionException(pkg, ver.name);
+
+ ver.version = std::string(version);
+
+ result.push_back(ver);
+ }
+
+ return result;
+}
+
+void PcParser::parseVersionsField(
+ PcPackage &pkg,
+ std::vector<PcPackage::RequiredVersion> &modules,
+ std::string_view fieldName,
+ std::string_view str)
+{
+ if (!modules.empty())
+ raiseDuplicateFieldException(fieldName, pkg.filePath);
+
+ const auto trimmed = trimAndSubstitute(pkg, str);
+ modules = parseModuleList(pkg, trimmed.c_str());
+}
+
+void PcParser::parseLine(PcPackage &pkg, std::string_view str)
+{
+ str = trimmed(str);
+ if (str.empty())
+ return;
+
+ auto getFirstWord = [](std::string_view s) {
+ size_t pos = 0;
+ for (; pos < s.size(); ++pos) {
+ auto p = s.data() + pos;
+ if (!((*p >= 'A' && *p <= 'Z') ||
+ (*p >= 'a' && *p <= 'z') ||
+ (*p >= '0' && *p <= '9') ||
+ *p == '_' || *p == '.')) {
+ break;
+ }
+ }
+ return s.substr(0, pos);
+ };
+
+ const auto tag = getFirstWord(str);
+
+ str.remove_prefix(tag.size()); // cut tag
+ str = trimmed(str);
+
+ if (str.empty())
+ return;
+
+ if (str.front() == ':') {
+ // keyword
+ str.remove_prefix(1); // cut ':'
+ str = trimmed(str);
+
+ if (tag == "Name")
+ parseStringField(pkg, pkg.name, tag, str);
+ else if (tag == "Description")
+ parseStringField(pkg, pkg.description, tag, str);
+ else if (tag == "Version")
+ parseStringField(pkg, pkg.version, tag, str);
+ else if (tag == "Requires.private")
+ parseVersionsField(pkg, pkg.requiresPrivate, tag, str);
+ else if (tag == "Requires")
+ parseVersionsField(pkg, pkg.requiresPublic, tag, str);
+ else if (tag == "Libs.private")
+ parseLibs(pkg, pkg.libsPrivate, "Libs.private", str);
+ else if (tag == "Libs")
+ parseLibs(pkg, pkg.libs, "Libs", str);
+ else if (tag == "Cflags" || tag == "CFlags")
+ parseCFlags(pkg, str);
+ else if (tag == "Conflicts")
+ parseVersionsField(pkg, pkg.conflicts, tag, str);
+ else if (tag == "URL")
+ parseStringField(pkg, pkg.url, tag, str);
+ else {
+ // we don't error out on unknown keywords because they may
+ // represent additions to the .pc file format from future
+ // versions of pkg-config.
+ return;
+ }
+ } else if (str.front() == '=') {
+ // variable
+
+ str.remove_prefix(1); // cut '='
+ str = trimmed(str);
+
+ // TODO: support guesstimating of the prefix variable (pkg-config's --define-prefix option)
+ // from doc: "try to override the value of prefix for each .pc file found with a
+ // guesstimated value based on the location of the .pc file"
+ // https://gitlab.freedesktop.org/pkg-config/pkg-config/-/blob/pkg-config-0.29.2/parse.c#L998
+ // This option is disabled by default, and Qbs doesn't allow to override it yet, so we can
+ // ignore this feature for now
+
+ const auto value = trimAndSubstitute(pkg, str);
+ const auto [it, ok] = pkg.vars.insert({std::string(tag), value});
+ if (!ok)
+ raizeDuplicateVariableException(pkg, tag);
+ }
+}
+
+} // namespace qbs
diff --git a/src/lib/pkgconfig/pcparser.h b/src/lib/pkgconfig/pcparser.h
new file mode 100644
index 000000000..8443629a6
--- /dev/null
+++ b/src/lib/pkgconfig/pcparser.h
@@ -0,0 +1,84 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 Ivan Komissarov (abbapoh@gmail.com)
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qbs.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef PC_PARSER_H
+#define PC_PARSER_H
+
+#include "pcpackage.h"
+
+namespace qbs {
+
+class PkgConfig;
+
+class PcParser
+{
+public:
+ explicit PcParser(const PkgConfig &pkgConfig);
+
+ PcPackage parsePackageFile(const std::string &path);
+
+private:
+ std::string trimAndSubstitute(const PcPackage &pkg, std::string_view str) const;
+ void parseStringField(
+ PcPackage &pkg,
+ std::string &field,
+ std::string_view fieldName,
+ std::string_view str);
+ void parseLibs(
+ PcPackage &pkg,
+ std::vector<PcPackage::Flag> &libs,
+ std::string_view fieldName,
+ std::string_view str);
+ std::vector<PcPackage::Flag> doParseLibs(const std::vector<std::string> &argv);
+ void parseCFlags(PcPackage &pkg, std::string_view str);
+ std::vector<PcPackage::RequiredVersion> parseModuleList(PcPackage &pkg, std::string_view str);
+ void parseVersionsField(
+ PcPackage &pkg,
+ std::vector<PcPackage::RequiredVersion> &modules,
+ std::string_view fieldName,
+ std::string_view str);
+ void parseLine(PcPackage &pkg, std::string_view str);
+
+private:
+ const PkgConfig &m_pkgConfig;
+};
+
+} // namespace qbs
+
+#endif // PC_PARSER_H
diff --git a/src/lib/pkgconfig/pkgconfig.cpp b/src/lib/pkgconfig/pkgconfig.cpp
new file mode 100644
index 000000000..871dff99c
--- /dev/null
+++ b/src/lib/pkgconfig/pkgconfig.cpp
@@ -0,0 +1,265 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 Ivan Komissarov (abbapoh@gmail.com)
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qbs.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "pkgconfig.h"
+#include "pcparser.h"
+
+#if HAS_STD_FILESYSTEM
+# include <filesystem>
+#else
+# include <QtCore/QDir>
+# include <QtCore/QFileInfo>
+#endif
+
+#include <algorithm>
+#include <iostream>
+
+namespace qbs {
+
+namespace {
+
+std::string varToEnvVar(std::string_view pkg, std::string_view var)
+{
+ auto result = std::string("PKG_CONFIG_");
+ result += pkg;
+ result += '_';
+ result += var;
+
+ for (char &p : result) {
+ int c = std::toupper(p);
+
+ if (!std::isalnum(c))
+ c = '_';
+
+ p = char(c);
+ }
+
+ return result;
+}
+
+std::vector<std::string> split(std::string_view str, const char delim)
+{
+ std::vector<std::string> result;
+ size_t prev = 0;
+ size_t pos = 0;
+ do {
+ pos = str.find(delim, prev);
+ if (pos == std::string::npos) pos = str.length();
+ std::string token(str.substr(prev, pos - prev));
+ if (!token.empty())
+ result.push_back(token);
+ prev = pos + 1;
+ } while (pos < str.length() && prev < str.length());
+ return result;
+}
+
+constexpr inline char listSeparator() noexcept
+{
+#if defined(WIN32)
+ return ';';
+#else
+ return ':';
+#endif
+}
+
+[[noreturn]] void raizeUnknownPackageException(std::string_view package)
+{
+ std::string message;
+ message += "Can't find package '";
+ message += package;
+ message += "'";
+ throw PcException(message);
+}
+
+} // namespace
+
+PkgConfig::PkgConfig()
+ : PkgConfig(Options())
+{
+}
+
+PkgConfig::PkgConfig(Options options)
+ : m_options(std::move(options))
+{
+ if (m_options.searchPaths.empty())
+ m_options.searchPaths = split(PKG_CONFIG_PC_PATH, listSeparator());
+
+ if (m_options.topBuildDir.empty())
+ m_options.topBuildDir = "$(top_builddir)"; // pkg-config sets this for automake =)
+
+ if (m_options.systemLibraryPaths.empty())
+ m_options.systemLibraryPaths = split(PKG_CONFIG_SYSTEM_LIBRARY_PATH, ':');
+
+ // this is weird on Windows, but that's what pkg-config does
+ if (m_options.sysroot.empty())
+ m_options.globalVariables["pc_sysrootdir"] = "/";
+ else
+ m_options.globalVariables["pc_sysrootdir"] = m_options.sysroot;
+ m_options.globalVariables["pc_top_builddir"] = m_options.topBuildDir;
+
+ std::tie(m_packages, m_brokenPackages) = findPackages();
+}
+
+const PcPackage &PkgConfig::getPackage(std::string_view baseFileName) const
+{
+ // heterogeneous comparator so we can search the package using string_view
+ const auto lessThan = [](const PcPackage &package, const std::string_view &name)
+ {
+ return package.baseFileName < name;
+ };
+
+ const auto it = std::lower_bound(m_packages.begin(), m_packages.end(), baseFileName, lessThan);
+ if (it == m_packages.end() || baseFileName != it->baseFileName)
+ raizeUnknownPackageException(baseFileName);
+ return *it;
+}
+
+std::string_view PkgConfig::packageGetVariable(const PcPackage &pkg, std::string_view var) const
+{
+ std::string_view varval;
+
+ if (var.empty())
+ return varval;
+
+ const auto &globals = m_options.globalVariables;
+ if (auto it = globals.find(var); it != globals.end())
+ varval = it->second;
+
+ // Allow overriding specific variables using an environment variable of the
+ // form PKG_CONFIG_$PACKAGENAME_$VARIABLE
+ if (!pkg.baseFileName.empty()) {
+ const std::string envVariable = varToEnvVar(pkg.baseFileName, var);
+ const auto it = m_options.systemVariables.find(envVariable);
+ if (it != m_options.systemVariables.end())
+ return it->second;
+ }
+
+ if (varval.empty()) {
+ const auto it = pkg.vars.find(var);
+ varval = (it != pkg.vars.end()) ? it->second : std::string_view();
+ }
+
+ return varval;
+}
+
+#if HAS_STD_FILESYSTEM
+std::vector<std::string> getPcFilePaths(const std::vector<std::string> &searchPaths)
+{
+ std::vector<std::filesystem::path> paths;
+
+ for (const auto &searchPath : searchPaths) {
+ if (!std::filesystem::directory_entry(searchPath).exists())
+ continue;
+ const auto dir = std::filesystem::directory_iterator(searchPath);
+ std::copy_if(
+ std::filesystem::begin(dir),
+ std::filesystem::end(dir),
+ std::back_inserter(paths),
+ [](const auto &entry) { return entry.path().extension() == ".pc"; }
+ );
+ }
+ std::vector<std::string> result;
+ std::transform(
+ std::begin(paths),
+ std::end(paths),
+ std::back_inserter(result),
+ [](const auto &path) { return path.generic_string(); }
+ );
+ return result;
+}
+#else
+std::vector<std::string> getPcFilePaths(const std::vector<std::string> &searchPaths)
+{
+ std::vector<std::string> result;
+ for (const auto &path : searchPaths) {
+ QDir dir(QString::fromStdString(path));
+ const auto paths = dir.entryList({QStringLiteral("*.pc")});
+ std::transform(
+ std::begin(paths),
+ std::end(paths),
+ std::back_inserter(result),
+ [&dir](const auto &path) { return dir.filePath(path).toStdString(); }
+ );
+ }
+ return result;
+}
+#endif
+
+std::pair<PkgConfig::Packages, PkgConfig::BrokenPackages> PkgConfig::findPackages() const
+{
+ Packages result;
+ BrokenPackages brokenResult;
+ PcParser parser(*this);
+
+ const auto systemLibraryPaths = !m_options.allowSystemLibraryPaths ?
+ std::unordered_set<std::string>(
+ m_options.systemLibraryPaths.begin(),
+ m_options.systemLibraryPaths.end()) : std::unordered_set<std::string>();
+
+ const auto pcFilePaths = getPcFilePaths(m_options.searchPaths);
+
+ for (const auto &pcFilePath : pcFilePaths) {
+ if (m_options.disableUninstalled) {
+ if (pcFilePath.find("-uninstalled.pc") != std::string::npos)
+ continue;
+ }
+
+ try {
+ result.emplace_back(
+ parser.parsePackageFile(pcFilePath)
+ // Weird, but pkg-config removes libs first and only then appends
+ // sysroot. Looks like sysroot has to be used with
+ // allowSystemLibraryPaths: true
+ .removeSystemLibraryPaths(systemLibraryPaths)
+ .prependSysroot(m_options.sysroot));
+ } catch (const PcException &ex) {
+ // not sure if it's OK to use exceptions for handling errors like
+ brokenResult.push_back(PcBrokenPackage{pcFilePath, ex.what()});
+ }
+ }
+
+ const auto lessThanPackage = [](const PcPackage &lhs, const PcPackage &rhs)
+ {
+ return lhs.baseFileName < rhs.baseFileName;
+ };
+ std::sort(result.begin(), result.end(), lessThanPackage);
+ return {result, brokenResult};
+}
+
+} // namespace qbs
diff --git a/src/lib/pkgconfig/pkgconfig.h b/src/lib/pkgconfig/pkgconfig.h
new file mode 100644
index 000000000..17b5ea9fa
--- /dev/null
+++ b/src/lib/pkgconfig/pkgconfig.h
@@ -0,0 +1,88 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 Ivan Komissarov (abbapoh@gmail.com)
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qbs.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef PKGCONFIG_H
+#define PKGCONFIG_H
+
+#include "pcpackage.h"
+
+namespace qbs {
+
+class PkgConfig
+{
+public:
+ struct Options {
+ using VariablesMap = PcPackage::VariablesMap;
+
+ std::vector<std::string> searchPaths; // PKG_CONFIG_PATH, PKG_CONFIG_LIBDIR
+ std::string sysroot; // PKG_CONFIG_SYSROOT_DIR
+ std::string topBuildDir; // PKG_CONFIG_TOP_BUILD_DIR
+ 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
+ VariablesMap globalVariables;
+ VariablesMap systemVariables;
+ };
+
+ using Packages = std::vector<PcPackage>;
+ using BrokenPackages = std::vector<PcBrokenPackage>;
+
+ explicit PkgConfig();
+ explicit PkgConfig(Options options);
+
+ const Options &options() const { return m_options; }
+ const Packages &packages() const { return m_packages; }
+ const BrokenPackages &brokenPackages() const { return m_brokenPackages; }
+ const PcPackage &getPackage(std::string_view baseFileName) const;
+
+ std::string_view packageGetVariable(const PcPackage &pkg, std::string_view var) const;
+
+private:
+ std::pair<Packages, BrokenPackages> findPackages() const;
+
+private:
+ Options m_options;
+
+ Packages m_packages;
+ BrokenPackages m_brokenPackages;
+};
+
+} // namespace qbs
+
+#endif // PKGCONFIG_H
diff --git a/src/lib/pkgconfig/pkgconfig.pro b/src/lib/pkgconfig/pkgconfig.pro
new file mode 100644
index 000000000..7c1560ffd
--- /dev/null
+++ b/src/lib/pkgconfig/pkgconfig.pro
@@ -0,0 +1,24 @@
+TARGET = qbspkgconfig
+include(../staticlibrary.pri)
+
+DEFINES += \
+ PKG_CONFIG_PC_PATH=\\\"/usr/lib/pkgconfig:/usr/share/pkgconfig\\\" \
+ PKG_CONFIG_SYSTEM_LIBRARY_PATH=\\\"/usr/${QBS_LIBDIR_NAME}/\\\" \
+ QBS_PC_WITH_QT_SUPPORT=1
+
+macos {
+ DEFINES += HAS_STD_FILESYSTEM=0
+} else {
+ DEFINES += HAS_STD_FILESYSTEM=1
+}
+
+HEADERS += \
+ pcpackage.h \
+ pcparser.h \
+ pkgconfig.h
+
+SOURCES += \
+ pcpackage.cpp \
+ pcparser.cpp \
+ pkgconfig.cpp \
+
diff --git a/src/lib/pkgconfig/pkgconfig.qbs b/src/lib/pkgconfig/pkgconfig.qbs
new file mode 100644
index 000000000..25bcb3fdf
--- /dev/null
+++ b/src/lib/pkgconfig/pkgconfig.qbs
@@ -0,0 +1,61 @@
+import qbs.FileInfo
+import qbs.Utilities
+
+QbsStaticLibrary {
+ Depends { name: "cpp" }
+ Depends { name: "qbsbuildconfig" }
+
+ property stringList pcPaths: {
+ var result = [];
+ result.push(FileInfo.joinPaths(qbs.installPrefix, qbsbuildconfig.libDirName, "pkgconfig"));
+ result.push(FileInfo.joinPaths(qbs.installPrefix, "share", "pkgconfig"));
+ if (qbs.hostOS.contains("unix")) {
+ result.push("/usr/lib/pkgconfig/")
+ result.push("/usr/share/pkgconfig/")
+ }
+ return result
+ }
+ readonly property stringList pcPathsString: pcPaths.join(qbs.pathListSeparator)
+
+ property bool withQtSupport: true
+
+ readonly property stringList publicDefines: {
+ var result = [];
+ if (withQtSupport)
+ result.push("QBS_PC_WITH_QT_SUPPORT=1")
+ else
+ result.push("QBS_PC_WITH_QT_SUPPORT=0")
+ return result;
+ }
+
+ name: "qbspkgconfig"
+
+ files: [
+ "pcpackage.cpp",
+ "pcpackage.h",
+ "pcparser.cpp",
+ "pcparser.h",
+ "pkgconfig.cpp",
+ "pkgconfig.h",
+ ]
+
+ cpp.defines: {
+ var result = [
+ "PKG_CONFIG_PC_PATH=\"" + pcPathsString + "\"",
+ "PKG_CONFIG_SYSTEM_LIBRARY_PATH=\"/usr/" + qbsbuildconfig.libDirName + "\"",
+ ]
+ if ((qbs.targetOS.contains("darwin")
+ && Utilities.versionCompare(cpp.minimumMacosVersion, "10.15") < 0)
+ || qbs.toolchain.contains("mingw"))
+ result.push("HAS_STD_FILESYSTEM=0")
+ else
+ result.push("HAS_STD_FILESYSTEM=1")
+ result = result.concat(publicDefines);
+ return result
+ }
+
+ Export {
+ Depends { name: "cpp" }
+ cpp.defines: exportingProduct.publicDefines
+ }
+}
diff --git a/src/lib/pkgconfig/use_pkgconfig.pri b/src/lib/pkgconfig/use_pkgconfig.pri
new file mode 100644
index 000000000..baccff360
--- /dev/null
+++ b/src/lib/pkgconfig/use_pkgconfig.pri
@@ -0,0 +1,41 @@
+include(../../library_dirname.pri)
+
+isEmpty(QBSLIBDIR) {
+ QBSLIBDIR = $${OUT_PWD}/../../../$${QBS_LIBRARY_DIRNAME}
+}
+
+QBSPKGCONFIG_LIBNAME=qbspkgconfig
+
+unix {
+ LIBS += -L$${QBSLIBDIR} -l$${QBSPKGCONFIG_LIBNAME}
+}
+
+win32 {
+ CONFIG(debug, debug|release) {
+ QBSPKGCONFIG_LIB = $${QBSPKGCONFIG_LIBNAME}d
+ }
+ CONFIG(release, debug|release) {
+ QBSPKGCONFIG_LIB = $${QBSPKGCONFIG_LIBNAME}
+ }
+ msvc {
+ LIBS += /LIBPATH:$$QBSLIBDIR
+ QBSPKGCONFIG_LIB = $${QBSPKGCONFIG_LIB}.lib
+ LIBS += Shell32.lib
+ } else {
+ LIBS += -L$${QBSLIBDIR}
+ QBSPKGCONFIG_LIB = lib$${QBSPKGCONFIG_LIB}
+ }
+ LIBS += $${QBSPKGCONFIG_LIB}
+}
+
+INCLUDEPATH += \
+ $$PWD
+
+CONFIG += depend_includepath
+
+CONFIG(static, static|shared) {
+ DEFINES += QBS_STATIC_LIB
+}
+
+DEFINES += \
+ QBS_PC_WITH_QT_SUPPORT=1
diff --git a/tests/auto/CMakeLists.txt b/tests/auto/CMakeLists.txt
index 6f6097787..8b1a124aa 100644
--- a/tests/auto/CMakeLists.txt
+++ b/tests/auto/CMakeLists.txt
@@ -5,5 +5,6 @@ add_subdirectory(blackbox)
if(WITH_UNIT_TESTS)
add_subdirectory(buildgraph)
add_subdirectory(language)
+ add_subdirectory(pkgconfig)
add_subdirectory(tools)
endif()
diff --git a/tests/auto/auto.qbs b/tests/auto/auto.qbs
index 2f0aef37c..a4c4beedd 100644
--- a/tests/auto/auto.qbs
+++ b/tests/auto/auto.qbs
@@ -15,6 +15,7 @@ Project {
"buildgraph/buildgraph.qbs",
"cmdlineparser/cmdlineparser.qbs",
"language/language.qbs",
+ "pkgconfig/pkgconfig.qbs",
"tools/tools.qbs",
]
}
diff --git a/tests/auto/pkgconfig/CMakeLists.txt b/tests/auto/pkgconfig/CMakeLists.txt
new file mode 100644
index 000000000..4d60491ba
--- /dev/null
+++ b/tests/auto/pkgconfig/CMakeLists.txt
@@ -0,0 +1,8 @@
+add_qbs_test(pkgconfig
+ SOURCES
+ tst_pkgconfig.cpp
+ tst_pkgconfig.h
+ DEPENDS
+ qbspkgconfig
+ qbsscriptengine
+ )
diff --git a/tests/auto/pkgconfig/pkgconfig.qbs b/tests/auto/pkgconfig/pkgconfig.qbs
new file mode 100644
index 000000000..d42a5233b
--- /dev/null
+++ b/tests/auto/pkgconfig/pkgconfig.qbs
@@ -0,0 +1,19 @@
+import qbs
+import qbs.Utilities
+
+QbsUnittest {
+ Depends { name: "qbspkgconfig" }
+ condition: qbsbuildconfig.enableUnitTests
+ testName: "pkgconfig"
+ files: ["../shared.h", "tst_pkgconfig.h", "tst_pkgconfig.cpp"]
+ cpp.defines: base.concat([
+ "SRCDIR=" + Utilities.cStringQuote(path),
+ ])
+
+ Group {
+ name: "testdata"
+ prefix: "testdata/"
+ files: ["**/*"]
+ fileTags: []
+ }
+}
diff --git a/tests/auto/pkgconfig/testdata/non-l-required.json b/tests/auto/pkgconfig/testdata/non-l-required.json
new file mode 100644
index 000000000..d2dd90f06
--- /dev/null
+++ b/tests/auto/pkgconfig/testdata/non-l-required.json
@@ -0,0 +1,12 @@
+{
+ "Name": "Non-l flags required test package",
+ "Description": "Test package for checking order of non-L Libs & Cflags",
+ "Version": "1.0.0",
+ "Libs": [
+ {"Type": "StaticLibraryName", "Value": "/non-l-required.a"},
+ {"Type": "LinkerFlag", "Value": "-pthread"}
+ ],
+ "Cflags": [
+ {"Type": "IncludePath", "Value": "/non-l-required/include"}
+ ]
+}
diff --git a/tests/auto/pkgconfig/testdata/non-l-required.pc b/tests/auto/pkgconfig/testdata/non-l-required.pc
new file mode 100644
index 000000000..7e398e2e1
--- /dev/null
+++ b/tests/auto/pkgconfig/testdata/non-l-required.pc
@@ -0,0 +1,5 @@
+Name: Non-l flags required test package
+Description: Test package for checking order of non-L Libs & Cflags
+Version: 1.0.0
+Libs: /non-l-required.a -pthread
+Cflags: -I/non-l-required/include
diff --git a/tests/auto/pkgconfig/testdata/requires-test.json b/tests/auto/pkgconfig/testdata/requires-test.json
new file mode 100644
index 000000000..32acf4b91
--- /dev/null
+++ b/tests/auto/pkgconfig/testdata/requires-test.json
@@ -0,0 +1,18 @@
+{
+ "Name": "Requires test package",
+ "Description": "Dummy pkgconfig test package for testing Requires/Requires.private",
+ "Version": "1.0.0",
+ "Libs": [
+ {"Type": "LibraryPath", "Value": "/requires-test/lib"},
+ {"Type": "LibraryName", "Value": "requires-test"}
+ ],
+ "Cflags": [
+ {"Type": "IncludePath", "Value": "/requires-test/include"}
+ ],
+ "Requires": [
+ {"Comparison": "GreaterThanEqual", "Name": "public-dep", "Version": "1"}
+ ],
+ "RequiresPrivate": [
+ {"Comparison": "GreaterThanEqual", "Name": "private-dep", "Version": "1"}
+ ]
+}
diff --git a/tests/auto/pkgconfig/testdata/requires-test.pc b/tests/auto/pkgconfig/testdata/requires-test.pc
new file mode 100644
index 000000000..e483db2db
--- /dev/null
+++ b/tests/auto/pkgconfig/testdata/requires-test.pc
@@ -0,0 +1,8 @@
+Name: Requires test package
+Description: Dummy pkgconfig test package for testing Requires/Requires.private
+Version: 1.0.0
+Requires: public-dep >= 1
+Requires.private: private-dep >= 1
+Libs: -L/requires-test/lib -lrequires-test
+Cflags: -I/requires-test/include
+
diff --git a/tests/auto/pkgconfig/testdata/simple.json b/tests/auto/pkgconfig/testdata/simple.json
new file mode 100644
index 000000000..d58556e74
--- /dev/null
+++ b/tests/auto/pkgconfig/testdata/simple.json
@@ -0,0 +1,20 @@
+{
+ "Name": "Simple test",
+ "Description": "Dummy pkgconfig test package for testing pkgconfig",
+ "Version": "1.0.0",
+ "Vars": {
+ "prefix": "/usr",
+ "exec_prefix": "/usr",
+ "libdir": "/usr/lib",
+ "includedir": "/usr/include"
+ },
+ "Libs": [
+ {"Type": "LibraryName", "Value": "simple"}
+ ],
+ "LibsPrivate": [
+ {"Type": "LibraryName", "Value": "m"}
+ ],
+ "Cflags": [
+ {"Type": "IncludePath", "Value": "/usr/include"}
+ ]
+}
diff --git a/tests/auto/pkgconfig/testdata/simple.pc b/tests/auto/pkgconfig/testdata/simple.pc
new file mode 100644
index 000000000..2daa0350f
--- /dev/null
+++ b/tests/auto/pkgconfig/testdata/simple.pc
@@ -0,0 +1,12 @@
+prefix=/usr
+exec_prefix=${prefix}
+libdir=${exec_prefix}/lib
+includedir=${prefix}/include
+
+Name: Simple test
+Description: Dummy pkgconfig test package for testing pkgconfig
+Version: 1.0.0
+Requires:
+Libs: -lsimple
+Libs.private: -lm
+Cflags: -I${includedir}
diff --git a/tests/auto/pkgconfig/testdata/special-flags.json b/tests/auto/pkgconfig/testdata/special-flags.json
new file mode 100644
index 000000000..1949820a6
--- /dev/null
+++ b/tests/auto/pkgconfig/testdata/special-flags.json
@@ -0,0 +1,30 @@
+{
+ "Name": "Special flags test",
+ "Description": "Dummy pkgconfig test package for testing pkgconfig",
+ "Version": "1.0.0",
+ "Vars": {
+ "prefix": "/usr",
+ "exec_prefix": "/usr",
+ "libdir": "/usr/lib",
+ "includedir": "/usr/include"
+ },
+ "Libs": [
+ {"Type": "LibraryPath", "Value": "/foo"},
+ {"Type": "Framework", "Value": "Foo"},
+ {"Type": "LibraryName", "Value": "simple"},
+ {"Type": "LibraryPath", "Value": "/bar"},
+ {"Type": "Framework", "Value": "Bar"},
+ {"Type": "LinkerFlag", "Value": "-Wl,-framework"},
+ {"Type": "LinkerFlag", "Value": "-Wl,Baz"}
+ ],
+ "Cflags": [
+ {"Type": "IncludePath", "Value": "/foo"},
+ {"Type": "CompilerFlag", "Value": "-g"},
+ {"Type": "SystemIncludePath", "Value": "/system1"},
+ {"Type": "DirAfterIncludePath", "Value": "/after1"},
+ {"Type": "CompilerFlag", "Value": "-ffoo"},
+ {"Type": "IncludePath", "Value": "/bar"},
+ {"Type": "DirAfterIncludePath", "Value": "/after2"},
+ {"Type": "SystemIncludePath", "Value": "/system2"}
+ ]
+}
diff --git a/tests/auto/pkgconfig/testdata/special-flags.pc b/tests/auto/pkgconfig/testdata/special-flags.pc
new file mode 100644
index 000000000..0bdaeb1b0
--- /dev/null
+++ b/tests/auto/pkgconfig/testdata/special-flags.pc
@@ -0,0 +1,11 @@
+prefix=/usr
+exec_prefix=${prefix}
+libdir=${exec_prefix}/lib
+includedir=${prefix}/include
+
+Name: Special flags test
+Description: Dummy pkgconfig test package for testing pkgconfig
+Version: 1.0.0
+Requires:
+Libs: -L/foo -framework Foo -lsimple -L/bar -framework Bar -Wl,-framework -Wl,Baz
+Cflags: -I/foo -g -isystem /system1 -idirafter /after1 -ffoo -I/bar -idirafter /after2 -isystem /system2
diff --git a/tests/auto/pkgconfig/testdata/sysroot.json b/tests/auto/pkgconfig/testdata/sysroot.json
new file mode 100644
index 000000000..7e8b3f6bb
--- /dev/null
+++ b/tests/auto/pkgconfig/testdata/sysroot.json
@@ -0,0 +1,21 @@
+{
+ "Name": "Test for sysroot",
+ "Description": "Test package for testing sysroot",
+ "Version": "1.0.0",
+ "Vars": {
+ "prefix": "/opt",
+ "exec_prefix": "/opt",
+ "libdir": "/opt/lib",
+ "includedir": "/opt/include",
+ "sysroot": "/newroot"
+ },
+ "Libs": [
+ {"Type": "LibraryPath", "Value": "/newroot/opt/lib"},
+ {"Type": "LibraryName", "Value": "system"}
+ ],
+ "Cflags": [
+ {"Type": "IncludePath", "Value": "/newroot/opt/include"},
+ {"Type": "DirAfterIncludePath", "Value": "/newroot/opt/include/after"},
+ {"Type": "SystemIncludePath", "Value": "/newroot/opt/include/system"}
+ ]
+}
diff --git a/tests/auto/pkgconfig/testdata/sysroot.pc b/tests/auto/pkgconfig/testdata/sysroot.pc
new file mode 100644
index 000000000..d2f78987f
--- /dev/null
+++ b/tests/auto/pkgconfig/testdata/sysroot.pc
@@ -0,0 +1,12 @@
+prefix=/opt
+exec_prefix=${prefix}
+libdir=${exec_prefix}/lib
+includedir=${prefix}/include
+sysroot=${pc_sysrootdir}
+
+Name: Test for sysroot
+Description: Test package for testing sysroot
+Version: 1.0.0
+Requires:
+Libs: -L${libdir} -lsystem
+Cflags: -I${includedir} -idirafter ${includedir}/after -isystem ${includedir}/system
diff --git a/tests/auto/pkgconfig/testdata/system.json b/tests/auto/pkgconfig/testdata/system.json
new file mode 100644
index 000000000..89007e7ec
--- /dev/null
+++ b/tests/auto/pkgconfig/testdata/system.json
@@ -0,0 +1,17 @@
+{
+ "Name": "System library",
+ "Description": "Test package",
+ "Version": "1.0.0",
+ "Vars": {
+ "prefix": "/usr",
+ "exec_prefix": "/usr",
+ "libdir": "/usr/lib",
+ "includedir": "/usr/include"
+ },
+ "Libs": [
+ {"Type": "LibraryName", "Value": "system"}
+ ],
+ "Cflags": [
+ {"Type": "IncludePath", "Value": "/usr/include"}
+ ]
+}
diff --git a/tests/auto/pkgconfig/testdata/system.pc b/tests/auto/pkgconfig/testdata/system.pc
new file mode 100644
index 000000000..2cef2ed9b
--- /dev/null
+++ b/tests/auto/pkgconfig/testdata/system.pc
@@ -0,0 +1,10 @@
+prefix=/usr
+exec_prefix=${prefix}
+libdir=${exec_prefix}/lib
+includedir=${prefix}/include
+
+Name: System library
+Description: Test package
+Version: 1.0.0
+Libs: -L${libdir} -lsystem
+Cflags: -I${includedir}
diff --git a/tests/auto/pkgconfig/testdata/tilde.json b/tests/auto/pkgconfig/testdata/tilde.json
new file mode 100644
index 000000000..01ea5d050
--- /dev/null
+++ b/tests/auto/pkgconfig/testdata/tilde.json
@@ -0,0 +1,11 @@
+{
+ "Name": "tilde",
+ "Description": "tilde test module",
+ "Version": "1.0",
+ "Libs": [
+ {"Type": "LibraryPath", "Value": "~"}
+ ],
+ "Cflags": [
+ {"Type": "IncludePath", "Value": "~"}
+ ]
+}
diff --git a/tests/auto/pkgconfig/testdata/tilde.pc b/tests/auto/pkgconfig/testdata/tilde.pc
new file mode 100644
index 000000000..c3babc120
--- /dev/null
+++ b/tests/auto/pkgconfig/testdata/tilde.pc
@@ -0,0 +1,5 @@
+Name: tilde
+Description: tilde test module
+Version: 1.0
+Libs: -L~
+Cflags: -I~
diff --git a/tests/auto/pkgconfig/testdata/variables.json b/tests/auto/pkgconfig/testdata/variables.json
new file mode 100644
index 000000000..7565b6804
--- /dev/null
+++ b/tests/auto/pkgconfig/testdata/variables.json
@@ -0,0 +1,17 @@
+{
+ "Name": "Complex variables",
+ "Description": "Test complex variable output",
+ "Version": "1.0",
+ "Vars": {
+ "prefix": "/local",
+ "exec_prefix": "/local",
+ "libdir": "/local/lib",
+ "includedir": "\"/local/include\"",
+ "cppflags": "-I\"/local/include\"/foo -DFOO=\\\"/bar\\\""
+ },
+ "Cflags": [
+ {"Type": "IncludePath", "Value": "/local/include"},
+ {"Type": "IncludePath", "Value": "/local/include/foo"},
+ {"Type": "Define", "Value": "FOO=\"/bar\""}
+ ]
+}
diff --git a/tests/auto/pkgconfig/testdata/variables.pc b/tests/auto/pkgconfig/testdata/variables.pc
new file mode 100644
index 000000000..b27ab78e1
--- /dev/null
+++ b/tests/auto/pkgconfig/testdata/variables.pc
@@ -0,0 +1,11 @@
+prefix=/local
+exec_prefix=${prefix}
+libdir=${exec_prefix}/lib
+includedir="${prefix}/include"
+cppflags=-I${includedir}/foo \
+ -DFOO=\"/bar\"
+
+Name: Complex variables
+Description: Test complex variable output
+Version: 1.0
+Cflags: -I${includedir} ${cppflags}
diff --git a/tests/auto/pkgconfig/testdata/whitespace.json b/tests/auto/pkgconfig/testdata/whitespace.json
new file mode 100644
index 000000000..dcfa3ece3
--- /dev/null
+++ b/tests/auto/pkgconfig/testdata/whitespace.json
@@ -0,0 +1,24 @@
+{
+ "Name": "Whitespace test",
+ "Description": "Dummy pkgconfig test package for testing pkgconfig",
+ "Version": "1.0.0",
+ "Vars": {
+ "prefix": "/usr",
+ "exec_prefix": "/usr",
+ "libdir": "\"/usr/white space/lib\"",
+ "includedir": "\"/usr/white space/include\""
+ },
+ "Libs": [
+ {"Type": "LibraryPath", "Value": "/usr/white space/lib"},
+ {"Type": "LibraryName", "Value": "foo bar"},
+ {"Type": "LibraryName", "Value": "bar baz"},
+ {"Type": "LinkerFlag", "Value": "-r:foo"}
+ ],
+ "Cflags": [
+ {"Type": "IncludePath", "Value": "/usr/white space/include"},
+ {"Type": "IncludePath", "Value": "$(top_builddir)"},
+ {"Type": "IncludePath", "Value": "include dir"},
+ {"Type": "IncludePath", "Value": "other include dir"},
+ {"Type": "Define", "Value": "lala=misc"}
+ ]
+}
diff --git a/tests/auto/pkgconfig/testdata/whitespace.pc b/tests/auto/pkgconfig/testdata/whitespace.pc
new file mode 100644
index 000000000..693bbc4d0
--- /dev/null
+++ b/tests/auto/pkgconfig/testdata/whitespace.pc
@@ -0,0 +1,11 @@
+prefix=/usr
+exec_prefix=${prefix}
+libdir="${exec_prefix}/white space/lib"
+includedir="${prefix}/white space/include"
+
+Name: Whitespace test
+Description: Dummy pkgconfig test package for testing pkgconfig
+Version: 1.0.0
+Requires:
+Libs: -L${libdir} -lfoo\ bar "-lbar baz" -r:foo
+Cflags: -I${includedir} -I$(top_builddir) -Iinclude\ dir "-Iother include dir" -Dlala=misc
diff --git a/tests/auto/pkgconfig/tst_pkgconfig.cpp b/tests/auto/pkgconfig/tst_pkgconfig.cpp
new file mode 100644
index 000000000..b05dd4923
--- /dev/null
+++ b/tests/auto/pkgconfig/tst_pkgconfig.cpp
@@ -0,0 +1,181 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 Ivan Komissarov (abbapoh@gmail.com)
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qbs.
+**
+** $QT_BEGIN_LICENSE:GPL-EXCEPT$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "tst_pkgconfig.h"
+
+#include "../shared.h"
+
+#include <tools/fileinfo.h>
+#include <tools/hostosinfo.h>
+#include <pkgconfig.h>
+#include <jsextensions/pkgconfigjs.h>
+
+#include <QJsonArray>
+#include <QJsonDocument>
+
+using HostOsInfo = qbs::Internal::HostOsInfo;
+using PcPackage = qbs::PcPackage;
+using PkgConfig = qbs::PkgConfig;
+using Options = qbs::PkgConfig::Options;
+
+TestPkgConfig::TestPkgConfig()
+ : m_sourceDataDir(testDataSourceDir(SRCDIR "/testdata"))
+ , m_workingDataDir(testWorkDir(QStringLiteral("pkgconfig")))
+{
+}
+
+void TestPkgConfig::initTestCase()
+{
+ QString errorMessage;
+ qbs::Internal::removeDirectoryWithContents(m_workingDataDir, &errorMessage);
+ QVERIFY2(qbs::Internal::copyFileRecursion(m_sourceDataDir,
+ m_workingDataDir, false, true, &errorMessage),
+ qPrintable(errorMessage));
+}
+
+void TestPkgConfig::pkgConfig()
+{
+ QFETCH(QString, fileName);
+ QFETCH(QVariantMap, optionsMap);
+
+ 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");
+ QVERIFY(jsonFile.open(QIODevice::ReadOnly));
+ QJsonParseError error{};
+ const auto json = QJsonDocument::fromJson(jsonFile.readAll(), &error).toVariant().toMap();
+ QCOMPARE(error.error, QJsonParseError::NoError);
+
+ const auto &package = pkgConfig.getPackage(fileName.toStdString());
+ QCOMPARE(QString::fromStdString(package.baseFileName), fileName);
+ 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());
+
+ auto vars = json["Vars"].toMap();
+ vars["pcfiledir"] = QFileInfo(m_workingDataDir).absoluteFilePath();
+
+ for (const auto &[key, value]: package.vars) {
+ QCOMPARE(QString::fromStdString(value),
+ vars.value(QString::fromStdString(key)).toString());
+ }
+
+ const auto jsonLibs = json.value("Libs").toJsonArray().toVariantList();
+ QCOMPARE(package.libs.size(), size_t(jsonLibs.size()));
+ for (size_t i = 0; i < package.libs.size(); ++i) {
+ const auto &item = package.libs[i];
+ const auto jsonItem = jsonLibs.at(i).toMap();
+
+ QCOMPARE(item.type,
+ *PcPackage::Flag::typeFromString(jsonItem.value("Type").toString().toStdString()));
+ QCOMPARE(QString::fromStdString(item.value), jsonItem.value("Value").toString());
+ }
+
+ const auto jsonLibsPrivate = json.value("LibsPrivate").toJsonArray().toVariantList();
+ QCOMPARE(package.libsPrivate.size(), size_t(jsonLibsPrivate.size()));
+ for (size_t i = 0; i < package.libsPrivate.size(); ++i) {
+ const auto &item = package.libsPrivate[i];
+ const auto jsonItem = jsonLibsPrivate.at(i).toMap();
+
+ QCOMPARE(item.type,
+ *PcPackage::Flag::typeFromString(jsonItem.value("Type").toString().toStdString()));
+ QCOMPARE(QString::fromStdString(item.value), jsonItem.value("Value").toString());
+ }
+
+ const auto jsonCFlags = json.value("Cflags").toJsonArray().toVariantList();
+ QCOMPARE(package.cflags.size(), size_t(jsonCFlags.size()));
+ for (size_t i = 0; i < package.cflags.size(); ++i) {
+ const auto &item = package.cflags[i];
+ const auto jsonItem = jsonCFlags.at(i).toMap();
+
+ QCOMPARE(item.type,
+ *PcPackage::Flag::typeFromString(jsonItem.value("Type").toString().toStdString()));
+ QCOMPARE(QString::fromStdString(item.value), jsonItem.value("Value").toString());
+ }
+
+ for (const auto &item: package.requiresPublic)
+ qInfo() << "requires" << item.name.c_str() << item.version.c_str();
+
+ const auto jsonRequires = json.value("Requires").toJsonArray().toVariantList();
+ QCOMPARE(package.requiresPublic.size(), size_t(jsonRequires.size()));
+ for (size_t i = 0; i < package.requiresPublic.size(); ++i) {
+ const auto &item = package.requiresPublic[i];
+ const auto jsonItem = jsonRequires.at(i).toMap();
+
+ QCOMPARE(item.comparison,
+ *PcPackage::RequiredVersion::comparisonFromString(
+ jsonItem.value("Comparison").toString().toStdString()));
+ QCOMPARE(QString::fromStdString(item.name), jsonItem.value("Name").toString());
+ QCOMPARE(QString::fromStdString(item.version), jsonItem.value("Version").toString());
+ }
+
+ const auto jsonRequiresPrivate = json.value("RequiresPrivate").toJsonArray().toVariantList();
+ QCOMPARE(package.requiresPrivate.size(), size_t(jsonRequiresPrivate.size()));
+ for (size_t i = 0; i < package.requiresPrivate.size(); ++i) {
+ const auto &item = package.requiresPrivate[i];
+ const auto jsonItem = jsonRequiresPrivate.at(i).toMap();
+
+ QCOMPARE(item.comparison,
+ *PcPackage::RequiredVersion::comparisonFromString(
+ jsonItem.value("Comparison").toString().toStdString()));
+ QCOMPARE(QString::fromStdString(item.name), jsonItem.value("Name").toString());
+ QCOMPARE(QString::fromStdString(item.version), jsonItem.value("Version").toString());
+ }
+}
+
+void TestPkgConfig::pkgConfig_data()
+{
+ QTest::addColumn<QString>("fileName");
+ 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("sysroot")
+ << QStringLiteral("sysroot") << QVariantMap({{"sysroot", "/newroot"}});
+ QTest::newRow("tilde") << QStringLiteral("tilde") << QVariantMap();
+ QTest::newRow("variables") << QStringLiteral("variables") << QVariantMap();
+ QTest::newRow("whitespace") << QStringLiteral("whitespace") << QVariantMap();
+}
+
+void TestPkgConfig::benchSystem()
+{
+ if (HostOsInfo::hostOs() == HostOsInfo::HostOsWindows)
+ QSKIP("Not available on Windows");
+ QBENCHMARK {
+ PkgConfig pkgConfig;
+ QVERIFY(!pkgConfig.packages().empty());
+ }
+}
+
+QTEST_MAIN(TestPkgConfig)
diff --git a/tests/auto/pkgconfig/tst_pkgconfig.h b/tests/auto/pkgconfig/tst_pkgconfig.h
new file mode 100644
index 000000000..687411862
--- /dev/null
+++ b/tests/auto/pkgconfig/tst_pkgconfig.h
@@ -0,0 +1,53 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 Ivan Komissarov (abbapoh@gmail.com)
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qbs.
+**
+** $QT_BEGIN_LICENSE:GPL-EXCEPT$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef QBS_TST_API_H
+#define QBS_TST_API_H
+
+#include <QtCore/qobject.h>
+#include <QtCore/qvariant.h>
+
+class TestPkgConfig : public QObject
+{
+ Q_OBJECT
+
+public:
+ TestPkgConfig();
+
+private slots:
+ void initTestCase();
+ void pkgConfig();
+ void pkgConfig_data();
+ void benchSystem();
+
+private:
+ const QString m_sourceDataDir;
+ const QString m_workingDataDir;
+};
+
+#endif // Include guard.
diff --git a/tests/auto/shared.h b/tests/auto/shared.h
index e97fa9166..94c22b47e 100644
--- a/tests/auto/shared.h
+++ b/tests/auto/shared.h
@@ -332,7 +332,7 @@ inline QString testWorkDir(const QString &testName)
if (!dir.endsWith(QLatin1Char('/')))
dir += QLatin1Char('/');
}
- return dir + testName + "/testWorkDir";
+ return QDir::cleanPath(dir + testName + "/testWorkDir");
}
inline bool copyDllExportHeader(const QString &srcDataDir, const QString &targetDataDir)