From 984d25eca24063f6261cbf627fc6128e7c9bf1b3 Mon Sep 17 00:00:00 2001 From: Mehmet <140706386+mehmetfiskindal@users.noreply.github.com> Date: Mon, 28 Sep 2026 08:58:46 +0300 Subject: [PATCH 1/9] Add Sailfish OS RPM target for Gea apps --- README.md | 7 +- package.json | 2 + .../raspberry-pi-os/main/rpios_display.cpp | 10 ++- targets/raspberry-pi-os/main/rpios_main.cpp | 12 +++ targets/sailfish-os/CMakeLists.txt | 29 +++++++ targets/sailfish-os/README.md | 32 ++++++++ targets/sailfish-os/build-sailfish-os.ps1 | 24 ++++++ targets/sailfish-os/prepare.mjs | 82 +++++++++++++++++++ 8 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 targets/sailfish-os/CMakeLists.txt create mode 100644 targets/sailfish-os/README.md create mode 100644 targets/sailfish-os/build-sailfish-os.ps1 create mode 100644 targets/sailfish-os/prepare.mjs diff --git a/README.md b/README.md index d50d3a7..a1cbb46 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ desktop apps. Apps render through the shared gea framework raster pipeline window, with mouse, touchscreen (multi-touch), keyboard, and scroll-wheel input mapped onto the framework input pipelines, plus persistent `localStorage` (file-backed) and HTTP(S) `fetch` (libcurl). The -current focus is Raspberry Pi OS on the Raspberry Pi 5; other distros can be -added as sibling targets. +targets include Raspberry Pi OS on the Raspberry Pi 5 and Sailfish OS. ## What Is Here | Path | Purpose | | --- | --- | | `targets/raspberry-pi-os` | Raspberry Pi OS (Bookworm+ desktop, labwc/Wayland) SDL2 target: platform sources, build script, and target docs. | +| `targets/sailfish-os` | Sailfish OS 5.1 SDK target: host code generation, portable CMake project, and i486 RPM packaging. | ## Quick Start @@ -32,6 +32,9 @@ or closing the window quits. See [targets/raspberry-pi-os/README.md](targets/raspberry-pi-os/README.md) for window size/scale/DPR/storage knobs and target architecture. +For Sailfish OS, use the standalone PowerShell build script described in +[targets/sailfish-os/README.md](targets/sailfish-os/README.md). + ## Dependencies - `build-essential`, `libsdl2-dev`, `libcurl4-openssl-dev`, `nodejs`, `npm` diff --git a/package.json b/package.json index 2334394..3a67144 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "targets", "!targets/**/dist/**", "!targets/**/build/**", + "!targets/**/generated/**", + "!targets/**/*.log", "!targets/**/test/**", "README.md", "LICENSE", diff --git a/targets/raspberry-pi-os/main/rpios_display.cpp b/targets/raspberry-pi-os/main/rpios_display.cpp index f3fecfc..763a390 100644 --- a/targets/raspberry-pi-os/main/rpios_display.cpp +++ b/targets/raspberry-pi-os/main/rpios_display.cpp @@ -56,7 +56,11 @@ int env_int(const char *name, int fallback, int lo, int hi) int g_canvas_width = env_int("GEA_RPIOS_WIDTH", gea::platform::display::kWidth, 64, 4096); int g_canvas_height = env_int("GEA_RPIOS_HEIGHT", gea::platform::display::kHeight, 64, 4096); +#ifdef GEA_SAILFISH_OS +int g_window_scale = env_int("GEA_RPIOS_SCALE", 1, 1, 8); +#else int g_window_scale = env_int("GEA_RPIOS_SCALE", 2, 1, 8); +#endif gea::framework::graphics::Canvas g_canvas; uint16_t *g_framebuffer = nullptr; @@ -106,12 +110,16 @@ void ensure_window() } #ifndef GEA_RPIOS_APP_TITLE #define GEA_RPIOS_APP_TITLE "gea" +#endif + int window_flags = SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE; +#ifdef GEA_SAILFISH_OS + window_flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; #endif g_window = SDL_CreateWindow(GEA_RPIOS_APP_TITLE, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, g_canvas_width * g_window_scale, g_canvas_height * g_window_scale, - SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE); + window_flags); if (!g_window) { std::fprintf(stderr, "[rpios display] SDL_CreateWindow failed: %s\n", SDL_GetError()); return; diff --git a/targets/raspberry-pi-os/main/rpios_main.cpp b/targets/raspberry-pi-os/main/rpios_main.cpp index 82a9e12..3d469b5 100644 --- a/targets/raspberry-pi-os/main/rpios_main.cpp +++ b/targets/raspberry-pi-os/main/rpios_main.cpp @@ -30,7 +30,9 @@ #include "event.h" #include "events.h" #include "input.h" +#ifndef GEA_SAILFISH_OS #include "resident_apps.h" +#endif #include "services/frame_scheduler.h" #include "css/declarative.h" #include "css/engine.h" @@ -92,7 +94,11 @@ double devicePixelRatio() // CSS animation engine. Mirrors the geaos main loop / @geastack/core runtime.cpp. void driveCssAnimations(std::uint32_t nowMs) { +#ifdef GEA_SAILFISH_OS + const char *active = nullptr; +#else const char *active = gea::framework::apps::ResidentApps::activeId(); +#endif static char lastScanned[64] = {0}; static bool singleAppScanned = false; const bool shouldScan = active ? std::strcmp(active, lastScanned) != 0 : !singleAppScanned; @@ -435,7 +441,9 @@ void dispatchPendingEvents() case gea::framework::events::EventType::Frame: case gea::framework::events::EventType::Timeout: case gea::framework::events::EventType::SettingsToggle: +#ifndef GEA_SAILFISH_OS case gea::framework::events::EventType::AppLaunch: +#endif break; } } @@ -502,7 +510,9 @@ int main(int argc, char **argv) // synced by rpios_storage_bridge.cpp) and the native StorageFacade // (mirrors runtime.cpp's boot-time Storage.load()). gea::framework::services::StorageService::init(); +#ifndef GEA_SAILFISH_OS rpios_runtime_storage_load(); +#endif gea::host::Storage.load(); gea::framework::app::Application::init(w, h, dpr); @@ -575,7 +585,9 @@ int main(int argc, char **argv) // Persist localStorage mutations from this frame (no-op unless changed; // mirrors runtime.cpp's per-frame Storage.flushPending()). Both views: // the geatsc runtime store (bridge) and the native facade. +#ifndef GEA_SAILFISH_OS rpios_runtime_storage_flush(); +#endif gea::host::Storage.flushPending(); struct timespec t_af1; clock_gettime(CLOCK_MONOTONIC, &t_af1); diff --git a/targets/sailfish-os/CMakeLists.txt b/targets/sailfish-os/CMakeLists.txt new file mode 100644 index 0000000..db10333 --- /dev/null +++ b/targets/sailfish-os/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.16) +project(gea_sailfish LANGUAGES C CXX) +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(SDL2 REQUIRED IMPORTED_TARGET sdl2) +pkg_check_modules(CURL REQUIRED IMPORTED_TARGET libcurl) +include(${CMAKE_CURRENT_SOURCE_DIR}/sources.cmake) + +add_executable(${GEA_PACKAGE_NAME} ${GEA_C_SOURCES} ${GEA_CXX_SOURCES}) +target_include_directories(${GEA_PACKAGE_NAME} PRIVATE ${GEA_INCLUDE_DIRS}) +target_compile_options(${GEA_PACKAGE_NAME} PRIVATE + $<$:-include${CMAKE_CURRENT_SOURCE_DIR}/platform/include/rpios_prelude.h> + -funwind-tables) +target_compile_definitions(${GEA_PACKAGE_NAME} PRIVATE + GEA_EMBEDDED_HAS_GENERATED_FONTS=1 + $<$:GEA_EMBEDDED_GIF_C_API> + $<$:GEA_SAILFISH_OS> + GEA_RPIOS_APP_ID="${GEA_APP_ID}" + GEA_RPIOS_APP_TITLE="${GEA_APP_TITLE}") +target_link_libraries(${GEA_PACKAGE_NAME} PRIVATE PkgConfig::SDL2 PkgConfig::CURL pthread m rt) +target_link_options(${GEA_PACKAGE_NAME} PRIVATE -Wl,--export-dynamic) + +install(TARGETS ${GEA_PACKAGE_NAME} RUNTIME DESTINATION bin) +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${GEA_PACKAGE_NAME}.desktop DESTINATION share/applications) +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${GEA_PACKAGE_NAME}.png + DESTINATION share/icons/hicolor/128x128/apps) diff --git a/targets/sailfish-os/README.md b/targets/sailfish-os/README.md new file mode 100644 index 0000000..23965c8 --- /dev/null +++ b/targets/sailfish-os/README.md @@ -0,0 +1,32 @@ +# Sailfish OS target + +Builds a Gea JSX/CSS app into a local Sailfish OS RPM. The host runs Vite and +geatsc; `sfdk` compiles the staged C/C++ sources for Sailfish OS 5.1.0.11. + +Requirements: Node.js 20.19+, `npm install` in this Linux repository and in +the app's workspace, a checkout of `geastack/core`, and Sailfish SDK with the +selected build target. The default core checkout is `../core` relative to this +Linux repository. Use `-CoreRepository` for another location. +Use `-SdkRoot` when Sailfish SDK is installed outside `C:\SailfishOS`. + +From the Linux repository on Windows: + +```powershell +./targets/sailfish-os/build-sailfish-os.ps1 -AppDirectory ../examples/apps/tic-tac-toe -Architecture i486 +``` + +The RPM is written to +`targets/sailfish-os/build/-/project/RPMS/`. +Use `-PrepareOnly` to run code generation and stage the portable CMake/RPM +project without invoking `sfdk`. + +The Windows script invokes the installed `sfdk.exe` with session handling off. +The SDK build engine must be running. On this host, invoking `sfdk.exe` inside +MSYS2 fails to identify its Docker engine, while the PowerShell invocation +works. MSYS2 remains installed for the SDK's other command-line workflows. + +The Sailfish target shares SDL2 rendering, touch, storage, and network sources +with the Raspberry Pi OS target. Its compile definition selects a scale of 1 +and a fullscreen window. The i486 RPM was installed on the Sailfish OS +5.1.0.11 emulator; Tic Tac Toe rendered and mouse clicks changed the board. +Physical device behavior has not been tested. diff --git a/targets/sailfish-os/build-sailfish-os.ps1 b/targets/sailfish-os/build-sailfish-os.ps1 new file mode 100644 index 0000000..a5bc532 --- /dev/null +++ b/targets/sailfish-os/build-sailfish-os.ps1 @@ -0,0 +1,24 @@ +param( + [Parameter(Mandatory=$true)][string]$AppDirectory, + [ValidateSet('i486','armv7hl','aarch64')][string]$Architecture = 'i486', + [string]$CoreRepository = '', + [string]$SdkRoot = 'C:\SailfishOS', + [switch]$PrepareOnly +) +$ErrorActionPreference = 'Stop' +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$argsForNode = @((Join-Path $here 'prepare.mjs'), (Resolve-Path $AppDirectory).Path, $Architecture) +if ($CoreRepository) { $argsForNode += (Resolve-Path $CoreRepository).Path } +& node @argsForNode +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +if ($PrepareOnly) { exit 0 } +$project = Join-Path $here "build/$((Get-Content (Join-Path $AppDirectory 'package.json') | ConvertFrom-Json).gea.id)-$Architecture/project" +$sfdk = Join-Path $SdkRoot 'bin/sfdk.exe' +if (!(Test-Path $sfdk)) { throw "Sailfish SDK missing at $sfdk" } +Push-Location $project +try { + & $sfdk --no-session -c "target=SailfishOS-5.1.0.11-$Architecture" build +} finally { + Pop-Location +} +exit $LASTEXITCODE diff --git a/targets/sailfish-os/prepare.mjs b/targets/sailfish-os/prepare.mjs new file mode 100644 index 0000000..bc60920 --- /dev/null +++ b/targets/sailfish-os/prepare.mjs @@ -0,0 +1,82 @@ +#!/usr/bin/env node +import fs from 'node:fs' +import path from 'node:path' +import { spawnSync } from 'node:child_process' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const here = path.dirname(fileURLToPath(import.meta.url)) +const linuxRoot = path.resolve(here, '../..') +const [appArg, arch = 'i486', coreArg] = process.argv.slice(2) +if (!appArg || !['i486', 'armv7hl', 'aarch64'].includes(arch)) { + console.error('usage: node prepare.mjs [i486|armv7hl|aarch64] [core-repository]') + process.exit(2) +} +const appDir = path.resolve(appArg) +const coreRepo = path.resolve(coreArg || path.join(linuxRoot, '../core')) +const meta = JSON.parse(fs.readFileSync(path.join(appDir, 'package.json'), 'utf8')) +const app = meta.gea +if (!app?.id || !/^[a-z0-9][a-z0-9-]*$/.test(app.id)) throw new Error('gea.id must be a lowercase package identifier') +const entry = app.entry || 'index.tsx' +if (!fs.existsSync(path.join(appDir, entry))) throw new Error(`app entry missing: ${entry}`) +const packageName = `harbour-gea-${app.id}` +const generated = path.join(here, 'generated', app.id) +const stage = path.join(here, 'build', `${app.id}-${arch}`, 'project') +fs.mkdirSync(path.dirname(generated), { recursive: true }) +const npmCore = path.join(linuxRoot, 'node_modules/@geastack/core') +const args = [path.join(npmCore, 'scripts/build-gea-vite-geatsc.mjs'), + '--app-dir', appDir, '--entry', entry, '--out-dir', generated, + '--geatsc-bin', path.join(linuxRoot, 'node_modules/@geastack/compiler/dist/cli.js'), + '--geatsc-gea-plugin', path.join(linuxRoot, 'node_modules/@geastack/geatsc-plugin-gea/dist/index.js'), + '--font-viewport-width', '410', '--font-viewport-height', '502', '--font-device-pixel-ratio', '2'] +const result = spawnSync(process.execPath, args, { stdio: 'inherit', cwd: linuxRoot }) +if (result.status !== 0) process.exit(result.status || 1) + +// sfdk mounts this project into its Linux build engine. Every compiled input +// must therefore be inside this directory, with no Windows absolute paths. +const resolvedStage = path.resolve(stage) +const allowedParent = path.resolve(here, 'build') + path.sep +if (!resolvedStage.startsWith(allowedParent)) throw new Error('unsafe stage directory') +fs.rmSync(stage, { recursive: true, force: true }) +fs.mkdirSync(stage, { recursive: true }) +const copyTree = (from, to) => fs.cpSync(from, to, { recursive: true, + filter: (item) => !['.git', 'node_modules', 'dist', 'build'].includes(path.basename(item)) }) +for (const name of ['core', 'host', 'engine', 'elements', 'geaos']) { + const source = path.join(coreRepo, 'packages', name) + if (!fs.existsSync(source)) throw new Error(`missing framework package: ${source}`) + copyTree(source, path.join(stage, 'framework', name)) +} +copyTree(generated, path.join(stage, 'generated')) +copyTree(path.join(linuxRoot, 'targets/raspberry-pi-os/main'), path.join(stage, 'platform/main')) +copyTree(path.join(linuxRoot, 'targets/raspberry-pi-os/include'), path.join(stage, 'platform/include')) +fs.copyFileSync(path.join(here, 'CMakeLists.txt'), path.join(stage, 'CMakeLists.txt')) + +const env = Object.fromEntries(['core', 'host', 'engine', 'elements', 'geaos'].map((name) => [ + ({ core: 'GEA_CORE', host: 'GEA_HOST_DIR', engine: 'GEA_ENGINE_DIR', elements: 'GEA_ELEMENTS_DIR', geaos: 'GEA_GEAOS_PACKAGE_DIR' })[name], + path.join(stage, 'framework', name).replaceAll('\\', '/')])) +const { includeFlags, cSources, cxxSources } = await import(pathToFileURL(path.join(coreRepo, 'packages/core/gea_sources.mjs'))) +const rel = (file) => path.relative(stage, file).replaceAll('\\', '/') +const c = [...cSources(env).map(rel), 'platform/main/rpios_apps.c'] +const cxx = cxxSources(env).filter((s) => !/\/(?:host\/camera|runtime|services\/[a-z_]+)\.cpp$/.test(s)).map(rel) +for (const name of ['display', 'audio', 'memory', 'network', 'sensors', 'storage', 'timers', 'app_platform', 'main']) cxx.push(`platform/main/rpios_${name}.cpp`) +cxx.push('framework/core/gea_app_entry.cpp') +for (const line of fs.readFileSync(path.join(generated, 'geatsc-sources.txt'), 'utf8').split(/\r?\n/).filter(Boolean)) { + const file = path.resolve(line) + if (!file.startsWith(path.resolve(generated) + path.sep)) throw new Error(`generated source outside output: ${file}`) + cxx.push(`generated/${path.relative(generated, file).replaceAll('\\', '/')}`) +} +for (const name of ['gea_embedded_font_generated.cpp', 'gea_embedded_assets_generated.cpp']) { + if (fs.existsSync(path.join(generated, name))) cxx.push(`generated/${name}`) +} +const includes = ['platform/include', 'generated', ...includeFlags(env).map((s) => rel(s.slice(2)))] +for (const file of [...c, ...cxx]) if (!fs.existsSync(path.join(stage, file))) throw new Error(`source missing: ${file}`) +const cmakeList = (key, values) => `set(${key}\n${values.map((s) => ` "${'${CMAKE_CURRENT_SOURCE_DIR}'}/${s}"`).join('\n')}\n)\n` +fs.writeFileSync(path.join(stage, 'sources.cmake'), + `set(GEA_PACKAGE_NAME "${packageName}")\nset(GEA_APP_ID "${app.id}")\nset(GEA_APP_TITLE "${String(app.name || app.id).replaceAll('"', '')}")\n` + + cmakeList('GEA_INCLUDE_DIRS', [...new Set(includes)]) + cmakeList('GEA_C_SOURCES', c) + cmakeList('GEA_CXX_SOURCES', cxx)) +const icon = app.icons?.['128'] || app.icons?.['256'] || app.icons?.['512'] +if (!icon || !fs.existsSync(path.join(appDir, icon))) throw new Error('app needs a PNG icon of at least 128px') +fs.copyFileSync(path.join(appDir, icon), path.join(stage, `${packageName}.png`)) +fs.writeFileSync(path.join(stage, `${packageName}.desktop`), `[Desktop Entry]\nType=Application\nName=${app.name || app.id}\nExec=${packageName}\nIcon=${packageName}\n`) +fs.mkdirSync(path.join(stage, 'rpm')) +fs.writeFileSync(path.join(stage, 'rpm', `${packageName}.spec`), `Name: ${packageName}\nVersion: ${meta.version || '0.1.0'}\nRelease: 1\nSummary: Gea ${app.name || app.id} for Sailfish OS\nLicense: ${meta.license || 'MIT'}\nBuildRequires: cmake\nBuildRequires: pkgconfig(sdl2)\nBuildRequires: pkgconfig(libcurl)\n\n%description\nGea JSX application for Sailfish OS.\n\n%build\n%cmake .\n%make_build\n\n%install\n%make_install\n\n%files\n%{_bindir}/${packageName}\n%{_datadir}/applications/${packageName}.desktop\n%{_datadir}/icons/hicolor/128x128/apps/${packageName}.png\n`) +console.log(stage) From e44e9ee3c74dfc720cd777b36ce64c9bd9946972 Mon Sep 17 00:00:00 2001 From: Mehmet <140706386+mehmetfiskindal@users.noreply.github.com> Date: Mon, 28 Sep 2026 09:05:46 +0300 Subject: [PATCH 2/9] Add Bash entry point for Sailfish builds --- .gitattributes | 1 + README.md | 2 +- package.json | 2 +- targets/sailfish-os/README.md | 19 ++++++-- targets/sailfish-os/build-sailfish-os.sh | 57 ++++++++++++++++++++++++ 5 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 .gitattributes create mode 100755 targets/sailfish-os/build-sailfish-os.sh diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ac746f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +targets/sailfish-os/*.sh text eol=lf diff --git a/README.md b/README.md index a1cbb46..9b6c1b7 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ or closing the window quits. See [targets/raspberry-pi-os/README.md](targets/raspberry-pi-os/README.md) for window size/scale/DPR/storage knobs and target architecture. -For Sailfish OS, use the standalone PowerShell build script described in +For Sailfish OS, use the standalone Bash or PowerShell build script described in [targets/sailfish-os/README.md](targets/sailfish-os/README.md). ## Dependencies diff --git a/package.json b/package.json index 3a67144..63f2007 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@geastack/linux", "version": "0.1.0", - "description": "Native Linux desktop build targets (SDL2 — Raspberry Pi OS) for GeaStack apps.", + "description": "Native SDL2 targets for Raspberry Pi OS and Sailfish OS GeaStack apps.", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/targets/sailfish-os/README.md b/targets/sailfish-os/README.md index 23965c8..142ebc6 100644 --- a/targets/sailfish-os/README.md +++ b/targets/sailfish-os/README.md @@ -6,8 +6,18 @@ geatsc; `sfdk` compiles the staged C/C++ sources for Sailfish OS 5.1.0.11. Requirements: Node.js 20.19+, `npm install` in this Linux repository and in the app's workspace, a checkout of `geastack/core`, and Sailfish SDK with the selected build target. The default core checkout is `../core` relative to this -Linux repository. Use `-CoreRepository` for another location. -Use `-SdkRoot` when Sailfish SDK is installed outside `C:\SailfishOS`. +Linux repository. Pass `--core-repo` (Bash) or `-CoreRepository` (PowerShell) +for another location. + +From the Linux repository on Linux: + +```sh +./targets/sailfish-os/build-sailfish-os.sh ../examples/apps/tic-tac-toe --arch i486 +``` + +The script finds `sfdk` on `PATH`. Pass `--sfdk /path/to/SailfishOS/bin/sfdk` +if it is elsewhere. `--prepare-only` runs code generation and stages the +portable project without invoking `sfdk`. From the Linux repository on Windows: @@ -15,10 +25,11 @@ From the Linux repository on Windows: ./targets/sailfish-os/build-sailfish-os.ps1 -AppDirectory ../examples/apps/tic-tac-toe -Architecture i486 ``` +Use `-SdkRoot` when Sailfish SDK is installed outside `C:\SailfishOS`. + The RPM is written to `targets/sailfish-os/build/-/project/RPMS/`. -Use `-PrepareOnly` to run code generation and stage the portable CMake/RPM -project without invoking `sfdk`. +Use `-PrepareOnly` in PowerShell for code generation without invoking `sfdk`. The Windows script invokes the installed `sfdk.exe` with session handling off. The SDK build engine must be running. On this host, invoking `sfdk.exe` inside diff --git a/targets/sailfish-os/build-sailfish-os.sh b/targets/sailfish-os/build-sailfish-os.sh new file mode 100755 index 0000000..58969f4 --- /dev/null +++ b/targets/sailfish-os/build-sailfish-os.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Generate a Gea app on the host, then build its Sailfish RPM with sfdk. +set -euo pipefail + +TARGET_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +APP_DIR="" +ARCH=i486 +CORE_REPO="" +SFDK_BIN="${SFDK_BIN:-sfdk}" +PREPARE_ONLY=0 + +usage() { + echo "usage: $0 [--arch i486|armv7hl|aarch64] [--core-repo path] [--sfdk path] [--prepare-only]" >&2 +} + +while (($#)); do + case "$1" in + --arch|--core-repo|--sfdk) + (($# >= 2)) || { usage; exit 2; } + case "$1" in + --arch) ARCH="$2" ;; + --core-repo) CORE_REPO="$2" ;; + --sfdk) SFDK_BIN="$2" ;; + esac + shift 2 + ;; + --prepare-only) PREPARE_ONLY=1; shift ;; + -h|--help) usage; exit 0 ;; + -*) usage; exit 2 ;; + *) + [[ -z "$APP_DIR" ]] || { usage; exit 2; } + APP_DIR="$1" + shift + ;; + esac +done + +[[ -n "$APP_DIR" ]] || { usage; exit 2; } +case "$ARCH" in i486|armv7hl|aarch64) ;; *) usage; exit 2 ;; esac +command -v node >/dev/null || { echo 'node is required' >&2; exit 1; } +APP_DIR="$(cd "$APP_DIR" && pwd -P)" + +if [[ -n "$CORE_REPO" ]]; then + CORE_REPO="$(cd "$CORE_REPO" && pwd -P)" + node "$TARGET_DIR/prepare.mjs" "$APP_DIR" "$ARCH" "$CORE_REPO" +else + node "$TARGET_DIR/prepare.mjs" "$APP_DIR" "$ARCH" +fi + +((PREPARE_ONLY)) && exit 0 +if [[ "$SFDK_BIN" == */* ]]; then + SFDK_BIN="$(cd "$(dirname "$SFDK_BIN")" && pwd -P)/$(basename "$SFDK_BIN")" +fi +command -v "$SFDK_BIN" >/dev/null || { echo "sfdk not found: $SFDK_BIN" >&2; exit 1; } +APP_ID="$(node -e 'const fs=require("node:fs"); const p=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.stdout.write(p.gea.id)' "$APP_DIR/package.json")" +PROJECT_DIR="$TARGET_DIR/build/$APP_ID-$ARCH/project" +(cd "$PROJECT_DIR" && "$SFDK_BIN" -c "target=SailfishOS-5.1.0.11-$ARCH" build) From 7b37c56f047a8359e4bdf8d482ad35fb03e3ece5 Mon Sep 17 00:00:00 2001 From: Mehmet <140706386+mehmetfiskindal@users.noreply.github.com> Date: Mon, 28 Sep 2026 09:45:24 +0300 Subject: [PATCH 3/9] Persist Sailfish runtime storage and normalize RPM versions --- targets/raspberry-pi-os/main/rpios_main.cpp | 4 ---- .../main/rpios_storage_bridge.cpp | 19 ++++++++----------- targets/sailfish-os/prepare.mjs | 8 ++++++-- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/targets/raspberry-pi-os/main/rpios_main.cpp b/targets/raspberry-pi-os/main/rpios_main.cpp index 3d469b5..57fce15 100644 --- a/targets/raspberry-pi-os/main/rpios_main.cpp +++ b/targets/raspberry-pi-os/main/rpios_main.cpp @@ -510,9 +510,7 @@ int main(int argc, char **argv) // synced by rpios_storage_bridge.cpp) and the native StorageFacade // (mirrors runtime.cpp's boot-time Storage.load()). gea::framework::services::StorageService::init(); -#ifndef GEA_SAILFISH_OS rpios_runtime_storage_load(); -#endif gea::host::Storage.load(); gea::framework::app::Application::init(w, h, dpr); @@ -585,9 +583,7 @@ int main(int argc, char **argv) // Persist localStorage mutations from this frame (no-op unless changed; // mirrors runtime.cpp's per-frame Storage.flushPending()). Both views: // the geatsc runtime store (bridge) and the native facade. -#ifndef GEA_SAILFISH_OS rpios_runtime_storage_flush(); -#endif gea::host::Storage.flushPending(); struct timespec t_af1; clock_gettime(CLOCK_MONOTONIC, &t_af1); diff --git a/targets/raspberry-pi-os/main/rpios_storage_bridge.cpp b/targets/raspberry-pi-os/main/rpios_storage_bridge.cpp index 590e327..a33b76e 100644 --- a/targets/raspberry-pi-os/main/rpios_storage_bridge.cpp +++ b/targets/raspberry-pi-os/main/rpios_storage_bridge.cpp @@ -2,9 +2,8 @@ * Persistence bridge for the geatsc runtime's `localStorage`. * * geatsc-compiled apps don't use gea::host::StorageFacade — the generated - * runtime has its own in-RAM store, gea::runtime::host::Storage, shared - * through gea::runtime::host::local_storage() (see the app's generated - * host_globals.{h,cpp}). Nothing in the generated runtime persists it, so + * runtime has its own in-RAM store, gea::host::storage::table() in the + * generated gea_runtime.h. Nothing in the generated runtime persists it, so * without this bridge every localStorage write dies with the process. * * The bridge syncs that store with the target's StorageService file backend @@ -20,8 +19,7 @@ * already on the include path). */ -#include "runtime_pch.h" -#include "host_globals.h" +#include "gea_runtime.h" #include "services/storage_service.h" @@ -35,9 +33,9 @@ std::string g_last_blob; std::string serializeEntries() { - auto &storage = gea::runtime::host::local_storage(); + auto &storage = gea::host::storage::table(); std::string out; - for (const auto &entry : storage.entries) { + for (const auto &entry : storage) { for (const std::string *part : {&entry.first, &entry.second}) { const std::uint32_t len = (std::uint32_t)part->size(); char header[sizeof(len)]; @@ -55,8 +53,8 @@ extern "C" void rpios_runtime_storage_load() { std::string blob; gea::framework::services::StorageService::loadKv(blob); - auto &storage = gea::runtime::host::local_storage(); - storage.entries.clear(); + auto &storage = gea::host::storage::table(); + storage.clear(); std::size_t pos = 0; auto readChunk = [&](std::string &out) { if (pos + sizeof(std::uint32_t) > blob.size()) return false; @@ -69,8 +67,7 @@ extern "C" void rpios_runtime_storage_load() return true; }; std::string k, v; - while (readChunk(k) && readChunk(v)) storage.entries.push_back({k, v}); - storage.length = (double)storage.entries.size(); + while (readChunk(k) && readChunk(v)) storage[k] = v; g_last_blob = blob; } diff --git a/targets/sailfish-os/prepare.mjs b/targets/sailfish-os/prepare.mjs index bc60920..e9f72a8 100644 --- a/targets/sailfish-os/prepare.mjs +++ b/targets/sailfish-os/prepare.mjs @@ -16,6 +16,10 @@ const coreRepo = path.resolve(coreArg || path.join(linuxRoot, '../core')) const meta = JSON.parse(fs.readFileSync(path.join(appDir, 'package.json'), 'utf8')) const app = meta.gea if (!app?.id || !/^[a-z0-9][a-z0-9-]*$/.test(app.id)) throw new Error('gea.id must be a lowercase package identifier') +const npmVersion = meta.version || '0.1.0' +const versionMatch = typeof npmVersion === 'string' && npmVersion.match(/^(\d+\.\d+\.\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/) +if (!versionMatch) throw new Error('package.json version must be a valid x.y.z version with optional prerelease and build metadata') +const rpmVersion = versionMatch[1] + (versionMatch[2] ? `~${versionMatch[2].replaceAll('-', '.')}` : '') const entry = app.entry || 'index.tsx' if (!fs.existsSync(path.join(appDir, entry))) throw new Error(`app entry missing: ${entry}`) const packageName = `harbour-gea-${app.id}` @@ -57,7 +61,7 @@ const { includeFlags, cSources, cxxSources } = await import(pathToFileURL(path.j const rel = (file) => path.relative(stage, file).replaceAll('\\', '/') const c = [...cSources(env).map(rel), 'platform/main/rpios_apps.c'] const cxx = cxxSources(env).filter((s) => !/\/(?:host\/camera|runtime|services\/[a-z_]+)\.cpp$/.test(s)).map(rel) -for (const name of ['display', 'audio', 'memory', 'network', 'sensors', 'storage', 'timers', 'app_platform', 'main']) cxx.push(`platform/main/rpios_${name}.cpp`) +for (const name of ['display', 'audio', 'memory', 'network', 'sensors', 'storage', 'storage_bridge', 'timers', 'app_platform', 'main']) cxx.push(`platform/main/rpios_${name}.cpp`) cxx.push('framework/core/gea_app_entry.cpp') for (const line of fs.readFileSync(path.join(generated, 'geatsc-sources.txt'), 'utf8').split(/\r?\n/).filter(Boolean)) { const file = path.resolve(line) @@ -78,5 +82,5 @@ if (!icon || !fs.existsSync(path.join(appDir, icon))) throw new Error('app needs fs.copyFileSync(path.join(appDir, icon), path.join(stage, `${packageName}.png`)) fs.writeFileSync(path.join(stage, `${packageName}.desktop`), `[Desktop Entry]\nType=Application\nName=${app.name || app.id}\nExec=${packageName}\nIcon=${packageName}\n`) fs.mkdirSync(path.join(stage, 'rpm')) -fs.writeFileSync(path.join(stage, 'rpm', `${packageName}.spec`), `Name: ${packageName}\nVersion: ${meta.version || '0.1.0'}\nRelease: 1\nSummary: Gea ${app.name || app.id} for Sailfish OS\nLicense: ${meta.license || 'MIT'}\nBuildRequires: cmake\nBuildRequires: pkgconfig(sdl2)\nBuildRequires: pkgconfig(libcurl)\n\n%description\nGea JSX application for Sailfish OS.\n\n%build\n%cmake .\n%make_build\n\n%install\n%make_install\n\n%files\n%{_bindir}/${packageName}\n%{_datadir}/applications/${packageName}.desktop\n%{_datadir}/icons/hicolor/128x128/apps/${packageName}.png\n`) +fs.writeFileSync(path.join(stage, 'rpm', `${packageName}.spec`), `Name: ${packageName}\nVersion: ${rpmVersion}\nRelease: 1\nSummary: Gea ${app.name || app.id} for Sailfish OS\nLicense: ${meta.license || 'MIT'}\nBuildRequires: cmake\nBuildRequires: pkgconfig(sdl2)\nBuildRequires: pkgconfig(libcurl)\n\n%description\nGea JSX application for Sailfish OS.\n\n%build\n%cmake .\n%make_build\n\n%install\n%make_install\n\n%files\n%{_bindir}/${packageName}\n%{_datadir}/applications/${packageName}.desktop\n%{_datadir}/icons/hicolor/128x128/apps/${packageName}.png\n`) console.log(stage) From eeff214f0d9bc7d69a70f8334db293dec272e24f Mon Sep 17 00:00:00 2001 From: Mehmet Fiskindal <140706386+mehmetfiskindal@users.noreply.github.com> Date: Tue, 29 Sep 2026 23:32:53 +0300 Subject: [PATCH 4/9] Make Sailfish RPMs Harbour compliant --- package-lock.json | 633 ++++++++++++++---- package.json | 5 +- .../raspberry-pi-os/main/rpios_storage.cpp | 9 +- targets/sailfish-os/CMakeLists.txt | 11 +- targets/sailfish-os/README.md | 10 + targets/sailfish-os/prepare.mjs | 23 +- 6 files changed, 556 insertions(+), 135 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0581ad7..7c31257 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,11 @@ "dependencies": { "@geastack/compiler": "^1.0.15", "@geastack/core": "^0.1.23", - "@geastack/geatsc-plugin-gea": "^0.1.6" + "@geastack/geatsc-plugin-gea": "^0.1.6", + "sharp": "^0.35.5" + }, + "engines": { + "node": ">=20.19" } }, "node_modules/@acemir/cssom": { @@ -279,15 +283,14 @@ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", - "optional": true, "engines": { "node": ">=18" } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", - "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", "cpu": [ "arm64" ], @@ -303,13 +306,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.3" + "@img/sharp-libvips-darwin-arm64": "1.3.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", - "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", "cpu": [ "x64" ], @@ -325,20 +328,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.3" + "@img/sharp-libvips-darwin-x64": "1.3.4" } }, "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", - "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", "license": "Apache-2.0", "optional": true, "os": [ "freebsd" ], "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -348,9 +351,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", - "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", "cpu": [ "arm64" ], @@ -364,9 +367,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", - "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", "cpu": [ "x64" ], @@ -380,12 +383,15 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", - "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -396,12 +402,15 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", - "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -412,12 +421,15 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", - "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -428,12 +440,15 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", - "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -444,12 +459,15 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", - "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -460,12 +478,15 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", - "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -476,12 +497,15 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", - "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -492,12 +516,15 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", - "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -508,12 +535,15 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", - "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -526,16 +556,19 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.3" + "@img/sharp-libvips-linux-arm": "1.3.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", - "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -548,16 +581,19 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.3" + "@img/sharp-libvips-linux-arm64": "1.3.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", - "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -570,16 +606,19 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.3" + "@img/sharp-libvips-linux-ppc64": "1.3.4" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", - "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -592,16 +631,19 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.3" + "@img/sharp-libvips-linux-riscv64": "1.3.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", - "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -614,16 +656,19 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.3" + "@img/sharp-libvips-linux-s390x": "1.3.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", - "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -636,16 +681,19 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.3" + "@img/sharp-libvips-linux-x64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", - "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -658,16 +706,19 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", - "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -680,13 +731,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", - "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -700,16 +751,16 @@ } }, "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", - "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", "cpu": [ "wasm32" ], "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -719,9 +770,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", - "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", "cpu": [ "arm64" ], @@ -738,9 +789,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", - "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", "cpu": [ "ia32" ], @@ -757,9 +808,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", - "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", "cpu": [ "x64" ], @@ -1083,6 +1134,346 @@ "csstype": "^3.2.2" } }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1540,7 +1931,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", - "optional": true, "bin": { "semver": "bin/semver.js" }, @@ -1549,11 +1939,10 @@ } }, "node_modules/sharp": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", - "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", "license": "Apache-2.0", - "optional": true, "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", @@ -1566,31 +1955,31 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.4", - "@img/sharp-darwin-x64": "0.35.4", - "@img/sharp-freebsd-wasm32": "0.35.4", - "@img/sharp-libvips-darwin-arm64": "1.3.3", - "@img/sharp-libvips-darwin-x64": "1.3.3", - "@img/sharp-libvips-linux-arm": "1.3.3", - "@img/sharp-libvips-linux-arm64": "1.3.3", - "@img/sharp-libvips-linux-ppc64": "1.3.3", - "@img/sharp-libvips-linux-riscv64": "1.3.3", - "@img/sharp-libvips-linux-s390x": "1.3.3", - "@img/sharp-libvips-linux-x64": "1.3.3", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", - "@img/sharp-libvips-linuxmusl-x64": "1.3.3", - "@img/sharp-linux-arm": "0.35.4", - "@img/sharp-linux-arm64": "0.35.4", - "@img/sharp-linux-ppc64": "0.35.4", - "@img/sharp-linux-riscv64": "0.35.4", - "@img/sharp-linux-s390x": "0.35.4", - "@img/sharp-linux-x64": "0.35.4", - "@img/sharp-linuxmusl-arm64": "0.35.4", - "@img/sharp-linuxmusl-x64": "0.35.4", - "@img/sharp-webcontainers-wasm32": "0.35.4", - "@img/sharp-win32-arm64": "0.35.4", - "@img/sharp-win32-ia32": "0.35.4", - "@img/sharp-win32-x64": "0.35.4" + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" }, "peerDependenciesMeta": { "@types/node": { diff --git a/package.json b/package.json index 63f2007..b6b6bd0 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,10 @@ "access": "public" }, "dependencies": { - "@geastack/core": "^0.1.23", "@geastack/compiler": "^1.0.15", - "@geastack/geatsc-plugin-gea": "^0.1.6" + "@geastack/core": "^0.1.23", + "@geastack/geatsc-plugin-gea": "^0.1.6", + "sharp": "^0.35.5" }, "scripts": { "test": "for f in $(git ls-files | grep -E '(^|/)(test[-_][^/]*|[^/]*\\.test)\\.mjs$'); do echo \"==> $f\"; node \"$f\" || exit 1; done" diff --git a/targets/raspberry-pi-os/main/rpios_storage.cpp b/targets/raspberry-pi-os/main/rpios_storage.cpp index ca36e75..39f14ab 100644 --- a/targets/raspberry-pi-os/main/rpios_storage.cpp +++ b/targets/raspberry-pi-os/main/rpios_storage.cpp @@ -4,8 +4,9 @@ * loadKv/saveKv store it verbatim) and for device-settings strings * (getString/setString, a small chunked KV file of our own). * - * Layout: $GEA_RPIOS_STORAGE_DIR, else $XDG_DATA_HOME/gea/, else - * ~/.local/share/gea/. Files: localstorage.bin, settings.bin. + * Layout: $GEA_RPIOS_STORAGE_DIR, else $XDG_DATA_HOME/gea/ on + * Raspberry Pi OS or $XDG_DATA_HOME// on Sailfish, + * falling back to ~/.local/share. Files: localstorage.bin, settings.bin. * Writes are atomic (tmp + rename) so a crash mid-write never corrupts the * previous state. saveKv is called from the frame task once per frame at most * (StorageFacade::flushPending only writes when dirty), so plain synchronous @@ -48,7 +49,11 @@ std::string storageDir() const char *home = std::getenv("HOME"); base = std::string(home && *home ? home : ".") + "/.local/share"; } +#ifdef GEA_SAILFISH_OS + return base + "/" + GEA_SAILFISH_ORGANIZATION_NAME + "/" + GEA_SAILFISH_APPLICATION_NAME; +#else return base + "/gea/" + GEA_RPIOS_APP_ID; +#endif }(); return dir; } diff --git a/targets/sailfish-os/CMakeLists.txt b/targets/sailfish-os/CMakeLists.txt index db10333..0c12eef 100644 --- a/targets/sailfish-os/CMakeLists.txt +++ b/targets/sailfish-os/CMakeLists.txt @@ -19,11 +19,16 @@ target_compile_definitions(${GEA_PACKAGE_NAME} PRIVATE $<$:GEA_EMBEDDED_GIF_C_API> $<$:GEA_SAILFISH_OS> GEA_RPIOS_APP_ID="${GEA_APP_ID}" - GEA_RPIOS_APP_TITLE="${GEA_APP_TITLE}") + GEA_RPIOS_APP_TITLE="${GEA_APP_TITLE}" + GEA_SAILFISH_ORGANIZATION_NAME="${GEA_SAILFISH_ORGANIZATION_NAME}" + GEA_SAILFISH_APPLICATION_NAME="${GEA_SAILFISH_APPLICATION_NAME}") target_link_libraries(${GEA_PACKAGE_NAME} PRIVATE PkgConfig::SDL2 PkgConfig::CURL pthread m rt) target_link_options(${GEA_PACKAGE_NAME} PRIVATE -Wl,--export-dynamic) install(TARGETS ${GEA_PACKAGE_NAME} RUNTIME DESTINATION bin) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${GEA_PACKAGE_NAME}.desktop DESTINATION share/applications) -install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${GEA_PACKAGE_NAME}.png - DESTINATION share/icons/hicolor/128x128/apps) +foreach(GEA_ICON_SIZE 86 108 128 172) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/icons/${GEA_ICON_SIZE}.png + DESTINATION share/icons/hicolor/${GEA_ICON_SIZE}x${GEA_ICON_SIZE}/apps + RENAME ${GEA_PACKAGE_NAME}.png) +endforeach() diff --git a/targets/sailfish-os/README.md b/targets/sailfish-os/README.md index 142ebc6..dd94d17 100644 --- a/targets/sailfish-os/README.md +++ b/targets/sailfish-os/README.md @@ -19,6 +19,13 @@ The script finds `sfdk` on `PATH`. Pass `--sfdk /path/to/SailfishOS/bin/sfdk` if it is elsewhere. `--prepare-only` runs code generation and stages the portable project without invoking `sfdk`. +The app's `gea.icons` entry supplies the icon source. Preparation generates +86, 108, 128, and 172 pixel PNGs for Harbour. For sandbox permissions, set +`gea.sailfish.organizationName`, `applicationName`, and `permissions` in the +app's `package.json`. The default identity is `org.geastack` and the app ID +with hyphens replaced by underscores; permissions default to an empty list. +Set only permissions the app actually needs, such as `Audio` for sound. + From the Linux repository on Windows: ```powershell @@ -30,6 +37,9 @@ Use `-SdkRoot` when Sailfish SDK is installed outside `C:\SailfishOS`. The RPM is written to `targets/sailfish-os/build/-/project/RPMS/`. Use `-PrepareOnly` in PowerShell for code generation without invoking `sfdk`. +Before submitting a device package to Harbour, run `sfdk check` on its RPM. +The `i486` build is for the emulator; build for the architecture of the target +device before publishing. The Windows script invokes the installed `sfdk.exe` with session handling off. The SDK build engine must be running. On this host, invoking `sfdk.exe` inside diff --git a/targets/sailfish-os/prepare.mjs b/targets/sailfish-os/prepare.mjs index e9f72a8..bb8bfd8 100644 --- a/targets/sailfish-os/prepare.mjs +++ b/targets/sailfish-os/prepare.mjs @@ -3,6 +3,7 @@ import fs from 'node:fs' import path from 'node:path' import { spawnSync } from 'node:child_process' import { fileURLToPath, pathToFileURL } from 'node:url' +import sharp from 'sharp' const here = path.dirname(fileURLToPath(import.meta.url)) const linuxRoot = path.resolve(here, '../..') @@ -16,6 +17,13 @@ const coreRepo = path.resolve(coreArg || path.join(linuxRoot, '../core')) const meta = JSON.parse(fs.readFileSync(path.join(appDir, 'package.json'), 'utf8')) const app = meta.gea if (!app?.id || !/^[a-z0-9][a-z0-9-]*$/.test(app.id)) throw new Error('gea.id must be a lowercase package identifier') +const sailfish = app.sailfish || {} +const organizationName = sailfish.organizationName || 'org.geastack' +const applicationName = sailfish.applicationName || app.id.replaceAll('-', '_') +const permissions = sailfish.permissions || [] +if (!/^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)+$/.test(organizationName)) throw new Error('gea.sailfish.organizationName must be a reverse-domain name') +if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(applicationName)) throw new Error('gea.sailfish.applicationName must contain only letters, digits, and underscores') +if (!Array.isArray(permissions) || !permissions.every((item) => typeof item === 'string' && /^[A-Za-z]+$/.test(item))) throw new Error('gea.sailfish.permissions must be an array of permission names') const npmVersion = meta.version || '0.1.0' const versionMatch = typeof npmVersion === 'string' && npmVersion.match(/^(\d+\.\d+\.\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/) if (!versionMatch) throw new Error('package.json version must be a valid x.y.z version with optional prerelease and build metadata') @@ -75,12 +83,15 @@ const includes = ['platform/include', 'generated', ...includeFlags(env).map((s) for (const file of [...c, ...cxx]) if (!fs.existsSync(path.join(stage, file))) throw new Error(`source missing: ${file}`) const cmakeList = (key, values) => `set(${key}\n${values.map((s) => ` "${'${CMAKE_CURRENT_SOURCE_DIR}'}/${s}"`).join('\n')}\n)\n` fs.writeFileSync(path.join(stage, 'sources.cmake'), - `set(GEA_PACKAGE_NAME "${packageName}")\nset(GEA_APP_ID "${app.id}")\nset(GEA_APP_TITLE "${String(app.name || app.id).replaceAll('"', '')}")\n` + + `set(GEA_PACKAGE_NAME "${packageName}")\nset(GEA_APP_ID "${app.id}")\nset(GEA_APP_TITLE "${String(app.name || app.id).replaceAll('"', '')}")\nset(GEA_SAILFISH_ORGANIZATION_NAME "${organizationName}")\nset(GEA_SAILFISH_APPLICATION_NAME "${applicationName}")\n` + cmakeList('GEA_INCLUDE_DIRS', [...new Set(includes)]) + cmakeList('GEA_C_SOURCES', c) + cmakeList('GEA_CXX_SOURCES', cxx)) -const icon = app.icons?.['128'] || app.icons?.['256'] || app.icons?.['512'] -if (!icon || !fs.existsSync(path.join(appDir, icon))) throw new Error('app needs a PNG icon of at least 128px') -fs.copyFileSync(path.join(appDir, icon), path.join(stage, `${packageName}.png`)) -fs.writeFileSync(path.join(stage, `${packageName}.desktop`), `[Desktop Entry]\nType=Application\nName=${app.name || app.id}\nExec=${packageName}\nIcon=${packageName}\n`) +const icon = app.icons?.['512'] || app.icons?.['256'] || app.icons?.['128'] +if (!icon || !fs.existsSync(path.join(appDir, icon))) throw new Error('app needs an icon source') +fs.mkdirSync(path.join(stage, 'icons')) +for (const size of [86, 108, 128, 172]) { + await sharp(path.join(appDir, icon)).resize(size, size, { fit: 'contain' }).png().toFile(path.join(stage, 'icons', `${size}.png`)) +} +fs.writeFileSync(path.join(stage, `${packageName}.desktop`), `[Desktop Entry]\nType=Application\nName=${app.name || app.id}\nExec=${packageName}\nIcon=${packageName}\nX-Nemo-Application-Type=generic\n\n[X-Sailjail]\nOrganizationName=${organizationName}\nApplicationName=${applicationName}\nPermissions=${[...new Set(permissions)].join(';')}\n`) fs.mkdirSync(path.join(stage, 'rpm')) -fs.writeFileSync(path.join(stage, 'rpm', `${packageName}.spec`), `Name: ${packageName}\nVersion: ${rpmVersion}\nRelease: 1\nSummary: Gea ${app.name || app.id} for Sailfish OS\nLicense: ${meta.license || 'MIT'}\nBuildRequires: cmake\nBuildRequires: pkgconfig(sdl2)\nBuildRequires: pkgconfig(libcurl)\n\n%description\nGea JSX application for Sailfish OS.\n\n%build\n%cmake .\n%make_build\n\n%install\n%make_install\n\n%files\n%{_bindir}/${packageName}\n%{_datadir}/applications/${packageName}.desktop\n%{_datadir}/icons/hicolor/128x128/apps/${packageName}.png\n`) +fs.writeFileSync(path.join(stage, 'rpm', `${packageName}.spec`), `Name: ${packageName}\nVersion: ${rpmVersion}\nRelease: 1\nSummary: Gea ${app.name || app.id} for Sailfish OS\nLicense: ${meta.license || 'MIT'}\nBuildRequires: cmake\nBuildRequires: pkgconfig(sdl2)\nBuildRequires: pkgconfig(libcurl)\n\n%description\nGea JSX application for Sailfish OS.\n\n%build\n%cmake .\n%make_build\n\n%install\n%make_install\n\n%files\n%{_bindir}/${packageName}\n%{_datadir}/applications/${packageName}.desktop\n%{_datadir}/icons/hicolor/*/apps/${packageName}.png\n`) console.log(stage) From d89b93693c847f4cc194e199ed9bd236a281b288 Mon Sep 17 00:00:00 2001 From: Mehmet Fiskindal <140706386+mehmetfiskindal@users.noreply.github.com> Date: Tue, 29 Sep 2026 23:40:54 +0300 Subject: [PATCH 5/9] Validate Sailfish RPM metadata before packaging --- targets/sailfish-os/prepare.mjs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/targets/sailfish-os/prepare.mjs b/targets/sailfish-os/prepare.mjs index bb8bfd8..255ce7e 100644 --- a/targets/sailfish-os/prepare.mjs +++ b/targets/sailfish-os/prepare.mjs @@ -17,6 +17,10 @@ const coreRepo = path.resolve(coreArg || path.join(linuxRoot, '../core')) const meta = JSON.parse(fs.readFileSync(path.join(appDir, 'package.json'), 'utf8')) const app = meta.gea if (!app?.id || !/^[a-z0-9][a-z0-9-]*$/.test(app.id)) throw new Error('gea.id must be a lowercase package identifier') +const appName = String(app.name || app.id) +const rpmLicense = String(meta.license || 'MIT') +if (/[\x00-\x1f\x7f%\\";$]/.test(appName)) throw new Error('gea.name must not contain control characters or RPM/CMake syntax') +if (/[\x00-\x1f\x7f%]/.test(rpmLicense)) throw new Error('package license must not contain control characters or RPM macros') const sailfish = app.sailfish || {} const organizationName = sailfish.organizationName || 'org.geastack' const applicationName = sailfish.applicationName || app.id.replaceAll('-', '_') @@ -83,7 +87,7 @@ const includes = ['platform/include', 'generated', ...includeFlags(env).map((s) for (const file of [...c, ...cxx]) if (!fs.existsSync(path.join(stage, file))) throw new Error(`source missing: ${file}`) const cmakeList = (key, values) => `set(${key}\n${values.map((s) => ` "${'${CMAKE_CURRENT_SOURCE_DIR}'}/${s}"`).join('\n')}\n)\n` fs.writeFileSync(path.join(stage, 'sources.cmake'), - `set(GEA_PACKAGE_NAME "${packageName}")\nset(GEA_APP_ID "${app.id}")\nset(GEA_APP_TITLE "${String(app.name || app.id).replaceAll('"', '')}")\nset(GEA_SAILFISH_ORGANIZATION_NAME "${organizationName}")\nset(GEA_SAILFISH_APPLICATION_NAME "${applicationName}")\n` + + `set(GEA_PACKAGE_NAME "${packageName}")\nset(GEA_APP_ID "${app.id}")\nset(GEA_APP_TITLE "${appName}")\nset(GEA_SAILFISH_ORGANIZATION_NAME "${organizationName}")\nset(GEA_SAILFISH_APPLICATION_NAME "${applicationName}")\n` + cmakeList('GEA_INCLUDE_DIRS', [...new Set(includes)]) + cmakeList('GEA_C_SOURCES', c) + cmakeList('GEA_CXX_SOURCES', cxx)) const icon = app.icons?.['512'] || app.icons?.['256'] || app.icons?.['128'] if (!icon || !fs.existsSync(path.join(appDir, icon))) throw new Error('app needs an icon source') @@ -91,7 +95,7 @@ fs.mkdirSync(path.join(stage, 'icons')) for (const size of [86, 108, 128, 172]) { await sharp(path.join(appDir, icon)).resize(size, size, { fit: 'contain' }).png().toFile(path.join(stage, 'icons', `${size}.png`)) } -fs.writeFileSync(path.join(stage, `${packageName}.desktop`), `[Desktop Entry]\nType=Application\nName=${app.name || app.id}\nExec=${packageName}\nIcon=${packageName}\nX-Nemo-Application-Type=generic\n\n[X-Sailjail]\nOrganizationName=${organizationName}\nApplicationName=${applicationName}\nPermissions=${[...new Set(permissions)].join(';')}\n`) +fs.writeFileSync(path.join(stage, `${packageName}.desktop`), `[Desktop Entry]\nType=Application\nName=${appName}\nExec=${packageName}\nIcon=${packageName}\nX-Nemo-Application-Type=generic\n\n[X-Sailjail]\nOrganizationName=${organizationName}\nApplicationName=${applicationName}\nPermissions=${[...new Set(permissions)].join(';')}\n`) fs.mkdirSync(path.join(stage, 'rpm')) -fs.writeFileSync(path.join(stage, 'rpm', `${packageName}.spec`), `Name: ${packageName}\nVersion: ${rpmVersion}\nRelease: 1\nSummary: Gea ${app.name || app.id} for Sailfish OS\nLicense: ${meta.license || 'MIT'}\nBuildRequires: cmake\nBuildRequires: pkgconfig(sdl2)\nBuildRequires: pkgconfig(libcurl)\n\n%description\nGea JSX application for Sailfish OS.\n\n%build\n%cmake .\n%make_build\n\n%install\n%make_install\n\n%files\n%{_bindir}/${packageName}\n%{_datadir}/applications/${packageName}.desktop\n%{_datadir}/icons/hicolor/*/apps/${packageName}.png\n`) +fs.writeFileSync(path.join(stage, 'rpm', `${packageName}.spec`), `Name: ${packageName}\nVersion: ${rpmVersion}\nRelease: 1\nSummary: Gea ${appName} for Sailfish OS\nLicense: ${rpmLicense}\nBuildRequires: cmake\nBuildRequires: pkgconfig(sdl2)\nBuildRequires: pkgconfig(libcurl)\n\n%description\nGea JSX application for Sailfish OS.\n\n%build\n%cmake .\n%make_build\n\n%install\n%make_install\n\n%files\n%{_bindir}/${packageName}\n%{_datadir}/applications/${packageName}.desktop\n%{_datadir}/icons/hicolor/*/apps/${packageName}.png\n`) console.log(stage) From 0c123e03d7e6b26527d541272d2cbf75ff675e16 Mon Sep 17 00:00:00 2001 From: Mehmet Fiskindal <140706386+mehmetfiskindal@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:50:33 +0300 Subject: [PATCH 6/9] Give Sailfish OS its own platform sources --- README.md | 2 +- .../raspberry-pi-os/main/rpios_display.cpp | 21 +- targets/raspberry-pi-os/main/rpios_main.cpp | 8 - .../raspberry-pi-os/main/rpios_storage.cpp | 7 +- targets/sailfish-os/CMakeLists.txt | 7 +- targets/sailfish-os/README.md | 12 +- .../sailfish-os/include/sailfish_prelude.h | 15 + .../main/sailfish_app_platform.cpp | 36 + targets/sailfish-os/main/sailfish_apps.c | 32 + targets/sailfish-os/main/sailfish_audio.cpp | 55 ++ targets/sailfish-os/main/sailfish_display.cpp | 475 +++++++++++++ targets/sailfish-os/main/sailfish_main.cpp | 625 ++++++++++++++++++ targets/sailfish-os/main/sailfish_memory.cpp | 34 + targets/sailfish-os/main/sailfish_network.cpp | 234 +++++++ targets/sailfish-os/main/sailfish_sensors.cpp | 122 ++++ targets/sailfish-os/main/sailfish_storage.cpp | 182 +++++ .../main/sailfish_storage_bridge.cpp | 80 +++ targets/sailfish-os/main/sailfish_timers.cpp | 128 ++++ targets/sailfish-os/prepare.mjs | 8 +- 19 files changed, 2048 insertions(+), 35 deletions(-) create mode 100644 targets/sailfish-os/include/sailfish_prelude.h create mode 100644 targets/sailfish-os/main/sailfish_app_platform.cpp create mode 100644 targets/sailfish-os/main/sailfish_apps.c create mode 100644 targets/sailfish-os/main/sailfish_audio.cpp create mode 100644 targets/sailfish-os/main/sailfish_display.cpp create mode 100644 targets/sailfish-os/main/sailfish_main.cpp create mode 100644 targets/sailfish-os/main/sailfish_memory.cpp create mode 100644 targets/sailfish-os/main/sailfish_network.cpp create mode 100644 targets/sailfish-os/main/sailfish_sensors.cpp create mode 100644 targets/sailfish-os/main/sailfish_storage.cpp create mode 100644 targets/sailfish-os/main/sailfish_storage_bridge.cpp create mode 100644 targets/sailfish-os/main/sailfish_timers.cpp diff --git a/README.md b/README.md index 9b6c1b7..8d06f9a 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ targets include Raspberry Pi OS on the Raspberry Pi 5 and Sailfish OS. | Path | Purpose | | --- | --- | | `targets/raspberry-pi-os` | Raspberry Pi OS (Bookworm+ desktop, labwc/Wayland) SDL2 target: platform sources, build script, and target docs. | -| `targets/sailfish-os` | Sailfish OS 5.1 SDK target: host code generation, portable CMake project, and i486 RPM packaging. | +| `targets/sailfish-os` | Sailfish OS 5.1 target: its own platform sources, host code generation, portable CMake project, and i486 RPM packaging. | ## Quick Start diff --git a/targets/raspberry-pi-os/main/rpios_display.cpp b/targets/raspberry-pi-os/main/rpios_display.cpp index 763a390..18f2789 100644 --- a/targets/raspberry-pi-os/main/rpios_display.cpp +++ b/targets/raspberry-pi-os/main/rpios_display.cpp @@ -56,11 +56,7 @@ int env_int(const char *name, int fallback, int lo, int hi) int g_canvas_width = env_int("GEA_RPIOS_WIDTH", gea::platform::display::kWidth, 64, 4096); int g_canvas_height = env_int("GEA_RPIOS_HEIGHT", gea::platform::display::kHeight, 64, 4096); -#ifdef GEA_SAILFISH_OS -int g_window_scale = env_int("GEA_RPIOS_SCALE", 1, 1, 8); -#else int g_window_scale = env_int("GEA_RPIOS_SCALE", 2, 1, 8); -#endif gea::framework::graphics::Canvas g_canvas; uint16_t *g_framebuffer = nullptr; @@ -87,8 +83,15 @@ int g_flush_depth = 0; void ensure_canvas() { if (g_framebuffer) return; - g_framebuffer_pixels = (size_t)g_canvas_width * (size_t)g_canvas_height; - g_framebuffer = static_cast(std::calloc(g_framebuffer_pixels, sizeof(uint16_t))); + const size_t pixels = (size_t)g_canvas_width * (size_t)g_canvas_height; + uint16_t *fb = static_cast(std::calloc(pixels, sizeof(uint16_t))); + if (!fb) { + std::fprintf(stderr, "[rpios display] framebuffer allocation failed (%dx%d)\n", + g_canvas_width, g_canvas_height); + return; + } + g_framebuffer = fb; + g_framebuffer_pixels = pixels; g_canvas.bindPixels(g_framebuffer, g_canvas_width, g_canvas_height); } @@ -112,9 +115,6 @@ void ensure_window() #define GEA_RPIOS_APP_TITLE "gea" #endif int window_flags = SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE; -#ifdef GEA_SAILFISH_OS - window_flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; -#endif g_window = SDL_CreateWindow(GEA_RPIOS_APP_TITLE, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, g_canvas_width * g_window_scale, @@ -310,12 +310,14 @@ gea::framework::graphics::Canvas *Display::canvas() void Display::rebindCanvasToFramebuffer() { ensure_canvas(); + if (!g_framebuffer) return; g_canvas.bindPixels(g_framebuffer, g_canvas_width, g_canvas_height); } void Display::clear() { ensure_canvas(); + if (!g_framebuffer) return; std::memset(g_framebuffer, 0, g_framebuffer_pixels * sizeof(uint16_t)); flush(); } @@ -323,6 +325,7 @@ void Display::clear() void Display::clearNoFlush() { ensure_canvas(); + if (!g_framebuffer) return; std::memset(g_framebuffer, 0, g_framebuffer_pixels * sizeof(uint16_t)); } diff --git a/targets/raspberry-pi-os/main/rpios_main.cpp b/targets/raspberry-pi-os/main/rpios_main.cpp index 57fce15..82a9e12 100644 --- a/targets/raspberry-pi-os/main/rpios_main.cpp +++ b/targets/raspberry-pi-os/main/rpios_main.cpp @@ -30,9 +30,7 @@ #include "event.h" #include "events.h" #include "input.h" -#ifndef GEA_SAILFISH_OS #include "resident_apps.h" -#endif #include "services/frame_scheduler.h" #include "css/declarative.h" #include "css/engine.h" @@ -94,11 +92,7 @@ double devicePixelRatio() // CSS animation engine. Mirrors the geaos main loop / @geastack/core runtime.cpp. void driveCssAnimations(std::uint32_t nowMs) { -#ifdef GEA_SAILFISH_OS - const char *active = nullptr; -#else const char *active = gea::framework::apps::ResidentApps::activeId(); -#endif static char lastScanned[64] = {0}; static bool singleAppScanned = false; const bool shouldScan = active ? std::strcmp(active, lastScanned) != 0 : !singleAppScanned; @@ -441,9 +435,7 @@ void dispatchPendingEvents() case gea::framework::events::EventType::Frame: case gea::framework::events::EventType::Timeout: case gea::framework::events::EventType::SettingsToggle: -#ifndef GEA_SAILFISH_OS case gea::framework::events::EventType::AppLaunch: -#endif break; } } diff --git a/targets/raspberry-pi-os/main/rpios_storage.cpp b/targets/raspberry-pi-os/main/rpios_storage.cpp index 39f14ab..27e43b8 100644 --- a/targets/raspberry-pi-os/main/rpios_storage.cpp +++ b/targets/raspberry-pi-os/main/rpios_storage.cpp @@ -4,8 +4,7 @@ * loadKv/saveKv store it verbatim) and for device-settings strings * (getString/setString, a small chunked KV file of our own). * - * Layout: $GEA_RPIOS_STORAGE_DIR, else $XDG_DATA_HOME/gea/ on - * Raspberry Pi OS or $XDG_DATA_HOME// on Sailfish, + * Layout: $GEA_RPIOS_STORAGE_DIR, else $XDG_DATA_HOME/gea/, * falling back to ~/.local/share. Files: localstorage.bin, settings.bin. * Writes are atomic (tmp + rename) so a crash mid-write never corrupts the * previous state. saveKv is called from the frame task once per frame at most @@ -49,11 +48,7 @@ std::string storageDir() const char *home = std::getenv("HOME"); base = std::string(home && *home ? home : ".") + "/.local/share"; } -#ifdef GEA_SAILFISH_OS - return base + "/" + GEA_SAILFISH_ORGANIZATION_NAME + "/" + GEA_SAILFISH_APPLICATION_NAME; -#else return base + "/gea/" + GEA_RPIOS_APP_ID; -#endif }(); return dir; } diff --git a/targets/sailfish-os/CMakeLists.txt b/targets/sailfish-os/CMakeLists.txt index 0c12eef..58854ba 100644 --- a/targets/sailfish-os/CMakeLists.txt +++ b/targets/sailfish-os/CMakeLists.txt @@ -12,14 +12,13 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/sources.cmake) add_executable(${GEA_PACKAGE_NAME} ${GEA_C_SOURCES} ${GEA_CXX_SOURCES}) target_include_directories(${GEA_PACKAGE_NAME} PRIVATE ${GEA_INCLUDE_DIRS}) target_compile_options(${GEA_PACKAGE_NAME} PRIVATE - $<$:-include${CMAKE_CURRENT_SOURCE_DIR}/platform/include/rpios_prelude.h> + $<$:-include${CMAKE_CURRENT_SOURCE_DIR}/platform/include/sailfish_prelude.h> -funwind-tables) target_compile_definitions(${GEA_PACKAGE_NAME} PRIVATE GEA_EMBEDDED_HAS_GENERATED_FONTS=1 $<$:GEA_EMBEDDED_GIF_C_API> - $<$:GEA_SAILFISH_OS> - GEA_RPIOS_APP_ID="${GEA_APP_ID}" - GEA_RPIOS_APP_TITLE="${GEA_APP_TITLE}" + GEA_SAILFISH_APP_ID="${GEA_APP_ID}" + GEA_SAILFISH_APP_TITLE="${GEA_APP_TITLE}" GEA_SAILFISH_ORGANIZATION_NAME="${GEA_SAILFISH_ORGANIZATION_NAME}" GEA_SAILFISH_APPLICATION_NAME="${GEA_SAILFISH_APPLICATION_NAME}") target_link_libraries(${GEA_PACKAGE_NAME} PRIVATE PkgConfig::SDL2 PkgConfig::CURL pthread m rt) diff --git a/targets/sailfish-os/README.md b/targets/sailfish-os/README.md index dd94d17..9976cd4 100644 --- a/targets/sailfish-os/README.md +++ b/targets/sailfish-os/README.md @@ -46,8 +46,14 @@ The SDK build engine must be running. On this host, invoking `sfdk.exe` inside MSYS2 fails to identify its Docker engine, while the PowerShell invocation works. MSYS2 remains installed for the SDK's other command-line workflows. -The Sailfish target shares SDL2 rendering, touch, storage, and network sources -with the Raspberry Pi OS target. Its compile definition selects a scale of 1 -and a fullscreen window. The i486 RPM was installed on the Sailfish OS +The Sailfish target owns its platform sources in `main/` and `include/`. +Its SDL2 backend starts fullscreen with a scale of 1. Platform symbols and +configuration use the `sailfish_` and `GEA_SAILFISH_` prefixes. Storage uses +the organization and application names from the app's Sailjail identity. + +Runtime overrides are `GEA_SAILFISH_WIDTH`, `GEA_SAILFISH_HEIGHT`, +`GEA_SAILFISH_SCALE`, `GEA_SAILFISH_DPR`, and `GEA_SAILFISH_STORAGE_DIR`. + +The i486 RPM was installed on the Sailfish OS 5.1.0.11 emulator; Tic Tac Toe rendered and mouse clicks changed the board. Physical device behavior has not been tested. diff --git a/targets/sailfish-os/include/sailfish_prelude.h b/targets/sailfish-os/include/sailfish_prelude.h new file mode 100644 index 0000000..6b0b72d --- /dev/null +++ b/targets/sailfish-os/include/sailfish_prelude.h @@ -0,0 +1,15 @@ +/* targets/sailfish-os/include/sailfish_prelude.h + * Forced-include header (-include) for the sailfish-os build to pull in standard + * headers that geatsc's runtime emit depends on but doesn't explicitly + * include (same role as targets/geaos/include/geaos_prelude.h; gcc libstdc++ + * requires them up front). */ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/targets/sailfish-os/main/sailfish_app_platform.cpp b/targets/sailfish-os/main/sailfish_app_platform.cpp new file mode 100644 index 0000000..dc96852 --- /dev/null +++ b/targets/sailfish-os/main/sailfish_app_platform.cpp @@ -0,0 +1,36 @@ +/* targets/sailfish-os/main/sailfish_app_platform.cpp + * AppManager platform stub. Single-app build — Apps.launch() just records the + * requested id and returns success. No process swap. + * (Linux analogue of targets/geaos/main/geaos_app_platform.cpp.) */ +#include "apps.h" +#include + +namespace { + +class LinuxAppLauncherPlatform final : public gea::framework::apps::AppLauncherPlatform { +public: + explicit LinuxAppLauncherPlatform(std::string currentId) : currentId_(std::move(currentId)) {} + const char *currentInstalledAppId() override { return currentId_.c_str(); } + bool runningAppIsLauncher(const char *launcherAppId) override + { + if (!launcherAppId) return false; + return currentId_ == launcherAppId; + } + bool launchInstalledApp(const char *) override { return false; } +private: + std::string currentId_; +}; + +} // namespace + +namespace gea::sailfish { + +void installAppLauncherPlatform(const char *currentAppId) +{ + static LinuxAppLauncherPlatform *instance = nullptr; + if (instance) return; + instance = new LinuxAppLauncherPlatform(currentAppId ? currentAppId : ""); + gea::framework::apps::AppManager::setPlatform(instance); +} + +} // namespace gea::sailfish diff --git a/targets/sailfish-os/main/sailfish_apps.c b/targets/sailfish-os/main/sailfish_apps.c new file mode 100644 index 0000000..ba0c7d3 --- /dev/null +++ b/targets/sailfish-os/main/sailfish_apps.c @@ -0,0 +1,32 @@ +#include + +/* The apps.h header in @geastack/core is C++-only. The framework + * itself calls the gea_embedded_apps_* C API via generated app code (and + * compiles it as C), so we declare the prototypes inline here. + * (Identical to targets/geaos/main/geaos_apps.c — single-app build.) */ +int gea_embedded_apps_launch(const char *app_id); +int gea_embedded_apps_return_to_launcher_on_reset(void); +void gea_embedded_apps_start_launcher_button_task(void); +const char *gea_embedded_apps_get_current_id(void); + +static char s_current_app_id[64] = ""; + +int gea_embedded_apps_launch(const char *app_id) +{ + if (!app_id || !app_id[0]) return 0; + strncpy(s_current_app_id, app_id, sizeof(s_current_app_id) - 1); + s_current_app_id[sizeof(s_current_app_id) - 1] = '\0'; + return 1; +} + +int gea_embedded_apps_return_to_launcher_on_reset(void) +{ + return 0; +} + +void gea_embedded_apps_start_launcher_button_task(void) {} + +const char *gea_embedded_apps_get_current_id(void) +{ + return s_current_app_id; +} diff --git a/targets/sailfish-os/main/sailfish_audio.cpp b/targets/sailfish-os/main/sailfish_audio.cpp new file mode 100644 index 0000000..aba106c --- /dev/null +++ b/targets/sailfish-os/main/sailfish_audio.cpp @@ -0,0 +1,55 @@ +/* targets/sailfish-os/main/sailfish_audio.cpp + * Audio is stubbed on the linux target for now. Apps just want sound to not + * crash the runtime; an SDL_audio/ALSA oscillator backend is a follow-up. + * Every method returns inert values and discards input. + * (Same surface as macos_audio.cpp plus the playFile/playPcm/stopPlayback + * trio geaos_audio.cpp defines — all are required to link host/audio.cpp.) */ + +#include "audio.h" + +namespace gea::platform::audio { + +namespace { +int g_volume = 80; +} + +// AudioParam +AudioParam::AudioParam(NativeAudioHandle oscillator) : oscillator_(oscillator) {} +double AudioParam::value() const { return 0.0; } +void AudioParam::setValue(double) {} +void AudioParam::setValueAtTime(double, double) {} + +// AudioNode +AudioNode::AudioNode(NativeAudioHandle native) : native_(native) {} +NativeAudioHandle AudioNode::nativeId() const { return native_; } + +// AudioDestinationNode +AudioDestinationNode::AudioDestinationNode(NativeAudioHandle native) : AudioNode(native) {} + +// OscillatorNode +OscillatorNode::OscillatorNode(NativeAudioHandle native) : AudioNode(native), frequency(native) {} +OscillatorType OscillatorNode::type() const { return OscillatorType::Sine; } +void OscillatorNode::setType(OscillatorType) {} +void OscillatorNode::connect(const AudioDestinationNode &) {} +void OscillatorNode::start(double) {} +void OscillatorNode::stop(double) {} + +// AudioContext +double AudioContext::currentTime() const { return 0.0; } +AudioDestinationNode AudioContext::destination() const { return AudioDestinationNode(0); } +OscillatorNode AudioContext::createOscillator() const { return OscillatorNode(0); } + +// AudioSystem +AudioContext AudioSystem::sharedContext() { return AudioContext{}; } +int AudioSystem::volume() { return g_volume; } +void AudioSystem::setVolume(int v) +{ + if (v < 0) v = 0; + if (v > 100) v = 100; + g_volume = v; +} +bool AudioSystem::playFile(const std::string &) { return false; } +bool AudioSystem::playPcm(const std::int16_t *, std::size_t, int, int) { return false; } +void AudioSystem::stopPlayback() {} + +} // namespace gea::platform::audio diff --git a/targets/sailfish-os/main/sailfish_display.cpp b/targets/sailfish-os/main/sailfish_display.cpp new file mode 100644 index 0000000..c7924d6 --- /dev/null +++ b/targets/sailfish-os/main/sailfish_display.cpp @@ -0,0 +1,475 @@ +/* targets/sailfish-os/main/sailfish_display.cpp + * Sailfish OS display backend — a fullscreen SDL2 window under Wayland. + * + * Architecture (same shape as targets/geaos/main/geaos_display.cpp, minus + * the MTK OVL/fbdev machinery): + * - The framework's Canvas is bound to an in-process RGB565 buffer at the + * logical window resolution. + * - Display::flush() / flushRects() upload the dirty region of that buffer + * into an SDL_PIXELFORMAT_RGB565 streaming texture and present it. The + * renderer is created with PRESENTVSYNC, so a presenting frame blocks to + * vblank — the main loop reads sailfish_display_consume_vsync_wait() to skip + * its own nanosleep on those frames (same double-pacing fix as geaos). + * - Display::pushClip / popClip forward to the Canvas clip stack — the + * framework relies on a working clip to constrain each dirty-region + * replay; stubbing them tanks fps (see the geaos header comment). + * - The window is an integer-scaled view of the logical canvas + * (GEA_SAILFISH_SCALE, default 1): the canvas stays at the app's tuned + * logical size while the window fills the device screen. + * + * Window size / scale / DPR come from the environment so apps tuned for a + * particular panel can be reproduced exactly: + * GEA_SAILFISH_WIDTH logical canvas width (default 410 — amoled-2.06) + * GEA_SAILFISH_HEIGHT logical canvas height (default 502) + * GEA_SAILFISH_SCALE integer window scale (default 1) + * + * The window is resizable: the main loop reacts to SDL_WINDOWEVENT_SIZE_CHANGED + * by calling sailfish_display_resize(), which reallocates the framebuffer + + * texture at the new logical size (window size / scale) and re-points the + * Canvas binding. F11 toggles borderless fullscreen (same resize path). + */ + +#include "display.h" +#include "canvas.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace { + +int env_int(const char *name, int fallback, int lo, int hi) +{ + const char *v = std::getenv(name); + if (!v || !*v) return fallback; + const int n = std::atoi(v); + if (n < lo || n > hi) return fallback; + return n; +} + +int g_canvas_width = env_int("GEA_SAILFISH_WIDTH", gea::platform::display::kWidth, 64, 4096); +int g_canvas_height = env_int("GEA_SAILFISH_HEIGHT", gea::platform::display::kHeight, 64, 4096); +int g_window_scale = env_int("GEA_SAILFISH_SCALE", 1, 1, 8); + +gea::framework::graphics::Canvas g_canvas; +uint16_t *g_framebuffer = nullptr; +size_t g_framebuffer_pixels = 0; + +SDL_Window *g_window = nullptr; +SDL_Renderer *g_renderer = nullptr; +SDL_Texture *g_texture = nullptr; +bool g_sdl_ok = false; +bool g_vsync_waited = false; + +// Static-backdrop cache (see gea_backdrop_cache below). Globals so +// sailfish_display_resize can grow it when the logical canvas grows. +uint16_t *g_backdrop_buffer = nullptr; +int g_backdrop_cap = 0; + +std::vector g_stream_rows; + +int g_brightness = 100; +uint8_t g_alpha_global = 255; +int g_flush_rows = 0; +int g_flush_depth = 0; + +void ensure_canvas() +{ + if (g_framebuffer) return; + const size_t pixels = (size_t)g_canvas_width * (size_t)g_canvas_height; + uint16_t *fb = static_cast(std::calloc(pixels, sizeof(uint16_t))); + if (!fb) { + std::fprintf(stderr, "[sailfish display] framebuffer allocation failed (%dx%d)\n", + g_canvas_width, g_canvas_height); + return; + } + g_framebuffer = fb; + g_framebuffer_pixels = pixels; + g_canvas.bindPixels(g_framebuffer, g_canvas_width, g_canvas_height); +} + +void ensure_window() +{ + if (g_sdl_ok) return; + ensure_canvas(); + static bool attempted = false; + if (attempted) return; + attempted = true; + + // Fingers drive the touch pipeline directly (SDL_FINGER* in sailfish_main); + // without this hint SDL would synthesize mouse events from touches and the + // same finger would inject twice. + SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"); + if (SDL_Init(SDL_INIT_VIDEO) != 0) { + std::fprintf(stderr, "[sailfish display] SDL_Init failed: %s\n", SDL_GetError()); + return; + } +#ifndef GEA_SAILFISH_APP_TITLE +#define GEA_SAILFISH_APP_TITLE "gea" +#endif + int window_flags = SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE; + window_flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; + g_window = SDL_CreateWindow(GEA_SAILFISH_APP_TITLE, + SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + g_canvas_width * g_window_scale, + g_canvas_height * g_window_scale, + window_flags); + if (!g_window) { + std::fprintf(stderr, "[sailfish display] SDL_CreateWindow failed: %s\n", SDL_GetError()); + return; + } + g_renderer = SDL_CreateRenderer(g_window, -1, + SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); + if (!g_renderer) { + // Software renderer fallback (no vsync pacing; the main loop's + // nanosleep then caps the rate instead). + g_renderer = SDL_CreateRenderer(g_window, -1, SDL_RENDERER_SOFTWARE); + } + if (!g_renderer) { + std::fprintf(stderr, "[sailfish display] SDL_CreateRenderer failed: %s\n", SDL_GetError()); + return; + } + // Integer-scale the logical canvas to the window; nearest keeps pixels crisp. + SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "nearest"); + SDL_RenderSetLogicalSize(g_renderer, g_canvas_width, g_canvas_height); + g_texture = SDL_CreateTexture(g_renderer, SDL_PIXELFORMAT_RGB565, + SDL_TEXTUREACCESS_STREAMING, + g_canvas_width, g_canvas_height); + if (!g_texture) { + std::fprintf(stderr, "[sailfish display] SDL_CreateTexture failed: %s\n", SDL_GetError()); + return; + } + SDL_RendererInfo info{}; + SDL_GetRendererInfo(g_renderer, &info); + g_sdl_ok = true; + std::fprintf(stderr, "[sailfish display] SDL window %dx%d (scale %d, renderer %s%s)\n", + g_canvas_width, g_canvas_height, g_window_scale, info.name, + (info.flags & SDL_RENDERER_PRESENTVSYNC) ? ", vsync" : ""); +} + +void upload_rect(int x0, int y0, int x1, int y1) +{ + if (!g_sdl_ok || !g_framebuffer) return; + x0 = std::max(0, x0); + y0 = std::max(0, y0); + x1 = std::min(g_canvas_width - 1, x1); + y1 = std::min(g_canvas_height - 1, y1); + if (x0 > x1 || y0 > y1) return; + SDL_Rect rect{x0, y0, x1 - x0 + 1, y1 - y0 + 1}; + const uint16_t *src = g_framebuffer + (size_t)y0 * g_canvas_width + x0; + SDL_UpdateTexture(g_texture, &rect, src, g_canvas_width * (int)sizeof(uint16_t)); +} + +// The texture holds the complete current frame (updated per dirty rect), so a +// present is always a full-texture copy into the backbuffer plus a flip. +void present_texture() +{ + if (!g_sdl_ok) return; + SDL_RenderClear(g_renderer); + SDL_RenderCopy(g_renderer, g_texture, nullptr, nullptr); + SDL_RenderPresent(g_renderer); + g_vsync_waited = true; +} + +// Copy a block of RGB565 rows into the software canvas (used by streamRect so +// g_framebuffer stays the complete current frame for the next full upload). +void copy_rgb565_rows_to_canvas(const uint16_t *pixels, int x, int y, int width, int rows) +{ + if (!pixels || !g_framebuffer || width <= 0 || rows <= 0) return; + for (int row = 0; row < rows; row++) { + const int dstY = y + row; + if (dstY < 0 || dstY >= g_canvas_height) continue; + int dstX = x; + int srcOffset = 0; + int run = width; + if (dstX < 0) { srcOffset = -dstX; run -= srcOffset; dstX = 0; } + if (dstX + run > g_canvas_width) run = g_canvas_width - dstX; + if (run <= 0) continue; + std::memcpy(g_framebuffer + (size_t)dstY * g_canvas_width + dstX, + pixels + (size_t)row * width + srcOffset, + (size_t)run * sizeof(uint16_t)); + } +} + +} // namespace + +extern "C" int sailfish_canvas_width() { return g_canvas_width; } +extern "C" int sailfish_canvas_height() { return g_canvas_height; } + +// Returns 1 (and clears the flag) if a present blocked on vsync since the last +// call — the main loop uses this to avoid double-pacing (its own nanosleep on +// top of SDL's vsynced present). Same contract as geaos_display_consume_vsync_wait. +extern "C" int sailfish_display_consume_vsync_wait() +{ + if (g_vsync_waited) { g_vsync_waited = false; return 1; } + return 0; +} + +// Full-screen scratch for the renderer's static-backdrop cache (see +// DisplayList::maybeBakeStaticBackdrop and the geaos_display.cpp comment: the +// weak nullptr default makes dirty-region replay erase static content painted +// before an animated subtree). Desktop RAM is plentiful; one RGB565 canvas. +// Grown by sailfish_display_resize when the logical canvas outgrows it. +extern "C" std::uint16_t *gea_backdrop_cache(int *cap_px) +{ + if (!g_backdrop_buffer && g_canvas_width > 0 && g_canvas_height > 0) { + g_backdrop_cap = g_canvas_width * g_canvas_height; + g_backdrop_buffer = static_cast(std::malloc((size_t)g_backdrop_cap * sizeof(std::uint16_t))); + if (!g_backdrop_buffer) g_backdrop_cap = 0; + } + if (cap_px) *cap_px = g_backdrop_buffer ? g_backdrop_cap : 0; + return g_backdrop_buffer; +} + +// Re-present the current texture (window exposed/restored after being +// obscured; the framework doesn't know pixels were lost). +extern "C" void sailfish_display_present() +{ + if (g_sdl_ok) present_texture(); +} + +extern "C" void sailfish_display_toggle_fullscreen() +{ + if (!g_window) return; + const bool full = (SDL_GetWindowFlags(g_window) & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0; + SDL_SetWindowFullscreen(g_window, full ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP); + // The resulting SDL_WINDOWEVENT_SIZE_CHANGED drives the resize path. +} + +extern "C" int sailfish_display_window_scale() { return g_window_scale; } + +// Rebuild the framebuffer + streaming texture at a new logical size (window +// resized or fullscreen toggled). The canvas rebinds to the new buffer; the +// caller (sailfish_main) then updates the framework viewport metrics and marks +// the tree fully dirty so the next frame repaints everything. +extern "C" int sailfish_display_resize(int new_width, int new_height) +{ + if (new_width < 64) new_width = 64; + if (new_height < 64) new_height = 64; + if (new_width > 4096) new_width = 4096; + if (new_height > 4096) new_height = 4096; + if (new_width == g_canvas_width && new_height == g_canvas_height) return 0; + + const size_t pixels = (size_t)new_width * (size_t)new_height; + uint16_t *fb = static_cast(std::calloc(pixels, sizeof(uint16_t))); + if (!fb) return 0; + + if (g_sdl_ok) { + SDL_Texture *texture = SDL_CreateTexture(g_renderer, SDL_PIXELFORMAT_RGB565, + SDL_TEXTUREACCESS_STREAMING, + new_width, new_height); + if (!texture) { + std::fprintf(stderr, "[sailfish display] resize texture failed: %s\n", SDL_GetError()); + std::free(fb); + return 0; + } + SDL_DestroyTexture(g_texture); + g_texture = texture; + } + + std::free(g_framebuffer); + g_framebuffer = fb; + g_framebuffer_pixels = pixels; + g_canvas_width = new_width; + g_canvas_height = new_height; + g_canvas.bindPixels(g_framebuffer, g_canvas_width, g_canvas_height); + if (g_sdl_ok) SDL_RenderSetLogicalSize(g_renderer, g_canvas_width, g_canvas_height); + + const int cap = new_width * new_height; + if (g_backdrop_buffer && cap > g_backdrop_cap) { + uint16_t *grown = static_cast(std::realloc(g_backdrop_buffer, (size_t)cap * sizeof(uint16_t))); + if (grown) { + g_backdrop_buffer = grown; + g_backdrop_cap = cap; + } + // realloc failure keeps the old (smaller) cache; the renderer checks + // the cap and skips baking when it doesn't fit. + } + return 1; +} + +namespace gea::platform::display { + +bool Display::init() { ensure_window(); return g_sdl_ok; } +bool Display::start() { return true; } + +gea::framework::graphics::Canvas *Display::canvas() +{ + ensure_canvas(); + return &g_canvas; +} + +// The retained renderer only rebinds the canvas on the ESP32 fused-replay +// flush; here it just re-asserts the full-frame staging binding. +void Display::rebindCanvasToFramebuffer() +{ + ensure_canvas(); + if (!g_framebuffer) return; + g_canvas.bindPixels(g_framebuffer, g_canvas_width, g_canvas_height); +} + +void Display::clear() +{ + ensure_canvas(); + if (!g_framebuffer) return; + std::memset(g_framebuffer, 0, g_framebuffer_pixels * sizeof(uint16_t)); + flush(); +} + +void Display::clearNoFlush() +{ + ensure_canvas(); + if (!g_framebuffer) return; + std::memset(g_framebuffer, 0, g_framebuffer_pixels * sizeof(uint16_t)); +} + +void Display::print(const char *) {} + +void Display::flush() +{ + ensure_window(); + upload_rect(0, 0, g_canvas_width - 1, g_canvas_height - 1); + present_texture(); + g_canvas.resetDirty(); +} + +void Display::flushRects(const DisplayFlushRect *rects, int count, bool) +{ + ensure_window(); + if (!rects || count <= 0) return; + for (int i = 0; i < count; i++) { + upload_rect(rects[i].x0, rects[i].y0, rects[i].x1, rects[i].y1); + } + present_texture(); + g_canvas.resetDirty(); +} + +bool Display::streamRect(int x, int y, int w, int h, DisplayStreamRasterFn raster, void *user) +{ + if (!raster || w <= 0 || h <= 0) return false; + ensure_window(); + if (!g_sdl_ok) return false; + + int x0 = std::max(0, x); + int y0 = std::max(0, y); + int x1 = std::min(g_canvas_width - 1, x + w - 1); + int y1 = std::min(g_canvas_height - 1, y + h - 1); + if (x0 > x1 || y0 > y1) return true; + + const int width = x1 - x0 + 1; + const int maxRows = std::max(1, g_flush_rows > 0 ? g_flush_rows : 80); + g_stream_rows.resize((size_t)width * (size_t)maxRows); + for (int row = y0; row <= y1; row += maxRows) { + int rows = maxRows; + if (row + rows > y1 + 1) rows = y1 - row + 1; + raster(g_stream_rows.data(), width, rows, x0, row, user); + copy_rgb565_rows_to_canvas(g_stream_rows.data(), x0, row, width, rows); + } + upload_rect(x0, y0, x1, y1); + present_texture(); + g_canvas.resetDirty(); + return true; +} + +bool Display::present(const DisplayPresentCommand *, int) +{ + // No batched hardware present here. Returning false routes endBatch() + // through replayPresentBatchToCanvas so the canvas pixel buffer receives + // each frame (see the macos_display.mm comment: returning true silently + // drops per-frame draws and freezes canvas animations on frame 1). + return false; +} + +void Display::setFlushConfig(int chunk_rows, int queue_depth) { g_flush_rows = chunk_rows; g_flush_depth = queue_depth; } +int Display::flushChunkRows() { return g_flush_rows; } +int Display::flushQueueDepth() { return g_flush_depth; } +int Display::flushBufferBytes() { return 0; } + +// Clip forwarding to the Canvas — without this the framework's per-dirty-rect +// clip narrowing is lost and Canvas::fillRect iterates the full canvas for +// every dirty region (20× cost; see geaos_display.cpp). +void Display::pushClip(int x, int y, int w, int h) { ensure_canvas(); g_canvas.pushClip(x, y, w, h); } +void Display::popClip() { ensure_canvas(); g_canvas.popClip(); } +void Display::resetClip() { ensure_canvas(); g_canvas.resetClip(); } +void Display::setAlpha(uint8_t a) { g_alpha_global = a; } +uint8_t Display::alpha() { return g_alpha_global; } +int Display::brightness() { return g_brightness; } +void Display::setBrightness(int b) +{ + if (b < 0) b = 0; + if (b > 100) b = 100; + g_brightness = b; +} + +// SDL's PRESENTVSYNC already paces presents; the explicit TE-style frame wait +// used by some panels is not needed here. +void Display::setVSync(bool) {} +void Display::invalidate() {} +bool Display::vsyncEnabled() { return false; } +void Display::vsyncWaitForFrame() {} + +void Display::clip(int *x0, int *y0, int *x1, int *y1) +{ + if (x0) *x0 = 0; + if (y0) *y0 = 0; + if (x1) *x1 = g_canvas_width - 1; + if (y1) *y1 = g_canvas_height - 1; +} + +void Display::fillRect(int x, int y, int w, int h, uint16_t color) { ensure_canvas(); g_canvas.fillRect(x, y, w, h, color); } +void Display::scrollRect(int x, int y, int w, int h, int dx, int dy) { ensure_canvas(); g_canvas.scrollRect(x, y, w, h, dx, dy); } +void Display::resetScrollRegion() {} +void Display::strokeRect(int x, int y, int w, int h, uint16_t color) { ensure_canvas(); g_canvas.strokeRect(x, y, w, h, color); } +void Display::fillCircle(int x, int y, int r, uint16_t color) { ensure_canvas(); g_canvas.fillCircle(x, y, r, color); } +void Display::strokeCircle(int x, int y, int r, uint16_t color) { ensure_canvas(); g_canvas.strokeCircle(x, y, r, color); } +void Display::drawLine(int x0, int y0, int x1, int y1, uint16_t color) { ensure_canvas(); g_canvas.drawLine(x0, y0, x1, y1, color); } +void Display::drawArc(int cx, int cy, int r, int s, int e, uint16_t color) { ensure_canvas(); g_canvas.drawArc(cx, cy, r, s, e, color); } +void Display::fillTriangle(int x0, int y0, int x1, int y1, int x2, int y2, uint16_t color) { ensure_canvas(); g_canvas.fillTriangle(x0, y0, x1, y1, x2, y2, color); } +void Display::drawText(const char *text, int x, int y, uint16_t color, float scale) +{ + ensure_canvas(); + g_canvas.drawText(text, x, y, color, scale); +} +void Display::drawTextFont(const char *text, int x, int y, uint16_t color, int font_id) +{ + ensure_canvas(); + g_canvas.drawTextFont(text, x, y, color, font_id); +} +void Display::drawTextFontFamily(const char *text, int x, int y, uint16_t color, int family_id, int size_px) +{ + ensure_canvas(); + g_canvas.drawTextFontFamily(text, x, y, color, family_id, size_px); +} +void Display::setPixel(int x, int y, uint16_t color) { ensure_canvas(); g_canvas.fillRect(x, y, 1, 1, color); } +void Display::fillRoundedRect(int x, int y, int w, int h, int tl, int tr, int br, int bl, uint16_t color) { ensure_canvas(); g_canvas.fillRoundedRect(x, y, w, h, tl, tr, br, bl, color); } +void Display::fillRoundedRectBoxesRgb565(const int16_t *xs, const int16_t *ys, int count, int w, int h, + int tl, int tr, int br, int bl, const uint16_t *colors) +{ + ensure_canvas(); + g_canvas.fillRoundedRectBoxesRgb565(xs, ys, count, w, h, tl, tr, br, bl, colors); +} +void Display::strokeRoundedRect(int x, int y, int w, int h, int tl, int tr, int br, int bl, int lw, uint16_t color) +{ + ensure_canvas(); + g_canvas.strokeRoundedRect(x, y, w, h, tl, tr, br, bl, lw, color); +} +void Display::blitImage(const gea::framework::graphics::pixel::native_t *src, const uint8_t *alpha, int src_w, int src_h, int dx, int dy) +{ + ensure_canvas(); + g_canvas.drawImage(src, alpha, src_w, src_h, dx, dy); +} +void Display::blitImageScaled(const gea::framework::graphics::pixel::native_t *src, const uint8_t *alpha, int src_w, int src_h, + int dx, int dy, int dst_w, int dst_h) +{ + ensure_canvas(); + g_canvas.drawImage(src, alpha, src_w, src_h, dx, dy, dst_w, dst_h); +} +void Display::setWorldOverlay(const uint16_t *, int, int, int, int) {} +void Display::setWorldScroll(int) {} + +} // namespace gea::platform::display diff --git a/targets/sailfish-os/main/sailfish_main.cpp b/targets/sailfish-os/main/sailfish_main.cpp new file mode 100644 index 0000000..b616db0 --- /dev/null +++ b/targets/sailfish-os/main/sailfish_main.cpp @@ -0,0 +1,625 @@ +/* targets/sailfish-os/main/sailfish_main.cpp + * Sailfish OS entry point. Runs the gea app loop with an SDL2 window as the + * display (sailfish_display.cpp) and SDL mouse/touch/keyboard events as input. + * + * Analogue of targets/geaos/main/geaos_main.cpp minus the watch UX (launcher + * button overlay, settings drag, camera overlay, debug-input FIFO). The + * runtime pipeline is: + * Application::init(w, h, dpr) + * per frame: + * pump SDL events: + * mouse (pointer 0) / fingers (multi-touch) → touch event queue + * keys → input::queueKeyDown, text → focused value + * wheel → Tree::scrollByKeyStep + input::queueRotaryDelta + * window resize/minimize/expose → display resize / pause / re-present + * dispatch queued events (touch → TouchRuntime; key/rotary drains) + * Application::frame(now_ms) + * Storage.flushPending() // persist localStorage if dirty + * Tree::refresh(root, w, h) // shared display-list path + * → Display::flushRects(...) // SDL texture upload + present + * + * The key/rotary drains and the Storage load/flush calls mirror what + * core/runtime.cpp does for targets that use the shared runtime loop — this + * target excludes runtime.cpp (it runs its own loop), so it must replicate + * that per-frame plumbing itself. + */ +#include "app.h" +#include "display.h" +#include "host/storage.h" +#include "host/timers.h" +#include "event.h" +#include "events.h" +#include "input.h" +#include "services/frame_scheduler.h" +#include "css/declarative.h" +#include "css/engine.h" +#include "ui/document.h" +#include "ui/refresh_perf.h" +#include "ui/node.h" +#include "ui/style.h" +#include "ui/tree_internal.h" +#include "touch.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" int gea_embedded_now_ms(void); +extern "C" int sailfish_canvas_width(); +extern "C" int sailfish_canvas_height(); +extern "C" int sailfish_display_consume_vsync_wait(); +extern "C" void sailfish_display_present(); +extern "C" void sailfish_display_toggle_fullscreen(); +extern "C" int sailfish_display_window_scale(); +extern "C" int sailfish_display_resize(int new_width, int new_height); +extern "C" int gea_embedded_apps_launch(const char *app_id); +extern "C" void sailfish_runtime_storage_load(); +extern "C" void sailfish_runtime_storage_flush(); +extern "C" void sailfish_install_wifi_driver(); + +namespace gea::sailfish { +void installAppLauncherPlatform(const char *currentAppId); +} + +#ifndef GEA_SAILFISH_APP_ID +#define GEA_SAILFISH_APP_ID "app" +#endif + +namespace { + +double devicePixelRatio() +{ + const char *v = std::getenv("GEA_SAILFISH_DPR"); + if (v && *v) { + const double dpr = std::atof(v); + if (dpr >= 0.5 && dpr <= 8.0) return dpr; + } + return 2.0; +} + +// Drive CSS animations each frame. Like geaos, this target runs its own loop +// and calls Application::frame() directly instead of going through the +// runtime's run_app_frame(), which is the only place that starts and ticks the +// CSS animation engine. Mirrors the geaos main loop / @geastack/core runtime.cpp. +void driveCssAnimations(std::uint32_t nowMs) +{ + static bool started = false; + if (!started) { + started = true; + gea::css::DeclarativeAnimations::scanAndStart(nowMs); + gea::embedded::ui::StyleSheet::instance().startCssAnimations(nowMs); + } + + gea::css::AnimationEngine::instance().tick(nowMs); +} + +volatile sig_atomic_t g_stop = 0; +bool g_paused = false; // window minimized/hidden — skip frames +bool g_resize_pending = false; // window size changed — apply before next frame +int g_resize_window_w = 0; +int g_resize_window_h = 0; + +// Inject an SDL mouse position as a touch. SDL mouse-event coordinates are +// ALREADY in logical/canvas space: SDL_RenderSetLogicalSize installs a +// renderer event watch that rewrites SDL_MOUSEMOTION/BUTTON coordinates into +// the logical coordinate system. Do NOT call SDL_RenderWindowToLogical here — +// that scales a second time and lands every touch at half the true position +// (found via a uinput-verified drag that hit-tested at ~(126,185) when the +// cursor sat at logical (254,387)). +void injectMouse(gea::platform::touch::Phase phase, bool touching, int x, int y) +{ + const int maxX = sailfish_canvas_width() - 1; + const int maxY = sailfish_canvas_height() - 1; + if (x < 0) x = 0; + if (y < 0) y = 0; + if (x > maxX) x = maxX; + if (y > maxY) y = maxY; + gea::platform::touch::Touchscreen::injectEvent(phase, touching, x, y); +} + +// ---- multi-touch (real touchscreens) -------------------------------------- +// SDL fingers → TouchRuntime::queueTouchEvent with a stable pointer id. +// injectEvent can't be used here: it feeds only the primary pointer. Pointer 0 +// is shared with the mouse — a touchscreen tap and a left-click behave +// identically (SDL's touch→mouse synthesis is disabled in sailfish_display.cpp, +// so a finger never arrives twice). + +constexpr int kMaxFingers = 10; +SDL_FingerID g_finger_ids[kMaxFingers]; +bool g_finger_used[kMaxFingers] = {}; + +int fingerSlot(SDL_FingerID id, bool allocate) +{ + for (int i = 0; i < kMaxFingers; i++) + if (g_finger_used[i] && g_finger_ids[i] == id) return i; + if (!allocate) return -1; + for (int i = 0; i < kMaxFingers; i++) { + if (!g_finger_used[i]) { + g_finger_used[i] = true; + g_finger_ids[i] = id; + return i; + } + } + return -1; +} + +void injectFinger(gea::platform::touch::Phase phase, bool touching, float nx, float ny, int pointerId) +{ + // tfinger coords are normalized; the renderer's logical-size event watch + // keeps them proportional to the logical canvas. + int x = (int)(nx * (float)sailfish_canvas_width()); + int y = (int)(ny * (float)sailfish_canvas_height()); + const int maxX = sailfish_canvas_width() - 1; + const int maxY = sailfish_canvas_height() - 1; + if (x < 0) x = 0; + if (y < 0) y = 0; + if (x > maxX) x = maxX; + if (y > maxY) y = maxY; + gea::framework::events::TouchRuntime::queueTouchEvent( + static_cast(phase), touching, x, y, pointerId); +} + +// ---- keyboard -------------------------------------------------------------- + +// SDL keysym → web keyCode (the framework's key event currency; see +// input::queueKeyDown). Unmapped keys return 0 and are dropped. +int webKeyCodeForSdl(SDL_Keycode sym) +{ + if (sym >= SDLK_a && sym <= SDLK_z) return 'A' + (int)(sym - SDLK_a); + if (sym >= SDLK_0 && sym <= SDLK_9) return '0' + (int)(sym - SDLK_0); + switch (sym) { + case SDLK_RETURN: + case SDLK_KP_ENTER: return 13; + case SDLK_BACKSPACE: return 8; + case SDLK_TAB: return 9; + case SDLK_SPACE: return 32; + case SDLK_PAGEUP: return 33; + case SDLK_PAGEDOWN: return 34; + case SDLK_END: return 35; + case SDLK_HOME: return 36; + case SDLK_LEFT: return 37; + case SDLK_UP: return 38; + case SDLK_RIGHT: return 39; + case SDLK_DOWN: return 40; + case SDLK_DELETE: return 46; + default: return 0; + } +} + +// Mark a mutated so its new value actually reaches the panel — same +// bits the virtual keyboard sets (see virtual_keyboard.cpp applyKeyToActiveInput: +// render.dirty alone records the DrawText command but the dirty-region tracker +// never flushes it). +void markNodeFullyDirty(gea::embedded::ui::Tree &tree, int nodeId) +{ + if (nodeId < 0 || nodeId >= tree.nodeCount()) return; + tree.markNodeDisplayCommandsDirty(nodeId); + tree.setDisplayListRebuildRequired(true); + gea::embedded::ui::Node &node = tree.node(nodeId); + node.render.dirty = 1; + node.render.layout_dirty = 1; + node.render.non_scroll_dirty = 1; +} + +// Physical-keyboard text entry: mutate the focused 's value attribute +// and fire the synthetic input/keydown events, mirroring what the on-screen +// virtual keyboard does for taps. Returns false when no input is focused. +bool appendTextToActiveInput(const char *utf8) +{ + auto &tree = gea::embedded::ui::Tree::instance(); + const int activeId = tree.activeInputId(); + if (activeId < 0) return false; + + const char *currentValue = tree.getAttribute(activeId, "value"); + std::string next = currentValue ? std::string(currentValue) : std::string(); + bool changed = false; + for (const char *p = utf8; *p; p++) { + // ASCII printable only — the raster pipeline's fonts cover ASCII. + if (*p >= 32 && *p < 127) { + next.push_back(*p); + changed = true; + } + } + if (!changed) return true; + tree.setAttribute(activeId, "value", next.c_str()); + markNodeFullyDirty(tree, activeId); + + gea::framework::events::PointerEvent ev{}; + ev.type = gea::framework::events::PointerEventType::Input; + ev.targetId = activeId; + tree.dispatchEvent(ev); + return true; +} + +bool applyBackspaceToActiveInput() +{ + auto &tree = gea::embedded::ui::Tree::instance(); + const int activeId = tree.activeInputId(); + if (activeId < 0) return false; + + const char *currentValue = tree.getAttribute(activeId, "value"); + std::string next = currentValue ? std::string(currentValue) : std::string(); + if (!next.empty()) { + next.pop_back(); + tree.setAttribute(activeId, "value", next.c_str()); + markNodeFullyDirty(tree, activeId); + gea::framework::events::PointerEvent ev{}; + ev.type = gea::framework::events::PointerEventType::Input; + ev.targetId = activeId; + tree.dispatchEvent(ev); + } + gea::framework::events::PointerEvent kd{}; + kd.type = gea::framework::events::PointerEventType::KeyDown; + kd.targetId = activeId; + kd.keyCode = 8; + tree.dispatchEvent(kd); + return true; +} + +// ---- per-frame input drains (mirrors core/runtime.cpp, which this target +// excludes because it runs its own loop) ------------------------------------ + +void dispatchRotaryInput() +{ + const int delta = gea::framework::input::consumeRotaryDelta(); + if (delta == 0) return; + gea::embedded::ui::dispatchDocumentRotary(delta); +} + +void dispatchKeyInput() +{ + auto &tree = gea::embedded::ui::Tree::instance(); + for (int keyCode = gea::framework::input::consumeKeyDown(); keyCode != 0; + keyCode = gea::framework::input::consumeKeyDown()) { + gea::framework::events::PointerEvent event{}; + event.type = gea::framework::events::PointerEventType::KeyDown; + event.keyCode = keyCode; + event.bubbles = true; + event.cancelable = true; + + const int inputId = tree.activeInputId(); + bool stopped = false; + if (inputId >= 0) { + event.targetId = inputId; + event.currentTargetId = inputId; + tree.dispatchEvent(event); + stopped = event.propagationStopped; + } + if (!stopped) { + event.propagationStopped = false; + gea::embedded::ui::dispatchDocumentKeyDown(event); + } + } +} + +void setPaused(bool paused) +{ + if (g_paused == paused) return; + g_paused = paused; + gea::framework::events::TouchRuntime::setDispatchEnabled(!paused); + if (!paused) sailfish_display_present(); // pixels may be stale after unminimize +} + +void pumpSdlEvents() +{ + using gea::platform::touch::Phase; + static bool mouseDown = false; + SDL_Event event; + while (SDL_PollEvent(&event)) { + switch (event.type) { + case SDL_QUIT: + g_stop = 1; + break; + case SDL_KEYDOWN: { + const SDL_Keycode sym = event.key.keysym.sym; + if (sym == SDLK_ESCAPE) { + g_stop = 1; + break; + } + if (sym == SDLK_F11) { + sailfish_display_toggle_fullscreen(); + break; + } + // Backspace/Enter on a focused take the virtual-keyboard + // path (value mutation + synthetic events); everything else is + // queued as a web keyCode and drained by dispatchKeyInput. + if (sym == SDLK_BACKSPACE && applyBackspaceToActiveInput()) break; + const int keyCode = webKeyCodeForSdl(sym); + if (keyCode != 0) gea::framework::input::queueKeyDown(keyCode); + break; + } + case SDL_TEXTINPUT: + appendTextToActiveInput(event.text.text); + break; + case SDL_MOUSEWHEEL: { + static const bool inputTrace = [] { + const char *v = std::getenv("GEA_INPUT_TRACE"); + return v && *v && *v != '0'; + }(); + int notches = event.wheel.y; + if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) notches = -notches; + if (notches != 0) { + // Wheel-up (notches > 0) scrolls toward the top: negative dy. + const bool scrolled = gea::embedded::ui::Tree::instance().scrollByKeyStep(-notches * 48); + // Also surface it as rotary detents for apps that listen + // (knob-first apps from the elecrow rotary board). + gea::framework::input::queueRotaryDelta(-notches); + if (inputTrace) + std::fprintf(stderr, "[sailfish.input] wheel notches=%d scrolled=%d\n", notches, scrolled ? 1 : 0); + } + break; + } + case SDL_MOUSEBUTTONDOWN: + if (event.button.button == SDL_BUTTON_LEFT) { + mouseDown = true; + injectMouse(Phase::Down, true, event.button.x, event.button.y); + } + break; + case SDL_MOUSEMOTION: + if (mouseDown) { + injectMouse(Phase::Move, true, event.motion.x, event.motion.y); + } + break; + case SDL_MOUSEBUTTONUP: + if (event.button.button == SDL_BUTTON_LEFT) { + mouseDown = false; + injectMouse(Phase::Up, false, event.button.x, event.button.y); + } + break; + case SDL_FINGERDOWN: { + const int slot = fingerSlot(event.tfinger.fingerId, true); + if (slot >= 0) injectFinger(Phase::Down, true, event.tfinger.x, event.tfinger.y, slot); + break; + } + case SDL_FINGERMOTION: { + const int slot = fingerSlot(event.tfinger.fingerId, false); + if (slot >= 0) injectFinger(Phase::Move, true, event.tfinger.x, event.tfinger.y, slot); + break; + } + case SDL_FINGERUP: { + const int slot = fingerSlot(event.tfinger.fingerId, false); + if (slot >= 0) { + injectFinger(Phase::Up, false, event.tfinger.x, event.tfinger.y, slot); + g_finger_used[slot] = false; + } + break; + } + case SDL_WINDOWEVENT: + switch (event.window.event) { + case SDL_WINDOWEVENT_SIZE_CHANGED: + g_resize_pending = true; + g_resize_window_w = event.window.data1; + g_resize_window_h = event.window.data2; + break; + case SDL_WINDOWEVENT_MINIMIZED: + case SDL_WINDOWEVENT_HIDDEN: + setPaused(true); + break; + case SDL_WINDOWEVENT_RESTORED: + case SDL_WINDOWEVENT_SHOWN: + setPaused(false); + break; + case SDL_WINDOWEVENT_EXPOSED: + sailfish_display_present(); + break; + } + break; + } + } +} + +void dispatchPendingEvents() +{ + for (int i = 0; i < 64; ++i) { + gea::framework::events::Event event{}; + if (!gea::framework::services::FrameScheduler::receiveEvent(&event)) return; + switch (event.type) { + case gea::framework::events::EventType::Touch: + gea::framework::events::TouchRuntime::dispatchEvent(event); + break; + case gea::framework::events::EventType::Frame: + case gea::framework::events::EventType::Timeout: + case gea::framework::events::EventType::SettingsToggle: + break; + } + } +} + +void on_sigterm(int) { g_stop = 1; } + +// Async-signal-safe-ish crash handler. Prints the faulting PC + a short +// backtrace before the process dies (same as geaos_main.cpp). +void on_fatal(int sig, siginfo_t *info, void *ctx) +{ + (void)ctx; + dprintf(2, "\n[sailfish] FATAL signal %d at addr %p\n", sig, + info ? info->si_addr : nullptr); + void *frames[32]; + int n = backtrace(frames, 32); + dprintf(2, "[sailfish] backtrace (%d frames):\n", n); + backtrace_symbols_fd(frames, n, 2); + _exit(128 + sig); +} + +} // namespace + +int main(int argc, char **argv) +{ + (void)argc; (void)argv; + + setvbuf(stdout, nullptr, _IOLBF, 0); + setvbuf(stderr, nullptr, _IOLBF, 0); + + signal(SIGTERM, on_sigterm); + signal(SIGINT, on_sigterm); + { + struct sigaction sa{}; + sa.sa_sigaction = on_fatal; + sa.sa_flags = SA_SIGINFO; + sigaction(SIGSEGV, &sa, nullptr); + sigaction(SIGBUS, &sa, nullptr); + sigaction(SIGABRT, &sa, nullptr); + } + + if (!gea::platform::display::Display::init()) { + std::fprintf(stderr, "[sailfish] display init failed (no SDL window)\n"); + return 1; + } + gea::platform::display::Display::start(); + + int w = sailfish_canvas_width(); + int h = sailfish_canvas_height(); + const double dpr = devicePixelRatio(); + + std::fprintf(stderr, "[sailfish] init %dx%d dpr=%.1f app=%s\n", w, h, dpr, GEA_SAILFISH_APP_ID); + + // Record the app identity: AppManager platform (Apps.currentInstalledAppId) + // and the C-side current-id used by generated launcher code. + gea_embedded_apps_launch(GEA_SAILFISH_APP_ID); + gea::sailfish::installAppLauncherPlatform(GEA_SAILFISH_APP_ID); + // Report the desktop's real link state — apps gate remote fetches on it. + sailfish_install_wifi_driver(); + + // Restore persisted localStorage BEFORE Application::init — app store + // init() reads localStorage during mount. Two views share the blob file: + // the geatsc runtime's host storage (what generated apps actually use; + // synced by sailfish_storage_bridge.cpp) and the native StorageFacade + // (mirrors runtime.cpp's boot-time Storage.load()). + gea::framework::services::StorageService::init(); + sailfish_runtime_storage_load(); + gea::host::Storage.load(); + + gea::framework::app::Application::init(w, h, dpr); + std::fprintf(stderr, "[sailfish] init returned\n"); + + auto eventQueue = gea::framework::services::FrameScheduler::createEventQueue(); + if (!eventQueue || !gea::framework::events::TouchRuntime::start()) { + std::fprintf(stderr, "[sailfish] touch input unavailable\n"); + } + + // Physical-keyboard text entry for focused nodes (SDL_TEXTINPUT). + SDL_StartTextInput(); + + auto &tree = gea::embedded::ui::Tree::instance(); + + // Per-second frame-rate logger, off by default; GEA_LOOP_PERF=1 enables it + // (same diagnostic as geaos_main.cpp's [geaos.loop] line). + static const bool loopPerfLog = [] { + const char *v = std::getenv("GEA_LOOP_PERF"); + return v && *v && *v != '0'; + }(); + int64_t fps_window_start_ns = 0; + int fps_window_frames = 0; + int64_t fps_window_app_frame_us = 0; + int64_t fps_window_tree_refresh_us = 0; + + while (!g_stop) { + struct timespec t0; + clock_gettime(CLOCK_MONOTONIC, &t0); + + pumpSdlEvents(); + + // Minimized/hidden: keep pumping events (so quit works) but skip + // frames entirely; input dispatch is gated off in setPaused. + if (g_paused) { + struct timespec idle{0, 100000000L}; // 100 ms + nanosleep(&idle, nullptr); + continue; + } + + // Apply a window resize before the frame: rebuild the framebuffer + + // texture at the new logical size, re-derive the framework viewport, + // and mark every node dirty so the next refresh repaints the world. + if (g_resize_pending) { + g_resize_pending = false; + const int scale = sailfish_display_window_scale(); + const int newW = g_resize_window_w / (scale > 0 ? scale : 1); + const int newH = g_resize_window_h / (scale > 0 ? scale : 1); + if (sailfish_display_resize(newW, newH)) { + w = sailfish_canvas_width(); + h = sailfish_canvas_height(); + gea::embedded::ui::setViewportMetrics(w, h, dpr); + gea::embedded::ui::Document::setPreferredMountSize(w, h); + const int count = tree.nodeCount(); + for (int id = 0; id < count; ++id) markNodeFullyDirty(tree, id); + std::fprintf(stderr, "[sailfish] resized to %dx%d (window %dx%d)\n", + w, h, g_resize_window_w, g_resize_window_h); + } + } + + dispatchPendingEvents(); + + struct timespec t_af0; + clock_gettime(CLOCK_MONOTONIC, &t_af0); + const int nowMs = gea_embedded_now_ms(); + dispatchRotaryInput(); + dispatchKeyInput(); + driveCssAnimations(static_cast(nowMs)); + gea::framework::app::Application::frame(nowMs); + // Persist localStorage mutations from this frame (no-op unless changed; + // mirrors runtime.cpp's per-frame Storage.flushPending()). Both views: + // the geatsc runtime store (bridge) and the native facade. + sailfish_runtime_storage_flush(); + gea::host::Storage.flushPending(); + struct timespec t_af1; + clock_gettime(CLOCK_MONOTONIC, &t_af1); + + int root = tree.mountedRoot(); + if (root >= 0) { + gea::embedded::ui::NodeHandle(root).style().width(w); + gea::embedded::ui::NodeHandle(root).style().height(h); + tree.refresh(root, w, h); + } + + struct timespec t1; + clock_gettime(CLOCK_MONOTONIC, &t1); + long elapsedNs = (t1.tv_sec - t0.tv_sec) * 1000000000L + (t1.tv_nsec - t0.tv_nsec); + + fps_window_frames++; + fps_window_app_frame_us += ((t_af1.tv_sec - t_af0.tv_sec) * 1000000L + (t_af1.tv_nsec - t_af0.tv_nsec) / 1000L); + fps_window_tree_refresh_us += ((t1.tv_sec - t_af1.tv_sec) * 1000000L + (t1.tv_nsec - t_af1.tv_nsec) / 1000L); + int64_t now_ns = (int64_t)t1.tv_sec * 1000000000LL + (int64_t)t1.tv_nsec; + if (fps_window_start_ns == 0) fps_window_start_ns = now_ns; + if (now_ns - fps_window_start_ns >= 1000000000L) { + if (loopPerfLog) { + const double window_s = (now_ns - fps_window_start_ns) / 1e9; + const double fps_real = fps_window_frames / window_s; + const double appMs = (fps_window_app_frame_us / 1000.0) / fps_window_frames; + const double treeMs = (fps_window_tree_refresh_us / 1000.0) / fps_window_frames; + const auto rafs = gea::host::animationFramePerfStatsRead(); + gea::host::animationFramePerfStatsReset(); + std::fprintf(stderr, "[sailfish.loop] fps=%.1f frames=%d window=%.2fs | app=%.2fms tree.refresh=%.2fms | raf req=%lld ran=%lld drop=%lld\n", + fps_real, fps_window_frames, window_s, appMs, treeMs, + (long long)rafs.requestCount, (long long)rafs.callbackCount, + (long long)rafs.droppedCount); + } + fps_window_start_ns = now_ns; + fps_window_frames = 0; + fps_window_app_frame_us = 0; + fps_window_tree_refresh_us = 0; + } + + // If the display presented this frame, SDL's vsynced present already + // paced it — an extra nanosleep would beat against the vsync period + // (same double-pacing trap as geaos). Only sleep on idle frames. + if (!sailfish_display_consume_vsync_wait()) { + const long sleepNs = (long)gea::framework::services::FrameScheduler::frameIntervalMs() * 1000000L; + long remainNs = sleepNs - elapsedNs; + if (remainNs > 0) { + struct timespec ts { remainNs / 1000000000L, remainNs % 1000000000L }; + nanosleep(&ts, nullptr); + } + } + } + + std::fprintf(stderr, "[sailfish] shutting down\n"); + return 0; +} diff --git a/targets/sailfish-os/main/sailfish_memory.cpp b/targets/sailfish-os/main/sailfish_memory.cpp new file mode 100644 index 0000000..6656e79 --- /dev/null +++ b/targets/sailfish-os/main/sailfish_memory.cpp @@ -0,0 +1,34 @@ +/* targets/sailfish-os/main/sailfish_memory.cpp + * Framework allocator — plain libc malloc (same as geaos/macos: there is no + * SPIRAM split on a desktop). */ +#include "memory.h" + +#include +#include + +namespace gea::framework::memory { + +void *Allocator::allocatePreferSpiram(std::size_t size, std::size_t alignment) +{ + if (size == 0) return nullptr; + if (alignment <= alignof(std::max_align_t)) return std::malloc(size); + void *ptr = nullptr; + if (posix_memalign(&ptr, alignment, size) != 0) return nullptr; + return ptr; +} + +void *Allocator::reallocatePreferSpiram(void *ptr, std::size_t size) +{ + if (size == 0) { + std::free(ptr); + return nullptr; + } + return std::realloc(ptr, size); +} + +void Allocator::free(void *ptr) noexcept +{ + std::free(ptr); +} + +} // namespace gea::framework::memory diff --git a/targets/sailfish-os/main/sailfish_network.cpp b/targets/sailfish-os/main/sailfish_network.cpp new file mode 100644 index 0000000..25ee2c2 --- /dev/null +++ b/targets/sailfish-os/main/sailfish_network.cpp @@ -0,0 +1,234 @@ +/* targets/sailfish-os/main/sailfish_network.cpp + * Real HTTP(S) for the framework's fetch facade, via libcurl. + * + * core's host/host/fetch.cpp has three arms: browser (emscripten), ESP-IDF, + * and a desktop fallback that calls two WEAK hooks — + * test_record_request(url, init) then test_canned_response(url) + * — and returns whatever the latter produces (empty by default). The Android + * target established the pattern of strong-overriding that seam to inject + * real responses; this target implements that seam using Sailfish's libcurl + * package. + * + * test_canned_response only receives the URL, so test_record_request stashes + * the request init in a thread_local for it to pick up. Both hooks are called + * back-to-back on the same thread by FetchHost::fetch — the caller's thread + * for a synchronous app fetch, or a detached worker for fetchAsync — so the + * thread_local is safe under concurrent async fetches, and blocking curl + * transfers only stall the thread that asked for them. + */ + +#include "host/fetch.h" +#include "wifi.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +thread_local gea::host::FetchRequestInit t_pending_init; +thread_local bool t_has_pending_init = false; + +size_t writeBody(char *data, size_t size, size_t nmemb, void *user) +{ + auto *body = static_cast *>(user); + body->insert(body->end(), reinterpret_cast(data), + reinterpret_cast(data) + size * nmemb); + return size * nmemb; +} + +struct HeaderState { + gea::host::FetchHeaderMap *headers; + std::string *statusText; +}; + +size_t writeHeader(char *data, size_t size, size_t nmemb, void *user) +{ + auto *state = static_cast(user); + std::string line(data, size * nmemb); + while (!line.empty() && (line.back() == '\r' || line.back() == '\n')) line.pop_back(); + if (line.rfind("HTTP/", 0) == 0) { + // Status line — headers of any previous hop (redirect) are stale. + state->headers->clear(); + const size_t code = line.find(' '); + if (code != std::string::npos) { + const size_t text = line.find(' ', code + 1); + if (text != std::string::npos) *state->statusText = line.substr(text + 1); + } + return size * nmemb; + } + const size_t colon = line.find(':'); + if (colon != std::string::npos) { + std::string key = line.substr(0, colon); + size_t start = colon + 1; + while (start < line.size() && line[start] == ' ') start++; + (*state->headers)[key] = line.substr(start); + } + return size * nmemb; +} + +gea::host::FetchResponse curlFetch(const std::string &url, const gea::host::FetchRequestInit &init) +{ + static std::once_flag globalInit; + std::call_once(globalInit, [] { curl_global_init(CURL_GLOBAL_DEFAULT); }); + + gea::host::FetchResponse response; + CURL *curl = curl_easy_init(); + if (!curl) return response; + + HeaderState headerState{&response.headers, &response.status_text}; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &writeBody); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response.body); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, &writeHeader); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &headerState); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L); + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); // required off the main thread + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); // gzip/deflate, auto-decoded + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 10000L); + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, (long)(init.timeout_ms > 0 ? init.timeout_ms : 30000)); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "gea-sailfish/1.0"); + + if (init.method == "POST") { + curl_easy_setopt(curl, CURLOPT_POST, 1L); + } else if (!init.method.empty() && init.method != "GET") { + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, init.method.c_str()); + } + if (!init.body.empty() || init.method == "POST" || init.method == "PUT" || init.method == "PATCH") { + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, init.body.data()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)init.body.size()); + } + + curl_slist *requestHeaders = nullptr; + for (const auto &kv : init.headers) { + const std::string line = kv.first + ": " + kv.second; + requestHeaders = curl_slist_append(requestHeaders, line.c_str()); + } + if (requestHeaders) curl_easy_setopt(curl, CURLOPT_HTTPHEADER, requestHeaders); + + const CURLcode rc = curl_easy_perform(curl); + if (rc == CURLE_OK) { + long code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code); + response.status = (double)code; + response.ok = code >= 200 && code < 300; + } else { + std::fprintf(stderr, "[sailfish net] fetch %s failed: %s\n", url.c_str(), curl_easy_strerror(rc)); + response.body.clear(); + } + + if (requestHeaders) curl_slist_free_all(requestHeaders); + curl_easy_cleanup(curl); + return response; +} + +} // namespace + +namespace gea::framework::host { + +// Strong overrides of the weak desktop-fallback hooks in host/host/fetch.cpp. +void test_record_request(const std::string &, const gea::host::FetchRequestInit &init) +{ + t_pending_init = init; + t_has_pending_init = true; +} + +gea::host::FetchResponse test_canned_response(const std::string &url) +{ + gea::host::FetchRequestInit init; + if (t_has_pending_init) { + init = std::move(t_pending_init); + t_pending_init = {}; + t_has_pending_init = false; + } + return curlFetch(url, init); +} + +} // namespace gea::framework::host + +namespace { + +// WiFi *status* driver. Several apps gate remote fetches on +// `wifi().connected()` (e.g. maps skips tile downloads without it), so the +// desktop must report its real link state even though fetch itself just uses +// the host TCP stack. "Connected" = the kernel has a default route — true for +// wired Ethernet too, which is the honest answer to "can I reach the +// network". Scanning/configuration are desktop no-ops (NetworkManager owns +// the link). +class LinuxWifiDriver final : public gea::framework::network::WifiDriver { +public: + bool init() override { return true; } + bool enabled() const override { return true; } + void setEnabled(bool) override {} + bool connected() const override { return !defaultRouteInterface().empty(); } + int rssi() override { return connected() ? -50 : 0; } + std::string ssid() override { return defaultRouteInterface(); } + std::string ip() const override + { + const std::string iface = defaultRouteInterface(); + if (iface.empty()) return ""; + std::string result; + struct ifaddrs *addrs = nullptr; + if (getifaddrs(&addrs) != 0) return ""; + for (struct ifaddrs *a = addrs; a; a = a->ifa_next) { + if (!a->ifa_addr || a->ifa_addr->sa_family != AF_INET) continue; + if (iface != a->ifa_name) continue; + char buf[INET_ADDRSTRLEN] = {0}; + const auto *sin = reinterpret_cast(a->ifa_addr); + if (inet_ntop(AF_INET, &sin->sin_addr, buf, sizeof(buf))) result = buf; + break; + } + freeifaddrs(addrs); + return result; + } + std::string mac() override + { + const std::string iface = defaultRouteInterface(); + if (iface.empty()) return ""; + std::ifstream f("/sys/class/net/" + iface + "/address"); + std::string mac; + std::getline(f, mac); + return mac; + } + void configure(const std::string &, const std::string &) override {} + void scan() override {} + bool scanning() const override { return false; } + int scanCount() const override { return 0; } + gea::framework::network::WifiNetwork networkAt(int) const override { return {}; } + std::vector scanResults() const override { return {}; } + +private: + // Interface of the kernel default route (/proc/net/route destination 0). + static std::string defaultRouteInterface() + { + std::ifstream route("/proc/net/route"); + std::string line; + std::getline(route, line); // header + while (std::getline(route, line)) { + std::istringstream fields(line); + std::string iface, dest; + if (!(fields >> iface >> dest)) continue; + if (dest == "00000000") return iface; + } + return ""; + } +}; + +} // namespace + +extern "C" void sailfish_install_wifi_driver() +{ + static LinuxWifiDriver driver; + gea::framework::network::WifiAdapter::setDriver(&driver); +} diff --git a/targets/sailfish-os/main/sailfish_sensors.cpp b/targets/sailfish-os/main/sailfish_sensors.cpp new file mode 100644 index 0000000..ecad438 --- /dev/null +++ b/targets/sailfish-os/main/sailfish_sensors.cpp @@ -0,0 +1,122 @@ +/* targets/sailfish-os/main/sailfish_sensors.cpp + * Accelerometer + memory-diagnostics stubs, plus the platform Touchscreen. + * + * Unlike geaos (which spawns an evdev reader thread), the linux target has no + * hardware touch reader: the SDL event pump in sailfish_main.cpp translates mouse + * events into Touchscreen::injectEvent calls on the main thread. This file + * only keeps the observer + latest-point cache the shared touch_runtime + * expects (see targets/geaos/main/geaos_sensors.cpp for the contract). */ + +#include "imu.h" +#include "memory.h" +#include "touch.h" + +#include +#include + +namespace gea::platform::sensors { + +void Accelerometer::init() {} +void Accelerometer::close() {} +void Accelerometer::calibrateBias() {} +int Accelerometer::tiltX() { return 0; } +int Accelerometer::tiltY() { return 0; } +double Accelerometer::accelerationX() { return 0.0; } +double Accelerometer::accelerationY() { return 0.0; } +double Accelerometer::accelerationZ() { return 9.80665; } +double Accelerometer::gyroscopeX() { return 0.0; } +double Accelerometer::gyroscopeY() { return 0.0; } +double Accelerometer::gyroscopeZ() { return 0.0; } +void Accelerometer::setWebTilt(int, int) {} + +} // namespace gea::platform::sensors + +// Memory diagnostics — zeros; the embedded-only metrics have no desktop analog. +namespace gea::platform::memory { + +std::uint32_t Memory::internalFree() { return 0; } +std::uint32_t Memory::internalLargestFreeBlock() { return 0; } +std::uint32_t Memory::internalMinimumFree() { return 0; } +std::uint32_t Memory::psramFree() { return 0; } +std::uint32_t Memory::currentTaskStackHighWaterMark() { return 0; } +std::uint32_t Memory::geaMainStackBytes() { return 0; } +std::uint32_t Memory::geaInitStackBytes() { return 0; } +std::uint32_t Memory::appFrameStackWords() { return 0; } +std::uint32_t Memory::appFrameStackBytes() { return 0; } +std::uint32_t Memory::displayFlushConfiguredRows() { return 0; } +std::uint32_t Memory::displayFlushConfiguredDepth() { return 0; } +std::uint32_t Memory::displayFlushBufferMaxBytes() { return 0; } +std::uint32_t Memory::displayFlushRows() { return 0; } +std::uint32_t Memory::displayFlushDepth() { return 0; } +std::uint32_t Memory::displayFlushBufferBytes() { return 0; } + +} // namespace gea::platform::memory + +namespace gea::platform::touch { + +namespace { + +struct TouchCache { + bool touching = false; + int x = 0; + int y = 0; +}; + +Touchscreen::Observer g_observer = nullptr; +TouchCache g_cache; +pthread_mutex_t g_touch_mutex = PTHREAD_MUTEX_INITIALIZER; + +void emitTouch(Phase phase, bool touching, int x, int y) +{ + pthread_mutex_lock(&g_touch_mutex); + g_cache.touching = touching; + g_cache.x = x; + g_cache.y = y; + Touchscreen::Observer observer = g_observer; + pthread_mutex_unlock(&g_touch_mutex); + if (observer) observer(phase, touching, x, y); +} + +} // namespace + +void Touchscreen::setObserver(Observer observer) +{ + g_observer = observer; +} + +void Touchscreen::injectEvent(Phase phase, bool touching, int x, int y) +{ + // Mirror a hardware reader: cache the coords (consumeLatestMove reads the + // cache) and notify the observer. Without the cache update, dispatch()'s + // consumeLatestMove() would overwrite injected move coords with a stale + // (0,0), collapsing every injected drag to a single bogus jump. + emitTouch(phase, touching, x, y); +} + +bool Touchscreen::init() +{ + // No reader thread: SDL mouse events are injected from the main loop. + return true; +} + +int Touchscreen::read(int *x, int *y) +{ + return readCached(x, y); +} + +int Touchscreen::readCached(int *x, int *y) +{ + pthread_mutex_lock(&g_touch_mutex); + const TouchCache cache = g_cache; + pthread_mutex_unlock(&g_touch_mutex); + if (x) *x = cache.x; + if (y) *y = cache.y; + return cache.touching ? 1 : 0; +} + +void Touchscreen::consumeLatestMove(int *x, int *y) +{ + (void)readCached(x, y); +} + +} // namespace gea::platform::touch diff --git a/targets/sailfish-os/main/sailfish_storage.cpp b/targets/sailfish-os/main/sailfish_storage.cpp new file mode 100644 index 0000000..09ae89e --- /dev/null +++ b/targets/sailfish-os/main/sailfish_storage.cpp @@ -0,0 +1,182 @@ +/* targets/sailfish-os/main/sailfish_storage.cpp + * File-backed StorageService — persistence for the app-facing `localStorage` + * (gea::host::StorageFacade serializes the whole KV set to one opaque blob; + * loadKv/saveKv store it verbatim) and for device-settings strings + * (getString/setString, a small chunked KV file of our own). + * + * Layout: $GEA_SAILFISH_STORAGE_DIR, else + * $XDG_DATA_HOME//, using the Sailjail identity. + * Falls back to ~/.local/share. Files: localstorage.bin, settings.bin. + * Writes are atomic (tmp + rename) so a crash mid-write never corrupts the + * previous state. saveKv is called from the frame task once per frame at most + * (StorageFacade::flushPending only writes when dirty), so plain synchronous + * IO is fine. + * + * The embedded analogue is targets/esp32/services/storage_service.cpp (NVS + * blob "ls_kv"). + */ + +#include "services/storage_service.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +std::string storageDir() +{ + static std::string dir = [] { + const char *override_dir = std::getenv("GEA_SAILFISH_STORAGE_DIR"); + if (override_dir && *override_dir) return std::string(override_dir); + std::string base; + const char *xdg = std::getenv("XDG_DATA_HOME"); + if (xdg && *xdg) { + base = xdg; + } else { + const char *home = std::getenv("HOME"); + base = std::string(home && *home ? home : ".") + "/.local/share"; + } + return base + "/" + GEA_SAILFISH_ORGANIZATION_NAME + "/" + GEA_SAILFISH_APPLICATION_NAME; + }(); + return dir; +} + +// mkdir -p for the storage dir (each missing component, EEXIST is fine). +bool ensureDir(const std::string &path) +{ + std::string partial; + partial.reserve(path.size()); + for (size_t i = 0; i <= path.size(); i++) { + if (i < path.size() && path[i] != '/') { + partial.push_back(path[i]); + continue; + } + if (!partial.empty() && ::mkdir(partial.c_str(), 0755) != 0 && errno != EEXIST) return false; + if (i < path.size()) partial.push_back('/'); + } + return true; +} + +bool readFile(const std::string &path, std::string &out) +{ + FILE *f = std::fopen(path.c_str(), "rb"); + if (!f) return false; + std::fseek(f, 0, SEEK_END); + const long size = std::ftell(f); + std::fseek(f, 0, SEEK_SET); + if (size < 0) { std::fclose(f); return false; } + out.resize((size_t)size); + const size_t got = size > 0 ? std::fread(&out[0], 1, (size_t)size, f) : 0; + std::fclose(f); + if (got != (size_t)size) { out.clear(); return false; } + return true; +} + +bool writeFileAtomic(const std::string &path, const std::string &data) +{ + if (!ensureDir(storageDir())) return false; + const std::string tmp = path + ".tmp"; + FILE *f = std::fopen(tmp.c_str(), "wb"); + if (!f) return false; + const bool wrote = data.empty() || std::fwrite(data.data(), 1, data.size(), f) == data.size(); + const bool flushed = std::fflush(f) == 0 && ::fsync(fileno(f)) == 0; + std::fclose(f); + if (!wrote || !flushed) { ::unlink(tmp.c_str()); return false; } + if (::rename(tmp.c_str(), path.c_str()) != 0) { ::unlink(tmp.c_str()); return false; } + return true; +} + +std::string localStoragePath() { return storageDir() + "/localstorage.bin"; } +std::string settingsPath() { return storageDir() + "/settings.bin"; } + +// Settings map, loaded lazily; persisted with the same 4-byte-length chunk +// framing StorageFacade uses for the localStorage blob. +std::unordered_map &settings() +{ + static std::unordered_map map = [] { + std::unordered_map loaded; + std::string blob; + if (readFile(settingsPath(), blob)) { + size_t pos = 0; + auto readChunk = [&](std::string &out) { + if (pos + sizeof(std::uint32_t) > blob.size()) return false; + std::uint32_t len = 0; + std::memcpy(&len, blob.data() + pos, sizeof(len)); + pos += sizeof(len); + if (pos + len > blob.size()) return false; + out.assign(blob.data() + pos, len); + pos += len; + return true; + }; + std::string k, v; + while (readChunk(k) && readChunk(v)) loaded[k] = v; + } + return loaded; + }(); + return map; +} + +bool persistSettings() +{ + std::string blob; + for (const auto &kv : settings()) { + for (const std::string *part : {&kv.first, &kv.second}) { + const std::uint32_t len = (std::uint32_t)part->size(); + char header[sizeof(len)]; + std::memcpy(header, &len, sizeof(len)); + blob.append(header, sizeof(header)); + blob.append(*part); + } + } + return writeFileAtomic(settingsPath(), blob); +} + +} // namespace + +namespace gea::framework::services { + +bool StorageService::init() { return ensureDir(storageDir()); } + +bool StorageService::getString(const char *key, char *buf, unsigned capacity) +{ + if (!key || !buf || capacity == 0) return false; + const auto &map = settings(); + const auto it = map.find(key); + if (it == map.end()) return false; + std::snprintf(buf, capacity, "%s", it->second.c_str()); + return true; +} + +bool StorageService::setString(const char *key, const char *value) +{ + if (!key) return false; + settings()[key] = value ? value : ""; + return persistSettings(); +} + +bool StorageService::loadKv(std::string &out) +{ + out.clear(); + readFile(localStoragePath(), out); + return true; +} + +void StorageService::saveKv(const std::string &blob) +{ + if (blob.empty()) { + ::unlink(localStoragePath().c_str()); + return; + } + writeFileAtomic(localStoragePath(), blob); +} + +} // namespace gea::framework::services diff --git a/targets/sailfish-os/main/sailfish_storage_bridge.cpp b/targets/sailfish-os/main/sailfish_storage_bridge.cpp new file mode 100644 index 0000000..b2a902c --- /dev/null +++ b/targets/sailfish-os/main/sailfish_storage_bridge.cpp @@ -0,0 +1,80 @@ +/* targets/sailfish-os/main/sailfish_storage_bridge.cpp + * Persistence bridge for the geatsc runtime's `localStorage`. + * + * geatsc-compiled apps don't use gea::host::StorageFacade — the generated + * runtime has its own in-RAM store, gea::host::storage::table() in the + * generated gea_runtime.h. Nothing in the generated runtime persists it, so + * without this bridge every localStorage write dies with the process. + * + * The bridge syncs that store with the target's StorageService file backend + * (sailfish_storage.cpp): load() before Application::init (app store init() + * reads localStorage), flush() once per frame — serialize, compare against + * the last persisted blob, write only on change. The blob framing matches + * gea::host::StorageFacade (4-byte host-order length-prefixed key/value + * chunks), so the two views stay interchangeable; a given app only ever + * mutates one of them. + * + * This file #includes the app's generated headers, so unlike the other + * sailfish_*.cpp platform files it recompiles per app (the generated dir is + * already on the include path). + */ + +#include "gea_runtime.h" + +#include "services/storage_service.h" + +#include +#include +#include + +namespace { + +std::string g_last_blob; + +std::string serializeEntries() +{ + auto &storage = gea::host::storage::table(); + std::string out; + for (const auto &entry : storage) { + for (const std::string *part : {&entry.first, &entry.second}) { + const std::uint32_t len = (std::uint32_t)part->size(); + char header[sizeof(len)]; + std::memcpy(header, &len, sizeof(len)); + out.append(header, sizeof(header)); + out.append(*part); + } + } + return out; +} + +} // namespace + +extern "C" void sailfish_runtime_storage_load() +{ + std::string blob; + gea::framework::services::StorageService::loadKv(blob); + auto &storage = gea::host::storage::table(); + storage.clear(); + std::size_t pos = 0; + auto readChunk = [&](std::string &out) { + if (pos + sizeof(std::uint32_t) > blob.size()) return false; + std::uint32_t len = 0; + std::memcpy(&len, blob.data() + pos, sizeof(len)); + pos += sizeof(len); + if (pos + len > blob.size()) return false; + out.assign(blob.data() + pos, len); + pos += len; + return true; + }; + std::string k, v; + while (readChunk(k) && readChunk(v)) storage[k] = v; + g_last_blob = blob; +} + +extern "C" void sailfish_runtime_storage_flush() +{ + std::string blob = serializeEntries(); + if (blob == g_last_blob) return; + gea::framework::services::StorageService::saveKv(blob); + g_last_blob.swap(blob); +} diff --git a/targets/sailfish-os/main/sailfish_timers.cpp b/targets/sailfish-os/main/sailfish_timers.cpp new file mode 100644 index 0000000..1d43ce3 --- /dev/null +++ b/targets/sailfish-os/main/sailfish_timers.cpp @@ -0,0 +1,128 @@ +/* targets/sailfish-os/main/sailfish_timers.cpp + * gea_embedded_now_ms() implementation + FrameScheduler glue. + * Same POSIX CLOCK_MONOTONIC + mutex-guarded ring queue as + * targets/geaos/main/geaos_timers.cpp. */ + +#include "services/frame_scheduler.h" + +#include "event.h" + +#include +#include +#include +#include + +namespace { +int millisSinceBoot() +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (int)(ts.tv_sec * 1000L + ts.tv_nsec / 1000000L); +} +int g_frame_interval_ms = gea::framework::services::FrameScheduler::kDefaultFrameIntervalMs; + +struct LinuxEventQueue { + gea::framework::events::Event events[gea::framework::services::FrameScheduler::kEventQueueDepth]{}; + int head = 0; + int tail = 0; + int count = 0; +}; + +LinuxEventQueue g_event_queue; +bool g_event_queue_created = false; +pthread_mutex_t g_event_queue_mutex = PTHREAD_MUTEX_INITIALIZER; + +void pushEventLocked(const gea::framework::events::Event &event) +{ + if (g_event_queue.count == gea::framework::services::FrameScheduler::kEventQueueDepth) { + // Scroll-pacing trace (GEA_SCROLL_TRACE=1): a full queue silently drops + // the OLDEST event — if that's a touch Down/Up the gesture state + // corrupts, which shows up as scroll jumps. Make the drop visible. + static const bool scrollTrace = [] { + const char *v = std::getenv("GEA_SCROLL_TRACE"); + return v && *v && *v != '0'; + }(); + if (scrollTrace) { + const auto &dropped = g_event_queue.events[g_event_queue.head]; + std::fprintf(stderr, "[sc.qdrop] t=%d type=%d ph=%d\n", millisSinceBoot(), + static_cast(dropped.type), static_cast(dropped.touchPhase)); + } + g_event_queue.head = (g_event_queue.head + 1) % gea::framework::services::FrameScheduler::kEventQueueDepth; + g_event_queue.count--; + } + g_event_queue.events[g_event_queue.tail] = event; + g_event_queue.tail = (g_event_queue.tail + 1) % gea::framework::services::FrameScheduler::kEventQueueDepth; + g_event_queue.count++; +} +} // namespace + +extern "C" int gea_embedded_now_ms(void) { return millisSinceBoot(); } + +namespace gea::framework::services { + +EventQueue FrameScheduler::createEventQueue() +{ + pthread_mutex_lock(&g_event_queue_mutex); + g_event_queue.head = 0; + g_event_queue.tail = 0; + g_event_queue.count = 0; + g_event_queue_created = true; + pthread_mutex_unlock(&g_event_queue_mutex); + return EventQueue{&g_event_queue}; +} + +EventQueue FrameScheduler::eventQueue() +{ + pthread_mutex_lock(&g_event_queue_mutex); + const bool created = g_event_queue_created; + pthread_mutex_unlock(&g_event_queue_mutex); + return created ? EventQueue{&g_event_queue} : EventQueue{}; +} + +bool FrameScheduler::sendEvent(const gea::framework::events::Event &event, int) +{ + pthread_mutex_lock(&g_event_queue_mutex); + if (!g_event_queue_created) { + pthread_mutex_unlock(&g_event_queue_mutex); + return false; + } + pushEventLocked(event); + pthread_mutex_unlock(&g_event_queue_mutex); + return true; +} + +bool FrameScheduler::receiveEvent(gea::framework::events::Event *event) +{ + if (!event) return false; + pthread_mutex_lock(&g_event_queue_mutex); + if (!g_event_queue_created || g_event_queue.count == 0) { + pthread_mutex_unlock(&g_event_queue_mutex); + return false; + } + *event = g_event_queue.events[g_event_queue.head]; + g_event_queue.head = (g_event_queue.head + 1) % kEventQueueDepth; + g_event_queue.count--; + pthread_mutex_unlock(&g_event_queue_mutex); + return true; +} +void FrameScheduler::start(EventQueue) {} +void FrameScheduler::runFrame(const FrameCallbacks &callbacks) +{ + if (callbacks.frame) callbacks.frame(millisSinceBoot(), callbacks.context); +} +void FrameScheduler::setFrameIntervalMs(int intervalMs) +{ + if (intervalMs < kMinFrameIntervalMs) intervalMs = kMinFrameIntervalMs; + if (intervalMs > kMaxFrameIntervalMs) intervalMs = kMaxFrameIntervalMs; + g_frame_interval_ms = intervalMs; +} +int FrameScheduler::frameIntervalMs() { return g_frame_interval_ms; } +void FrameScheduler::setFrameRate(double fps) +{ + if (fps <= 0.0) return; + setFrameIntervalMs(static_cast(1000.0 / fps + 0.5)); +} +double FrameScheduler::frameRate() { return 1000.0 / static_cast(g_frame_interval_ms); } +int FrameScheduler::nowMs() { return millisSinceBoot(); } + +} // namespace gea::framework::services diff --git a/targets/sailfish-os/prepare.mjs b/targets/sailfish-os/prepare.mjs index 255ce7e..9cdce40 100644 --- a/targets/sailfish-os/prepare.mjs +++ b/targets/sailfish-os/prepare.mjs @@ -62,8 +62,8 @@ for (const name of ['core', 'host', 'engine', 'elements', 'geaos']) { copyTree(source, path.join(stage, 'framework', name)) } copyTree(generated, path.join(stage, 'generated')) -copyTree(path.join(linuxRoot, 'targets/raspberry-pi-os/main'), path.join(stage, 'platform/main')) -copyTree(path.join(linuxRoot, 'targets/raspberry-pi-os/include'), path.join(stage, 'platform/include')) +copyTree(path.join(here, 'main'), path.join(stage, 'platform/main')) +copyTree(path.join(here, 'include'), path.join(stage, 'platform/include')) fs.copyFileSync(path.join(here, 'CMakeLists.txt'), path.join(stage, 'CMakeLists.txt')) const env = Object.fromEntries(['core', 'host', 'engine', 'elements', 'geaos'].map((name) => [ @@ -71,9 +71,9 @@ const env = Object.fromEntries(['core', 'host', 'engine', 'elements', 'geaos'].m path.join(stage, 'framework', name).replaceAll('\\', '/')])) const { includeFlags, cSources, cxxSources } = await import(pathToFileURL(path.join(coreRepo, 'packages/core/gea_sources.mjs'))) const rel = (file) => path.relative(stage, file).replaceAll('\\', '/') -const c = [...cSources(env).map(rel), 'platform/main/rpios_apps.c'] +const c = [...cSources(env).map(rel), 'platform/main/sailfish_apps.c'] const cxx = cxxSources(env).filter((s) => !/\/(?:host\/camera|runtime|services\/[a-z_]+)\.cpp$/.test(s)).map(rel) -for (const name of ['display', 'audio', 'memory', 'network', 'sensors', 'storage', 'storage_bridge', 'timers', 'app_platform', 'main']) cxx.push(`platform/main/rpios_${name}.cpp`) +for (const name of ['display', 'audio', 'memory', 'network', 'sensors', 'storage', 'storage_bridge', 'timers', 'app_platform', 'main']) cxx.push(`platform/main/sailfish_${name}.cpp`) cxx.push('framework/core/gea_app_entry.cpp') for (const line of fs.readFileSync(path.join(generated, 'geatsc-sources.txt'), 'utf8').split(/\r?\n/).filter(Boolean)) { const file = path.resolve(line) From 1f5e626057f12353a2466108ed9f698e36b2839d Mon Sep 17 00:00:00 2001 From: Mehmet Date: Wed, 30 Sep 2026 14:49:15 +0300 Subject: [PATCH 7/9] Fix display allocation and Sailfish touch mapping --- targets/raspberry-pi-os/main/rpios_display.cpp | 1 + targets/sailfish-os/main/sailfish_display.cpp | 13 +++++++++++++ targets/sailfish-os/main/sailfish_main.cpp | 11 +++++++---- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/targets/raspberry-pi-os/main/rpios_display.cpp b/targets/raspberry-pi-os/main/rpios_display.cpp index 18f2789..ac57750 100644 --- a/targets/raspberry-pi-os/main/rpios_display.cpp +++ b/targets/raspberry-pi-os/main/rpios_display.cpp @@ -99,6 +99,7 @@ void ensure_window() { if (g_sdl_ok) return; ensure_canvas(); + if (!g_framebuffer) return; static bool attempted = false; if (attempted) return; attempted = true; diff --git a/targets/sailfish-os/main/sailfish_display.cpp b/targets/sailfish-os/main/sailfish_display.cpp index c7924d6..a716cbf 100644 --- a/targets/sailfish-os/main/sailfish_display.cpp +++ b/targets/sailfish-os/main/sailfish_display.cpp @@ -97,6 +97,7 @@ void ensure_window() { if (g_sdl_ok) return; ensure_canvas(); + if (!g_framebuffer) return; static bool attempted = false; if (attempted) return; attempted = true; @@ -201,6 +202,18 @@ void copy_rgb565_rows_to_canvas(const uint16_t *pixels, int x, int y, int width, extern "C" int sailfish_canvas_width() { return g_canvas_width; } extern "C" int sailfish_canvas_height() { return g_canvas_height; } +extern "C" int sailfish_display_finger_to_logical(float nx, float ny, float *x, float *y) +{ + if (!g_sdl_ok || !g_window || !g_renderer || !x || !y) return 0; + int window_width = 0; + int window_height = 0; + SDL_GetWindowSize(g_window, &window_width, &window_height); + if (window_width <= 0 || window_height <= 0) return 0; + SDL_RenderWindowToLogical(g_renderer, static_cast(nx * window_width), + static_cast(ny * window_height), x, y); + return 1; +} + // Returns 1 (and clears the flag) if a present blocked on vsync since the last // call — the main loop uses this to avoid double-pacing (its own nanosleep on // top of SDL's vsynced present). Same contract as geaos_display_consume_vsync_wait. diff --git a/targets/sailfish-os/main/sailfish_main.cpp b/targets/sailfish-os/main/sailfish_main.cpp index b616db0..ad6f2ad 100644 --- a/targets/sailfish-os/main/sailfish_main.cpp +++ b/targets/sailfish-os/main/sailfish_main.cpp @@ -60,6 +60,7 @@ extern "C" void sailfish_display_present(); extern "C" void sailfish_display_toggle_fullscreen(); extern "C" int sailfish_display_window_scale(); extern "C" int sailfish_display_resize(int new_width, int new_height); +extern "C" int sailfish_display_finger_to_logical(float nx, float ny, float *x, float *y); extern "C" int gea_embedded_apps_launch(const char *app_id); extern "C" void sailfish_runtime_storage_load(); extern "C" void sailfish_runtime_storage_flush(); @@ -153,10 +154,12 @@ int fingerSlot(SDL_FingerID id, bool allocate) void injectFinger(gea::platform::touch::Phase phase, bool touching, float nx, float ny, int pointerId) { - // tfinger coords are normalized; the renderer's logical-size event watch - // keeps them proportional to the logical canvas. - int x = (int)(nx * (float)sailfish_canvas_width()); - int y = (int)(ny * (float)sailfish_canvas_height()); + // Finger coordinates are normalized to the window, including letterbox bars. + float logicalX = 0.0f; + float logicalY = 0.0f; + if (!sailfish_display_finger_to_logical(nx, ny, &logicalX, &logicalY)) return; + int x = (int)logicalX; + int y = (int)logicalY; const int maxX = sailfish_canvas_width() - 1; const int maxY = sailfish_canvas_height() - 1; if (x < 0) x = 0; From d316f6225312240653dbf74d786371e3271ae38e Mon Sep 17 00:00:00 2001 From: Mehmet Date: Wed, 30 Sep 2026 14:56:30 +0300 Subject: [PATCH 8/9] Keep Raspberry Pi OS sources unchanged --- .../raspberry-pi-os/main/rpios_display.cpp | 18 +++--------------- .../raspberry-pi-os/main/rpios_storage.cpp | 4 ++-- .../main/rpios_storage_bridge.cpp | 19 +++++++++++-------- 3 files changed, 16 insertions(+), 25 deletions(-) diff --git a/targets/raspberry-pi-os/main/rpios_display.cpp b/targets/raspberry-pi-os/main/rpios_display.cpp index ac57750..f3fecfc 100644 --- a/targets/raspberry-pi-os/main/rpios_display.cpp +++ b/targets/raspberry-pi-os/main/rpios_display.cpp @@ -83,15 +83,8 @@ int g_flush_depth = 0; void ensure_canvas() { if (g_framebuffer) return; - const size_t pixels = (size_t)g_canvas_width * (size_t)g_canvas_height; - uint16_t *fb = static_cast(std::calloc(pixels, sizeof(uint16_t))); - if (!fb) { - std::fprintf(stderr, "[rpios display] framebuffer allocation failed (%dx%d)\n", - g_canvas_width, g_canvas_height); - return; - } - g_framebuffer = fb; - g_framebuffer_pixels = pixels; + g_framebuffer_pixels = (size_t)g_canvas_width * (size_t)g_canvas_height; + g_framebuffer = static_cast(std::calloc(g_framebuffer_pixels, sizeof(uint16_t))); g_canvas.bindPixels(g_framebuffer, g_canvas_width, g_canvas_height); } @@ -99,7 +92,6 @@ void ensure_window() { if (g_sdl_ok) return; ensure_canvas(); - if (!g_framebuffer) return; static bool attempted = false; if (attempted) return; attempted = true; @@ -115,12 +107,11 @@ void ensure_window() #ifndef GEA_RPIOS_APP_TITLE #define GEA_RPIOS_APP_TITLE "gea" #endif - int window_flags = SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE; g_window = SDL_CreateWindow(GEA_RPIOS_APP_TITLE, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, g_canvas_width * g_window_scale, g_canvas_height * g_window_scale, - window_flags); + SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE); if (!g_window) { std::fprintf(stderr, "[rpios display] SDL_CreateWindow failed: %s\n", SDL_GetError()); return; @@ -311,14 +302,12 @@ gea::framework::graphics::Canvas *Display::canvas() void Display::rebindCanvasToFramebuffer() { ensure_canvas(); - if (!g_framebuffer) return; g_canvas.bindPixels(g_framebuffer, g_canvas_width, g_canvas_height); } void Display::clear() { ensure_canvas(); - if (!g_framebuffer) return; std::memset(g_framebuffer, 0, g_framebuffer_pixels * sizeof(uint16_t)); flush(); } @@ -326,7 +315,6 @@ void Display::clear() void Display::clearNoFlush() { ensure_canvas(); - if (!g_framebuffer) return; std::memset(g_framebuffer, 0, g_framebuffer_pixels * sizeof(uint16_t)); } diff --git a/targets/raspberry-pi-os/main/rpios_storage.cpp b/targets/raspberry-pi-os/main/rpios_storage.cpp index 27e43b8..ca36e75 100644 --- a/targets/raspberry-pi-os/main/rpios_storage.cpp +++ b/targets/raspberry-pi-os/main/rpios_storage.cpp @@ -4,8 +4,8 @@ * loadKv/saveKv store it verbatim) and for device-settings strings * (getString/setString, a small chunked KV file of our own). * - * Layout: $GEA_RPIOS_STORAGE_DIR, else $XDG_DATA_HOME/gea/, - * falling back to ~/.local/share. Files: localstorage.bin, settings.bin. + * Layout: $GEA_RPIOS_STORAGE_DIR, else $XDG_DATA_HOME/gea/, else + * ~/.local/share/gea/. Files: localstorage.bin, settings.bin. * Writes are atomic (tmp + rename) so a crash mid-write never corrupts the * previous state. saveKv is called from the frame task once per frame at most * (StorageFacade::flushPending only writes when dirty), so plain synchronous diff --git a/targets/raspberry-pi-os/main/rpios_storage_bridge.cpp b/targets/raspberry-pi-os/main/rpios_storage_bridge.cpp index a33b76e..590e327 100644 --- a/targets/raspberry-pi-os/main/rpios_storage_bridge.cpp +++ b/targets/raspberry-pi-os/main/rpios_storage_bridge.cpp @@ -2,8 +2,9 @@ * Persistence bridge for the geatsc runtime's `localStorage`. * * geatsc-compiled apps don't use gea::host::StorageFacade — the generated - * runtime has its own in-RAM store, gea::host::storage::table() in the - * generated gea_runtime.h. Nothing in the generated runtime persists it, so + * runtime has its own in-RAM store, gea::runtime::host::Storage, shared + * through gea::runtime::host::local_storage() (see the app's generated + * host_globals.{h,cpp}). Nothing in the generated runtime persists it, so * without this bridge every localStorage write dies with the process. * * The bridge syncs that store with the target's StorageService file backend @@ -19,7 +20,8 @@ * already on the include path). */ -#include "gea_runtime.h" +#include "runtime_pch.h" +#include "host_globals.h" #include "services/storage_service.h" @@ -33,9 +35,9 @@ std::string g_last_blob; std::string serializeEntries() { - auto &storage = gea::host::storage::table(); + auto &storage = gea::runtime::host::local_storage(); std::string out; - for (const auto &entry : storage) { + for (const auto &entry : storage.entries) { for (const std::string *part : {&entry.first, &entry.second}) { const std::uint32_t len = (std::uint32_t)part->size(); char header[sizeof(len)]; @@ -53,8 +55,8 @@ extern "C" void rpios_runtime_storage_load() { std::string blob; gea::framework::services::StorageService::loadKv(blob); - auto &storage = gea::host::storage::table(); - storage.clear(); + auto &storage = gea::runtime::host::local_storage(); + storage.entries.clear(); std::size_t pos = 0; auto readChunk = [&](std::string &out) { if (pos + sizeof(std::uint32_t) > blob.size()) return false; @@ -67,7 +69,8 @@ extern "C" void rpios_runtime_storage_load() return true; }; std::string k, v; - while (readChunk(k) && readChunk(v)) storage[k] = v; + while (readChunk(k) && readChunk(v)) storage.entries.push_back({k, v}); + storage.length = (double)storage.entries.size(); g_last_blob = blob; } From d3e1c3797cc8bdaec0500fb014eb23305c40de4b Mon Sep 17 00:00:00 2001 From: Mehmet Fiskindal <140706386+mehmetfiskindal@users.noreply.github.com> Date: Wed, 30 Sep 2026 21:14:58 +0300 Subject: [PATCH 9/9] Fix Sailfish density, touch mapping and app preparation --- targets/sailfish-os/CMakeLists.txt | 6 ++++ targets/sailfish-os/README.md | 30 ++++++++++++++++++- targets/sailfish-os/main/sailfish_display.cpp | 22 ++++---------- targets/sailfish-os/main/sailfish_main.cpp | 13 ++++---- targets/sailfish-os/prepare.mjs | 23 +++++++++++--- 5 files changed, 65 insertions(+), 29 deletions(-) diff --git a/targets/sailfish-os/CMakeLists.txt b/targets/sailfish-os/CMakeLists.txt index 58854ba..5c000b2 100644 --- a/targets/sailfish-os/CMakeLists.txt +++ b/targets/sailfish-os/CMakeLists.txt @@ -1,5 +1,8 @@ cmake_minimum_required(VERSION 3.16) project(gea_sailfish LANGUAGES C CXX) +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) +endif() set(CMAKE_C_STANDARD 11) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -16,11 +19,14 @@ target_compile_options(${GEA_PACKAGE_NAME} PRIVATE -funwind-tables) target_compile_definitions(${GEA_PACKAGE_NAME} PRIVATE GEA_EMBEDDED_HAS_GENERATED_FONTS=1 + GEA_EMBEDDED_TTF_RUNTIME_FONTS=1 $<$:GEA_EMBEDDED_GIF_C_API> GEA_SAILFISH_APP_ID="${GEA_APP_ID}" GEA_SAILFISH_APP_TITLE="${GEA_APP_TITLE}" GEA_SAILFISH_ORGANIZATION_NAME="${GEA_SAILFISH_ORGANIZATION_NAME}" GEA_SAILFISH_APPLICATION_NAME="${GEA_SAILFISH_APPLICATION_NAME}") +set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platform/main/sailfish_main.cpp + PROPERTIES COMPILE_DEFINITIONS GEA_SAILFISH_DEFAULT_DPR=${GEA_SAILFISH_DEFAULT_DPR}) target_link_libraries(${GEA_PACKAGE_NAME} PRIVATE PkgConfig::SDL2 PkgConfig::CURL pthread m rt) target_link_options(${GEA_PACKAGE_NAME} PRIVATE -Wl,--export-dynamic) diff --git a/targets/sailfish-os/README.md b/targets/sailfish-os/README.md index 9976cd4..1d985cf 100644 --- a/targets/sailfish-os/README.md +++ b/targets/sailfish-os/README.md @@ -9,6 +9,12 @@ selected build target. The default core checkout is `../core` relative to this Linux repository. Pass `--core-repo` (Bash) or `-CoreRepository` (PowerShell) for another location. +Code generation uses this repository's npm core, compiler, and plugin. The +source manifest and native framework come from the core checkout. Preparation +checks that the npm core and checkout package versions match and fails before +code generation if they differ. Use `npm ci` for the locked tool dependencies +and a matching core checkout. Local core edits are still included in the build. + From the Linux repository on Linux: ```sh @@ -24,7 +30,10 @@ The app's `gea.icons` entry supplies the icon source. Preparation generates `gea.sailfish.organizationName`, `applicationName`, and `permissions` in the app's `package.json`. The default identity is `org.geastack` and the app ID with hyphens replaced by underscores; permissions default to an empty list. -Set only permissions the app actually needs, such as `Audio` for sound. +Set only permissions the app actually needs, such as `Audio` for sound and +`Internet` for `fetch` or other network requests. For an app using both, set +`"permissions": ["Audio", "Internet"]`. An empty list does not allow networking; +permissions are not inferred from app source code. From the Linux repository on Windows: @@ -54,6 +63,25 @@ the organization and application names from the app's Sailjail identity. Runtime overrides are `GEA_SAILFISH_WIDTH`, `GEA_SAILFISH_HEIGHT`, `GEA_SAILFISH_SCALE`, `GEA_SAILFISH_DPR`, and `GEA_SAILFISH_STORAGE_DIR`. +Scale and DPR both default to 1: one CSS pixel is one canvas pixel and one SDL +window coordinate unit. The initial canvas is 410x502; resize events update it +to the available window dimensions divided by scale. DPR controls canvas pixels +per CSS pixel, while scale controls presentation magnification. Font generation +uses the same initial 410x502 viewport and the app's DPR; runtime TTF fonts adapt +to the current viewport and DPR after resize. CMake defaults to a Release build. + +Set `gea.sailfish.devicePixelRatio` in the app's `package.json` to change its +CSS density (default 1). For example, `"devicePixelRatio": 2` renders one CSS +pixel as two canvas pixels while keeping the SDL window scale at 1. A 720x1527 +window then provides a 360x763.5 CSS viewport. The same setting is used for font +generation and the executable's default DPR; `GEA_SAILFISH_DPR` overrides it at +runtime. This is useful for phone layouts designed around a 360 CSS pixel width. + +Fonts declared with `@font-face` are embedded as TTF files and rasterized at +runtime. Character coverage depends on the core checkout's runtime TTF +rasterizer. Turkish text requires a core version that includes Latin Extended-A; +the ASCII-only rasterizer does not provide those glyphs. + The i486 RPM was installed on the Sailfish OS 5.1.0.11 emulator; Tic Tac Toe rendered and mouse clicks changed the board. Physical device behavior has not been tested. diff --git a/targets/sailfish-os/main/sailfish_display.cpp b/targets/sailfish-os/main/sailfish_display.cpp index a716cbf..791519e 100644 --- a/targets/sailfish-os/main/sailfish_display.cpp +++ b/targets/sailfish-os/main/sailfish_display.cpp @@ -14,14 +14,16 @@ * framework relies on a working clip to constrain each dirty-region * replay; stubbing them tanks fps (see the geaos header comment). * - The window is an integer-scaled view of the logical canvas - * (GEA_SAILFISH_SCALE, default 1): the canvas stays at the app's tuned - * logical size while the window fills the device screen. + * (GEA_SAILFISH_SCALE, default 1). After a window resize, the canvas is + * resized to window size / scale. At the default scale and DPR of 1, + * one CSS pixel is one canvas pixel and one SDL window coordinate unit. * * Window size / scale / DPR come from the environment so apps tuned for a * particular panel can be reproduced exactly: - * GEA_SAILFISH_WIDTH logical canvas width (default 410 — amoled-2.06) - * GEA_SAILFISH_HEIGHT logical canvas height (default 502) + * GEA_SAILFISH_WIDTH initial canvas width (default 410) + * GEA_SAILFISH_HEIGHT initial canvas height (default 502) * GEA_SAILFISH_SCALE integer window scale (default 1) + * GEA_SAILFISH_DPR canvas pixels per CSS pixel (app setting, otherwise 1) * * The window is resizable: the main loop reacts to SDL_WINDOWEVENT_SIZE_CHANGED * by calling sailfish_display_resize(), which reallocates the framebuffer + @@ -202,18 +204,6 @@ void copy_rgb565_rows_to_canvas(const uint16_t *pixels, int x, int y, int width, extern "C" int sailfish_canvas_width() { return g_canvas_width; } extern "C" int sailfish_canvas_height() { return g_canvas_height; } -extern "C" int sailfish_display_finger_to_logical(float nx, float ny, float *x, float *y) -{ - if (!g_sdl_ok || !g_window || !g_renderer || !x || !y) return 0; - int window_width = 0; - int window_height = 0; - SDL_GetWindowSize(g_window, &window_width, &window_height); - if (window_width <= 0 || window_height <= 0) return 0; - SDL_RenderWindowToLogical(g_renderer, static_cast(nx * window_width), - static_cast(ny * window_height), x, y); - return 1; -} - // Returns 1 (and clears the flag) if a present blocked on vsync since the last // call — the main loop uses this to avoid double-pacing (its own nanosleep on // top of SDL's vsynced present). Same contract as geaos_display_consume_vsync_wait. diff --git a/targets/sailfish-os/main/sailfish_main.cpp b/targets/sailfish-os/main/sailfish_main.cpp index ad6f2ad..81ec873 100644 --- a/targets/sailfish-os/main/sailfish_main.cpp +++ b/targets/sailfish-os/main/sailfish_main.cpp @@ -60,7 +60,6 @@ extern "C" void sailfish_display_present(); extern "C" void sailfish_display_toggle_fullscreen(); extern "C" int sailfish_display_window_scale(); extern "C" int sailfish_display_resize(int new_width, int new_height); -extern "C" int sailfish_display_finger_to_logical(float nx, float ny, float *x, float *y); extern "C" int gea_embedded_apps_launch(const char *app_id); extern "C" void sailfish_runtime_storage_load(); extern "C" void sailfish_runtime_storage_flush(); @@ -83,7 +82,7 @@ double devicePixelRatio() const double dpr = std::atof(v); if (dpr >= 0.5 && dpr <= 8.0) return dpr; } - return 2.0; + return GEA_SAILFISH_DEFAULT_DPR; } // Drive CSS animations each frame. Like geaos, this target runs its own loop @@ -154,12 +153,10 @@ int fingerSlot(SDL_FingerID id, bool allocate) void injectFinger(gea::platform::touch::Phase phase, bool touching, float nx, float ny, int pointerId) { - // Finger coordinates are normalized to the window, including letterbox bars. - float logicalX = 0.0f; - float logicalY = 0.0f; - if (!sailfish_display_finger_to_logical(nx, ny, &logicalX, &logicalY)) return; - int x = (int)logicalX; - int y = (int)logicalY; + // SDL_RenderSetLogicalSize's event watch already normalizes finger events + // to the logical viewport, including letterbox offsets. Scale only once. + int x = (int)(nx * (float)sailfish_canvas_width()); + int y = (int)(ny * (float)sailfish_canvas_height()); const int maxX = sailfish_canvas_width() - 1; const int maxY = sailfish_canvas_height() - 1; if (x < 0) x = 0; diff --git a/targets/sailfish-os/prepare.mjs b/targets/sailfish-os/prepare.mjs index 9cdce40..fa30aee 100644 --- a/targets/sailfish-os/prepare.mjs +++ b/targets/sailfish-os/prepare.mjs @@ -14,6 +14,15 @@ if (!appArg || !['i486', 'armv7hl', 'aarch64'].includes(arch)) { } const appDir = path.resolve(appArg) const coreRepo = path.resolve(coreArg || path.join(linuxRoot, '../core')) +const corePackage = path.join(coreRepo, 'packages/core') +const npmCore = path.join(linuxRoot, 'node_modules/@geastack/core') +const coreMetadata = path.join(corePackage, 'package.json') +if (!fs.existsSync(coreMetadata)) throw new Error(`missing core package: ${coreMetadata}`) +const checkoutVersion = JSON.parse(fs.readFileSync(coreMetadata, 'utf8')).version +const installedVersion = JSON.parse(fs.readFileSync(path.join(npmCore, 'package.json'), 'utf8')).version +if (!checkoutVersion || checkoutVersion !== installedVersion) { + throw new Error(`core version mismatch: npm @geastack/core ${installedVersion}, checkout ${checkoutVersion}. Use a core checkout matching the installed npm package, or install the matching npm version.`) +} const meta = JSON.parse(fs.readFileSync(path.join(appDir, 'package.json'), 'utf8')) const app = meta.gea if (!app?.id || !/^[a-z0-9][a-z0-9-]*$/.test(app.id)) throw new Error('gea.id must be a lowercase package identifier') @@ -25,6 +34,8 @@ const sailfish = app.sailfish || {} const organizationName = sailfish.organizationName || 'org.geastack' const applicationName = sailfish.applicationName || app.id.replaceAll('-', '_') const permissions = sailfish.permissions || [] +const devicePixelRatio = sailfish.devicePixelRatio ?? 1 +if (typeof devicePixelRatio !== 'number' || !Number.isFinite(devicePixelRatio) || devicePixelRatio < 0.5 || devicePixelRatio > 8) throw new Error('gea.sailfish.devicePixelRatio must be a number between 0.5 and 8') if (!/^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)+$/.test(organizationName)) throw new Error('gea.sailfish.organizationName must be a reverse-domain name') if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(applicationName)) throw new Error('gea.sailfish.applicationName must contain only letters, digits, and underscores') if (!Array.isArray(permissions) || !permissions.every((item) => typeof item === 'string' && /^[A-Za-z]+$/.test(item))) throw new Error('gea.sailfish.permissions must be an array of permission names') @@ -38,12 +49,13 @@ const packageName = `harbour-gea-${app.id}` const generated = path.join(here, 'generated', app.id) const stage = path.join(here, 'build', `${app.id}-${arch}`, 'project') fs.mkdirSync(path.dirname(generated), { recursive: true }) -const npmCore = path.join(linuxRoot, 'node_modules/@geastack/core') +// The npm scripts/runtime and native checkout must have the same core version. const args = [path.join(npmCore, 'scripts/build-gea-vite-geatsc.mjs'), '--app-dir', appDir, '--entry', entry, '--out-dir', generated, '--geatsc-bin', path.join(linuxRoot, 'node_modules/@geastack/compiler/dist/cli.js'), '--geatsc-gea-plugin', path.join(linuxRoot, 'node_modules/@geastack/geatsc-plugin-gea/dist/index.js'), - '--font-viewport-width', '410', '--font-viewport-height', '502', '--font-device-pixel-ratio', '2'] + '--font-viewport-width', '410', '--font-viewport-height', '502', '--font-device-pixel-ratio', String(devicePixelRatio), + '--runtime-ttf-fonts'] const result = spawnSync(process.execPath, args, { stdio: 'inherit', cwd: linuxRoot }) if (result.status !== 0) process.exit(result.status || 1) @@ -55,7 +67,7 @@ if (!resolvedStage.startsWith(allowedParent)) throw new Error('unsafe stage dire fs.rmSync(stage, { recursive: true, force: true }) fs.mkdirSync(stage, { recursive: true }) const copyTree = (from, to) => fs.cpSync(from, to, { recursive: true, - filter: (item) => !['.git', 'node_modules', 'dist', 'build'].includes(path.basename(item)) }) + filter: (item) => !['.git', 'node_modules', 'dist', 'build', 'test', 'tests', '.build-test'].includes(path.basename(item)) }) for (const name of ['core', 'host', 'engine', 'elements', 'geaos']) { const source = path.join(coreRepo, 'packages', name) if (!fs.existsSync(source)) throw new Error(`missing framework package: ${source}`) @@ -88,12 +100,15 @@ for (const file of [...c, ...cxx]) if (!fs.existsSync(path.join(stage, file))) t const cmakeList = (key, values) => `set(${key}\n${values.map((s) => ` "${'${CMAKE_CURRENT_SOURCE_DIR}'}/${s}"`).join('\n')}\n)\n` fs.writeFileSync(path.join(stage, 'sources.cmake'), `set(GEA_PACKAGE_NAME "${packageName}")\nset(GEA_APP_ID "${app.id}")\nset(GEA_APP_TITLE "${appName}")\nset(GEA_SAILFISH_ORGANIZATION_NAME "${organizationName}")\nset(GEA_SAILFISH_APPLICATION_NAME "${applicationName}")\n` + + `set(GEA_SAILFISH_DEFAULT_DPR ${devicePixelRatio})\n` + cmakeList('GEA_INCLUDE_DIRS', [...new Set(includes)]) + cmakeList('GEA_C_SOURCES', c) + cmakeList('GEA_CXX_SOURCES', cxx)) const icon = app.icons?.['512'] || app.icons?.['256'] || app.icons?.['128'] if (!icon || !fs.existsSync(path.join(appDir, icon))) throw new Error('app needs an icon source') fs.mkdirSync(path.join(stage, 'icons')) for (const size of [86, 108, 128, 172]) { - await sharp(path.join(appDir, icon)).resize(size, size, { fit: 'contain' }).png().toFile(path.join(stage, 'icons', `${size}.png`)) + await sharp(path.join(appDir, icon)).ensureAlpha().resize(size, size, { + fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 }, + }).png().toFile(path.join(stage, 'icons', `${size}.png`)) } fs.writeFileSync(path.join(stage, `${packageName}.desktop`), `[Desktop Entry]\nType=Application\nName=${appName}\nExec=${packageName}\nIcon=${packageName}\nX-Nemo-Application-Type=generic\n\n[X-Sailjail]\nOrganizationName=${organizationName}\nApplicationName=${applicationName}\nPermissions=${[...new Set(permissions)].join(';')}\n`) fs.mkdirSync(path.join(stage, 'rpm'))