git: 166cea3628c7 - main - Uses/cabal.mk: Introduce CABAL_WRAPPER_SCRIPTS variable.

From: Gleb Popov <arrowd_at_FreeBSD.org>
Date: Thu, 28 Jul 2022 19:53:36 UTC
The branch main has been updated by arrowd:

URL: https://cgit.FreeBSD.org/ports/commit/?id=166cea3628c726601ebfd9a5497a4808015eebd3

commit 166cea3628c726601ebfd9a5497a4808015eebd3
Author:     Gleb Popov <arrowd@FreeBSD.org>
AuthorDate: 2022-07-28 19:48:58 +0000
Commit:     Gleb Popov <arrowd@FreeBSD.org>
CommitDate: 2022-07-28 19:52:56 +0000

    Uses/cabal.mk: Introduce CABAL_WRAPPER_SCRIPTS variable.
    
    Before this change every Haskell executable was wrapped into a shell script
    which was installed into ${PREFIX}/bin while the actual executable was installed
    into {PREFIX}/libexec/cabal. This was required to set env variables pointing the
    Haskell program to its data files under ${PREFIX}/share. However, not every
    Haskell program uses this feature.
    
    Now the shell wrapping is off by default and CABAL_WRAPPER_SCRIPTS knob can be
    used to enable it for a given port/executable.
    
    Adjust all Haskell ports affected by this change.
---
 Mk/Uses/cabal.mk                       |  20 +-
 devel/hs-alex/Makefile                 |   2 +
 devel/hs-cabal-install/Makefile        |   4 +-
 devel/hs-git-annex/Makefile            |  13 +-
 devel/hs-git-annex/pkg-plist           |   2 -
 devel/hs-happy/Makefile                |   2 +
 devel/hs-profiteur/Makefile            |   1 +
 devel/kdevelop/files/patch-craft.patch | 698 +++++++++++++++++++++++++++++++++
 devel/kdevelop/files/patch-gdb.patch   |  22 ++
 math/hs-Agda/Makefile                  |   1 +
 security/hs-cryptol/Makefile           |   3 +-
 textproc/hs-pandoc/Makefile            |  13 +-
 12 files changed, 760 insertions(+), 21 deletions(-)

diff --git a/Mk/Uses/cabal.mk b/Mk/Uses/cabal.mk
index a85a7b62529b..d04d6f5ed3fe 100644
--- a/Mk/Uses/cabal.mk
+++ b/Mk/Uses/cabal.mk
@@ -33,6 +33,12 @@
 #			and "-${opt_CABAL_FLAGS}" otherwise.
 #  opt_EXECUTABLES	Variant of EXECUTABLES to be used with options framework.
 #
+#  CABAL_WRAPPER_SCRIPTS	A subset of ${EXECUTABLES} containing Haskell
+#			programs to be wrapped into a shell script that sets
+#			*_datadir environment variables before running the program.
+#			This is needed for Haskell programs that install their
+#			data files under share/ directory.
+#
 #  FOO_DATADIR_VARS     Additional environment vars to add to FOO executable's
 #                       wrapper script.
 #
@@ -266,20 +272,28 @@ do-build:
 
 .  if !target(do-install)
 do-install:
+.    if defined(CABAL_WRAPPER_SCRIPTS) && !empty(CABAL_WRAPPER_SCRIPTS)
 	${MKDIR} ${STAGEDIR}${PREFIX}/${CABAL_LIBEXEC}
+.    endif
 .    for exe in ${EXECUTABLES}
+.      if defined(CABAL_WRAPPER_SCRIPTS) && ${CABAL_WRAPPER_SCRIPTS:M${exe}}
 	${INSTALL_PROGRAM} \
 		$$(find ${WRKSRC}/dist-newstyle -name ${exe} -type f -perm +111) \
 		${STAGEDIR}${PREFIX}/${CABAL_LIBEXEC}/${exe}
 	${ECHO_CMD} '#!/bin/sh' > ${STAGEDIR}${PREFIX}/bin/${exe}
 	${ECHO_CMD} '' >> ${STAGEDIR}${PREFIX}/bin/${exe}
 	${ECHO_CMD} 'export ${exe:S/-/_/g}_datadir=${DATADIR}' >> ${STAGEDIR}${PREFIX}/bin/${exe}
-.      for dep in ${${exe}_DATADIR_VARS}
+.        for dep in ${${exe}_DATADIR_VARS}
 	${ECHO_CMD} 'export ${dep:S/-/_/g}_datadir=${DATADIR}' >> ${STAGEDIR}${PREFIX}/bin/${exe}
-.      endfor
+.        endfor
 	${ECHO_CMD} '' >> ${STAGEDIR}${PREFIX}/bin/${exe}
 	${ECHO_CMD} 'exec ${PREFIX}/${CABAL_LIBEXEC}/${exe} "$$@"' >> ${STAGEDIR}${PREFIX}/bin/${exe}
 	${CHMOD} +x ${STAGEDIR}${PREFIX}/bin/${exe}
+.      else
+	${INSTALL_PROGRAM} \
+		$$(find ${WRKSRC}/dist-newstyle -name ${exe} -type f -perm +111) \
+		${STAGEDIR}${PREFIX}/bin/${exe}
+.      endif
 .    endfor
 .  endif
 
@@ -287,7 +301,9 @@ do-install:
 cabal-post-install-script:
 .      for exe in ${EXECUTABLES}
 		${ECHO_CMD} 'bin/${exe}' >> ${TMPPLIST}
+.        if defined(CABAL_WRAPPER_SCRIPTS) && ${CABAL_WRAPPER_SCRIPTS:M${exe}}
 		${ECHO_CMD} '${CABAL_LIBEXEC}/${exe}' >> ${TMPPLIST}
+.        endif
 .    endfor
 .  endif
 
diff --git a/devel/hs-alex/Makefile b/devel/hs-alex/Makefile
index 0ea72a1d16a3..23b7954770c1 100644
--- a/devel/hs-alex/Makefile
+++ b/devel/hs-alex/Makefile
@@ -10,6 +10,8 @@ LICENSE=	BSD3CLAUSE
 
 USES=		cabal
 
+CABAL_WRAPPER_SCRIPTS=	${EXECUTABLES}
+
 OPTIONS_DEFINE=	EXAMPLES
 
 PORTEXAMPLES=	Makefile *.x *.y
diff --git a/devel/hs-cabal-install/Makefile b/devel/hs-cabal-install/Makefile
index 65f03b649522..aa1ed7e47bdf 100644
--- a/devel/hs-cabal-install/Makefile
+++ b/devel/hs-cabal-install/Makefile
@@ -37,10 +37,8 @@ USE_CABAL=	async-2.2.4 \
 		th-compat-0.1.3 \
 		zlib-0.6.2.3_1
 
+EXECUTABLES=		cabal
 SKIP_CABAL_EXTRACT=	yes
-SKIP_CABAL_PLIST=	yes
-
-PLIST_FILES=	bin/cabal
 
 post-extract:
 	${MKDIR} ${WRKSRC}/_build/tarballs/
diff --git a/devel/hs-git-annex/Makefile b/devel/hs-git-annex/Makefile
index d093691c16f3..f46dc959669c 100644
--- a/devel/hs-git-annex/Makefile
+++ b/devel/hs-git-annex/Makefile
@@ -1,5 +1,6 @@
 PORTNAME=	git-annex
 PORTVERSION=	10.20220525
+PORTREVISION=	1
 CATEGORIES=	devel haskell
 
 MAINTAINER=	haskell@FreeBSD.org
@@ -261,8 +262,6 @@ DBUS_USE_CABAL=		dbus-1.2.24 \
 CABAL_FLAGS=		production torrentparser magicmime \
 			-benchmark -debuglocks
 
-EXECUTABLES=	git-annex
-
 MAN1PAGES=	git-annex-add git-annex-expire git-annex-lookupkey \
 		git-annex-remotedaemon git-annex-ungroup \
 		git-annex-addunused git-annex-find git-annex-map \
@@ -313,13 +312,7 @@ post-install:
 .endfor
 
 post-stage:
-	${LN} -sf git-annex ${STAGEDIR}${PREFIX}/${CABAL_LIBEXEC}/git-annex-shell
-	${LN} -sf git-annex ${STAGEDIR}${PREFIX}/${CABAL_LIBEXEC}/git-remote-tor-annex
-	${CP} ${STAGEDIR}${PREFIX}/bin/git-annex ${STAGEDIR}${PREFIX}/bin/git-annex-shell
-	${CP} ${STAGEDIR}${PREFIX}/bin/git-annex ${STAGEDIR}${PREFIX}/bin/git-remote-tor-annex
-	${REINPLACE_CMD} 's|${PREFIX}/${CABAL_LIBEXEC}/git-annex|${PREFIX}/${CABAL_LIBEXEC}/git-annex-shell|' \
-		${STAGEDIR}${PREFIX}/bin/git-annex-shell
-	${REINPLACE_CMD} 's|${PREFIX}/${CABAL_LIBEXEC}/git-annex|${PREFIX}/${CABAL_LIBEXEC}/git-remote-tor-annex|' \
-		${STAGEDIR}${PREFIX}/bin/git-remote-tor-annex
+	${LN} -sf git-annex ${STAGEDIR}${PREFIX}/bin/git-annex-shell
+	${LN} -sf git-annex ${STAGEDIR}${PREFIX}/bin/git-remote-tor-annex
 
 .include <bsd.port.mk>
diff --git a/devel/hs-git-annex/pkg-plist b/devel/hs-git-annex/pkg-plist
index 8f7b027d77c3..5fe344fbeada 100644
--- a/devel/hs-git-annex/pkg-plist
+++ b/devel/hs-git-annex/pkg-plist
@@ -1,7 +1,5 @@
 bin/git-annex-shell
 bin/git-remote-tor-annex
-libexec/cabal/git-annex-shell
-libexec/cabal/git-remote-tor-annex
 man/man1/git-annex-add.1.gz
 man/man1/git-annex-addunused.1.gz
 man/man1/git-annex-addurl.1.gz
diff --git a/devel/hs-happy/Makefile b/devel/hs-happy/Makefile
index a318d34b4b7d..4b115d7569d5 100644
--- a/devel/hs-happy/Makefile
+++ b/devel/hs-happy/Makefile
@@ -10,6 +10,8 @@ LICENSE=	BSD3CLAUSE
 
 USES=		cabal
 
+CABAL_WRAPPER_SCRIPTS=	${EXECUTABLES}
+
 PORTEXAMPLES=	*.ly README glr/* igloo/*
 
 OPTIONS_DEFINE=	EXAMPLES
diff --git a/devel/hs-profiteur/Makefile b/devel/hs-profiteur/Makefile
index d26feebfaeaf..f918854e81b0 100644
--- a/devel/hs-profiteur/Makefile
+++ b/devel/hs-profiteur/Makefile
@@ -49,6 +49,7 @@ USE_CABAL=	OneTuple-0.3.1_2 \
 		vector-0.12.3.1_2 \
 		witherable-0.4.2_3
 
+CABAL_WRAPPER_SCRIPTS=		${EXECUTABLES}
 profiteur_DATADIR_VARS=		js-jquery
 
 post-install:
diff --git a/devel/kdevelop/files/patch-craft.patch b/devel/kdevelop/files/patch-craft.patch
new file mode 100644
index 000000000000..207fdb667ec7
--- /dev/null
+++ b/devel/kdevelop/files/patch-craft.patch
@@ -0,0 +1,698 @@
+diff --git plugins/CMakeLists.txt plugins/CMakeLists.txt
+index 8bea62a97e..32a27dad9e 100644
+--- plugins/CMakeLists.txt
++++ plugins/CMakeLists.txt
+@@ -80,6 +80,7 @@ ecm_optional_add_subdirectory(genericprojectmanager)
+ 
+ # BEGIN: Runtimes
+ add_subdirectory(android)
++add_subdirectory(craft)
+ if (UNIX)
+     add_subdirectory(docker)
+     add_subdirectory(flatpak)
+diff --git plugins/craft/CMakeLists.txt plugins/craft/CMakeLists.txt
+new file mode 100644
+index 0000000000..8cf28b6c01
+--- /dev/null
++++ plugins/craft/CMakeLists.txt
+@@ -0,0 +1,22 @@
++add_definitions(-DTRANSLATION_DOMAIN=\"kdevcraft\")
++
++declare_qt_logging_category(craftplugin_LOG_SRCS
++    TYPE PLUGIN
++    HEADER debug_craft.h
++    IDENTIFIER CRAFT
++    CATEGORY_BASENAME "craft"
++)
++
++#qt5_add_resources(craftplugin_SRCS kdevcraftplugin.qrc)
++kdevplatform_add_plugin(kdevcraft SOURCES craftplugin.cpp craftruntime.cpp ${craftplugin_LOG_SRCS})
++target_link_libraries(kdevcraft
++    KF5::CoreAddons
++    KDev::Interfaces
++    KDev::Util
++    KDev::OutputView
++    KDev::Project
++)
++
++if(BUILD_TESTING)
++    add_subdirectory(tests)
++endif()
+diff --git plugins/craft/craftplugin.cpp plugins/craft/craftplugin.cpp
+new file mode 100644
+index 0000000000..c27e82a8b5
+--- /dev/null
++++ plugins/craft/craftplugin.cpp
+@@ -0,0 +1,84 @@
++// SPDX-FileCopyrightText: 2022 Gleb Popov <arrowd@FreeBSD.org>
++// SPDX-License-Identifier: BSD-3-Clause
++
++#include "craftplugin.h"
++#include "craftruntime.h"
++#include "debug_craft.h"
++
++#include <interfaces/icore.h>
++#include <interfaces/iproject.h>
++#include <interfaces/iprojectcontroller.h>
++#include <interfaces/iruntimecontroller.h>
++#include <interfaces/iuicontroller.h>
++
++#include <KParts/MainWindow>
++#include <KPluginFactory>
++#include <KLocalizedString>
++#include <KConfigGroup>
++#include <KMessageBox>
++
++K_PLUGIN_FACTORY_WITH_JSON(KDevCraftFactory, "kdevcraft.json", registerPlugin<CraftPlugin>();)
++
++using namespace KDevelop;
++
++CraftPlugin::CraftPlugin(QObject* parent, const QVariantList& /*args*/)
++    : IPlugin(QStringLiteral("kdevcraft"), parent), m_shouldAutoEnable(Uninitialized)
++{
++    const QString pythonExecutable = CraftRuntime::findPython();
++    if (pythonExecutable.isEmpty())
++        return;
++
++    // If KDevelop itself runs under Craft env, this plugin has nothing to do
++    if (qEnvironmentVariableIsSet("KDEROOT"))
++        return;
++
++    connect(ICore::self()->projectController(), &IProjectController::projectAboutToBeOpened, this,
++        [pythonExecutable, this](KDevelop::IProject* project) {
++            const QString craftRoot = CraftRuntime::findCraftRoot(project->path());
++
++            if (craftRoot.isEmpty())
++                return;
++
++            qCDebug(CRAFT) << "Found Craft root at" << craftRoot;
++
++            auto* runtime = m_runtimes.value(craftRoot, nullptr);
++
++            if (!runtime) {
++                runtime = new CraftRuntime(craftRoot, pythonExecutable);
++                ICore::self()->runtimeController()->addRuntimes(runtime);
++                m_runtimes.insert(craftRoot, runtime);
++            }
++
++            bool haveConfigEntry = project->projectConfiguration()->group("Project")
++                                           .entryMap().contains(QLatin1String("AutoEnableCraftRuntime"));
++
++            if (!haveConfigEntry && m_shouldAutoEnable == Uninitialized) {
++                const QString msgboxText =
++                    i18n("The project being loaded (%1) is detected to reside under a\n"
++                         "Craft root [%2] .\nDo you want to automatically switch to the Craft runtime?\n"
++                         "Note that this will switch the runtime for all projects in a session!",
++                         project->name(), craftRoot);
++
++                auto answer = KMessageBox::questionYesNo(ICore::self()->uiController()->activeMainWindow(), msgboxText);
++                m_shouldAutoEnable = answer == KMessageBox::Yes ? AutoEnable : DoNotAutoEnable;
++                project->projectConfiguration()->group("Project")
++                        .writeEntry("AutoEnableCraftRuntime", answer == KMessageBox::Yes);
++            }
++            else if (!haveConfigEntry)
++                project->projectConfiguration()->group("Project")
++                        .writeEntry("AutoEnableCraftRuntime", m_shouldAutoEnable == AutoEnable);
++            else
++                m_shouldAutoEnable =
++                    project->projectConfiguration()->group("Project")
++                            .readEntry("AutoEnableCraftRuntime", false)
++                                ? AutoEnable
++                                : DoNotAutoEnable ;
++
++            if (m_shouldAutoEnable == AutoEnable) {
++                qCDebug(CRAFT) << "Enabling Craft runtime at" << craftRoot << "with" << pythonExecutable;
++                ICore::self()->runtimeController()->setCurrentRuntime(runtime);
++            }
++        });
++}
++
++#include "craftplugin.moc"
+diff --git plugins/craft/craftplugin.h plugins/craft/craftplugin.h
+new file mode 100644
+index 0000000000..62ef30b928
+--- /dev/null
++++ plugins/craft/craftplugin.h
+@@ -0,0 +1,28 @@
++// SPDX-FileCopyrightText: 2022 Gleb Popov <arrowd@FreeBSD.org>
++// SPDX-License-Identifier: BSD-3-Clause
++
++#ifndef CRAFTPLUGIN_H
++#define CRAFTPLUGIN_H
++
++#include <QHash>
++
++#include <interfaces/iplugin.h>
++
++class CraftRuntime;
++
++class CraftPlugin : public KDevelop::IPlugin
++{
++    Q_OBJECT
++public:
++    CraftPlugin(QObject* parent, const QVariantList& args);
++
++private:
++    QHash<QString, CraftRuntime*> m_runtimes;
++    enum {
++        DoNotAutoEnable,
++        AutoEnable,
++        Uninitialized
++    } m_shouldAutoEnable;
++};
++
++#endif // CRAFTPLUGIN_H
+diff --git plugins/craft/craftruntime.cpp plugins/craft/craftruntime.cpp
+new file mode 100644
+index 0000000000..f30766e511
+--- /dev/null
++++ plugins/craft/craftruntime.cpp
+@@ -0,0 +1,173 @@
++// SPDX-FileCopyrightText: 2022 Gleb Popov <arrowd@FreeBSD.org>
++// SPDX-License-Identifier: BSD-3-Clause
++
++#include "craftruntime.h"
++#include "debug_craft.h"
++
++#include <QFileInfo>
++#include <QStandardPaths>
++#include <QProcess>
++#include <KProcess>
++
++using namespace KDevelop;
++
++namespace {
++    auto craftSetupHelperRelativePath() { return QLatin1String{"/craft/bin/CraftSetupHelper.py"}; }
++}
++
++CraftRuntime::CraftRuntime(const QString& craftRoot, const QString& pythonExecutable)
++    : m_craftRoot(craftRoot), m_pythonExecutable(pythonExecutable)
++{
++    Q_ASSERT(!pythonExecutable.isEmpty());
++
++    m_watcher.addPath(craftRoot + craftSetupHelperRelativePath());
++
++    connect(&m_watcher, &QFileSystemWatcher::fileChanged, this, [this](const QString &path)
++    {
++        if (QFileInfo::exists(path)) {
++            refreshEnvCache();
++            if (!m_watcher.files().contains(path)) {
++                m_watcher.addPath(path);
++            }
++        }
++    });
++    refreshEnvCache();
++}
++
++QString CraftRuntime::name() const
++{
++    return QStringLiteral("Craft [%1]").arg(m_craftRoot);
++}
++
++QString CraftRuntime::findCraftRoot(Path startingPoint)
++{
++    // CraftRuntime doesn't handle remote directories, because it needs
++    // to check file existence in the findCraftRoot() function
++    if (startingPoint.isRemote())
++        return QString();
++
++    QString craftRoot;
++    while(true) {
++        bool craftSettingsIniExists = QFileInfo::exists(startingPoint.path() + QLatin1String("/etc/CraftSettings.ini"));
++        bool craftSetupHelperExists = QFileInfo::exists(startingPoint.path() + craftSetupHelperRelativePath());
++        if (craftSettingsIniExists && craftSetupHelperExists) {
++            craftRoot = startingPoint.path();
++            break;
++        }
++
++        if (!startingPoint.hasParent())
++            break;
++        startingPoint = startingPoint.parent();
++    }
++
++    return QFileInfo(craftRoot).canonicalFilePath();
++}
++
++QString CraftRuntime::findPython()
++{
++    // Craft requires Python 3.6+, not any "python3", but
++    // - If the user set up Craft already, there is a high probability that
++    //   "python3" is a correct one
++    // - We are running only CraftSetupHelper.py, not the whole Craft, so
++    //   the 3.6+ requirement might be not relevant for this case.
++    // So just search for "python3" and hope for the best.
++    return QStandardPaths::findExecutable(QStringLiteral("python3"));
++}
++
++void CraftRuntime::setEnabled(bool /*enabled*/)
++{
++}
++
++void CraftRuntime::refreshEnvCache()
++{
++    QProcess python;
++    python.start(m_pythonExecutable, QStringList{m_craftRoot + craftSetupHelperRelativePath(), QStringLiteral("--getenv")});
++
++    if(!python.waitForFinished(5000)) {
++        qCWarning(CRAFT) << "CraftSetupHelper.py execution timed out";
++        return;
++    }
++
++    if (python.exitStatus() != QProcess::NormalExit) {
++        qCWarning(CRAFT) << "CraftSetupHelper.py execution failed with code" << python.exitCode();
++        return;
++    }
++
++    m_envCache.clear();
++
++    const QList<QByteArray> output = python.readAllStandardOutput().split('\n');
++    for (const auto& line : output) {
++        // line contains things like "VAR=VALUE"
++        int equalsSignIndex = line.indexOf('=');
++        if (equalsSignIndex == -1)
++            continue;
++
++        QByteArray varName = line.left(equalsSignIndex);
++        QByteArray value = line.mid(equalsSignIndex + 1);
++        m_envCache.emplace_back(varName, value);
++    }
++}
++
++QByteArray CraftRuntime::getenv(const QByteArray& varname) const
++{
++    auto it = std::find_if(m_envCache.begin(), m_envCache.end(),
++                           [&varname](const EnvironmentVariable& envVar)
++    {
++        return envVar.name == varname;
++    });
++
++    return it != m_envCache.end() ? it->value : QByteArray();
++}
++
++QString CraftRuntime::findExecutable(const QString& executableName) const
++{
++    auto runtimePaths = QString::fromLocal8Bit(getenv(QByteArrayLiteral("PATH"))).split(QLatin1Char(':'));
++
++    return QStandardPaths::findExecutable(executableName, runtimePaths);
++}
++
++Path CraftRuntime::pathInHost(const Path& runtimePath) const
++{
++    return runtimePath;
++}
++
++Path CraftRuntime::pathInRuntime(const Path& localPath) const
++{
++    return localPath;
++}
++
++void CraftRuntime::startProcess(KProcess* process) const
++{
++    QStringList program = process->program();
++    QString executableInRuntime = findExecutable(program.constFirst());
++    if (executableInRuntime != program.constFirst()) {
++        program.first() = std::move(executableInRuntime);
++        process->setProgram(program);
++    }
++    setEnvironmentVariables(process);
++    process->start();
++}
++
++void CraftRuntime::startProcess(QProcess* process) const
++{
++    QString executableInRuntime = findExecutable(process->program());
++    process->setProgram(executableInRuntime);
++    setEnvironmentVariables(process);
++    process->start();
++}
++
++void CraftRuntime::setEnvironmentVariables(QProcess* process) const
++{
++    auto env = process->processEnvironment();
++
++    for(const auto& envVar : m_envCache) {
++        env.insert(QString::fromLocal8Bit(envVar.name), QString::fromLocal8Bit(envVar.value));
++    }
++
++    process->setProcessEnvironment(env);
++}
++
++EnvironmentVariable::EnvironmentVariable(const QByteArray& name, const QByteArray& value)
++    : name(name.trimmed()), value(value)
++{
++}
+diff --git plugins/craft/craftruntime.h plugins/craft/craftruntime.h
+new file mode 100644
+index 0000000000..c23ee0246e
+--- /dev/null
++++ plugins/craft/craftruntime.h
+@@ -0,0 +1,62 @@
++// SPDX-FileCopyrightText: 2022 Gleb Popov <arrowd@FreeBSD.org>
++// SPDX-License-Identifier: BSD-3-Clause
++
++#ifndef CRAFTRUNTIME_H
++#define CRAFTRUNTIME_H
++
++#include <vector>
++
++#include <QString>
++#include <QFileSystemWatcher>
++#include <interfaces/iruntime.h>
++#include <util/path.h>
++
++class QProcess;
++
++namespace KDevelop {
++    class IProject;
++}
++
++// An auxiliary structure to hold normalized name and value of an env var
++struct EnvironmentVariable {
++    EnvironmentVariable(const QByteArray& name, const QByteArray& value);
++
++    QByteArray name;
++    QByteArray value;
++};
++Q_DECLARE_TYPEINFO(EnvironmentVariable, Q_MOVABLE_TYPE);
++
++
++class CraftRuntime : public KDevelop::IRuntime
++{
++    Q_OBJECT
++public:
++    CraftRuntime(const QString& craftRoot, const QString& pythonExecutable);
++
++    QString name() const override;
++
++    void setEnabled(bool enabled) override;
++
++    void startProcess(KProcess* process) const override;
++    void startProcess(QProcess* process) const override;
++    KDevelop::Path pathInHost(const KDevelop::Path& runtimePath) const override;
++    KDevelop::Path pathInRuntime(const KDevelop::Path& localPath) const override;
++    QString findExecutable(const QString& executableName) const override;
++    QByteArray getenv(const QByteArray& varname) const override;
++
++    KDevelop::Path buildPath() const override { return {}; }
++
++    static QString findCraftRoot(KDevelop::Path startingPoint);
++    static QString findPython();
++
++private:
++    void setEnvironmentVariables(QProcess* process) const;
++    void refreshEnvCache();
++
++    const QString m_craftRoot;
++    const QString m_pythonExecutable;
++    QFileSystemWatcher m_watcher;
++    std::vector<EnvironmentVariable> m_envCache;
++};
++
++#endif // CRAFTRUNTIME_H
+diff --git plugins/craft/kdevcraft.json plugins/craft/kdevcraft.json
+new file mode 100644
+index 0000000000..bd05794c23
+--- /dev/null
++++ plugins/craft/kdevcraft.json
+@@ -0,0 +1,22 @@
++{
++    "KPlugin": {
++        "Authors": [
++            {
++                "Email": "arrowd@FreeBSD.org",
++                "Name": "Gleb Popov",
++                "Name[ru]": "Глеб Попов"
++            }
++        ],
++        "Category": "Runtimes",
++        "Description": "Exposes KDE Craft environment as a runtime",
++        "Description[ru]": "Представляет среду KDE Craft как среду выполнения KDevelop",
++        "Icon": "kdevelop",
++        "Id": "kdevcraft",
++        "License": "BSD3",
++        "Name": "Craft runtime",
++        "Name[ru]": "Поддержка Craft",
++        "Version": "0.1"
++    },
++    "X-KDevelop-Category": "Global",
++    "X-KDevelop-Mode": "GUI"
++}
+diff --git plugins/craft/tests/CMakeLists.txt plugins/craft/tests/CMakeLists.txt
+new file mode 100644
+index 0000000000..3ed8f32d5c
+--- /dev/null
++++ plugins/craft/tests/CMakeLists.txt
+@@ -0,0 +1,16 @@
++include_directories(
++    ..
++    ${CMAKE_CURRENT_BINARY_DIR}/..
++)
++
++set(test_craftruntime_SRCS
++    test_craftruntime.cpp
++    ../craftruntime.cpp
++    ${craftplugin_LOG_SRCS}
++)
++
++ecm_add_test(${test_craftruntime_SRCS}
++    TEST_NAME test_craftruntime
++    LINK_LIBRARIES Qt5::Test KDev::Tests)
++
++target_compile_definitions(test_craftruntime PRIVATE -DCRAFT_ROOT_MOCK="${CMAKE_CURRENT_SOURCE_DIR}/craft_root_mock")
+diff --git plugins/craft/tests/craft_root_mock/bin/env plugins/craft/tests/craft_root_mock/bin/env
+new file mode 100755
+index 0000000000..630848fc56
+--- /dev/null
++++ plugins/craft/tests/craft_root_mock/bin/env
+@@ -0,0 +1,6 @@
++#!/usr/bin/env python3
++
++import os
++
++for var in os.environ:
++    print(var + "=" + os.environ[var])
+diff --git plugins/craft/tests/craft_root_mock/bin/printenv plugins/craft/tests/craft_root_mock/bin/printenv
+new file mode 100755
+index 0000000000..375a1945e4
+--- /dev/null
++++ plugins/craft/tests/craft_root_mock/bin/printenv
+@@ -0,0 +1,5 @@
++#!/usr/bin/env python3
++
++print("PYTHONPATH=/usr/lib/python3/site-packages")
++print("BAD LINE")
++print("FOO=")
+diff --git plugins/craft/tests/craft_root_mock/craft/bin/CraftSetupHelper.py plugins/craft/tests/craft_root_mock/craft/bin/CraftSetupHelper.py
+new file mode 100644
+index 0000000000..1ae643b89b
+--- /dev/null
++++ plugins/craft/tests/craft_root_mock/craft/bin/CraftSetupHelper.py
+@@ -0,0 +1,9 @@
++import os
++import sys
++
++root = os.path.realpath(os.path.dirname(os.path.realpath(__file__)) + "/../../")
++
++if "--getenv" in sys.argv:
++    print("KDEROOT=" + root)
++    print("PYTHONPATH=" + root + "/lib/site-packages")
++    print("PATH=" + root + "/bin:" + os.environ["PATH"])
+diff --git plugins/craft/tests/craft_root_mock/etc/CraftSettings.ini plugins/craft/tests/craft_root_mock/etc/CraftSettings.ini
+new file mode 100644
+index 0000000000..e69de29bb2
+diff --git plugins/craft/tests/test_craftruntime.cpp plugins/craft/tests/test_craftruntime.cpp
+new file mode 100644
+index 0000000000..ede259d0e2
+--- /dev/null
++++ plugins/craft/tests/test_craftruntime.cpp
+@@ -0,0 +1,164 @@
++// SPDX-FileCopyrightText: 2022 Gleb Popov <arrowd@FreeBSD.org>
++// SPDX-License-Identifier: BSD-3-Clause
++
++#include "test_craftruntime.h"
++
++#include <QFile>
++#include <QProcess>
++#include <QStandardPaths>
++#include <QTest>
++
++#include <KIO/CopyJob>
++#include <KProcess>
++
++#include <tests/testcore.h>
++#include <tests/testhelpers.h>
++
++#include "../craftruntime.h"
++
++using namespace KDevelop;
++
++QTEST_MAIN(CraftRuntimeTest)
++
++class TempDirWrapper
++{
++public:
++    TempDirWrapper() = default;
++    TempDirWrapper(const QString& craftRoot, const QString& pythonExecutable)
++        : m_tempCraftRoot(new QTemporaryDir())
++    {
++        QVERIFY(m_tempCraftRoot->isValid());
++        copyCraftRoot(craftRoot);
++        m_runtime = std::make_shared<CraftRuntime>(m_tempCraftRoot->path(), pythonExecutable);
++    }
++
++    QString path() const
++    {
++        QVERIFY_RETURN(m_tempCraftRoot, QString());
++        return m_tempCraftRoot->path();
++    }
++
++    CraftRuntime* operator->() const
++    {
++        QVERIFY_RETURN(m_runtime, nullptr);
++        return m_runtime.get();
++    }
++private:
++    void copyCraftRoot(const QString& oldRoot) const {
++        const QLatin1String craftSettingsRelativePath("/etc/CraftSettings.ini");
++        const QDir dest(m_tempCraftRoot->path());
++
++        auto* job = KIO::copy(QUrl::fromLocalFile(oldRoot + QLatin1String("/craft")),
++                              QUrl::fromLocalFile(dest.path()));
++        QVERIFY(job->exec());
++
++        QVERIFY(dest.mkpath(QLatin1String("bin")));
++        QVERIFY(dest.mkpath(QLatin1String("etc")));
++
++        QVERIFY(QFile::copy(oldRoot + craftSettingsRelativePath,
++                            dest.path() + craftSettingsRelativePath));
++    }
++    std::shared_ptr<CraftRuntime> m_runtime;
++    std::shared_ptr<QTemporaryDir> m_tempCraftRoot;
++};
++
++Q_DECLARE_METATYPE(TempDirWrapper)
++
++// When this test itself is ran under a Craft root, its environment gets in the way
++static void breakoutFromCraftRoot() {
++    auto craftRoot = qgetenv("KDEROOT");
++    if (craftRoot.isEmpty())
++        return;
++
++    auto paths = qgetenv("PATH").split(':');
++    std::remove_if(paths.begin(), paths.end(), [craftRoot](const QByteArray& path) {
++        return path.startsWith(craftRoot);
++    });
++    qputenv("PATH", paths.join(':'));
++
++    qunsetenv("KDEROOT");
++    qunsetenv("craftRoot");
++}
++
++void CraftRuntimeTest::initTestCase_data()
++{
++    breakoutFromCraftRoot();
++
++    const QString pythonExecutable = CraftRuntime::findPython();
++    if (pythonExecutable.isEmpty())
++        QSKIP("No python found, skipping kdevcraft tests.");
++
++    QTest::addColumn<TempDirWrapper>("runtimeInstance");
++
++    QTest::newRow("Mock") << TempDirWrapper(QStringLiteral(CRAFT_ROOT_MOCK), pythonExecutable);
++
++    auto craftRoot = CraftRuntime::findCraftRoot(Path(QStringLiteral(".")));
++    if (!craftRoot.isEmpty())
++        QTest::newRow("Real") << TempDirWrapper(craftRoot, pythonExecutable);
++}
++
++void CraftRuntimeTest::testFindCraftRoot()
++{
++    QFETCH_GLOBAL(TempDirWrapper, runtimeInstance);
++    QCOMPARE(CraftRuntime::findCraftRoot(Path(runtimeInstance.path())), runtimeInstance.path());
++    QCOMPARE(CraftRuntime::findCraftRoot(Path(runtimeInstance.path()).cd(QStringLiteral("bin"))), runtimeInstance.path());
++}
++
++void CraftRuntimeTest::testGetenv()
++{
++    QFETCH_GLOBAL(TempDirWrapper, runtimeInstance);
++
++    QVERIFY(!runtimeInstance->getenv("KDEROOT").isEmpty());
++
++    QDir craftDir1 = QDir(QString::fromLocal8Bit(runtimeInstance->getenv("KDEROOT")));
++    QDir craftDir2 = QDir(runtimeInstance.path());
++    QCOMPARE(craftDir1.canonicalPath(), craftDir2.canonicalPath());
++
++    QString pythonpathValue = QString::fromLocal8Bit(runtimeInstance->getenv("PYTHONPATH"));
++    QVERIFY(!pythonpathValue.isEmpty());
++    QDir craftPythonPathDir = QDir(pythonpathValue);
++
++    QVERIFY(craftPythonPathDir.path().startsWith(craftDir1.path()));
++}
++
++void CraftRuntimeTest::testStartProcess()
++{
++    QFETCH_GLOBAL(TempDirWrapper, runtimeInstance);
++
++    QString envPath = QStandardPaths::findExecutable(QStringLiteral("env"));
++    if (envPath.isEmpty())
++        QSKIP("Skipping startProcess() test, no \"env\" executable found");
++
++    QString envUnderCraftPath = runtimeInstance.path() + QStringLiteral("/bin/env");
++    QVERIFY(QFile::copy(envPath, envUnderCraftPath));
++
++    QProcess p;
++    p.setProgram(QStringLiteral("env"));
++    runtimeInstance->startProcess(&p);
++
++    // test that CraftRuntime::startProcess prefers programs under Craft root
++    QCOMPARE(QDir(p.program()).canonicalPath(), QDir(envUnderCraftPath).canonicalPath());
++
++    p.waitForFinished();
++    QVERIFY(QFile::remove(envUnderCraftPath));
++}
++
++void CraftRuntimeTest::testStartProcessEnv()
++{
++    QFETCH_GLOBAL(TempDirWrapper, runtimeInstance);
++
++    QString printenvPath = QStandardPaths::findExecutable(QStringLiteral("printenv"));
++    if (printenvPath.isEmpty())
++        QSKIP("Skipping startProcess() test, no \"printenv\" executable found");
++
++    QString printenvUnderCraftPath = runtimeInstance.path() + QStringLiteral("/bin/printenv");
++    QVERIFY(QFile::copy(printenvPath, printenvUnderCraftPath));
++
++    KProcess p;
++    p.setProgram(QStringLiteral("printenv"), QStringList {QStringLiteral("PYTHONPATH")});
++    p.setOutputChannelMode(KProcess::OnlyStdoutChannel);
++    runtimeInstance->startProcess(&p);
++    p.waitForFinished();
++
++    QVERIFY(p.readAllStandardOutput().contains("site-packages"));
++}
+diff --git plugins/craft/tests/test_craftruntime.h plugins/craft/tests/test_craftruntime.h
+new file mode 100644
+index 0000000000..d5a470f946
+--- /dev/null
++++ plugins/craft/tests/test_craftruntime.h
+@@ -0,0 +1,20 @@
++// SPDX-FileCopyrightText: 2022 Gleb Popov <arrowd@FreeBSD.org>
++// SPDX-License-Identifier: BSD-3-Clause
++
++#ifndef KDEVPLATFORM_PLUGIN_TEST_CRAFTRUNTIME_H
++#define KDEVPLATFORM_PLUGIN_TEST_CRAFTRUNTIME_H
++
++#include <QObject>
++
++class CraftRuntimeTest: public QObject
++{
++    Q_OBJECT
++private Q_SLOTS:
++    void initTestCase_data();
++    void testFindCraftRoot();
++    void testGetenv();
++    void testStartProcess();
++    void testStartProcessEnv();
++};
++
++#endif // KDEVPLATFORM_PLUGIN_TEST_CRAFTRUNTIME_H
diff --git a/devel/kdevelop/files/patch-gdb.patch b/devel/kdevelop/files/patch-gdb.patch
new file mode 100644
index 000000000000..9e22db710ae7
--- /dev/null
+++ b/devel/kdevelop/files/patch-gdb.patch
@@ -0,0 +1,22 @@
+diff --git plugins/gdb/gdb.cpp plugins/gdb/gdb.cpp
+index eb15feb446..11c115400f 100644
+--- plugins/gdb/gdb.cpp
++++ plugins/gdb/gdb.cpp
+@@ -12,6 +12,8 @@
+ #include "debuglog.h"
+ 
+ #include <interfaces/icore.h>
++#include <interfaces/iruntime.h>
++#include <interfaces/iruntimecontroller.h>
+ #include <interfaces/iuicontroller.h>
+ #include <sublime/message.h>
+ 
+@@ -79,7 +81,7 @@ bool GdbDebugger::start(KConfigGroup& config, const QStringList& extraArguments)
+     }
+     fullCommand += arguments.join(QLatin1Char(' '));
+ 
+-    m_process->start();
++    KDevelop::ICore::self()->runtimeController()->currentRuntime()->startProcess(m_process);
+ 
+     qCDebug(DEBUGGERGDB) << "Starting GDB with command" << fullCommand;
+ #if KCOREADDONS_VERSION < QT_VERSION_CHECK(5, 78, 0)
diff --git a/math/hs-Agda/Makefile b/math/hs-Agda/Makefile
index 3171a7fe97ae..4d8a1bce5991 100644
--- a/math/hs-Agda/Makefile
+++ b/math/hs-Agda/Makefile
@@ -83,6 +83,7 @@ USE_CABAL=	OneTuple-0.3.1_2 \
 		zlib-0.6.3.0
 
 EXECUTABLES=		agda-mode agda
+CABAL_WRAPPER_SCRIPTS=	${EXECUTABLES}
 agda_DATADIR_VARS=	Agda
 agda-mode_DATADIR_VARS=	Agda
 
diff --git a/security/hs-cryptol/Makefile b/security/hs-cryptol/Makefile
index b3bf20ee10d8..87bfe3ce028e 100644
--- a/security/hs-cryptol/Makefile
+++ b/security/hs-cryptol/Makefile
@@ -122,7 +122,8 @@ USE_CABAL=	GraphSCC-1.0.4 \
 		zlib-0.6.3.0 \
 		zlib-bindings-0.1.1.5_2
 
-EXECUTABLES=	cryptol cryptol-html
+EXECUTABLES=		cryptol cryptol-html
+CABAL_WRAPPER_SCRIPTS=	${EXECUTABLES}
 
 post-install:
 	cd ${WRKSRC}/lib && ${COPYTREE_SHARE} . ${STAGEDIR}${DATADIR}
diff --git a/textproc/hs-pandoc/Makefile b/textproc/hs-pandoc/Makefile
index cece20947927..73972b841859 100644
--- a/textproc/hs-pandoc/Makefile
+++ b/textproc/hs-pandoc/Makefile
@@ -15,7 +15,6 @@ OPTIONS_DEFINE=		EMBED_DATA TRYPANDOC
 
 EMBED_DATA_DESC=	Embed data files in binary for relocatable executable
 EMBED_DATA_CABAL_FLAGS=	embed_data_files
-EMBED_DATA_USE_CABAL=	file-embed-0.0.15.0
 
 TRYPANDOC_DESC=		Build trypandoc cgi executable
 TRYPANDOC_CABAL_FLAGS=	trypandoc
@@ -177,11 +176,19 @@ USE_CABAL=	Glob-0.10.2_3 \
 		zip-archive-0.4.2.1 \
 		zlib-0.6.3.0
 
-CABAL_PROJECT=	remove
-EXECUTABLES=	pandoc
+CABAL_PROJECT=		remove
+EXECUTABLES=		pandoc
+CABAL_WRAPPER_SCRIPTS=	${EXECUTABLES}
 
 OPTIONS_SUB=	yes
 
+.include <bsd.port.options.mk>
+
+.if ${PORT_OPTIONS:MEMBED_DATA}
+# No need to use wrapper scripts when all data is compiled into an executable
+.undef CABAL_WRAPPER_SCRIPTS
+.endif
+
 .include <bsd.port.pre.mk>
*** 2 LINES SKIPPED ***