diff options
author | Ivan Komissarov <abbapoh@gmail.com> | 2021-05-22 14:45:25 +0200 |
---|---|---|
committer | Ivan Komissarov <ABBAPOH@gmail.com> | 2021-09-23 11:32:23 +0000 |
commit | e87dd50dd5a74d0ddc8f9753a67a0a2500947e5f (patch) | |
tree | cd3085b0a6f5e8ec08b14ff166210e632e6b2a32 /src | |
parent | 91e0274d265a8ce7070c3d039700341b8d31cf5c (diff) | |
download | qbs-e87dd50dd5a74d0ddc8f9753a67a0a2500947e5f.tar.gz |
Long live qbs-pkgconfig!
This patchset introduce a static library for parsing .pc files. Code is
based on the original pkg-config source code https://
gitlab.freedesktop.org/pkg-config/pkg-config and is written in pure C++
(except for the places where we need access to filesystem as
std::filesystem is not available for all platforms - in that case, Qt
classes are used)
Parsing .pc files manually allows to have more control over dependencies
between modules, e.g. to generate a standalone module per one .pc file
and merge properties using Qbs itself, not via pkg-config.
Library is almost feature-complete and all tests copied from pkg-config
pass. Some functionality is omitted (e.g. prefix variables (what is
this?) or validating dependencies since Qbs does this as well)
Bechmark shows that parsing ~100 files takes about 10-15ms. Running
pkg-config on the same set of files takes ~4 seconds:
RESULT : TestPkgConfig::benchSystem():
14 msecs per iteration (total: 57, iterations: 4)
Fixes: QBS-1615
Change-Id: I5bfdfa588aa04d9d69fd738dd2beea14174c0242
Reviewed-by: Christian Kandeler <christian.kandeler@qt.io>
Diffstat (limited to 'src')
-rw-r--r-- | src/lib/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/lib/corelib/CMakeLists.txt | 3 | ||||
-rw-r--r-- | src/lib/corelib/corelib.pro | 2 | ||||
-rw-r--r-- | src/lib/corelib/corelib.qbs | 3 | ||||
-rw-r--r-- | src/lib/corelib/jsextensions/jsextensions.cpp | 1 | ||||
-rw-r--r-- | src/lib/corelib/jsextensions/jsextensions.pri | 4 | ||||
-rw-r--r-- | src/lib/corelib/jsextensions/pkgconfigjs.cpp | 211 | ||||
-rw-r--r-- | src/lib/corelib/jsextensions/pkgconfigjs.h | 106 | ||||
-rw-r--r-- | src/lib/corelib/tools/stlutils.h | 6 | ||||
-rw-r--r-- | src/lib/libs.qbs | 1 | ||||
-rw-r--r-- | src/lib/pkgconfig/CMakeLists.txt | 27 | ||||
-rw-r--r-- | src/lib/pkgconfig/pcpackage.cpp | 172 | ||||
-rw-r--r-- | src/lib/pkgconfig/pcpackage.h | 137 | ||||
-rw-r--r-- | src/lib/pkgconfig/pcparser.cpp | 762 | ||||
-rw-r--r-- | src/lib/pkgconfig/pcparser.h | 84 | ||||
-rw-r--r-- | src/lib/pkgconfig/pkgconfig.cpp | 265 | ||||
-rw-r--r-- | src/lib/pkgconfig/pkgconfig.h | 88 | ||||
-rw-r--r-- | src/lib/pkgconfig/pkgconfig.pro | 24 | ||||
-rw-r--r-- | src/lib/pkgconfig/pkgconfig.qbs | 61 | ||||
-rw-r--r-- | src/lib/pkgconfig/use_pkgconfig.pri | 41 |
20 files changed, 1998 insertions, 1 deletions
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 |