Compare commits

11 Commits
dev ... main

Author SHA1 Message Date
63d79740ed fix: add --amend flag to docker manifest create
All checks were successful
Build SS14 Watchdog Server / build-arm64 (push) Successful in 1m1s
Build SS14 Watchdog Server / build-amd64 (push) Successful in 49s
Build SS14 Watchdog Server / create-manifest (push) Successful in 7s
The manifest already exists from previous builds, so --amend is needed
to update it with new architecture images.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 17:53:21 +01:00
Codex Agent
0651098b66 fix: disable provenance to fix multi-arch manifest creation
Some checks failed
Build SS14 Watchdog Server / build-amd64 (push) Failing after 32s
Build SS14 Watchdog Server / build-arm64 (push) Successful in 50s
Build SS14 Watchdog Server / create-manifest (push) Has been skipped
Buildx with provenance creates manifest lists even for single-platform
builds. This breaks `docker manifest create` which expects plain images.

Adding provenance: false ensures plain images are pushed, allowing
the manifest creation step to work correctly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 09:16:10 +01:00
Codex Agent
d21b1e9877 ci: Add multi-arch manifest with :main tag
Some checks failed
Build SS14 Watchdog Server / build-amd64 (push) Successful in 2m47s
Build SS14 Watchdog Server / build-arm64 (push) Successful in 32m28s
Build SS14 Watchdog Server / create-manifest (push) Failing after 6s
- arm64 build pushes :main-arm64
- amd64 build pushes :main-amd64
- New create-manifest job combines into :main (auto-selects arch)
- Manifest job can run on any self-hosted runner

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 08:37:37 +01:00
Codex Agent
b9bbe80856 ci: Use :latest tag instead of :main
Some checks failed
Build SS14 Watchdog Server / build-amd64 (push) Successful in 53s
Build SS14 Watchdog Server / build-arm64 (push) Has been cancelled
- amd64: :latest and :sha-xxx
- arm64: :latest-arm64 and :sha-xxx-arm64

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 08:30:17 +01:00
Codex Agent
906d405e43 Refactor to LiamAEdwards watchdog approach
Some checks failed
Build SS14 Watchdog Server / build-amd64 (push) Successful in 3m31s
Build SS14 Watchdog Server / build-arm64 (push) Has been cancelled
- Dockerfile: Download server from wylab CDN at build time, build watchdog
- start.sh: Simplified to copy defaults and run watchdog
- appsettings.yml: Configure for wylab instance with CDN manifest
- main.yml: Simplified workflow with auto-tagging via metadata-action
- Delete update_build_metadata.py (no longer needed)

Based on: https://github.com/LiamAEdwards/SS14-Docker-Linux-Server

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 08:21:19 +01:00
f74b25bc3c chore: Remove update_build_metadata.py (watchdog handles updates)
All checks were successful
Build SS14 Watchdog Server / build-amd64 (push) Successful in 1m39s
Build SS14 Watchdog Server / build-arm64 (push) Successful in 9m7s
2025-12-16 07:44:09 +01:00
1d20c8c80d chore: Remove start.sh (watchdog is now the entrypoint)
Some checks failed
Build SS14 Watchdog Server / build-arm64 (push) Has been cancelled
Build SS14 Watchdog Server / build-amd64 (push) Has been cancelled
2025-12-16 07:44:09 +01:00
1916037ff8 ci: Simplify workflow for watchdog build (no game source compilation)
Some checks failed
Build SS14 Watchdog Server / build-amd64 (push) Has been cancelled
Build SS14 Watchdog Server / build-arm64 (push) Has been cancelled
Main branch now builds watchdog-only image that auto-updates from CDN.
Dev branch retains the full game source build workflow.
2025-12-16 07:43:44 +01:00
129c4bebd1 config: Update watchdog key to wylab, clean up build section
Some checks failed
Build and publish wylab SS14 server / build-arm64 (push) Has been cancelled
Build and publish wylab SS14 server / build-amd64 (push) Has been cancelled
2025-12-16 07:42:52 +01:00
050061a7aa config: Update watchdog instance to wylab with CDN manifest
Some checks failed
Build and publish wylab SS14 server / build-arm64 (push) Has been cancelled
Build and publish wylab SS14 server / build-amd64 (push) Has been cancelled
2025-12-16 07:42:51 +01:00
aaade96257 refactor: Switch to SS14.Watchdog for auto-updates from CDN
Some checks failed
Build and publish wylab SS14 server / build-amd64 (push) Has been cancelled
Build and publish wylab SS14 server / build-arm64 (push) Has been cancelled
- Watchdog automatically downloads and updates game server from CDN manifest
- No need to rebuild image for game updates
- Game server binaries pulled from https://cdn.wylab.me/fork/wylab/manifest
- Dev branch retains the build-from-source approach
2025-12-16 07:41:49 +01:00
6 changed files with 80 additions and 397 deletions

View File

@@ -1,138 +1,93 @@
#
name: Build and publish wylab SS14 server
name: Build SS14 Watchdog Server
# Build whenever the Docker wrapper changes.
on:
push:
branches: ['main']
repository_dispatch:
types: ['ss14-package-ready']
workflow_dispatch:
# Workflow-wide defaults. Adjust IMAGE_NAME/REGISTRY if you are publishing elsewhere.
env:
REGISTRY: git.wylab.me
IMAGE_NAME: wylab/ws14-docker-linux-server
WYLAB_SOURCE_REPO: https://git.wylab.me/wylab/wylab-station-14.git
WYLAB_SOURCE_REF: master
BUILDKIT_PROGRESS: plain
jobs:
build-arm64:
# Requires a self-hosted macOS arm64 runner
runs-on: [self-hosted, macos-arm64]
timeout-minutes: 240
timeout-minutes: 60
permissions:
contents: read
packages: write
env:
TARGET_PLATFORM: linux-arm64
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Resolve wylab commit
id: wylab
env:
PAYLOAD_COMMIT: ${{ github.event.client_payload.commit }}
run: |
set -euo pipefail
if [ -n "${PAYLOAD_COMMIT}" ]; then
COMMIT="${PAYLOAD_COMMIT}"
else
REF="${WYLAB_SOURCE_REF}"
COMMIT=$(git ls-remote "${WYLAB_SOURCE_REPO}" "${REF}" | head -n1 | cut -f1)
fi
if [ -z "${COMMIT}" ]; then
echo "Unable to resolve commit to build." >&2
exit 1
fi
echo "commit=${COMMIT}" >> "$GITHUB_OUTPUT"
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME || github.actor }}
password: ${{ secrets.REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}
- name: Build and push Docker image (arm64)
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
uses: docker/build-push-action@v5
with:
context: .
provenance: false
platforms: linux/arm64
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-arm64
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-arm64,mode=max
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:arm64
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}-arm64
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.wylab.outputs.commit }}-arm64
build-args: |
SOURCE_REPO=${{ env.WYLAB_SOURCE_REPO }}
SOURCE_REF=${{ steps.wylab.outputs.commit }}
TARGET_PLATFORM=${{ env.TARGET_PLATFORM }}
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main-arm64
build-amd64:
# Requires a self-hosted Linux amd64 runner
runs-on: [self-hosted, linux-amd64]
timeout-minutes: 240
timeout-minutes: 60
permissions:
contents: read
packages: write
env:
TARGET_PLATFORM: linux-x64
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Resolve wylab commit
id: wylab
env:
PAYLOAD_COMMIT: ${{ github.event.client_payload.commit }}
run: |
set -euo pipefail
if [ -n "${PAYLOAD_COMMIT}" ]; then
COMMIT="${PAYLOAD_COMMIT}"
else
REF="${WYLAB_SOURCE_REF}"
COMMIT=$(git ls-remote "${WYLAB_SOURCE_REPO}" "${REF}" | head -n1 | cut -f1)
fi
if [ -z "${COMMIT}" ]; then
echo "Unable to resolve commit to build." >&2
exit 1
fi
echo "commit=${COMMIT}" >> "$GITHUB_OUTPUT"
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME || github.actor }}
password: ${{ secrets.REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}
- name: Build and push Docker image (amd64)
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
uses: docker/build-push-action@v5
with:
context: .
provenance: false
platforms: linux/amd64
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-amd64
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-amd64,mode=max
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.wylab.outputs.commit }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:amd64
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}-amd64
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.wylab.outputs.commit }}-amd64
build-args: |
SOURCE_REPO=${{ env.WYLAB_SOURCE_REPO }}
SOURCE_REF=${{ steps.wylab.outputs.commit }}
TARGET_PLATFORM=${{ env.TARGET_PLATFORM }}
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main-amd64
create-manifest:
needs: [build-amd64, build-arm64]
runs-on: [self-hosted]
steps:
- name: Log in to the container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME || github.actor }}
password: ${{ secrets.REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}
- name: Create and push multi-arch manifest
run: |
docker manifest create --amend ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main-amd64 \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main-arm64
docker manifest push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main

View File

@@ -1,61 +1,50 @@
#
# syntax=docker/dockerfile:1.7
#
# SS14 Watchdog Docker Image (based on LiamAEdwards/SS14-Docker-Linux-Server)
# Downloads game server from CDN and builds SS14.Watchdog
#
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG SOURCE_REPO=https://git.wylab.me/wylab/wylab-station-14.git
ARG SOURCE_REF=master
ARG TARGETPLATFORM
ARG TARGET_PLATFORM=auto
# Install dependencies needed to build and package the server
RUN --mount=type=cache,target=/var/cache/apt \
--mount=type=cache,target=/var/lib/apt/lists \
apt-get update && \
apt-get install -y --no-install-recommends git python3 unzip
# Update and install necessary tools
RUN apt-get -y update && \
apt-get -y install curl unzip wget git jq
WORKDIR /src
# Clone the wylab-station-14 source
RUN git clone "${SOURCE_REPO}" content
WORKDIR /src/content
RUN git checkout "${SOURCE_REF}"
# Initialize submodules / engine checkout
RUN python3 RUN_THIS.py --quiet
# Build and package the server for the requested platform
RUN --mount=type=cache,target=/root/.nuget/packages \
if [ "${TARGET_PLATFORM}" = "auto" ]; then \
case "${TARGETPLATFORM:-linux/amd64}" in \
"linux/amd64") SERVER_RID="linux-x64" ;; \
"linux/arm64") SERVER_RID="linux-arm64" ;; \
"linux/arm/v7"|"linux/arm/v6") SERVER_RID="linux-arm" ;; \
*) echo "Unsupported TARGETPLATFORM '${TARGETPLATFORM}'. Set TARGET_PLATFORM explicitly."; exit 1 ;; \
esac; \
# Determine platform RID based on Docker TARGETPLATFORM
RUN if [ "${TARGETPLATFORM:-linux/amd64}" = "linux/arm64" ]; then \
echo "linux-arm64" > /tmp/platform_rid; \
else \
SERVER_RID="${TARGET_PLATFORM}"; \
echo "linux-x64" > /tmp/platform_rid; \
fi && \
echo "Building server runtime for ${SERVER_RID} (docker TARGETPLATFORM=${TARGETPLATFORM:-unknown})" && \
dotnet run --project Content.Packaging/Content.Packaging.csproj server \
--platform "${SERVER_RID}" \
--configuration Release
echo "Building for platform: $(cat /tmp/platform_rid)"
# Extract packaged build into the filesystem layout the runtime stage expects
RUN mkdir -p /ss14-default && \
unzip "release/SS14.Server_${TARGET_PLATFORM}.zip" -d /ss14-default/
# Download and extract SS14 server from WYLAB CDN
RUN PLATFORM_RID=$(cat /tmp/platform_rid) && \
SERVER_URL=$(curl -sL https://cdn.wylab.me/fork/wylab/manifest | \
jq -r ".builds | to_entries | sort_by(.value.time) | last | .value.server.\"${PLATFORM_RID}\".url") && \
echo "Downloading server from: $SERVER_URL" && \
wget -O SS14.Server.zip "$SERVER_URL" && \
unzip SS14.Server.zip -d /ss14-default/
# Download and build Watchdog
RUN PLATFORM_RID=$(cat /tmp/platform_rid) && \
wget https://github.com/space-wizards/SS14.Watchdog/archive/refs/heads/master.zip -O Watchdog.zip && \
unzip Watchdog.zip -d Watchdog && \
cd Watchdog/SS14.Watchdog-master && \
dotnet publish SS14.Watchdog -c Release -r "${PLATFORM_RID}" --no-self-contained -o /ss14-default/publish
# Server stage
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS server
RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates python3 && \
rm -rf /var/lib/apt/lists/*
# Copy from the build stage
COPY --from=build /ss14-default /ss14-default
COPY update_build_metadata.py /update_build_metadata.py
# Install necessary tools
RUN apt-get -y update && apt-get -y install unzip && \
rm -rf /var/lib/apt/lists/*
# Expose necessary ports
EXPOSE 1212/tcp
@@ -65,13 +54,12 @@ EXPOSE 8080/tcp
# Set volume
VOLUME [ "/ss14" ]
# Add configurations (files go to root, not publish/)
ADD appsettings.yml /ss14-default/appsettings.yml
ADD server_config.toml /ss14-default/server_config.toml
# Add configurations
COPY appsettings.yml /ss14-default/publish/appsettings.yml
COPY server_config.toml /ss14-default/publish/server_config.toml
COPY start.sh /start.sh
RUN python3 /update_build_metadata.py /ss14-default/server_config.toml || true && \
chmod +x /start.sh
RUN chmod +x /start.sh
# Set the entry point for the container
ENTRYPOINT ["/start.sh"]

View File

@@ -25,35 +25,25 @@ Serilog:
AllowedHosts: "*"
# Force Kestrel to bind to 0.0.0.0 (IPv4) instead of [::] (IPv6)
# This fixes the Host header issue with http://[::]:8080
Urls: "http://0.0.0.0:8080"
# API URL your watchdog is accessible from.
# This NEEDS to be reachable by the game server.
# If you don't want the watchdog to be public,
# do `http://localhost:8080/` here.
#BaseUrl: https://your.domain.com/watchdog/
BaseUrl: http://localhost:8080/
Servers:
Instances:
# ID (and directory) of your server.
test:
# Name of the server
Name: "Test Instance"
# Token to control the instance remotely
wylab:
Name: "WyLab Station 14"
ApiToken: "foobar"
# Port OF THE GAME SERVER.
# This should match the HTTP status API
# or watchdog can't contact the server.
ApiPort: 1212
TimeoutSeconds: 120
# Override the baseUrl to use localhost instead of [::]
# Override the baseUrl to use localhost
EnvironmentVariables:
ROBUST_CVAR_watchdog__baseUrl: "http://localhost:8080/"
# Auto update configuration. This can be
# omitted to skip auto updates.
# Auto update from WyLab CDN
UpdateType: "Manifest"
Updates:
ManifestUrl: "https://central.spacestation14.io/builds/wizards/manifest.json"
ManifestUrl: "https://cdn.wylab.me/fork/wylab/manifest"

View File

@@ -34,8 +34,8 @@ bind = "*:1212"
[watchdog]
# Token must match ApiToken in appsettings.yml
token = "foobar"
# Key must match the instance key in appsettings.yml (e.g., "test")
key = "test"
# Key must match the instance key in appsettings.yml
key = "wylab"
# BaseUrl must match BaseUrl in appsettings.yml
baseUrl = "http://localhost:8080/"
@@ -74,25 +74,8 @@ server_url = ""
hub_urls = "https://central.spacestation14.io/hub/"
[build]
# *Absolutely all of these can be supplied using a "build.json" file*
# For further information, see https://github.com/space-wizards/space-station-14/blob/master/Tools/gen_build_info.py
# The main reason you'd want to supply any of these manually is for a custom fork and if you have no tools.
# Useful to override if the existing version is bad.
# See https://github.com/space-wizards/RobustToolbox/tags for version values, remove the 'v'.
# The value listed here is almost certainly wrong - it is ONLY a demonstration of format.
# engine_version = "0.7.6"
# This one is optional, the launcher will delete other ZIPs of the same fork to save space.
# fork_id = "abacusstation"
# Automatically set if self-hosting client zip, but otherwise use this when updating client build.
# There is no required format, any change counts as a new version.
# version = "Example1"
# download_url and build are auto-populated by update_build_metadata.py from CDN manifest
download_url = "https://cdn.wylab.me/fork/wylab/version/bce50cad4cebdc8443947911be72c8c0ffbad713/file/SS14.Client.zip"
build = "D51F7777997E329D9B5A8B307AB8F49FF0A699D1B52EC7B7505C3DBBFF92E103"
# Watchdog automatically provides build info from CDN manifest
# These are only used as fallback if watchdog doesn't provide them
[database]
# Database engine: sqlite or postgres

View File

@@ -1,36 +1,10 @@
#!/bin/bash
# Check if directory is empty
# Copy default files to volume on first run
if [ ! "$(ls -A /ss14)" ]; then
# Copy the default files
cp -R /ss14-default/* /ss14/
cp -r /ss14-default/* /ss14/
fi
# Locate the watchdog binary if it exists (older builds)
WATCHDOG_PATH=$(find /ss14 -maxdepth 4 -type f -name "SS14.Watchdog" | head -n 1)
if [ -n "$WATCHDOG_PATH" ]; then
WATCHDOG_DIR=$(dirname "$WATCHDOG_PATH")
cd "$WATCHDOG_DIR" || exit 1
chmod +x "$WATCHDOG_PATH"
exec "$WATCHDOG_PATH" "$@"
fi
# Fallback for new builds where only Robust.Server is shipped
SERVER_PATH=$(find /ss14 -maxdepth 4 -type f -name "Robust.Server" | head -n 1)
if [ -z "$SERVER_PATH" ]; then
echo "No SS14.Watchdog or Robust.Server executable found under /ss14" >&2
exit 1
fi
SERVER_DIR=$(dirname "$SERVER_PATH")
cd "$SERVER_DIR" || exit 1
# Auto-sync build.download_url and build hash from Robust.Cdn manifest.
CONFIG_PATH="$SERVER_DIR/server_config.toml"
if command -v python3 >/dev/null && [ -f "$CONFIG_PATH" ] && [ -f /update_build_metadata.py ]; then
python3 /update_build_metadata.py "$CONFIG_PATH" || echo "[WARN] Unable to update build metadata"
fi
chmod +x "$SERVER_PATH"
exec "$SERVER_PATH" "$@"
# Run watchdog
cd /ss14/publish/
exec ./SS14.Watchdog "$@"

View File

@@ -1,207 +0,0 @@
#!/usr/bin/env python3
import hashlib
import json
import os
import re
import shutil
import sys
import urllib.error
import urllib.request
from datetime import datetime
from pathlib import Path
try:
import tomllib # Python 3.11+
except ModuleNotFoundError: # pragma: no cover - fallback for older runtimes
try:
import tomli as tomllib # type: ignore
except ModuleNotFoundError: # pragma: no cover
tomllib = None # type: ignore
def fetch_manifest(url: str) -> dict | None:
try:
with urllib.request.urlopen(url, timeout=15) as resp:
return json.load(resp)
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as exc:
print(f"[WARN] Failed to download manifest from {url}: {exc}", file=sys.stderr)
return None
def select_latest_build(builds: dict) -> dict | None:
if not builds:
return None
# Builds are keyed by commit hash; pick the newest by the ISO8601 timestamp.
def build_time(item):
_, data = item
try:
return datetime.fromisoformat(data.get("time", "").rstrip("Z"))
except ValueError:
return datetime.min
latest_key, latest_data = max(builds.items(), key=build_time)
client = latest_data.get("client")
if not client:
print(f"[WARN] Manifest entry {latest_key} is missing client info", file=sys.stderr)
return None
client.setdefault("version", latest_key)
return client
def update_config(config_path: str, url: str, sha256: str) -> bool:
try:
config_text = Path(config_path).read_text(encoding="utf-8")
except OSError as exc:
print(f"[WARN] Cannot read config {config_path}: {exc}", file=sys.stderr)
return False
download_pattern = re.compile(r"(?m)^(#\s*)?download_url\s*=.*$")
build_pattern = re.compile(r"(?m)^(#\s*)?build\s*=.*$")
new_text = download_pattern.sub(f'download_url = "{url}"', config_text, count=1)
new_text = build_pattern.sub(f'build = "{sha256.upper()}"', new_text, count=1)
if new_text == config_text:
print("[WARN] No download_url/build entries were updated in server_config.toml", file=sys.stderr)
return False
try:
Path(config_path).write_text(new_text, encoding="utf-8")
except OSError as exc:
print(f"[WARN] Cannot write config {config_path}: {exc}", file=sys.stderr)
return False
print(f"[INFO] Updated build.download_url to {url}")
return True
def parse_build_section(config_path: str) -> tuple[str | None, str | None] | None:
if tomllib is None:
print("[WARN] tomllib/tomli is not available; cannot parse server_config.toml", file=sys.stderr)
return None
try:
contents = Path(config_path).read_text(encoding="utf-8")
except OSError as exc:
print(f"[WARN] Cannot read config {config_path}: {exc}", file=sys.stderr)
return None
try:
data = tomllib.loads(contents)
except (tomllib.TOMLDecodeError, AttributeError) as exc: # type: ignore[attr-defined]
print(f"[WARN] Failed to parse TOML {config_path}: {exc}", file=sys.stderr)
return None
build_section = data.get("build")
if not isinstance(build_section, dict):
print("[WARN] No [build] section found in server_config.toml", file=sys.stderr)
return None
download_url = build_section.get("download_url") or build_section.get("download-url")
build_hash = build_section.get("build")
download_url = download_url.strip() if isinstance(download_url, str) else None
build_hash = build_hash.strip() if isinstance(build_hash, str) else None
return download_url, build_hash
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as file_obj:
for chunk in iter(lambda: file_obj.read(65536), b""):
digest.update(chunk)
return digest.hexdigest().upper()
def download_to_path(url: str, dest: Path) -> bool:
tmp_path = dest.with_suffix(dest.suffix + ".tmp")
dest.parent.mkdir(parents=True, exist_ok=True)
try:
with urllib.request.urlopen(url, timeout=60) as resp, tmp_path.open("wb") as out_file:
shutil.copyfileobj(resp, out_file)
tmp_path.replace(dest)
return True
except (urllib.error.URLError, TimeoutError, OSError) as exc:
print(f"[WARN] Failed to download {url}: {exc}", file=sys.stderr)
try:
tmp_path.unlink(missing_ok=True) # type: ignore[call-arg]
except TypeError:
if tmp_path.exists():
tmp_path.unlink()
return False
def ensure_client_zip(config_path: str) -> bool:
parsed = parse_build_section(config_path)
if not parsed:
return False
download_url, expected_hash = parsed
if not download_url:
print("[WARN] build.download_url is not set; Content.Client.zip cannot be downloaded", file=sys.stderr)
return False
client_zip = Path(config_path).with_name("Content.Client.zip")
expected_hash_upper = expected_hash.upper() if expected_hash else None
if client_zip.exists() and expected_hash_upper:
try:
current_hash = sha256_file(client_zip)
except OSError as exc:
print(f"[WARN] Cannot read {client_zip}: {exc}", file=sys.stderr)
else:
if current_hash == expected_hash_upper:
print("[INFO] Content.Client.zip already matches configured build hash")
return True
print("[INFO] Content.Client.zip hash mismatch; re-downloading")
if not download_to_path(download_url, client_zip):
return False
if expected_hash_upper:
try:
downloaded_hash = sha256_file(client_zip)
except OSError as exc:
print(f"[WARN] Cannot read downloaded {client_zip}: {exc}", file=sys.stderr)
return False
if downloaded_hash != expected_hash_upper:
print(
f"[WARN] Downloaded Content.Client.zip hash {downloaded_hash} does not match expected {expected_hash_upper}",
file=sys.stderr,
)
client_zip.unlink(missing_ok=True) # type: ignore[call-arg]
return False
print(f"[INFO] Downloaded Content.Client.zip to {client_zip}")
return True
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: update_build_metadata.py <path-to-server_config.toml>", file=sys.stderr)
sys.exit(1)
manifest_url = os.environ.get(
"ROBUST_CDN_MANIFEST", "https://cdn.wylab.me/fork/wylab/manifest"
)
manifest = fetch_manifest(manifest_url)
if not manifest:
sys.exit(0)
client_info = select_latest_build(manifest.get("builds", {}))
if not client_info:
sys.exit(0)
client_url = client_info.get("url")
client_hash = client_info.get("sha256")
if not (client_url and client_hash):
print("[WARN] Client entry missing url/sha256", file=sys.stderr)
sys.exit(0)
config_path = sys.argv[1]
if update_config(config_path, client_url, client_hash):
ensure_client_zip(config_path)
else:
print("[WARN] Skipping client download because config update failed", file=sys.stderr)