AlgoJudge documentation0.2

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

IdLabelFamilyImage
c89-gccC89 / ANSI C (GCC)CGCC
c89-clangC89 / ANSI C (Clang)CClang
c99-gccC99 (GCC)CGCC
c99-clangC99 (Clang)CClang
c11-gccC11 (GCC)CGCC
c11-clangC11 (Clang)CClang
c23-gccC23 (GCC)CGCC
c23-clangC23 (Clang)CClang
cpp11-gccC++11 (GCC)C++GCC
cpp11-clangC++11 (Clang)C++Clang
cpp17-gccC++17 (GCC)C++GCC
cpp17-clangC++17 (Clang)C++Clang
cpp20-gccC++20 (GCC)C++GCC
cpp20-clangC++20 (Clang)C++Clang
cpp23-gccC++23 (GCC)C++GCC
cpp23-clangC++23 (Clang)C++Clang
python3Python 3 (CPython)PythonCPython
pypy3Python 3 (PyPy)PythonPyPy

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

FamilyFile extensionsBuilt with
C.cthe compiler, -O2 -std=… and -static
C++.cpp, .cc, .cxx, .c++the compiler, -O2 -std=… and -static
Python.pypy_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 keyVariable
GCCAJ_Sandbox__Image__Gcc
ClangAJ_Sandbox__Image__Clang
CPythonAJ_Sandbox__Image__Python
PyPyAJ_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-toolchainone 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:65534where 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:65534the safe default, and the one a step that is not measured runs as
/usr/local/bin/aj-shimthe 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-shim

Pin 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.

On this page