1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
name: Unix Release
on:
workflow_dispatch:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
build-unix-release:
strategy:
fail-fast: false
matrix:
os:
runs-on: ${{ matrix.os }}
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust build outputs
uses: Swatinem/rust-cache@v2
- name: Install Linux build dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libasound2-dev libpcaudio-dev libsonic-dev
- name: Restore voice model assets cache
uses: actions/cache/restore@v4
id: cache-voice
with:
path: .hematite/assets/voice
key: kokoro-voice-assets-v1.0
restore-keys: |
kokoro-voice-assets-
- name: Download voice model assets
run: |
mkdir -p .hematite/assets/voice
base="https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files-v1.0"
[[ -f .hematite/assets/voice/kokoro-v1.0.onnx ]] || curl -L --retry 3 --max-time 300 -o .hematite/assets/voice/kokoro-v1.0.onnx "$base/kokoro-v1.0.onnx"
[[ -f .hematite/assets/voice/voices.bin ]] || curl -L --retry 3 --max-time 120 -o .hematite/assets/voice/voices.bin "$base/voices-v1.0.bin"
- name: Save voice model assets cache
if: always() && steps.cache-voice.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: .hematite/assets/voice
key: kokoro-voice-assets-v1.0
# ── ORT Linux workaround ────────────────────────────────────────────────────
# cdn.pyke.io (the CDN that serves pyke's custom ORT builds for the `ort`
# Rust crate) broke for the Linux target in April 2026 while macOS/Windows
# continued to work. We bypass it entirely by downloading the identical
# ORT 1.24.2 binary from Microsoft's official GitHub releases instead.
#
# Four non-obvious requirements to make this work:
# 1. Create a bare libonnxruntime.so symlink — Microsoft ships only the
# versioned .so.X.Y.Z; the linker needs the unversioned name.
# 2. Set ORT_PREFER_DYNAMIC_LINK=1 — ort-sys defaults to static (.a) when
# ORT_LIB_LOCATION is set; Microsoft's release is dynamic-only.
# 3. ORT_LIB_LOCATION points to the root dir (containing lib/); ort-sys
# searches {root}/lib/ for the .so.
# 4. LIBRARY_PATH + LD_LIBRARY_PATH — lld needs the explicit search path
# even after ort-sys validates the location successfully.
#
# The cache key is stable (not tied to Cargo.lock) so subsequent releases
# hit the cache and never contact pyke.io or GitHub for ORT again.
# If pyke.io recovers, this path still works and is strictly more reliable.
# To revert to the default CDN path: remove these four steps entirely.
# ────────────────────────────────────────────────────────────────────────────
- name: Cache ORT library (Linux)
if: runner.os == 'Linux'
uses: actions/cache@v4
id: cache-ort-lib
with:
path: ~/.cache/ort-lib
key: ort-ms-1.24.2-x86_64-linux-v5
- name: Pre-fetch ORT library (Linux)
if: runner.os == 'Linux' && steps.cache-ort-lib.outputs.cache-hit != 'true'
shell: bash
run: |
rm -rf ~/.cache/ort-lib && mkdir -p ~/.cache/ort-lib
GH_URL="https://github.com/microsoft/onnxruntime/releases/download/v1.24.2/onnxruntime-linux-x64-1.24.2.tgz"
echo "Downloading ORT 1.24.2 from Microsoft GitHub releases..."
curl -fSL --retry 3 --retry-delay 10 --max-time 180 -L -o /tmp/ort.tgz "$GH_URL"
tar -xzf /tmp/ort.tgz -C ~/.cache/ort-lib --strip-components=1
echo "=== ORT extraction layout ==="
find ~/.cache/ort-lib -name "libonnxruntime*" | sort
echo "============================="
# Find the directory containing the versioned .so and ensure bare symlink exists
VERSIONED=$(find ~/.cache/ort-lib -name "libonnxruntime.so.*" -not -type l | head -1)
if [ -z "$VERSIONED" ]; then
echo "ERROR: libonnxruntime.so.* not found after extraction" >&2
exit 1
fi
LIB_DIR=$(dirname "$VERSIONED")
echo "Library directory: $LIB_DIR"
if [ ! -f "$LIB_DIR/libonnxruntime.so" ]; then
ln -sf "$(basename "$VERSIONED")" "$LIB_DIR/libonnxruntime.so"
echo "Created symlink: libonnxruntime.so -> $(basename "$VERSIONED")"
fi
# Store the resolved path for the next step
echo "$LIB_DIR" > ~/.cache/ort-lib/.ort_lib_dir
- name: Set ORT_LIB_LOCATION (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
if [ -f "$HOME/.cache/ort-lib/.ort_lib_dir" ]; then
LIB_DIR=$(cat "$HOME/.cache/ort-lib/.ort_lib_dir")
ORT_ROOT=$(dirname "$LIB_DIR")
echo "ORT_LIB_LOCATION=$ORT_ROOT" >> "$GITHUB_ENV"
echo "ORT_PREFER_DYNAMIC_LINK=1" >> "$GITHUB_ENV"
# LIBRARY_PATH: tells the C linker (lld) where to find -lonnxruntime at link time.
# LD_LIBRARY_PATH: tells the dynamic loader where to find it at runtime/test time.
echo "LIBRARY_PATH=$LIB_DIR" >> "$GITHUB_ENV"
echo "LD_LIBRARY_PATH=$LIB_DIR" >> "$GITHUB_ENV"
echo "ORT_LIB_LOCATION=$ORT_ROOT LIBRARY_PATH=$LIB_DIR"
ls "$LIB_DIR/libonnxruntime.so" && echo "symlink confirmed" || echo "WARNING: symlink missing"
else
echo "::warning::ORT pre-fetch did not run or failed - build will attempt CDN download"
fi
- name: Build portable archive
shell: bash
run: |
for attempt in 1 2 3; do
echo "Build attempt $attempt..."
bash ./scripts/package-unix.sh && break
if [ $attempt -lt 3 ]; then
echo "Build failed (attempt $attempt), retrying in 30s..."
sleep 30
else
echo "All build attempts failed."
exit 1
fi
done
- name: Upload packaged artifacts
uses: actions/upload-artifact@v4
with:
name: hematite-${{ runner.os }}
path: dist/*/*.tar.gz
- name: Publish GitHub release assets
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2
with:
files: dist/*/*.tar.gz