mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-08-22 10:30:00 +00:00
StartLimitInterval and StartLimitBurst were set under [Service], where systemd 230 and later ignore them — they moved to [Unit] in 2016. The unit silently fell back to the system defaults of 5 starts in 10 s, and because RestartSec=10 spaces attempts further apart than that window, the limiter could never trip: a bot that could not reach its radio restarted every 10 seconds indefinitely instead of stopping in failed state. Moved to [Unit] as StartLimitIntervalSec=60 / StartLimitBurst=3, the values the file already declared. Applied to both the shipped unit and the one generated for the .deb, which had drifted the same way.
304 lines
13 KiB
Bash
Executable File
304 lines
13 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" || true)"
|
|
fi
|
|
# Never fall back to a hardcoded version: silently stamping a stale number onto
|
|
# the package produces an apparent downgrade that upgrades will refuse.
|
|
if [[ -z "${VERSION}" ]]; then
|
|
echo "ERROR: Could not read the version from pyproject.toml" >&2
|
|
echo " Install tomli (Python 3.10) or pass the version explicitly:" >&2
|
|
echo " ./scripts/build-deb.sh 1.0.0" >&2
|
|
exit 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
|
|
|
|
# Restart policy. These belong in [Unit], not [Service] — systemd 230 moved them,
|
|
# and it silently ignores them anywhere else. Give up after 3 starts in 60 s so a
|
|
# bot that cannot reach its radio lands in `failed` instead of looping forever.
|
|
StartLimitIntervalSec=60
|
|
StartLimitBurst=3
|
|
|
|
[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=1G
|
|
CPUQuota=200%
|
|
|
|
[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"
|