From 040a204d274af5b79133f8f233b470c0f9c91fbc Mon Sep 17 00:00:00 2001 From: Ivan Date: Mon, 18 May 2026 05:09:30 -0500 Subject: [PATCH] feat(scripts): add offline bundle creation and installation scripts --- scripts/create-offline-bundle.sh | 157 +++++++++++++++++++++++++++++++ scripts/install-offline.sh | 78 +++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 scripts/create-offline-bundle.sh create mode 100644 scripts/install-offline.sh diff --git a/scripts/create-offline-bundle.sh b/scripts/create-offline-bundle.sh new file mode 100644 index 00000000..5d693b24 --- /dev/null +++ b/scripts/create-offline-bundle.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Create an offline bundle for air-gapped (zero-network) builds. + +This script downloads and caches everything needed to build MeshChatX without +any network access. Run it on an online machine, then transfer the resulting +vendor/offline/ directory to your air-gapped build host. + +Usage: + bash scripts/create-offline-bundle.sh [options] + +Options: + --warm-packaging Pre-download electron-builder packaging tools + (appimagetool, app-builder, etc.) by running a + dummy Linux build. Increases bundle size. + --out-dir PATH Output directory (default: vendor/offline) + -h, --help Show this help + +Prerequisites on the online machine: + - pnpm (matching packageManager in package.json) + - uv + - Node.js >= 24 + - Python >= 3.11 + +The bundle includes: + - node_modules/ (fully resolved, with all postinstall artifacts) + - uv cache (all Python wheels) + - electron-builder cache (app-builder and related tools) + - Verification that all vendored assets are present + +On the air-gapped machine: + bash scripts/install-offline.sh + MESHCHATX_OFFLINE_BUILD=1 make build +EOF +} + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${ROOT_DIR}" + +WARM_PACKAGING="0" +OUT_DIR="${ROOT_DIR}/vendor/offline" + + while [[ $# -gt 0 ]]; do + case "$1" in + --warm-packaging) + WARM_PACKAGING="1" + shift + ;; + --out-dir) + OUT_DIR="${2:?missing value for --out-dir}" + shift 2 + ;; + --) + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage + exit 1 + ;; + esac +done + +require_cmd() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "Missing required command: $1" >&2 + exit 1 + fi +} + +require_cmd node +require_cmd pnpm +require_cmd uv +require_cmd python3 + +VERSION=$(node -p "require('./package.json').version") +PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]') +ARCH=$(uname -m) +BUNDLE_NAME="meshchatx-offline-bundle-${VERSION}-${PLATFORM}-${ARCH}" +BUNDLE_DIR="${OUT_DIR}/${BUNDLE_NAME}" + +rm -rf "${BUNDLE_DIR}" +mkdir -p "${BUNDLE_DIR}" + +echo "==> [1/7] Verifying lockfiles..." +if [[ ! -f "pnpm-lock.yaml" ]]; then + echo "Missing pnpm-lock.yaml. Run 'pnpm install' first." >&2 + exit 1 +fi +if [[ ! -f "uv.lock" ]]; then + echo "Missing uv.lock. Run 'uv lock' first." >&2 + exit 1 +fi + +echo "==> [2/7] Ensuring vendored assets..." +node scripts/fetch-micron-wasm.mjs +python3 scripts/build/fetch_reticulum_manual.py +python3 scripts/build/fetch_repository_wheels.py + +echo "==> [3/7] Installing Node.js dependencies..." +pnpm install --frozen-lockfile + +echo "==> [4/7] Running postinstall hooks..." +mkdir -p "${BUNDLE_DIR}/electron-builder-cache" +ELECTRON_BUILDER_CACHE="${BUNDLE_DIR}/electron-builder-cache" pnpm run electron-postinstall + +echo "==> [5/7] Bundling node_modules..." +if tar --help 2>/dev/null | grep -q -- '--hard-dereference'; then + tar czf "${BUNDLE_DIR}/node_modules.tar.gz" --hard-dereference node_modules/ +else + NM_BUNDLE="${BUNDLE_DIR}/node_modules-bundle" + cp -rL node_modules "${NM_BUNDLE}" + (cd "${BUNDLE_DIR}" && tar czf node_modules.tar.gz node_modules-bundle/) + rm -rf "${NM_BUNDLE}" +fi + +echo "==> [6/7] Installing Python dependencies and populating uv cache..." +mkdir -p "${BUNDLE_DIR}/uv-cache" +UV_CACHE_DIR="${BUNDLE_DIR}/uv-cache" uv sync --group dev + +echo "==> [7/7] Finalizing..." +if [[ "${WARM_PACKAGING}" == "1" ]]; then + echo " Pre-downloading electron-builder packaging tools..." + ELECTRON_BUILDER_CACHE="${BUNDLE_DIR}/electron-builder-cache" \ + npx electron-builder --linux dir --publish=never >/dev/null 2>&1 || true +fi + +cat > "${BUNDLE_DIR}/.bundle-manifest.json" </dev/null | head -n 1) +fi + +if [[ -z "${BUNDLE}" ]]; then + echo "No offline bundle found in ${ROOT_DIR}/vendor/offline/" >&2 + echo "Create one on an online machine with:" >&2 + echo " bash scripts/create-offline-bundle.sh" >&2 + exit 1 +fi + +BUNDLE_NAME=$(basename "${BUNDLE}") +echo "Found offline bundle: ${BUNDLE_NAME}" + +if [[ -d "node_modules" ]]; then + echo "node_modules already exists. Skipping extraction." +else + NM_TAR="${BUNDLE}/node_modules.tar.gz" + if [[ ! -f "${NM_TAR}" ]]; then + echo "Missing node_modules.tar.gz in bundle" >&2 + exit 1 + fi + echo "Extracting node_modules from bundle..." + tar xzf "${NM_TAR}" + echo "node_modules extracted." +fi + +export MESHCHATX_OFFLINE_BUILD=1 +export UV_CACHE_DIR="${BUNDLE}/uv-cache" +export ELECTRON_BUILDER_CACHE="${BUNDLE}/electron-builder-cache" + +echo "" +echo "Running offline install..." +make install + +echo "" +echo "========================================" +echo "Offline install complete." +echo "" +echo "Build with:" +echo " MESHCHATX_OFFLINE_BUILD=1 make build" +echo "" +echo "Or package with:" +echo " MESHCHATX_OFFLINE_BUILD=1 pnpm run dist:linux" +echo "========================================"