From 6c4dfa37f42e9124172a2ea5c8e9cdb4ecac7373 Mon Sep 17 00:00:00 2001 From: Borys Stelmakh Date: Fri, 1 May 2026 05:12:04 +0400 Subject: [PATCH] Add CI workflow and configuration for SampSharp Docker image --- .github/workflows/build-and-publish.yml | 66 +++++++++++++ 6.0.3/example/Dockerfile | 26 ++++++ 6.0.3/example/README.md | 17 ++++ .../example}/filterscripts/empty.amx | Bin .../example}/gamemodes/empty.amx | Bin {example => 6.0.3/example}/pawn.json | 0 {example => 6.0.3/example}/src/Example.csproj | 0 {example => 6.0.3/example}/src/Example.sln | 0 {example => 6.0.3/example}/src/Program.cs | 0 Dockerfile | 70 ++++++++++++++ README.md | 77 ++++++++++++++- example/Dockerfile | 88 ++++++++++++++---- example/README.md | 45 +++++++-- example/config.json | 30 ++++++ 14 files changed, 389 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/build-and-publish.yml create mode 100644 6.0.3/example/Dockerfile create mode 100644 6.0.3/example/README.md rename {example => 6.0.3/example}/filterscripts/empty.amx (100%) rename {example => 6.0.3/example}/gamemodes/empty.amx (100%) rename {example => 6.0.3/example}/pawn.json (100%) rename {example => 6.0.3/example}/src/Example.csproj (100%) rename {example => 6.0.3/example}/src/Example.sln (100%) rename {example => 6.0.3/example}/src/Program.cs (100%) create mode 100644 Dockerfile create mode 100644 example/config.json diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml new file mode 100644 index 0000000..a5b5887 --- /dev/null +++ b/.github/workflows/build-and-publish.yml @@ -0,0 +1,66 @@ +name: Build & Publish + +on: + push: + branches: ['main', 'master'] + tags: ['v*'] + pull_request: + branches: ['main', 'master'] + schedule: + - cron: '0 6 * * 1' + workflow_dispatch: + inputs: + sampsharp_ref: + description: 'SampSharp ref (commit/branch/tag)' + default: 'master' + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + name: Build & publish base image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Compute image tags + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,prefix=sha-,format=short + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build & push + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + SAMPSHARP_REF=${{ github.event.inputs.sampsharp_ref || 'master' }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64 diff --git a/6.0.3/example/Dockerfile b/6.0.3/example/Dockerfile new file mode 100644 index 0000000..2b87a48 --- /dev/null +++ b/6.0.3/example/Dockerfile @@ -0,0 +1,26 @@ +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env +WORKDIR /app + +# Copy csproj and restore as distinct layers +COPY src/*.csproj ./ +RUN dotnet restore + +# Copy everything else and build +COPY src/* ./ +RUN dotnet publish -c Release -o out + +# Build runtime image +FROM ghcr.io/sampsharp/sampsharp-docker:6.0.3 +WORKDIR /samp +COPY --from=build-env /app/out ./gamemode + +# Install SA-MP server with pawnctl +COPY pawn.json . +RUN sampctl package ensure + +# Because these gamemodes were not created by pawnctl, we need to copy them after sampctl has initialized. In a future version of SampSharp +# the empty gamemode and filterscript will automatically be written to the corresponding directory if they are missing. +COPY gamemodes ./gamemodes +COPY filterscripts ./filterscripts + +ENTRYPOINT ["sampctl", "package", "run"] \ No newline at end of file diff --git a/6.0.3/example/README.md b/6.0.3/example/README.md new file mode 100644 index 0000000..4b86d9b --- /dev/null +++ b/6.0.3/example/README.md @@ -0,0 +1,17 @@ +Docker example +============== + +### Building +``` +docker build . -t testserver +``` + +### Running + +``` +docker run --rm -it -p 7777:7777/udp testserver +``` + +### Quircks +- SampSharp's default filterscripts and gamemode are not installed with the plugin through sampctl, so they're included in the image manually for now. In the future, the SampSharp plugin will automatically write these modes into the corresponding directories if they could not be found. +- SampSharp performs a check on the configured pawn gamemodes by default. It checks for a line containing `gamemode0 empty 1`, but sampctl generates the line `gamemode0 empty` (without the 1). Normally SampSharp wouldn't start in this situation. We work around this for now with the `skip_empty_check true` option. \ No newline at end of file diff --git a/example/filterscripts/empty.amx b/6.0.3/example/filterscripts/empty.amx similarity index 100% rename from example/filterscripts/empty.amx rename to 6.0.3/example/filterscripts/empty.amx diff --git a/example/gamemodes/empty.amx b/6.0.3/example/gamemodes/empty.amx similarity index 100% rename from example/gamemodes/empty.amx rename to 6.0.3/example/gamemodes/empty.amx diff --git a/example/pawn.json b/6.0.3/example/pawn.json similarity index 100% rename from example/pawn.json rename to 6.0.3/example/pawn.json diff --git a/example/src/Example.csproj b/6.0.3/example/src/Example.csproj similarity index 100% rename from example/src/Example.csproj rename to 6.0.3/example/src/Example.csproj diff --git a/example/src/Example.sln b/6.0.3/example/src/Example.sln similarity index 100% rename from example/src/Example.sln rename to 6.0.3/example/src/Example.sln diff --git a/example/src/Program.cs b/6.0.3/example/src/Program.cs similarity index 100% rename from example/src/Program.cs rename to 6.0.3/example/src/Program.cs diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..72303b5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,70 @@ +# syntax=docker/dockerfile:1.7 +# +# sampsharp-docker — slim base image: SampSharp native host + .NET runtime. +# +# Intentionally does NOT include open.mp / omp-server: SampSharp and open.mp +# have independent release cadences, gluing them into one layer is awkward. +# The consumer brings omp-server from a separate source (built from sources, +# release artefact, or its own image) — see example/. +# +# Contents: +# /server/components/SampSharp.so — native host for .NET gamemodes +# .NET 10 runtime — for executing managed gamemodes +# tini — clean SIGTERM handling +# +# Build args: +# SAMPSHARP_REPO / SAMPSHARP_REF — git source of SampSharp +# DOTNET_VERSION — major .NET runtime for the final stage + +ARG DOTNET_VERSION=10.0 +ARG SAMPSHARP_REPO=https://github.com/ikkentim/SampSharp.git +ARG SAMPSHARP_REF=master + +# ============================================================================= +# Stage 1: SampSharp.so (sampsharp-component) +# ============================================================================= +FROM debian:bookworm-slim AS sampsharp-builder + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential cmake ninja-build git ca-certificates pkg-config \ + && rm -rf /var/lib/apt/lists/* + +ARG SAMPSHARP_REPO +ARG SAMPSHARP_REF +WORKDIR /build +# Full clone (not --depth=1) — otherwise checkout of an arbitrary SHA fails. +RUN git clone --recursive "$SAMPSHARP_REPO" SampSharp \ + && git -C SampSharp checkout "$SAMPSHARP_REF" \ + && git -C SampSharp submodule update --init --recursive + +RUN --mount=type=cache,target=/build/SampSharp-build \ + set -e; \ + rm -f /build/SampSharp-build/CMakeCache.txt; \ + rm -rf /build/SampSharp-build/CMakeFiles; \ + cmake -B /build/SampSharp-build -S SampSharp/src/sampsharp-component -G Ninja \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo; \ + cmake --build /build/SampSharp-build --config RelWithDebInfo; \ + mkdir -p /artifacts; \ + cp /build/SampSharp-build/artifacts/SampSharp.so /artifacts/ + +# ============================================================================= +# Stage 2: runtime (.NET 10 + SampSharp.so) +# ============================================================================= +FROM mcr.microsoft.com/dotnet/runtime:${DOTNET_VERSION} AS runtime +WORKDIR /server + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libssl3 libstdc++6 ca-certificates tini \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=sampsharp-builder /artifacts/SampSharp.so /server/components/SampSharp.so + +# Directory skeleton - open.mp expects these to exist at startup. +RUN mkdir -p /server/components /server/plugins /server/scriptfiles \ + /server/gamemodes /server/filterscripts /server/include + +# Note: omp-server is not included — the consumer ships its own with the +# gamemode image. tini wraps the entry point; the actual CMD is up to the +# consumer. +ENTRYPOINT ["/usr/bin/tini", "--"] +CMD ["./omp-server"] diff --git a/README.md b/README.md index 8d8d531..484af0b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,79 @@ sampsharp-docker ================ -See `example/` for how to use this image. \ No newline at end of file +Slim base image for SampSharp gamemodes running on open.mp x64. + +Contents: + +* `/server/components/SampSharp.so` — native host that loads the .NET runtime + and your gamemode assembly inside open.mp's component system. +* `.NET 10 runtime` — for executing managed gamemodes. +* `tini` as the entry-point wrapper for clean signal handling. + +What's intentionally **not** included: + +* `omp-server` and the open.mp core components (`Actors.so`, `Vehicles.so`, …). + The image isn't tied to a specific open.mp version — bring your own + `omp-server` build (from source, a release artefact, or a separate image) + in the consumer Dockerfile. See `example/` for a working pattern. + +Usage +----- + +```Dockerfile +FROM ghcr.io//sampsharp-docker:latest + +# 1) Provide omp-server + open.mp core components (your build/release). +COPY --from= /omp-server /server/omp-server +COPY --from= /components/ /server/components/ + +# 2) Drop your published .NET gamemode in. +COPY ./gamemode-publish/ /server/gamemode/MyMode/ + +# 3) open.mp config that points at your gamemode. +COPY ./config.json /server/config.json +``` + +The base image sets `WORKDIR=/server`, `ENTRYPOINT=tini`, `CMD=./omp-server`, +and `EXPOSE 7777/udp` — override any of those if your scenario needs it. + +Building locally +---------------- + +```sh +docker build -t sampsharp-docker:dev . +``` + +To pin a specific SampSharp commit / tag instead of `master`: + +```sh +docker build \ + --build-arg SAMPSHARP_REF=v0.11.0 \ + -t sampsharp-docker:0.11.0 . +``` + +Tags published by CI +-------------------- + +* `:latest` — `master` of SampSharp, rebuilt weekly. +* `:sha-` — every push to the default branch. +* `:` — release tags pushed to this repo. + +CI is defined in `.github/workflows/build-and-publish.yml`. The default branch +push rebuilds and republishes; `workflow_dispatch` can override +`SAMPSHARP_REF` to build against an arbitrary upstream ref on demand. + +Example +------- + +See [`example/`](./example) for a complete consumer Dockerfile that builds a +trivial SampSharp gamemode against this base image, pulls in `omp-server` from +source, and runs the result. + +Legacy (SampSharp 6.0.3 + classic SA-MP) +---------------------------------------- + +The previous incarnation of this image, targeting SampSharp 6.0.3 on classic +SA-MP via `sampctl`, is preserved under [`6.0.3/`](./6.0.3) along with its +matching example in [`6.0.3/example/`](./6.0.3/example). It's no longer built +by CI but kept for reference and for users still on the classic stack. diff --git a/example/Dockerfile b/example/Dockerfile index 2b87a48..69f40f9 100644 --- a/example/Dockerfile +++ b/example/Dockerfile @@ -1,26 +1,74 @@ -FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env -WORKDIR /app +# syntax=docker/dockerfile:1.7 +# +# Example consumer of sampsharp-docker. +# +# Stage 1: builds open.mp x64 from upstream sources to provide omp-server + +# core components (Actors.so, Vehicles.so, etc.). In a real deployment +# you'd typically replace this with `COPY --from=`. +# Stage 2: placeholder for your .NET gamemode build (csproj + publish dir). +# Stage 3: assembly - pulls SampSharp.so from the base image, omp-server from +# stage 1, your gamemode from stage 2, plus the config.json next to +# this Dockerfile. -# Copy csproj and restore as distinct layers -COPY src/*.csproj ./ -RUN dotnet restore +# ============================================================================= +# Stage 1: open.mp build (replace with your own image / release artefact in prod) +# ============================================================================= +FROM debian:bookworm-slim AS openmp-builder +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential cmake ninja-build git ca-certificates pkg-config \ + clang lld libssl-dev python3 python3-pip \ + && pip install --break-system-packages 'conan<2' \ + && rm -rf /var/lib/apt/lists/* -# Copy everything else and build -COPY src/* ./ -RUN dotnet publish -c Release -o out +ARG OPENMP_REPO=https://github.com/openmultiplayer/open.mp.git +ARG OPENMP_REF=master +WORKDIR /build +RUN git clone --recursive "$OPENMP_REPO" open.mp \ + && git -C open.mp checkout "$OPENMP_REF" \ + && git -C open.mp submodule update --init --recursive -# Build runtime image -FROM ghcr.io/sampsharp/sampsharp-docker:6.0.3 -WORKDIR /samp -COPY --from=build-env /app/out ./gamemode +RUN --mount=type=cache,target=/root/.conan \ + --mount=type=cache,target=/build/openmp-build \ + set -e; \ + rm -f /build/openmp-build/CMakeCache.txt; \ + rm -rf /build/openmp-build/CMakeFiles; \ + cmake -B /build/openmp-build -S open.mp -G Ninja \ + -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DTARGET_BUILD_ARCH=x86_64 -DSHARED_OPENSSL=TRUE; \ + cmake --build /build/openmp-build --config RelWithDebInfo; \ + mkdir -p /artifacts/components; \ + find /build/openmp-build -maxdepth 6 -name omp-server -executable -type f -exec cp {} /artifacts/ \; ; \ + find /build/openmp-build -maxdepth 6 -name '*.so' -path '*components*' -exec cp {} /artifacts/components/ \; ; \ + test -x /artifacts/omp-server -# Install SA-MP server with pawnctl -COPY pawn.json . -RUN sampctl package ensure +# ============================================================================= +# Stage 2: your gamemode (placeholder — replace with the real build) +# ============================================================================= +# Real example would look something like: +# +# FROM mcr.microsoft.com/dotnet/sdk:10.0 AS gamemode-builder +# WORKDIR /src +# COPY MyMode.csproj ./ +# RUN dotnet restore +# COPY . . +# RUN dotnet publish MyMode.csproj -c Release -o /publish +# +# For this example we just stage an empty directory. +FROM busybox:stable AS gamemode-builder +RUN mkdir -p /publish -# Because these gamemodes were not created by pawnctl, we need to copy them after sampctl has initialized. In a future version of SampSharp -# the empty gamemode and filterscript will automatically be written to the corresponding directory if they are missing. -COPY gamemodes ./gamemodes -COPY filterscripts ./filterscripts +# ============================================================================= +# Stage 3: final image +# ============================================================================= +FROM ghcr.io//sampsharp-docker:latest +# (Replace with the actual repository owner once the image is published.) -ENTRYPOINT ["sampctl", "package", "run"] \ No newline at end of file +COPY --from=openmp-builder /artifacts/omp-server /server/omp-server +COPY --from=openmp-builder /artifacts/components/ /server/components/ + +COPY --from=gamemode-builder /publish /server/gamemode/MyMode/ + +COPY ./config.json /server/config.json + +# WORKDIR, ENTRYPOINT, CMD, EXPOSE are inherited from the base image. diff --git a/example/README.md b/example/README.md index 4b86d9b..f24a9af 100644 --- a/example/README.md +++ b/example/README.md @@ -1,17 +1,44 @@ Docker example ============== -### Building -``` -docker build . -t testserver -``` +A consumer Dockerfile that demonstrates how to build a SampSharp/open.mp x64 +gamemode image on top of `sampsharp-docker`. The Dockerfile is split into +three stages: -### Running +1. **`openmp-builder`** — clones and builds open.mp from upstream sources, + producing `omp-server` and the core `components/*.so`. In production you'd + typically replace this stage with a `COPY --from=` to avoid + rebuilding the server on every gamemode commit. +2. **`gamemode-builder`** — placeholder stage. Replace it with a real `dotnet + publish` of your `MyMode.csproj`. +3. **Final image** — pulls `SampSharp.so` and the .NET runtime from the + `sampsharp-docker` base, plus omp-server, the gamemode publish output, and + `config.json`. +Building +-------- + +```sh +docker build -t sampsharp-example . ``` -docker run --rm -it -p 7777:7777/udp testserver + +The first build compiles open.mp, which takes ~10 minutes on a clean cache. +Subsequent rebuilds reuse the cached open.mp build unless its source ref +changes. + +Running +------- + +```sh +docker run --rm -it -p 7777:7777/udp sampsharp-example ``` -### Quircks -- SampSharp's default filterscripts and gamemode are not installed with the plugin through sampctl, so they're included in the image manually for now. In the future, the SampSharp plugin will automatically write these modes into the corresponding directories if they could not be found. -- SampSharp performs a check on the configured pawn gamemodes by default. It checks for a line containing `gamemode0 empty 1`, but sampctl generates the line `gamemode0 empty` (without the 1). Normally SampSharp wouldn't start in this situation. We work around this for now with the `skip_empty_check true` option. \ No newline at end of file +Notes +----- + +- The placeholder gamemode stage produces an empty `gamemode/MyMode/`. With + no actual managed assembly the server will start, fail to load the gamemode + and exit. Wire up your own `dotnet publish` step before expecting it to + serve. +- Replace `` in the final-stage `FROM` directive with the GHCR namespace + the base image is published under. diff --git a/example/config.json b/example/config.json new file mode 100644 index 0000000..f5c3494 --- /dev/null +++ b/example/config.json @@ -0,0 +1,30 @@ +{ + "name": "SampSharp Example", + "max_players": 50, + "language": "en", + "announce": false, + "gamemode": "MyMode", + "sampsharp": { + "directory": "gamemode/MyMode", + "assembly": "MyMode" + }, + "pawn": { + "main_scripts": ["empty 0"], + "side_scripts": [], + "legacy_plugins": [] + }, + "network": { + "port": 7777, + "public_addr": "", + "bind": "" + }, + "rcon": { + "enable": false, + "password": "" + }, + "logging": { + "enable": true, + "file": "log.txt", + "use_timestamp": true + } +}