Initial commit: X-Plane G1000 web cockpit + bridge + Tauri desktop app
- server/: Node bridge (datarefs/commands, navdata, CIFP procedures, flight plan) - web/: React cockpit (PFD/MFD/Map, VFR six-pack, AFCS, FMS CDU), PWA, collapsible sidebar - desktop/: Tauri 2 launcher (Bun sidecar, system tray, updater) + Linux build via Docker - scripts/: prep-desktop, build-linux, Gitea release + latest.json Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the Linux AppImage + .deb (x86_64) in Docker. Run from the repo root,
|
||||
# AFTER scripts/prep-desktop.sh has produced the linux sidecar + web resources.
|
||||
# On Apple Silicon this runs under qemu (linux/amd64) and is slow but works.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
IMG=xpcockpit-linux-builder
|
||||
echo "==> building docker image ($IMG)"
|
||||
docker build --platform linux/amd64 -f desktop/Dockerfile.linux -t "$IMG" desktop >/dev/null
|
||||
|
||||
# The updater signing key is a secret and is NOT in the repo. If it's present
|
||||
# locally we sign + emit updater artifacts; otherwise we build plain installable
|
||||
# bundles (appimage + deb) with updater artifacts disabled via a config override.
|
||||
SIGN_ENV=(); BUNDLES="appimage,deb,updater"; CFG=""
|
||||
if [[ -f desktop/.tauri-signing.key && -f desktop/.tauri-signing.pw ]]; then
|
||||
SIGN_ENV=(-e "TAURI_SIGNING_PRIVATE_KEY=$(cat desktop/.tauri-signing.key)"
|
||||
-e "TAURI_SIGNING_PRIVATE_KEY_PASSWORD=$(cat desktop/.tauri-signing.pw)")
|
||||
echo "==> signing key found — emitting signed updater artifacts"
|
||||
else
|
||||
BUNDLES="appimage,deb"; CFG='--config {"bundle":{"createUpdaterArtifacts":false}}'
|
||||
echo "==> no signing key — building plain appimage + deb (no updater artifacts)"
|
||||
fi
|
||||
|
||||
echo "==> tauri build (x86_64-unknown-linux-gnu) in container — this takes a while under emulation"
|
||||
docker run --rm --platform linux/amd64 \
|
||||
-v "$ROOT":/work \
|
||||
-e CARGO_TARGET_DIR=/work/target-linux \
|
||||
-e APPIMAGE_EXTRACT_AND_RUN=1 \
|
||||
-e ARCH=x86_64 \
|
||||
"${SIGN_ENV[@]}" \
|
||||
-w /work/desktop \
|
||||
"$IMG" \
|
||||
bash -c "export PATH=/usr/local/cargo/bin:\$PATH; tauri build --target x86_64-unknown-linux-gnu --bundles $BUNDLES $CFG"
|
||||
|
||||
echo "==> artifacts:"
|
||||
find target-linux/x86_64-unknown-linux-gnu/release/bundle -maxdepth 2 -type f \
|
||||
\( -name '*.AppImage' -o -name '*.deb' -o -name '*.AppImage.sig' -o -name '*.tar.gz' -o -name '*.sig' \) 2>/dev/null
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prepare the Tauri bundle inputs: build the web cockpit, copy it in as a
|
||||
# resource, and compile the Node bridge into Bun single-file sidecars named with
|
||||
# the Tauri target triples. Run from the repo root.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
export PATH="$HOME/.bun/bin:$PATH"
|
||||
|
||||
echo "==> building web cockpit"
|
||||
( cd web && npm run build >/dev/null )
|
||||
|
||||
echo "==> copying cockpit into desktop resources"
|
||||
rm -rf desktop/src-tauri/resources/web
|
||||
mkdir -p desktop/src-tauri/resources/web
|
||||
cp -R web/dist/. desktop/src-tauri/resources/web/
|
||||
|
||||
echo "==> compiling bridge sidecars (Bun)"
|
||||
mkdir -p desktop/src-tauri/binaries
|
||||
bun build --compile --target=bun-darwin-arm64 server/bridge.js \
|
||||
--outfile desktop/src-tauri/binaries/xpbridge-aarch64-apple-darwin
|
||||
bun build --compile --target=bun-linux-x64-baseline server/bridge.js \
|
||||
--outfile desktop/src-tauri/binaries/xpbridge-x86_64-unknown-linux-gnu
|
||||
chmod +x desktop/src-tauri/binaries/xpbridge-*
|
||||
|
||||
echo "==> done"
|
||||
ls -lh desktop/src-tauri/binaries/
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env node
|
||||
// Publish a release to Gitea and (re)write the updater's latest.json.
|
||||
//
|
||||
// Reads the built artifacts (macOS .app.tar.gz + .sig, Linux .AppImage + .sig,
|
||||
// plus .dmg / .deb for manual install), creates a versioned release with those
|
||||
// assets, then builds latest.json (Tauri v2 updater format) pointing at the
|
||||
// release download URLs and uploads it to a fixed-tag "updater" release so the
|
||||
// app's updater endpoint URL stays constant.
|
||||
//
|
||||
// Env: GITEA_URL (e.g. https://git.kgva.ch), GITEA_REPO (owner/name),
|
||||
// GITEA_TOKEN (or a token file at /tmp/gitea_token).
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
|
||||
const GITEA_URL = (process.env.GITEA_URL || 'https://git.kgva.ch').replace(/\/$/, '');
|
||||
const REPO = process.env.GITEA_REPO || 'karim/xplane-cockpit';
|
||||
const TOKEN = process.env.GITEA_TOKEN
|
||||
|| (fs.existsSync('/tmp/gitea_token') ? fs.readFileSync('/tmp/gitea_token', 'utf8').trim() : '');
|
||||
const VERSION = JSON.parse(fs.readFileSync(path.join(ROOT, 'desktop/src-tauri/tauri.conf.json'), 'utf8')).version;
|
||||
const UPDATER_TAG = 'updater';
|
||||
|
||||
if (!TOKEN) { console.error('No GITEA_TOKEN (env or /tmp/gitea_token).'); process.exit(1); }
|
||||
|
||||
const api = (p, opts = {}) => fetch(`${GITEA_URL}/api/v1${p}`, {
|
||||
...opts,
|
||||
headers: { Authorization: `token ${TOKEN}`, Accept: 'application/json', ...(opts.headers || {}) },
|
||||
});
|
||||
|
||||
// Collect built artifacts that exist (mac and/or linux builds may have run).
|
||||
function findArtifacts() {
|
||||
const macBundle = path.join(ROOT, 'desktop/src-tauri/target/aarch64-apple-darwin/release/bundle');
|
||||
const linBundle = path.join(ROOT, 'target-linux/x86_64-unknown-linux-gnu/release/bundle');
|
||||
const out = { assets: [], updater: {} };
|
||||
const add = (file, platformKey) => {
|
||||
if (!fs.existsSync(file)) return;
|
||||
out.assets.push(file);
|
||||
if (platformKey) {
|
||||
const sig = file + '.sig';
|
||||
if (fs.existsSync(sig)) out.updater[platformKey] = { file, sig: fs.readFileSync(sig, 'utf8').trim() };
|
||||
}
|
||||
};
|
||||
const glob1 = (dir, re) => fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => re.test(f)).map((f) => path.join(dir, f)) : [];
|
||||
// macOS
|
||||
glob1(path.join(macBundle, 'macos'), /\.app\.tar\.gz$/).forEach((f) => add(f, 'darwin-aarch64'));
|
||||
glob1(path.join(macBundle, 'dmg'), /\.dmg$/).forEach((f) => out.assets.push(f));
|
||||
// Linux
|
||||
glob1(path.join(linBundle, 'appimage'), /\.AppImage$/).forEach((f) => add(f, 'linux-x86_64'));
|
||||
glob1(path.join(linBundle, 'deb'), /\.deb$/).forEach((f) => out.assets.push(f));
|
||||
return out;
|
||||
}
|
||||
|
||||
async function getReleaseByTag(tag) {
|
||||
const r = await api(`/repos/${REPO}/releases/tags/${encodeURIComponent(tag)}`);
|
||||
return r.ok ? r.json() : null;
|
||||
}
|
||||
|
||||
async function ensureRelease(tag, name, body) {
|
||||
let rel = await getReleaseByTag(tag);
|
||||
if (rel) return rel;
|
||||
const r = await api(`/repos/${REPO}/releases`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tag_name: tag, name, body, draft: false, prerelease: false }),
|
||||
});
|
||||
if (!r.ok) throw new Error(`create release ${tag}: ${r.status} ${await r.text()}`);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
async function uploadAsset(rel, file, asName) {
|
||||
const name = asName || path.basename(file);
|
||||
// delete an existing asset with the same name first (Gitea won't overwrite)
|
||||
for (const a of rel.assets || []) {
|
||||
if (a.name === name) await api(`/repos/${REPO}/releases/${rel.id}/assets/${a.id}`, { method: 'DELETE' });
|
||||
}
|
||||
const blob = new Blob([fs.readFileSync(file)]);
|
||||
const fd = new FormData();
|
||||
fd.append('attachment', blob, name);
|
||||
const r = await api(`/repos/${REPO}/releases/${rel.id}/assets?name=${encodeURIComponent(name)}`, { method: 'POST', body: fd });
|
||||
if (!r.ok) throw new Error(`upload ${name}: ${r.status} ${await r.text()}`);
|
||||
console.log(' ↑', name);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
const dlUrl = (tag, name) => `${GITEA_URL}/${REPO}/releases/download/${encodeURIComponent(tag)}/${encodeURIComponent(name)}`;
|
||||
|
||||
async function main() {
|
||||
const { assets, updater } = findArtifacts();
|
||||
if (!assets.length) { console.error('No build artifacts found — run the builds first.'); process.exit(1); }
|
||||
console.log(`Release v${VERSION} → ${GITEA_URL}/${REPO}`);
|
||||
console.log('Artifacts:', assets.map((a) => path.basename(a)).join(', '));
|
||||
|
||||
const verTag = `v${VERSION}`;
|
||||
const rel = await ensureRelease(verTag, `X-Plane Cockpit ${verTag}`, `Automated release ${verTag}.`);
|
||||
for (const f of assets) await uploadAsset(rel, f);
|
||||
const relFresh = await getReleaseByTag(verTag); // refresh asset list
|
||||
|
||||
// Build latest.json referencing this release's updater artifacts.
|
||||
const platforms = {};
|
||||
for (const [key, { file, sig }] of Object.entries(updater)) {
|
||||
platforms[key] = { signature: sig, url: dlUrl(verTag, path.basename(file)) };
|
||||
}
|
||||
const latest = {
|
||||
version: VERSION,
|
||||
notes: `X-Plane Cockpit ${verTag}`,
|
||||
pub_date: new Date().toISOString(),
|
||||
platforms,
|
||||
};
|
||||
const latestPath = path.join(ROOT, 'desktop/latest.json');
|
||||
fs.writeFileSync(latestPath, JSON.stringify(latest, null, 2));
|
||||
console.log('latest.json platforms:', Object.keys(platforms).join(', ') || '(none)');
|
||||
|
||||
// Upload latest.json to the fixed "updater" release (constant endpoint URL).
|
||||
const upd = await ensureRelease(UPDATER_TAG, 'Updater channel', 'Rolling pointer used by the in-app updater.');
|
||||
await uploadAsset(upd, latestPath, 'latest.json');
|
||||
console.log('Updater endpoint:', dlUrl(UPDATER_TAG, 'latest.json'));
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error(e); process.exit(1); });
|
||||
Reference in New Issue
Block a user