milestone: Phase 4-5 completion + KF6 honesty + KDE session + GPU CS ioctl

Phase 4 KDE Plasma:
- 20 KF6 + kglobalacceld + plasma-workspace + plasma-desktop + plasma-framework enabled
- kf6-kio honest reduced build (package-local QtNetwork compat headers, no sysroot fakery)
- kf6-kdeclarative enabled
- redbear-kde-session launcher (DRM/virtual backend, plasmashell/kded6, readiness markers)
- Phase 4 checker: required plasmashell/kded6 process checks (FAIL on absence)

Phase 5 Hardware GPU:
- CS ioctl checker (GEM allocation, PRIME sharing, private CS submit/wait over /scheme/drm/card0)
- Enhanced GPU checker with hardware rendering readiness summary
- test-phase5-cs-runtime.sh harness

Qt6Quick honesty: qtdeclarative exports Qt6Quick metadata; downstream QML/Kirigami/KWin proof still insufficient.
Oracle-verified: Phase 4-5 (5 rounds).

Build: zero warnings.
This commit is contained in:
2026-04-29 11:05:22 +01:00
parent c3a91a5c4b
commit 6fa895652f
20 changed files with 2444 additions and 891 deletions
@@ -1,4 +1,5 @@
#TODO: KDeclarative — KDE QtQuick integration. QML disabled for Redox.
# KDeclarative — reduced real build for Red Bear OS.
# QML-backed runtime pieces stay disabled with BUILD_WITH_QML=OFF.
[source]
tar = "https://invent.kde.org/frameworks/kdeclarative/-/archive/v6.10.0/kdeclarative-v6.10.0.tar.gz"
+7 -608
View File
@@ -1,4 +1,10 @@
#TODO: KIO — file I/O abstraction, network transparency, job system. Core KDE framework.
# KIO — reduced real KIOCore build for Red Bear OS.
#
# Honesty boundary:
# - KIOCORE_ONLY=ON, BUILD_WITH_QML=OFF, USE_DBUS=OFF stay intentional.
# - QtNetwork is still unavailable on Redox, so KIOCore uses source-local
# Redox compatibility headers for the small QHostInfo/QHostAddress surface it needs.
# - This recipe no longer forges QtNetwork headers into the shared sysroot.
[source]
tar = "https://invent.kde.org/frameworks/kio/-/archive/v6.10.0/kio-v6.10.0.tar.gz"
@@ -42,613 +48,6 @@ for qtdir in plugins mkspecs metatypes modules; do
fi
done
if [ ! -d "${COOKBOOK_SYSROOT}/lib/cmake/Qt6Network" ]; then
cat > "${COOKBOOK_SYSROOT}/include/QHostAddress" <<'EOF'
#pragma once
#include <QDataStream>
#include <QMetaType>
#include <QString>
class QHostAddress
{
public:
enum NetworkLayerProtocol {
UnknownNetworkLayerProtocol = -1,
AnyIPProtocol,
IPv4Protocol,
IPv6Protocol,
};
QHostAddress() = default;
explicit QHostAddress(const QString &address)
: m_address(address)
{
}
QString toString() const
{
return m_address;
}
NetworkLayerProtocol protocol() const
{
return AnyIPProtocol;
}
private:
QString m_address;
friend QDataStream &operator<<(QDataStream &stream, const QHostAddress &address)
{
stream << address.m_address;
return stream;
}
friend QDataStream &operator>>(QDataStream &stream, QHostAddress &address)
{
stream >> address.m_address;
return stream;
}
};
Q_DECLARE_METATYPE(QHostAddress)
EOF
cat > "${COOKBOOK_SYSROOT}/include/QHostInfo" <<'EOF'
#pragma once
#include <QList>
#include <QMetaType>
#include <QString>
#include <QHostAddress>
class QHostInfo
{
public:
enum HostInfoError {
NoError = 0,
HostNotFound = 1,
UnknownError = 2,
};
QHostInfo() = default;
explicit QHostInfo(const QString &hostName)
: m_hostName(hostName)
{
}
static QString localHostName()
{
return QStringLiteral("redox");
}
void setHostName(const QString &hostName)
{
m_hostName = hostName;
}
QString hostName() const
{
return m_hostName;
}
void setAddresses(const QList<QHostAddress> &addresses)
{
m_addresses = addresses;
}
QList<QHostAddress> addresses() const
{
return m_addresses;
}
void setError(HostInfoError error)
{
m_error = error;
}
HostInfoError error() const
{
return m_error;
}
void setErrorString(const QString &errorString)
{
m_errorString = errorString;
}
QString errorString() const
{
return m_errorString;
}
private:
QString m_hostName;
QList<QHostAddress> m_addresses;
HostInfoError m_error = UnknownError;
QString m_errorString;
};
Q_DECLARE_METATYPE(QHostInfo)
EOF
cat > "${COOKBOOK_SYSROOT}/include/QLocalSocket" <<'EOF'
#pragma once
#include <QByteArray>
#include <QIODevice>
#include <QString>
#include <cstring>
class QLocalSocket : public QIODevice
{
public:
enum LocalSocketState {
UnconnectedState,
ConnectingState,
ConnectedState,
ClosingState,
};
enum LocalSocketError {
ConnectionRefusedError,
PeerClosedError,
ServerNotFoundError,
SocketAccessError,
SocketResourceError,
SocketTimeoutError,
DatagramTooLargeError,
ConnectionError,
UnsupportedSocketOperationError,
OperationError,
UnknownSocketError,
};
explicit QLocalSocket(QObject *parent = nullptr)
: QIODevice(parent)
{
}
void connectToServer(const QString &)
{
m_state = UnconnectedState;
m_errorString = QStringLiteral("QtNetwork disabled on Redox");
setOpenMode(ReadWrite);
}
void setReadBufferSize(qint64 size)
{
m_readBufferSize = size;
}
LocalSocketState state() const
{
return m_state;
}
LocalSocketError error() const
{
return m_error;
}
QString errorString() const
{
return m_errorString;
}
qint64 bytesAvailable() const override
{
return m_buffer.size() + QIODevice::bytesAvailable();
}
bool waitForReadyRead(int) override
{
return false;
}
bool waitForBytesWritten(int) override
{
return true;
}
Q_SIGNALS:
void disconnected();
protected:
qint64 readData(char *data, qint64 maxSize) override
{
const qint64 toRead = qMin<qint64>(maxSize, m_buffer.size());
if (toRead <= 0) {
return -1;
}
memcpy(data, m_buffer.constData(), static_cast<size_t>(toRead));
m_buffer.remove(0, static_cast<int>(toRead));
return toRead;
}
qint64 writeData(const char *data, qint64 maxSize) override
{
m_written.append(data, static_cast<int>(maxSize));
return maxSize;
}
private:
QByteArray m_buffer;
QByteArray m_written;
qint64 m_readBufferSize = 0;
LocalSocketState m_state = UnconnectedState;
LocalSocketError m_error = ConnectionError;
QString m_errorString;
};
EOF
cat > "${COOKBOOK_SYSROOT}/include/QLocalServer" <<'EOF'
#pragma once
#include <QObject>
#include <QString>
class QLocalSocket;
class QLocalServer : public QObject
{
public:
explicit QLocalServer(QObject *parent = nullptr)
: QObject(parent)
{
}
bool listen(const QString &name)
{
m_name = name;
return false;
}
QString errorString() const
{
return QStringLiteral("QtNetwork disabled on Redox");
}
QLocalSocket *nextPendingConnection()
{
return nullptr;
}
Q_SIGNALS:
void newConnection();
private:
QString m_name;
};
EOF
python3 - <<'PY'
from pathlib import Path
import os
source = Path(os.environ["COOKBOOK_SOURCE"])
def replace(path: Path, old: str, new: str) -> None:
text = path.read_text()
if old in text:
path.write_text(text.replace(old, new))
elif new not in text:
raise SystemExit(f"missing pattern in {path}: {old!r}")
replace(
source / "CMakeLists.txt",
"find_package(Qt6 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Widgets Network Concurrent Xml Test)",
"find_package(Qt6 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Widgets Concurrent Xml)",
)
replace(
source / "CMakeLists.txt",
"find_package(LibMount REQUIRED)",
"find_package(LibMount)",
)
replace(
source / "CMakeLists.txt",
'''if (CMAKE_SYSTEM_NAME MATCHES "Linux")
find_package(LibMount)
set(HAVE_LIB_MOUNT ${LibMount_FOUND})
endif()''',
'''if (CMAKE_SYSTEM_NAME MATCHES "Linux" AND NOT REDOX)
find_package(LibMount)
set(HAVE_LIB_MOUNT ${LibMount_FOUND})
endif()''',
)
replace(
source / "KF6KIOConfig.cmake.in",
'''find_dependency(Qt6Network "@REQUIRED_QT_VERSION@")''',
'''find_dependency(Qt6Concurrent "@REQUIRED_QT_VERSION@")
find_dependency(Qt6Xml "@REQUIRED_QT_VERSION@")''',
)
replace(
source / "src/CMakeLists.txt",
'''# KIOCore-only executables
if (NOT ANDROID)
add_subdirectory(kioworkers)
add_subdirectory(schemehandlers)
endif()
if (HAVE_QTDBUS)
add_subdirectory(kiod)
add_subdirectory(kssld)
endif()
add_subdirectory(kioworker)
''',
'''# KIOCore-only executables
if (NOT KIOCORE_ONLY)
if (NOT ANDROID)
add_subdirectory(kioworkers)
add_subdirectory(schemehandlers)
endif()
if (HAVE_QTDBUS)
add_subdirectory(kiod)
add_subdirectory(kssld)
endif()
add_subdirectory(kioworker)
endif()
''',
)
core_cmake = source / "src/core/CMakeLists.txt"
core_text = core_cmake.read_text()
for line in [
" hostinfo.cpp\\n",
" ksslcertificatemanager.cpp\\n",
" Qt6::Network\\n",
]:
core_text = core_text.replace(line, "")
core_cmake.write_text(core_text)
replace(
source / "src/core/askuseractioninterface.h",
"#include <QSsl>\\n\\n",
"",
)
replace(
source / "src/core/slavebase.h",
"#include <QByteArray>\\n#include <QHostInfo>\\n#include <QSsl>\\n\\n#include <memory>\\n\\nclass KConfigGroup;\\nclass KRemoteEncoding;\\nclass QUrl;\\n",
"#include <QByteArray>\\n\\n#include <memory>\\n\\nclass KConfigGroup;\\nclass KRemoteEncoding;\\nclass QHostInfo;\\nclass QUrl;\\n",
)
replace(
source / "src/core/slavebase.cpp",
"#include <QMap>\\n#include <QSsl>\\n#include <QtGlobal>\\n",
"#include <QMap>\\n#include <QHostInfo>\\n#include <QtGlobal>\\n",
)
replace(
source / "src/core/workerinterface_p.h",
"#include <QHostInfo>\\n#include <QObject>\\n",
"#include <QObject>\\n",
)
replace(
source / "src/core/workerinterface_p.h",
"private Q_SLOTS:\\n void slotHostInfo(const QHostInfo &info);\\n\\nprotected:\\n",
"protected:\\n",
)
replace(
source / "src/core/workerinterface.cpp",
'''#include "connection_p.h"\n#include "kiocoredebug.h"\n''',
'''#include "connection_p.h"\n#include "kiocoredebug.h"\n\n#include <QHostInfo>\n''',
)
replace(
source / "src/core/workerinterface.cpp",
''' case MSG_HOST_INFO_REQ: {
QString hostName;
stream >> hostName;
HostInfo::lookupHost(hostName, this, SLOT(slotHostInfo(QHostInfo)));
break;
}
''',
''' case MSG_HOST_INFO_REQ: {
QString hostName;
stream >> hostName;
QByteArray replyData;
QDataStream replyStream(&replyData, QIODevice::WriteOnly);
replyStream << hostName << QList<QHostAddress>() << int(QHostInfo::UnknownError) << QStringLiteral("Host lookup unavailable on Redox");
m_connection->send(CMD_HOST_INFO, replyData);
break;
}
''',
)
replace(
source / "src/core/workerinterface.cpp",
'''
void WorkerInterface::slotHostInfo(const QHostInfo &info)
{
QByteArray data;
QDataStream stream(&data, QIODevice::WriteOnly);
stream << info.hostName() << info.addresses() << info.error() << info.errorString();
m_connection->send(CMD_HOST_INFO, data);
}
''',
"\\n",
)
replace(
source / "src/core/ksslerroruidata_p.h",
"#include <QSslCertificate>\\n#include <QSslError>\\n#include <QString>\\n",
"#include <QString>\\n",
)
replace(
source / "src/core/ksslerroruidata_p.h",
''' QList<QSslCertificate> certificateChain;
QList<QSslError> sslErrors; // parallel list to certificateChain
QString ip;
''',
''' QString ip;
''',
)
replace(
source / "src/core/ksslerroruidata.cpp",
"#include <QHostAddress>\\n#include <QNetworkReply>\\n#include <QSslCipher>\\n\\n",
"",
)
replace(
source / "src/core/ksslerroruidata.cpp",
'''KSslErrorUiData::KSslErrorUiData(const QSslSocket *socket)
: d(new Private())
{
d->certificateChain = socket->peerCertificateChain();
d->sslErrors = socket->sslHandshakeErrors();
d->ip = socket->peerAddress().toString();
d->host = socket->peerName();
if (socket->isEncrypted()) {
d->sslProtocol = socket->sessionCipher().protocolString();
}
d->cipher = socket->sessionCipher().name();
d->usedBits = socket->sessionCipher().usedBits();
d->bits = socket->sessionCipher().supportedBits();
}
''',
'''KSslErrorUiData::KSslErrorUiData(const QSslSocket *socket)
: d(new Private())
{
d->usedBits = 0;
d->bits = 0;
(void)socket;
}
''',
)
replace(
source / "src/core/ksslerroruidata.cpp",
'''KSslErrorUiData::KSslErrorUiData(const QNetworkReply *reply, const QList<QSslError> &sslErrors)
: d(new Private())
{
const auto sslConfig = reply->sslConfiguration();
d->certificateChain = sslConfig.peerCertificateChain();
d->sslErrors = sslErrors;
d->host = reply->request().url().host();
d->sslProtocol = sslConfig.sessionCipher().protocolString();
d->cipher = sslConfig.sessionCipher().name();
d->usedBits = sslConfig.sessionCipher().usedBits();
d->bits = sslConfig.sessionCipher().supportedBits();
}
''',
'''KSslErrorUiData::KSslErrorUiData(const QNetworkReply *reply, const QList<QSslError> &sslErrors)
: d(new Private())
{
d->usedBits = 0;
d->bits = 0;
(void)reply;
(void)sslErrors;
}
''',
)
PY
if ! grep -q '^#include <QHostInfo>$' "${COOKBOOK_SOURCE}/src/core/workerinterface.cpp"; then
sed -i '/#include <QDateTime>/a #include <QHostInfo>' "${COOKBOOK_SOURCE}/src/core/workerinterface.cpp"
fi
cat > "${COOKBOOK_SOURCE}/src/core/connectionbackend.cpp" <<'EOF'
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2000 Stephan Kulow <coolo@kde.org>
SPDX-FileCopyrightText: 2000 David Faure <coolo@kde.org>
SPDX-FileCopyrightText: 2007 Thiago Macieira <thiago@kde.org>
SPDX-FileCopyrightText: 2024 Harald Sitter <sitter@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "connectionbackend_p.h"
#include <KLocalizedString>
using namespace KIO;
ConnectionBackend::ConnectionBackend(QObject *parent)
: QObject(parent)
, state(Idle)
, socket(nullptr)
, localServer(nullptr)
, signalEmitted(false)
{
}
ConnectionBackend::~ConnectionBackend() = default;
void ConnectionBackend::setSuspended(bool enable)
{
(void)enable;
}
bool ConnectionBackend::connectToRemote(const QUrl &url)
{
(void)url;
errorString = i18n("Local IPC is unavailable on Redox without QtNetwork");
state = Idle;
return false;
}
ConnectionBackend::ConnectionResult ConnectionBackend::listenForRemote()
{
state = Idle;
errorString = i18n("Local IPC is unavailable on Redox without QtNetwork");
return {false, errorString};
}
bool ConnectionBackend::waitForIncomingTask(int ms)
{
(void)ms;
return false;
}
bool ConnectionBackend::sendCommand(int cmd, const QByteArray &data) const
{
(void)cmd;
(void)data;
return false;
}
ConnectionBackend *ConnectionBackend::nextPendingConnection()
{
return nullptr;
}
void ConnectionBackend::socketReadyRead()
{
}
void ConnectionBackend::socketDisconnected()
{
state = Idle;
Q_EMIT disconnected();
}
EOF
fi
sed -i "s/^ecm_install_po_files_as_qm/#ecm_install_po_files_as_qm/" \
"${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true
sed -i 's/^ki18n_install(po)/#ki18n_install(po)/' \
"${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true
sed -i '/find_package(Qt6.*Widgets)/a find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)' \
"${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true
sed -i '/include(ECMQmlModule)/s/^/#/' "${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true
rm -f CMakeCache.txt
rm -rf CMakeFiles
@@ -183,6 +183,10 @@ target_include_directories(KF6KIOCore PUBLIC
"$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/..>" # kio_version.h
)
target_include_directories(KF6KIOCore PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/redox_qtnetwork_compat
)
target_include_directories(KF6KIOCore INTERFACE
"$<INSTALL_INTERFACE:${KDE_INSTALL_INCLUDEDIR_KF}/KIOCore>"
"$<INSTALL_INTERFACE:${KDE_INSTALL_INCLUDEDIR_KF}/KIO>"
@@ -327,4 +331,3 @@ install(FILES
# make available to ecm_add_qch in parent folder
set(KIOCore_QCH_SOURCES ${KIOCore_HEADERS} ${KIO_namespaced_HEADERS} PARENT_SCOPE)
@@ -133,22 +133,11 @@ public:
return m_hostName;
}
int lookupId() const
{
return m_lookupId;
}
void setLookupId(int id)
{
m_lookupId = id;
}
private:
Q_DISABLE_COPY(NameLookupThreadRequest)
QString m_hostName;
QSemaphore m_semaphore;
QHostInfo m_hostInfo;
int m_lookupId;
};
}
@@ -162,30 +151,9 @@ class NameLookUpThreadWorker : public QObject
public Q_SLOTS:
void lookupHost(const std::shared_ptr<KIO::NameLookupThreadRequest> &request)
{
const QString hostName = request->hostName();
const int lookupId = QHostInfo::lookupHost(hostName, this, SLOT(lookupFinished(QHostInfo)));
request->setLookupId(lookupId);
m_lookups.insert(lookupId, request);
request->setResult(QHostInfo::fromName(request->hostName()));
request->semaphore()->release();
}
void abortLookup(const std::shared_ptr<KIO::NameLookupThreadRequest> &request)
{
QHostInfo::abortHostLookup(request->lookupId());
m_lookups.remove(request->lookupId());
}
void lookupFinished(const QHostInfo &hostInfo)
{
auto it = m_lookups.find(hostInfo.lookupId());
if (it != m_lookups.end()) {
(*it)->setResult(hostInfo);
(*it)->semaphore()->release();
m_lookups.erase(it);
}
}
private:
QMap<int, std::shared_ptr<NameLookupThreadRequest>> m_lookups;
};
class NameLookUpThread : public QThread
@@ -271,11 +239,6 @@ QHostInfo HostInfo::lookupHost(const QString &hostName, unsigned long timeout)
if (!hostInfo.hostName().isEmpty() && hostInfo.error() == QHostInfo::NoError) {
HostInfo::cacheLookup(hostInfo); // cache the look up...
}
} else {
auto abortFunc = [worker, request]() {
worker->abortLookup(request);
};
QMetaObject::invokeMethod(worker, abortFunc, Qt::QueuedConnection);
}
// qDebug() << "Name look up succeeded for" << hostName;
@@ -0,0 +1,87 @@
#pragma once
#include <QByteArray>
#include <QDataStream>
#include <QMetaType>
#include <QString>
#include <arpa/inet.h>
class QHostAddress
{
public:
enum NetworkLayerProtocol {
UnknownNetworkLayerProtocol = -1,
AnyIPProtocol,
IPv4Protocol,
IPv6Protocol,
};
QHostAddress() = default;
explicit QHostAddress(const QString &address)
{
setAddress(address);
}
void setAddress(const QString &address)
{
m_address = address;
if (address.isEmpty()) {
m_protocol = UnknownNetworkLayerProtocol;
return;
}
const QByteArray utf8 = address.toUtf8();
unsigned char ipv4[4] = {};
unsigned char ipv6[16] = {};
if (inet_pton(AF_INET, utf8.constData(), ipv4) == 1) {
m_protocol = IPv4Protocol;
return;
}
if (inet_pton(AF_INET6, utf8.constData(), ipv6) == 1) {
m_protocol = IPv6Protocol;
return;
}
m_protocol = UnknownNetworkLayerProtocol;
}
bool isNull() const
{
return m_protocol == UnknownNetworkLayerProtocol;
}
QString toString() const
{
return m_address;
}
NetworkLayerProtocol protocol() const
{
return m_protocol;
}
private:
QString m_address;
NetworkLayerProtocol m_protocol = UnknownNetworkLayerProtocol;
friend QDataStream &operator<<(QDataStream &stream, const QHostAddress &address)
{
stream << address.m_address << static_cast<qint32>(address.m_protocol);
return stream;
}
friend QDataStream &operator>>(QDataStream &stream, QHostAddress &address)
{
qint32 protocol = UnknownNetworkLayerProtocol;
stream >> address.m_address >> protocol;
address.m_protocol = static_cast<QHostAddress::NetworkLayerProtocol>(protocol);
return stream;
}
};
Q_DECLARE_METATYPE(QHostAddress)
@@ -0,0 +1,166 @@
#pragma once
#include <QByteArray>
#include <QList>
#include <QMetaType>
#include <QString>
#include <QStringList>
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
#include <QHostAddress>
class QHostInfo
{
public:
enum HostInfoError {
NoError = 0,
HostNotFound = 1,
UnknownError = 2,
};
QHostInfo() = default;
explicit QHostInfo(const QString &hostName)
: m_hostName(hostName)
{
}
static QString localHostName()
{
char buffer[256] = {};
if (gethostname(buffer, sizeof(buffer)) == 0) {
buffer[sizeof(buffer) - 1] = '\0';
if (buffer[0] != '\0') {
return QString::fromUtf8(buffer);
}
}
return QStringLiteral("redox");
}
static QHostInfo fromName(const QString &hostName)
{
QHostInfo info(hostName);
const QHostAddress literalAddress(hostName);
if (!literalAddress.isNull()) {
info.setAddresses({literalAddress});
info.setError(NoError);
info.setErrorString(QString());
return info;
}
addrinfo hints = {};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
addrinfo *results = nullptr;
const QByteArray hostNameUtf8 = hostName.toUtf8();
const int lookupResult = getaddrinfo(hostNameUtf8.constData(), nullptr, &hints, &results);
if (lookupResult != 0) {
info.setError(lookupResult == EAI_NONAME ? HostNotFound : UnknownError);
info.setErrorString(QString::fromUtf8(gai_strerror(lookupResult)));
return info;
}
QList<QHostAddress> addresses;
QStringList seenAddresses;
for (const addrinfo *entry = results; entry != nullptr; entry = entry->ai_next) {
const void *rawAddress = nullptr;
int family = AF_UNSPEC;
switch (entry->ai_family) {
case AF_INET:
rawAddress = &reinterpret_cast<const sockaddr_in *>(entry->ai_addr)->sin_addr;
family = AF_INET;
break;
case AF_INET6:
rawAddress = &reinterpret_cast<const sockaddr_in6 *>(entry->ai_addr)->sin6_addr;
family = AF_INET6;
break;
default:
continue;
}
char buffer[INET6_ADDRSTRLEN] = {};
if (inet_ntop(family, rawAddress, buffer, sizeof(buffer)) == nullptr) {
continue;
}
const QString addressText = QString::fromUtf8(buffer);
if (addressText.isEmpty() || seenAddresses.contains(addressText)) {
continue;
}
seenAddresses.append(addressText);
addresses.append(QHostAddress(addressText));
}
freeaddrinfo(results);
if (addresses.isEmpty()) {
info.setError(HostNotFound);
info.setErrorString(QStringLiteral("Host lookup returned no usable addresses"));
return info;
}
info.setAddresses(addresses);
info.setError(NoError);
info.setErrorString(QString());
return info;
}
void setHostName(const QString &hostName)
{
m_hostName = hostName;
}
QString hostName() const
{
return m_hostName;
}
void setAddresses(const QList<QHostAddress> &addresses)
{
m_addresses = addresses;
}
QList<QHostAddress> addresses() const
{
return m_addresses;
}
void setError(HostInfoError error)
{
m_error = error;
}
HostInfoError error() const
{
return m_error;
}
void setErrorString(const QString &errorString)
{
m_errorString = errorString;
}
QString errorString() const
{
return m_errorString;
}
private:
QString m_hostName;
QList<QHostAddress> m_addresses;
HostInfoError m_error = UnknownError;
QString m_errorString;
};
Q_DECLARE_METATYPE(QHostInfo)
@@ -9,103 +9,8 @@
#include "commands_p.h"
#include "connection_p.h"
#include "hostinfo.h"
#include "kiocoredebug.h"
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include <QHostInfo>
#include "usernotificationhandler_p.h"
#include "workerbase.h"
@@ -113,6 +18,7 @@
#include <QDataStream>
#include <QDateTime>
#include <QHostInfo>
using namespace KIO;
@@ -364,9 +270,12 @@ bool WorkerInterface::dispatch(int _cmd, const QByteArray &rawdata)
case MSG_HOST_INFO_REQ: {
QString hostName;
stream >> hostName;
const QHostInfo info = HostInfo::lookupHost(hostName, 1500);
QByteArray replyData;
QDataStream replyStream(&replyData, QIODevice::WriteOnly);
replyStream << hostName << QList<QHostAddress>() << int(QHostInfo::UnknownError) << QStringLiteral("Host lookup unavailable on Redox");
replyStream << info.hostName() << info.addresses() << int(info.error()) << info.errorString();
m_connection->send(CMD_HOST_INFO, replyData);
break;
}
@@ -37,6 +37,8 @@ mkdir -pv "$COOKBOOK_STAGE/usr/bin"
mkdir -pv "$COOKBOOK_STAGE/usr/share/redbear/greeter"
cp -v "$COOKBOOK_SOURCE/redbear-greeter-compositor" "$COOKBOOK_STAGE/usr/share/redbear/greeter/redbear-greeter-compositor"
chmod 0755 "$COOKBOOK_STAGE/usr/share/redbear/greeter/redbear-greeter-compositor"
cp -v "$COOKBOOK_SOURCE/redbear-kde-session" "$COOKBOOK_STAGE/usr/bin/redbear-kde-session"
chmod 0755 "$COOKBOOK_STAGE/usr/bin/redbear-kde-session"
cp -v "$COOKBOOK_RECIPE/../../../../local/Assets/images/Red Bear OS loading background.png" "$COOKBOOK_STAGE/usr/share/redbear/greeter/background.png"
cp -v "$COOKBOOK_RECIPE/../../../../local/Assets/images/Red Bear OS icon.png" "$COOKBOOK_STAGE/usr/share/redbear/greeter/icon.png"
cp -v "$COOKBOOK_SOURCE/redbear-greeter-compositor" "$COOKBOOK_STAGE/usr/bin/redbear-greeter-compositor"
@@ -0,0 +1,244 @@
#!/usr/bin/env bash
set -euo pipefail
VALIDATION_REQUEST="/run/redbear-kde-session.validation-request"
VALIDATION_SUCCESS="/run/redbear-kde-session.validation-success"
kwin_pid=""
optional_pids=()
export DESKTOP_SESSION="${DESKTOP_SESSION:-plasmawayland}"
export DISPLAY=""
export KDE_FULL_SESSION="${KDE_FULL_SESSION:-true}"
export KDE_SESSION_VERSION="${KDE_SESSION_VERSION:-6}"
export LIBSEAT_BACKEND="${LIBSEAT_BACKEND:-seatd}"
export LOGNAME="${LOGNAME:-${USER:-root}}"
export PATH="${PATH:-/usr/bin:/bin}"
export QML2_IMPORT_PATH="${QML2_IMPORT_PATH:-/usr/qml}"
export QT_PLUGIN_PATH="${QT_PLUGIN_PATH:-/usr/plugins}"
export QT_QPA_PLATFORM="${QT_QPA_PLATFORM:-wayland}"
export QT_QPA_PLATFORM_PLUGIN_PATH="${QT_QPA_PLATFORM_PLUGIN_PATH:-/usr/plugins/platforms}"
export SEATD_SOCK="${SEATD_SOCK:-/run/seatd.sock}"
export USER="${USER:-root}"
export WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-0}"
export XCURSOR_THEME="${XCURSOR_THEME:-Pop}"
export XDG_CURRENT_DESKTOP="${XDG_CURRENT_DESKTOP:-KDE}"
export XDG_SESSION_DESKTOP="${XDG_SESSION_DESKTOP:-KDE}"
export XDG_SESSION_ID="${XDG_SESSION_ID:-c1}"
export XDG_SESSION_TYPE="${XDG_SESSION_TYPE:-wayland}"
export XKB_CONFIG_ROOT="${XKB_CONFIG_ROOT:-/usr/share/X11/xkb}"
if [ -z "${XDG_RUNTIME_DIR:-}" ]; then
export XDG_RUNTIME_DIR="/tmp/run/user/$(id -u)"
fi
mkdir -p "$XDG_RUNTIME_DIR"
chmod 700 "$XDG_RUNTIME_DIR" 2>/dev/null || true
choose_state_dir() {
local requested="${REDBEAR_KDE_SESSION_STATE_DIR:-}"
if [ -n "$requested" ]; then
mkdir -p "$requested" 2>/dev/null || true
if [ -d "$requested" ] && [ -w "$requested" ]; then
printf '%s\n' "$requested"
return 0
fi
fi
if [ -d /run ] && [ -w /run ]; then
printf '%s\n' "/run"
return 0
fi
printf '%s\n' "$XDG_RUNTIME_DIR"
}
session_state_dir="$(choose_state_dir)"
mkdir -p "$session_state_dir"
chmod 700 "$session_state_dir" 2>/dev/null || true
session_env_file="$session_state_dir/redbear-kde-session.env"
session_ready_file="$session_state_dir/redbear-kde-session.ready"
panel_ready_file="$session_state_dir/redbear-kde-session.panel-ready"
rm -f "$session_ready_file" "$panel_ready_file"
cleanup() {
local status=$?
trap - EXIT INT TERM
for pid in "${optional_pids[@]}"; do
if kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
fi
done
if [ -n "$kwin_pid" ] && kill -0 "$kwin_pid" 2>/dev/null; then
kill "$kwin_pid" 2>/dev/null || true
wait "$kwin_pid" 2>/dev/null || true
fi
exit "$status"
}
trap cleanup EXIT INT TERM
kwin_mode="virtual"
set_kwin_mode() {
local requested="${REDBEAR_KDE_SESSION_BACKEND:-auto}"
case "$requested" in
drm)
if [ -z "${KWIN_DRM_DEVICES:-}" ] && [ -e /scheme/drm/card0 ]; then
export KWIN_DRM_DEVICES=/scheme/drm/card0
fi
if [ -n "${KWIN_DRM_DEVICES:-}" ]; then
kwin_mode="drm"
else
kwin_mode="virtual"
fi
;;
virtual)
kwin_mode="virtual"
;;
auto|"")
if [ -n "${KWIN_DRM_DEVICES:-}" ]; then
kwin_mode="drm"
elif [ -e /scheme/drm/card0 ]; then
export KWIN_DRM_DEVICES=/scheme/drm/card0
kwin_mode="drm"
else
kwin_mode="virtual"
fi
;;
*)
kwin_mode="virtual"
;;
esac
}
set_kwin_mode
if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ] && command -v dbus-launch >/dev/null 2>&1; then
eval "$(dbus-launch --sh-syntax)"
fi
write_session_environment() {
{
printf 'DBUS_SESSION_BUS_ADDRESS=%s\n' "${DBUS_SESSION_BUS_ADDRESS:-}"
printf 'DESKTOP_SESSION=%s\n' "$DESKTOP_SESSION"
printf 'KDE_FULL_SESSION=%s\n' "$KDE_FULL_SESSION"
printf 'KDE_SESSION_VERSION=%s\n' "$KDE_SESSION_VERSION"
printf 'KWIN_DRM_DEVICES=%s\n' "${KWIN_DRM_DEVICES:-}"
printf 'KWIN_MODE=%s\n' "$kwin_mode"
printf 'QML2_IMPORT_PATH=%s\n' "$QML2_IMPORT_PATH"
printf 'QT_PLUGIN_PATH=%s\n' "$QT_PLUGIN_PATH"
printf 'QT_QPA_PLATFORM=%s\n' "$QT_QPA_PLATFORM"
printf 'QT_QPA_PLATFORM_PLUGIN_PATH=%s\n' "$QT_QPA_PLATFORM_PLUGIN_PATH"
printf 'SEATD_SOCK=%s\n' "$SEATD_SOCK"
printf 'SESSION_STATE_DIR=%s\n' "$session_state_dir"
printf 'WAYLAND_DISPLAY=%s\n' "$WAYLAND_DISPLAY"
printf 'XDG_CURRENT_DESKTOP=%s\n' "$XDG_CURRENT_DESKTOP"
printf 'XDG_RUNTIME_DIR=%s\n' "$XDG_RUNTIME_DIR"
printf 'XDG_SESSION_DESKTOP=%s\n' "$XDG_SESSION_DESKTOP"
printf 'XDG_SESSION_ID=%s\n' "$XDG_SESSION_ID"
printf 'XDG_SESSION_TYPE=%s\n' "$XDG_SESSION_TYPE"
printf 'XKB_CONFIG_ROOT=%s\n' "$XKB_CONFIG_ROOT"
} > "$session_env_file"
chmod 600 "$session_env_file" 2>/dev/null || true
}
write_session_environment
if command -v dbus-update-activation-environment >/dev/null 2>&1; then
dbus-update-activation-environment \
DBUS_SESSION_BUS_ADDRESS \
DBUS_SESSION_BUS_PID \
DESKTOP_SESSION \
KDE_FULL_SESSION \
KDE_SESSION_VERSION \
KWIN_DRM_DEVICES \
QML2_IMPORT_PATH \
QT_PLUGIN_PATH \
QT_QPA_PLATFORM \
QT_QPA_PLATFORM_PLUGIN_PATH \
WAYLAND_DISPLAY \
XDG_CURRENT_DESKTOP \
XDG_RUNTIME_DIR \
XDG_SESSION_DESKTOP \
XDG_SESSION_ID \
XDG_SESSION_TYPE \
XKB_CONFIG_ROOT \
XCURSOR_THEME
fi
wait_for_wayland_socket() {
local socket_path="$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY"
local attempts=0
while [ "$attempts" -lt 40 ]; do
if [ -S "$socket_path" ] || [ -e "$socket_path" ]; then
return 0
fi
if [ -n "$kwin_pid" ] && ! kill -0 "$kwin_pid" 2>/dev/null; then
return 1
fi
attempts=$((attempts + 1))
sleep 1
done
return 1
}
mark_validation_success() {
if [ -e "$VALIDATION_REQUEST" ]; then
: > "$VALIDATION_SUCCESS" 2>/dev/null || true
fi
}
launch_optional_component() {
local program="$1"
local ready_marker="$2"
if ! command -v "$program" >/dev/null 2>&1; then
return 0
fi
"$program" &
local pid=$!
optional_pids+=("$pid")
if [ -n "$ready_marker" ]; then
sleep 1
if kill -0 "$pid" 2>/dev/null; then
: > "$ready_marker"
fi
fi
}
kwin_args=()
if [ "$kwin_mode" = "drm" ]; then
kwin_args+=(--drm)
else
kwin_args+=(--virtual)
fi
kwin_wayland_wrapper "${kwin_args[@]}" &
kwin_pid=$!
if ! wait_for_wayland_socket; then
printf '%s\n' "redbear-kde-session: kwin_wayland_wrapper failed to expose $XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" >&2
exit 1
fi
: > "$session_ready_file"
mark_validation_success
launch_optional_component kded6 ""
launch_optional_component plasmashell "$panel_ready_file"
wait "$kwin_pid"
@@ -26,3 +26,4 @@ template = "cargo"
"/usr/bin/redbear-phase3-kwin-check" = "redbear-phase3-kwin-check"
"/usr/bin/redbear-phase4-kde-check" = "redbear-phase4-kde-check"
"/usr/bin/redbear-phase5-gpu-check" = "redbear-phase5-gpu-check"
"/usr/bin/redbear-phase5-cs-check" = "redbear-phase5-cs-check"
@@ -127,6 +127,10 @@ path = "src/bin/redbear-phase4-kde-check.rs"
name = "redbear-phase5-gpu-check"
path = "src/bin/redbear-phase5-gpu-check.rs"
[[bin]]
name = "redbear-phase5-cs-check"
path = "src/bin/redbear-phase5-cs-check.rs"
[dependencies]
redbear-login-protocol = { path = "../../redbear-login-protocol/source" }
serde = { version = "1", features = ["derive"] }
@@ -1,58 +1,129 @@
// Phase 4 KDE Plasma preflight check.
// Validates KF6 library presence, plasma binaries, and session entry points.
// Does NOT validate real KDE Plasma session behavior (blocked on Qt6Quick/QML + real KWin).
// Phase 4 KDE Plasma session check.
// Validates the installed KDE session entry point plus a bounded runtime surface
// exposed by the Red Bear session launcher and helper service.
use std::process;
const PROGRAM: &str = "redbear-phase4-kde-check";
const USAGE: &str = "Usage: redbear-phase4-kde-check [--json]\n\n\
Phase 4 KDE Plasma preflight check. Validates KF6 library and plasma binary\n\
presence. Does NOT validate real KDE session behavior (gated on Qt6Quick/QML).";
Phase 4 KDE Plasma session check. Validates KF6 library presence, the\n\
Red Bear KDE session entry point, KDE session environment capture, core\n\
helper processes, and a basic panel-readiness proxy.";
#[cfg(target_os = "redox")]
use std::{
collections::BTreeMap,
env, fs,
path::{Path, PathBuf},
process::Command,
};
#[cfg(target_os = "redox")]
const REDBEAR_KDE_SESSION_ENV_FILE: &str = "redbear-kde-session.env";
#[cfg(target_os = "redox")]
const REDBEAR_KDE_SESSION_READY_FILE: &str = "redbear-kde-session.ready";
#[cfg(target_os = "redox")]
const REDBEAR_KDE_SESSION_PANEL_READY_FILE: &str = "redbear-kde-session.panel-ready";
#[cfg(target_os = "redox")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CheckResult { Pass, Fail, Skip }
enum CheckResult {
Pass,
Fail,
Skip,
}
#[cfg(target_os = "redox")]
impl CheckResult {
fn label(self) -> &'static str {
match self { Self::Pass => "PASS", Self::Fail => "FAIL", Self::Skip => "SKIP" }
match self {
Self::Pass => "PASS",
Self::Fail => "FAIL",
Self::Skip => "SKIP",
}
}
}
#[cfg(target_os = "redox")]
struct Check { name: String, result: CheckResult, detail: String }
struct Check {
name: String,
result: CheckResult,
detail: String,
}
#[cfg(target_os = "redox")]
impl Check {
fn pass(name: &str, detail: &str) -> Self {
Check { name: name.to_string(), result: CheckResult::Pass, detail: detail.to_string() }
fn pass(name: &str, detail: impl Into<String>) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Pass,
detail: detail.into(),
}
}
fn fail(name: &str, detail: &str) -> Self {
Check { name: name.to_string(), result: CheckResult::Fail, detail: detail.to_string() }
fn fail(name: &str, detail: impl Into<String>) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Fail,
detail: detail.into(),
}
}
fn skip(name: &str, detail: &str) -> Self {
Check { name: name.to_string(), result: CheckResult::Skip, detail: detail.to_string() }
fn skip(name: &str, detail: impl Into<String>) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Skip,
detail: detail.into(),
}
}
}
#[cfg(target_os = "redox")]
struct Report { checks: Vec<Check>, json_mode: bool }
struct Report {
checks: Vec<Check>,
json_mode: bool,
}
#[cfg(target_os = "redox")]
impl Report {
fn new(json_mode: bool) -> Self { Report { checks: Vec::new(), json_mode } }
fn add(&mut self, check: Check) { self.checks.push(check); }
fn any_failed(&self) -> bool { self.checks.iter().any(|c| c.result == CheckResult::Fail) }
fn new(json_mode: bool) -> Self {
Self {
checks: Vec::new(),
json_mode,
}
}
fn add(&mut self, check: Check) {
self.checks.push(check);
}
fn any_failed(&self) -> bool {
self.checks
.iter()
.any(|check| check.result == CheckResult::Fail)
}
fn check_passed(&self, name: &str) -> bool {
self.checks
.iter()
.find(|check| check.name == name)
.is_some_and(|check| check.result == CheckResult::Pass)
}
fn print(&self) {
if self.json_mode { self.print_json(); } else { self.print_human(); }
if self.json_mode {
self.print_json();
} else {
self.print_human();
}
}
fn print_human(&self) {
for check in &self.checks {
let icon = match check.result {
CheckResult::Pass => "[PASS]", CheckResult::Fail => "[FAIL]", CheckResult::Skip => "[SKIP]",
CheckResult::Pass => "[PASS]",
CheckResult::Fail => "[FAIL]",
CheckResult::Skip => "[SKIP]",
};
println!("{icon} {}: {}", check.name, check.detail);
}
@@ -60,129 +131,573 @@ impl Report {
fn print_json(&self) {
#[derive(serde::Serialize)]
struct JsonCheck { name: String, result: String, detail: String }
struct JsonCheck {
name: String,
result: String,
detail: String,
}
#[derive(serde::Serialize)]
struct JsonReport {
kf6_libs_present: bool, plasma_binaries_present: bool,
session_entry: bool, kirigami_available: bool, checks: Vec<JsonCheck>,
overall_success: bool,
kf6_libs_present: bool,
plasma_binaries_present: bool,
session_entry: bool,
session_environment: bool,
plasmashell_process: bool,
kded6_process: bool,
panel_rendering_ready: bool,
kirigami_available: bool,
checks: Vec<JsonCheck>,
}
let kf6_libs = self.checks.iter().find(|c| c.name == "KF6_LIBRARIES").map_or(false, |c| c.result == CheckResult::Pass);
let plasma_bins = self.checks.iter().find(|c| c.name == "PLASMA_BINARIES").map_or(false, |c| c.result == CheckResult::Pass);
let session_entry = self.checks.iter().find(|c| c.name == "SESSION_ENTRY").map_or(false, |c| c.result == CheckResult::Pass);
let kirigami = self.checks.iter().find(|c| c.name == "KIRIGAMI_STATUS").map_or(false, |c| c.result == CheckResult::Pass);
let checks: Vec<JsonCheck> = self.checks.iter().map(|c| JsonCheck {
name: c.name.clone(), result: c.result.label().to_string(), detail: c.detail.clone(),
}).collect();
if let Err(err) = serde_json::to_writer(std::io::stdout(), &JsonReport { kf6_libs_present: kf6_libs, plasma_binaries_present: plasma_bins, session_entry, kirigami_available: kirigami, checks }) {
let checks = self
.checks
.iter()
.map(|check| JsonCheck {
name: check.name.clone(),
result: check.result.label().to_string(),
detail: check.detail.clone(),
})
.collect::<Vec<_>>();
let report = JsonReport {
overall_success: !self.any_failed(),
kf6_libs_present: self.check_passed("KF6_LIBRARIES"),
plasma_binaries_present: self.check_passed("PLASMA_BINARIES"),
session_entry: self.check_passed("SESSION_ENTRY"),
session_environment: self.check_passed("SESSION_ENVIRONMENT"),
plasmashell_process: self.check_passed("PLASMASHELL_PROCESS"),
kded6_process: self.check_passed("KDED6_PROCESS"),
panel_rendering_ready: self.check_passed("PANEL_RENDERING_READY"),
kirigami_available: self.check_passed("KIRIGAMI_STATUS"),
checks,
};
if let Err(err) = serde_json::to_writer(std::io::stdout(), &report) {
eprintln!("{PROGRAM}: failed to serialize JSON: {err}");
}
}
}
#[cfg(target_os = "redox")]
#[derive(Clone, Debug)]
struct SessionEnvironment {
source: String,
values: BTreeMap<String, String>,
}
#[cfg(target_os = "redox")]
fn parse_args() -> Result<bool, String> {
let mut json_mode = false;
for arg in std::env::args().skip(1) {
match arg.as_str() {
"--json" => json_mode = true,
"-h" | "--help" => { println!("{USAGE}"); return Err(String::new()); }
"-h" | "--help" => {
println!("{USAGE}");
return Err(String::new());
}
_ => return Err(format!("unsupported argument: {arg}")),
}
}
Ok(json_mode)
}
#[cfg(target_os = "redox")]
fn check_kf6_libraries() -> Check {
let key_libs = [
"/usr/lib/libKF6CoreAddons.so", "/usr/lib/libKF6ConfigCore.so",
"/usr/lib/libKF6I18n.so", "/usr/lib/libKF6WindowSystem.so",
"/usr/lib/libKF6Notifications.so", "/usr/lib/libKF6Service.so",
"/usr/lib/libKF6CoreAddons.so",
"/usr/lib/libKF6ConfigCore.so",
"/usr/lib/libKF6I18n.so",
"/usr/lib/libKF6WindowSystem.so",
"/usr/lib/libKF6Notifications.so",
"/usr/lib/libKF6Service.so",
"/usr/lib/libKF6WaylandClient.so",
];
let mut found = 0usize;
let mut missing = Vec::new();
for lib in key_libs {
if std::path::Path::new(lib).exists() {
if Path::new(lib).exists() {
found += 1;
} else {
missing.push(lib);
}
}
if found >= 6 {
let preview: Vec<_> = missing.iter().take(3).map(|s| s.rsplit('/').next().unwrap_or(s)).collect();
if missing.is_empty() {
Check::pass("KF6_LIBRARIES", &format!("{}/{} key KF6 libs found", found, key_libs.len()))
Check::pass(
"KF6_LIBRARIES",
format!("{found}/{} key KF6 libraries found", key_libs.len()),
)
} else {
Check::pass("KF6_LIBRARIES", &format!("{}/{} found, missing: {}", found, key_libs.len(), preview.join(", ")))
let preview = missing
.iter()
.take(3)
.map(|path| path.rsplit('/').next().unwrap_or(path))
.collect::<Vec<_>>()
.join(", ");
Check::pass(
"KF6_LIBRARIES",
format!("{found}/{} found, missing: {preview}", key_libs.len()),
)
}
} else {
Check::fail("KF6_LIBRARIES", &format!("only {}/{} key KF6 libs found", found, key_libs.len()))
Check::fail(
"KF6_LIBRARIES",
format!("only {found}/{} key KF6 libraries found", key_libs.len()),
)
}
}
#[cfg(target_os = "redox")]
fn check_plasma_binaries() -> Check {
let bins = ["/usr/bin/plasmashell", "/usr/bin/systemsettings", "/usr/bin/kwin_wayland_wrapper"];
let mut found = 0usize;
for bin in bins {
if std::path::Path::new(bin).exists() { found += 1; }
}
if found >= 2 {
Check::pass("PLASMA_BINARIES", &format!("{}/{} plasma binaries present", found, bins.len()))
} else if found == 1 {
Check::fail("PLASMA_BINARIES", &format!("only {}/{} plasma binaries present", found, bins.len()))
} else {
Check::fail("PLASMA_BINARIES", "no plasma binaries found")
let required = [
"/usr/bin/redbear-kde-session",
"/usr/bin/kwin_wayland_wrapper",
"/usr/bin/plasmashell",
"/usr/bin/kded6",
];
let optional: &[&str] = &[];
let missing_required = required
.iter()
.copied()
.filter(|path| !Path::new(path).exists())
.collect::<Vec<_>>();
if !missing_required.is_empty() {
return Check::fail(
"PLASMA_BINARIES",
format!(
"missing required session binaries: {}",
missing_required.join(", ")
),
);
}
let found_optional = optional
.iter()
.copied()
.filter(|path| Path::new(path).exists())
.collect::<Vec<_>>();
Check::pass(
"PLASMA_BINARIES",
format!(
"required session binaries present; optional helpers found: {}/{}",
found_optional.len(),
optional.len()
),
)
}
#[cfg(target_os = "redox")]
fn check_session_entry() -> Check {
let entries = ["/usr/bin/startplasma-wayland", "/usr/lib/plasma-session"];
for e in entries {
if std::path::Path::new(e).exists() {
return Check::pass("SESSION_ENTRY", e);
let entry = "/usr/bin/redbear-kde-session";
if Path::new(entry).exists() {
Check::pass("SESSION_ENTRY", entry)
} else {
Check::fail("SESSION_ENTRY", "missing /usr/bin/redbear-kde-session")
}
}
#[cfg(target_os = "redox")]
fn env_value(name: &str) -> Option<String> {
env::var(name).ok().filter(|value| !value.trim().is_empty())
}
#[cfg(target_os = "redox")]
fn candidate_state_dirs() -> Vec<PathBuf> {
let mut dirs = vec![
PathBuf::from("/run"),
PathBuf::from("/run/redbear-display-session"),
];
if let Some(dir) = env_value("XDG_RUNTIME_DIR") {
let runtime_dir = PathBuf::from(dir);
if !dirs.contains(&runtime_dir) {
dirs.push(runtime_dir);
}
}
Check::fail("SESSION_ENTRY", "no KDE session entry point found")
dirs
}
#[cfg(target_os = "redox")]
fn candidate_state_files(file_name: &str) -> Vec<PathBuf> {
candidate_state_dirs()
.into_iter()
.map(|dir| dir.join(file_name))
.collect::<Vec<_>>()
}
#[cfg(target_os = "redox")]
fn parse_key_value_file(path: &Path) -> Result<BTreeMap<String, String>, String> {
let contents = fs::read_to_string(path)
.map_err(|err| format!("failed to read {}: {err}", path.display()))?;
let mut values = BTreeMap::new();
for raw_line in contents.lines() {
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
values.insert(key.to_string(), value.to_string());
}
}
Ok(values)
}
#[cfg(target_os = "redox")]
fn load_session_environment() -> Result<SessionEnvironment, String> {
for path in candidate_state_files(REDBEAR_KDE_SESSION_ENV_FILE) {
if path.exists() {
let values = parse_key_value_file(&path)?;
return Ok(SessionEnvironment {
source: path.display().to_string(),
values,
});
}
}
let mut values = BTreeMap::new();
for key in [
"XDG_SESSION_TYPE",
"XDG_CURRENT_DESKTOP",
"KDE_FULL_SESSION",
"QT_PLUGIN_PATH",
"QT_QPA_PLATFORM_PLUGIN_PATH",
"QML2_IMPORT_PATH",
"WAYLAND_DISPLAY",
"XDG_RUNTIME_DIR",
] {
if let Some(value) = env_value(key) {
values.insert(key.to_string(), value);
}
}
if values.is_empty() {
let paths = candidate_state_files(REDBEAR_KDE_SESSION_ENV_FILE)
.into_iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join(", ");
Err(format!("no KDE session environment file found in: {paths}"))
} else {
Ok(SessionEnvironment {
source: String::from("current process environment"),
values,
})
}
}
#[cfg(target_os = "redox")]
fn check_required_env_value(
values: &BTreeMap<String, String>,
key: &str,
expected: &str,
) -> Result<(), String> {
match values.get(key) {
Some(value) if value == expected => Ok(()),
Some(value) => Err(format!("{key}={value} (expected {expected})")),
None => Err(format!("missing {key}")),
}
}
#[cfg(target_os = "redox")]
fn check_nonempty_env_value(values: &BTreeMap<String, String>, key: &str) -> Result<(), String> {
match values.get(key) {
Some(value) if !value.trim().is_empty() => Ok(()),
Some(_) => Err(format!("{key} is empty")),
None => Err(format!("missing {key}")),
}
}
#[cfg(target_os = "redox")]
fn check_session_environment() -> Check {
match load_session_environment() {
Ok(session) => {
let checks = [
check_required_env_value(&session.values, "XDG_SESSION_TYPE", "wayland"),
check_required_env_value(&session.values, "XDG_CURRENT_DESKTOP", "KDE"),
check_required_env_value(&session.values, "KDE_FULL_SESSION", "true"),
check_nonempty_env_value(&session.values, "QT_PLUGIN_PATH"),
check_nonempty_env_value(&session.values, "QT_QPA_PLATFORM_PLUGIN_PATH"),
check_nonempty_env_value(&session.values, "QML2_IMPORT_PATH"),
];
let failures = checks
.into_iter()
.filter_map(Result::err)
.collect::<Vec<_>>();
if failures.is_empty() {
Check::pass(
"SESSION_ENVIRONMENT",
format!("captured KDE session environment from {}", session.source),
)
} else {
Check::fail(
"SESSION_ENVIRONMENT",
format!(
"invalid KDE session environment from {}: {}",
session.source,
failures.join("; ")
),
)
}
}
Err(err) => Check::fail("SESSION_ENVIRONMENT", err),
}
}
#[cfg(target_os = "redox")]
fn run_command(program: &str, args: &[&str], label: &str) -> Result<String, String> {
let output = Command::new(program)
.args(args)
.output()
.map_err(|err| format!("failed to run {label}: {err}"))?;
if !output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let detail = if !stderr.trim().is_empty() {
stderr.trim().to_string()
} else if !stdout.trim().is_empty() {
stdout.trim().to_string()
} else {
String::from("no output")
};
return Err(format!(
"{label} exited with status {}: {detail}",
output.status
));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
#[cfg(target_os = "redox")]
fn process_output() -> Result<String, String> {
run_command("ps", &[], "ps")
}
#[cfg(target_os = "redox")]
fn output_has_process(output: &str, process_name: &str) -> bool {
output.lines().any(|line| line.contains(process_name))
}
#[cfg(target_os = "redox")]
fn check_required_process(process_name: &str, binary_path: &str, check_name: &str) -> Check {
if !Path::new(binary_path).exists() {
return Check::fail(check_name, format!("{binary_path} is not installed"));
}
match process_output() {
Ok(output) => {
if output_has_process(&output, process_name) {
Check::pass(check_name, format!("{process_name} appears in ps output"))
} else {
Check::fail(
check_name,
format!("{process_name} is not present in ps output"),
)
}
}
Err(err) => Check::fail(check_name, err),
}
}
#[cfg(target_os = "redox")]
fn first_existing_state_file(file_name: &str) -> Option<PathBuf> {
candidate_state_files(file_name)
.into_iter()
.find(|path| path.exists())
}
#[cfg(target_os = "redox")]
fn wayland_socket_from_session_env(values: &BTreeMap<String, String>) -> Option<PathBuf> {
let runtime_dir = values.get("XDG_RUNTIME_DIR")?;
let display = values.get("WAYLAND_DISPLAY")?;
Some(PathBuf::from(runtime_dir).join(display))
}
#[cfg(target_os = "redox")]
fn check_panel_rendering_readiness() -> Check {
if !Path::new("/usr/bin/plasmashell").exists() {
return Check::skip(
"PANEL_RENDERING_READY",
"plasmashell is not installed, panel readiness cannot be checked",
);
}
if let Some(path) = first_existing_state_file(REDBEAR_KDE_SESSION_PANEL_READY_FILE) {
return Check::pass(
"PANEL_RENDERING_READY",
format!("panel readiness marker present at {}", path.display()),
);
}
let session = match load_session_environment() {
Ok(session) => session,
Err(err) => return Check::fail("PANEL_RENDERING_READY", err),
};
let socket_path = match wayland_socket_from_session_env(&session.values) {
Some(path) => path,
None => {
return Check::fail(
"PANEL_RENDERING_READY",
"session environment is missing XDG_RUNTIME_DIR or WAYLAND_DISPLAY",
);
}
};
let processes = match process_output() {
Ok(output) => output,
Err(err) => return Check::fail("PANEL_RENDERING_READY", err),
};
if output_has_process(&processes, "plasmashell") && socket_path.exists() {
Check::pass(
"PANEL_RENDERING_READY",
format!(
"plasmashell is running and Wayland socket is present at {}",
socket_path.display()
),
)
} else {
Check::fail(
"PANEL_RENDERING_READY",
format!(
"missing panel marker and runtime proxy (plasmashell process/socket {})",
socket_path.display()
),
)
}
}
#[cfg(target_os = "redox")]
fn check_session_ready_marker() -> Check {
if let Some(path) = first_existing_state_file(REDBEAR_KDE_SESSION_READY_FILE) {
Check::pass(
"SESSION_READY_MARKER",
format!("session readiness marker present at {}", path.display()),
)
} else {
let paths = candidate_state_files(REDBEAR_KDE_SESSION_READY_FILE)
.into_iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join(", ");
Check::fail(
"SESSION_READY_MARKER",
format!("no readiness marker found in: {paths}"),
)
}
}
#[cfg(target_os = "redox")]
fn check_kirigami_status() -> Check {
let kirigami_lib = "/usr/lib/libKF6Kirigami.so";
if std::path::Path::new(kirigami_lib).exists() {
if Path::new(kirigami_lib).exists() {
Check::pass("KIRIGAMI_STATUS", "kirigami library present")
} else {
Check::skip("KIRIGAMI_STATUS", "kirigami not available (QML stub, requires Qt6Quick)")
Check::skip(
"KIRIGAMI_STATUS",
"kirigami not available (QML stub, requires Qt6Quick)",
)
}
}
fn run() -> Result<(), String> {
#[cfg(not(target_os = "redox"))]
{
if std::env::args().any(|a| a == "-h" || a == "--help") { println!("{USAGE}"); return Err(String::new()); }
if std::env::args().any(|arg| arg == "-h" || arg == "--help") {
println!("{USAGE}");
return Err(String::new());
}
println!("{PROGRAM}: KDE Plasma check requires Redox runtime");
return Ok(());
}
#[cfg(target_os = "redox")]
{
let json_mode = parse_args()?;
let mut report = Report::new(json_mode);
report.add(check_kf6_libraries());
report.add(check_plasma_binaries());
report.add(check_session_entry());
report.add(check_session_environment());
report.add(check_session_ready_marker());
report.add(check_required_process(
"plasmashell",
"/usr/bin/plasmashell",
"PLASMASHELL_PROCESS",
));
report.add(check_required_process(
"kded6",
"/usr/bin/kded6",
"KDED6_PROCESS",
));
report.add(check_panel_rendering_readiness());
report.add(check_kirigami_status());
report.print();
if report.any_failed() { return Err("one or more Phase 4 checks failed".to_string()); }
if report.any_failed() {
return Err(String::from("one or more Phase 4 KDE checks failed"));
}
Ok(())
}
}
fn main() {
if let Err(err) = run() {
if err.is_empty() { process::exit(0); }
if err.is_empty() {
process::exit(0);
}
eprintln!("{PROGRAM}: {err}");
process::exit(1);
}
}
#[cfg(all(test, target_os = "redox"))]
mod tests {
use super::*;
#[test]
fn parse_key_value_file_collects_session_values() {
let temp_dir = std::env::temp_dir().join("redbear-phase4-kde-check-tests");
fs::create_dir_all(&temp_dir).expect("temp dir should be created");
let path = temp_dir.join("env.txt");
fs::write(
&path,
"XDG_SESSION_TYPE=wayland\nKDE_FULL_SESSION=true\nQML2_IMPORT_PATH=/usr/qml\n",
)
.expect("env file should be written");
let parsed = parse_key_value_file(&path).expect("env file should parse");
assert_eq!(
parsed.get("XDG_SESSION_TYPE"),
Some(&String::from("wayland"))
);
assert_eq!(parsed.get("KDE_FULL_SESSION"), Some(&String::from("true")));
assert_eq!(
parsed.get("QML2_IMPORT_PATH"),
Some(&String::from("/usr/qml"))
);
}
#[test]
fn check_required_env_value_matches_expected_value() {
let mut values = BTreeMap::new();
values.insert(String::from("XDG_SESSION_TYPE"), String::from("wayland"));
assert!(check_required_env_value(&values, "XDG_SESSION_TYPE", "wayland").is_ok());
assert!(check_required_env_value(&values, "XDG_SESSION_TYPE", "x11").is_err());
}
}
@@ -0,0 +1,673 @@
// Phase 5 GPU command-submission validation checker.
// Validates DRM command-submission protocol reachability over /scheme/drm/card0.
// Does NOT claim real hardware render validation yet.
use std::process;
const PROGRAM: &str = "redbear-phase5-cs-check";
const USAGE: &str = "Usage: redbear-phase5-cs-check [--json]\n\n\
Phase 5 GPU command-submission validation. Probes DRM private CS ioctls,\n\
PRIME buffer sharing, GEM allocation, and fence/wait support. Real\n\
hardware rendering validation is still pending.";
#[cfg(target_os = "redox")]
const DRM_IOCTL_BASE: usize = 0x00A0;
#[cfg(target_os = "redox")]
const DRM_IOCTL_GEM_CREATE: usize = DRM_IOCTL_BASE + 26;
#[cfg(target_os = "redox")]
const DRM_IOCTL_GEM_CLOSE: usize = DRM_IOCTL_BASE + 27;
#[cfg(target_os = "redox")]
const DRM_IOCTL_PRIME_HANDLE_TO_FD: usize = DRM_IOCTL_BASE + 29;
#[cfg(target_os = "redox")]
const DRM_IOCTL_PRIME_FD_TO_HANDLE: usize = DRM_IOCTL_BASE + 30;
#[cfg(target_os = "redox")]
const DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT: usize = DRM_IOCTL_BASE + 31;
#[cfg(target_os = "redox")]
const DRM_IOCTL_REDOX_PRIVATE_CS_WAIT: usize = DRM_IOCTL_BASE + 32;
#[cfg(target_os = "redox")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CheckResult {
Pass,
Fail,
Skip,
}
#[cfg(target_os = "redox")]
impl CheckResult {
fn label(self) -> &'static str {
match self {
Self::Pass => "PASS",
Self::Fail => "FAIL",
Self::Skip => "SKIP",
}
}
}
#[cfg(target_os = "redox")]
struct Check {
name: String,
result: CheckResult,
detail: String,
}
#[cfg(target_os = "redox")]
impl Check {
fn pass(name: &str, detail: &str) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Pass,
detail: detail.to_string(),
}
}
fn fail(name: &str, detail: &str) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Fail,
detail: detail.to_string(),
}
}
fn skip(name: &str, detail: &str) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Skip,
detail: detail.to_string(),
}
}
}
#[cfg(target_os = "redox")]
struct Report {
checks: Vec<Check>,
json_mode: bool,
}
#[cfg(target_os = "redox")]
impl Report {
fn new(json_mode: bool) -> Self {
Self {
checks: Vec::new(),
json_mode,
}
}
fn add(&mut self, check: Check) {
self.checks.push(check);
}
fn any_failed(&self) -> bool {
self.checks.iter().any(|check| check.result == CheckResult::Fail)
}
fn print(&self) {
if self.json_mode {
self.print_json();
} else {
self.print_human();
}
}
fn print_human(&self) {
for check in &self.checks {
let icon = match check.result {
CheckResult::Pass => "[PASS]",
CheckResult::Fail => "[FAIL]",
CheckResult::Skip => "[SKIP]",
};
println!("{icon} {}: {}", check.name, check.detail);
}
}
fn print_json(&self) {
#[derive(serde::Serialize)]
struct JsonCheck {
name: String,
result: String,
detail: String,
}
#[derive(serde::Serialize)]
struct JsonReport {
command_submission_protocol: bool,
prime_buffer_sharing: bool,
gem_buffer_allocation: bool,
fence_sync_support: bool,
hardware_validation_pending: bool,
checks: Vec<JsonCheck>,
}
let check_passed = |name: &str| {
self.checks
.iter()
.find(|check| check.name == name)
.is_some_and(|check| check.result == CheckResult::Pass)
};
let checks = self
.checks
.iter()
.map(|check| JsonCheck {
name: check.name.clone(),
result: check.result.label().to_string(),
detail: check.detail.clone(),
})
.collect::<Vec<_>>();
if let Err(err) = serde_json::to_writer(
std::io::stdout(),
&JsonReport {
command_submission_protocol: check_passed("CS_IOCTL_PROTOCOL"),
prime_buffer_sharing: check_passed("PRIME_BUFFER_SHARING"),
gem_buffer_allocation: check_passed("GEM_BUFFER_ALLOCATION"),
fence_sync_support: check_passed("FENCE_SYNC_SUPPORT"),
hardware_validation_pending: true,
checks,
},
) {
eprintln!("{PROGRAM}: failed to serialize JSON: {err}");
}
}
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmGemCreateWire {
size: u64,
handle: u32,
pad: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmGemCloseWire {
handle: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmPrimeHandleToFdWire {
handle: u32,
flags: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmPrimeHandleToFdResponseWire {
fd: i32,
pad: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmPrimeFdToHandleWire {
fd: i32,
pad: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmPrimeFdToHandleResponseWire {
handle: u32,
pad: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct RedoxPrivateCsSubmit {
src_handle: u32,
dst_handle: u32,
src_offset: u64,
dst_offset: u64,
byte_count: u64,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct RedoxPrivateCsSubmitResult {
seqno: u64,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct RedoxPrivateCsWait {
seqno: u64,
timeout_ns: u64,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct RedoxPrivateCsWaitResult {
completed: u8,
pad: [u8; 7],
completed_seqno: u64,
}
#[cfg(target_os = "redox")]
fn parse_args() -> Result<bool, String> {
let mut json_mode = false;
for arg in std::env::args().skip(1) {
match arg.as_str() {
"--json" => json_mode = true,
"-h" | "--help" => {
println!("{USAGE}");
return Err(String::new());
}
_ => return Err(format!("unsupported argument: {arg}")),
}
}
Ok(json_mode)
}
#[cfg(target_os = "redox")]
fn decode_wire_exact<T: Copy>(bytes: &[u8]) -> Result<T, String> {
use std::mem::{MaybeUninit, size_of};
if bytes.len() != size_of::<T>() {
return Err(format!(
"unexpected DRM response size: expected {} bytes, got {}",
size_of::<T>(),
bytes.len()
));
}
let mut out = MaybeUninit::<T>::uninit();
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), out.as_mut_ptr().cast::<u8>(), size_of::<T>());
Ok(out.assume_init())
}
}
#[cfg(target_os = "redox")]
fn bytes_of<T>(value: &T) -> &[u8] {
unsafe {
std::slice::from_raw_parts(
(value as *const T).cast::<u8>(),
std::mem::size_of::<T>(),
)
}
}
#[cfg(target_os = "redox")]
fn open_drm_card(path: &str) -> Result<std::fs::File, String> {
std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
.map_err(|err| format!("failed to open {path}: {err}"))
}
#[cfg(target_os = "redox")]
fn drm_query(file: &mut std::fs::File, request: usize, payload: &[u8]) -> Result<Vec<u8>, String> {
use std::io::{Read, Write};
let mut request_buf = request.to_le_bytes().to_vec();
request_buf.extend_from_slice(payload);
file.write_all(&request_buf)
.map_err(|err| format!("failed to send DRM ioctl {request:#x}: {err}"))?;
let mut response = vec![0u8; 4096];
let len = file
.read(&mut response)
.map_err(|err| format!("failed to read DRM ioctl {request:#x} response: {err}"))?;
response.truncate(len);
Ok(response)
}
#[cfg(target_os = "redox")]
fn close_gem(file: &mut std::fs::File, handle: u32) {
let request = DrmGemCloseWire { handle };
let _ = drm_query(file, DRM_IOCTL_GEM_CLOSE, bytes_of(&request));
}
#[cfg(target_os = "redox")]
fn run_redox(json_mode: bool) -> Result<(), String> {
let mut report = Report::new(json_mode);
let card_path = "/scheme/drm/card0";
if !std::path::Path::new(card_path).exists() {
report.add(Check::fail(
"CS_IOCTL_PROTOCOL",
"/scheme/drm/card0 missing; cannot probe command submission",
));
report.add(Check::skip(
"GEM_BUFFER_ALLOCATION",
"blocked: DRM card is unavailable",
));
report.add(Check::skip(
"PRIME_BUFFER_SHARING",
"blocked: DRM card is unavailable",
));
report.add(Check::skip(
"FENCE_SYNC_SUPPORT",
"blocked: DRM card is unavailable",
));
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"real hardware rendering validation still requires bare-metal evidence",
));
report.print();
return Err("one or more Phase 5 CS checks failed".to_string());
}
let mut exporter = match open_drm_card(card_path) {
Ok(file) => file,
Err(err) => {
report.add(Check::fail("CS_IOCTL_PROTOCOL", &err));
report.add(Check::skip(
"GEM_BUFFER_ALLOCATION",
"blocked: DRM card could not be opened",
));
report.add(Check::skip(
"PRIME_BUFFER_SHARING",
"blocked: DRM card could not be opened",
));
report.add(Check::skip(
"FENCE_SYNC_SUPPORT",
"blocked: DRM card could not be opened",
));
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"real hardware rendering validation still requires bare-metal evidence",
));
report.print();
return Err("one or more Phase 5 CS checks failed".to_string());
}
};
let mut importer = match open_drm_card(card_path) {
Ok(file) => file,
Err(err) => {
report.add(Check::fail("CS_IOCTL_PROTOCOL", &format!("opened exporter but importer failed: {err}")));
report.add(Check::skip(
"GEM_BUFFER_ALLOCATION",
"blocked: second DRM handle could not be opened",
));
report.add(Check::skip(
"PRIME_BUFFER_SHARING",
"blocked: second DRM handle could not be opened",
));
report.add(Check::skip(
"FENCE_SYNC_SUPPORT",
"blocked: second DRM handle could not be opened",
));
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"real hardware rendering validation still requires bare-metal evidence",
));
report.print();
return Err("one or more Phase 5 CS checks failed".to_string());
}
};
let mut exporter_handle = None;
let mut importer_src_handle = None;
let mut importer_dst_handle = None;
let create_exporter = DrmGemCreateWire {
size: 4096,
..DrmGemCreateWire::default()
};
match drm_query(&mut exporter, DRM_IOCTL_GEM_CREATE, bytes_of(&create_exporter))
.and_then(|response| decode_wire_exact::<DrmGemCreateWire>(&response))
{
Ok(created) => {
exporter_handle = Some(created.handle);
report.add(Check::pass(
"GEM_BUFFER_ALLOCATION",
&format!("allocated exporter GEM handle {} (4096 bytes)", created.handle),
));
}
Err(err) => {
report.add(Check::fail("GEM_BUFFER_ALLOCATION", &err));
report.add(Check::skip(
"PRIME_BUFFER_SHARING",
"blocked: GEM allocation failed",
));
report.add(Check::skip(
"CS_IOCTL_PROTOCOL",
"blocked: GEM allocation failed",
));
report.add(Check::skip(
"FENCE_SYNC_SUPPORT",
"blocked: GEM allocation failed",
));
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"real hardware rendering validation still requires bare-metal evidence",
));
report.print();
return Err("one or more Phase 5 CS checks failed".to_string());
}
}
if let Some(handle) = exporter_handle {
let export = DrmPrimeHandleToFdWire { handle, flags: 0 };
let prime_result = drm_query(&mut exporter, DRM_IOCTL_PRIME_HANDLE_TO_FD, bytes_of(&export))
.and_then(|response| decode_wire_exact::<DrmPrimeHandleToFdResponseWire>(&response))
.and_then(|exported| {
if exported.fd < 0 {
return Err(format!(
"PRIME export returned invalid token {} for GEM {}",
exported.fd, handle
));
}
let import = DrmPrimeFdToHandleWire {
fd: exported.fd,
pad: 0,
};
drm_query(&mut importer, DRM_IOCTL_PRIME_FD_TO_HANDLE, bytes_of(&import))
.and_then(|response| decode_wire_exact::<DrmPrimeFdToHandleResponseWire>(&response))
.map(|imported| (exported.fd, imported.handle))
});
match prime_result {
Ok((token, imported_handle)) => {
importer_src_handle = Some(imported_handle);
report.add(Check::pass(
"PRIME_BUFFER_SHARING",
&format!(
"export token {} imported as GEM handle {} on a second DRM fd",
token, imported_handle
),
));
}
Err(err) => {
report.add(Check::fail("PRIME_BUFFER_SHARING", &err));
report.add(Check::skip(
"CS_IOCTL_PROTOCOL",
"blocked: PRIME import/export failed",
));
report.add(Check::skip(
"FENCE_SYNC_SUPPORT",
"blocked: PRIME import/export failed",
));
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"real hardware rendering validation still requires bare-metal evidence",
));
close_gem(&mut exporter, handle);
report.print();
return Err("one or more Phase 5 CS checks failed".to_string());
}
}
}
let create_importer = DrmGemCreateWire {
size: 4096,
..DrmGemCreateWire::default()
};
match drm_query(&mut importer, DRM_IOCTL_GEM_CREATE, bytes_of(&create_importer))
.and_then(|response| decode_wire_exact::<DrmGemCreateWire>(&response))
{
Ok(created) => importer_dst_handle = Some(created.handle),
Err(err) => {
report.add(Check::fail(
"CS_IOCTL_PROTOCOL",
&format!("secondary GEM allocation for CS submit failed: {err}"),
));
report.add(Check::skip(
"FENCE_SYNC_SUPPORT",
"blocked: no destination GEM for CS submit",
));
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"real hardware rendering validation still requires bare-metal evidence",
));
if let Some(handle) = importer_src_handle {
close_gem(&mut importer, handle);
}
if let Some(handle) = exporter_handle {
close_gem(&mut exporter, handle);
}
report.print();
return Err("one or more Phase 5 CS checks failed".to_string());
}
}
let submit_result = match (importer_src_handle, importer_dst_handle) {
(Some(src_handle), Some(dst_handle)) => {
let submit = RedoxPrivateCsSubmit {
src_handle,
dst_handle,
src_offset: 0,
dst_offset: 0,
byte_count: 64,
};
drm_query(
&mut importer,
DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT,
bytes_of(&submit),
)
.and_then(|response| decode_wire_exact::<RedoxPrivateCsSubmitResult>(&response))
.map(|result| (src_handle, dst_handle, result.seqno))
}
_ => Err("command submission prerequisites were incomplete".to_string()),
};
match submit_result {
Ok((src_handle, dst_handle, seqno)) => {
report.add(Check::pass(
"CS_IOCTL_PROTOCOL",
&format!(
"private CS submit accepted shared GEM {} -> local GEM {} (seqno {})",
src_handle, dst_handle, seqno
),
));
let wait = RedoxPrivateCsWait {
seqno,
timeout_ns: 0,
};
match drm_query(
&mut importer,
DRM_IOCTL_REDOX_PRIVATE_CS_WAIT,
bytes_of(&wait),
)
.and_then(|response| decode_wire_exact::<RedoxPrivateCsWaitResult>(&response))
{
Ok(wait_result) => {
let completed = match wait_result.completed {
0 => false,
1 => true,
value => {
report.add(Check::fail(
"FENCE_SYNC_SUPPORT",
&format!(
"wait ioctl returned invalid completion flag {} for seqno {}",
value, seqno
),
));
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"protocol-level CS proof exists, but real hardware rendering validation is still pending",
));
report.print();
return Err("one or more Phase 5 CS checks failed".to_string());
}
};
report.add(Check::pass(
"FENCE_SYNC_SUPPORT",
&format!(
"bounded wait ioctl responded for seqno {} (completed={}, completed_seqno={}); real sync-object validation is still pending",
seqno, completed, wait_result.completed_seqno
),
));
}
Err(err) => {
report.add(Check::fail("FENCE_SYNC_SUPPORT", &err));
}
}
}
Err(err) => {
report.add(Check::fail("CS_IOCTL_PROTOCOL", &err));
report.add(Check::skip(
"FENCE_SYNC_SUPPORT",
"blocked: command submission ioctl failed",
));
}
}
if let Some(handle) = importer_dst_handle {
close_gem(&mut importer, handle);
}
if let Some(handle) = importer_src_handle {
close_gem(&mut importer, handle);
}
if let Some(handle) = exporter_handle {
close_gem(&mut exporter, handle);
}
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"protocol-level CS proof exists, but real hardware rendering validation is still pending",
));
report.print();
if report.any_failed() {
return Err("one or more Phase 5 CS checks failed".to_string());
}
Ok(())
}
fn run() -> Result<(), String> {
#[cfg(not(target_os = "redox"))]
{
if std::env::args().any(|arg| arg == "-h" || arg == "--help") {
println!("{USAGE}");
return Err(String::new());
}
println!("{PROGRAM}: CS check requires Redox runtime");
Ok(())
}
#[cfg(target_os = "redox")]
{
let json_mode = parse_args()?;
run_redox(json_mode)
}
}
fn main() {
if let Err(err) = run() {
if err.is_empty() {
process::exit(0);
}
eprintln!("{PROGRAM}: {err}");
process::exit(1);
}
}
@@ -10,6 +10,15 @@ const USAGE: &str = "Usage: redbear-phase5-gpu-check [--json]\n\n\
GPU firmware, and Mesa rendering infrastructure. Hardware validation\n\
requires real AMD/Intel GPU + command submission (CS ioctl).";
#[cfg(target_os = "redox")]
const DRM_IOCTL_BASE: usize = 0x00A0;
#[cfg(target_os = "redox")]
const DRM_IOCTL_GEM_CREATE: usize = DRM_IOCTL_BASE + 26;
#[cfg(target_os = "redox")]
const DRM_IOCTL_GEM_CLOSE: usize = DRM_IOCTL_BASE + 27;
#[cfg(target_os = "redox")]
const DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT: usize = DRM_IOCTL_BASE + 31;
#[cfg(target_os = "redox")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CheckResult { Pass, Fail, Skip }
@@ -65,21 +74,68 @@ impl Report {
#[derive(serde::Serialize)]
struct JsonReport {
drm_device: bool, gpu_firmware: bool, mesa_dri: bool,
display_modes: bool, checks: Vec<JsonCheck>,
display_modes: bool, cs_ioctl: bool, gem_buffers: bool,
hardware_rendering_ready: bool, checks: Vec<JsonCheck>,
}
let drm = self.checks.iter().find(|c| c.name == "DRM_DEVICE").map_or(false, |c| c.result == CheckResult::Pass);
let firmware = self.checks.iter().find(|c| c.name == "GPU_FIRMWARE").map_or(false, |c| c.result == CheckResult::Pass);
let mesa = self.checks.iter().find(|c| c.name == "MESA_DRI").map_or(false, |c| c.result == CheckResult::Pass);
let modes = self.checks.iter().find(|c| c.name == "DISPLAY_MODES").map_or(false, |c| c.result == CheckResult::Pass);
let cs_ioctl = self.checks.iter().find(|c| c.name == "CS_IOCTL_PROTOCOL").map_or(false, |c| c.result == CheckResult::Pass);
let gem_buffers = self.checks.iter().find(|c| c.name == "GEM_BUFFER_ALLOCATION").map_or(false, |c| c.result == CheckResult::Pass);
let hardware_ready = self.checks.iter().find(|c| c.name == "HARDWARE_RENDERING_READY").map_or(false, |c| c.result == CheckResult::Pass);
let checks: Vec<JsonCheck> = self.checks.iter().map(|c| JsonCheck {
name: c.name.clone(), result: c.result.label().to_string(), detail: c.detail.clone(),
}).collect();
if let Err(err) = serde_json::to_writer(std::io::stdout(), &JsonReport { drm_device: drm, gpu_firmware: firmware, mesa_dri: mesa, display_modes: modes, checks }) {
if let Err(err) = serde_json::to_writer(std::io::stdout(), &JsonReport {
drm_device: drm,
gpu_firmware: firmware,
mesa_dri: mesa,
display_modes: modes,
cs_ioctl,
gem_buffers,
hardware_rendering_ready: hardware_ready,
checks,
}) {
eprintln!("{PROGRAM}: failed to serialize JSON: {err}");
}
}
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmGemCreateWire {
size: u64,
handle: u32,
pad: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmGemCloseWire {
handle: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct RedoxPrivateCsSubmit {
src_handle: u32,
dst_handle: u32,
src_offset: u64,
dst_offset: u64,
byte_count: u64,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct RedoxPrivateCsSubmitResult {
seqno: u64,
}
#[cfg(target_os = "redox")]
fn parse_args() -> Result<bool, String> {
let mut json_mode = false;
@@ -95,13 +151,18 @@ fn parse_args() -> Result<bool, String> {
#[cfg(target_os = "redox")]
fn check_drm_device() -> Check {
let paths = ["/scheme/drm/card0", "/dev/dri/card0"];
for p in paths {
if std::path::Path::new(p).exists() {
return Check::pass("DRM_DEVICE", p);
}
let scheme_path = "/scheme/drm/card0";
if std::path::Path::new(scheme_path).exists() {
return Check::pass("DRM_DEVICE", scheme_path);
}
Check::fail("DRM_DEVICE", "no DRM device found at /scheme/drm/card0 or /dev/dri/card0")
let dev_alias = "/dev/dri/card0";
if std::path::Path::new(dev_alias).exists() {
return Check::fail(
"DRM_DEVICE",
"/dev/dri/card0 exists, but Phase 5 CS probing requires /scheme/drm/card0",
);
}
Check::fail("DRM_DEVICE", "no DRM device found at /scheme/drm/card0")
}
#[cfg(target_os = "redox")]
@@ -155,6 +216,216 @@ fn check_display_modes() -> Check {
}
}
#[cfg(target_os = "redox")]
fn decode_wire_exact<T: Copy>(bytes: &[u8]) -> Result<T, String> {
use std::mem::{MaybeUninit, size_of};
if bytes.len() != size_of::<T>() {
return Err(format!(
"unexpected DRM response size: expected {} bytes, got {}",
size_of::<T>(),
bytes.len()
));
}
let mut out = MaybeUninit::<T>::uninit();
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), out.as_mut_ptr().cast::<u8>(), size_of::<T>());
Ok(out.assume_init())
}
}
#[cfg(target_os = "redox")]
fn bytes_of<T>(value: &T) -> &[u8] {
unsafe {
std::slice::from_raw_parts(
(value as *const T).cast::<u8>(),
std::mem::size_of::<T>(),
)
}
}
#[cfg(target_os = "redox")]
fn open_scheme_drm_card() -> Result<std::fs::File, String> {
std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("/scheme/drm/card0")
.map_err(|err| format!("failed to open /scheme/drm/card0: {err}"))
}
#[cfg(target_os = "redox")]
fn drm_query(file: &mut std::fs::File, request: usize, payload: &[u8]) -> Result<Vec<u8>, String> {
use std::io::{Read, Write};
let mut request_buf = request.to_le_bytes().to_vec();
request_buf.extend_from_slice(payload);
file.write_all(&request_buf)
.map_err(|err| format!("failed to send DRM ioctl {request:#x}: {err}"))?;
let mut response = vec![0u8; 4096];
let len = file
.read(&mut response)
.map_err(|err| format!("failed to read DRM ioctl {request:#x} response: {err}"))?;
response.truncate(len);
Ok(response)
}
#[cfg(target_os = "redox")]
fn check_gem_buffer_allocation() -> Check {
let mut card = match open_scheme_drm_card() {
Ok(card) => card,
Err(err) => return Check::fail("GEM_BUFFER_ALLOCATION", &err),
};
let request = DrmGemCreateWire {
size: 4096,
..DrmGemCreateWire::default()
};
match drm_query(&mut card, DRM_IOCTL_GEM_CREATE, bytes_of(&request))
.and_then(|response| decode_wire_exact::<DrmGemCreateWire>(&response))
{
Ok(created) => {
let _ = drm_query(
&mut card,
DRM_IOCTL_GEM_CLOSE,
bytes_of(&DrmGemCloseWire {
handle: created.handle,
}),
);
Check::pass(
"GEM_BUFFER_ALLOCATION",
&format!("allocated GEM handle {} over /scheme/drm/card0", created.handle),
)
}
Err(err) => Check::fail("GEM_BUFFER_ALLOCATION", &err),
}
}
#[cfg(target_os = "redox")]
fn check_cs_ioctl_protocol() -> Check {
let mut card = match open_scheme_drm_card() {
Ok(card) => card,
Err(err) => return Check::fail("CS_IOCTL_PROTOCOL", &err),
};
let first = DrmGemCreateWire {
size: 4096,
..DrmGemCreateWire::default()
};
let second = first;
let created_a = match drm_query(&mut card, DRM_IOCTL_GEM_CREATE, bytes_of(&first))
.and_then(|response| decode_wire_exact::<DrmGemCreateWire>(&response))
{
Ok(created) => created,
Err(err) => {
return Check::fail(
"CS_IOCTL_PROTOCOL",
&format!("source GEM allocation failed before CS probe: {err}"),
);
}
};
let created_b = match drm_query(&mut card, DRM_IOCTL_GEM_CREATE, bytes_of(&second))
.and_then(|response| decode_wire_exact::<DrmGemCreateWire>(&response))
{
Ok(created) => created,
Err(err) => {
let _ = drm_query(
&mut card,
DRM_IOCTL_GEM_CLOSE,
bytes_of(&DrmGemCloseWire {
handle: created_a.handle,
}),
);
return Check::fail(
"CS_IOCTL_PROTOCOL",
&format!("destination GEM allocation failed before CS probe: {err}"),
);
}
};
let submit = RedoxPrivateCsSubmit {
src_handle: created_a.handle,
dst_handle: created_b.handle,
src_offset: 0,
dst_offset: 0,
byte_count: 64,
};
let result = drm_query(
&mut card,
DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT,
bytes_of(&submit),
)
.and_then(|response| decode_wire_exact::<RedoxPrivateCsSubmitResult>(&response));
let _ = drm_query(
&mut card,
DRM_IOCTL_GEM_CLOSE,
bytes_of(&DrmGemCloseWire {
handle: created_b.handle,
}),
);
let _ = drm_query(
&mut card,
DRM_IOCTL_GEM_CLOSE,
bytes_of(&DrmGemCloseWire {
handle: created_a.handle,
}),
);
match result {
Ok(response) => Check::pass(
"CS_IOCTL_PROTOCOL",
&format!(
"private CS submit accepted GEM {} -> {} (seqno {})",
created_a.handle, created_b.handle, response.seqno
),
),
Err(err) => Check::fail("CS_IOCTL_PROTOCOL", &err),
}
}
#[cfg(target_os = "redox")]
fn check_hardware_rendering_ready(report: &Report) -> Check {
let required = [
"DRM_DEVICE",
"GPU_FIRMWARE",
"MESA_DRI",
"DISPLAY_MODES",
"GEM_BUFFER_ALLOCATION",
"CS_IOCTL_PROTOCOL",
];
let missing = required
.iter()
.copied()
.filter(|name| {
!report
.checks
.iter()
.any(|check| check.name == *name && check.result == CheckResult::Pass)
})
.collect::<Vec<_>>();
if missing.is_empty() {
Check::pass(
"HARDWARE_RENDERING_READY",
"Phase 5 preflight prerequisites are present; real hardware rendering validation is still pending",
)
} else {
Check::fail(
"HARDWARE_RENDERING_READY",
&format!(
"missing hardware rendering prerequisites: {}",
missing.join(", ")
),
)
}
}
fn run() -> Result<(), String> {
#[cfg(not(target_os = "redox"))]
{
@@ -170,6 +441,10 @@ fn run() -> Result<(), String> {
report.add(check_gpu_firmware());
report.add(check_mesa_dri_hardware());
report.add(check_display_modes());
report.add(check_gem_buffer_allocation());
report.add(check_cs_ioctl_protocol());
let readiness = check_hardware_rendering_ready(&report);
report.add(readiness);
report.print();
if report.any_failed() { return Err("one or more Phase 5 checks failed".to_string()); }
Ok(())