Files
PubScript/desktop/build-deb.sh
T

167 lines
5.9 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
PKG_NAME="pubscript"
PKG_VERSION="0.1.0"
ARCH="amd64"
DEB_NAME="${PKG_NAME}_${PKG_VERSION}_${ARCH}.deb"
STAGE_ROOT="/tmp/opencode/pubscript-deb-stage"
STAGE_DIR="${STAGE_ROOT}/${PKG_NAME}_${PKG_VERSION}_${ARCH}"
echo "==> [1/6] Building production web bundle (apps/web/dist)..."
pnpm --filter @pubscript/web run build
echo "==> [2/6] Preparing Debian package staging tree at ${STAGE_DIR}..."
rm -rf "$STAGE_DIR"
mkdir -p \
"$STAGE_DIR/DEBIAN" \
"$STAGE_DIR/usr/bin" \
"$STAGE_DIR/usr/share/applications" \
"$STAGE_DIR/usr/share/icons/hicolor/scalable/apps" \
"$STAGE_DIR/usr/share/pixmaps" \
"$STAGE_DIR/opt/pubscript/apps/web" \
"$STAGE_DIR/opt/pubscript/server/dist"
echo "==> [3/6] Bundling server entry and native SQLite/Argon2 runtime modules..."
cat > "$STAGE_DIR/opt/pubscript/package.json" <<'EOF'
{
"name": "pubscript-desktop-runtime",
"version": "0.1.0",
"private": true,
"type": "module"
}
EOF
# Bundle server TypeScript + Fastify + @pubscript/core into a single ESM file,
# keeping only native addons (better-sqlite3, argon2) external.
ESBUILD_BIN="$(find "$ROOT_DIR/node_modules/.pnpm" -path '*/esbuild/bin/esbuild' -type f | head -n 1)"
"$ESBUILD_BIN" server/src/index.ts \
--bundle \
--platform=node \
--target=node22 \
--format=esm \
--banner:js="import { createRequire as __psCreateRequire } from 'node:module'; const require = __psCreateRequire(import.meta.url);" \
--outfile="$STAGE_DIR/opt/pubscript/server/dist/server.mjs" \
--external:better-sqlite3 \
--external:argon2
# Copy better-sqlite3 and argon2 plus their transitive runtime dependencies
node - "$ROOT_DIR/server" "$STAGE_DIR/opt/pubscript/node_modules" <<'NODE_EOF'
import { cpSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
import { dirname, join } from "node:path";
import { createRequire } from "node:module";
const [serverDir, outModules] = process.argv.slice(2);
mkdirSync(outModules, { recursive: true });
const seen = new Set();
function copyPkg(pkgName, fromDir) {
if (seen.has(pkgName)) return;
seen.add(pkgName);
const req = createRequire(join(fromDir, "package.json"));
let pkgJsonPath;
try {
pkgJsonPath = req.resolve(`${pkgName}/package.json`);
} catch {
const entry = req.resolve(pkgName);
let cur = dirname(realpathSync(entry));
while (cur !== "/" && !existsSync(join(cur, "package.json"))) {
cur = dirname(cur);
}
pkgJsonPath = join(cur, "package.json");
}
const realPkgDir = dirname(realpathSync(pkgJsonPath));
const destDir = join(outModules, pkgName);
mkdirSync(dirname(destDir), { recursive: true });
cpSync(realPkgDir, destDir, { recursive: true, dereference: true });
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
const deps = Object.keys(pkgJson.dependencies ?? {});
for (const dep of deps) {
copyPkg(dep, realPkgDir);
}
}
copyPkg("better-sqlite3", serverDir);
copyPkg("argon2", serverDir);
NODE_EOF
cp -a server/src/migrations "$STAGE_DIR/opt/pubscript/server/dist/migrations"
cp -a apps/web/dist "$STAGE_DIR/opt/pubscript/apps/web/dist"
echo "==> [4/6] Compiling native GTK3 + WebKit2GTK desktop binary (/usr/bin/pubscript)..."
gcc $(pkg-config --cflags gtk+-3.0) \
-O2 -Wall -Wextra \
desktop/src/pubscript-desktop.c \
$(pkg-config --libs gtk+-3.0) -ldl \
-o "$STAGE_DIR/usr/bin/pubscript"
chmod 0755 "$STAGE_DIR/usr/bin/pubscript"
echo "==> [5/6] Installing icons, .desktop launcher, and DEBIAN metadata..."
cp apps/web/public/logo.svg "$STAGE_DIR/usr/share/icons/hicolor/scalable/apps/pubscript.svg"
cp apps/web/public/logo.svg "$STAGE_DIR/usr/share/pixmaps/pubscript.svg"
cat > "$STAGE_DIR/usr/share/applications/pubscript.desktop" <<'EOF'
[Desktop Entry]
Version=1.0
Type=Application
Name=PubScript
GenericName=Novel & Manuscript Writing Studio
Comment=Local-first writing studio for novelists with AI co-writing, plot boards, and manuscript export
Exec=/usr/bin/pubscript %U
Icon=pubscript
Terminal=false
Categories=Office;WordProcessor;TextEditor;Publishing;
Keywords=writing;novel;manuscript;markdown;epub;author;editor;
StartupNotify=true
StartupWMClass=pubscript
EOF
INSTALLED_KB="$(du -sk "$STAGE_DIR/usr" "$STAGE_DIR/opt" | awk '{s+=$1} END {print s}')"
cat > "$STAGE_DIR/DEBIAN/control" <<EOF
Package: ${PKG_NAME}
Version: ${PKG_VERSION}
Section: editors
Priority: optional
Architecture: ${ARCH}
Installed-Size: ${INSTALLED_KB}
Depends: libc6 (>= 2.34), libgtk-3-0 (>= 3.24), libwebkit2gtk-4.1-0 | libwebkit2gtk-4.0-37, nodejs (>= 18)
Maintainer: PubScript Studio <team@pubscript.local>
Homepage: https://pubscript.local
Description: PubScript — Local-First Novel & Manuscript Writing Studio
A multi-surface writing studio for novelists featuring an OnlyOffice/Word-grade
manuscript editor, granular text-anchored AI co-writing suggestions, autosave
revision history, character & world-building sheets, plot beat templates,
interactive whiteboard, and deterministic HTML/EPUB/ODT/DOCX/PDF export.
EOF
cat > "$STAGE_DIR/DEBIAN/postinst" <<'EOF'
#!/bin/sh
set -e
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database -q /usr/share/applications || true
fi
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor || true
fi
exit 0
EOF
chmod 0755 "$STAGE_DIR/DEBIAN/postinst"
echo "==> Verifying compiled desktop binary & embedded server via --headless-check..."
PUBSCRIPT_APP_ROOT="$STAGE_DIR/opt/pubscript" \
PUBSCRIPT_DESKTOP_PORT=18099 \
"$STAGE_DIR/usr/bin/pubscript" --headless-check
echo "==> [6/6] Building .deb archive with dpkg-deb..."
mkdir -p "$ROOT_DIR/dist"
dpkg-deb --root-owner-group --build "$STAGE_DIR" "$ROOT_DIR/dist/${DEB_NAME}"
cp -f "$ROOT_DIR/dist/${DEB_NAME}" "$ROOT_DIR/${DEB_NAME}"
echo "==> Built Debian package: $ROOT_DIR/dist/${DEB_NAME} (and $ROOT_DIR/${DEB_NAME})"
dpkg-deb --info "$ROOT_DIR/dist/${DEB_NAME}"