Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Local secrets / env
.env
.env.*
!.env.sample

# Toolchain + build outputs (downloaded/built inside the image)
.cosmocc/
*.o
king
test_king
*.com.dbg
*.aarch64.elf
.aarch64/
dist/

# Git metadata (submodules must already be checked out into vendor/)
.git/
.gitignore
.gitmodules

# CI / compose noise (not needed inside build context)
.github/
docker-compose.yml
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: CI

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
docker-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build and test (Docker)
run: make docker-test

- name: Build runtime image
run: make docker-image

docker-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Lint (Docker / LLVM 21)
run: make docker-lint
13 changes: 11 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,28 @@ This file provides guidance to AI coding agents when working with code in this r

## Build

Prefer Docker so the host only needs Docker (no local cosmocc / LLVM):

```bash
make
git submodule update --init # vendor/mbedtls required in the build context
make docker-test # compile + unit tests in Docker
make docker-king # extract ./king APE via Docker
make docker-lint # clang-format + clang-tidy (LLVM 21) in Docker
make docker-image # scratch runtime image tagged king:local
```

The Makefile auto-downloads `cosmocc` (Cosmopolitan C compiler) on first run and builds mbedtls from the vendored submodule. The output is `king`, an Actually Portable Executable.
Native (downloads cosmocc onto the host):

```bash
make
make test # build and run tests
make clean # remove build artifacts
make mbedclean # also clean mbedtls
make distclean # also remove downloaded cosmocc
```

The Makefile auto-downloads `cosmocc` for native builds and builds mbedtls from the vendored submodule. The output is `king`, an Actually Portable Executable. Dockerfile stages: `builder`, `test`, `lint`, `runtime`.

## Run

```bash
Expand Down
72 changes: 67 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,78 @@
FROM ubuntu:24.04 AS builder
# Multi-stage Cosmopolitan build for the king Discord bot.
# Host requirement: Docker only (no cosmocc / clang on the Mac).
#
# Targets:
# builder — compile king + test_king with cosmocc
# test — run unit tests (fails the build on test failure)
# lint — clang-format + clang-tidy (LLVM 21, matches CI)
# runtime — scratch image with assimilated APE (default)
#
# Prefer Makefile wrappers: `make docker-test`, `make docker-king`, etc.

# ── shared base with build deps ──────────────────────────────────
FROM ubuntu:24.04 AS base

RUN apt-get update && apt-get install -y --no-install-recommends \
make curl unzip ca-certificates git \
make curl unzip ca-certificates \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /src

# ── compile with cosmocc ────────────────────────────────────────
FROM base AS builder

COPY . .

RUN git submodule update --init && make && .cosmocc/current/bin/assimilate /src/king
# vendor/mbedtls comes from `git submodule update --init` on the host
# (or `git clone --recursive`). Keep the image free of a .git checkout.
RUN if [ ! -f vendor/mbedtls/include/mbedtls/ssl.h ]; then \
echo "error: vendor/mbedtls missing. Run: git submodule update --init" >&2; \
exit 1; \
fi

# Cache cosmocc across builds when BuildKit is available. Binaries are
# copied to /artifacts because cache mounts are not part of the image layer.
RUN --mount=type=cache,target=/src/.cosmocc,sharing=locked \
make \
&& make test_king \
&& mkdir -p /artifacts \
&& cp -a king test_king /artifacts/ \
&& cp -a king /artifacts/king.assimilated \
&& .cosmocc/current/bin/assimilate /artifacts/king.assimilated

# ── unit tests ───────────────────────────────────────────────────
FROM builder AS test

RUN /artifacts/test_king

# ── lint (same LLVM major as .github/workflows) ──────────────────
FROM ubuntu:24.04 AS lint

RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl gnupg make \
&& curl -fsSL https://apt.llvm.org/llvm-snapshot.gpg.key \
| gpg --dearmor -o /usr/share/keyrings/llvm.gpg \
&& echo "deb [signed-by=/usr/share/keyrings/llvm.gpg] https://apt.llvm.org/noble/ llvm-toolchain-noble-21 main" \
> /etc/apt/sources.list.d/llvm.list \
&& apt-get update && apt-get install -y --no-install-recommends \
clang-format-21 clang-tidy-21 \
&& ln -sf /usr/bin/clang-format-21 /usr/bin/clang-format \
&& ln -sf /usr/bin/clang-tidy-21 /usr/bin/clang-tidy \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /src
COPY . .

RUN if [ ! -f vendor/mbedtls/include/mbedtls/ssl.h ]; then \
echo "error: vendor/mbedtls missing. Run: git submodule update --init" >&2; \
exit 1; \
fi

RUN make lint

FROM scratch
# ── runtime image (default) ──────────────────────────────────────
FROM scratch AS runtime

COPY --from=builder /src/king /king
COPY --from=builder /artifacts/king.assimilated /king

ENTRYPOINT ["/king"]
61 changes: 55 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,21 +1,37 @@
# Cosmopolitan build for King Discord bot
# Produces an Actually Portable Executable (APE)
#
# Usage:
# make — builds the 'king' APE binary (auto-downloads cosmocc)
# Usage (Docker-first — preferred on macOS; only Docker needed on the host):
# make docker-test — build + run unit tests in Docker
# make docker-lint — clang-format + clang-tidy in Docker (LLVM 21)
# make docker-king — write ./king APE binary via Docker (no host cosmocc)
# make docker-image — build runtime scratch image tagged king:local
# make docker-run — run king:local with .env / env vars
#
# Native (downloads cosmocc onto the host):
# make — builds the 'king' APE binary
# make test — builds and runs tests
# make lint — runs clang-format and clang-tidy checks
# make clean — removes build artifacts
# make distclean — also removes downloaded cosmocc toolchain

# ── cosmocc toolchain (auto-downloaded) ──────────────────────────
# ── Docker-first wrappers (no host cosmocc / clang) ──────────────
DOCKER ?= docker
DOCKER_IMAGE ?= king
export DOCKER_BUILDKIT ?= 1

# Skip cosmocc download for Docker / lint / clean goals
DOCKER_GOALS = docker-test docker-lint docker-king docker-image docker-run docker-builder
SKIP_COSMOCC_GOALS = lint clean distclean mbedclean $(DOCKER_GOALS)

# ── cosmocc toolchain (auto-downloaded for native builds) ────────
COSMOCC_VERSION = 3.9.2
COSMOCC_SHA256 = f4ff13af65fcd309f3f1cfd04275996fb7f72a4897726628a8c9cf732e850193
COSMOCC_DIR = .cosmocc/$(COSMOCC_VERSION)
COSMOCC_BIN = $(COSMOCC_DIR)/bin

# Download cosmocc if not already present (skip for lint/clean targets)
ifneq ($(filter-out lint clean distclean mbedclean,$(MAKECMDGOALS)),)
# Download cosmocc if not already present (skip for lint/clean/docker targets)
ifneq ($(filter-out $(SKIP_COSMOCC_GOALS),$(MAKECMDGOALS)),)
DOWNLOAD := $(shell scripts/download-cosmocc.sh $(COSMOCC_DIR) $(COSMOCC_VERSION) $(COSMOCC_SHA256))
endif
ifeq ($(MAKECMDGOALS),)
Expand Down Expand Up @@ -90,4 +106,37 @@ mbedclean:
distclean: clean mbedclean
rm -rf .cosmocc

.PHONY: all test lint clean mbedclean distclean
# ── Docker targets ───────────────────────────────────────────────
# All compile/test/lint work happens inside the image. The host only
# needs Docker; cosmocc and LLVM are not installed locally.

docker-builder:
$(DOCKER) build --target builder -t $(DOCKER_IMAGE):builder .

docker-test:
$(DOCKER) build --target test -t $(DOCKER_IMAGE):test .

docker-lint:
$(DOCKER) build --target lint -t $(DOCKER_IMAGE):lint .

# Extract the portable APE (pre-assimilate) to ./king on the host.
docker-king: docker-builder
@cid=$$($(DOCKER) create $(DOCKER_IMAGE):builder); \
$(DOCKER) cp $$cid:/artifacts/king ./king; \
$(DOCKER) rm $$cid >/dev/null; \
echo "wrote ./king"

docker-image:
$(DOCKER) build --target runtime -t $(DOCKER_IMAGE):local .

docker-run: docker-image
@if [ -f .env ]; then \
$(DOCKER) run --rm --env-file .env $(DOCKER_IMAGE):local; \
else \
$(DOCKER) run --rm \
-e DISCORD_TOKEN -e DISCORD_REPLY -e DISCORD_REACTION -e LOG_LEVEL \
$(DOCKER_IMAGE):local; \
fi

.PHONY: all test lint clean mbedclean distclean \
docker-builder docker-test docker-lint docker-king docker-image docker-run
65 changes: 45 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,40 @@

A tiny discord bot written in C. say "king" in a channel and it reacts with 👑.

Built with [Cosmopolitan Libc](https://justine.lol/cosmopolitan/), so `make` spits out a single binary that runs on Linux, macOS, Windows, FreeBSD, OpenBSD, and NetBSD. No Docker, no runtime deps.
Built with [Cosmopolitan Libc](https://justine.lol/cosmopolitan/), so the build produces a single Actually Portable Executable that runs on Linux, macOS, Windows, FreeBSD, OpenBSD, and NetBSD.

## Build
## Build (Docker-first)

Only **Docker** is required on the host — no cosmocc, clang, or other toolchains.

```bash
git clone --recursive https://github.com/SteveParson/king.git
cd king
# if you cloned without --recursive:
# git submodule update --init

make docker-test # compile + run unit tests in Docker
make docker-king # write ./king to the host via Docker
make docker-image # build scratch runtime image (king:local)
```

| Target | What it does |
|--------|----------------|
| `make docker-test` | Build and run unit tests inside Docker |
| `make docker-lint` | `clang-format` + `clang-tidy` (LLVM 21) inside Docker |
| `make docker-king` | Produce `./king` APE on the host (no local cosmocc) |
| `make docker-image` | Build `king:local` scratch image |
| `make docker-run` | Run `king:local` with `.env` or exported env vars |

`docker compose build test|lint|king` works the same way if you prefer Compose.

### Native build (optional)

If you want cosmocc on the machine itself:

```bash
make
make test
```

## Run
Expand All @@ -18,7 +45,18 @@ export DISCORD_TOKEN='<your_token_here>'
./king
```

Or copy `.env.sample` to `.env`, fill in your token, and source it before running.
Or copy `.env.sample` to `.env`, fill in your token, and source it before running — or use Docker:

```bash
make docker-run
# equivalent: docker run --rm --env-file .env king:local
```

A pre-built image is also published to the GitHub Container Registry:

```bash
docker run -e DISCORD_TOKEN='<your_token_here>' ghcr.io/steveparson/king:latest
```

You need **Message Content Intent** enabled in the [Discord Developer Portal](https://discord.com/developers/applications).

Expand All @@ -29,28 +67,15 @@ You need **Message Content Intent** enabled in the [Discord Developer Portal](ht
| `DISCORD_REACTION` | `👑` | Reaction emoji |
| `LOG_LEVEL` | `info` | `error`, `info`, or `debug` |

## Docker

A pre-built image is published to the GitHub Container Registry:

```bash
docker run -e DISCORD_TOKEN='<your_token_here>' ghcr.io/steveparson/king:latest
```

Or using a `.env` file:
## Test / lint

```bash
cp .env.sample .env
# edit .env with your token
docker run --env-file .env ghcr.io/steveparson/king:latest
make docker-test
make docker-lint
```

## Test

Run `make test` to run the tests.

## How it works

~1000 lines of C, Connects to the Discord Gateway over WebSocket/TLS (mbedtls, vendored), listens for messages, checks for the word "king" (case-insensitive, whole-word match), and fires off a REST reply + reaction. Heartbeats, reconnects, the usual Gateway lifecycle stuff.
~1000 lines of C. Connects to the Discord Gateway over WebSocket/TLS (mbedtls, vendored), listens for messages, checks for the word "king" (case-insensitive, whole-word match), and fires off a REST reply + reaction. Heartbeats, reconnects, the usual Gateway lifecycle stuff.

Things we cook in this repo might not be very baked, but from our bit of testing, it 'works'.
Loading
Loading