aboutsummaryrefslogtreecommitdiffstats
path: root/packaging/build
diff options
context:
space:
mode:
Diffstat (limited to 'packaging/build')
-rwxr-xr-xpackaging/build/build-client.sh116
-rwxr-xr-xpackaging/build/build-common.sh75
-rwxr-xr-xpackaging/build/build-hub.sh73
-rwxr-xr-xpackaging/build/build-node.sh89
-rwxr-xr-xpackaging/build/build-packages.sh156
5 files changed, 509 insertions, 0 deletions
diff --git a/packaging/build/build-client.sh b/packaging/build/build-client.sh
new file mode 100755
index 0000000..fbdf51e
--- /dev/null
+++ b/packaging/build/build-client.sh
@@ -0,0 +1,116 @@
+#!/usr/bin/env bash
+# Build the meshbay-client package: Electron app + .desktop launcher.
+#
+# Usage: ./build-client.sh [staging-dir] [--arm64]
+#
+# Requires Node >= 22. If not in PATH, set NODEJS_DIR=/opt/nodejs or similar.
+set -euo pipefail
+
+REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+STAGING="${1:-/tmp/meshbay-build}"
+ROOT="$STAGING/meshbay-client-root"
+CLIENT="$REPO/packages/meshbay-client"
+
+ARCH_FLAG=""
+ELECTRON_ARCH="x64"
+DEB_ARCH="amd64"
+for arg in "$@"; do
+ if [ "$arg" = "--arm64" ]; then
+ ARCH_FLAG="--arm64"
+ ELECTRON_ARCH="arm64"
+ DEB_ARCH="arm64"
+ fi
+done
+
+# Node.js version check
+if [ -n "${NODEJS_DIR:-}" ]; then
+ export PATH="$NODEJS_DIR/bin:$PATH"
+fi
+NODE_VER=$(node --version 2>/dev/null || echo "none")
+echo "==> meshbay-client (Node $NODE_VER, arch=$ELECTRON_ARCH)"
+
+if ! node --version >/dev/null 2>&1; then
+ echo "!! Node.js not found. Set NODEJS_DIR or install Node >= 22." >&2
+ exit 1
+fi
+
+NODE_MAJOR=$(node -e "process.stdout.write(String(process.versions.node.split('.')[0]))")
+if [ "$NODE_MAJOR" -lt 22 ]; then
+ echo "!! Node $NODE_VER is too old — need >= 22 for Electron" >&2
+ exit 1
+fi
+
+rm -rf "$ROOT"
+mkdir -p "$ROOT"
+
+# --- Build the Electron app -----------------------------------------------
+cd "$CLIENT"
+
+echo " npm ci"
+npm ci --ignore-scripts 2>&1 | tail -3
+
+echo " approving + installing electron"
+npm approve-scripts electron 2>/dev/null || true
+node node_modules/electron/install.js 2>&1 | tail -1
+
+echo " syncing UI from hub package"
+npm run sync-ui 2>&1 | tail -1
+
+echo " electron-builder --dir $ARCH_FLAG"
+npx electron-builder --dir --linux $ARCH_FLAG 2>&1 | tail -5
+
+# Determine the unpacked output directory
+if [ "$ELECTRON_ARCH" = "arm64" ]; then
+ UNPACKED="$CLIENT/dist/linux-arm64-unpacked"
+else
+ UNPACKED="$CLIENT/dist/linux-unpacked"
+fi
+
+[ -d "$UNPACKED" ] || { echo "!! electron-builder output not found at $UNPACKED" >&2; exit 1; }
+
+# --- Assemble the package tree --------------------------------------------
+echo " assembling /opt/meshbay-client/"
+mkdir -p "$ROOT/opt/meshbay-client"
+cp -a "$UNPACKED"/* "$ROOT/opt/meshbay-client/"
+
+# Rename the electron binary
+if [ -f "$ROOT/opt/meshbay-client/meshbay-client" ]; then
+ mv "$ROOT/opt/meshbay-client/meshbay-client" "$ROOT/opt/meshbay-client/meshbay"
+elif [ -f "$ROOT/opt/meshbay-client/electron" ]; then
+ mv "$ROOT/opt/meshbay-client/electron" "$ROOT/opt/meshbay-client/meshbay"
+fi
+
+# Symlink in PATH
+mkdir -p "$ROOT/usr/bin"
+ln -sf /opt/meshbay-client/meshbay "$ROOT/usr/bin/meshbay"
+
+# --- Desktop launcher -----------------------------------------------------
+mkdir -p "$ROOT/usr/share/applications"
+cp "$REPO/packaging/desktop/meshbay.desktop" "$ROOT/usr/share/applications/"
+
+# --- Icons ----------------------------------------------------------------
+ICON_SRC="$CLIENT/build/icon.png"
+if [ -f "$ICON_SRC" ]; then
+ for size in 128 256 512; do
+ ICON_DIR="$ROOT/usr/share/icons/hicolor/${size}x${size}/apps"
+ mkdir -p "$ICON_DIR"
+ if command -v convert >/dev/null 2>&1; then
+ convert "$ICON_SRC" -resize "${size}x${size}" "$ICON_DIR/meshbay.png"
+ elif command -v magick >/dev/null 2>&1; then
+ magick "$ICON_SRC" -resize "${size}x${size}" "$ICON_DIR/meshbay.png"
+ else
+ # Fallback: copy the original at all sizes (better than no icon)
+ cp "$ICON_SRC" "$ICON_DIR/meshbay.png"
+ fi
+ done
+ echo " icons installed"
+else
+ echo " !! icon.png not found at $ICON_SRC — no icons" >&2
+fi
+
+# --- Firewall profile (Chromecast) ----------------------------------------
+mkdir -p "$ROOT/usr/lib/firewalld/services"
+cp "$REPO/packaging/firewall/firewalld/meshbay-cast.xml" \
+ "$ROOT/usr/lib/firewalld/services/"
+
+echo "==> meshbay-client staging ready at $ROOT ($DEB_ARCH)"
diff --git a/packaging/build/build-common.sh b/packaging/build/build-common.sh
new file mode 100755
index 0000000..3045689
--- /dev/null
+++ b/packaging/build/build-common.sh
@@ -0,0 +1,75 @@
+#!/usr/bin/env bash
+# Build the meshbay-common package: shared venv with ALL Python dependencies.
+#
+# Usage: ./build-common.sh [staging-dir]
+# staging-dir defaults to /tmp/meshbay-build
+#
+# Output: $STAGING/meshbay-common-root/ (ready for dpkg-deb or rpmbuild)
+set -euo pipefail
+
+REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+STAGING="${1:-/tmp/meshbay-build}"
+ROOT="$STAGING/meshbay-common-root"
+VENV_FINAL="/opt/meshbay-common/venv"
+VENV_BUILD="$ROOT$VENV_FINAL"
+WHEEL_DIR="$STAGING/wheels"
+
+VERSION=$(python3 -c "
+import tomllib, pathlib
+p = pathlib.Path('$REPO/packages/meshbay-common/pyproject.toml')
+print(tomllib.loads(p.read_text())['project']['version'])
+")
+echo "==> meshbay-common $VERSION"
+
+rm -rf "$ROOT"
+mkdir -p "$ROOT/opt/meshbay-common" "$WHEEL_DIR"
+
+# --- Create venv -----------------------------------------------------------
+echo " creating venv at $VENV_BUILD"
+python3 -m venv "$VENV_BUILD"
+"$VENV_BUILD/bin/pip" install --upgrade pip wheel 2>&1 | tail -1
+
+# --- Build wheels for all three packages -----------------------------------
+echo " building wheels"
+for pkg in meshbay-common meshbay-hub meshbay-node; do
+ "$VENV_BUILD/bin/pip" wheel \
+ --no-deps \
+ --wheel-dir "$WHEEL_DIR" \
+ "$REPO/packages/$pkg" 2>&1 | tail -1
+done
+
+# --- Install everything into the venv -------------------------------------
+echo " installing all packages + dependencies"
+"$VENV_BUILD/bin/pip" install \
+ --find-links "$WHEEL_DIR" \
+ meshbay-common meshbay-hub meshbay-node 2>&1 | tail -3
+
+# --- Strip build tools from the venv (not needed at runtime) ---------------
+echo " stripping build tools"
+"$VENV_BUILD/bin/pip" uninstall -y pip setuptools wheel 2>&1 | tail -1
+rm -rf "$VENV_BUILD/lib"/python*/site-packages/pip*
+rm -rf "$VENV_BUILD/lib"/python*/site-packages/setuptools*
+rm -rf "$VENV_BUILD/lib"/python*/site-packages/wheel*
+rm -f "$VENV_BUILD/bin/pip"*
+
+# --- Fix shebangs and pyvenv.cfg ------------------------------------------
+# The venv was built under $ROOT but will be installed at /opt/meshbay-common/venv/
+echo " fixing paths ($ROOT -> '')"
+for f in "$VENV_BUILD/bin"/*; do
+ [ -f "$f" ] || continue
+ [ -L "$f" ] && continue
+ head -1 "$f" | grep -q "^#!" || continue
+ sed -i "1s|$ROOT||" "$f"
+done
+sed -i "s|$ROOT||g" "$VENV_BUILD/pyvenv.cfg"
+
+# --- Remove __pycache__ (will be regenerated on first import) --------------
+find "$VENV_BUILD" -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
+
+# --- Keep a copy of the wheels for reference -------------------------------
+mkdir -p "$ROOT/opt/meshbay-common/wheels"
+cp "$WHEEL_DIR"/meshbay_common-*.whl "$ROOT/opt/meshbay-common/wheels/"
+
+echo "==> meshbay-common staging ready at $ROOT"
+echo " venv: $VENV_BUILD"
+echo " version: $VERSION"
diff --git a/packaging/build/build-hub.sh b/packaging/build/build-hub.sh
new file mode 100755
index 0000000..45d2ab9
--- /dev/null
+++ b/packaging/build/build-hub.sh
@@ -0,0 +1,73 @@
+#!/usr/bin/env bash
+# Build the meshbay-hub package: hub-specific files on top of meshbay-common.
+#
+# Expects build-common.sh to have run first (shared venv exists in staging).
+#
+# Usage: ./build-hub.sh [staging-dir]
+set -euo pipefail
+
+REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+STAGING="${1:-/tmp/meshbay-build}"
+COMMON_ROOT="$STAGING/meshbay-common-root"
+ROOT="$STAGING/meshbay-hub-root"
+
+VENV="$COMMON_ROOT/opt/meshbay-common/venv"
+PYVER=$(basename "$VENV"/lib/python3.*)
+SITE="$VENV/lib/$PYVER/site-packages"
+
+VERSION=$(python3 -c "
+import tomllib, pathlib
+p = pathlib.Path('$REPO/packages/meshbay-hub/pyproject.toml')
+print(tomllib.loads(p.read_text())['project']['version'])
+")
+echo "==> meshbay-hub $VERSION"
+
+[ -d "$VENV" ] || { echo "!! shared venv not found — run build-common.sh first" >&2; exit 1; }
+
+rm -rf "$ROOT"
+mkdir -p "$ROOT"
+
+# --- Hub code in the shared venv (owned by meshbay-hub package) -----------
+# MOVE (not copy) from the common staging tree so dpkg/rpm knows these files
+# belong to meshbay-hub, not meshbay-common. build-common.sh installed them
+# so that shebangs and dependency resolution are correct.
+HUB_ROOT="$ROOT/opt/meshbay-common/venv/lib/$PYVER/site-packages"
+mkdir -p "$HUB_ROOT"
+mv "$SITE/meshbay_hub" "$HUB_ROOT/"
+mv "$SITE"/meshbay_hub-*.dist-info "$HUB_ROOT/"
+
+# Entry point
+mkdir -p "$ROOT/opt/meshbay-common/venv/bin"
+mv "$VENV/bin/meshbay-hub" "$ROOT/opt/meshbay-common/venv/bin/"
+
+# Symlink in PATH
+mkdir -p "$ROOT/usr/bin"
+ln -sf /opt/meshbay-common/venv/bin/meshbay-hub "$ROOT/usr/bin/meshbay-hub"
+
+# --- Hub-specific assets --------------------------------------------------
+mkdir -p "$ROOT/opt/meshbay-hub"
+
+# Alembic config (migrations are inside the installed package at meshbay_hub/db/migrations/)
+if [ -f "$REPO/packages/meshbay-hub/alembic.ini" ]; then
+ mkdir -p "$ROOT/opt/meshbay-hub/migrations"
+ cp "$REPO/packages/meshbay-hub/alembic.ini" "$ROOT/opt/meshbay-hub/migrations/"
+fi
+
+# Config example
+mkdir -p "$ROOT/opt/meshbay-hub/share"
+if [ -f "$REPO/packaging/conf/hub.toml.example" ]; then
+ cp "$REPO/packaging/conf/hub.toml.example" "$ROOT/opt/meshbay-hub/share/"
+fi
+
+# Also install to /etc/meshbay/ for discoverability
+mkdir -p "$ROOT/etc/meshbay"
+if [ -f "$ROOT/opt/meshbay-hub/share/hub.toml.example" ]; then
+ cp "$ROOT/opt/meshbay-hub/share/hub.toml.example" "$ROOT/etc/meshbay/"
+fi
+
+# --- Systemd unit ---------------------------------------------------------
+mkdir -p "$ROOT/usr/lib/systemd/system"
+cp "$REPO/packaging/systemd/meshbay-hub.service" \
+ "$ROOT/usr/lib/systemd/system/meshbay-hub.service"
+
+echo "==> meshbay-hub staging ready at $ROOT"
diff --git a/packaging/build/build-node.sh b/packaging/build/build-node.sh
new file mode 100755
index 0000000..d8a5af4
--- /dev/null
+++ b/packaging/build/build-node.sh
@@ -0,0 +1,89 @@
+#!/usr/bin/env bash
+# Build the meshbay-node package: node-specific files on top of meshbay-common.
+#
+# Expects build-common.sh to have run first (shared venv exists in staging).
+#
+# Usage: ./build-node.sh [staging-dir]
+set -euo pipefail
+
+REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+STAGING="${1:-/tmp/meshbay-build}"
+COMMON_ROOT="$STAGING/meshbay-common-root"
+ROOT="$STAGING/meshbay-node-root"
+
+VENV="$COMMON_ROOT/opt/meshbay-common/venv"
+PYVER=$(basename "$VENV"/lib/python3.*)
+SITE="$VENV/lib/$PYVER/site-packages"
+
+VERSION=$(python3 -c "
+import tomllib, pathlib
+p = pathlib.Path('$REPO/packages/meshbay-node/pyproject.toml')
+print(tomllib.loads(p.read_text())['project']['version'])
+")
+echo "==> meshbay-node $VERSION"
+
+[ -d "$VENV" ] || { echo "!! shared venv not found — run build-common.sh first" >&2; exit 1; }
+
+rm -rf "$ROOT"
+mkdir -p "$ROOT"
+
+# --- Node code in the shared venv (owned by meshbay-node package) ---------
+# MOVE (not copy) from the common staging tree — same rationale as build-hub.sh.
+NODE_ROOT="$ROOT/opt/meshbay-common/venv/lib/$PYVER/site-packages"
+mkdir -p "$NODE_ROOT"
+mv "$SITE/meshbay_node" "$NODE_ROOT/"
+mv "$SITE"/meshbay_node-*.dist-info "$NODE_ROOT/"
+
+# Entry point
+mkdir -p "$ROOT/opt/meshbay-common/venv/bin"
+mv "$VENV/bin/meshbay-node" "$ROOT/opt/meshbay-common/venv/bin/"
+
+# Symlink in PATH
+mkdir -p "$ROOT/usr/bin"
+ln -sf /opt/meshbay-common/venv/bin/meshbay-node "$ROOT/usr/bin/meshbay-node"
+
+# --- Node-specific assets -------------------------------------------------
+mkdir -p "$ROOT/opt/meshbay-node/share"
+
+# Default env with TMDB token (read at build time).
+# Override with MESHBAY_TMDB_TOKEN_FILE; falls back to QE/node.env (gitignored).
+TMDB_TOKEN_FILE="${MESHBAY_TMDB_TOKEN_FILE:-$REPO/QE/node.env}"
+TMDB_TOKEN=""
+if [ -f "$TMDB_TOKEN_FILE" ]; then
+ TMDB_TOKEN=$(grep -oP 'MESHBAY_TMDB_DEFAULT_TOKEN=\K.*' "$TMDB_TOKEN_FILE" || true)
+fi
+if [ -n "$TMDB_TOKEN" ]; then
+ cat > "$ROOT/opt/meshbay-node/share/default.env" <<EOF
+# Default environment for meshbay-node.
+# Copied to ~/.config/meshbay/node.env by 'meshbay-node init' if it does not exist.
+# The operator may override any value there or in the systemd EnvironmentFile.
+
+# TMDB API token for the Videos app (read-only, shared across installations)
+MESHBAY_TMDB_DEFAULT_TOKEN=$TMDB_TOKEN
+EOF
+ echo " TMDB token baked into default.env"
+else
+ echo " !! TMDB token not found in QE/node.env — default.env will be empty" >&2
+ touch "$ROOT/opt/meshbay-node/share/default.env"
+fi
+
+# --- Systemd units --------------------------------------------------------
+mkdir -p "$ROOT/usr/lib/systemd/system"
+mkdir -p "$ROOT/usr/lib/systemd/user"
+cp "$REPO/packaging/systemd/meshbay-node.service" \
+ "$ROOT/usr/lib/systemd/system/meshbay-node@.service"
+cp "$REPO/packaging/systemd/meshbay-node-user.service" \
+ "$ROOT/usr/lib/systemd/user/meshbay-node.service"
+
+# --- Firewall profiles ----------------------------------------------------
+# firewalld (Fedora)
+mkdir -p "$ROOT/usr/lib/firewalld/services"
+cp "$REPO/packaging/firewall/firewalld/meshbay-node.xml" \
+ "$ROOT/usr/lib/firewalld/services/"
+
+# UFW (Ubuntu/Debian)
+mkdir -p "$ROOT/etc/ufw/applications.d"
+cp "$REPO/packaging/firewall/ufw/meshbay" \
+ "$ROOT/etc/ufw/applications.d/"
+
+echo "==> meshbay-node staging ready at $ROOT"
diff --git a/packaging/build/build-packages.sh b/packaging/build/build-packages.sh
new file mode 100755
index 0000000..ccacb81
--- /dev/null
+++ b/packaging/build/build-packages.sh
@@ -0,0 +1,156 @@
+#!/usr/bin/env bash
+# Build all MeshBay packages for the current platform.
+#
+# Usage:
+# ./build-packages.sh # auto-detect format (deb on Ubuntu, rpm on Fedora)
+# ./build-packages.sh deb # force .deb
+# ./build-packages.sh rpm # force .rpm
+# ./build-packages.sh deb --arm64 # arm64 .deb (Pi)
+#
+# Expects to run on the target machine:
+# - Ubuntu 26.04 (cbesson@meshbay.org) for .deb amd64
+# - Fedora 44 (local) for .rpm x86_64
+# - Raspberry Pi for .deb arm64 (deferred)
+#
+# Output: packages in $STAGING/out/
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO="$(cd "$SCRIPT_DIR/../.." && pwd)"
+STAGING="${MESHBAY_BUILD_DIR:-/tmp/meshbay-build}"
+OUT="$STAGING/out"
+
+# --- Detect format --------------------------------------------------------
+FORMAT="${1:-auto}"
+shift 2>/dev/null || true
+ARM64_FLAG=""
+for arg in "$@"; do
+ [ "$arg" = "--arm64" ] && ARM64_FLAG="--arm64"
+done
+
+if [ "$FORMAT" = "auto" ]; then
+ if [ -f /etc/debian_version ]; then
+ FORMAT="deb"
+ elif [ -f /etc/fedora-release ] || [ -f /etc/redhat-release ]; then
+ FORMAT="rpm"
+ else
+ echo "!! Cannot detect OS — pass 'deb' or 'rpm' as argument" >&2
+ exit 1
+ fi
+fi
+
+ARCH=$(dpkg --print-architecture 2>/dev/null || rpm --eval '%{_arch}' 2>/dev/null || uname -m)
+if [ -n "$ARM64_FLAG" ]; then
+ ARCH="arm64"
+fi
+
+VERSION=$(python3 -c "
+import tomllib, pathlib
+p = pathlib.Path('$REPO/packages/meshbay-common/pyproject.toml')
+print(tomllib.loads(p.read_text())['project']['version'])
+")
+
+echo "========================================"
+echo " MeshBay $VERSION — $FORMAT $ARCH"
+echo "========================================"
+echo " Repo: $REPO"
+echo " Staging: $STAGING"
+echo " Output: $OUT"
+echo ""
+
+rm -rf "$OUT"
+mkdir -p "$OUT"
+
+# --- Step 1: Build staging trees ------------------------------------------
+
+echo "--- [1/4] meshbay-common (shared venv + all deps) ---"
+bash "$SCRIPT_DIR/build-common.sh" "$STAGING"
+
+echo ""
+echo "--- [2/4] meshbay-hub ---"
+bash "$SCRIPT_DIR/build-hub.sh" "$STAGING"
+
+echo ""
+echo "--- [3/4] meshbay-node ---"
+bash "$SCRIPT_DIR/build-node.sh" "$STAGING"
+
+echo ""
+echo "--- [4/4] meshbay-client ---"
+bash "$SCRIPT_DIR/build-client.sh" "$STAGING" $ARM64_FLAG
+
+# --- Step 2: Package ------------------------------------------------------
+
+echo ""
+echo "--- Packaging ($FORMAT) ---"
+
+if [ "$FORMAT" = "deb" ]; then
+ build_deb() {
+ local pkg="$1"
+ local root="$STAGING/${pkg}-root"
+ local deb_src="$REPO/packaging/deb/$pkg/DEBIAN"
+ local deb_dir="$root/DEBIAN"
+
+ mkdir -p "$deb_dir"
+ for f in "$deb_src"/*; do
+ [ -f "$f" ] || continue
+ sed -e "s/__VERSION__/$VERSION/g" \
+ -e "s/__ARCH__/$ARCH/g" \
+ "$f" > "$deb_dir/$(basename "$f")"
+ chmod 755 "$deb_dir/$(basename "$f")" 2>/dev/null || true
+ done
+ # control files should be 644, scripts 755
+ [ -f "$deb_dir/control" ] && chmod 644 "$deb_dir/control"
+ [ -f "$deb_dir/conffiles" ] && chmod 644 "$deb_dir/conffiles"
+
+ dpkg-deb --build --root-owner-group "$root" "$OUT/${pkg}_${VERSION}_${ARCH}.deb"
+ echo " -> $OUT/${pkg}_${VERSION}_${ARCH}.deb"
+ }
+
+ build_deb meshbay-common
+ build_deb meshbay-hub
+ build_deb meshbay-node
+ build_deb meshbay-client
+
+elif [ "$FORMAT" = "rpm" ]; then
+ RPMBUILD_DIR="$STAGING/rpmbuild"
+ rm -rf "$RPMBUILD_DIR"
+ mkdir -p "$RPMBUILD_DIR"/{SPECS,SOURCES,BUILD,RPMS,SRPMS}
+
+ build_rpm() {
+ local pkg="$1"
+ local spec="$REPO/packaging/rpm/${pkg}.spec"
+ local root="$STAGING/${pkg}-root"
+
+ # Substitute version and pass the staging root as a macro
+ sed "s/__VERSION__/$VERSION/g" "$spec" > "$RPMBUILD_DIR/SPECS/${pkg}.spec"
+
+ rpmbuild \
+ --define "_topdir $RPMBUILD_DIR" \
+ --define "_staging_root $root" \
+ -bb "$RPMBUILD_DIR/SPECS/${pkg}.spec" 2>&1 | tail -5
+
+ local rpm_file
+ rpm_file=$(find "$RPMBUILD_DIR/RPMS" -name "${pkg}-${VERSION}*.rpm" -print -quit)
+ if [ -n "$rpm_file" ]; then
+ cp "$rpm_file" "$OUT/"
+ echo " -> $OUT/$(basename "$rpm_file")"
+ fi
+ }
+
+ build_rpm meshbay-common
+ build_rpm meshbay-hub
+ build_rpm meshbay-node
+ build_rpm meshbay-client
+fi
+
+# --- Summary --------------------------------------------------------------
+echo ""
+echo "========================================"
+echo " Build complete"
+echo "========================================"
+ls -lh "$OUT"/
+echo ""
+echo "Install order:"
+echo " 1. meshbay-common"
+echo " 2. meshbay-hub and/or meshbay-node"
+echo " 3. meshbay-client"