From 5eb6f51f4a2a84d3f0f3f7352253780730beee1b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 17 Jan 2026 03:05:50 -0800 Subject: [PATCH] core: add preprocessor for versioning --- CMakeLists.txt | 2 + changelog/next.md | 1 + src/build/build.hpp.in | 5 ++ src/core/CMakeLists.txt | 3 +- src/core/qmlglobal.cpp | 9 +++ src/core/qmlglobal.hpp | 15 +++++ src/core/rootwrapper.cpp | 30 ++++++++- src/core/scan.cpp | 133 +++++++++++++++++++++++++++++---------- src/core/scan.hpp | 8 +++ src/core/scanenv.cpp | 22 +++++++ src/core/scanenv.hpp | 17 +++++ src/launch/command.cpp | 4 +- 12 files changed, 209 insertions(+), 40 deletions(-) create mode 100644 src/core/scanenv.cpp create mode 100644 src/core/scanenv.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 81e896f..7633f4f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,8 @@ cmake_minimum_required(VERSION 3.20) project(quickshell VERSION "0.2.1" LANGUAGES CXX C) +set(UNRELEASED_FEATURES) + set(QT_MIN_VERSION "6.6.0") set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/changelog/next.md b/changelog/next.md index cab03e6..30e998b 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -23,6 +23,7 @@ set shell id. - Added initial support for network management. - Added support for grabbing focus from popup windows. - Added support for IPC signal listeners. +- Added Quickshell version checking and version gated preprocessing. ## Other Changes diff --git a/src/build/build.hpp.in b/src/build/build.hpp.in index 075abd1..66fb664 100644 --- a/src/build/build.hpp.in +++ b/src/build/build.hpp.in @@ -1,6 +1,11 @@ #pragma once // NOLINTBEGIN +#define QS_VERSION "@quickshell_VERSION@" +#define QS_VERSION_MAJOR @quickshell_VERSION_MAJOR@ +#define QS_VERSION_MINOR @quickshell_VERSION_MINOR@ +#define QS_VERSION_PATCH @quickshell_VERSION_PATCH@ +#define QS_UNRELEASED_FEATURES "@UNRELEASED_FEATURES@" #define GIT_REVISION "@GIT_REVISION@" #define DISTRIBUTOR "@DISTRIBUTOR@" #define DISTRIBUTOR_DEBUGINFO_AVAILABLE @DEBUGINFO_AVAILABLE@ diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index bbfb8c4..fb63f40 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -12,6 +12,7 @@ qt_add_library(quickshell-core STATIC singleton.cpp generation.cpp scan.cpp + scanenv.cpp qsintercept.cpp incubator.cpp lazyloader.cpp @@ -51,7 +52,7 @@ qt_add_qml_module(quickshell-core install_qml_module(quickshell-core) -target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets) +target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets quickshell-build) qs_module_pch(quickshell-core SET large) diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index 07238f6..03fb818 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -29,6 +29,7 @@ #include "paths.hpp" #include "qmlscreen.hpp" #include "rootwrapper.hpp" +#include "scanenv.hpp" QuickshellSettings::QuickshellSettings() { QObject::connect( @@ -313,6 +314,14 @@ QString QuickshellGlobal::iconPath(const QString& icon, const QString& fallback) return IconImageProvider::requestString(icon, "", fallback); } +bool QuickshellGlobal::hasVersion(qint32 major, qint32 minor, const QStringList& features) { + return qs::scan::env::PreprocEnv::hasVersion(major, minor, features); +} + +bool QuickshellGlobal::hasVersion(qint32 major, qint32 minor) { + return QuickshellGlobal::hasVersion(major, minor, QStringList()); +} + QuickshellGlobal* QuickshellGlobal::create(QQmlEngine* engine, QJSEngine* /*unused*/) { auto* qsg = new QuickshellGlobal(); auto* generation = EngineGeneration::findEngineGeneration(engine); diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index 1fc363b..3ca70be 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -217,6 +217,21 @@ public: /// /// The popup can also be blocked by setting `QS_NO_RELOAD_POPUP=1`. Q_INVOKABLE void inhibitReloadPopup() { this->mInhibitReloadPopup = true; } + /// Check if Quickshell's version is at least `major.minor` and the listed + /// unreleased features are available. If Quickshell is newer than the given version + /// it is assumed that all unreleased features are present. The unreleased feature list + /// may be omitted. + /// + /// > [!NOTE] You can feature gate code blocks using Quickshell's preprocessor which + /// > has the same function available. + /// > + /// > ```qml + /// > //@ if hasVersion(0, 3, ["feature"]) + /// > ... + /// > //@ endif + /// > ``` + Q_INVOKABLE static bool hasVersion(qint32 major, qint32 minor, const QStringList& features); + Q_INVOKABLE static bool hasVersion(qint32 major, qint32 minor); void clearReloadPopupInhibit() { this->mInhibitReloadPopup = false; } [[nodiscard]] bool isReloadPopupInhibited() const { return this->mInhibitReloadPopup; } diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index 25c46cc..1e75819 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -63,9 +63,6 @@ void RootWrapper::reloadGraph(bool hard) { qs::core::QmlToolingSupport::updateTooling(rootPath, scanner); this->configDirWatcher.addPath(rootPath.path()); - auto* generation = new EngineGeneration(rootPath, std::move(scanner)); - generation->wrapper = this; - // todo: move into EngineGeneration if (this->generation != nullptr) { qInfo() << "Reloading configuration..."; @@ -74,6 +71,33 @@ void RootWrapper::reloadGraph(bool hard) { QDir::setCurrent(this->originalWorkingDirectory); + if (!scanner.scanErrors.isEmpty()) { + qCritical() << "Failed to load configuration"; + QString errorString = "Failed to load configuration"; + for (auto& error: scanner.scanErrors) { + const auto& file = error.file; + QString rel; + if (file.startsWith(rootPath.path() % '/')) { + rel = '@' % file.sliced(rootPath.path().length() + 1); + } else { + rel = file; + } + + auto msg = " error in " % rel % '[' % QString::number(error.line) % ":0]: " % error.message; + errorString += '\n' % msg; + qCritical().noquote() << msg; + } + + if (this->generation != nullptr && this->generation->qsgInstance != nullptr) { + emit this->generation->qsgInstance->reloadFailed(errorString); + } + + return; + } + + auto* generation = new EngineGeneration(rootPath, std::move(scanner)); + generation->wrapper = this; + QUrl url; url.setScheme("qs"); url.setPath("@/qs/" % rootFile.fileName()); diff --git a/src/core/scan.cpp b/src/core/scan.cpp index 453b7dc..8ca1f51 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -1,9 +1,11 @@ #include "scan.hpp" #include +#include #include #include #include +#include #include #include #include @@ -15,6 +17,7 @@ #include #include "logcat.hpp" +#include "scanenv.hpp" QS_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg); @@ -115,51 +118,113 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna auto stream = QTextStream(&file); auto imports = QVector(); - while (!stream.atEnd()) { - auto line = stream.readLine().trimmed(); - if (!singleton && line == "pragma Singleton") { - singleton = true; - } else if (!internal && line == "//@ pragma Internal") { - internal = true; - } else if (line.startsWith("import")) { - // we dont care about "import qs" as we always load the root folder - if (auto importCursor = line.indexOf(" qs."); importCursor != -1) { - importCursor += 4; - QString path; + bool inHeader = false; + auto ifScopes = QVector(); + bool sourceMasked = false; + int lineNum = 0; + QString overrideText; + bool isOverridden = false; - while (importCursor != line.length()) { - auto c = line.at(importCursor); - if (c == '.') c = '/'; - else if (c == ' ') break; - else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') - || c == '_') - { - } else { - qCWarning(logQmlScanner) << "Import line contains invalid characters: " << line; - goto next; + auto pragmaEngine = QJSEngine(); + pragmaEngine.globalObject().setPrototype( + pragmaEngine.newQObject(new qs::scan::env::PreprocEnv()) + ); + + auto postError = [&, this](QString error) { + this->scanErrors.append({.file = path, .message = std::move(error), .line = lineNum}); + }; + + while (!stream.atEnd()) { + ++lineNum; + bool hideMask = false; + auto rawLine = stream.readLine(); + auto line = rawLine.trimmed(); + if (!sourceMasked && inHeader) { + if (!singleton && line == "pragma Singleton") { + singleton = true; + } else if (line.startsWith("import")) { + // we dont care about "import qs" as we always load the root folder + if (auto importCursor = line.indexOf(" qs."); importCursor != -1) { + importCursor += 4; + QString path; + + while (importCursor != line.length()) { + auto c = line.at(importCursor); + if (c == '.') c = '/'; + else if (c == ' ') break; + else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') + || c == '_') + { + } else { + qCWarning(logQmlScanner) << "Import line contains invalid characters: " << line; + goto next; + } + + path.append(c); + importCursor += 1; } - path.append(c); - importCursor += 1; + imports.append(this->rootPath.filePath(path)); + } else if (auto startQuot = line.indexOf('"'); + startQuot != -1 && line.length() >= startQuot + 3) + { + auto endQuot = line.indexOf('"', startQuot + 1); + if (endQuot == -1) continue; + + auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1); + imports.push_back(name); } - - imports.append(this->rootPath.filePath(path)); - } else if (auto startQuot = line.indexOf('"'); - startQuot != -1 && line.length() >= startQuot + 3) - { - auto endQuot = line.indexOf('"', startQuot + 1); - if (endQuot == -1) continue; - - auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1); - imports.push_back(name); + } else if (!internal && line == "//@ pragma Internal") { + internal = true; + } else if (line.contains('{')) { + inHeader = true; } - } else if (line.contains('{')) break; + } + + if (line.startsWith("//@ if ")) { + auto code = line.sliced(7); + auto value = pragmaEngine.evaluate(code, path, 1234); + bool mask = true; + + if (value.isError()) { + postError(QString("Evaluating if: %0").arg(value.toString())); + } else if (!value.isBool()) { + postError(QString("If expression \"%0\" is not a boolean").arg(value.toString())); + } else if (value.toBool()) { + mask = false; + } + if (!sourceMasked && mask) hideMask = true; + mask = sourceMasked || mask; // cant unmask if a nested if passes + ifScopes.append(mask); + if (mask) isOverridden = true; + sourceMasked = mask; + } else if (line.startsWith("//@ endif")) { + if (ifScopes.isEmpty()) { + postError("endif without matching if"); + } else { + ifScopes.pop_back(); + + if (ifScopes.isEmpty()) sourceMasked = false; + else sourceMasked = ifScopes.last(); + } + } + + if (!hideMask && sourceMasked) overrideText.append("// MASKED: " % rawLine % '\n'); + else overrideText.append(rawLine % '\n'); next:; } + if (!ifScopes.isEmpty()) { + postError("unclosed preprocessor if block"); + } + file.close(); + if (isOverridden) { + this->fileIntercepts.insert(path, overrideText); + } + if (logQmlScanner().isDebugEnabled() && !imports.isEmpty()) { qCDebug(logQmlScanner) << "Found imports" << imports; } diff --git a/src/core/scan.hpp b/src/core/scan.hpp index 2dc8c3c..29f8f6a 100644 --- a/src/core/scan.hpp +++ b/src/core/scan.hpp @@ -23,6 +23,14 @@ public: QVector scannedFiles; QHash fileIntercepts; + struct ScanError { + QString file; + QString message; + int line; + }; + + QVector scanErrors; + private: QDir rootPath; diff --git a/src/core/scanenv.cpp b/src/core/scanenv.cpp new file mode 100644 index 0000000..b8c514c --- /dev/null +++ b/src/core/scanenv.cpp @@ -0,0 +1,22 @@ +#include "scanenv.hpp" + +#include + +#include "build.hpp" + +namespace qs::scan::env { + +bool PreprocEnv::hasVersion(int major, int minor, const QStringList& features) { + if (QS_VERSION_MAJOR > major) return true; + if (QS_VERSION_MAJOR == major && QS_VERSION_MINOR > minor) return true; + + auto availFeatures = QString(QS_UNRELEASED_FEATURES).split(';'); + + for (const auto& feature: features) { + if (!availFeatures.contains(feature)) return false; + } + + return QS_VERSION_MAJOR == major && QS_VERSION_MINOR == minor; +} + +} // namespace qs::scan::env diff --git a/src/core/scanenv.hpp b/src/core/scanenv.hpp new file mode 100644 index 0000000..0abde2e --- /dev/null +++ b/src/core/scanenv.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include + +namespace qs::scan::env { + +class PreprocEnv: public QObject { + Q_OBJECT; + +public: + Q_INVOKABLE static bool + hasVersion(int major, int minor, const QStringList& features = QStringList()); +}; + +} // namespace qs::scan::env diff --git a/src/launch/command.cpp b/src/launch/command.cpp index d867584..151fc24 100644 --- a/src/launch/command.cpp +++ b/src/launch/command.cpp @@ -519,8 +519,8 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { } if (state.misc.printVersion) { - qCInfo(logBare).noquote().nospace() - << "quickshell 0.2.1, revision " << GIT_REVISION << ", distributed by: " << DISTRIBUTOR; + qCInfo(logBare).noquote().nospace() << "quickshell " << QS_VERSION << ", revision " + << GIT_REVISION << ", distributed by: " << DISTRIBUTOR; if (state.log.verbosity > 1) { qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR;