diff --git a/.gitea/workflows/busybox.yml b/.gitea/workflows/busybox.yml new file mode 100644 index 0000000..4000465 --- /dev/null +++ b/.gitea/workflows/busybox.yml @@ -0,0 +1,68 @@ +name: busybox static musl build + +on: + push: + branches: + - main + paths: + - .gitea/workflows/busybox.yml + - scripts/build_busybox.sh + - scripts/publish_release.py + - versions/busybox.version + workflow_dispatch: + +permissions: + contents: write + +jobs: + build-and-release: + runs-on: alpine-latest + + steps: + - name: Bootstrap workspace + shell: sh + env: + REPO_URL: ${{ gitea.server_url }} + REPO_NAME: ${{ gitea.repository }} + GIT_SHA: ${{ gitea.sha }} + run: | + apk add --no-cache git + workdir="$(pwd)" + tmpdir="$(mktemp -d)" + git clone --depth 1 "$REPO_URL/$REPO_NAME.git" "$tmpdir/repo" + git -C "$tmpdir/repo" fetch --depth 1 origin "$GIT_SHA" + git -C "$tmpdir/repo" checkout "$GIT_SHA" + find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} + + cp -a "$tmpdir/repo"/. "$workdir"/ + + - name: Read version + id: meta + shell: sh + working-directory: ${{ gitea.workspace }} + run: | + version="$(tr -d ' \n\r' < versions/busybox.version)" + artifact="busybox-${version}-linux-amd64-musl-static" + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "artifact=${artifact}" >> "$GITHUB_OUTPUT" + + - name: Build static busybox + shell: sh + working-directory: ${{ gitea.workspace }} + run: | + chmod +x scripts/build_busybox.sh + ./scripts/build_busybox.sh "${{ steps.meta.outputs.version }}" "$PWD/dist" + + - name: Publish release assets + shell: sh + env: + GITEA_TOKEN: ${{ gitea.token }} + GITEA_API_URL: ${{ gitea.api_url }} + REPO_OWNER: ${{ gitea.repository_owner }} + REPO_NAME: static-musl-builds + TOOL: busybox + VERSION: ${{ steps.meta.outputs.version }} + TARGET_SHA: ${{ gitea.sha }} + working-directory: ${{ gitea.workspace }} + run: | + apk add --no-cache python3 + python3 scripts/publish_release.py "dist/${{ steps.meta.outputs.artifact }}" "dist/${{ steps.meta.outputs.artifact }}.tar.gz" "dist/${{ steps.meta.outputs.artifact }}.sha256" diff --git a/.gitea/workflows/doggo.yml b/.gitea/workflows/doggo.yml new file mode 100644 index 0000000..fa9c2e7 --- /dev/null +++ b/.gitea/workflows/doggo.yml @@ -0,0 +1,68 @@ +name: doggo static musl build + +on: + push: + branches: + - main + paths: + - .gitea/workflows/doggo.yml + - scripts/build_doggo.sh + - scripts/publish_release.py + - versions/doggo.version + workflow_dispatch: + +permissions: + contents: write + +jobs: + build-and-release: + runs-on: alpine-latest + + steps: + - name: Bootstrap workspace + shell: sh + env: + REPO_URL: ${{ gitea.server_url }} + REPO_NAME: ${{ gitea.repository }} + GIT_SHA: ${{ gitea.sha }} + run: | + apk add --no-cache git + workdir="$(pwd)" + tmpdir="$(mktemp -d)" + git clone --depth 1 "$REPO_URL/$REPO_NAME.git" "$tmpdir/repo" + git -C "$tmpdir/repo" fetch --depth 1 origin "$GIT_SHA" + git -C "$tmpdir/repo" checkout "$GIT_SHA" + find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} + + cp -a "$tmpdir/repo"/. "$workdir"/ + + - name: Read version + id: meta + shell: sh + working-directory: ${{ gitea.workspace }} + run: | + version="$(tr -d ' \n\r' < versions/doggo.version)" + artifact="doggo-${version}-linux-amd64-musl-static" + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "artifact=${artifact}" >> "$GITHUB_OUTPUT" + + - name: Build static doggo + shell: sh + working-directory: ${{ gitea.workspace }} + run: | + chmod +x scripts/build_doggo.sh + ./scripts/build_doggo.sh "${{ steps.meta.outputs.version }}" "$PWD/dist" + + - name: Publish release assets + shell: sh + env: + GITEA_TOKEN: ${{ gitea.token }} + GITEA_API_URL: ${{ gitea.api_url }} + REPO_OWNER: ${{ gitea.repository_owner }} + REPO_NAME: static-musl-builds + TOOL: doggo + VERSION: ${{ steps.meta.outputs.version }} + TARGET_SHA: ${{ gitea.sha }} + working-directory: ${{ gitea.workspace }} + run: | + apk add --no-cache python3 + python3 scripts/publish_release.py "dist/${{ steps.meta.outputs.artifact }}" "dist/${{ steps.meta.outputs.artifact }}.tar.gz" "dist/${{ steps.meta.outputs.artifact }}.sha256" diff --git a/.gitea/workflows/gdu.yml b/.gitea/workflows/gdu.yml index 07875a2..54aae08 100644 --- a/.gitea/workflows/gdu.yml +++ b/.gitea/workflows/gdu.yml @@ -7,6 +7,7 @@ on: paths: - .gitea/workflows/gdu.yml - scripts/build_gdu.sh + - scripts/publish_release.py - versions/gdu.version workflow_dispatch: diff --git a/.gitea/workflows/speedtest-go.yml b/.gitea/workflows/speedtest-go.yml new file mode 100644 index 0000000..7852dee --- /dev/null +++ b/.gitea/workflows/speedtest-go.yml @@ -0,0 +1,68 @@ +name: speedtest-go static musl build + +on: + push: + branches: + - main + paths: + - .gitea/workflows/speedtest-go.yml + - scripts/build_speedtest-go.sh + - scripts/publish_release.py + - versions/speedtest-go.version + workflow_dispatch: + +permissions: + contents: write + +jobs: + build-and-release: + runs-on: alpine-latest + + steps: + - name: Bootstrap workspace + shell: sh + env: + REPO_URL: ${{ gitea.server_url }} + REPO_NAME: ${{ gitea.repository }} + GIT_SHA: ${{ gitea.sha }} + run: | + apk add --no-cache git + workdir="$(pwd)" + tmpdir="$(mktemp -d)" + git clone --depth 1 "$REPO_URL/$REPO_NAME.git" "$tmpdir/repo" + git -C "$tmpdir/repo" fetch --depth 1 origin "$GIT_SHA" + git -C "$tmpdir/repo" checkout "$GIT_SHA" + find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} + + cp -a "$tmpdir/repo"/. "$workdir"/ + + - name: Read version + id: meta + shell: sh + working-directory: ${{ gitea.workspace }} + run: | + version="$(tr -d ' \n\r' < versions/speedtest-go.version)" + artifact="speedtest-go-${version}-linux-amd64-musl-static" + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "artifact=${artifact}" >> "$GITHUB_OUTPUT" + + - name: Build static speedtest-go + shell: sh + working-directory: ${{ gitea.workspace }} + run: | + chmod +x scripts/build_speedtest-go.sh + ./scripts/build_speedtest-go.sh "${{ steps.meta.outputs.version }}" "$PWD/dist" + + - name: Publish release assets + shell: sh + env: + GITEA_TOKEN: ${{ gitea.token }} + GITEA_API_URL: ${{ gitea.api_url }} + REPO_OWNER: ${{ gitea.repository_owner }} + REPO_NAME: static-musl-builds + TOOL: speedtest-go + VERSION: ${{ steps.meta.outputs.version }} + TARGET_SHA: ${{ gitea.sha }} + working-directory: ${{ gitea.workspace }} + run: | + apk add --no-cache python3 + python3 scripts/publish_release.py "dist/${{ steps.meta.outputs.artifact }}" "dist/${{ steps.meta.outputs.artifact }}.tar.gz" "dist/${{ steps.meta.outputs.artifact }}.sha256" diff --git a/README.md b/README.md index 1b3cfcf..5efde73 100644 --- a/README.md +++ b/README.md @@ -2,50 +2,16 @@ Automated static `musl` builds for portable Linux binaries. -Current target: -- `iperf3` (x86_64 / amd64) +## Tools +- `iperf3` +- `gdu` +- `speedtest-go` +- `doggo` +- `busybox` -## What this repo does +Each release publishes: +- the raw binary +- a `.tar.gz` +- a `.sha256` -- Tracks a pinned upstream version in `versions/` -- Builds a fully static `musl` binary with Gitea Actions -- Publishes versioned release assets to Gitea Releases -- Is designed to grow with more binaries later - -## Current output - -For each `iperf3` release build, the workflow publishes: -- `iperf3--linux-amd64-musl-static` -- `iperf3--linux-amd64-musl-static.tar.gz` -- `iperf3--linux-amd64-musl-static.sha256` - -## Repo layout - -- `versions/iperf3.version` — tracked upstream version -- `scripts/build_iperf3.sh` — static build script -- `scripts/sync_iperf3_version.py` — checks upstream and updates pinned version -- `scripts/publish_release.py` — idempotent release asset publisher for Gitea -- `.gitea/workflows/iperf3.yml` — CI pipeline - -## How updates happen - -1. `scripts/sync_iperf3_version.py` checks the latest upstream iperf3 release. -2. If a newer version exists, it updates `versions/iperf3.version`. -3. A commit is pushed. -4. Gitea Actions builds the static binary and uploads release assets. - -## Manual local update example - -```bash -python3 scripts/sync_iperf3_version.py --update-file -if ! git diff --quiet -- versions/iperf3.version; then - git add versions/iperf3.version - git commit -m "chore(iperf3): bump to $(cat versions/iperf3.version)" - git push -fi -``` - -## Notes - -- The current workflow targets `amd64` first because that is the most broadly useful Linux target. -- The structure is intentionally generic so more binaries can be added later without reworking the repo. +Versions are pinned in `versions/*.version` and refreshed by cron. Matching Gitea Actions workflows build and publish updated releases automatically. diff --git a/scripts/build_busybox.sh b/scripts/build_busybox.sh new file mode 100755 index 0000000..cc68867 --- /dev/null +++ b/scripts/build_busybox.sh @@ -0,0 +1,60 @@ +#!/bin/sh +set -eu + +VERSION="${1:?usage: build_busybox.sh [output_dir]}" +OUTPUT_DIR="${2:-$PWD/dist}" +PREFIX_NAME="busybox-${VERSION}-linux-amd64-musl-static" + +mkdir -p "$OUTPUT_DIR" + +apk add --no-cache build-base bzip2 curl file tar xz + +SRCDIR="${TMPDIR:-/tmp}/busybox-src-${VERSION}" +rm -rf "$SRCDIR" +mkdir -p "$SRCDIR" + +curl -fsSL "https://busybox.net/downloads/busybox-${VERSION}.tar.bz2" | tar -xj -C "$SRCDIR" --strip-components=1 + +cd "$SRCDIR" +make defconfig >/dev/null + +set_kconfig() { + key="$1" + value="$2" + if grep -q "^${key}=" .config; then + sed -i "s#^${key}=.*#${key}=${value}#" .config + elif grep -q "^# ${key} is not set$" .config; then + sed -i "s|^# ${key} is not set$|${key}=${value}|" .config + else + echo "${key}=${value}" >> .config + fi +} + +set_kconfig CONFIG_STATIC y +set_kconfig CONFIG_FEATURE_SEAMLESS_XZ y +set_kconfig CONFIG_FEATURE_SEAMLESS_BZ2 y +set_kconfig CONFIG_UNXZ y +set_kconfig CONFIG_XZ y +set_kconfig CONFIG_BZIP2 y +set_kconfig CONFIG_UNZIP y + +yes '' | make oldconfig >/dev/null +make -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 2)" busybox +cp busybox "$OUTPUT_DIR/$PREFIX_NAME" +strip "$OUTPUT_DIR/$PREFIX_NAME" 2>/dev/null || true + +( + cd "$OUTPUT_DIR" + tar -czf "${PREFIX_NAME}.tar.gz" "$PREFIX_NAME" + sha256sum "$PREFIX_NAME" "${PREFIX_NAME}.tar.gz" > "${PREFIX_NAME}.sha256" +) + +file "$OUTPUT_DIR/$PREFIX_NAME" +ldd "$OUTPUT_DIR/$PREFIX_NAME" || true + +if file "$OUTPUT_DIR/$PREFIX_NAME" | grep -qi 'dynamically linked'; then + echo "error: output binary is still dynamically linked" >&2 + exit 1 +fi + +echo "build OK: $OUTPUT_DIR/$PREFIX_NAME" diff --git a/scripts/build_doggo.sh b/scripts/build_doggo.sh new file mode 100755 index 0000000..6045fa4 --- /dev/null +++ b/scripts/build_doggo.sh @@ -0,0 +1,38 @@ +#!/bin/sh +set -eu + +VERSION="${1:?usage: build_doggo.sh [output_dir]}" +OUTPUT_DIR="${2:-$PWD/dist}" +VERSION="${VERSION#v}" +PREFIX_NAME="doggo-${VERSION}-linux-amd64-musl-static" + +mkdir -p "$OUTPUT_DIR" + +apk add --no-cache curl file go git tar xz + +SRCDIR="${TMPDIR:-/tmp}/doggo-src-${VERSION}" +rm -rf "$SRCDIR" +mkdir -p "$SRCDIR" + +curl -fsSL "https://github.com/mr-karan/doggo/archive/refs/tags/v${VERSION}.tar.gz" | tar -xz -C "$SRCDIR" --strip-components=1 + +cd "$SRCDIR" +CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -trimpath -ldflags="-s -w -extldflags=-static" -o "$OUTPUT_DIR/$PREFIX_NAME" ./cmd/doggo + +strip "$OUTPUT_DIR/$PREFIX_NAME" 2>/dev/null || true + +( + cd "$OUTPUT_DIR" + tar -czf "${PREFIX_NAME}.tar.gz" "$PREFIX_NAME" + sha256sum "$PREFIX_NAME" "${PREFIX_NAME}.tar.gz" > "${PREFIX_NAME}.sha256" +) + +file "$OUTPUT_DIR/$PREFIX_NAME" +ldd "$OUTPUT_DIR/$PREFIX_NAME" || true + +if file "$OUTPUT_DIR/$PREFIX_NAME" | grep -qi 'dynamically linked'; then + echo "error: output binary is still dynamically linked" >&2 + exit 1 +fi + +echo "build OK: $OUTPUT_DIR/$PREFIX_NAME" diff --git a/scripts/build_speedtest-go.sh b/scripts/build_speedtest-go.sh new file mode 100755 index 0000000..d0c3bc7 --- /dev/null +++ b/scripts/build_speedtest-go.sh @@ -0,0 +1,38 @@ +#!/bin/sh +set -eu + +VERSION="${1:?usage: build_speedtest-go.sh [output_dir]}" +OUTPUT_DIR="${2:-$PWD/dist}" +VERSION="${VERSION#v}" +PREFIX_NAME="speedtest-go-${VERSION}-linux-amd64-musl-static" + +mkdir -p "$OUTPUT_DIR" + +apk add --no-cache curl file go git tar xz + +SRCDIR="${TMPDIR:-/tmp}/speedtest-go-src-${VERSION}" +rm -rf "$SRCDIR" +mkdir -p "$SRCDIR" + +curl -fsSL "https://github.com/showwin/speedtest-go/archive/refs/tags/v${VERSION}.tar.gz" | tar -xz -C "$SRCDIR" --strip-components=1 + +cd "$SRCDIR" +CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -trimpath -ldflags="-s -w -extldflags=-static" -o "$OUTPUT_DIR/$PREFIX_NAME" . + +strip "$OUTPUT_DIR/$PREFIX_NAME" 2>/dev/null || true + +( + cd "$OUTPUT_DIR" + tar -czf "${PREFIX_NAME}.tar.gz" "$PREFIX_NAME" + sha256sum "$PREFIX_NAME" "${PREFIX_NAME}.tar.gz" > "${PREFIX_NAME}.sha256" +) + +file "$OUTPUT_DIR/$PREFIX_NAME" +ldd "$OUTPUT_DIR/$PREFIX_NAME" || true + +if file "$OUTPUT_DIR/$PREFIX_NAME" | grep -qi 'dynamically linked'; then + echo "error: output binary is still dynamically linked" >&2 + exit 1 +fi + +echo "build OK: $OUTPUT_DIR/$PREFIX_NAME" diff --git a/scripts/sync_busybox_version.py b/scripts/sync_busybox_version.py new file mode 100755 index 0000000..e1c4fc5 --- /dev/null +++ b/scripts/sync_busybox_version.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +import argparse +import re +import sys +import urllib.request +from pathlib import Path + +DOWNLOADS_URL = "https://busybox.net/downloads/" +VERSION_RE = re.compile(r"busybox-([0-9]+(?:\.[0-9]+)+)\.tar\.bz2") + + +def version_key(version: str): + return tuple(int(part) for part in version.split(".")) + + +def fetch_latest_version() -> str: + req = urllib.request.Request( + DOWNLOADS_URL, + headers={"User-Agent": "static-musl-builds-updater"}, + ) + with urllib.request.urlopen(req, timeout=30) as resp: + html = resp.read().decode("utf-8", "replace") + versions = sorted(set(VERSION_RE.findall(html)), key=version_key) + if not versions: + raise RuntimeError("No BusyBox versions found on downloads page") + return versions[-1] + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--version-file", required=True, type=Path) + parser.add_argument("--update-file", action="store_true") + args = parser.parse_args() + + latest = fetch_latest_version() + current = args.version_file.read_text(encoding="utf-8").strip() if args.version_file.exists() else "" + + print(f"current={current or ''}") + print(f"latest={latest}") + + if current == latest: + print("status=up-to-date") + return 0 + + print("status=update-available") + if args.update_file: + args.version_file.parent.mkdir(parents=True, exist_ok=True) + args.version_file.write_text(latest + "\n", encoding="utf-8") + print(f"updated_file={args.version_file}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/sync_github_release.py b/scripts/sync_github_release.py new file mode 100755 index 0000000..e04a798 --- /dev/null +++ b/scripts/sync_github_release.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +import argparse +import json +import re +import sys +import urllib.request +from pathlib import Path + +VERSION_RE = re.compile(r"[0-9]+(?:\.[0-9]+)*") + + +def fetch_latest_tag(repo: str) -> str: + url = f"https://api.github.com/repos/{repo}/releases/latest" + req = urllib.request.Request( + url, + headers={ + "Accept": "application/vnd.github+json", + "User-Agent": "static-musl-builds-updater", + }, + ) + with urllib.request.urlopen(req, timeout=30) as resp: + data = json.load(resp) + return data["tag_name"].strip() + + +def normalize_version(tag: str, strip_prefix: str) -> str: + version = tag + if strip_prefix and version.startswith(strip_prefix): + version = version[len(strip_prefix):] + version = version.strip() + if not VERSION_RE.fullmatch(version): + raise ValueError(f"Unexpected version format: {tag!r} -> {version!r}") + return version + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--repo", required=True, help="owner/repo on GitHub") + parser.add_argument("--version-file", required=True, type=Path) + parser.add_argument("--strip-prefix", default="v") + parser.add_argument("--update-file", action="store_true") + args = parser.parse_args() + + latest_tag = fetch_latest_tag(args.repo) + latest = normalize_version(latest_tag, args.strip_prefix) + current = args.version_file.read_text(encoding="utf-8").strip() if args.version_file.exists() else "" + + print(f"repo={args.repo}") + print(f"current={current or ''}") + print(f"latest={latest}") + + if current == latest: + print("status=up-to-date") + return 0 + + print("status=update-available") + if args.update_file: + args.version_file.parent.mkdir(parents=True, exist_ok=True) + args.version_file.write_text(latest + "\n", encoding="utf-8") + print(f"updated_file={args.version_file}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/versions/busybox.version b/versions/busybox.version new file mode 100644 index 0000000..bf50e91 --- /dev/null +++ b/versions/busybox.version @@ -0,0 +1 @@ +1.37.0 diff --git a/versions/doggo.version b/versions/doggo.version new file mode 100644 index 0000000..e25d8d9 --- /dev/null +++ b/versions/doggo.version @@ -0,0 +1 @@ +1.1.5 diff --git a/versions/speedtest-go.version b/versions/speedtest-go.version new file mode 100644 index 0000000..a412349 --- /dev/null +++ b/versions/speedtest-go.version @@ -0,0 +1 @@ +1.7.10