#!/usr/bin/env bash # Builds everything a Felis release installs, for every architecture, into one directory: # # felis-linux- the felis binary, panel included # felis-image-felis-linux-.tar the control-plane image (OCI layout tars: # felis-image-game-linux-.tar the limbo, lobby and paper images `ctr images import` # felis-image-base-linux-.tar the registry and PostgreSQL reads them as-is) # felis-images-linux-.txt one "bundle role name manifest-digest config-digest" # line per image in the three tars # felis-velocity.jar the proxy plugin (JVM bytecode, one for every arch) # SHA256SUMS the sha256 of every file above # # Every name above is a contract with deploy/bootstrap.sh, which installs from a release's # assets (or from a directory like this one, FELIS_ARTIFACT_DIR) instead of building on the # host: with them a host needs neither Docker, Gradle, Go nor Docker Hub. The images are split # in three because they change at different rates: the control plane with every release, the # game images when game-stack.lock or a plugin changes, the base images almost never. An # upgrade downloads only the tars holding an image the host does not have yet. # # .github/workflows/release.yml runs this on a tag and publishes the directory; e2e.yml runs # it on a branch and installs from the directory. # # Needs docker with buildx, and binfmt/QEMU for the architectures other than the builder's: # the game images' runtime stages run apt-get on the target platform. The builder's own felis # binary writes the image tars, so Go is not needed here either. # # Usage: deploy/build-release-artifacts.sh # FELIS_RELEASE_ARCHES the architectures to build (default "amd64 arm64") set -Eeuo pipefail log() { printf '\033[1;36m[release]\033[0m %s\n' "$*"; } die() { printf '\033[1;31m[fail]\033[0m %s\n' "$*" >&2; exit 1; } [ "$#" -eq 2 ] || die "usage: $0 " VERSION="$1" OUT="$2" ARCHES="${FELIS_RELEASE_ARCHES:-amd64 arm64}" cd "$(dirname "$0")/.." # The Gradle image the plugin builds run in: deploy/{limbo,lobby}/Dockerfile and bootstrap's # Velocity build name the same one (bootstrap_asset_test.go holds them together). PLUGIN_BUILD_IMAGE="gradle:9.8.0-jdk25@sha256:2b2fc1b1dfc3604a2acc916839f36eb5ee48fd7f232427fc5faca224c73bcb01" # The names and pins bootstrap uses, read from bootstrap itself so the two cannot drift. bootstrap_value() { local v v="$(sed -n "s/^$1=\"\(.*\)\"\$/\1/p" deploy/bootstrap.sh)" [ -n "$v" ] || die "deploy/bootstrap.sh sets no $1" printf '%s' "$v" } REGISTRY_URL="$(bootstrap_value REGISTRY_URL)" REGISTRY_IMAGE="$(bootstrap_value REGISTRY_IMAGE)" POSTGRES_IMAGE="$(bootstrap_value POSTGRES_IMAGE)" # The final stage of the repo Dockerfile, the base every felis image runs on. FELIS_BASE_IMAGE="$(awk '$1 == "FROM" && $2 ~ /^gcr\.io\/distroless\// { print $2 }' Dockerfile)" [ -n "$FELIS_BASE_IMAGE" ] || die "the Dockerfile names no distroless base" # lock_value reads one KEY=value line of deploy/game-stack.lock, which bootstrap reads the # same way: never sourced, values limited to URL and version characters. lock_value() { local v v="$(awk -F= -v k="$1" '$1 == k { print substr($0, length(k) + 2); exit }' deploy/game-stack.lock)" case "$v" in ''|*[!A-Za-z0-9._:/+%-]*) die "deploy/game-stack.lock: $1 is missing or malformed" ;; esac printf '%s' "$v" } # image_tag_for_version is bootstrap's: the tag bootstrap names this release's image with. image_tag_for_version() { local v v="$(printf '%s' "$1" | tr -c 'A-Za-z0-9_.-' '-')" case "$v" in ""|dev|[!A-Za-z0-9_]*) printf 'demo' ;; *) printf '%s' "${v:0:128}" ;; esac } host_arch() { case "$(uname -m)" in x86_64|amd64) printf 'amd64' ;; aarch64|arm64) printf 'arm64' ;; *) die "unsupported builder architecture $(uname -m)" ;; esac } mkdir -p "$OUT" [ -z "$(ls -A "$OUT")" ] || die "${OUT} is not empty; SHA256SUMS must describe exactly what one run built" WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT platforms="" for arch in $ARCHES; do case "$arch" in amd64|arm64) ;; *) die "unsupported architecture ${arch}" ;; esac platforms="${platforms:+${platforms},}linux/${arch}" done # ---- the felis binaries -------------------------------------------------------------- # Through the repo Dockerfile, the one recipe that runs the panel build before go build: # a plain `go build` compiles against the placeholder panel and ships it. log "building felis ${VERSION} for ${platforms}" docker buildx build --platform "$platforms" \ --build-arg FELIS_VERSION="$VERSION" \ --output "type=local,dest=${WORK}/bin" . for arch in $ARCHES; do src="${WORK}/bin/usr/local/bin/felis" # buildx nests the output per platform only when it builds more than one. [ -f "${WORK}/bin/linux_${arch}/usr/local/bin/felis" ] && src="${WORK}/bin/linux_${arch}/usr/local/bin/felis" install -m 0755 "$src" "${OUT}/felis-linux-${arch}" # By ELF machine, never by running it: binfmt would run the wrong architecture happily. case "$arch" in amd64) want='x86-64' ;; arm64) want='ARM aarch64' ;; esac file "${OUT}/felis-linux-${arch}" | grep -q "$want" \ || die "felis-linux-${arch} is not a ${want} ELF: TARGETARCH did not reach the go build" done HOST_FELIS="${OUT}/felis-linux-$(host_arch)" [ -x "$HOST_FELIS" ] || die "no felis binary for the builder's own architecture ($(host_arch)); add it to FELIS_RELEASE_ARCHES" got="$("$HOST_FELIS" version | head -n 1)" [ "$got" = "felis ${VERSION}" ] || die "felis reports '${got}', want 'felis ${VERSION}': the version stamp did not reach the binary" # ---- the Velocity plugin ------------------------------------------------------------- # The command bootstrap's source path runs, on a copy of the tree so the checkout stays clean. log "building felis-velocity.jar in ${PLUGIN_BUILD_IMAGE%%@*}" mkdir -p "${WORK}/velocity" tar -C . --exclude=build --exclude=.gradle -cf - plugins/velocity plugins/shared | tar -C "${WORK}/velocity" -xf - docker run --rm --user "$(id -u):$(id -g)" -e HOME=/tmp -e GRADLE_USER_HOME=/tmp/gradle \ -v "${WORK}/velocity:/src" -w /src/plugins/velocity \ "$PLUGIN_BUILD_IMAGE" gradle --no-daemon clean build jars=( "${WORK}"/velocity/plugins/velocity/build/libs/felis-velocity-*.jar ) [ "${#jars[@]}" -eq 1 ] && [ -f "${jars[0]}" ] || die "the felis-velocity build must produce exactly one plugin jar" install -m 0644 "${jars[0]}" "${OUT}/felis-velocity.jar" # ---- the images ---------------------------------------------------------------------- FELIS_IMAGE="${REGISTRY_URL}/felis/felis:$(image_tag_for_version "$VERSION")" game_build_args=( --build-arg "LIMBO_JAR_URL=$(lock_value LIMBO_JAR_URL)" --build-arg "LIMBO_JAR_SHA256=$(lock_value LIMBO_JAR_SHA256)" --build-arg "LIMBO_SCHEM_URL=$(lock_value LIMBO_SCHEM_URL)" --build-arg "LIMBO_SCHEM_SHA256=$(lock_value LIMBO_SCHEM_SHA256)" --build-arg "LIMBO_VERSION=$(lock_value LIMBO_VERSION)" --build-arg "PAPER_JAR_URL=$(lock_value PAPER_JAR_URL)" --build-arg "PAPER_JAR_SHA256=$(lock_value PAPER_JAR_SHA256)" --build-arg "LUCKPERMS_JAR_URL=$(lock_value LUCKPERMS_JAR_URL)" --build-arg "LUCKPERMS_JAR_SHA256=$(lock_value LUCKPERMS_JAR_SHA256)" ) # oci_image builds one image for one platform into an # OCI layout tar. No attestations: the bundle carries images only. oci_image() { local arch="$1" dest="$2" shift 2 docker buildx build --platform "linux/${arch}" --provenance=false --sbom=false \ --output "type=oci,dest=${dest}" "$@" } # bundle writes one image tar and appends its lines # to the architecture's listing. bundle() { local arch="$1" group="$2" name shift 2 name="felis-image-${group}-linux-${arch}.tar" "$HOST_FELIS" image-bundle --platform "linux/${arch}" \ --out "${OUT}/${name}" --list "${WORK}/${name}.txt" "$@" awk -v b="$name" '{ print b, $0 }' "${WORK}/${name}.txt" >> "${OUT}/felis-images-linux-${arch}.txt" } for arch in $ARCHES; do # The control-plane image wraps the released binary itself, byte for byte, the way # bootstrap wraps a downloaded binary. log "building the linux/${arch} images" mkdir -p "${WORK}/felis-${arch}" cp "${OUT}/felis-linux-${arch}" "${WORK}/felis-${arch}/felis" cat > "${WORK}/felis-${arch}/Dockerfile" < "${WORK}/SHA256SUMS" mv "${WORK}/SHA256SUMS" "${OUT}/SHA256SUMS" ls -l "$OUT"