mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-08-01 00:09:48 +00:00
291 lines
12 KiB
Bash
Executable File
291 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# build-deb.sh — Build a .deb package for meshcore-bot
|
|
# Usage: ./scripts/build-deb.sh [version]
|
|
#
|
|
# Produces: dist/meshcore-bot_<version>_all.deb
|
|
# Requirements: dpkg-deb, fakeroot (sudo apt install fakeroot)
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
|
|
# ── Version ──────────────────────────────────────────────────────────────────
|
|
VERSION="${1:-}"
|
|
if [[ -z "${VERSION}" ]]; then
|
|
VERSION="$(python3 -c "import tomllib; d=tomllib.load(open('${PROJECT_ROOT}/pyproject.toml','rb')); print(d['project']['version'])" 2>/dev/null || \
|
|
python3 -c "import tomli; d=tomli.load(open('${PROJECT_ROOT}/pyproject.toml','rb')); print(d['project']['version'])" 2>/dev/null || \
|
|
grep -Po '(?<=^version = ")([^"]+)' "${PROJECT_ROOT}/pyproject.toml" || echo "0.9.1")"
|
|
fi
|
|
|
|
# Validate VERSION: must be semver-like (digits and dots only) to prevent
|
|
# shell injection via the unquoted <<EOF heredoc below.
|
|
if [[ ! "${VERSION}" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?([~+][A-Za-z0-9._-]+)?$ ]]; then
|
|
echo "ERROR: Invalid version string: '${VERSION}'" >&2
|
|
echo " Must match semver format, e.g. 1.2.3 or 1.2.3~rc1" >&2
|
|
exit 1
|
|
fi
|
|
|
|
PACKAGE_NAME="meshcore-bot"
|
|
ARCH="all"
|
|
INSTALL_ROOT="/opt/meshcore-bot"
|
|
CONF_DIR="/etc/meshcore-bot"
|
|
LOG_DIR="/var/log/meshcore-bot"
|
|
DATA_DIR="/var/lib/meshcore-bot"
|
|
SYSTEMD_DIR="/lib/systemd/system"
|
|
|
|
BUILD_DIR="${PROJECT_ROOT}/dist/deb-build/${PACKAGE_NAME}_${VERSION}_${ARCH}"
|
|
OUT_DIR="${PROJECT_ROOT}/dist"
|
|
|
|
echo "==> Building ${PACKAGE_NAME} ${VERSION} (arch=${ARCH})"
|
|
echo " Project root : ${PROJECT_ROOT}"
|
|
echo " Build dir : ${BUILD_DIR}"
|
|
echo " Output dir : ${OUT_DIR}"
|
|
echo ""
|
|
|
|
# ── Clean & create staging tree ──────────────────────────────────────────────
|
|
rm -rf "${BUILD_DIR}"
|
|
mkdir -p \
|
|
"${BUILD_DIR}/DEBIAN" \
|
|
"${BUILD_DIR}${INSTALL_ROOT}" \
|
|
"${BUILD_DIR}${CONF_DIR}" \
|
|
"${BUILD_DIR}${LOG_DIR}" \
|
|
"${BUILD_DIR}${DATA_DIR}" \
|
|
"${BUILD_DIR}${SYSTEMD_DIR}" \
|
|
"${OUT_DIR}"
|
|
|
|
# ── Copy application files ────────────────────────────────────────────────────
|
|
echo "==> Copying application files…"
|
|
rsync -a \
|
|
--exclude='.git' \
|
|
--exclude='.github' \
|
|
--exclude='.cursor' \
|
|
--exclude='__pycache__' \
|
|
--exclude='*.pyc' \
|
|
--exclude='*.egg-info' \
|
|
--exclude='.venv' \
|
|
--exclude='venv' \
|
|
--exclude='dist' \
|
|
--exclude='local' \
|
|
--exclude='*.db' \
|
|
--exclude='*.log' \
|
|
--exclude='config.ini' \
|
|
--exclude='.env' \
|
|
--exclude='*.key' \
|
|
--exclude='*.pem' \
|
|
--exclude='*.p12' \
|
|
--exclude='*.pfx' \
|
|
--exclude='node_modules' \
|
|
"${PROJECT_ROOT}/" "${BUILD_DIR}${INSTALL_ROOT}/"
|
|
|
|
# ── Default config ────────────────────────────────────────────────────────────
|
|
echo "==> Installing default config…"
|
|
cp "${PROJECT_ROOT}/config.ini.example" "${BUILD_DIR}${CONF_DIR}/config.ini"
|
|
# Service installs keep mutable state and local plugins out of the root-owned
|
|
# application tree. Only replace the documented defaults; custom package
|
|
# builders can still provide absolute paths of their own.
|
|
sed -i \
|
|
-e "s|^db_path = meshcore_bot.db$|db_path = ${DATA_DIR}/meshcore_bot.db|" \
|
|
-e "s|^#local_dir_path = local$|local_dir_path = ${DATA_DIR}/local|" \
|
|
-e "s|^log_file = meshcore_bot.log$|log_file = ${LOG_DIR}/meshcore_bot.log|" \
|
|
"${BUILD_DIR}${CONF_DIR}/config.ini"
|
|
# Symlink so the app finds it in the install root
|
|
ln -sf "${CONF_DIR}/config.ini" "${BUILD_DIR}${INSTALL_ROOT}/config.ini"
|
|
|
|
# ── systemd unit ──────────────────────────────────────────────────────────────
|
|
echo "==> Installing systemd unit…"
|
|
cat > "${BUILD_DIR}${SYSTEMD_DIR}/${PACKAGE_NAME}.service" << 'UNIT'
|
|
[Unit]
|
|
Description=MeshCore Bot - Mesh Network Bot Service
|
|
Documentation=https://github.com/agessaman/meshcore-bot
|
|
After=network.target
|
|
Wants=network.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=meshcore
|
|
Group=meshcore
|
|
WorkingDirectory=/opt/meshcore-bot
|
|
ExecStart=/opt/meshcore-bot/venv/bin/python /opt/meshcore-bot/meshcore_bot.py --config /etc/meshcore-bot/config.ini
|
|
ExecReload=/bin/kill -HUP $MAINPID
|
|
Restart=always
|
|
RestartSec=10
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
SyslogIdentifier=meshcore-bot
|
|
Environment=PYTHONPATH=/opt/meshcore-bot
|
|
Environment=PYTHONUNBUFFERED=1
|
|
NoNewPrivileges=true
|
|
PrivateTmp=true
|
|
ProtectSystem=strict
|
|
ProtectHome=true
|
|
UMask=0077
|
|
ConfigurationDirectory=meshcore-bot
|
|
ConfigurationDirectoryMode=0700
|
|
StateDirectory=meshcore-bot
|
|
StateDirectoryMode=0700
|
|
LogsDirectory=meshcore-bot
|
|
LogsDirectoryMode=0750
|
|
ReadWritePaths=/etc/meshcore-bot
|
|
ReadWritePaths=/var/log/meshcore-bot
|
|
ReadWritePaths=/var/lib/meshcore-bot
|
|
LimitNOFILE=65536
|
|
MemoryMax=512M
|
|
StartLimitInterval=60
|
|
StartLimitBurst=3
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
UNIT
|
|
|
|
# ── DEBIAN/control ─────────────────────────────────────────────────────────────
|
|
echo "==> Writing DEBIAN/control…"
|
|
# Compute rough installed size (kB)
|
|
INSTALLED_SIZE=$(du -s "${BUILD_DIR}${INSTALL_ROOT}" | awk '{print $1}')
|
|
|
|
cat > "${BUILD_DIR}/DEBIAN/control" << EOF
|
|
Package: ${PACKAGE_NAME}
|
|
Version: ${VERSION}
|
|
Section: net
|
|
Priority: optional
|
|
Architecture: ${ARCH}
|
|
Installed-Size: ${INSTALLED_SIZE}
|
|
Depends: python3 (>= 3.10), python3-pip, python3-venv, adduser
|
|
Recommends: systemd
|
|
Suggests: sqlite3
|
|
Maintainer: MeshCore Bot Team <noreply@example.com>
|
|
Description: MeshCore Bot - Mesh Network Automation Bot
|
|
A feature-rich bot for MeshCore mesh radio networks.
|
|
Supports command handling, scheduled messages, web viewer,
|
|
Discord/Telegram bridges, inbound webhooks, and more.
|
|
Homepage: https://github.com/agessaman/meshcore-bot
|
|
EOF
|
|
|
|
# ── DEBIAN/conffiles ──────────────────────────────────────────────────────────
|
|
echo "${CONF_DIR}/config.ini" > "${BUILD_DIR}/DEBIAN/conffiles"
|
|
|
|
# Debian runs the old prerm before this new preinst. The helper preserves
|
|
# markers written by hardened prerm versions and recognizes the first legacy
|
|
# transition through a root-owned persistent policy marker.
|
|
cp "${PROJECT_ROOT}/scripts/debian_service_state.sh" "${BUILD_DIR}/DEBIAN/preinst"
|
|
chmod 0755 "${BUILD_DIR}/DEBIAN/preinst"
|
|
|
|
# ── DEBIAN/postinst ───────────────────────────────────────────────────────────
|
|
cat > "${BUILD_DIR}/DEBIAN/postinst" << 'POSTINST'
|
|
#!/bin/bash
|
|
set -e
|
|
|
|
INSTALL_ROOT="/opt/meshcore-bot"
|
|
CONF_DIR="/etc/meshcore-bot"
|
|
LOG_DIR="/var/log/meshcore-bot"
|
|
DATA_DIR="/var/lib/meshcore-bot"
|
|
SERVICE_USER="meshcore"
|
|
|
|
# Create service user if it doesn't exist
|
|
if ! id -u "${SERVICE_USER}" >/dev/null 2>&1; then
|
|
adduser --system --group --no-create-home \
|
|
--home "${INSTALL_ROOT}" \
|
|
--shell /usr/sbin/nologin \
|
|
--gecos "MeshCore Bot service account" \
|
|
"${SERVICE_USER}"
|
|
fi
|
|
|
|
# Root owns executable code and the virtualenv; the service can write only its
|
|
# explicit configuration/state/log trees. 0700/0600 also protect API keys,
|
|
# database contents, local plugin config, and backups from other local users.
|
|
chown -R root:root "${INSTALL_ROOT}"
|
|
find "${INSTALL_ROOT}" -type d -exec chmod 0755 {} +
|
|
find "${INSTALL_ROOT}" -type f -exec chmod go-w {} +
|
|
install -d -o "${SERVICE_USER}" -g "${SERVICE_USER}" -m 0750 "${LOG_DIR}"
|
|
install -d -o "${SERVICE_USER}" -g "${SERVICE_USER}" -m 0700 \
|
|
"${DATA_DIR}" "${DATA_DIR}/local" "${DATA_DIR}/local/commands" \
|
|
"${DATA_DIR}/local/service_plugins" "${DATA_DIR}/backups"
|
|
install -d -o "${SERVICE_USER}" -g "${SERVICE_USER}" -m 0700 "${CONF_DIR}"
|
|
# Previous package versions resolved relative settings from /etc/meshcore-bot.
|
|
# The service is stopped by prerm, so SQLite backup migration is coherent.
|
|
python3 "${INSTALL_ROOT}/scripts/migrate_service_layout.py" \
|
|
--config "${CONF_DIR}/config.ini" \
|
|
--legacy-base "${CONF_DIR}" \
|
|
--state-dir "${DATA_DIR}" \
|
|
--log-dir "${LOG_DIR}"
|
|
chown -R "${SERVICE_USER}:${SERVICE_USER}" "${CONF_DIR}" "${DATA_DIR}" "${LOG_DIR}"
|
|
find "${CONF_DIR}" "${DATA_DIR}" -type d -exec chmod 0700 {} +
|
|
find "${CONF_DIR}" "${DATA_DIR}" -type f -exec chmod 0600 {} +
|
|
chmod 0750 "${LOG_DIR}"
|
|
find "${LOG_DIR}" -type f -exec chmod 0600 {} +
|
|
|
|
# Rebuild the venv from trusted requirements. Older releases made it writable
|
|
# by the service user, so an in-place upgrade could preserve executable .pth or
|
|
# module persistence.
|
|
VENV_BUILD="${INSTALL_ROOT}/.venv-build-$$"
|
|
VENV_OLD="${INSTALL_ROOT}/.venv-old-$$"
|
|
rm -rf "${VENV_BUILD}" "${VENV_OLD}"
|
|
echo "Building fresh Python virtualenv…"
|
|
python3 -m venv "${VENV_BUILD}"
|
|
"${VENV_BUILD}/bin/pip" install --quiet --upgrade pip
|
|
"${VENV_BUILD}/bin/pip" install --quiet -r "${INSTALL_ROOT}/requirements.txt"
|
|
if [ -d "${INSTALL_ROOT}/venv" ]; then
|
|
mv "${INSTALL_ROOT}/venv" "${VENV_OLD}"
|
|
fi
|
|
if ! mv "${VENV_BUILD}" "${INSTALL_ROOT}/venv"; then
|
|
[ -d "${VENV_OLD}" ] && mv "${VENV_OLD}" "${INSTALL_ROOT}/venv"
|
|
exit 1
|
|
fi
|
|
rm -rf "${VENV_OLD}"
|
|
|
|
# A legacy prerm destroys the prior state before new package code can observe
|
|
# it. The helper uses an explicit availability-first policy for that one
|
|
# transition, then preserves enablement/activity exactly on later upgrades.
|
|
bash "${INSTALL_ROOT}/scripts/debian_service_state.sh" restore "${2:-}"
|
|
|
|
echo ""
|
|
echo "meshcore-bot installed successfully."
|
|
echo "Edit /etc/meshcore-bot/config.ini before starting the service."
|
|
POSTINST
|
|
chmod 0755 "${BUILD_DIR}/DEBIAN/postinst"
|
|
|
|
# ── DEBIAN/prerm ──────────────────────────────────────────────────────────────
|
|
cat > "${BUILD_DIR}/DEBIAN/prerm" << 'PRERM'
|
|
#!/bin/bash
|
|
set -e
|
|
|
|
bash /opt/meshcore-bot/scripts/debian_service_state.sh preremove "${1:-}"
|
|
PRERM
|
|
chmod 0755 "${BUILD_DIR}/DEBIAN/prerm"
|
|
|
|
# ── DEBIAN/postrm ─────────────────────────────────────────────────────────────
|
|
cat > "${BUILD_DIR}/DEBIAN/postrm" << 'POSTRM'
|
|
#!/bin/bash
|
|
set -e
|
|
|
|
if [ "$1" = "purge" ]; then
|
|
rm -rf /opt/meshcore-bot/venv
|
|
rm -rf /var/lib/meshcore-bot
|
|
if command -v systemctl >/dev/null 2>&1; then
|
|
systemctl daemon-reload || true
|
|
fi
|
|
fi
|
|
POSTRM
|
|
chmod 0755 "${BUILD_DIR}/DEBIAN/postrm"
|
|
|
|
# ── Set permissions ───────────────────────────────────────────────────────────
|
|
echo "==> Setting permissions…"
|
|
find "${BUILD_DIR}" -type d -exec chmod 0755 {} \;
|
|
find "${BUILD_DIR}" -type f -exec chmod 0644 {} \;
|
|
chmod 0755 "${BUILD_DIR}/DEBIAN/preinst" "${BUILD_DIR}/DEBIAN/postinst" \
|
|
"${BUILD_DIR}/DEBIAN/prerm" "${BUILD_DIR}/DEBIAN/postrm"
|
|
chmod 0600 "${BUILD_DIR}${CONF_DIR}/config.ini"
|
|
# Make Python entry point executable
|
|
chmod 0755 "${BUILD_DIR}${INSTALL_ROOT}/meshcore_bot.py" 2>/dev/null || true
|
|
|
|
# ── Build the .deb ────────────────────────────────────────────────────────────
|
|
DEB_FILE="${OUT_DIR}/${PACKAGE_NAME}_${VERSION}_${ARCH}.deb"
|
|
echo "==> Building .deb: ${DEB_FILE}"
|
|
fakeroot dpkg-deb --build "${BUILD_DIR}" "${DEB_FILE}"
|
|
|
|
echo ""
|
|
echo "Done! Package: ${DEB_FILE}"
|
|
echo ""
|
|
echo "Install with: sudo dpkg -i ${DEB_FILE}"
|
|
echo " sudo apt-get install -f # fix dependencies if needed"
|