diff --git a/.github/workflows/build-and-upload.yml b/.github/workflows/build-and-upload.yml index 2dd97162..5b627dbc 100644 --- a/.github/workflows/build-and-upload.yml +++ b/.github/workflows/build-and-upload.yml @@ -311,18 +311,42 @@ jobs: - name: Ensure rollup native binary run: npm install @rollup/rollup-linux-x64-gnu --no-save + - name: Install Flatpak build dependencies (Electron) + run: | + sudo apt-get update + sudo apt-get install -y flatpak flatpak-builder + sudo flatpak remote-add --system --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + sudo flatpak install --system --noninteractive flathub \ + org.freedesktop.Platform//24.08 \ + org.freedesktop.Sdk//24.08 + - name: Build Linux binaries (Electron) run: npm run build:linux --workspace @neuralnomads/codenomad-electron-app + - name: Build Flatpak bundle (Electron) + run: npm run build:flatpak --workspace @neuralnomads/codenomad-electron-app + - name: Verify bundled Node resource (Electron Linux) run: node scripts/verify-bundled-node.cjs packages/electron-app/electron/resources@linux-x64 + - name: Verify Electron Linux artifacts + run: | + set -euo pipefail + shopt -s nullglob + zips=(packages/electron-app/release/*.zip) + appimages=(packages/electron-app/release/*.AppImage) + flatpaks=(packages/electron-app/release/*.flatpak) + if [ "${#zips[@]}" -eq 0 ] || [ "${#appimages[@]}" -eq 0 ] || [ "${#flatpaks[@]}" -eq 0 ]; then + echo "Missing Electron Linux artifact(s): zip=${#zips[@]} appimage=${#appimages[@]} flatpak=${#flatpaks[@]}" >&2 + exit 1 + fi + - name: Upload release assets if: ${{ inputs.upload && inputs.tag != '' }} run: | set -euo pipefail shopt -s nullglob - for file in packages/electron-app/release/*.zip packages/electron-app/release/*.AppImage; do + for file in packages/electron-app/release/*.zip packages/electron-app/release/*.AppImage packages/electron-app/release/*.flatpak; do [ -f "$file" ] || continue echo "Uploading $file" gh release upload "$TAG" "$file" --clobber @@ -336,6 +360,7 @@ jobs: path: | packages/electron-app/release/*.zip packages/electron-app/release/*.AppImage + packages/electron-app/release/*.flatpak retention-days: ${{ inputs.actions_artifacts_retention_days }} if-no-files-found: error @@ -637,7 +662,15 @@ jobs: libwebkit2gtk-4.1-dev \ libsoup-3.0-dev \ libayatana-appindicator3-dev \ - librsvg2-dev + librsvg2-dev \ + xdg-utils \ + rpm \ + flatpak \ + flatpak-builder + sudo flatpak remote-add --system --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + sudo flatpak install --system --noninteractive flathub \ + org.gnome.Platform//47 \ + org.gnome.Sdk//47 - name: Set workspace versions if: ${{ inputs.set_versions && inputs.version != '' }} @@ -673,12 +706,43 @@ jobs: working-directory: packages/tauri-app run: npm exec -- tauri build + - name: Build Flatpak bundle (Tauri) + run: npm run build:flatpak --workspace @codenomad/tauri-app + + - name: Verify Tauri Flatpak runtime closure + run: | + set -euo pipefail + shopt -s nullglob + flatpaks=(packages/tauri-app/target/release/bundle/flatpak/*.flatpak) + if [ "${#flatpaks[@]}" -eq 0 ]; then + echo "Missing Tauri Flatpak artifact" >&2 + exit 1 + fi + + flatpak install --user --noninteractive --bundle "${flatpaks[0]}" + cleanup() { + flatpak uninstall --user --noninteractive ai.neuralnomads.codenomad.client >/dev/null 2>&1 || true + } + trap cleanup EXIT + + flatpak run --user --command=sh ai.neuralnomads.codenomad.client -c ' + set -e + test -x /app/bin/codenomad-tauri + test -d /app/lib/CodeNomad/resources + ldd /app/bin/codenomad-tauri | tee /tmp/codenomad-tauri-ldd.txt + ! grep -q "not found" /tmp/codenomad-tauri-ldd.txt + ' + - name: Package Tauri artifacts (Linux) if: ${{ inputs.upload || inputs.upload_actions_artifacts }} run: | set -euo pipefail SEARCH_ROOT="packages/tauri-app/target" ARTIFACT_DIR="packages/tauri-app/release-tauri" + VERSION_TO_USE="${VERSION:-}" + if [ -z "$VERSION_TO_USE" ]; then + VERSION_TO_USE=$(node -p "require('./packages/tauri-app/package.json').version") + fi rm -rf "$ARTIFACT_DIR" mkdir -p "$ARTIFACT_DIR" shopt -s nullglob globstar @@ -688,15 +752,21 @@ jobs: } appimage=$(find_one "*.AppImage") + deb=$(find_one "*.deb") + rpm=$(find_one "*.rpm") + flatpak=$(find_one "*.flatpak") fallback_bin="$SEARCH_ROOT/release/codenomad-tauri" - if [ -z "$appimage" ] || [ ! -f "$fallback_bin" ]; then - echo "Missing bundle(s): appimage=${appimage:-none} binary=$fallback_bin" >&2 + if [ -z "$appimage" ] || [ -z "$deb" ] || [ -z "$rpm" ] || [ -z "$flatpak" ] || [ ! -f "$fallback_bin" ]; then + echo "Missing bundle(s): appimage=${appimage:-none} deb=${deb:-none} rpm=${rpm:-none} flatpak=${flatpak:-none} binary=$fallback_bin" >&2 exit 1 fi - cp "$appimage" "$ARTIFACT_DIR/CodeNomad-Tauri-linux-x64-${VERSION}.AppImage" - zip -j "$ARTIFACT_DIR/CodeNomad-Tauri-linux-x64-${VERSION}.zip" "$fallback_bin" + cp "$appimage" "$ARTIFACT_DIR/CodeNomad-Tauri-linux-x64-${VERSION_TO_USE}.AppImage" + cp "$deb" "$ARTIFACT_DIR/CodeNomad-Tauri-linux-x64-${VERSION_TO_USE}.deb" + cp "$rpm" "$ARTIFACT_DIR/CodeNomad-Tauri-linux-x64-${VERSION_TO_USE}.rpm" + cp "$flatpak" "$ARTIFACT_DIR/CodeNomad-Tauri-linux-x64-${VERSION_TO_USE}.flatpak" + zip -j "$ARTIFACT_DIR/CodeNomad-Tauri-linux-x64-${VERSION_TO_USE}.zip" "$fallback_bin" - name: Upload Actions artifacts (Tauri Linux) if: ${{ inputs.upload_actions_artifacts }} diff --git a/packages/electron-app/package.json b/packages/electron-app/package.json index f6a1fe75..74909621 100644 --- a/packages/electron-app/package.json +++ b/packages/electron-app/package.json @@ -34,6 +34,7 @@ "build:linux": "node scripts/build.js linux", "build:linux-arm64": "node scripts/build.js linux-arm64", "build:linux-rpm": "node scripts/build.js linux-rpm", + "build:flatpak": "node scripts/build-flatpak.js", "build:all": "node scripts/build.js all", "package:mac": "node scripts/build.js mac", "package:win": "node scripts/build.js win", diff --git a/packages/electron-app/scripts/build-flatpak.js b/packages/electron-app/scripts/build-flatpak.js new file mode 100644 index 00000000..f1925ba0 --- /dev/null +++ b/packages/electron-app/scripts/build-flatpak.js @@ -0,0 +1,119 @@ +#!/usr/bin/env node + +import fs from "fs" +import path from "path" +import { spawnSync } from "child_process" +import { fileURLToPath } from "url" + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const root = path.resolve(__dirname, "..") +const workspaceRoot = path.resolve(root, "..", "..") +const version = process.env.VERSION || JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version +const appId = "ai.neuralnomads.codenomad.client" +const releaseRoot = path.join(root, "release") +const appSource = path.join(releaseRoot, "linux-unpacked") +const buildRoot = path.join(root, "target", "flatpak") +const stagingRoot = path.join(buildRoot, "staging") +const repoRoot = path.join(buildRoot, "repo") +const flatpakBuildRoot = path.join(buildRoot, "build") +const manifestPath = path.join(buildRoot, `${appId}.electron.json`) +const artifactPath = path.join(releaseRoot, `CodeNomad-Electron-linux-x64-${version}.flatpak`) + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { stdio: "inherit", ...options }) + if (result.error) throw result.error + if (result.status !== 0) { + throw new Error(`${command} ${args.join(" ")} exited with code ${result.status || 1}`) + } +} + +function copyRequired(from, to) { + if (!fs.existsSync(from)) { + throw new Error(`Missing required Flatpak input: ${from}`) + } + fs.cpSync(from, to, { recursive: true, dereference: true }) +} + +fs.rmSync(buildRoot, { recursive: true, force: true }) +fs.mkdirSync(stagingRoot, { recursive: true }) +fs.mkdirSync(releaseRoot, { recursive: true }) + +copyRequired(appSource, path.join(stagingRoot, "CodeNomad")) +copyRequired(path.join(root, "electron", "resources", "server", "public", "pwa-512x512.png"), path.join(stagingRoot, `${appId}.png`)) + +fs.writeFileSync( + path.join(stagingRoot, `${appId}.desktop`), + [ + "[Desktop Entry]", + "Type=Application", + "Name=CodeNomad", + "Exec=codenomad-electron", + `Icon=${appId}`, + "Terminal=false", + "Categories=Development;IDE;", + "StartupWMClass=CodeNomad", + "", + ].join("\n"), +) + +fs.writeFileSync( + path.join(stagingRoot, "codenomad-electron"), + [ + "#!/bin/sh", + "exec /app/CodeNomad/CodeNomad \"$@\"", + "", + ].join("\n"), + { mode: 0o755 }, +) + +const manifest = { + "app-id": appId, + runtime: "org.freedesktop.Platform", + "runtime-version": "24.08", + sdk: "org.freedesktop.Sdk", + branch: "stable", + command: "codenomad-electron", + "finish-args": [ + "--socket=wayland", + "--socket=x11", + "--share=ipc", + "--device=dri", + "--socket=pulseaudio", + "--filesystem=home", + "--share=network", + "--talk-name=org.freedesktop.Notifications", + ], + modules: [ + { + name: "codenomad-electron", + buildsystem: "simple", + "build-commands": [ + "mkdir -p /app/CodeNomad", + "cp -a CodeNomad/. /app/CodeNomad/", + "install -Dm755 codenomad-electron /app/bin/codenomad-electron", + `install -Dm644 ${appId}.desktop /app/share/applications/${appId}.desktop`, + `install -Dm644 ${appId}.png /app/share/icons/hicolor/512x512/apps/${appId}.png`, + ], + sources: [ + { + type: "dir", + path: stagingRoot, + }, + ], + }, + ], +} + +fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`) +fs.rmSync(repoRoot, { recursive: true, force: true }) +fs.rmSync(flatpakBuildRoot, { recursive: true, force: true }) +fs.rmSync(artifactPath, { force: true }) + +run("flatpak-builder", ["--force-clean", `--repo=${repoRoot}`, flatpakBuildRoot, manifestPath], { + cwd: workspaceRoot, +}) +run("flatpak", ["build-bundle", repoRoot, artifactPath, appId, "stable"], { + cwd: workspaceRoot, +}) + +console.log(`[flatpak] wrote ${artifactPath}`) diff --git a/packages/opencode-plugin/package.json b/packages/opencode-plugin/package.json index a77516bb..7c9d2cb7 100644 --- a/packages/opencode-plugin/package.json +++ b/packages/opencode-plugin/package.json @@ -10,7 +10,7 @@ "README.md" ], "scripts": { - "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json" + "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && npm exec -- tsc -p tsconfig.json" }, "dependencies": { "@opencode-ai/plugin": "1.3.7" diff --git a/packages/tauri-app/package.json b/packages/tauri-app/package.json index 590a3a4c..d496c925 100644 --- a/packages/tauri-app/package.json +++ b/packages/tauri-app/package.json @@ -11,6 +11,7 @@ "sync:version": "node ./scripts/sync-tauri-version.js", "prebuild": "node ./scripts/prebuild.js", "bundle:server": "npm run prebuild", + "build:flatpak": "node ./scripts/build-flatpak.js", "build": "tauri build", "smoke:resources": "node ../../scripts/smoke-packaged-resources.cjs --resources src-tauri/resources --loading src-tauri/resources/ui-loading" }, diff --git a/packages/tauri-app/scripts/build-flatpak.js b/packages/tauri-app/scripts/build-flatpak.js new file mode 100644 index 00000000..8f415faf --- /dev/null +++ b/packages/tauri-app/scripts/build-flatpak.js @@ -0,0 +1,96 @@ +#!/usr/bin/env node +const fs = require("fs") +const path = require("path") +const { spawnSync } = require("child_process") + +const root = path.resolve(__dirname, "..") +const workspaceRoot = path.resolve(root, "..", "..") +const version = process.env.VERSION || require(path.join(root, "package.json")).version +const appId = "ai.neuralnomads.codenomad.client" +const buildRoot = path.join(root, "target", "flatpak") +const stagingRoot = path.join(buildRoot, "staging") +const repoRoot = path.join(buildRoot, "repo") +const flatpakBuildRoot = path.join(buildRoot, "build") +const manifestPath = path.join(buildRoot, `${appId}.json`) +const artifactDir = path.join(root, "target", "release", "bundle", "flatpak") +const artifactPath = path.join(artifactDir, `CodeNomad-Tauri-linux-x64-${version}.flatpak`) +const desktopSource = path.join(root, "src-tauri", "icons", "linux", `${appId}.desktop`) + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { stdio: "inherit", ...options }) + if (result.error) throw result.error + if (result.status !== 0) { + throw new Error(`${command} ${args.join(" ")} exited with code ${result.status || 1}`) + } +} + +function copyRequired(from, to) { + if (!fs.existsSync(from)) { + throw new Error(`Missing required Flatpak input: ${from}`) + } + fs.cpSync(from, to, { recursive: true, dereference: true }) +} + +fs.rmSync(buildRoot, { recursive: true, force: true }) +fs.mkdirSync(stagingRoot, { recursive: true }) +fs.mkdirSync(artifactDir, { recursive: true }) + +copyRequired(path.join(root, "target", "release", "codenomad-tauri"), path.join(stagingRoot, "codenomad-tauri")) +copyRequired(path.join(root, "target", "release", "resources"), path.join(stagingRoot, "resources")) +copyRequired( + path.join(root, "src-tauri", "icons", "linux", "512x512.png"), + path.join(stagingRoot, "codenomad-tauri.png"), +) +copyRequired(desktopSource, path.join(stagingRoot, `${appId}.desktop`)) + +const manifest = { + "app-id": appId, + runtime: "org.gnome.Platform", + "runtime-version": "47", + sdk: "org.gnome.Sdk", + branch: "stable", + command: "codenomad-tauri", + "finish-args": [ + "--socket=wayland", + "--socket=x11", + "--share=ipc", + "--device=dri", + "--socket=pulseaudio", + "--filesystem=home", + "--share=network", + "--talk-name=org.freedesktop.Notifications", + ], + modules: [ + { + name: "codenomad-tauri", + "buildsystem": "simple", + "build-commands": [ + "install -Dm755 codenomad-tauri /app/bin/codenomad-tauri", + "mkdir -p /app/lib/CodeNomad", + "cp -a resources /app/lib/CodeNomad/resources", + `install -Dm644 ${appId}.desktop /app/share/applications/${appId}.desktop`, + "install -Dm644 codenomad-tauri.png /app/share/icons/hicolor/512x512/apps/codenomad-tauri.png", + ], + sources: [ + { + type: "dir", + path: stagingRoot, + }, + ], + }, + ], +} + +fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`) +fs.rmSync(repoRoot, { recursive: true, force: true }) +fs.rmSync(flatpakBuildRoot, { recursive: true, force: true }) +fs.rmSync(artifactPath, { force: true }) + +run("flatpak-builder", ["--force-clean", `--repo=${repoRoot}`, flatpakBuildRoot, manifestPath], { + cwd: workspaceRoot, +}) +run("flatpak", ["build-bundle", repoRoot, artifactPath, appId, "stable"], { + cwd: workspaceRoot, +}) + +console.log(`[flatpak] wrote ${artifactPath}`) diff --git a/scripts/desktop-server-resources.cjs b/scripts/desktop-server-resources.cjs index 000219cd..c906b5a8 100644 --- a/scripts/desktop-server-resources.cjs +++ b/scripts/desktop-server-resources.cjs @@ -23,7 +23,7 @@ function copyRequiredArtifact(serverRoot, serverDest, name, log) { if (!fs.existsSync(from)) { throw new Error(`Missing required server artifact: ${from}`) } - fs.cpSync(from, to, { recursive: true, dereference: true }) + fs.cpSync(from, to, { recursive: true }) log(`copied ${name}`) }