diff options
46 files changed, 2521 insertions, 3 deletions
@@ -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) |