Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 76 additions & 6 deletions .github/workflows/build-and-upload.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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 != '' }}
Expand Down Expand Up @@ -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
Expand All @@ -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 }}
Expand Down
1 change: 1 addition & 0 deletions packages/electron-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
119 changes: 119 additions & 0 deletions packages/electron-app/scripts/build-flatpak.js
Original file line number Diff line number Diff line change
@@ -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}`)
2 changes: 1 addition & 1 deletion packages/opencode-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions packages/tauri-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
96 changes: 96 additions & 0 deletions packages/tauri-app/scripts/build-flatpak.js
Original file line number Diff line number Diff line change
@@ -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}`)
2 changes: 1 addition & 1 deletion scripts/desktop-server-resources.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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}`)
}

Expand Down
Loading