Languages
The eighteen toolchains a Runner ships with, why an id names a compiler as well as a standard, and the four images without which it judges nothing.
The Runner ships with eighteen toolchains in three families, and they share four images. The catalog is data: adding a row is a table change, not a redesign.
An id has two levels, because it answers two questions
A participant reading a problem header wants the standard — "C++17". A
participant choosing on the submit form needs the toolchain, because g++
and clang++ disagree about enough that a submission accepted by one is
occasionally rejected by the other.
So the id carries both: cpp17-gcc, never cpp17. A standard is not a
toolchain. cpp17 on its own resolves to nothing and is refused, because
guessing which compiler somebody meant is exactly what two levels exist to
prevent.
The catalog
| Id | Label | Family | Image |
|---|---|---|---|
c89-gcc | C89 / ANSI C (GCC) | C | GCC |
c89-clang | C89 / ANSI C (Clang) | C | Clang |
c99-gcc | C99 (GCC) | C | GCC |
c99-clang | C99 (Clang) | C | Clang |
c11-gcc | C11 (GCC) | C | GCC |
c11-clang | C11 (Clang) | C | Clang |
c23-gcc | C23 (GCC) | C | GCC |
c23-clang | C23 (Clang) | C | Clang |
cpp11-gcc | C++11 (GCC) | C++ | GCC |
cpp11-clang | C++11 (Clang) | C++ | Clang |
cpp17-gcc | C++17 (GCC) | C++ | GCC |
cpp17-clang | C++17 (Clang) | C++ | Clang |
cpp20-gcc | C++20 (GCC) | C++ | GCC |
cpp20-clang | C++20 (Clang) | C++ | Clang |
cpp23-gcc | C++23 (GCC) | C++ | GCC |
cpp23-clang | C++23 (Clang) | C++ | Clang |
python3 | Python 3 (CPython) | Python | CPython |
pypy3 | Python 3 (PyPy) | Python | PyPy |
Every C and C++ standard is offered through both compilers, which is what a course teaching a particular standard may legitimately want to pin.
PyPy is a separate row rather than a footnote because the gap changes what a participant can do: a Python solution that times out on CPython frequently does not on PyPy.
What each family accepts, and how it is built
| Family | File extensions | Built with |
|---|---|---|
| C | .c | the compiler, -O2 -std=… and -static |
| C++ | .cpp, .cc, .cxx, .c++ | the compiler, -O2 -std=… and -static |
| Python | .py | py_compile |
Extensions are matched case-insensitively. A file the chosen toolchain does not
accept is refused as a compilation error, not as an infrastructure failure:
choosing C++ and uploading main.py is the participant's own mistake, the
compiler would have said the same thing thirty seconds later, and a message
naming the extensions is more use than a parse error in a language nobody thought
they were writing.
-O2 is on every compiled row because a limit is stated against optimized code;
judging a debug build would make every limit a different limit.
Python is not compiled, but it does have a build step. py_compile turns a syntax
error into one failure a participant can read, rather than into every test
failing with the same traceback.
Two ids that resolve but are not offered
cpp and python were the whole catalog once, and a problem package's
config.yml names the language of a checker, an interactor and a model solution
using them. They still resolve — cpp to cpp20-gcc, python to python3 — so
that packages written against the documented format keep building their own
judging programs. A checker and an interactor are built by the same path, in the
same image, with the same command as a submission.
They are not offered on any form.
cpp resolves to C++20 specifically because that is the standard the single C++
entry carried before the catalog existed, so a package judged yesterday is
judged the same way today.
What is not here
Java and Rust are not in the catalog and are not accepted. Java was considered and left out for a measurement reason: the JVM reserves address space rather than allocating it, so a memory limit would measure something different for it than for everything else.
Forwarding to an external judge offers six languages including Java and Pascal — by forwarding to a service that runs them, not by running them.
Four images, and a Runner without them judges nothing
| Image key | Variable |
|---|---|
| GCC | AJ_Sandbox__Image__Gcc |
| Clang | AJ_Sandbox__Image__Clang |
| CPython | AJ_Sandbox__Image__Python |
| PyPy | AJ_Sandbox__Image__Pypy |
Eighteen toolchains share four images because every C and C++ standard is a
-std flag rather than an image of its own.
The defaults, and the older AJ_Sandbox__Image__Cpp spelling that is still read,
are on Configuration — stated once, so that the two
pages cannot drift apart.
A Runner fetches the four images at start, and will not register without them
The Runner image contains no compiler. Every submission is built and run inside
one of the four images above, so at start the Runner fetches each one the
operator named and proves it carries the measuring shim. A Runner that cannot
get all four exits instead of registering, naming the AJ_Sandbox__Image__*
setting it could not satisfy — so nothing is claimed that it would fail.
Until 2026-09-16 it did the opposite, and an installation could run for weeks that way: nothing on the judging path fetched anything, so a Runner started, was approved, claimed work and failed every job. If you are reading an older account of this, that is what it describes.
A fetch that fails over a copy already on the host is a warning, not a refusal: the air-gapped installation and the private registry an anonymous pull cannot authenticate to both keep judging with what they have.
The defaults are the tags a development stack builds locally, so a development Runner wants them built before it starts. A deployment names all four explicitly and pins the same version as the Runner — that pairing is what the release was tested as.
The compiler version is a capability the Runner reports, which is why the images are the project's own and pinned rather than a stock compiler image that moves. A tag that moves means two submissions to the same problem were judged by two different compilers.
Building an image of your own
AJ_Sandbox__Image__* will take any image, which is how you pin a compiler
version we do not ship or add one to a fork. Four things are expected of it.
| What, and why | |
|---|---|
/etc/algojudge-toolchain | one line per compiler, whatever --version prints. The Runner reports it as a capability, and it is how two submissions to one problem can be shown to have met the same compiler |
/out, owned by 65534:65534 | where a build puts what it made. Part of the image because the container is unprivileged and cannot create a directory at the root of its own filesystem |
USER 65534:65534 | the safe default, and the one a step that is not measured runs as |
/usr/local/bin/aj-shim | the measuring shim, below |
The shim, and how to get one
A measured run is started as aj-shim <input> <output> <program> [args...]. The
shim becomes PID 1, takes the two channels onto the submission's standard input
and output, drops to 65534, forks, and reports the child's own processor time
and peak memory — see Isolation for what that is worth
and why it is not trusted on its own.
The input is a socket for a batch problem: the Runner has the test's input in a sealed file in memory and hands the descriptor over it, which is how a judged container comes to hold no part of the package. For an interactive one it is a named pipe with the interactor at the far end.
Copy ours rather than writing one. It is statically linked and depends on nothing in the image around it:
COPY --from=ghcr.io/algojudge/lang-gcc:0.2.0 /usr/local/bin/aj-shim /usr/local/bin/aj-shimPin the same version as the Runner, for the reason the callout above gives about tags that move.
An image without it cannot judge
Leave the shim out and the image judges nothing: a judged container keeps no log and writes its answer to a channel only the shim opens, and the test's input arrives as a descriptor only the shim can receive. The Runner refuses the run and says which image, once per image rather than once per submission — and an image replaced under the same tag needs the Runner restarted, because the answer is remembered.
What you must not do is ship a different program under that name. The Runner reads what the report says; a shim that reports less than the program spent is a submission that beats the time limit, and one that reports more is a correct solution refused.
What decides which toolchains a problem allows
Not the Runner. An assignment's configuration may list the languages it accepts, and the Runner refuses anything outside that list as a policy violation — a verdict rather than an infrastructure failure, because the participant chose it, their code may be perfect, and what they broke is a rule of the activity. The submission stays rejudgeable if a manager widens the set afterwards.
An empty list means the assignment said nothing, which allows anything this Runner can build. It is not a way of allowing none.
The list may name a family as well as a toolchain: cpp in an assignment's
languages accepts every C++ row. That two-level lookup exists to close a silent
failure — a rule keyed on the whole id alone would have found nothing for
cpp17-gcc and reported no violations, which reads exactly like a clean
submission.