build: fetch the Windows tray from release, don't cross-compile it
from-source builds have no business pulling a 9GB MSVC sysroot for one Tauri exe. CI builds it once; the server image downloads the signed artifact and hash-checks it against the manifest. no release / offline -> no tray, server 404-skips. also: make rebuild/up/down/logs so `up -d build` stops biting.
This commit is contained in:
parent
865d82ded0
commit
7035da041d
4 changed files with 183 additions and 1 deletions
|
|
@ -30,6 +30,11 @@ Format: version, date, then grouped by category (Added, Changed, Removed, Fixed,
|
|||
direct child process elsewhere.
|
||||
- Desktop server route expanded to `/desktop/:platform/:arch` for multi-OS
|
||||
binary serving.
|
||||
- Windows tray reaches the server image by fetching the signed exe from the
|
||||
newest release (hash-verified against the manifest), not by cross-compiling
|
||||
Tauri-for-Windows in every from-source build — that drags in the ~9GB MSVC
|
||||
sysroot. CI builds it once; from-source servers download the verified
|
||||
artifact. Absent/offline degrades to no tray (optional component).
|
||||
- `signalDesktopRestart` works on Windows (`taskkill /F /IM`) instead of
|
||||
no-opping.
|
||||
|
||||
|
|
|
|||
28
Makefile
28
Makefile
|
|
@ -1,4 +1,7 @@
|
|||
.PHONY: help db-up db-down server agent clean kernel-enforcer test lint version
|
||||
.PHONY: help db-up db-down server agent clean kernel-enforcer test lint version up rebuild rebuild-clean down logs prune-cruft
|
||||
|
||||
# docker compose v2 (override with COMPOSE=docker-compose for the v1 plugin)
|
||||
COMPOSE ?= docker compose
|
||||
|
||||
VERSION ?= $(shell git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "dev")
|
||||
BUILD_TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
|
@ -17,6 +20,29 @@ help: ## Show this help message
|
|||
version: ## Print current version
|
||||
@echo "VERSION=$(VERSION)"
|
||||
|
||||
up: ## Start the stack (NO rebuild — reuses the existing image)
|
||||
$(COMPOSE) up -d
|
||||
|
||||
rebuild: ## Rebuild changed layers and restart (the everyday command)
|
||||
$(COMPOSE) up -d --build
|
||||
|
||||
rebuild-clean: ## Full no-cache rebuild from scratch, then restart
|
||||
$(COMPOSE) build --no-cache && $(COMPOSE) up -d
|
||||
|
||||
down: ## Stop the stack
|
||||
$(COMPOSE) down
|
||||
|
||||
logs: ## Tail the server logs
|
||||
$(COMPOSE) logs -f server
|
||||
|
||||
prune-cruft: ## Remove retired RedFlag images (redflag-web, desktop-stage-test)
|
||||
-docker image rm redflag-web:latest redflag-desktop-stage-test:latest 2>/dev/null; true
|
||||
|
||||
fetch-desktop-windows: ## Fetch+verify the signed Windows tray from the latest release into ./dist
|
||||
@mkdir -p dist
|
||||
sh scripts/fetch-desktop-windows.sh amd64 ./dist
|
||||
@ls -lh dist/redflag-desktop.exe 2>/dev/null || echo "no Windows tray in the latest release yet"
|
||||
|
||||
db-up: ## Start PostgreSQL database
|
||||
docker-compose up -d postgres
|
||||
@echo "Waiting for database to be ready..."
|
||||
|
|
|
|||
114
scripts/fetch-desktop-windows.sh
Executable file
114
scripts/fetch-desktop-windows.sh
Executable file
|
|
@ -0,0 +1,114 @@
|
|||
#!/bin/sh
|
||||
# fetch-desktop-windows.sh — obtain the prebuilt, signed Windows desktop tray
|
||||
# binary from the newest published release and verify it against that release's
|
||||
# manifest hash.
|
||||
#
|
||||
# Why this exists: the agent is Go and cross-compiles to Windows for free
|
||||
# (Dockerfile bakes redflag-agent.exe directly). The desktop tray is Tauri/Rust
|
||||
# and its Windows cross-compile drags in the whole MSVC sysroot (~9GB of build
|
||||
# toolchain). Forcing that into every from-source `docker-compose build` is
|
||||
# unacceptable — build-from-source is the primary distribution path. So the
|
||||
# expensive compile lives in CI (release.yml, cargo-xwin) exactly once, and
|
||||
# from-source servers fetch the resulting signed artifact here.
|
||||
#
|
||||
# Trust: TLS to the forge + the manifest sha256 gate this download, the same
|
||||
# trust any release download carries. The server re-signs the binary with its
|
||||
# own Ed25519 key at startup before serving it to the fleet — that is the
|
||||
# load-bearing signature agents verify. This step gets a known-good exe into the
|
||||
# serving path; it is not the fleet trust root.
|
||||
#
|
||||
# The desktop component is OPTIONAL (manifest required:false). Every failure that
|
||||
# is not active tampering degrades to "no Windows tray" rather than breaking the
|
||||
# build: offline, no release yet, no desktop asset in the release, no manifest
|
||||
# entry. A hash MISMATCH is the one hard stop — that is tampering, not absence.
|
||||
set -eu
|
||||
|
||||
ARCH="${1:-amd64}"
|
||||
OUT_DIR="${2:-/out}"
|
||||
REPO_API="${DESKTOP_RELEASE_REPO_API:-https://codeberg.org/api/v1/repos/Fimeg/RedFlag}"
|
||||
|
||||
log() { printf '%s\n' "[INFO] [build] [desktop-fetch] $*" >&2; }
|
||||
warn() { printf '%s\n' "[WARN] [build] [desktop-fetch] $*" >&2; }
|
||||
fail() { printf '%s\n' "[ERROR] [build] [desktop-fetch] $*" >&2; exit 1; }
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
# Newest release INCLUDING prereleases. /releases/latest skips prereleases, and
|
||||
# every tag below v0.3.0 publishes as a prerelease during the alpha — using it
|
||||
# would always come back empty. The list endpoint sorts newest-first.
|
||||
log "querying newest release from $REPO_API"
|
||||
if ! curl -sfL "$REPO_API/releases?limit=1&draft=false" -o "$work/releases.json"; then
|
||||
warn "release API unreachable (offline or forge down) — skipping Windows tray"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$(jq 'length' "$work/releases.json" 2>/dev/null || echo 0)" = "0" ]; then
|
||||
log "no published releases at $REPO_API yet — Windows tray ships once a release exists"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
manifest_url="$(jq -r '.[0].assets[]? | select(.name == "manifest.json") | .browser_download_url' "$work/releases.json" 2>/dev/null | head -1)"
|
||||
tag="$(jq -r '.[0].tag_name // empty' "$work/releases.json" 2>/dev/null)"
|
||||
if [ -z "$manifest_url" ] || [ "$manifest_url" = "null" ]; then
|
||||
warn "newest release ($tag) has no manifest.json asset — skipping Windows tray"
|
||||
exit 0
|
||||
fi
|
||||
log "newest release: $tag"
|
||||
|
||||
if ! curl -sfL "$manifest_url" -o "$work/manifest.json"; then
|
||||
warn "manifest download failed — skipping Windows tray"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Pull the expected hash + filename for desktop-windows/<arch> out of the manifest.
|
||||
expected_sha="$(jq -r --arg a "$ARCH" \
|
||||
'.artifacts[]? | select(.platform == "desktop-windows" and .architecture == $a) | .sha256' \
|
||||
"$work/manifest.json" 2>/dev/null | head -1)"
|
||||
exe_name="$(jq -r --arg a "$ARCH" \
|
||||
'.artifacts[]? | select(.platform == "desktop-windows" and .architecture == $a) | .filename' \
|
||||
"$work/manifest.json" 2>/dev/null | head -1)"
|
||||
|
||||
if [ -z "$expected_sha" ] || [ "$expected_sha" = "null" ]; then
|
||||
warn "release $tag carries no desktop-windows/$ARCH binary — Windows tray unavailable from this release"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# The exe ships inside the platform zip (release.yml attaches zips, not loose
|
||||
# exes). Derive the zip asset name from the release version.
|
||||
ver="${tag#v}"
|
||||
zip_name="redflag-${ver}-windows-${ARCH}.zip"
|
||||
zip_url="$(jq -r --arg z "$zip_name" \
|
||||
'.[0].assets[]? | select(.name == $z) | .browser_download_url' \
|
||||
"$work/releases.json" 2>/dev/null | head -1)"
|
||||
if [ -z "$zip_url" ] || [ "$zip_url" = "null" ]; then
|
||||
warn "release $tag has a manifest entry but no $zip_name asset — skipping Windows tray"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "downloading $zip_name"
|
||||
if ! curl -sfL "$zip_url" -o "$work/win.zip"; then
|
||||
warn "zip download failed — skipping Windows tray"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! unzip -o -q "$work/win.zip" -d "$work/unz"; then
|
||||
warn "zip extraction failed — skipping Windows tray"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
src="$work/unz/$exe_name"
|
||||
if [ ! -f "$src" ]; then
|
||||
warn "$exe_name not found inside $zip_name — skipping Windows tray"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
actual_sha="$(sha256sum "$src" | awk '{print $1}')"
|
||||
if [ "$actual_sha" != "$expected_sha" ]; then
|
||||
# Tampering — refuse the build. This is the one non-recoverable case.
|
||||
fail "hash mismatch on $exe_name: manifest=$expected_sha actual=$actual_sha"
|
||||
fi
|
||||
|
||||
cp "$src" "$OUT_DIR/redflag-desktop.exe"
|
||||
log "verified Windows tray $tag (desktop-windows/$ARCH) -> $OUT_DIR/redflag-desktop.exe"
|
||||
|
|
@ -174,6 +174,28 @@ RUN cd desktop && cargo build --release && \
|
|||
mkdir -p /out && \
|
||||
cp target/release/redflag-desktop /out/redflag-desktop
|
||||
|
||||
# Stage 2d: Fetch the prebuilt, signed Windows desktop tray from the newest
|
||||
# release and verify it against the release manifest hash. The Windows desktop
|
||||
# is Tauri/Rust — cross-compiling it pulls the whole MSVC sysroot (~9GB of build
|
||||
# toolchain), which has no business in a from-source server build. CI
|
||||
# (release.yml, cargo-xwin) builds it once where the toolchain is ephemeral;
|
||||
# this stage fetches the signed result. Optional component: anything short of a
|
||||
# hash mismatch degrades to "no Windows tray". See scripts/fetch-desktop-windows.sh.
|
||||
FROM alpine:3.21 AS desktop-windows-fetcher
|
||||
|
||||
ARG BUILD_VERSION=dev
|
||||
ARG DESKTOP_RELEASE_REPO_API=https://codeberg.org/api/v1/repos/Fimeg/RedFlag
|
||||
ENV DESKTOP_RELEASE_REPO_API=${DESKTOP_RELEASE_REPO_API}
|
||||
|
||||
RUN apk --no-cache add curl jq unzip
|
||||
|
||||
COPY scripts/fetch-desktop-windows.sh /usr/local/bin/fetch-desktop-windows.sh
|
||||
|
||||
# BUILD_VERSION sits in this layer's cache key so bumping the version re-fetches
|
||||
# rather than reusing a stale tray from an earlier build.
|
||||
RUN echo "fetch Windows tray for build $BUILD_VERSION" && \
|
||||
sh /usr/local/bin/fetch-desktop-windows.sh amd64 /out
|
||||
|
||||
# Stage 3: Final image with server and all agent binaries
|
||||
FROM alpine:3.21
|
||||
|
||||
|
|
@ -195,6 +217,21 @@ COPY --from=helper-builder /out/helper-linux-amd64 ./binaries/helper-linux-amd64
|
|||
# Copy the Tauri desktop app (system tray + local UI shell)
|
||||
COPY --from=desktop-builder /out/redflag-desktop ./binaries/linux-amd64/redflag-desktop
|
||||
|
||||
# Stage in the Windows desktop tray IF the newest release carried one (fetched +
|
||||
# hash-verified by the desktop-windows-fetcher stage). When absent — no release
|
||||
# yet, offline build, or a release with no desktop — the server simply serves no
|
||||
# Windows tray and the installer 404-skips it. The heavy Windows compile never
|
||||
# happens here; only the verified artifact lands.
|
||||
COPY --from=desktop-windows-fetcher /out /opt/winfetch
|
||||
RUN if [ -f /opt/winfetch/redflag-desktop.exe ]; then \
|
||||
mkdir -p ./binaries/windows-amd64 && \
|
||||
mv /opt/winfetch/redflag-desktop.exe ./binaries/windows-amd64/redflag-desktop.exe && \
|
||||
echo "[INFO] [build] [desktop] staged Windows tray into image"; \
|
||||
else \
|
||||
echo "[INFO] [build] [desktop] no Windows tray staged (none in latest release)"; \
|
||||
fi; \
|
||||
rm -rf /opt/winfetch
|
||||
|
||||
# Copy and setup entrypoint script
|
||||
COPY server/docker-entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
|
|
|||
Loading…
Reference in a new issue