# Package version = "1.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 : 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 : finish with 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