305c3977a6
- AGENTS.md: mark PLAN.md as user-owned (never modify; it stays tracked) - move the .env ignore rule to the garage root so it covers any depth - drop the hardcoded "port 7654" hint from the no-server message; the target is now reported from the resolved SERVER_URL. The 7654 fallback remains only as the upstream TR default.
239 lines
10 KiB
Nim
239 lines
10 KiB
Nim
# Package
|
|
version = "0.1.0"
|
|
author = "Davide Cappellini"
|
|
description = "DevControlBot — control-skeleton bot that does nothing but show up bright"
|
|
license = "MIT"
|
|
srcDir = "."
|
|
bin = @[]
|
|
|
|
# Dependencies
|
|
# Installed globally with `nimble install`; nothing is vendored locally and
|
|
# no nimble.paths / vendor/ is used. `runBot` resolves each package's real
|
|
# location at run time with `nimble path` and passes explicit --path: flags,
|
|
# so the build does not rely on $HOME or on the compiler's bundled nimblepath.
|
|
requires "nim >= 2.0.0"
|
|
requires "robocode_tankroyale_botapi >= 1.0.7"
|
|
requires "jsony >= 1.1.5"
|
|
|
|
# The build-and-run logic lives here, not in DevControlBot.sh.
|
|
#
|
|
# NimScript cannot install signal handlers, so the temp build dir is owned by a
|
|
# small POSIX shell program (below) that Nimble executes via `exec` (which goes
|
|
# through the shell). That program traps EXIT/INT/TERM and always removes the
|
|
# throwaway build dir, including when compilation fails or the run is
|
|
# interrupted. Nothing is ever written to a persistent out/ directory.
|
|
#
|
|
# `exec` turns ANY non-zero exit into a NimScript exception, which prints a
|
|
# stack trace plus an escaped copy of the whole script. That is nonsense for the
|
|
# expected "no game server at SERVER_URL" outcome. So the shell always exits 0 and
|
|
# hands the real code back through the file named by $DEVCONTROLBOT_STATUS_FILE,
|
|
# which DevControlBot.sh exports and then re-exits with. Direct `nimble runBot`
|
|
# (no such variable) keeps the old behaviour: the shell exits with the real code
|
|
# and Nimble raises.
|
|
#
|
|
# DevControlBot.sh is a thin wrapper: it locates itself, chdirs here so Nimble
|
|
# finds this file, forwards caller args, calls `runBot` and forwards signals.
|
|
import std/os
|
|
|
|
const
|
|
runScript = """
|
|
# Resolve the globally-installed dependencies at run time. Plain `nim c` only
|
|
# finds them when $HOME is set (the Nim distribution's nim.cfg carries
|
|
# `nimblepath="$home/.nimble/pkgs2/"`), which is NOT true in bare environments
|
|
# (env -i, nix build sandboxes, CI). `nimble path` is the reliable,
|
|
# cwd-independent way to ask for their real location.
|
|
# Distinct exit codes, so a caller can tell the failure modes apart.
|
|
EXIT_NO_SERVER=1 # bot could not connect to the game server (expected)
|
|
EXIT_NO_DEPS=2 # a dependency is not installed
|
|
EXIT_COMPILE=3 # compilation failed
|
|
|
|
STATUS_FILE="${DEVCONTROLBOT_STATUS_FILE:-}"
|
|
|
|
# report <code>: write the real exit code to the status file. Returns 1 when
|
|
# there is no status file, i.e. the caller wants it as the shell's own code.
|
|
report() {
|
|
[ -n "$STATUS_FILE" ] || return 1
|
|
printf '%s\n' "$1" > "$STATUS_FILE"
|
|
}
|
|
|
|
# die <code>: finish with <code> as the run's real result — quietly (exit 0,
|
|
# the status file carries the code) or, with no listener, by exiting with it.
|
|
die() {
|
|
if report "$1"; then exit 0; else exit "$1"; fi
|
|
}
|
|
|
|
DEPS=""
|
|
for pkg in robocode_tankroyale_botapi jsony; do
|
|
pkgdir="$(nimble path "$pkg" 2>/dev/null | head -n 1)"
|
|
if [ -z "$pkgdir" ] || [ ! -d "$pkgdir" ]; then
|
|
echo "[devcontrolbot] COMPILE BLOCKED: dependency '$pkg' is not installed." >&2
|
|
echo "[devcontrolbot] fix with: nimble install $pkg" >&2
|
|
die $EXIT_NO_DEPS
|
|
fi
|
|
echo "[devcontrolbot] dependency $pkg -> $pkgdir"
|
|
DEPS="$DEPS '--path:$pkgdir'"
|
|
done
|
|
|
|
# Build mode: $DEVCONTROLBOT_DEBUG is set to "debug" by DevControlBot.sh when
|
|
# the caller passed --debug; anything else (including an unset variable, i.e. a
|
|
# direct `nimble runBot`) means release. Release is the quiet default.
|
|
BUILD_MODE="${DEVCONTROLBOT_DEBUG:-release}"
|
|
|
|
BUILD_DIR="$(mktemp -d "${TMPDIR:-/tmp}/devcontrolbot.XXXXXX")"
|
|
BINARY="$BUILD_DIR/DevControlBot"
|
|
|
|
# ---- optional .env ------------------------------------------------------------
|
|
# Configuration reaches the bot through the ENVIRONMENT, not through any code
|
|
# of ours: robocode_tankroyale_botapi's start() reads SERVER_URL and
|
|
# SERVER_SECRET (robocode_tankroyale_botapi.nim:424-425, documented at :396-397)
|
|
# and loadBotInfo() reads BOT_NAME / BOT_VERSION / ... from the environment when
|
|
# the JSON is absent (bot_info.nim:117-130). That is the documented, supported
|
|
# mechanism the official BotLauncher uses, so the only thing we do here is make
|
|
# those variables PRESENT in the bot's process environment for local runs.
|
|
#
|
|
# Loaded HERE, in the runBot task, and not in DevControlBot.sh, so it applies both
|
|
# to `./DevControlBot.sh` and to a direct `nimble runBot` (one implementation, and
|
|
# the wrapper stays a thin wrapper).
|
|
# - optional: a missing file is not an error, nothing is printed but a note,
|
|
# the bot just uses the API defaults.
|
|
# - CRLF-tolerant: CRs are stripped so a Windows-edited file does not end up
|
|
# with the value "bar\r".
|
|
# - both `FOO=bar` and `export FOO=bar` styles.
|
|
# - never echoed, never `set -x`: only the file NAME is reported.
|
|
# - $DEVCONTROLBOT_ENV_FILE overrides the default path (".env" next to the
|
|
# .nimble, which is the cwd because DevControlBot.sh chdirs there).
|
|
#
|
|
# PRECEDENCE — CRITICAL. The .env file must NEVER override a variable that is
|
|
# already present in the environment: the official BotLauncher's values (and the
|
|
# caller's explicit `FOO=bar ./DevControlBot.sh`) must always win, otherwise this
|
|
# convenience file could silently break a BotLauncher run. Sourcing the file
|
|
# directly would do the opposite, because an assignment in a sourced file
|
|
# overwrites an already-exported variable of the same name. So the file is
|
|
# FILTERED line by line: a `NAME=value` line is dropped when NAME is already set
|
|
# in the environment (tested with `[ -n "${NAME+x}" ]`, i.e. set-but-maybe-empty,
|
|
# not non-empty), and only the remaining, still-unset names are sourced with
|
|
# `set -a`. The filtering is what implements the precedence, not the source order.
|
|
ENV_FILE="${DEVCONTROLBOT_ENV_FILE:-.env}"
|
|
if [ -f "$ENV_FILE" ]; then
|
|
ENV_RAW="$BUILD_DIR/env.raw"
|
|
ENV_KEEP="$BUILD_DIR/env.keep"
|
|
ENV_ADDED=""
|
|
# CRLF tolerance: drop the CRs, keep everything else verbatim.
|
|
tr -d '\r' < "$ENV_FILE" > "$ENV_RAW"
|
|
: > "$ENV_KEEP"
|
|
while IFS= read -r line || [ -n "$line" ]; do
|
|
case "$line" in
|
|
''|\#*) continue ;; # blank / comment
|
|
export\ *|export=*) line="${line#export }" ;;
|
|
esac
|
|
name="${line%%=*}"
|
|
# Only plain NAME=value assignments; anything else is ignored, so the file
|
|
# can never smuggle in commands. `name` is validated as an identifier
|
|
# BEFORE being used in the eval below.
|
|
printf '%s' "$name" | grep -q '^[A-Za-z_][A-Za-z_0-9]*$' || continue
|
|
# PRECEDENCE: skip if the variable already exists in the environment.
|
|
if eval "[ -n \"\${$name+x}\" ]"; then
|
|
continue
|
|
fi
|
|
printf '%s\n' "$line" >> "$ENV_KEEP"
|
|
ENV_ADDED="$ENV_ADDED $name"
|
|
done < "$ENV_RAW"
|
|
if [ -n "$ENV_ADDED" ]; then
|
|
set -a
|
|
. "$ENV_KEEP"
|
|
set +a
|
|
fi
|
|
rm -f "$ENV_RAW" "$ENV_KEEP"
|
|
# Variable NAMES only, never values.
|
|
echo "[devcontrolbot] env file loaded: $ENV_FILE (names added:${ENV_ADDED:- none}; values never printed)"
|
|
echo "[devcontrolbot] variables already in the environment win and were left untouched"
|
|
else
|
|
echo "[devcontrolbot] no env file at $ENV_FILE (optional, using API defaults)"
|
|
fi
|
|
|
|
cleanup() {
|
|
rc=$?
|
|
trap - EXIT INT TERM
|
|
rm -rf "$BUILD_DIR"
|
|
if [ -d "$BUILD_DIR" ]; then
|
|
echo "[devcontrolbot] cleanup failed: $BUILD_DIR still present" >&2
|
|
exit 1
|
|
fi
|
|
exit $rc
|
|
}
|
|
trap cleanup EXIT
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
|
|
# Release: hints and the progress/dot lines are noise, so the compiler output is
|
|
# captured to a log and only shown if the build fails (errors are ALWAYS shown).
|
|
# Debug: classic build, full compiler output (config-file hints, diagnostics).
|
|
LOG="$BUILD_DIR/build.log"
|
|
if [ "$BUILD_MODE" = "debug" ]; then
|
|
echo "[devcontrolbot] compiling DevControlBot.nim (DEBUG BUILD) -> $BINARY"
|
|
# `eval` re-parses $DEPS so the embedded quotes do the word splitting: the
|
|
# dependency paths survive spaces, without relying on glob or brace expansion.
|
|
# No pipe: POSIX sh has no PIPESTATUS, and `nim | tee` would report tee's
|
|
# status, not the compiler's. Buffer first, then replay the log verbatim.
|
|
eval nim c $DEPS --out:"$BINARY" DevControlBot.nim >"$LOG" 2>&1
|
|
compile_rc=$?
|
|
cat "$LOG"
|
|
else
|
|
echo "[devcontrolbot] compiling DevControlBot.nim -> $BINARY"
|
|
eval nim c -d:release --hints:off $DEPS --out:"$BINARY" DevControlBot.nim >"$LOG" 2>&1
|
|
compile_rc=$?
|
|
fi
|
|
if [ $compile_rc -ne 0 ]; then
|
|
if [ "$BUILD_MODE" != "debug" ]; then
|
|
echo "[devcontrolbot] --- compiler output ---" >&2
|
|
cat "$LOG" >&2
|
|
echo "[devcontrolbot] --- end compiler output ---" >&2
|
|
fi
|
|
echo "[devcontrolbot] COMPILE FAILED (nim c exited $compile_rc) — errors above." >&2
|
|
die $EXIT_COMPILE
|
|
fi
|
|
rm -f "$LOG"
|
|
if [ "$BUILD_MODE" = "debug" ]; then
|
|
echo "[devcontrolbot] build ok (debug)"
|
|
else
|
|
echo "[devcontrolbot] build ok (release)"
|
|
fi
|
|
|
|
# The bot reads argv[1] as its metadata JSON path. If the caller already
|
|
# supplied a JSON, use that instead of the bundled one.
|
|
if [ -n "${1:-}" ] && [ "${1##*.}" = "json" ]; then
|
|
echo "[devcontrolbot] using caller-supplied metadata: $1"
|
|
"$BINARY" "$@"
|
|
else
|
|
# NOTE: no `exec` here: the shell must survive so the EXIT trap can clean up.
|
|
echo "[devcontrolbot] running $BINARY DevControlBot.json $*"
|
|
"$BINARY" DevControlBot.json "$@"
|
|
fi
|
|
bot_rc=$?
|
|
if [ $bot_rc -ne 0 ]; then
|
|
echo "[devcontrolbot] bot exited with code $bot_rc." >&2
|
|
if [ $bot_rc -eq $EXIT_NO_SERVER ]; then
|
|
# The Tank Royale client answers a refused connection with this exact
|
|
# exit code, so it means "no server", not "broken bot".
|
|
echo "[devcontrolbot] No game server on ${SERVER_URL:-ws://localhost:7654} (SERVER_URL)." >&2
|
|
echo "[devcontrolbot] Start the Tank Royale server, or point SERVER_URL at a running one." >&2
|
|
else
|
|
echo "[devcontrolbot] The bot ran but failed at runtime (crash or bad metadata)." >&2
|
|
fi
|
|
fi
|
|
die $bot_rc
|
|
"""
|
|
|
|
task runBot, "Compile, run and clean up the bot":
|
|
# Nimble passes every CLI argument here (its own flags, then the task name,
|
|
# then the caller's arguments). Ours are the ones after the task name.
|
|
var args = ""
|
|
var seen = false
|
|
for a in commandLineParams():
|
|
if seen:
|
|
if args != "": args = args & " "
|
|
args = args & quoteShell(a)
|
|
elif a == "runBot":
|
|
seen = true
|
|
exec "sh -c " & quoteShell(runScript) & " devcontrolbot " & args
|