mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-16 23:07:14 +00:00
60bb67f025
* feat(packaging): add fnOS FPK packaging and CI workflow Add packaging/fnos/ shell (manifest, lifecycle cmd scripts, install/ upgrade/uninstall wizards, desktop entry, EULA) plus in-repo build script. A release now auto-builds langbot-<tag>-fnos.fpk via .github/workflows/build-fnos-fpk.yaml, uploaded to the release assets. * ci(fnos): skip release upload on manual dispatch github.event.release.tag_name is empty when triggered via workflow_dispatch, causing 'gh release upload' to fail with 'requires at least 2 arg(s)'. Restrict the step to release events. * ci(fnos): normalize release asset name Strip a trailing -fnos from the tag-derived version before appending the suffix, avoiding langbot-<v>-fnos-fnos.fpk when the tag itself already carries -fnos. * ci(fnos): use release tag version for auto build, manifest for manual Auto build (release/tag) reads version from tag like other release workflows; build.sh strips the v prefix when injecting into manifest. Manual dispatch falls back to the version maintained in manifest. * feat(fnos): add post-install deployment notice to install wizard Last wizard step now informs users that first startup takes about 5-10 minutes for dependency setup before the web UI is ready. * docs(fnos): add packaging directory README * refactor(fnos): rename app from ai.langbot to langbot Rename appname, desktop entry, data share, build artifacts, and all references from ai.langbot to langbot across packaging files. --------- Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
181 lines
6.7 KiB
Bash
181 lines
6.7 KiB
Bash
#!/usr/bin/env bash
|
||
# packaging/fnos/build.sh - Build LangBot fnOS FPK package (in-repo version)
|
||
# 在 LangBot 仓库内直接打包飞牛 fnOS 应用
|
||
# Usage:
|
||
# bash packaging/fnos/build.sh # 版本取 manifest 中 version=
|
||
# FPK_VERSION=4.10.9 bash packaging/fnos/build.sh # 注入版本(CI 用 release tag)
|
||
# Dependencies: python3+Pillow, node+npm, fnpack
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
SRC_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" # LangBot 仓库根
|
||
FPK_DIR="${SCRIPT_DIR}"
|
||
|
||
echo "==> LangBot fnOS FPK builder (in-repo)"
|
||
echo " Source root: ${SRC_ROOT}"
|
||
echo " FPK dir: ${FPK_DIR}"
|
||
|
||
# --- 0. Inject version (release tag) ---
|
||
if [ -n "${FPK_VERSION:-}" ]; then
|
||
sed -i "s/^version=.*/version=${FPK_VERSION#v}/" "${FPK_DIR}/manifest"
|
||
else
|
||
# 无注入版本时自动跟进仓库主版本(pyproject.toml)
|
||
PY_VER=$(grep -m1 '^version = ' "${SRC_ROOT}/pyproject.toml" | cut -d'"' -f2)
|
||
if [ -n "${PY_VER}" ]; then
|
||
sed -i "s/^version=.*/version=${PY_VER}/" "${FPK_DIR}/manifest"
|
||
fi
|
||
fi
|
||
MANIFEST_VER=$(grep '^version=' "${FPK_DIR}/manifest" | cut -d= -f2)
|
||
echo " FPK version: ${MANIFEST_VER}"
|
||
|
||
# --- 1. Build frontend ---
|
||
echo "[1/5] Building frontend (web/dist)..."
|
||
cd "${SRC_ROOT}/web"
|
||
if command -v pnpm >/dev/null 2>&1; then
|
||
pnpm install --frozen-lockfile 2>/dev/null || pnpm install
|
||
pnpm build
|
||
else
|
||
npm install
|
||
npx vite build
|
||
fi
|
||
[ -d dist ] || { echo "ERROR: web/dist missing" >&2; exit 1; }
|
||
echo " Frontend built"
|
||
|
||
# --- 2. Sync source into packaging/fnos/app/langbot/ ---
|
||
echo "[2/5] Syncing source to app/langbot/..."
|
||
rm -rf "${FPK_DIR}/app/langbot"
|
||
mkdir -p "${FPK_DIR}/app/langbot"
|
||
cd "${SRC_ROOT}"
|
||
rsync -a \
|
||
--exclude='.git' \
|
||
--exclude='.venv' \
|
||
--exclude='__pycache__' \
|
||
--exclude='*.pyc' \
|
||
--exclude='web/node_modules' \
|
||
--exclude='web/.vite' \
|
||
--exclude='tests' \
|
||
--exclude='packaging' \
|
||
--exclude='.pytest_cache' \
|
||
--exclude='.mypy_cache' \
|
||
--exclude='.ruff_cache' \
|
||
--exclude='data' \
|
||
--exclude='*.log' \
|
||
--exclude='.dockerignore' \
|
||
--exclude='Dockerfile' \
|
||
--exclude='docker/' \
|
||
--exclude='kubernetes.yaml' \
|
||
--exclude='.github/' \
|
||
--exclude='docs/' \
|
||
--exclude='examples/' \
|
||
--exclude='res/' \
|
||
./ "${FPK_DIR}/app/langbot/"
|
||
|
||
[ -d "${FPK_DIR}/app/langbot/web/dist" ] || { echo "ERROR: web/dist missing after rsync!" >&2; exit 1; }
|
||
echo " Source synced ($(du -sh "${FPK_DIR}/app/langbot" | cut -f1))"
|
||
|
||
# --- 2.5 Download bundled uv binaries (offline install on NAS) ---
|
||
echo "[2.5/5] Downloading bundled uv binaries..."
|
||
UV_VERSION="0.12.9"
|
||
mkdir -p "${FPK_DIR}/app/bin"
|
||
for arch in x86_64 aarch64; do
|
||
out="${FPK_DIR}/app/bin/uv-${arch}"
|
||
if [ -x "${out}" ]; then
|
||
echo " uv-${arch} already present, skip"
|
||
continue
|
||
fi
|
||
tmp="$(mktemp -d)"
|
||
if curl -sSL -o "${tmp}/uv.tar.gz" \
|
||
"https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${arch}-unknown-linux-gnu.tar.gz" \
|
||
&& tar xzf "${tmp}/uv.tar.gz" -C "${tmp}" \
|
||
&& cp "${tmp}/uv-${arch}-unknown-linux-gnu/uv" "${out}"; then
|
||
chmod +x "${out}"
|
||
echo " uv-${arch} downloaded (${UV_VERSION})"
|
||
else
|
||
echo " WARNING: failed to download uv for ${arch}, install will fall back to online install" >&2
|
||
fi
|
||
rm -rf "${tmp}"
|
||
done
|
||
|
||
# --- 3. Regenerate icons from res/logo-blue.png ---
|
||
echo "[3/5] Generating icons from res/logo-blue.png..."
|
||
export LOGO_SRC="${SRC_ROOT}/res/logo-blue.png"
|
||
export OUT_DIR="${FPK_DIR}"
|
||
python3 << 'PYEOF'
|
||
from PIL import Image
|
||
import os, sys
|
||
|
||
src = os.environ.get("LOGO_SRC")
|
||
out_dir = os.environ.get("OUT_DIR")
|
||
if not src or not out_dir:
|
||
print("ERROR: LOGO_SRC or OUT_DIR not set", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
if not os.path.isfile(src):
|
||
print(f"ERROR: logo source not found: {src}", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
img = Image.open(src).convert("RGBA")
|
||
|
||
for size, name in [(64, "ICON.PNG"), (256, "ICON_256.PNG")]:
|
||
img.resize((size, size), Image.LANCZOS).save(os.path.join(out_dir, name))
|
||
|
||
ui_dir = os.path.join(out_dir, "app/ui/images")
|
||
os.makedirs(ui_dir, exist_ok=True)
|
||
for size in [64, 256]:
|
||
img.resize((size, size), Image.LANCZOS).save(os.path.join(ui_dir, f"icon-{size}.png"))
|
||
|
||
desktop_dir = os.path.join(out_dir, "app/desktop/images")
|
||
os.makedirs(desktop_dir, exist_ok=True)
|
||
for size in [64, 256]:
|
||
img.resize((size, size), Image.LANCZOS).save(os.path.join(desktop_dir, f"icon-{size}.png"))
|
||
|
||
print(" Icons generated")
|
||
PYEOF
|
||
|
||
# --- 4. Validate structure ---
|
||
echo "[4/5] Validating FPK structure..."
|
||
ERRORS=0
|
||
[ -f "${FPK_DIR}/manifest" ] || { echo " MISSING: manifest"; ERRORS=$((ERRORS+1)); }
|
||
[ -f "${FPK_DIR}/config/privilege" ] || { echo " MISSING: config/privilege"; ERRORS=$((ERRORS+1)); }
|
||
[ -f "${FPK_DIR}/config/resource" ] || { echo " MISSING: config/resource"; ERRORS=$((ERRORS+1)); }
|
||
[ -f "${FPK_DIR}/ICON.PNG" ] || { echo " MISSING: ICON.PNG"; ERRORS=$((ERRORS+1)); }
|
||
[ -f "${FPK_DIR}/ICON_256.PNG" ] || { echo " MISSING: ICON_256.PNG"; ERRORS=$((ERRORS+1)); }
|
||
[ -f "${FPK_DIR}/app/ui/config" ] || { echo " MISSING: app/ui/config"; ERRORS=$((ERRORS+1)); }
|
||
[ -f "${FPK_DIR}/app/ui/images/icon-64.png" ] || { echo " MISSING: app/ui/images/icon-64.png"; ERRORS=$((ERRORS+1)); }
|
||
[ -f "${FPK_DIR}/app/ui/images/icon-256.png" ] || { echo " MISSING: app/ui/images/icon-256.png"; ERRORS=$((ERRORS+1)); }
|
||
[ -f "${FPK_DIR}/app/desktop/langbot.main.url" ] || { echo " MISSING: app/desktop/langbot.main.url"; ERRORS=$((ERRORS+1)); }
|
||
[ -f "${FPK_DIR}/app/desktop/images/icon-64.png" ] || { echo " MISSING: app/desktop/images/icon-64.png"; ERRORS=$((ERRORS+1)); }
|
||
[ -f "${FPK_DIR}/app/desktop/images/icon-256.png" ] || { echo " MISSING: app/desktop/images/icon-256.png"; ERRORS=$((ERRORS+1)); }
|
||
[ -d "${FPK_DIR}/cmd" ] || { echo " MISSING: cmd/"; ERRORS=$((ERRORS+1)); }
|
||
[ -d "${FPK_DIR}/wizard" ] || { echo " MISSING: wizard/"; ERRORS=$((ERRORS+1)); }
|
||
|
||
for script in "${FPK_DIR}/cmd/"*; do
|
||
[ -x "${script}" ] || { echo " NOT EXECUTABLE: cmd/$(basename "$script")"; ERRORS=$((ERRORS+1)); }
|
||
done
|
||
|
||
if [ "${ERRORS}" -gt 0 ]; then
|
||
echo "FAILED: ${ERRORS} validation errors" >&2
|
||
exit 1
|
||
fi
|
||
echo " Structure OK"
|
||
|
||
# --- 5. Build FPK ---
|
||
echo "[5/5] Building .fpk..."
|
||
if ! command -v fnpack >/dev/null 2>&1; then
|
||
echo "ERROR: fnpack not found in PATH." >&2
|
||
echo " Download from https://developer.fnnas.com/docs/cli/fnpack" >&2
|
||
exit 1
|
||
fi
|
||
|
||
cd "${FPK_DIR}"
|
||
fnpack build
|
||
FPK_FILE=$(ls -t *.fpk 2>/dev/null | head -1)
|
||
if [ -n "${FPK_FILE}" ]; then
|
||
echo ""
|
||
echo "==> Done! FPK: ${FPK_DIR}/${FPK_FILE}"
|
||
echo " Size: $(du -sh "${FPK_FILE}" | cut -f1)"
|
||
else
|
||
echo "WARNING: fnpack finished but no .fpk found in ${FPK_DIR}" >&2
|
||
exit 1
|
||
fi
|