feat: initial commit — void-installer multi-profile (stable-cinnamon + mainline-niri)

This commit is contained in:
mozempk
2026-04-22 23:53:16 +02:00
commit a16ac37d20
35 changed files with 3902 additions and 0 deletions

35
tools/start-xbps-proxy.sh Executable file
View File

@@ -0,0 +1,35 @@
#!/bin/bash
# Start the XBPS caching proxy in the background.
# Idempotent: does nothing if already running.
set -Eeuo pipefail
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CACHE_DIR="${XBPS_CACHE_DIR:-$PROJECT_DIR/cache/xbps-pkgs}"
PORT="${XBPS_PROXY_PORT:-3142}"
PIDFILE="${XBPS_PROXY_PIDFILE:-$PROJECT_DIR/out/xbps-proxy.pid}"
LOGFILE="${XBPS_PROXY_LOG:-$PROJECT_DIR/out/xbps-proxy.log}"
mkdir -p "$(dirname "$PIDFILE")" "$CACHE_DIR"
# Already running?
if [[ -f "$PIDFILE" ]] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
echo ">>> XBPS proxy already running (pid $(cat "$PIDFILE"), port $PORT)"
exit 0
fi
XBPS_CACHE_DIR="$CACHE_DIR" \
XBPS_PROXY_PORT="$PORT" \
python3 "$PROJECT_DIR/tools/xbps-proxy.py" > "$LOGFILE" 2>&1 &
echo $! > "$PIDFILE"
# Wait for the socket to open (up to 5 s).
for i in $(seq 1 20); do
if curl -sf --max-time 1 "http://127.0.0.1:$PORT/" >/dev/null 2>&1; then
break
fi
sleep 0.25
done
echo ">>> XBPS proxy started (pid $(cat "$PIDFILE"), port $PORT)"
echo " cache : $CACHE_DIR"
echo " log : $LOGFILE"

19
tools/stop-xbps-proxy.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/bash
# Stop the XBPS caching proxy.
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PIDFILE="${XBPS_PROXY_PIDFILE:-$PROJECT_DIR/out/xbps-proxy.pid}"
if [[ -f "$PIDFILE" ]]; then
PID="$(cat "$PIDFILE")"
if kill -0 "$PID" 2>/dev/null; then
kill "$PID"
echo ">>> XBPS proxy stopped (pid $PID)"
else
echo ">>> XBPS proxy not running (stale pidfile)"
fi
rm -f "$PIDFILE"
else
echo ">>> no XBPS proxy pidfile found"
fi

160
tools/xbps-proxy.py Normal file
View File

@@ -0,0 +1,160 @@
#!/usr/bin/env python3
"""
XBPS caching HTTP reverse proxy.
Cache-first, web fallback. Caches .xbps packages forever; never caches
-repodata files (must stay fresh for xbps to see new package versions).
Usage:
XBPS_CACHE_DIR=./cache/xbps-pkgs \\
XBPS_UPSTREAM=https://repo-default.voidlinux.org \\
XBPS_PROXY_PORT=3142 \\
python3 tools/xbps-proxy.py
"""
import http.server
import socket
import time
import urllib.request
import urllib.error
import os
import sys
from pathlib import Path
# Force IPv4 only — many hosts have no working IPv6 route to voidlinux mirrors,
# which surfaces as `[Errno 101] Network is unreachable` mid-download. Pin
# socket.getaddrinfo to AF_INET so urllib never tries an AAAA record.
_orig_getaddrinfo = socket.getaddrinfo
def _ipv4_only(host, port, family=0, type=0, proto=0, flags=0): # noqa: A002
return _orig_getaddrinfo(host, port, socket.AF_INET, type, proto, flags)
socket.getaddrinfo = _ipv4_only
UPSTREAM = os.environ.get("XBPS_UPSTREAM", "https://repo-default.voidlinux.org")
CACHE_DIR = Path(os.environ.get("XBPS_CACHE_DIR", "./cache/xbps-pkgs")).resolve()
PORT = int(os.environ.get("XBPS_PROXY_PORT", "3142"))
BIND = os.environ.get("XBPS_PROXY_BIND", "0.0.0.0")
# Suffixes that must always be fetched fresh (small metadata files).
ALWAYS_FRESH = ("-repodata",)
CACHE_DIR.mkdir(parents=True, exist_ok=True)
def should_cache(path: str) -> bool:
for suffix in ALWAYS_FRESH:
if path.endswith(suffix):
return False
return True
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self._handle(head_only=False)
def do_HEAD(self):
self._handle(head_only=True)
def _handle(self, head_only: bool):
path = self.path.lstrip("/")
if not path:
self.send_error(404, "empty path")
return
fpath = CACHE_DIR / path
hit = should_cache(path) and fpath.is_file()
if hit:
self._serve_cached(fpath, head_only)
print(f"[HIT ] {path}", flush=True)
return
upstream_url = f"{UPSTREAM}/{path}"
print(f"[MISS] {path} <- {upstream_url}", flush=True)
last_exc = None
for attempt in range(3):
try:
self._fetch_upstream(path, fpath, upstream_url, head_only)
return
except urllib.error.HTTPError as exc:
print(f"[ERR ] {path} HTTP {exc.code}", flush=True)
try:
self.send_error(exc.code, exc.reason)
except Exception: # noqa: BLE001
pass
return
except (urllib.error.URLError, OSError) as exc:
last_exc = exc
if attempt < 2:
print(f"[WARN] {path} attempt {attempt + 1} failed: {exc}; retrying", flush=True)
time.sleep(1 + attempt)
print(f"[ERR ] {path} {last_exc}", flush=True)
try:
self.send_error(503, str(last_exc))
except Exception: # noqa: BLE001
pass
def _fetch_upstream(self, path, fpath, upstream_url, head_only):
req = urllib.request.Request(upstream_url)
req.get_method = lambda: "HEAD" if head_only else "GET"
with urllib.request.urlopen(req, timeout=120) as resp:
ctype = resp.headers.get("Content-Type", "application/octet-stream")
clen = resp.headers.get("Content-Length")
self.send_response(200)
self.send_header("Content-Type", ctype)
if clen:
self.send_header("Content-Length", clen)
self.end_headers()
if head_only:
return
cache_this = should_cache(path)
tmp = None
tmp_fh = None
if cache_this:
fpath.parent.mkdir(parents=True, exist_ok=True)
tmp = fpath.with_suffix(fpath.suffix + ".tmp")
tmp_fh = open(tmp, "wb")
try:
while True:
chunk = resp.read(64 * 1024)
if not chunk:
break
self.wfile.write(chunk)
if tmp_fh is not None:
tmp_fh.write(chunk)
finally:
if tmp_fh is not None:
tmp_fh.close()
if cache_this and tmp is not None and tmp.exists() and not fpath.is_dir():
tmp.rename(fpath)
def _serve_cached(self, fpath: Path, head_only: bool):
size = fpath.stat().st_size
self.send_response(200)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Content-Length", str(size))
self.end_headers()
if head_only:
return
with open(fpath, "rb") as fh:
while True:
chunk = fh.read(64 * 1024)
if not chunk:
break
self.wfile.write(chunk)
# Silence default access log — we do our own.
def log_message(self, *_):
pass
if __name__ == "__main__":
server = http.server.ThreadingHTTPServer((BIND, PORT), Handler)
print(f"XBPS proxy : {BIND}:{PORT} -> {UPSTREAM}", flush=True)
print(f"Cache dir : {CACHE_DIR}", flush=True)
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nProxy stopped.", flush=True)