From 7e39f41b27525666f6001499edbfd3123f8246a5 Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Mon, 3 Aug 2026 23:08:07 +0200 Subject: [PATCH] Add fused desktop binaries (win/mac/linux) to the pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fuse the .love into the official LÖVE 11.5 release artifacts: win-x64 (love.exe concat + dlls), mac-universal (love.app bundle, Info.plist patched) and linux-x64 (AppImage rebuilt via readelf offset + unsquashfs/mksquashfs — the AppImage runtime is glibc-linked and cannot run on the alpine builder). The builder image pre-fetches the dist files to /opt/love-dist and gains squashfs-tools + binutils. Co-Authored-By: Claude Fable 5 --- README.md | 50 ++++++++++++++----- builder.Dockerfile | 19 ++++++-- example-makefile.make | 103 +++++++++++++++++++++++++++++++++++++++- example-woodpecker.yaml | 5 ++ 4 files changed, 160 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index c6a9127..be2850f 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ CI/CD templates for [LÖVE](https://love2d.org) (Love2D/Lua) game projects, in the same spirit as [tic80-tools](../tic80-tools). - `example-makefile.make` — project Makefile: local dev targets - (`build`, `love`, `web`, `export`, `watch`, `clean`) plus the - Woodpecker pipeline targets (`ci-version`, `ci-export`, `ci-upload`, - `ci-update`). + (`build`, `love`, `web`, `export`, `binaries`, `watch`, `clean`) + plus the Woodpecker pipeline targets (`ci-version`, `ci-export`, + `ci-binaries`, `ci-upload`, `ci-update`). - `example-woodpecker.yaml` — Woodpecker pipeline: version → export - (`.love` package + love.js web build) → upload (scp to the droparea) - → update (calls the teletypegames `/update` endpoint with - `platform=love`). + (`.love` package + love.js web build) → binaries (fused desktop + builds) → upload (scp to the droparea) → update (calls the + teletypegames `/update` endpoint with `platform=love`). - `example-tasks.json` — VS Code `.vscode/tasks.json` with the common dev tasks: **Run Löve** (`love .`, default build task, `Cmd+Shift+B`), **Build & Run Löve**, **Build .love package** and @@ -25,16 +25,40 @@ projects, in the same spirit as [tic80-tools](../tic80-tools). 3. Copy `example-tasks.json` to `.vscode/tasks.json`. The pipeline uploads `$(PROJECT)-$(VERSION).love.zip`, -`$(PROJECT)-$(VERSION).html.zip` and the versioned `metadata.json` to -the droparea, then triggers `/update` twice: once with `platform=love` -(desktop package) and once with `platform=love-web` (web build). +`$(PROJECT)-$(VERSION).html.zip`, the versioned `metadata.json` and +the fused binary zips (`-win-x64.zip`, `-mac-universal.zip`, +`-linux-x64.zip`) to the droparea, then triggers `/update` twice: once +with `platform=love` (desktop package) and once with +`platform=love-web` (web build). The API's `BinaryAttachment` concern +picks up the `--.zip` files automatically and +registers them as `win_x64` / `mac_universal` / `linux_x64` release +assets. + +## Native (fused) binaries + +`make binaries VERSION=x.y` builds desktop packages from the official +LÖVE $(LOVE_VERSION) release artifacts — no compilation involved: + +- **win-x64** — `love.exe` + the `.love` concatenated (the standard + LÖVE fusing mechanism), zipped together with the dlls. +- **mac-universal** — the `.love` dropped into the official + `love.app` (a universal x86_64+arm64 bundle), `Info.plist` patched + to the project name/identifier. Unsigned — Gatekeeper will warn on + first launch. +- **linux-x64** — the official AppImage taken apart, the `.love` + fused into `bin/love`, then reassembled. The AppImage runtime is + glibc-linked and cannot be executed on alpine, so the Makefile + computes the squashfs offset with `readelf` and repacks with + `mksquashfs` instead of running `--appimage-extract`. ## Builder image `builder.Dockerfile` bakes the pipeline's export-step dependencies -(zip/unzip/curl/make + `luac` for the syntax check) into +(zip/unzip/curl/make + `luac` for the syntax check, squashfs-tools + +binutils for the AppImage fusing) into `git.teletypegames.org/internal/love-builder`, and pre-fetches love.js to -`/opt/lovejs.zip` — the Makefile uses the cached copy when present +`/opt/lovejs.zip` plus the official LÖVE dist files to +`/opt/love-dist/` — the Makefile uses the cached copies when present and only falls back to downloading from GitHub outside the image: ```sh @@ -43,6 +67,8 @@ docker build --platform linux/amd64 -f builder.Dockerfile \ docker push git.teletypegames.org/internal/love-builder:latest ``` -Rebuild the image to pick up a newer love.js snapshot. +Rebuild the image to pick up a newer love.js snapshot or LÖVE +release (bump `LOVE_VERSION` in the Dockerfile and the Makefile +together). Detailed documentation lives on the wiki: `/development/love`. diff --git a/builder.Dockerfile b/builder.Dockerfile index 45d6801..7116b09 100644 --- a/builder.Dockerfile +++ b/builder.Dockerfile @@ -1,6 +1,11 @@ # CI builder image for Love2D projects: packaging tools + luac syntax check, -# with love.js pre-fetched to /opt/lovejs.zip so `make web` does not hit -# GitHub on every pipeline run (the Makefile falls back to curl elsewhere). +# with love.js and the official LÖVE dist files pre-fetched so the pipeline +# does not hit GitHub on every run (the Makefile falls back to curl elsewhere). +# +# squashfs-tools + binutils are needed for the linux AppImage fusing: +# the AppImage runtime is glibc-linked so it cannot be executed on alpine — +# the Makefile computes the squashfs offset with readelf and repacks with +# mksquashfs instead. # # Build & push (the CI runner is linux/amd64): # docker build --platform linux/amd64 -f builder.Dockerfile \ @@ -9,13 +14,21 @@ FROM alpine:3.22 -RUN apk add --no-cache make zip unzip curl git lua5.4 \ +ARG LOVE_VERSION=11.5 + +RUN apk add --no-cache make zip unzip curl git lua5.4 squashfs-tools binutils \ && ln -sf /usr/bin/lua5.4 /usr/bin/lua \ && ln -sf /usr/bin/luac5.4 /usr/bin/luac RUN curl -sSL https://github.com/2dengine/love.js/archive/refs/heads/master.zip \ -o /opt/lovejs.zip +RUN mkdir -p /opt/love-dist \ + && cd /opt/love-dist \ + && curl -sSLO https://github.com/love2d/love/releases/download/${LOVE_VERSION}/love-${LOVE_VERSION}-win64.zip \ + && curl -sSLO https://github.com/love2d/love/releases/download/${LOVE_VERSION}/love-${LOVE_VERSION}-macos.zip \ + && curl -sSLO https://github.com/love2d/love/releases/download/${LOVE_VERSION}/love-${LOVE_VERSION}-x86_64.AppImage + WORKDIR /workspace CMD ["/bin/sh"] diff --git a/example-makefile.make b/example-makefile.make index edd174a..f90b1da 100644 --- a/example-makefile.make +++ b/example-makefile.make @@ -59,6 +59,97 @@ web: love sed -i.bak 's|||g' $(WEB_DIR)/index.html && rm $(WEB_DIR)/index.html.bak @echo "==> Web build ready in $(WEB_DIR)" +# ----------------------------------------- +# Native (fused) binaries from the official LÖVE releases. +# The love-builder CI image pre-fetches the dist files to /opt/love-dist; +# local builds fall back to downloading them from GitHub. +# win: love.exe + .love concatenated; mac: .love dropped into the +# official (universal) love.app; linux: the official AppImage taken +# apart, the .love fused into bin/love, then reassembled with +# mksquashfs (squashfs-tools required). +# ----------------------------------------- + +LOVE_VERSION = 11.5 +LOVE_BASE_URL = https://github.com/love2d/love/releases/download/$(LOVE_VERSION) +LOVE_CACHE_DIR = /opt/love-dist + +BINARY_SLUGS = win-x64 mac-universal linux-x64 + +# $(1) dist file name +define f_fetch_love + mkdir -p $(DIST_DIR); \ + if [ -f "$(LOVE_CACHE_DIR)/$(1)" ]; then \ + echo "==> Using cached $(1)"; \ + cp "$(LOVE_CACHE_DIR)/$(1)" "$(DIST_DIR)/$(1)"; \ + elif [ ! -f "$(DIST_DIR)/$(1)" ]; then \ + echo "==> Downloading $(1)"; \ + curl -sSL "$(LOVE_BASE_URL)/$(1)" -o "$(DIST_DIR)/$(1)"; \ + fi +endef + +binaries: + @if [ -z "$(VERSION)" ]; then \ + echo "ERROR: VERSION not set!"; exit 1; \ + fi + @$(MAKE) binary-win binary-mac binary-linux VERSION=$(VERSION) + +binary-win: love + @echo "==> Fusing windows binary" + @$(call f_fetch_love,love-$(LOVE_VERSION)-win64.zip) + @set -e; \ + PKG_DIR="$(PROJECT)-$(VERSION)-win-x64"; \ + rm -rf "$$PKG_DIR" "$$PKG_DIR.zip" $(DIST_DIR)/win64; \ + unzip -q $(DIST_DIR)/love-$(LOVE_VERSION)-win64.zip -d $(DIST_DIR)/win64; \ + SRC=$$(dirname $$(find $(DIST_DIR)/win64 -name love.exe | head -n 1)); \ + mkdir -p "$$PKG_DIR"; \ + cat "$$SRC/love.exe" $(OUTPUT_LOVE) > "$$PKG_DIR/$(PROJECT).exe"; \ + cp "$$SRC"/*.dll "$$PKG_DIR/"; \ + cp "$$SRC/license.txt" "$$PKG_DIR/" 2>/dev/null || true; \ + zip -qr "$$PKG_DIR.zip" "$$PKG_DIR"; \ + rm -rf "$$PKG_DIR" $(DIST_DIR)/win64; \ + echo "==> $$PKG_DIR.zip kesz" + +binary-mac: love + @echo "==> Fusing macOS app bundle" + @$(call f_fetch_love,love-$(LOVE_VERSION)-macos.zip) + @set -e; \ + PKG_DIR="$(PROJECT)-$(VERSION)-mac-universal"; \ + rm -rf "$$PKG_DIR" "$$PKG_DIR.zip" $(DIST_DIR)/macos; \ + unzip -q $(DIST_DIR)/love-$(LOVE_VERSION)-macos.zip -d $(DIST_DIR)/macos; \ + mkdir -p "$$PKG_DIR"; \ + mv $(DIST_DIR)/macos/love.app "$$PKG_DIR/$(PROJECT).app"; \ + cp $(OUTPUT_LOVE) "$$PKG_DIR/$(PROJECT).app/Contents/Resources/"; \ + PLIST="$$PKG_DIR/$(PROJECT).app/Contents/Info.plist"; \ + sed -i.bak "s|LÖVE|$(PROJECT)|g" "$$PLIST" && rm "$$PLIST.bak"; \ + sed -i.bak "s|org\.love2d\.love|org.teletypegames.$(PROJECT)|g" "$$PLIST" && rm "$$PLIST.bak"; \ + zip -qry "$$PKG_DIR.zip" "$$PKG_DIR"; \ + rm -rf "$$PKG_DIR" $(DIST_DIR)/macos; \ + echo "==> $$PKG_DIR.zip kesz" + +# Az AppImage runtime glibc-dinamikus, alpine (musl) alatt nem futtatható, +# ezért nem a runtime-ot futtatjuk: az offsetet readelf-ből számoljuk +# (shoff + shentsize*shnum), a squashfs-t unsquashfs -o bontja ki. +binary-linux: love + @echo "==> Fusing linux AppImage" + @$(call f_fetch_love,love-$(LOVE_VERSION)-x86_64.AppImage) + @set -e; \ + PKG_DIR="$(PROJECT)-$(VERSION)-linux-x64"; \ + APPIMAGE="$(DIST_DIR)/love-$(LOVE_VERSION)-x86_64.AppImage"; \ + rm -rf "$$PKG_DIR" "$$PKG_DIR.zip" squashfs-root $(DIST_DIR)/game.squashfs $(DIST_DIR)/runtime; \ + OFFSET=$$(readelf -h "$$APPIMAGE" | awk '/Start of section headers/{o=$$5} /Size of section headers/{s=$$5} /Number of section headers/{n=$$5} END{print o+s*n}'); \ + unsquashfs -q -o $$OFFSET -d squashfs-root "$$APPIMAGE" >/dev/null; \ + cat squashfs-root/bin/love $(OUTPUT_LOVE) > squashfs-root/bin/love.fused; \ + mv squashfs-root/bin/love.fused squashfs-root/bin/love; \ + chmod +x squashfs-root/bin/love; \ + mksquashfs squashfs-root $(DIST_DIR)/game.squashfs -root-owned -noappend -quiet -comp gzip; \ + head -c $$OFFSET "$$APPIMAGE" > $(DIST_DIR)/runtime; \ + mkdir -p "$$PKG_DIR"; \ + cat $(DIST_DIR)/runtime $(DIST_DIR)/game.squashfs > "$$PKG_DIR/$(PROJECT).AppImage"; \ + chmod +x "$$PKG_DIR/$(PROJECT).AppImage"; \ + zip -qr "$$PKG_DIR.zip" "$$PKG_DIR"; \ + rm -rf "$$PKG_DIR" squashfs-root $(DIST_DIR)/game.squashfs $(DIST_DIR)/runtime; \ + echo "==> $$PKG_DIR.zip kesz" + export: love web @if [ -z "$(VERSION)" ]; then \ echo "ERROR: VERSION not set!"; exit 1; \ @@ -93,6 +184,10 @@ ci-export: @VERSION=$$(cat $(VERSION_FILE)); \ $(MAKE) export VERSION=$$VERSION +ci-binaries: + @VERSION=$$(cat $(VERSION_FILE)); \ + $(MAKE) binaries VERSION=$$VERSION + ci-upload: @VERSION=$$(cat $(VERSION_FILE)); \ FILE_LOVE="$(PROJECT)-$$VERSION.love.zip"; \ @@ -100,8 +195,12 @@ ci-upload: META_SRC="metadata.json"; \ META_DST="$(PROJECT)-$$VERSION.metadata.json"; \ cp $$META_SRC $$META_DST; \ + BINS=""; \ + for slug in $(BINARY_SLUGS); do \ + [ -f "$(PROJECT)-$$VERSION-$$slug.zip" ] && BINS="$$BINS $(PROJECT)-$$VERSION-$$slug.zip"; \ + done; \ sshpass -p "$(DROPAREA_SSH_PASSWORD)" scp -o StrictHostKeyChecking=no -P $(DROPAREA_PORT) \ - $$FILE_LOVE $$FILE_WEB $$META_DST \ + $$FILE_LOVE $$FILE_WEB $$META_DST $$BINS \ $(DROPAREA_USER)@$(DROPAREA_HOST):$(DROPAREA_TARGET_PATH)/ ci-update: @@ -109,4 +208,4 @@ ci-update: curl "$(UPDATE_SERVER)/update?secret=$(UPDATE_SECRET)&name=$(PROJECT)&platform=love&version=$$VERSION"; \ curl "$(UPDATE_SERVER)/update?secret=$(UPDATE_SECRET)&name=$(PROJECT)&platform=love-web&version=$$VERSION" -.PHONY: all build love web export watch clean ci-version ci-export ci-upload ci-update \ No newline at end of file +.PHONY: all build love web export binaries binary-win binary-mac binary-linux watch clean ci-version ci-export ci-binaries ci-upload ci-update \ No newline at end of file diff --git a/example-woodpecker.yaml b/example-woodpecker.yaml index 44fbbe0..ccd1d29 100644 --- a/example-woodpecker.yaml +++ b/example-woodpecker.yaml @@ -10,6 +10,11 @@ steps: commands: - make ci-export + - name: binaries + image: git.teletypegames.org/internal/love-builder:latest + commands: + - make ci-binaries + - name: upload image: alpine environment: