Retire legacy implementations and flatten managed layout
Build and test / test (macos-latest) (push) Canceled after 0s
Build and test / test (ubuntu-24.04) (push) Canceled after 0s
Build and test / test (windows-latest) (push) Canceled after 0s
Build and test / apple-client (push) Canceled after 0s

This commit is contained in:
2026-09-21 00:11:32 +02:00
parent dd811a0bb8
commit 08e6c5930a
422 changed files with 252 additions and 38242 deletions
-175
View File
@@ -1,175 +0,0 @@
#!/usr/bin/env python3
"""asc_api.py — minimal App Store Connect API helper for ad-hoc device registration.
Used by scripts/dist-ios-adhoc.sh to register friends' device UDIDs before building an
ad-hoc IPA. Talks to the App Store Connect API directly: signs an ES256 JWT with your
Team Key .p8 (via the `cryptography` package — no PyJWT/requests needed) and calls the
REST endpoints with urllib from the standard library.
Subcommands:
register register one device UDID (idempotent — an already-registered UDID is OK)
list list all registered devices (and the count, against the 100/year cap)
Auth is the same for both, supplied via flags (the wrapper script passes them from the
ASC_KEY_ID / ASC_ISSUER_ID / ASC_KEY_PATH env vars):
--key-id the Key ID of the App Store Connect API key
--issuer-id the Issuer ID (Users and Access -> Integrations)
--key path to the AuthKey_XXXXXXXXXX.p8 file
Examples:
python3 scripts/asc_api.py list \
--key-id ABC123 --issuer-id 11111111-2222-... --key ~/.appstoreconnect/AuthKey_ABC123.p8
python3 scripts/asc_api.py register --udid 00008110-0011... --name "My iPhone" \
--key-id ABC123 --issuer-id 11111111-2222-... --key ~/.appstoreconnect/AuthKey_ABC123.p8
The .p8 is created at App Store Connect -> Users and Access -> Integrations ->
App Store Connect API -> Team Keys, with Admin or App Manager access. Keep it out of the
repo.
"""
import argparse
import base64
import json
import sys
import time
import urllib.error
import urllib.request
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, utils
API_BASE = "https://api.appstoreconnect.apple.com"
def _b64url(data: bytes) -> str:
"""base64url without padding, as JWT requires."""
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
def make_jwt(key_id: str, issuer_id: str, key_path: str) -> str:
"""Build a short-lived ES256 JWT for the App Store Connect API."""
with open(key_path, "rb") as fh:
private_key = serialization.load_pem_private_key(fh.read(), password=None)
if not isinstance(private_key, ec.EllipticCurvePrivateKey):
sys.exit(f"error: {key_path} is not an EC private key (.p8 from App Store Connect)")
now = int(time.time())
header = {"alg": "ES256", "kid": key_id, "typ": "JWT"}
# exp must be <= 20 minutes out; 10 minutes is comfortable.
payload = {"iss": issuer_id, "iat": now, "exp": now + 600, "aud": "appstoreconnect-v1"}
signing_input = f"{_b64url(json.dumps(header).encode())}.{_b64url(json.dumps(payload).encode())}"
der_sig = private_key.sign(signing_input.encode("ascii"), ec.ECDSA(hashes.SHA256()))
# JWS wants raw r||s (two 32-byte big-endian ints), not the ASN.1/DER openssl emits.
r, s = utils.decode_dss_signature(der_sig)
raw_sig = r.to_bytes(32, "big") + s.to_bytes(32, "big")
return f"{signing_input}.{_b64url(raw_sig)}"
def _request(method: str, path: str, token: str, body: dict | None = None):
"""Perform an authenticated API call. Returns (status_code, parsed_json|None)."""
url = path if path.startswith("http") else f"{API_BASE}{path}"
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(url, data=data, method=method)
req.add_header("Authorization", f"Bearer {token}")
if data is not None:
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req) as resp:
raw = resp.read()
return resp.status, (json.loads(raw) if raw else None)
except urllib.error.HTTPError as exc:
raw = exc.read()
try:
parsed = json.loads(raw) if raw else None
except json.JSONDecodeError:
parsed = {"_raw": raw.decode("utf-8", "replace")}
return exc.code, parsed
def _errors_text(payload) -> str:
if isinstance(payload, dict) and payload.get("errors"):
return "; ".join(
f"{e.get('title', '')}: {e.get('detail', '')}".strip(": ")
for e in payload["errors"]
)
return json.dumps(payload)
def cmd_register(args, token: str) -> int:
body = {
"data": {
"type": "devices",
"attributes": {
"name": args.name or args.udid,
"platform": "IOS",
"udid": args.udid,
},
}
}
status, payload = _request("POST", "/v1/devices", token, body)
if status in (200, 201):
print(f" registered: {args.udid} ({args.name or args.udid})")
return 0
# A UDID that already exists comes back as a 409 conflict, or a 422 with an error
# detail mentioning the device already exists. Either way it's fine — idempotent.
text = _errors_text(payload)
if status == 409 or "already exist" in text.lower() or "already been taken" in text.lower():
print(f" already registered: {args.udid}")
return 0
print(f"error: failed to register {args.udid} (HTTP {status}): {text}", file=sys.stderr)
return 1
def cmd_list(args, token: str) -> int:
path = "/v1/devices?limit=200&sort=name"
rows = []
while path:
status, payload = _request("GET", path, token)
if status != 200:
print(f"error: list failed (HTTP {status}): {_errors_text(payload)}", file=sys.stderr)
return 1
for d in payload.get("data", []):
a = d.get("attributes", {})
rows.append((a.get("platform", "?"), a.get("status", "?"),
a.get("udid", "?"), a.get("name", "")))
path = (payload.get("links") or {}).get("next")
ios = [r for r in rows if r[0] == "IOS"]
for platform, dev_status, udid, name in rows:
print(f" [{platform:7}] {dev_status:8} {udid} {name}")
print(f"\n {len(rows)} device(s) total, {len(ios)} iOS (cap is 100 iOS/membership year)")
return 0
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
sub = parser.add_subparsers(dest="command", required=True)
def add_auth(p):
p.add_argument("--key-id", required=True)
p.add_argument("--issuer-id", required=True)
p.add_argument("--key", required=True, help="path to AuthKey_*.p8")
p_reg = sub.add_parser("register", help="register one device UDID (idempotent)")
add_auth(p_reg)
p_reg.add_argument("--udid", required=True)
p_reg.add_argument("--name", default=None)
p_list = sub.add_parser("list", help="list registered devices")
add_auth(p_list)
args = parser.parse_args()
token = make_jwt(args.key_id, args.issuer_id, args.key)
if args.command == "register":
return cmd_register(args, token)
if args.command == "list":
return cmd_list(args, token)
return 2
if __name__ == "__main__":
sys.exit(main())
-76
View File
@@ -1,76 +0,0 @@
#!/usr/bin/env bash
#
# build-all.sh — build every artifact this host can produce and stage it under
# dist/. Convenience wrapper over the per-artifact scripts in scripts/.
#
# On macOS: server + libs (macOS slice + xcframework) + macos-client (.app)
# optionally: iOS client for simulator (pass --ios-client)
# On Windows: server + libs (windows DLL) + windows-client (.exe folder)
# On Linux: server only (no native client presets target Linux)
#
# Usage:
# scripts/build-all.sh # build everything host can, into ./dist
# scripts/build-all.sh --dist /out
# scripts/build-all.sh --no-configure # passed through to each step
# scripts/build-all.sh --skip-client # skip the GUI client(s)
# scripts/build-all.sh --skip-libs # skip the library artifacts
# scripts/build-all.sh --skip-server # skip the server
# scripts/build-all.sh --ios-client # also build iOS simulator client (macOS only)
# scripts/build-all.sh -h|--help
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VC_SCRIPT_NAME="build-all"
source "$SCRIPT_DIR/common.sh"
PASS_ARGS=()
SKIP_CLIENT=false
SKIP_LIBS=false
SKIP_SERVER=false
BUILD_IOS_CLIENT=false
while [[ $# -gt 0 ]]; do
case "$1" in
--dist) PASS_ARGS+=( --dist "$2" ); vc_set_dist "$2"; shift 2 ;;
--no-configure) PASS_ARGS+=( --no-configure ); shift ;;
--skip-client) SKIP_CLIENT=true; shift ;;
--skip-libs) SKIP_LIBS=true; shift ;;
--skip-server) SKIP_SERVER=true; shift ;;
--ios-client) BUILD_IOS_CLIENT=true; shift ;;
-h|--help) vc_print_help "$0"; exit 0 ;;
*) vc_die "unknown arg: $1 (try --help)" ;;
esac
done
HOST="$(vc_host_os)"
vc_log "host: $HOST dist: $VC_DIST_DIR"
run() {
local name="$1"; shift
vc_step "$name $*"
"$SCRIPT_DIR/$name" "$@" "${PASS_ARGS[@]}"
}
if ! $SKIP_SERVER; then
run build-server.sh
fi
if ! $SKIP_LIBS; then
if [[ "$HOST" == "macos" ]]; then
run build-libs.sh --platform macos
elif [[ "$HOST" == "windows" ]]; then
run build-libs.sh --platform windows
fi
fi
if ! $SKIP_CLIENT; then
if [[ "$HOST" == "macos" ]]; then
run build-macos-client.sh
$BUILD_IOS_CLIENT && run build-ios-client.sh
elif [[ "$HOST" == "windows" ]]; then
run build-windows-client.sh
fi
fi
vc_ok "all done -> $VC_DIST_DIR"
vc_log "contents:"
( cd "$VC_DIST_DIR" && find . -maxdepth 3 | sort | sed 's/^/ /' ) || true
-101
View File
@@ -1,101 +0,0 @@
#!/usr/bin/env bash
#
# build-ios-client.sh — build the iOS SwiftUI client (VoiceCatiOS.app) for the
# iOS simulator and stage it into dist/ios-client/.
#
# Two steps:
# 1. Build VoiceCatCore.xcframework with all 3 slices (macOS + iOS device +
# iOS simulator) via clients/apple/scripts/build-xcframework.sh --all.
# The XCFramework must include the ios-arm64-simulator slice so the Swift
# Package binary target resolves when xcodebuild compiles the iOS app.
# 2. xcodebuild the VoiceCatiOS target against the iOS Simulator SDK, with
# SYMROOT and OBJROOT pointed at the same directory so the Swift Package
# and the app target find each other's build products.
#
# Usage:
# scripts/build-ios-client.sh # Debug sim build into ./dist
# scripts/build-ios-client.sh --dist /out
# scripts/build-ios-client.sh --no-configure # skip cmake configure (xcframework step)
# scripts/build-ios-client.sh --config Debug # Xcode config (default Debug)
# scripts/build-ios-client.sh -h|--help
#
# Output:
# clients/apple/iOS/build/Debug-iphonesimulator/VoiceCatiOS.app
# dist/ios-client/VoiceCatiOS.app
#
# To install and launch on a simulator after building, use:
# scripts/run-ios-simulator.sh
#
# macOS only. Requires VCPKG_ROOT and Xcode (with iOS Simulator runtime installed).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VC_SCRIPT_NAME="build-ios-client"
source "$SCRIPT_DIR/common.sh"
DO_CONFIGURE=true
XCODE_CONFIG="Debug"
while [[ $# -gt 0 ]]; do
case "$1" in
--dist) vc_set_dist "$2"; shift 2 ;;
--no-configure) DO_CONFIGURE=false; shift ;;
--config) XCODE_CONFIG="$2"; shift 2 ;;
-h|--help) vc_print_help "$0"; exit 0 ;;
*) vc_die "unknown arg: $1 (try --help)" ;;
esac
done
vc_require_macos
vc_log "dist dir: $VC_DIST_DIR config: $XCODE_CONFIG"
# ── Step 1: XCFramework (all slices) ──────────────────────────────────────────
# The iOS simulator build requires the ios-arm64-simulator slice, so we always
# build all three slices. --no-configure skips cmake configure but still runs
# cmake --build (which is fast with a warm build cache).
vc_step "build VoiceCatCore.xcframework (all slices: macOS + iOS device + iOS sim)"
XCFW_ARGS=( --all )
$DO_CONFIGURE || XCFW_ARGS+=( --no-configure )
"$VC_REPO_ROOT/clients/apple/scripts/build-xcframework.sh" "${XCFW_ARGS[@]}"
XCFW="$VC_REPO_ROOT/clients/apple/VoiceCatCore.xcframework"
[[ -d "$XCFW" ]] || vc_die "xcframework not found at $XCFW"
vc_ok "xcframework ready -> $XCFW"
# ── Step 2: xcodebuild for iOS Simulator ──────────────────────────────────────
# We use -target (not -scheme + -destination) to avoid the requirement of a
# matching simulator runtime in xcrun simctl. When using -target, xcodebuild
# skips destination resolution and builds directly against the requested SDK.
#
# SYMROOT and OBJROOT are both pinned to the same directory so the Swift Package
# (VoiceCatCore) and the app target (VoiceCatiOS) resolve each other's products.
# Without this, the package builds to clients/apple/build/ while the app target
# looks in clients/apple/iOS/build/ — the module import fails with "unable to
# resolve module dependency: 'VoiceCatCore'".
#
# CODE_SIGNING_ALLOWED=NO avoids provisioning-profile errors for local sim builds.
XCODEPROJ="$VC_REPO_ROOT/clients/apple/iOS/VoiceCatiOS.xcodeproj"
BUILD_DIR="$VC_REPO_ROOT/clients/apple/iOS/build"
SIM_SDK_VER="$(xcrun --sdk iphonesimulator --show-sdk-version 2>/dev/null)"
SIM_SDK="iphonesimulator${SIM_SDK_VER}"
vc_step "xcodebuild VoiceCatiOS ($XCODE_CONFIG / $SIM_SDK)"
xcodebuild \
-project "$XCODEPROJ" \
-target VoiceCatiOS \
-sdk "$SIM_SDK" \
-configuration "$XCODE_CONFIG" \
CODE_SIGNING_ALLOWED=NO \
ARCHS=arm64 \
ONLY_ACTIVE_ARCH=YES \
SYMROOT="$BUILD_DIR" \
OBJROOT="$BUILD_DIR" \
build
APP="$BUILD_DIR/${XCODE_CONFIG}-iphonesimulator/VoiceCatiOS.app"
[[ -d "$APP" ]] || vc_die "VoiceCatiOS.app not found at $APP"
vc_ok "built -> $APP"
# ── Stage ─────────────────────────────────────────────────────────────────────
vc_step "stage -> $VC_DIST_DIR/ios-client"
STAGE="$(vc_dist_subdir ios-client)"
cp -R "$APP" "$STAGE/"
vc_ok "VoiceCatiOS.app -> $STAGE/ ($(du -sh "$APP" | cut -f1))"
vc_ok "done -> $STAGE"
-174
View File
@@ -1,174 +0,0 @@
#!/usr/bin/env bash
#
# build-libs.sh — build libvoicecat for each supported client platform and
# stage the artifacts under dist/lib/.
#
# Artifacts per platform:
# macos → dist/lib/macos/ libvoicecat.a + libvoicecat-fat.a + voicecat.h + module.modulemap
# ios → dist/lib/ios-device/ (arm64-ios slice)
# ios-sim → dist/lib/ios-sim/ (arm64-ios-sim slice)
# windows → dist/lib/windows/ voicecat.dll + voicecat.h
# xcframework → dist/lib/VoiceCatCore.xcframework/ (stitched Apple slices)
#
# The Apple slices + XCFramework are produced by clients/apple/scripts/
# build-xcframework.sh (the validated flow); this script drives it and stages
# the outputs. The Windows DLL comes from the `windows-client` preset.
#
# Usage:
# scripts/build-libs.sh # host default (macos on macOS, windows on Windows)
# scripts/build-libs.sh --platform macos
# scripts/build-libs.sh --platform ios
# scripts/build-libs.sh --platform ios-sim
# scripts/build-libs.sh --platform windows
# scripts/build-libs.sh --all-apple # macos + ios + ios-sim + xcframework (macOS host)
# scripts/build-libs.sh --all # everything buildable on this host
# scripts/build-libs.sh --dist /out --no-configure
# scripts/build-libs.sh -h|--help
#
# Platform availability: macos / ios / ios-sim require a macOS host; windows
# requires a Windows host. Requires VCPKG_ROOT.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VC_SCRIPT_NAME="build-libs"
source "$SCRIPT_DIR/common.sh"
PLATFORMS=()
DO_CONFIGURE=true
while [[ $# -gt 0 ]]; do
case "$1" in
--dist) vc_set_dist "$2"; shift 2 ;;
--no-configure) DO_CONFIGURE=false; shift ;;
--platform) PLATFORMS+=( "$2" ); shift 2 ;;
--all-apple) PLATFORMs_all_apple=1; shift ;;
--all) PLATFORMS=( all-host ); shift ;;
-h|--help) vc_print_help "$0"; exit 0 ;;
*) vc_die "unknown arg: $1 (try --help)" ;;
esac
done
HOST="$(vc_host_os)"
# --all-apple expands to the three Apple platforms.
if [[ -n "${PLATFORMs_all_apple:-}" ]]; then
PLATFORMS=( macos ios ios-sim )
fi
# Default: host-appropriate single platform.
if [[ ${#PLATFORMS[@]} -eq 0 ]]; then
case "$HOST" in
macos) PLATFORMS=( macos ) ;;
windows) PLATFORMS=( windows ) ;;
*) vc_die "no default library platform for host '$HOST'. Pass --platform explicitly." ;;
esac
fi
# Expand the meta-target.
if [[ " ${PLATFORMS[*]} " == *" all-host "* ]]; then
PLATFORMS=()
case "$HOST" in
macos) PLATFORMS=( macos ios ios-sim ) ;;
windows) PLATFORMS=( windows ) ;;
*) vc_die "--all: nothing buildable on host '$HOST'." ;;
esac
fi
vc_log "dist dir: $VC_DIST_DIR"
vc_log "platforms: ${PLATFORMS[*]}"
# Validate platform availability against host.
for p in "${PLATFORMS[@]}"; do
case "$p" in
macos|ios|ios-sim) [[ "$HOST" == "macos" ]] || vc_die "platform '$p' requires a macOS host." ;;
windows) [[ "$HOST" == "windows" ]] || vc_die "platform '$p' requires a Windows host." ;;
*) vc_die "unknown platform '$p' (try --help)" ;;
esac
done
HAVE_APPLE=false
HAVE_WINDOWS=false
for p in "${PLATFORMS[@]}"; do
case "$p" in
macos|ios|ios-sim) HAVE_APPLE=true ;;
windows) HAVE_WINDOWS=true ;;
esac
done
# ── Apple slices + XCFramework ──────────────────────────────────────────────────
# Stage one slice's .a + headers + module map into dist/lib/<sub>.
stage_apple_slice() {
local preset="$1" sub="$2"
local lib="$VC_REPO_ROOT/build/$preset/lib/libvoicecat.a"
local fat="$VC_REPO_ROOT/build/$preset/lib/libvoicecat-fat.a"
local out; out="$(vc_dist_subdir "lib/$sub")"
[[ -f "$lib" ]] || vc_die "expected $lib not found (did the xcframework build run?)"
cp "$lib" "$out/"
[[ -f "$fat" ]] && cp "$fat" "$out/" || true
cp "$VC_REPO_ROOT/core/include/voicecat.h" "$out/"
cat > "$out/module.modulemap" <<'MM'
module VoiceCatC {
header "voicecat.h"
export *
}
MM
vc_ok "$sub: libvoicecat.a ($(vc_file_size "$lib") bytes) -> $out"
}
if $HAVE_APPLE; then
vc_resolve_vcpkg_root apple-dev
vc_step "build Apple slices via build-xcframework.sh"
ALL_THREE=false
if [[ " ${PLATFORMS[*]} " == *" macos "* && " ${PLATFORMS[*]} " == *" ios "* && " ${PLATFORMS[*]} " == *" ios-sim "* ]]; then
ALL_THREE=true
fi
xcfw_invoke() {
local args=()
$DO_CONFIGURE || args+=( --no-configure )
args+=( "$@" )
"$VC_REPO_ROOT/clients/apple/scripts/build-xcframework.sh" "${args[@]}"
}
if $ALL_THREE; then
xcfw_invoke --all
stage_apple_slice apple-dev macos
stage_apple_slice apple-ios ios-device
stage_apple_slice apple-ios-sim ios-sim
else
for p in "${PLATFORMS[@]}"; do
case "$p" in
macos) xcfw_invoke --preset apple-dev; stage_apple_slice apple-dev macos ;;
ios) xcfw_invoke --preset apple-ios; stage_apple_slice apple-ios ios-device ;;
ios-sim) xcfw_invoke --preset apple-ios-sim; stage_apple_slice apple-ios-sim ios-sim ;;
esac
done
fi
# Stage the stitched XCFramework (whatever slices the last call produced).
XCFW="$VC_REPO_ROOT/clients/apple/VoiceCatCore.xcframework"
[[ -d "$XCFW" ]] || vc_die "xcframework not found at $XCFW"
LIB_DIR="$VC_DIST_DIR/lib"
mkdir -p "$LIB_DIR"
rm -rf "$LIB_DIR/VoiceCatCore.xcframework"
cp -R "$XCFW" "$LIB_DIR/"
vc_ok "VoiceCatCore.xcframework -> $LIB_DIR/"
fi
# ── Windows DLL ─────────────────────────────────────────────────────────────────
if $HAVE_WINDOWS; then
vc_resolve_vcpkg_root windows-client
vc_step "build Windows DLL (preset: windows-client)"
if $DO_CONFIGURE; then
cmake --preset windows-client
fi
cmake --build --preset windows-client
DLL="$VC_REPO_ROOT/build/windows-client/bin/voicecat.dll"
[[ -f "$DLL" ]] || vc_die "expected $DLL not found"
OUT="$(vc_dist_subdir lib/windows)"
cp "$DLL" "$OUT/"
cp "$VC_REPO_ROOT/core/include/voicecat.h" "$OUT/"
vc_ok "windows: voicecat.dll ($(vc_file_size "$DLL") bytes) -> $OUT"
fi
vc_ok "done -> $VC_DIST_DIR/lib"
-88
View File
@@ -1,88 +0,0 @@
#!/usr/bin/env bash
# build-linux-binaries.sh — build stripped Linux server binaries locally using Docker.
#
# PRIMARY path: push to main (or trigger manually from the GitHub Actions tab) and
# download the artifacts from .github/workflows/build-linux.yml — no local disk
# pressure, both amd64 and arm64 handled in the cloud.
#
# LOCAL path (this script): uses Docker + buildx with BuildKit cache. Requires
# ~1015 GB of free disk for the vcpkg build cache. Fine on a Linux dev machine;
# on Windows/macOS prefer the GitHub Actions path to avoid filling your Docker VM disk.
#
# Usage:
# ./scripts/build-linux-binaries.sh # build both arches
# ./scripts/build-linux-binaries.sh amd64 # build one arch only
# ./scripts/build-linux-binaries.sh arm64
#
# Output:
# dist/linux-amd64/{voicecat-server,voicecat-admin}
# dist/linux-arm64/{voicecat-server,voicecat-admin}
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
DIST_DIR="${REPO_ROOT}/dist"
# ── arg parsing ────────────────────────────────────────────────────────────────
case "${1:-both}" in
amd64) ARCHS=("amd64") ;;
arm64) ARCHS=("arm64") ;;
both) ARCHS=("amd64" "arm64") ;;
*) echo "Usage: $0 [amd64|arm64|both]" >&2; exit 1 ;;
esac
# ── pre-flight ─────────────────────────────────────────────────────────────────
if ! command -v docker &>/dev/null; then
echo "Error: docker not found." >&2
echo "Install Docker Desktop: https://docs.docker.com/get-docker/" >&2
echo "Or use GitHub Actions (push to main and download artifacts)." >&2
exit 1
fi
if ! docker buildx version &>/dev/null; then
echo "Error: docker buildx not available." >&2
echo "Docker Desktop ships with buildx. On Linux: docker buildx install" >&2
exit 1
fi
echo "Note: first run compiles vcpkg packages from source (~10-15 GB build cache)."
echo "On Windows/macOS, consider using GitHub Actions instead (Actions → Build Linux Binaries → Run workflow)."
echo ""
# ── ensure a builder with multi-platform support ───────────────────────────────
BUILDER="voicecat-builder"
if ! docker buildx inspect "${BUILDER}" &>/dev/null; then
echo "→ Creating buildx builder '${BUILDER}' (docker-container driver)..."
docker buildx create --name "${BUILDER}" --driver docker-container --bootstrap
fi
docker buildx use "${BUILDER}"
# ── build each arch ────────────────────────────────────────────────────────────
for ARCH in "${ARCHS[@]}"; do
PLATFORM="linux/${ARCH}"
OUT_DIR="${DIST_DIR}/linux-${ARCH}"
mkdir -p "${OUT_DIR}"
echo "══ Building ${PLATFORM} ══════════════════════════════════════════════════"
if [[ "${ARCH}" == "arm64" ]] && [[ "$(uname -m)" != "aarch64" ]]; then
echo " (Running under QEMU on a non-arm64 host — will be slow)"
fi
docker buildx build \
--platform "${PLATFORM}" \
--target export \
--output "type=local,dest=${OUT_DIR}" \
--progress plain \
"${REPO_ROOT}"
echo "${PLATFORM} binaries:"
ls -lh "${OUT_DIR}/"
echo ""
done
echo "════════════════════════════════════════════════════════════════════════════"
echo "Done. Binaries in dist/:"
for ARCH in "${ARCHS[@]}"; do
ls -lh "${DIST_DIR}/linux-${ARCH}/"
done
-65
View File
@@ -1,65 +0,0 @@
#!/usr/bin/env bash
#
# build-macos-client.sh — build the macOS AppKit client (VoiceCatMac.app) and
# stage it into dist/macos-client/.
#
# Two steps:
# 1. Build VoiceCatCore.xcframework (macOS slice) by invoking the existing
# clients/apple/scripts/build-xcframework.sh — that builds libvoicecat.a
# (apple-dev preset), merges vcpkg deps into a fat .a, and stitches the
# XCFramework the Swift Package consumes.
# 2. xcodebuild the VoiceCatMac scheme into a known derivedDataPath, then
# copy VoiceCatMac.app into dist/macos-client/.
#
# Usage:
# scripts/build-macos-client.sh # build + stage into ./dist/macos-client
# scripts/build-macos-client.sh --dist /out
# scripts/build-macos-client.sh --no-configure # skip cmake configure (xcframework step)
# scripts/build-macos-client.sh --config Debug # Xcode build config (default Release)
# scripts/build-macos-client.sh -h|--help
#
# macOS only. Requires VCPKG_ROOT and Xcode.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VC_SCRIPT_NAME="build-macos-client"
source "$SCRIPT_DIR/common.sh"
DO_CONFIGURE=true
XCODE_CONFIG="Release"
while [[ $# -gt 0 ]]; do
case "$1" in
--dist) vc_set_dist "$2"; shift 2 ;;
--no-configure) DO_CONFIGURE=false; shift ;;
--config) XCODE_CONFIG="$2"; shift 2 ;;
-h|--help) vc_print_help "$0"; exit 0 ;;
*) vc_die "unknown arg: $1 (try --help)" ;;
esac
done
vc_require_macos
vc_log "dist dir: $VC_DIST_DIR"
vc_step "build VoiceCatCore.xcframework (macOS slice)"
XCFW_ARGS=()
$DO_CONFIGURE || XCFW_ARGS+=( --no-configure )
"$VC_REPO_ROOT/clients/apple/scripts/build-xcframework.sh" "${XCFW_ARGS[@]}"
XCFW="$VC_REPO_ROOT/clients/apple/VoiceCatCore.xcframework"
[[ -d "$XCFW" ]] || vc_die "xcframework not found at $XCFW"
vc_ok "xcframework ready -> $XCFW"
vc_step "xcodebuild VoiceCatMac ($XCODE_CONFIG)"
XCODEPROJ="$VC_REPO_ROOT/clients/apple/macOS/VoiceCatMac.xcodeproj"
DERIVED="$VC_REPO_ROOT/build/macos-app"
xcodebuild -project "$XCODEPROJ" -scheme VoiceCatMac \
-configuration "$XCODE_CONFIG" -derivedDataPath "$DERIVED" build
APP="$DERIVED/Build/Products/$XCODE_CONFIG/VoiceCatMac.app"
[[ -d "$APP" ]] || vc_die "VoiceCatMac.app not found at $APP"
vc_ok "built -> $APP"
vc_step "stage -> $VC_DIST_DIR/macos-client"
STAGE="$(vc_dist_subdir macos-client)"
cp -R "$APP" "$STAGE/"
vc_ok "VoiceCatMac.app -> $STAGE/ ($(du -sh "$APP" | cut -f1))"
vc_ok "done -> $STAGE"
+33
View File
@@ -0,0 +1,33 @@
#!/bin/zsh
set -euo pipefail
script_dir=${0:A:h}
build_one() {
local sdk=$1
local rid=$2
local sdk_path
sdk_path=$(xcrun --sdk "$sdk" --show-sdk-path)
local compiler
compiler=$(xcrun --sdk "$sdk" --find clang)
local build_dir="$script_dir/../artifacts/native-build-$rid-cmake"
cmake -S "$script_dir/../native/media" -B "$build_dir" \
-DCMAKE_SYSTEM_NAME=iOS \
-DCMAKE_C_COMPILER="$compiler" \
-DCMAKE_OSX_SYSROOT="$sdk_path" \
-DCMAKE_OSX_ARCHITECTURES=arm64 \
-DCMAKE_OSX_DEPLOYMENT_TARGET=18.0 \
-DVOICECAT_DOTNET_RID="$rid" \
-DVOICECAT_BUNDLED_OPUS=ON
cmake --build "$build_dir" --config Release --target voicecat_media --parallel 2
cmake --install "$build_dir" --config Release \
--component DotnetMedia --prefix "$script_dir/../artifacts/native"
local staged="$script_dir/../artifacts/native/runtimes/$rid/native/libvoicecat_media.a"
local opus="$build_dir/_deps/opus-build/libopus.a"
local rnnoise="$build_dir/librnnoise.a"
local combined="$staged.combined"
xcrun --sdk "$sdk" libtool -static -o "$combined" "$build_dir/libvoicecat_media.a" "$opus" "$rnnoise"
mv "$combined" "$staged"
}
build_one iphoneos ios-arm64
build_one iphonesimulator iossimulator-arm64
+18
View File
@@ -0,0 +1,18 @@
param(
[string]$BuildDirectory = "$PSScriptRoot/../artifacts/native-build",
[string]$RuntimeIdentifier = [System.Runtime.InteropServices.RuntimeInformation]::RuntimeIdentifier,
[string]$Generator,
[string]$CCompiler
)
$ErrorActionPreference = 'Stop'
$configure = @('-S', "$PSScriptRoot/../native/media", '-B', $BuildDirectory,
'-DCMAKE_BUILD_TYPE=Release', "-DVOICECAT_DOTNET_RID=$RuntimeIdentifier")
if ($Generator) { $configure += @('-G', $Generator) }
if ($CCompiler) { $configure += "-DCMAKE_C_COMPILER=$CCompiler" }
& cmake @configure
if ($LASTEXITCODE) { throw "Native configure failed: $LASTEXITCODE" }
& cmake --build $BuildDirectory --config Release --target voicecat_media --parallel 2
if ($LASTEXITCODE) { throw "Native build failed: $LASTEXITCODE" }
& cmake --install $BuildDirectory --config Release --component DotnetMedia --prefix "$PSScriptRoot/../artifacts/native"
if ($LASTEXITCODE) { throw "Native staging failed: $LASTEXITCODE" }
-62
View File
@@ -1,62 +0,0 @@
#!/usr/bin/env bash
#
# build-server.sh — build the production-shaped voicecat-server (+ voicecat-admin)
# and stage the binaries into dist/server/.
#
# Uses the `server-release` CMake preset (Release, stripped, no tests — see
# docs/building.md §5). Works on Windows, Linux, and macOS; the vcpkg triplet
# is auto-resolved by cmake/voicecat-toolchain.cmake.
#
# Usage:
# scripts/build-server.sh # build + stage into ./dist/server
# scripts/build-server.sh --dist /out # stage into /out/server
# scripts/build-server.sh --no-configure # skip cmake configure, just rebuild + stage
# scripts/build-server.sh -h|--help # show this help
#
# Override the default dist dir (<repo>/dist) with --dist or VOICECAT_DIST_DIR.
# Requires VCPKG_ROOT (or a pre-configured build/server-release cache).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VC_SCRIPT_NAME="build-server"
source "$SCRIPT_DIR/common.sh"
DO_CONFIGURE=true
while [[ $# -gt 0 ]]; do
case "$1" in
--dist) vc_set_dist "$2"; shift 2 ;;
--no-configure) DO_CONFIGURE=false; shift ;;
-h|--help) vc_print_help "$0"; exit 0 ;;
*) vc_die "unknown arg: $1 (try --help)" ;;
esac
done
vc_log "dist dir: $VC_DIST_DIR"
vc_resolve_vcpkg_root server-release
vc_step "configure + build (preset: server-release)"
if $DO_CONFIGURE; then
cmake --preset server-release
fi
cmake --build --preset server-release
BIN_DIR="$VC_REPO_ROOT/build/server-release/bin"
EXE_EXT=""
[[ "$(vc_host_os)" == "windows" ]] && EXE_EXT=".exe"
vc_log "staging binaries -> $VC_DIST_DIR/server"
STAGE="$(vc_dist_subdir server)"
for b in voicecat-server voicecat-admin; do
f="$BIN_DIR/$b$EXE_EXT"
if [[ ! -f "$f" ]]; then
# voicecat-admin only builds when vcpkg deps are on (server-release has
# them on), so it should exist — but be lenient about admin on odd configs.
[[ "$b" == "voicecat-admin" ]] && { vc_log "note: $b not found, skipping"; continue; }
vc_die "expected output not found: $f"
fi
cp "$f" "$STAGE/"
vc_ok "$b$EXE_EXT -> $STAGE/$(basename "$f") ($(vc_file_size "$f") bytes)"
done
vc_ok "done -> $STAGE"
-73
View File
@@ -1,73 +0,0 @@
#!/usr/bin/env bash
#
# build-windows-client.sh — build the Windows WinForms client and stage a
# runnable folder into dist/windows-client/.
#
# Two steps:
# 1. Build the native voicecat.dll via the `windows-client` CMake preset
# (MinGW, static runtime — no libgcc_s_seh-1.dll / libstdc++-6.dll deps).
# 2. `dotnet publish` the C# app (clients/windows/VoiceCat.App). The app's
# Directory.Build.props copies voicecat.dll into the output automatically.
#
# The staged folder contains VoiceCat.App.exe + voicecat.dll + all .NET deps,
# ready to run. By default it is framework-dependent (needs the .NET 10 runtime
# installed); pass --self-contained for a folder that runs without .NET.
#
# Usage:
# scripts/build-windows-client.sh # build + stage into ./dist/windows-client
# scripts/build-windows-client.sh --dist /out
# scripts/build-windows-client.sh --no-configure # skip cmake configure (DLL step)
# scripts/build-windows-client.sh --self-contained
# scripts/build-windows-client.sh -h|--help
#
# Windows only (MSYS2/MinGW). Requires VCPKG_ROOT and the .NET 10 SDK.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VC_SCRIPT_NAME="build-windows-client"
source "$SCRIPT_DIR/common.sh"
DO_CONFIGURE=true
SELF_CONTAINED=false
while [[ $# -gt 0 ]]; do
case "$1" in
--dist) vc_set_dist "$2"; shift 2 ;;
--no-configure) DO_CONFIGURE=false; shift ;;
--self-contained) SELF_CONTAINED=true; shift ;;
-h|--help) vc_print_help "$0"; exit 0 ;;
*) vc_die "unknown arg: $1 (try --help)" ;;
esac
done
vc_require_windows
vc_log "dist dir: $VC_DIST_DIR"
vc_resolve_vcpkg_root windows-client
vc_step "build native DLL (preset: windows-client)"
if $DO_CONFIGURE; then
cmake --preset windows-client
fi
cmake --build --preset windows-client
DLL="$VC_REPO_ROOT/build/windows-client/bin/voicecat.dll"
[[ -f "$DLL" ]] || vc_die "expected output not found: $DLL"
vc_ok "voicecat.dll -> $DLL ($(vc_file_size "$DLL") bytes)"
vc_step "publish C# app (dotnet publish)"
APP_PROJ="$VC_REPO_ROOT/clients/windows/VoiceCat.App/VoiceCat.App.csproj"
PUBLISH_DIR="$VC_REPO_ROOT/build/windows-client/publish"
rm -rf "$PUBLISH_DIR"
mkdir -p "$PUBLISH_DIR"
PUBLISH_ARGS=(dotnet publish "$APP_PROJ" -c Release -o "$PUBLISH_DIR")
if $SELF_CONTAINED; then
PUBLISH_ARGS+=( -r win-x64 --self-contained true )
fi
"${PUBLISH_ARGS[@]}"
APP_EXE="$PUBLISH_DIR/VoiceCat.App.exe"
[[ -f "$APP_EXE" ]] || vc_die "publish output missing VoiceCat.App.exe in $PUBLISH_DIR"
vc_step "stage -> $VC_DIST_DIR/windows-client"
STAGE="$(vc_dist_subdir windows-client)"
cp -R "$PUBLISH_DIR/." "$STAGE/"
vc_ok "VoiceCat.App.exe + deps -> $STAGE (voicecat.dll included)"
vc_ok "done -> $STAGE"
+34
View File
@@ -0,0 +1,34 @@
$ErrorActionPreference = 'Stop'
$allowed = @('MIT', 'BSD-2-Clause', 'BSD-3-Clause', 'Apache-2.0', 'ISC', '0BSD', 'MPL-2.0')
$seen = @{}
foreach ($lockPath in (Get-ChildItem -LiteralPath "$PSScriptRoot/.." -Filter 'packages*.lock.json' -Recurse)) {
$lock = Get-Content -Raw -LiteralPath $lockPath.FullName | ConvertFrom-Json
$assets = Get-Content -Raw -LiteralPath (Join-Path $lockPath.DirectoryName 'obj/project.assets.json') | ConvertFrom-Json
foreach ($framework in $lock.dependencies.PSObject.Properties) {
foreach ($package in $framework.Value.PSObject.Properties) {
if ($package.Value.type -eq 'Project') { continue }
$id = $package.Name.ToLowerInvariant()
$version = $package.Value.resolved
if ($seen.ContainsKey("$id/$version")) { continue }
$seen["$id/$version"] = $true
$nuspec = $null
foreach ($folder in $assets.packageFolders.PSObject.Properties.Name) {
$candidate = Join-Path $folder "$id/$version/$id.nuspec"
if (Test-Path -LiteralPath $candidate) { $nuspec = $candidate; break }
}
if (!$nuspec) { throw "Restore dependencies before auditing $id/$version." }
[xml]$spec = Get-Content -Raw -LiteralPath $nuspec
$license = $spec.package.metadata.license
if ($license.type -eq 'expression' -and $allowed -contains $license.InnerText) { continue }
# This pinned package contains public-domain SQLite builds; no NuGet license metadata.
if ($id -eq 'sourcegear.sqlite3' -and $version -eq '3.50.4.2' -and
$spec.package.metadata.projectUrl -eq 'https://sqlite.org/' -and
$spec.package.metadata.repository.commit -eq '9a2d8281d8f714fe54f7cbcd122479d17b533e89') { continue }
# This legacy pinned package predates NuGet license expressions (Apache-2.0).
if ($id -eq 'xunit.abstractions' -and $version -eq '2.0.3' -and
$spec.package.metadata.licenseUrl -eq 'https://raw.githubusercontent.com/xunit/xunit/master/license.txt') { continue }
throw "Unapproved license for $id/$version. Review before changing the allowlist."
}
}
}
Write-Output "Checked $($seen.Count) package licenses: approved allowlist passed."
-118
View File
@@ -1,118 +0,0 @@
#!/usr/bin/env bash
#
# common.sh — shared helpers for the scripts/ build scripts.
#
# Source this from the other scripts/ build scripts (after setting SCRIPT_DIR):
#
# SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# VC_SCRIPT_NAME="build-foo" # optional, for nicer log prefixes
# source "$SCRIPT_DIR/common.sh"
#
# What it provides:
# - repo-root + dist-dir resolution (default <repo>/dist; override with
# --dist <path> via vc_set_dist, or the VOICECAT_DIST_DIR env var).
# - VCPKG_ROOT detection (env var first, then an existing CMake cache).
# - host-platform guards (vc_require_macos / vc_require_windows / vc_host_os).
# - uniform [tag] logging + a portable file-size helper.
# - vc_dist_subdir <name>: create (clean) and echo $DIST/<name>.
# - vc_print_help <file>: print a script's leading "# " comment block as the
# --help text (so each script's header comment IS its help).
#
# Not meant to be run directly.
# This file lives in scripts/, so the repo root is one level up.
VC_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VC_REPO_ROOT="$(cd "$VC_SCRIPT_DIR/.." && pwd)"
# Display name for log lines (defaults to the caller's filename).
VC_SCRIPT_NAME="${VC_SCRIPT_NAME:-$(basename "${BASH_SOURCE[1]:-$0}")}"
# Dist dir — env override resolved here; --dist updates it via vc_set_dist.
VC_DIST_DIR="${VOICECAT_DIST_DIR:-$VC_REPO_ROOT/dist}"
# ── Logging ─────────────────────────────────────────────────────────────────────
vc_log() { printf '\033[1;34m[%s]\033[0m %s\n' "$VC_SCRIPT_NAME" "$*"; }
vc_step() { printf '\033[1;36m[%s] === %s ===\033[0m\n' "$VC_SCRIPT_NAME" "$*"; }
vc_ok() { printf '\033[1;32m[%s]\033[0m %s\n' "$VC_SCRIPT_NAME" "$*"; }
vc_err() { printf '\033[1;31m[%s] error:\033[0m %s\n' "$VC_SCRIPT_NAME" "$*" >&2; }
vc_die() { vc_err "$*"; exit 1; }
# Portable file size (macOS `stat -f%z` vs coreutils `stat -c%s`).
vc_file_size() {
stat -f%z "$1" 2>/dev/null || stat -c%s "$1" 2>/dev/null
}
# ── Dist dir ────────────────────────────────────────────────────────────────────
# Set/override the dist directory (called when a script sees --dist <path>).
vc_set_dist() {
VC_DIST_DIR="$1"
mkdir -p "$VC_DIST_DIR"
}
# Create (clean) and echo a subdirectory under dist/.
vc_dist_subdir() {
local path="$VC_DIST_DIR/$1"
rm -rf "$path"
mkdir -p "$path"
echo "$path"
}
# ── Platform helpers ────────────────────────────────────────────────────────────
vc_host_os() {
case "$(uname -s)" in
Darwin) echo macos ;;
MINGW*|MSYS*|CYGWIN*) echo windows ;;
Linux) echo linux ;;
*) echo unknown ;;
esac
}
vc_require_macos() {
[[ "$(uname -s)" == "Darwin" ]] \
|| vc_die "this script must be run on macOS (uname -s = $(uname -s))."
}
vc_require_windows() {
local s; s="$(uname -s)"
[[ "$s" == MINGW* || "$s" == MSYS* || "$s" == CYGWIN* ]] \
|| vc_die "this script must be run on Windows (MSYS2/MinGW). uname -s = $s."
}
# ── VCPKG_ROOT ──────────────────────────────────────────────────────────────────
# Resolution order: the env var, else an existing CMake cache (Z_VCPKG_ROOT_DIR) so a
# developer who already configured a preset doesn't need VCPKG_ROOT in their shell env,
# else the bundled submodule at <repo-root>/vcpkg. $1 = the preset whose cache to probe
# as a fallback (e.g. "dev" or "apple-dev").
vc_resolve_vcpkg_root() {
local fallback_preset="${1:-dev}"
if [[ -z "${VCPKG_ROOT:-}" ]]; then
local cache="$VC_REPO_ROOT/build/$fallback_preset/CMakeCache.txt"
if [[ -f "$cache" ]]; then
local detected
detected="$(grep -m1 '^Z_VCPKG_ROOT_DIR:INTERNAL=' "$cache" | cut -d= -f2- || true)"
if [[ -n "$detected" && -d "$detected" ]]; then
export VCPKG_ROOT="$detected"
fi
fi
fi
if [[ -z "${VCPKG_ROOT:-}" ]]; then
local bundled="$VC_REPO_ROOT/vcpkg"
if [[ -f "$bundled/scripts/buildsystems/vcpkg.cmake" ]]; then
export VCPKG_ROOT="$bundled"
fi
fi
if [[ -z "${VCPKG_ROOT:-}" || ! -d "$VCPKG_ROOT" ]]; then
vc_die "VCPKG_ROOT is not set or does not exist.
either init the bundled submodule:
git submodule update --init vcpkg
or point at an external vcpkg checkout:
export VCPKG_ROOT=/path/to/vcpkg"
fi
vc_log "VCPKG_ROOT=$VCPKG_ROOT"
}
# ── Help ────────────────────────────────────────────────────────────────────────
# Print a script's leading "# " comment block (skipping the shebang) as help.
vc_print_help() {
awk 'NR==1{next} /^#[[:space:]]?/{sub(/^#[[:space:]]?/,""); print; next} {exit}' "$1"
}
-285
View File
@@ -1,285 +0,0 @@
#!/usr/bin/env bash
#
# dist-ios-adhoc.sh — build an ad-hoc IPA of the iOS client and the files needed to
# install it on registered devices from an HTTPS web page (itms-services OTA install).
#
# This is for handing the app to a handful of friends BEFORE TestFlight. Ad-hoc builds
# only run on devices whose UDID is registered in your Apple Developer account, so the
# script registers UDIDs (via the App Store Connect API) before it signs.
#
# Pipeline:
# 1. Register each --udid with App Store Connect (idempotent; skip with --skip-register).
# 2. Build VoiceCatCore.xcframework (iOS device slice).
# 3. xcodebuild archive (Release, generic/platform=iOS), letting Xcode auto-create the
# ad-hoc Distribution cert + profile via -allowProvisioningUpdates + the API key.
# 4. xcodebuild -exportArchive with method=release-testing (Xcode's modern name for
# "ad-hoc") -> VoiceCatiOS.ipa.
# 5. Generate manifest.plist (OTA install manifest) + index.html (install page).
# 6. Stage VoiceCatiOS.ipa + manifest.plist + index.html into dist/ios-adhoc/.
#
# You then upload those three files to your HTTPS host and open index.html on the iPhone.
#
# Prerequisites (one-time, not scriptable):
# - Paid Apple Developer Program membership (Team ID is already set in the project).
# - An App Store Connect API "Team Key" (.p8) with Admin or App Manager access:
# App Store Connect -> Users and Access -> Integrations -> App Store Connect API.
# Note its Key ID and Issuer ID. Keep the .p8 out of the repo.
# - Each friend's device UDID (read it in Finder with the iPhone connected to a Mac).
#
# Auth — supply via env vars (or the matching flags):
# ASC_KEY_ID App Store Connect API Key ID (--key-id)
# ASC_ISSUER_ID App Store Connect API Issuer ID (--issuer-id)
# ASC_KEY_PATH path to AuthKey_XXXXXXXXXX.p8 (--key)
#
# Usage:
# scripts/dist-ios-adhoc.sh --udid <UDID> --name "Friend iPhone" \
# --base-url https://example.com/voicecat
# scripts/dist-ios-adhoc.sh --udids-file friends.csv --base-url https://example.com/vc
# scripts/dist-ios-adhoc.sh --skip-register --base-url https://example.com/vc # rebuild only
# scripts/dist-ios-adhoc.sh --dist /out --no-configure
# scripts/dist-ios-adhoc.sh -h|--help
#
# Flags:
# --udid <UDID> device to register (repeatable). Pair with an optional --name.
# --name <label> label for the immediately-preceding --udid (default: the UDID).
# --udids-file <file> CSV of "udid,name" lines (one device per line; blank/# lines skipped).
# --base-url <url> public HTTPS folder you'll upload the output to. Used to fill
# manifest.plist + index.html. If omitted, a __BASE_URL__ placeholder
# is written and you must edit both files before they work.
# --skip-register don't touch App Store Connect; just rebuild/export/stage.
# --key-id / --issuer-id / --key override the ASC_* env vars.
# --dist <path> output root (default <repo>/dist). Files land in <dist>/ios-adhoc/.
# --no-configure skip the cmake configure step in the xcframework build.
# -h, --help this help.
#
# macOS only. Requires Xcode, VCPKG_ROOT (or a configured build/apple-dev cache), and the
# Python `cryptography` package (already present in a standard macOS Python 3).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VC_SCRIPT_NAME="dist-ios-adhoc"
source "$SCRIPT_DIR/common.sh"
# ── Project constants (see clients/apple/iOS/VoiceCatiOS.xcodeproj) ───────────────
TEAM_ID="FJV8L966W4"
APP_BUNDLE_ID="cat.voice.VoiceCatiOS"
SCHEME="VoiceCatiOS"
APP_TITLE="VoiceCat"
XCODEPROJ="$VC_REPO_ROOT/clients/apple/iOS/VoiceCatiOS.xcodeproj"
# ── Args ──────────────────────────────────────────────────────────────────────────
KEY_ID="${ASC_KEY_ID:-}"
ISSUER_ID="${ASC_ISSUER_ID:-}"
KEY_PATH="${ASC_KEY_PATH:-}"
BASE_URL=""
SKIP_REGISTER=false
DO_CONFIGURE=true
UDIDS=() # parallel arrays: UDIDS[i] / UDID_NAMES[i]
UDID_NAMES=()
while [[ $# -gt 0 ]]; do
case "$1" in
--udid) UDIDS+=("$2"); UDID_NAMES+=("$2"); shift 2 ;;
--name) [[ ${#UDID_NAMES[@]} -gt 0 ]] || vc_die "--name must follow a --udid"
UDID_NAMES[${#UDID_NAMES[@]}-1]="$2"; shift 2 ;;
--udids-file) [[ -f "$2" ]] || vc_die "udids file not found: $2"
while IFS=',' read -r u n _; do
u="${u// /}"
[[ -z "$u" || "$u" == \#* ]] && continue
UDIDS+=("$u"); UDID_NAMES+=("${n:-$u}")
done < "$2"
shift 2 ;;
--base-url) BASE_URL="${2%/}"; shift 2 ;;
--skip-register) SKIP_REGISTER=true; shift ;;
--key-id) KEY_ID="$2"; shift 2 ;;
--issuer-id) ISSUER_ID="$2"; shift 2 ;;
--key) KEY_PATH="$2"; shift 2 ;;
--dist) vc_set_dist "$2"; shift 2 ;;
--no-configure) DO_CONFIGURE=false; shift ;;
-h|--help) vc_print_help "$0"; exit 0 ;;
*) vc_die "unknown arg: $1 (try --help)" ;;
esac
done
vc_require_macos
# Auth is needed for device registration AND for -allowProvisioningUpdates (Xcode uses the
# same key to mint the ad-hoc Distribution cert/profile).
[[ -n "$KEY_ID" ]] || vc_die "missing API Key ID (set ASC_KEY_ID or pass --key-id)"
[[ -n "$ISSUER_ID" ]] || vc_die "missing Issuer ID (set ASC_ISSUER_ID or pass --issuer-id)"
[[ -n "$KEY_PATH" ]] || vc_die "missing .p8 path (set ASC_KEY_PATH or pass --key)"
[[ -f "$KEY_PATH" ]] || vc_die "API key file not found: $KEY_PATH"
python3 -c "import cryptography" 2>/dev/null \
|| vc_die "Python 'cryptography' package not available: pip3 install cryptography"
if [[ -z "$BASE_URL" ]]; then
BASE_URL="__BASE_URL__"
vc_log "no --base-url given: manifest.plist + index.html will use the __BASE_URL__"
vc_log "placeholder — edit both files to your real HTTPS folder before uploading."
fi
OUT="$(vc_dist_subdir ios-adhoc)"
ARCHIVE="$OUT/VoiceCatiOS.xcarchive"
EXPORT_DIR="$OUT/export"
DERIVED="$OUT/DerivedData"
IPA="$OUT/VoiceCatiOS.ipa"
# ── Step 1: register devices ───────────────────────────────────────────────────────
if $SKIP_REGISTER; then
vc_step "skipping device registration (--skip-register)"
elif [[ ${#UDIDS[@]} -eq 0 ]]; then
vc_step "no --udid/--udids-file given — skipping device registration"
vc_log "(devices must already be registered, or this IPA won't install for them)"
else
vc_step "register ${#UDIDS[@]} device(s) with App Store Connect"
for i in "${!UDIDS[@]}"; do
python3 "$SCRIPT_DIR/asc_api.py" register \
--key-id "$KEY_ID" --issuer-id "$ISSUER_ID" --key "$KEY_PATH" \
--udid "${UDIDS[$i]}" --name "${UDID_NAMES[$i]}"
done
vc_ok "device registration complete"
fi
# ── Step 2: xcframework (iOS device slice) ──────────────────────────────────────────
vc_step "build VoiceCatCore.xcframework (iOS device slice)"
vc_resolve_vcpkg_root "apple-dev"
XCFW_ARGS=( --preset apple-ios )
$DO_CONFIGURE || XCFW_ARGS+=( --no-configure )
"$VC_REPO_ROOT/clients/apple/scripts/build-xcframework.sh" "${XCFW_ARGS[@]}"
XCFW="$VC_REPO_ROOT/clients/apple/VoiceCatCore.xcframework"
[[ -d "$XCFW" ]] || vc_die "xcframework not found at $XCFW"
vc_ok "xcframework ready -> $XCFW"
# ── Step 3: archive ─────────────────────────────────────────────────────────────────
vc_step "xcodebuild archive ($SCHEME / Release / generic iOS)"
xcodebuild archive \
-project "$XCODEPROJ" \
-scheme "$SCHEME" \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$ARCHIVE" \
-derivedDataPath "$DERIVED" \
-allowProvisioningUpdates \
-authenticationKeyPath "$KEY_PATH" \
-authenticationKeyID "$KEY_ID" \
-authenticationKeyIssuerID "$ISSUER_ID"
[[ -d "$ARCHIVE" ]] || vc_die "archive not produced at $ARCHIVE"
vc_ok "archived -> $ARCHIVE"
# ── Step 4: export ad-hoc IPA ───────────────────────────────────────────────────────
# method "release-testing" is Xcode 15.3+/26's name for the old "ad-hoc" method.
# manageAppGroups lets automatic signing carry the group.cat.voice.VoiceCat capability
# (used by the ReplayKit broadcast extension) into the generated profiles.
EXPORT_PLIST="$OUT/ExportOptions.plist"
cat > "$EXPORT_PLIST" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key> <string>release-testing</string>
<key>teamID</key> <string>$TEAM_ID</string>
<key>signingStyle</key> <string>automatic</string>
<key>manageAppGroups</key> <true/>
<key>stripSwiftSymbols</key> <true/>
</dict>
</plist>
PLIST
vc_step "xcodebuild -exportArchive (method=release-testing / ad-hoc)"
xcodebuild -exportArchive \
-archivePath "$ARCHIVE" \
-exportPath "$EXPORT_DIR" \
-exportOptionsPlist "$EXPORT_PLIST" \
-allowProvisioningUpdates \
-authenticationKeyPath "$KEY_PATH" \
-authenticationKeyID "$KEY_ID" \
-authenticationKeyIssuerID "$ISSUER_ID"
EXPORTED_IPA="$(find "$EXPORT_DIR" -maxdepth 1 -name '*.ipa' | head -n1)"
[[ -n "$EXPORTED_IPA" ]] || vc_die "no .ipa found in $EXPORT_DIR"
mv "$EXPORTED_IPA" "$IPA"
vc_ok "exported -> $IPA ($(du -h "$IPA" | cut -f1))"
# ── Step 5: read version + generate manifest.plist & index.html ─────────────────────
APP_IN_ARCHIVE="$(find "$ARCHIVE/Products/Applications" -maxdepth 1 -name '*.app' | head -n1)"
SHORT_VER="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' \
"$APP_IN_ARCHIVE/Info.plist" 2>/dev/null || echo "1.0")"
MANIFEST="$OUT/manifest.plist"
cat > "$MANIFEST" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items</key>
<array>
<dict>
<key>assets</key>
<array>
<dict>
<key>kind</key> <string>software-package</string>
<key>url</key> <string>$BASE_URL/VoiceCatiOS.ipa</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>bundle-identifier</key> <string>$APP_BUNDLE_ID</string>
<key>bundle-version</key> <string>$SHORT_VER</string>
<key>kind</key> <string>software</string>
<key>title</key> <string>$APP_TITLE</string>
</dict>
</dict>
</array>
</dict>
</plist>
PLIST
INDEX="$OUT/index.html"
cat > "$INDEX" <<HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Install $APP_TITLE</title>
<style>
body { font: 17px -apple-system, system-ui, sans-serif; margin: 0; padding: 2.5rem 1.5rem;
color: #111; background: #f5f5f7; -webkit-text-size-adjust: 100%; }
main { max-width: 30rem; margin: 0 auto; }
h1 { font-size: 1.6rem; }
.btn { display: block; text-align: center; text-decoration: none; margin: 1.5rem 0;
padding: 0.9rem 1rem; border-radius: 0.8rem; background: #0a84ff; color: #fff;
font-weight: 600; }
.note { font-size: 0.9rem; color: #555; line-height: 1.5; }
code { background: #e5e5ea; padding: 0.1rem 0.3rem; border-radius: 0.3rem; }
</style>
</head>
<body>
<main>
<h1>$APP_TITLE — test build</h1>
<p>Tap below on your <strong>iPhone</strong> to install (version $SHORT_VER).</p>
<a class="btn" href="itms-services://?action=download-manifest&amp;url=$BASE_URL/manifest.plist">
Install $APP_TITLE
</a>
<p class="note">
Only works on devices whose UDID was registered for this build, on iOS 18 or later.
If nothing happens, make sure you opened this page in <strong>Safari</strong> and that
it is served over <strong>HTTPS</strong>. After installing, the app launches normally —
no "trust developer" step is needed.
</p>
</main>
</body>
</html>
HTML
# ── Done ────────────────────────────────────────────────────────────────────────────
vc_ok "staged install files in $OUT:"
printf ' %s\n' "VoiceCatiOS.ipa" "manifest.plist" "index.html"
echo
if [[ "$BASE_URL" == "__BASE_URL__" ]]; then
vc_log "NEXT: replace __BASE_URL__ in manifest.plist and index.html with your HTTPS"
vc_log " folder URL, then upload all three files there."
else
vc_log "NEXT: upload the three files above to $BASE_URL/"
vc_log " then open $BASE_URL/index.html in Safari on a registered iPhone."
fi
+17
View File
@@ -0,0 +1,17 @@
[CmdletBinding(SupportsShouldProcess)]
param(
[string[]]$Runtime = @('win-x64', 'linux-x64'),
[string]$Output
)
$ErrorActionPreference = 'Stop'
if ($Runtime.Count -eq 0) { throw 'At least one runtime must be specified.' }
if ($Output -and $Runtime.Count -ne 1) { throw '-Output can only be used when publishing one runtime.' }
foreach ($targetRuntime in $Runtime) {
$targetOutput = if ($Output) { $Output } else { "$PSScriptRoot/../artifacts/server/$targetRuntime" }
if (-not $PSCmdlet.ShouldProcess($targetOutput, "Publish VoiceCat.Server for $targetRuntime")) { continue }
dotnet publish "$PSScriptRoot/../src/VoiceCat.Server/VoiceCat.Server.csproj" -c Release -r $targetRuntime --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishTrimmed=false -p:RestoreLockedMode=true "-p:NuGetLockFilePath=packages.publish.$targetRuntime.lock.json" -o $targetOutput
if ($LASTEXITCODE -ne 0) { throw "Managed server publish failed for $targetRuntime." }
}
-132
View File
@@ -1,132 +0,0 @@
#!/usr/bin/env bash
#
# run-ios-simulator.sh — install and launch VoiceCatiOS on an iPhone simulator.
#
# Picks a simulator in this order:
# 1. Any already-booted iPhone simulator.
# 2. The first available iPhone simulator (boots it, opens Simulator.app).
# Pass --device or --udid to pin a specific device.
#
# Usage:
# scripts/run-ios-simulator.sh # install + launch (Debug build)
# scripts/run-ios-simulator.sh --config Release
# scripts/run-ios-simulator.sh --device "iPhone 16 Pro"
# scripts/run-ios-simulator.sh --udid <UUID>
# scripts/run-ios-simulator.sh --build # run build-ios-client.sh first
# scripts/run-ios-simulator.sh --log # stream app logs after launch
# scripts/run-ios-simulator.sh -h|--help
#
# The app is installed via `xcrun simctl install` and launched via
# `xcrun simctl launch`. With --log, log lines stream to stdout until Ctrl+C
# (this does not kill the app).
#
# Bundle ID: cat.voice.VoiceCatiOS
# macOS only. Requires Xcode.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VC_SCRIPT_NAME="run-ios-simulator"
source "$SCRIPT_DIR/common.sh"
XCODE_CONFIG="Debug"
DEVICE_FILTER=""
DEVICE_UDID=""
DO_BUILD=false
DO_LOG=false
while [[ $# -gt 0 ]]; do
case "$1" in
--config) XCODE_CONFIG="$2"; shift 2 ;;
--device) DEVICE_FILTER="$2"; shift 2 ;;
--udid) DEVICE_UDID="$2"; shift 2 ;;
--build) DO_BUILD=true; shift ;;
--log) DO_LOG=true; shift ;;
-h|--help) vc_print_help "$0"; exit 0 ;;
*) vc_die "unknown arg: $1 (try --help)" ;;
esac
done
vc_require_macos
# ── Optionally build first ─────────────────────────────────────────────────────
if $DO_BUILD; then
vc_step "building VoiceCatiOS"
"$SCRIPT_DIR/build-ios-client.sh" --config "$XCODE_CONFIG"
fi
# ── Locate the .app ───────────────────────────────────────────────────────────
APP="$VC_REPO_ROOT/clients/apple/iOS/build/${XCODE_CONFIG}-iphonesimulator/VoiceCatiOS.app"
if [[ ! -d "$APP" ]]; then
vc_err "VoiceCatiOS.app not found at:"
vc_err " $APP"
vc_die "build it first with: scripts/build-ios-client.sh (or pass --build)"
fi
vc_log "app: $APP"
# ── Find a simulator ──────────────────────────────────────────────────────────
# Resolve: explicit UDID > explicit name filter > booted iPhone > any iPhone.
find_sim() {
# Each line from simctl looks like:
# " iPhone 16 (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX) (Shutdown)"
# The || true at the end prevents set -e from killing the script when no
# matching device exists (grep -oE exits 1 on no match).
local filter="${1:-}" state="${2:-}"
local pattern="iPhone"
[[ -n "$filter" ]] && pattern="$filter"
xcrun simctl list devices available 2>/dev/null \
| grep -E "$pattern" \
| { [[ -n "$state" ]] && grep "$state" || cat; } \
| head -1 \
| grep -oE '[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}' \
| head -1 || true
}
sim_name() {
xcrun simctl list devices available 2>/dev/null \
| grep "$1" | head -1 \
| sed 's/[[:space:]]*('"$1"').*//' | sed 's/^[[:space:]]*//' || true
}
if [[ -n "$DEVICE_UDID" ]]; then
UDID="$DEVICE_UDID"
elif [[ -n "$DEVICE_FILTER" ]]; then
UDID="$(find_sim "$DEVICE_FILTER")"
[[ -n "$UDID" ]] || vc_die "no available simulator matching '$DEVICE_FILTER'"
else
# Prefer a booted simulator; boot one if none is active.
UDID="$(find_sim "" "Booted")"
if [[ -z "$UDID" ]]; then
UDID="$(find_sim "")"
[[ -n "$UDID" ]] || vc_die "no iPhone simulator found.
Install an iOS runtime: Xcode > Settings > Platforms > iOS."
NAME="$(sim_name "$UDID")"
vc_step "booting simulator: $NAME ($UDID)"
xcrun simctl boot "$UDID"
open -a Simulator
# Wait for the simulator to finish booting before installing.
vc_log "waiting for simulator to boot…"
xcrun simctl bootstatus "$UDID" -b
fi
fi
NAME="$(sim_name "$UDID")"
vc_log "simulator: $NAME ($UDID)"
# Bring Simulator.app to the foreground (no-op if already open).
open -a Simulator
# ── Install ───────────────────────────────────────────────────────────────────
vc_step "installing VoiceCatiOS"
xcrun simctl install "$UDID" "$APP"
vc_ok "installed -> $UDID"
# ── Launch ────────────────────────────────────────────────────────────────────
BUNDLE_ID="cat.voice.VoiceCatiOS"
vc_step "launching $BUNDLE_ID"
xcrun simctl launch "$UDID" "$BUNDLE_ID"
vc_ok "launched — VoiceCat is running on $NAME"
# ── Optional log streaming ────────────────────────────────────────────────────
if $DO_LOG; then
vc_log "streaming logs (Ctrl+C to stop — does not kill the app)"
xcrun simctl spawn "$UDID" log stream --predicate "subsystem contains \"VoiceCat\""
fi
+41
View File
@@ -0,0 +1,41 @@
param(
[string]$HostName = "127.0.0.1",
[int]$Port = 8384,
[double]$Minutes = 30,
[int]$Pairs = 2,
[string]$CliDll = "$PSScriptRoot/../src/VoiceCat.Cli/bin/Release/net10.0/VoiceCat.Cli.dll"
)
$ErrorActionPreference = "Stop"
if ($Minutes -le 0 -or $Pairs -lt 1) { throw "Minutes and Pairs must be positive." }
$tempRoot = [IO.Path]::GetFullPath([IO.Path]::GetTempPath())
$work = [IO.Path]::GetFullPath((Join-Path $tempRoot ("voicecat-soak-" + [Guid]::NewGuid().ToString("N"))))
if (-not $work.StartsWith($tempRoot, [StringComparison]::OrdinalIgnoreCase) -or (Split-Path $work -Leaf) -notlike 'voicecat-soak-*') { throw "Unsafe soak workspace path." }
[IO.Directory]::CreateDirectory($work) | Out-Null
$deadline = [DateTime]::UtcNow.AddMinutes($Minutes)
$cycles = 0
try {
while ([DateTime]::UtcNow -lt $deadline) {
$processes = @()
for ($pair = 0; $pair -lt $Pairs; $pair++) {
$stamp = "$cycles-$pair"
foreach ($side in 0,1) {
$name = "Soak-$stamp-$side"; $send = "message-$stamp-$side"; $expect = "message-$stamp-$([int](1-$side))"
$start = [Diagnostics.ProcessStartInfo]::new("dotnet")
$start.UseShellExecute = $false; $start.RedirectStandardOutput = $true; $start.RedirectStandardError = $true; $start.CreateNoWindow = $true
$arguments = @($CliDll,"--host",$HostName,"--port","$Port","--nickname",$name,"--pins",(Join-Path $work "$name.pins"),"--trust-first","--voice","--expect-voice","--send-text",$send,"--expect-text",$expect,"--start-delay-ms","1000","--timeout-seconds","20")
$start.Arguments = ($arguments | ForEach-Object { '"' + $_.Replace('"','\"') + '"' }) -join ' '
$processes += [Diagnostics.Process]::Start($start)
}
}
foreach ($process in $processes) {
$stdout = $process.StandardOutput.ReadToEndAsync(); $stderr = $process.StandardError.ReadToEndAsync()
if (-not $process.WaitForExit(30000)) { $process.Kill($true); throw "Soak client timed out." }
if ($process.ExitCode -ne 0) { throw "Soak client failed: $($stderr.Result) $($stdout.Result)" }
$process.Dispose()
}
$cycles++
Write-Host "Completed soak cycle $cycles ($($Pairs * 2) clients)."
}
Write-Host "Soak passed: $cycles cycles, $($cycles * $Pairs * 2) client sessions."
}
finally { if (Test-Path $work) { Remove-Item -LiteralPath $work -Recurse -Force } }