From 68ee6efefecba5ac21c39ab7cb550b75b1cb01c2 Mon Sep 17 00:00:00 2001 From: magdev Date: Sun, 3 May 2026 12:21:10 +0200 Subject: [PATCH] bundled: write Symfony cache + log to user data dir (AppImage is read-only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symfony defaults Kernel::getCacheDir/getLogDir to /var/cache and /var/log. In bundled mode those resolve inside the AppImage's FUSE mount (/tmp/.mount_/usr/share//symfony/) — read-only. Migrations fail at startup with: Unable to create the "cache" directory (/tmp/.mount_/usr/share//symfony/var/cache/prod). …and frankenphp's worker can't warm a cache either, so even after the binary spawns, the app is in a half-working state (which probably also explains the persistent Reconnecting banner the user reported — once migrations fail the supervisor sets Offline; even a successful re-probe of /healthz wouldn't recover from a half-warm state). Two-part fix, framework-side seam + app-side override: 1. BackendConnection.cpp (runMigrations + spawnChild): mkdir /var/{cache,log} and pass them as APP_CACHE_DIR / APP_LOG_DIR env vars. resolves to ~/.local/share/ via QStandardPaths::AppDataLocation, so it's user-writable. 2. App Kernel.php (skeleton + todo): override getCacheDir / getLogDir to honour the env vars. Falls back to parent behaviour when unset (dev mode keeps writing to var/cache like normal). Database file already lives at /var/data.sqlite, so the DB side was fine. Caddy autosaves to ~/.config/caddy and TLS storage to ~/.local/share/caddy — both user-writable. Mercure ran in-memory mode in earlier logs so no extra storage redirect needed there. Co-Authored-By: Claude Opus 4.7 (1M context) --- examples/todo/symfony/src/Kernel.php | 12 +++++++++++ framework/qml/src/BackendConnection.cpp | 26 +++++++++++++++++++---- framework/skeleton/symfony/src/Kernel.php | 12 +++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/examples/todo/symfony/src/Kernel.php b/examples/todo/symfony/src/Kernel.php index 4cf4eed..aa2adec 100644 --- a/examples/todo/symfony/src/Kernel.php +++ b/examples/todo/symfony/src/Kernel.php @@ -10,4 +10,16 @@ use Symfony\Component\HttpKernel\Kernel as BaseKernel; final class Kernel extends BaseKernel { use MicroKernelTrait; + + // The bundled-mode supervisor sets APP_CACHE_DIR / APP_LOG_DIR to writable + // paths under the user data dir; the AppImage mount is read-only. + public function getCacheDir(): string + { + return $_SERVER['APP_CACHE_DIR'] ?? parent::getCacheDir(); + } + + public function getLogDir(): string + { + return $_SERVER['APP_LOG_DIR'] ?? parent::getLogDir(); + } } diff --git a/framework/qml/src/BackendConnection.cpp b/framework/qml/src/BackendConnection.cpp index 4be825c..cf5bac1 100644 --- a/framework/qml/src/BackendConnection.cpp +++ b/framework/qml/src/BackendConnection.cpp @@ -204,11 +204,20 @@ bool BackendConnection::runMigrations() QStringLiteral("-n"), }); + // AppImage mount is read-only; redirect Symfony's writable dirs into the + // user data dir (Kernel::getCacheDir/getLogDir read these env vars). + const QString cacheDir = m_dataDir + QStringLiteral("/var/cache"); + const QString logDir = m_dataDir + QStringLiteral("/var/log"); + QDir().mkpath(cacheDir); + QDir().mkpath(logDir); + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); - env.insert(QStringLiteral("APP_ENV"), QStringLiteral("prod")); - env.insert(QStringLiteral("APP_DEBUG"), QStringLiteral("0")); - env.insert(QStringLiteral("APP_SECRET"), QStringLiteral("bundled-mode-migrations-do-not-need-this")); - env.insert(QStringLiteral("DATABASE_URL"), databaseUrl()); + env.insert(QStringLiteral("APP_ENV"), QStringLiteral("prod")); + env.insert(QStringLiteral("APP_DEBUG"), QStringLiteral("0")); + env.insert(QStringLiteral("APP_SECRET"), QStringLiteral("bundled-mode-migrations-do-not-need-this")); + env.insert(QStringLiteral("DATABASE_URL"), databaseUrl()); + env.insert(QStringLiteral("APP_CACHE_DIR"), cacheDir); + env.insert(QStringLiteral("APP_LOG_DIR"), logDir); proc.setProcessEnvironment(env); proc.setWorkingDirectory(resolveSymfonyDir()); proc.setProcessChannelMode(QProcess::MergedChannels); @@ -249,12 +258,21 @@ bool BackendConnection::spawnChild(QString* errorOut) }); m_child->setWorkingDirectory(resolveSymfonyDir()); + // AppImage mount is read-only; redirect Symfony's writable dirs into the + // user data dir (Kernel::getCacheDir/getLogDir read these env vars). + const QString cacheDir = m_dataDir + QStringLiteral("/var/cache"); + const QString logDir = m_dataDir + QStringLiteral("/var/log"); + QDir().mkpath(cacheDir); + QDir().mkpath(logDir); + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.insert(QStringLiteral("APP_ENV"), QStringLiteral("prod")); env.insert(QStringLiteral("APP_DEBUG"), QStringLiteral("0")); env.insert(QStringLiteral("APP_SECRET"), randomSecret(16)); env.insert(QStringLiteral("BRIDGE_TOKEN"), m_token); env.insert(QStringLiteral("DATABASE_URL"), databaseUrl()); + env.insert(QStringLiteral("APP_CACHE_DIR"), cacheDir); + env.insert(QStringLiteral("APP_LOG_DIR"), logDir); env.insert(QStringLiteral("PORT"), QString::number(m_port)); env.insert(QStringLiteral("MERCURE_URL"), m_url + QStringLiteral("/.well-known/mercure")); env.insert(QStringLiteral("MERCURE_PUBLIC_URL"), m_url + QStringLiteral("/.well-known/mercure")); diff --git a/framework/skeleton/symfony/src/Kernel.php b/framework/skeleton/symfony/src/Kernel.php index 4cf4eed..aa2adec 100644 --- a/framework/skeleton/symfony/src/Kernel.php +++ b/framework/skeleton/symfony/src/Kernel.php @@ -10,4 +10,16 @@ use Symfony\Component\HttpKernel\Kernel as BaseKernel; final class Kernel extends BaseKernel { use MicroKernelTrait; + + // The bundled-mode supervisor sets APP_CACHE_DIR / APP_LOG_DIR to writable + // paths under the user data dir; the AppImage mount is read-only. + public function getCacheDir(): string + { + return $_SERVER['APP_CACHE_DIR'] ?? parent::getCacheDir(); + } + + public function getLogDir(): string + { + return $_SERVER['APP_LOG_DIR'] ?? parent::getLogDir(); + } }