Skip to content

Commit 196296e

Browse files
committed
Create zig runner
0 parents  commit 196296e

11 files changed

Lines changed: 1091 additions & 0 deletions

File tree

.dockerignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.git
2+
.gitignore
3+
.DS_Store
4+
zig-cache
5+
zig-out
6+
.zig-cache
7+
.zig-out
8+
*.tmp
9+
*.log
10+
*.swp
11+
*.swo
12+
node_modules
13+
dist
14+
target

.github/workflows/ci.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
9+
jobs:
10+
integration:
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: read
14+
packages: write
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v4
18+
19+
- name: Set up QEMU
20+
uses: docker/setup-qemu-action@v3
21+
22+
- name: Set up Docker Buildx
23+
uses: docker/setup-buildx-action@v3
24+
25+
- name: Log in to GHCR
26+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
27+
uses: docker/login-action@v3
28+
with:
29+
registry: ghcr.io
30+
username: ${{ github.actor }}
31+
password: ${{ secrets.GITHUB_TOKEN }}
32+
33+
- name: Build multi-arch container
34+
run: make container-build
35+
36+
- name: Run integration tests
37+
run: make test-integration CONTAINER=docker
38+
39+
- name: Push container
40+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
41+
run: make container-push

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.zig-cache/
2+
zig-out/
3+
.DS_Store
4+
*.tmp
5+
*.log

.tool-versions

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
zig 0.15.2

AGENT.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Code Execution HTTP Service
2+
3+
## Overview
4+
This service accepts source code and optional test/checker data over HTTP, executes the code in an isolated filesystem jail, and returns the execution result. It is designed to be fast, stateless, and container-friendly.
5+
6+
## HTTP API
7+
8+
### `POST /run`
9+
Execute user code.
10+
11+
Request JSON:
12+
- `timeout` (optional string): Execution limit, e.g. `"30s"`. Defaults to 30 seconds.
13+
- `solution_text` (string): Source code to run.
14+
- `lang_slug` (string): Language identifier. Supported values:
15+
- `clojure`, `cpp`, `csharp`, `dart`, `elixir`, `golang`, `haskell`, `java`, `js`, `kotlin`, `php`, `python`, `ruby`, `rust`, `swift`, `ts`, `zig`
16+
- `asserts` (optional string): JSON test data, stored as `asserts.json`.
17+
- `checker_text` (optional string): Checker code. Required for:
18+
- `cpp`, `csharp`, `dart`, `java`, `golang`, `haskell`, `kotlin`, `rust`, `swift`, `zig`
19+
20+
Response JSON:
21+
- `exit_code` (integer or null): Process exit code, if available.
22+
- `stdout` (string): Captured standard output.
23+
- `stderr` (string): Captured standard error.
24+
25+
### `GET /health`
26+
Returns HTTP 200 when the service is ready.
27+
28+
## Execution Flow
29+
1. Validate request parameters.
30+
2. Create a temporary filesystem jail using an overlay of the current root.
31+
3. Write input files into a language-specific working directory:
32+
- `solution_text` → language-specific filename (e.g. `solution.py`, `Solution.java`).
33+
- `checker_text` (if provided) → language-specific checker file.
34+
- `asserts` (if provided) → `asserts.json`.
35+
4. Execute `make test` inside the jail.
36+
5. Enforce timeout; on expiration, terminate the process group.
37+
6. Capture `stdout` and `stderr` and return them with the exit code.
38+
39+
## Files and Layout Expectations
40+
The service runs `make test` in its working directory. That directory is expected to contain language-specific runner logic. The service writes files into a subdirectory:
41+
- `check/` for most languages
42+
- `lib/` for Dart
43+
44+
## Language File Rules
45+
For each request, the service writes the solution and (if required) checker files into the language directory (`check/` or `lib/`). It then runs `make test`, which is responsible for compiling/executing the solution and checker for that language.
46+
47+
Filename mapping and checker requirement:
48+
- `clojure`: `solution.clj` (no checker)
49+
- `cpp`: `solution.cpp` + `checker.cpp` (checker required)
50+
- `csharp`: `Solution.cs` + `Checker.cs` (checker required)
51+
- `dart`: `solution.dart` + `checker.dart` (checker required)
52+
- `elixir`: `solution.exs` (no checker)
53+
- `golang`: `solution.go` + `checker.go` (checker required)
54+
- `haskell`: `Solution.hs` + `Checker.hs` (checker required)
55+
- `java`: `Solution.java` + `Checker.java` (checker required)
56+
- `js`: `solution.js` (no checker)
57+
- `kotlin`: `solution.kt` + `checker.kt` (checker required)
58+
- `php`: `solution.php` (no checker)
59+
- `python`: `solution.py` (no checker)
60+
- `ruby`: `solution.rb` (no checker)
61+
- `rust`: `solution.rs` + `checker.rs` (checker required)
62+
- `swift`: `solution.swift` + `checker.swift` (checker required)
63+
- `zig`: `solution.zig` + `checker.zig` (checker required)
64+
- `ts`: `solution.js` (no checker)
65+
66+
## Isolation and Safety
67+
- The execution occurs in a chrooted directory backed by an overlay filesystem.
68+
- On Linux, it creates new namespaces (filesystem, file descriptors, mount, network) before chrooting.
69+
- The service runs commands in a separate process group for reliable termination.
70+
71+
## Runtime Notes
72+
- If the process is PID 1 (container init), it spawns a child and acts as a signal reaper to avoid zombie processes.
73+
- The service is stateless; each request creates a fresh jail and cleans it up after execution.

Containerfile

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
ARG ZIG_VERSION=0.15.2
2+
ARG ALPINE_VERSION=3.23
3+
ARG ZIG_URL=
4+
ARG PORT=4040
5+
ARG RUN_CONCURRENCY=32
6+
ARG RUN_OUTPUT_MAX=1048576
7+
ARG DEBUG=
8+
ARG ALLOW_SHUTDOWN=
9+
10+
# Build stage
11+
FROM --platform=$BUILDPLATFORM alpine:${ALPINE_VERSION} AS builder
12+
WORKDIR /build
13+
14+
ARG ZIG_VERSION
15+
ARG ZIG_URL
16+
ARG TARGETPLATFORM
17+
ARG TARGETARCH
18+
19+
RUN apk add --no-cache curl tar xz make ca-certificates python3
20+
21+
RUN if [ -z "$TARGETARCH" ]; then \
22+
case "$(uname -m)" in \
23+
x86_64) TARGETARCH="amd64" ;; \
24+
aarch64) TARGETARCH="arm64" ;; \
25+
esac; \
26+
fi && \
27+
case "$TARGETARCH" in \
28+
amd64) ZIG_ARCH="x86_64" ;; \
29+
arm64) ZIG_ARCH="aarch64" ;; \
30+
*) echo "Unsupported TARGETARCH: $TARGETARCH" && exit 1 ;; \
31+
esac && \
32+
if [ -n "$ZIG_URL" ]; then \
33+
URL="$ZIG_URL"; \
34+
else \
35+
URL="$(ZIG_ARCH="$ZIG_ARCH" ZIG_VERSION="$ZIG_VERSION" python3 -c 'import json, os, urllib.request; ver=os.environ["ZIG_VERSION"]; arch=os.environ["ZIG_ARCH"]+"-linux"; data=json.load(urllib.request.urlopen("https://ziglang.org/download/index.json")); print(data[ver][arch]["tarball"])')"; \
36+
fi && \
37+
curl -fsSL "$URL" -o /tmp/zig.tar.xz && \
38+
ZIG_DIR="$(tar -tf /tmp/zig.tar.xz | head -n 1 | cut -d/ -f1)" && \
39+
tar -xJf /tmp/zig.tar.xz -C /opt && \
40+
mv "/opt/$ZIG_DIR" /opt/zig && \
41+
rm /tmp/zig.tar.xz
42+
43+
ENV PATH="/opt/zig:${PATH}"
44+
45+
COPY . .
46+
RUN zig build -Doptimize=ReleaseSafe
47+
48+
# Runtime stage
49+
FROM alpine:${ALPINE_VERSION}
50+
WORKDIR /app
51+
RUN adduser -S -u 10001 app
52+
COPY --from=builder /build/zig-out/bin/runner-zig /app/codebattle_runner
53+
ENV PORT=$PORT \
54+
RUN_CONCURRENCY=$RUN_CONCURRENCY \
55+
RUN_OUTPUT_MAX=$RUN_OUTPUT_MAX \
56+
DEBUG=$DEBUG \
57+
ALLOW_SHUTDOWN=$ALLOW_SHUTDOWN
58+
USER app
59+
60+
EXPOSE 4040
61+
ENTRYPOINT ["/app/codebattle_runner"]

Makefile

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
IMAGE ?= ghcr.io/hexlet-codebattle/runner-zig
2+
TAG ?= latest
3+
PLATFORMS ?= linux/amd64,linux/arm64
4+
CONTAINER ?= docker
5+
TEST_IMAGE ?= runner-zig-test
6+
7+
.PHONY: build lint lint-fix test test-container test-integration test-integration-arm test-integration-linux container-build container-push curl-local-health curl-local-test start
8+
9+
## Build multi-arch image directly for GHCR
10+
build:
11+
zig build
12+
13+
## Run Zig formatting check
14+
lint:
15+
zig fmt --check src
16+
17+
## Fix Zig code styling
18+
lint-fix:
19+
zig fmt src
20+
21+
## Run checker (expects check/checker.zig to exist)
22+
test:
23+
zig run check/checker.zig
24+
25+
## Run integration tests inside the builder container
26+
test-container:
27+
$(CONTAINER) build --target builder -t $(TEST_IMAGE) .
28+
$(CONTAINER) run --rm $(TEST_IMAGE) zig build test
29+
30+
## Run integration tests against an already-built container (container + HTTP checks)
31+
test-integration:
32+
@container_name="runner-zig-it-$$RANDOM"; \
33+
container_started=0; \
34+
trap 'if [ $$container_started -eq 1 ]; then $(CONTAINER) stop $$container_name >/dev/null 2>&1 || true; fi' EXIT; \
35+
$(CONTAINER) run -d --rm --pull=never --name $$container_name -p 4040:4040 $(IMAGE):$(TAG) >/dev/null && \
36+
container_started=1; \
37+
for i in 1 2 3 4 5; do \
38+
if $(MAKE) --no-print-directory curl-local-health >/dev/null; then break; fi; \
39+
sleep 1; \
40+
done; \
41+
$(MAKE) --no-print-directory curl-local-health && \
42+
seq 1 40 | xargs -n1 -P40 sh -c 'curl -sS -o /dev/null -w "%{http_code}\n" http://localhost:4040/run -H "content-type: application/json" -d @test-payload.json'; \
43+
if [ $$container_started -eq 1 ]; then $(CONTAINER) stop $$container_name >/dev/null 2>&1 || true; fi
44+
45+
## Build and push multi-arch image (linux/amd64 + linux/arm64) for GHCR
46+
build-and-push: container-build container-push
47+
48+
## Build multi-arch image (linux/amd64 + linux/arm64) for GHCR
49+
container-build:
50+
$(CONTAINER) build \
51+
--platform=linux/amd64 \
52+
--file Containerfile \
53+
--tag $(IMAGE):$(TAG)-amd64 \
54+
.
55+
$(CONTAINER) build \
56+
--platform=linux/arm64 \
57+
--file Containerfile \
58+
--tag $(IMAGE):$(TAG)-arm64 \
59+
.
60+
-@$(CONTAINER) rmi $(IMAGE):$(TAG) >/dev/null 2>&1 || true
61+
-@$(CONTAINER) manifest rm $(IMAGE):$(TAG) >/dev/null 2>&1 || true
62+
$(CONTAINER) manifest create $(IMAGE):$(TAG)
63+
$(CONTAINER) manifest add $(IMAGE):$(TAG) $(IMAGE):$(TAG)-amd64
64+
$(CONTAINER) manifest add $(IMAGE):$(TAG) $(IMAGE):$(TAG)-arm64
65+
66+
## Push multi-arch image manifest + all platform layers to GHCR
67+
container-push:
68+
$(CONTAINER) manifest push --all \
69+
$(IMAGE):$(TAG) \
70+
docker://$(IMAGE):$(TAG)
71+
72+
## Quick local smoke check against the running server
73+
curl-local-health:
74+
@curl -fsS http://localhost:4040/health && echo
75+
76+
## Run a local /run request for zig
77+
curl-local-test:
78+
@curl -sS http://localhost:4040/run \
79+
-H 'content-type: application/json' \
80+
-d @test-payload.json
81+
82+
## Start the server from the local build output
83+
start:
84+
./zig-out/bin/runner-zig

build.zig

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const std = @import("std");
2+
3+
pub fn build(b: *std.Build) void {
4+
const target = b.standardTargetOptions(.{});
5+
const optimize = b.standardOptimizeOption(.{});
6+
7+
const exe = b.addExecutable(.{
8+
.name = "runner-zig",
9+
.root_module = b.createModule(.{
10+
.root_source_file = b.path("src/main.zig"),
11+
.target = target,
12+
.optimize = optimize,
13+
}),
14+
});
15+
16+
b.installArtifact(exe);
17+
18+
const run_cmd = b.addRunArtifact(exe);
19+
if (b.args) |args| {
20+
run_cmd.addArgs(args);
21+
}
22+
23+
const run_step = b.step("run", "Run the HTTP service");
24+
run_step.dependOn(&run_cmd.step);
25+
26+
const tests = b.addTest(.{
27+
.root_module = b.createModule(.{
28+
.root_source_file = b.path("src/integration_test.zig"),
29+
.target = target,
30+
.optimize = optimize,
31+
}),
32+
});
33+
const run_tests = b.addRunArtifact(tests);
34+
const test_step = b.step("test", "Run integration tests");
35+
test_step.dependOn(&run_tests.step);
36+
}

0 commit comments

Comments
 (0)