forked from LiamAEdwards/SS14-Docker-Linux-Server
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63d79740ed | |||
|
|
0651098b66 | ||
|
|
d21b1e9877 | ||
|
|
b9bbe80856 | ||
|
|
906d405e43 | ||
| f74b25bc3c | |||
| 1d20c8c80d | |||
| 1916037ff8 | |||
| 129c4bebd1 | |||
| 050061a7aa | |||
| aaade96257 |
107
.github/workflows/main.yml
vendored
107
.github/workflows/main.yml
vendored
@@ -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
|
||||
|
||||
80
Dockerfile
80
Dockerfile
@@ -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"]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
36
start.sh
36
start.sh
@@ -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 "$@"
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user