feat: bootstrap static musl build pipeline for iperf3
iperf3 static musl build / build-and-release (push) Failing after 16s
iperf3 static musl build / build-and-release (push) Failing after 16s
This commit is contained in:
@@ -0,0 +1,55 @@
|
|||||||
|
name: iperf3 static musl build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- .gitea/workflows/iperf3.yml
|
||||||
|
- scripts/**
|
||||||
|
- versions/iperf3.version
|
||||||
|
- README.md
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: alpine:3.22
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Read version
|
||||||
|
id: meta
|
||||||
|
shell: sh
|
||||||
|
run: |
|
||||||
|
version="$(tr -d ' \n\r' < versions/iperf3.version)"
|
||||||
|
artifact="iperf3-${version}-linux-amd64-musl-static"
|
||||||
|
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "artifact=${artifact}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Build static iperf3
|
||||||
|
shell: sh
|
||||||
|
run: |
|
||||||
|
chmod +x scripts/build_iperf3.sh
|
||||||
|
./scripts/build_iperf3.sh "${{ steps.meta.outputs.version }}" "$PWD/dist"
|
||||||
|
|
||||||
|
- name: Publish release assets
|
||||||
|
shell: sh
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
GITEA_API_URL: ${{ gitea.api_url }}
|
||||||
|
REPO_OWNER: ${{ gitea.repository_owner }}
|
||||||
|
REPO_NAME: static-musl-builds
|
||||||
|
VERSION: ${{ steps.meta.outputs.version }}
|
||||||
|
TARGET_SHA: ${{ gitea.sha }}
|
||||||
|
run: |
|
||||||
|
python3 scripts/publish_release.py \
|
||||||
|
"dist/${{ steps.meta.outputs.artifact }}" \
|
||||||
|
"dist/${{ steps.meta.outputs.artifact }}.tar.gz" \
|
||||||
|
"dist/${{ steps.meta.outputs.artifact }}.sha256"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
dist/
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# static-musl-builds
|
||||||
|
|
||||||
|
Automated static `musl` builds for portable Linux binaries.
|
||||||
|
|
||||||
|
Current target:
|
||||||
|
- `iperf3` (x86_64 / amd64)
|
||||||
|
|
||||||
|
## What this repo does
|
||||||
|
|
||||||
|
- 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-<version>-linux-amd64-musl-static`
|
||||||
|
- `iperf3-<version>-linux-amd64-musl-static.tar.gz`
|
||||||
|
- `iperf3-<version>-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.
|
||||||
Executable
+48
@@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
VERSION="${1:?usage: build_iperf3.sh <version> [output_dir]}"
|
||||||
|
OUTPUT_DIR="${2:-$PWD/dist}"
|
||||||
|
WORKDIR="${TMPDIR:-/tmp}/iperf3-build-${VERSION}"
|
||||||
|
PREFIX_NAME="iperf3-${VERSION}-linux-amd64-musl-static"
|
||||||
|
|
||||||
|
rm -rf "$WORKDIR"
|
||||||
|
mkdir -p "$WORKDIR" "$OUTPUT_DIR"
|
||||||
|
cd "$WORKDIR"
|
||||||
|
|
||||||
|
apk add --no-cache \
|
||||||
|
bash \
|
||||||
|
build-base \
|
||||||
|
curl \
|
||||||
|
file \
|
||||||
|
linux-headers \
|
||||||
|
openssl-dev \
|
||||||
|
tar \
|
||||||
|
xz \
|
||||||
|
zlib-dev
|
||||||
|
|
||||||
|
curl -fsSLO "https://downloads.es.net/pub/iperf/iperf-${VERSION}.tar.gz"
|
||||||
|
tar -xzf "iperf-${VERSION}.tar.gz"
|
||||||
|
cd "iperf-${VERSION}"
|
||||||
|
|
||||||
|
export CFLAGS="-O2 -static"
|
||||||
|
export CPPFLAGS=""
|
||||||
|
export LDFLAGS="-static"
|
||||||
|
|
||||||
|
./configure \
|
||||||
|
--disable-shared \
|
||||||
|
--enable-static \
|
||||||
|
--without-sctp
|
||||||
|
|
||||||
|
make -j"$(getconf _NPROCESSORS_ONLN || echo 2)"
|
||||||
|
strip src/iperf3
|
||||||
|
|
||||||
|
install -m 0755 src/iperf3 "$OUTPUT_DIR/$PREFIX_NAME"
|
||||||
|
(
|
||||||
|
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
|
||||||
Executable
+112
@@ -0,0 +1,112 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def api_request(method: str, url: str, token: str, data: bytes | None = None, headers: dict | None = None):
|
||||||
|
req_headers = {
|
||||||
|
"Authorization": f"token {token}",
|
||||||
|
"Accept": "application/json",
|
||||||
|
**(headers or {}),
|
||||||
|
}
|
||||||
|
req = urllib.request.Request(url, data=data, headers=req_headers, method=method)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as resp:
|
||||||
|
body = resp.read()
|
||||||
|
if not body:
|
||||||
|
return None
|
||||||
|
return json.loads(body)
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
body = exc.read().decode("utf-8", "ignore")
|
||||||
|
raise RuntimeError(f"HTTP {exc.code} for {method} {url}: {body}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_release(api_base: str, owner: str, repo: str, tag: str, title: str, body: str, token: str, target: str):
|
||||||
|
tag_url = f"{api_base}/repos/{owner}/{repo}/releases/tags/{urllib.parse.quote(tag, safe='')}"
|
||||||
|
try:
|
||||||
|
release = api_request("GET", tag_url, token)
|
||||||
|
if release is not None:
|
||||||
|
return release
|
||||||
|
except RuntimeError as exc:
|
||||||
|
if "HTTP 404" not in str(exc):
|
||||||
|
raise
|
||||||
|
|
||||||
|
create_url = f"{api_base}/repos/{owner}/{repo}/releases"
|
||||||
|
payload = {
|
||||||
|
"tag_name": tag,
|
||||||
|
"name": title,
|
||||||
|
"body": body,
|
||||||
|
"draft": False,
|
||||||
|
"prerelease": False,
|
||||||
|
"target_commitish": target,
|
||||||
|
}
|
||||||
|
return api_request(
|
||||||
|
"POST",
|
||||||
|
create_url,
|
||||||
|
token,
|
||||||
|
data=json.dumps(payload).encode("utf-8"),
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def replace_release_assets(api_base: str, owner: str, repo: str, release: dict, files: list[Path], token: str):
|
||||||
|
release_id = release["id"]
|
||||||
|
existing_assets = {asset["name"]: asset for asset in release.get("assets", [])}
|
||||||
|
|
||||||
|
for path in files:
|
||||||
|
name = path.name
|
||||||
|
if name in existing_assets:
|
||||||
|
asset_id = existing_assets[name]["id"]
|
||||||
|
delete_url = f"{api_base}/repos/{owner}/{repo}/releases/{release_id}/assets/{asset_id}"
|
||||||
|
api_request("DELETE", delete_url, token)
|
||||||
|
|
||||||
|
upload_url = f"{api_base}/repos/{owner}/{repo}/releases/{release_id}/assets?name={urllib.parse.quote(name, safe='')}"
|
||||||
|
content_type = mimetypes.guess_type(str(path))[0] or "application/octet-stream"
|
||||||
|
data = path.read_bytes()
|
||||||
|
api_request(
|
||||||
|
"POST",
|
||||||
|
upload_url,
|
||||||
|
token,
|
||||||
|
data=data,
|
||||||
|
headers={"Content-Type": content_type},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
token = os.environ["GITEA_TOKEN"]
|
||||||
|
api_base = os.environ["GITEA_API_URL"].rstrip("/")
|
||||||
|
owner = os.environ["REPO_OWNER"]
|
||||||
|
repo = os.environ["REPO_NAME"]
|
||||||
|
version = os.environ["VERSION"]
|
||||||
|
target = os.environ.get("TARGET_SHA", "main")
|
||||||
|
files = [Path(p) for p in sys.argv[1:]]
|
||||||
|
if not files:
|
||||||
|
raise SystemExit("usage: publish_release.py <file> [file ...]")
|
||||||
|
missing = [str(p) for p in files if not p.exists()]
|
||||||
|
if missing:
|
||||||
|
raise SystemExit(f"missing files: {', '.join(missing)}")
|
||||||
|
|
||||||
|
tag = f"iperf3-v{version}"
|
||||||
|
title = f"iperf3 {version}"
|
||||||
|
body = (
|
||||||
|
f"Static musl build for iperf3 {version}.\\n\\n"
|
||||||
|
f"Repository: {owner}/{repo}\\n"
|
||||||
|
f"Commit: {target}"
|
||||||
|
)
|
||||||
|
|
||||||
|
release = get_or_create_release(api_base, owner, repo, tag, title, body, token, target)
|
||||||
|
replace_release_assets(api_base, owner, repo, release, files, token)
|
||||||
|
print(f"published_tag={tag}")
|
||||||
|
for path in files:
|
||||||
|
print(f"asset={path.name}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Executable
+54
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
VERSION_FILE = Path("versions/iperf3.version")
|
||||||
|
LATEST_RELEASE_URL = "https://api.github.com/repos/esnet/iperf/releases/latest"
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_latest_version() -> str:
|
||||||
|
req = urllib.request.Request(
|
||||||
|
LATEST_RELEASE_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)
|
||||||
|
tag = data["tag_name"].strip()
|
||||||
|
version = re.sub(r"^v", "", tag)
|
||||||
|
if not re.fullmatch(r"[0-9]+(?:\.[0-9]+)*", version):
|
||||||
|
raise ValueError(f"Unexpected iperf3 version format: {version!r}")
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--update-file", action="store_true", help="Write latest version into versions/iperf3.version if changed")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
latest = fetch_latest_version()
|
||||||
|
current = VERSION_FILE.read_text(encoding="utf-8").strip() if VERSION_FILE.exists() else ""
|
||||||
|
|
||||||
|
print(f"current={current or '<missing>'}")
|
||||||
|
print(f"latest={latest}")
|
||||||
|
|
||||||
|
if current == latest:
|
||||||
|
print("status=up-to-date")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print("status=update-available")
|
||||||
|
if args.update_file:
|
||||||
|
VERSION_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
VERSION_FILE.write_text(latest + "\n", encoding="utf-8")
|
||||||
|
print(f"updated_file={VERSION_FILE}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
3.21
|
||||||
Reference in New Issue
Block a user