|
1
|
#!/usr/bin/env bash |
|
2
|
# ────────────────────────────────────────────────────────────────────────────── |
|
3
|
# build-python.sh — Build the onde Python package using Maturin |
|
4
|
# ────────────────────────────────────────────────────────────────────────────── |
|
5
|
# |
|
6
|
# This script: |
|
7
|
# 1. Checks that `maturin` is available (or installs it). |
|
8
|
# 2. Builds the Python wheel using `maturin build --release` from the |
|
9
|
# python/ directory. |
|
10
|
# 3. Optionally runs `maturin develop` for development installs. |
|
11
|
# 4. Shows a summary with the output wheel location. |
|
12
|
# |
|
13
|
# Prerequisites: |
|
14
|
# - Rust toolchain (cargo, rustc on PATH) |
|
15
|
# - Python 3.9+ with pip or uv |
|
16
|
# - uniffi-bindgen ==0.31.0 on PATH (auto-installed if missing) |
|
17
|
# |
|
18
|
# Usage: |
|
19
|
# ./build-python.sh # Build release wheel |
|
20
|
# ./build-python.sh --develop # Install into current venv (editable) |
|
21
|
# ./build-python.sh --release # Build release wheel (explicit, default) |
|
22
|
# ./build-python.sh --help # Show usage |
|
23
|
# |
|
24
|
# Environment variables: |
|
25
|
# ONDE_FEATURES — Extra Cargo features to enable (e.g. "whisper") |
|
26
|
# CARGO — Path to cargo binary (default: cargo) |
|
27
|
# PYTHON — Path to python binary (default: python3) |
|
28
|
# MATURIN — Path to maturin binary (default: auto-detected) |
|
29
|
# UNIFFI_BINDGEN — Path to uniffi-bindgen binary (default: auto-detected) |
|
30
|
# ────────────────────────────────────────────────────────────────────────────── |
|
31
|
set -euo pipefail |
|
32
|
|
|
33
|
# ── Constants ───────────────────────────────────────────────────────────────── |
|
34
|
|
|
35
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
|
36
|
CRATE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" |
|
37
|
PYTHON_DIR="$SCRIPT_DIR" |
|
38
|
|
|
39
|
CARGO="${CARGO:-cargo}" |
|
40
|
PYTHON="${PYTHON:-python3}" |
|
41
|
|
|
42
|
# ── Helpers ─────────────────────────────────────────────────────────────────── |
|
43
|
|
|
44
|
RED='\033[0;31m' |
|
45
|
GREEN='\033[0;32m' |
|
46
|
YELLOW='\033[0;33m' |
|
47
|
CYAN='\033[0;36m' |
|
48
|
BOLD='\033[1m' |
|
49
|
NC='\033[0m' # No Color |
|
50
|
|
|
51
|
log() { echo -e "${CYAN}[onde]${NC} $*"; } |
|
52
|
ok() { echo -e "${GREEN}[onde]${NC} $*"; } |
|
53
|
warn() { echo -e "${YELLOW}[onde]${NC} $*"; } |
|
54
|
err() { echo -e "${RED}[onde]${NC} $*" >&2; } |
|
55
|
die() { err "$@"; exit 1; } |
|
56
|
|
|
57
|
usage() { |
|
58
|
cat <<EOF |
|
59
|
Usage: $(basename "$0") [OPTIONS] |
|
60
|
|
|
61
|
Build the onde Python package using Maturin. |
|
62
|
|
|
63
|
Options: |
|
64
|
--release Build a release wheel (default). |
|
65
|
--develop Install the package into the current virtualenv |
|
66
|
using \`maturin develop\` (editable/debug build). |
|
67
|
--publish Build and publish to PyPI (skips sdist). |
|
68
|
--help, -h Show this help message. |
|
69
|
|
|
70
|
Environment: |
|
71
|
ONDE_FEATURES Extra Cargo features, e.g. "whisper" |
|
72
|
CARGO Path to cargo (default: cargo) |
|
73
|
PYTHON Path to python3 (default: python3) |
|
74
|
MATURIN Path to maturin (default: auto-detected) |
|
75
|
UNIFFI_BINDGEN Path to uniffi-bindgen (default: auto-detected) |
|
76
|
|
|
77
|
Examples: |
|
78
|
# Build a release wheel |
|
79
|
./build-python.sh |
|
80
|
|
|
81
|
# Development install into current venv |
|
82
|
./build-python.sh --develop |
|
83
|
|
|
84
|
# Build with whisper feature enabled |
|
85
|
ONDE_FEATURES=whisper ./build-python.sh |
|
86
|
|
|
87
|
# Publish to PyPI (requires MATURIN_PYPI_TOKEN or ~/.pypirc) |
|
88
|
./build-python.sh --publish |
|
89
|
EOF |
|
90
|
exit 0 |
|
91
|
} |
|
92
|
|
|
93
|
# ── Argument parsing ────────────────────────────────────────────────────────── |
|
94
|
|
|
95
|
MODE="release" |
|
96
|
|
|
97
|
while [[ $# -gt 0 ]]; do |
|
98
|
case "$1" in |
|
99
|
--release) |
|
100
|
MODE="release" |
|
101
|
shift |
|
102
|
;; |
|
103
|
--develop) |
|
104
|
MODE="develop" |
|
105
|
shift |
|
106
|
;; |
|
107
|
--publish) |
|
108
|
MODE="publish" |
|
109
|
shift |
|
110
|
;; |
|
111
|
--help|-h) |
|
112
|
usage |
|
113
|
;; |
|
114
|
*) |
|
115
|
die "Unknown option: $1 (try --help)" |
|
116
|
;; |
|
117
|
esac |
|
118
|
done |
|
119
|
|
|
120
|
# ── Step 1: Ensure maturin is available ─────────────────────────────────────── |
|
121
|
|
|
122
|
find_or_install_maturin() { |
|
123
|
# 1. Explicit MATURIN env var. |
|
124
|
if [[ -n "${MATURIN:-}" ]]; then |
|
125
|
if command -v "$MATURIN" &>/dev/null; then |
|
126
|
echo "$MATURIN" |
|
127
|
return |
|
128
|
else |
|
129
|
die "MATURIN is set to '$MATURIN' but it is not executable." |
|
130
|
fi |
|
131
|
fi |
|
132
|
|
|
133
|
# 2. Already on PATH. |
|
134
|
if command -v maturin &>/dev/null; then |
|
135
|
echo "maturin" |
|
136
|
return |
|
137
|
fi |
|
138
|
|
|
139
|
# 3. Try installing via uv (preferred — fast, no venv needed). |
|
140
|
if command -v uv &>/dev/null; then |
|
141
|
warn "maturin not found. Installing via ${BOLD}uv tool install maturin${NC}..." |
|
142
|
uv tool install maturin >/dev/null 2>&1 || true |
|
143
|
if command -v maturin &>/dev/null; then |
|
144
|
echo "maturin" |
|
145
|
return |
|
146
|
fi |
|
147
|
fi |
|
148
|
|
|
149
|
# 4. Try installing via pip. |
|
150
|
if command -v pip3 &>/dev/null; then |
|
151
|
warn "maturin not found. Installing via ${BOLD}pip3 install maturin${NC}..." |
|
152
|
pip3 install maturin >/dev/null 2>&1 || true |
|
153
|
if command -v maturin &>/dev/null; then |
|
154
|
echo "maturin" |
|
155
|
return |
|
156
|
fi |
|
157
|
elif command -v pip &>/dev/null; then |
|
158
|
warn "maturin not found. Installing via ${BOLD}pip install maturin${NC}..." |
|
159
|
pip install maturin >/dev/null 2>&1 || true |
|
160
|
if command -v maturin &>/dev/null; then |
|
161
|
echo "maturin" |
|
162
|
return |
|
163
|
fi |
|
164
|
fi |
|
165
|
|
|
166
|
return 1 |
|
167
|
} |
|
168
|
|
|
169
|
log "Checking for ${BOLD}maturin${NC}..." |
|
170
|
|
|
171
|
MATURIN_BIN=$(find_or_install_maturin) || die \ |
|
172
|
"maturin not found and could not be installed.\n" \ |
|
173
|
"Install it manually: ${BOLD}pip install maturin${NC} or ${BOLD}uv tool install maturin${NC}" |
|
174
|
|
|
175
|
MATURIN_VERSION=$($MATURIN_BIN --version 2>/dev/null || echo "unknown") |
|
176
|
ok "maturin ready: ${BOLD}$MATURIN_BIN${NC} ($MATURIN_VERSION)" |
|
177
|
|
|
178
|
# ── Step 2: Ensure uniffi-bindgen is available ──────────────────────────────── |
|
179
|
# |
|
180
|
# Maturin with `bindings = "uniffi"` shells out to `uniffi-bindgen` to generate |
|
181
|
# the Python bindings. The version MUST match the `uniffi` crate version |
|
182
|
# pinned in Cargo.toml (==0.31.0). We check several locations: |
|
183
|
# 1. Explicit UNIFFI_BINDGEN env var |
|
184
|
# 2. Already on PATH |
|
185
|
# 3. The project's local uniffi-bindgen binary (uniffi-bindgen/ crate) |
|
186
|
# 4. pip install uniffi-bindgen==0.31.0 |
|
187
|
|
|
188
|
UNIFFI_VERSION="0.31.0" |
|
189
|
|
|
190
|
find_or_install_uniffi_bindgen() { |
|
191
|
# 1. Explicit env var. |
|
192
|
if [[ -n "${UNIFFI_BINDGEN:-}" ]]; then |
|
193
|
if command -v "$UNIFFI_BINDGEN" &>/dev/null; then |
|
194
|
echo "$UNIFFI_BINDGEN" |
|
195
|
return |
|
196
|
elif [[ -x "$UNIFFI_BINDGEN" ]]; then |
|
197
|
echo "$UNIFFI_BINDGEN" |
|
198
|
return |
|
199
|
else |
|
200
|
die "UNIFFI_BINDGEN is set to '$UNIFFI_BINDGEN' but it is not executable." |
|
201
|
fi |
|
202
|
fi |
|
203
|
|
|
204
|
# 2. Already on PATH. |
|
205
|
if command -v uniffi-bindgen &>/dev/null; then |
|
206
|
echo "uniffi-bindgen" |
|
207
|
return |
|
208
|
fi |
|
209
|
|
|
210
|
# 3. Project-local binary — use it if already built, otherwise build it. |
|
211
|
local local_bin="$CRATE_DIR/uniffi-bindgen/target/release/uniffi-bindgen" |
|
212
|
if [[ -x "$local_bin" ]]; then |
|
213
|
echo "$local_bin" |
|
214
|
return |
|
215
|
fi |
|
216
|
|
|
217
|
# 3b. Binary not built yet — try to compile the local uniffi-bindgen crate. |
|
218
|
local local_crate="$CRATE_DIR/uniffi-bindgen" |
|
219
|
if [[ -f "$local_crate/Cargo.toml" ]]; then |
|
220
|
warn "Local uniffi-bindgen binary not found. Building from ${BOLD}$local_crate${NC}..." |
|
221
|
if $CARGO build --release --manifest-path "$local_crate/Cargo.toml" 2>&1; then |
|
222
|
if [[ -x "$local_bin" ]]; then |
|
223
|
echo "$local_bin" |
|
224
|
return |
|
225
|
fi |
|
226
|
else |
|
227
|
warn "Failed to build local uniffi-bindgen crate, trying other methods..." |
|
228
|
fi |
|
229
|
fi |
|
230
|
|
|
231
|
# 4. Check common pip --user bin directories. |
|
232
|
for candidate in \ |
|
233
|
"$HOME/Library/Python/3.9/bin/uniffi-bindgen" \ |
|
234
|
"$HOME/Library/Python/3.10/bin/uniffi-bindgen" \ |
|
235
|
"$HOME/Library/Python/3.11/bin/uniffi-bindgen" \ |
|
236
|
"$HOME/Library/Python/3.12/bin/uniffi-bindgen" \ |
|
237
|
"$HOME/Library/Python/3.13/bin/uniffi-bindgen" \ |
|
238
|
"$HOME/.local/bin/uniffi-bindgen"; do |
|
239
|
if [[ -x "$candidate" ]]; then |
|
240
|
echo "$candidate" |
|
241
|
return |
|
242
|
fi |
|
243
|
done |
|
244
|
|
|
245
|
# 5. Install via pip. |
|
246
|
warn "uniffi-bindgen not found. Installing ${BOLD}uniffi-bindgen==$UNIFFI_VERSION${NC} via pip..." |
|
247
|
if command -v pip3 &>/dev/null; then |
|
248
|
pip3 install "uniffi-bindgen==$UNIFFI_VERSION" >/dev/null 2>&1 || true |
|
249
|
elif command -v pip &>/dev/null; then |
|
250
|
pip install "uniffi-bindgen==$UNIFFI_VERSION" >/dev/null 2>&1 || true |
|
251
|
fi |
|
252
|
|
|
253
|
# Re-check PATH and common locations after install. |
|
254
|
if command -v uniffi-bindgen &>/dev/null; then |
|
255
|
echo "uniffi-bindgen" |
|
256
|
return |
|
257
|
fi |
|
258
|
for candidate in \ |
|
259
|
"$HOME/Library/Python/3.9/bin/uniffi-bindgen" \ |
|
260
|
"$HOME/Library/Python/3.10/bin/uniffi-bindgen" \ |
|
261
|
"$HOME/Library/Python/3.11/bin/uniffi-bindgen" \ |
|
262
|
"$HOME/Library/Python/3.12/bin/uniffi-bindgen" \ |
|
263
|
"$HOME/Library/Python/3.13/bin/uniffi-bindgen" \ |
|
264
|
"$HOME/.local/bin/uniffi-bindgen"; do |
|
265
|
if [[ -x "$candidate" ]]; then |
|
266
|
echo "$candidate" |
|
267
|
return |
|
268
|
fi |
|
269
|
done |
|
270
|
|
|
271
|
return 1 |
|
272
|
} |
|
273
|
|
|
274
|
log "Checking for ${BOLD}uniffi-bindgen${NC} (==$UNIFFI_VERSION)..." |
|
275
|
|
|
276
|
UNIFFI_BIN=$(find_or_install_uniffi_bindgen) || die \ |
|
277
|
"uniffi-bindgen not found and could not be installed.\n" \ |
|
278
|
"Install it manually: ${BOLD}pip install uniffi-bindgen==$UNIFFI_VERSION${NC}\n" \ |
|
279
|
"The version MUST match the uniffi crate version in Cargo.toml." |
|
280
|
|
|
281
|
# Ensure the discovered binary is on PATH so maturin can find it. |
|
282
|
UNIFFI_BIN_DIR="$(dirname "$UNIFFI_BIN")" |
|
283
|
case ":$PATH:" in |
|
284
|
*":$UNIFFI_BIN_DIR:"*) ;; |
|
285
|
*) export PATH="$UNIFFI_BIN_DIR:$PATH" ;; |
|
286
|
esac |
|
287
|
|
|
288
|
UNIFFI_DETECTED_VERSION=$($UNIFFI_BIN --version 2>/dev/null || echo "unknown") |
|
289
|
ok "uniffi-bindgen ready: ${BOLD}$UNIFFI_BIN${NC} ($UNIFFI_DETECTED_VERSION)" |
|
290
|
|
|
291
|
# ── Step 3: Verify Python & Rust toolchains ─────────────────────────────────── |
|
292
|
|
|
293
|
if ! command -v "$PYTHON" &>/dev/null; then |
|
294
|
die "Python not found at '$PYTHON'. Set the PYTHON env var or install Python 3.8+." |
|
295
|
fi |
|
296
|
|
|
297
|
PYTHON_VERSION=$($PYTHON --version 2>&1) |
|
298
|
log "Python: ${BOLD}$PYTHON_VERSION${NC}" |
|
299
|
|
|
300
|
if ! command -v "$CARGO" &>/dev/null; then |
|
301
|
die "Cargo not found at '$CARGO'. Install Rust: https://rustup.rs" |
|
302
|
fi |
|
303
|
|
|
304
|
CARGO_VERSION=$($CARGO --version 2>&1) |
|
305
|
log "Cargo: ${BOLD}$CARGO_VERSION${NC}" |
|
306
|
|
|
307
|
# ── Step 4: Prepare feature flags ──────────────────────────────────────────── |
|
308
|
|
|
309
|
FEATURES_ARGS=() |
|
310
|
if [[ -n "${ONDE_FEATURES:-}" ]]; then |
|
311
|
# Split comma-separated features and pass each via --features |
|
312
|
IFS=',' read -ra FEAT_LIST <<< "$ONDE_FEATURES" |
|
313
|
for feat in "${FEAT_LIST[@]}"; do |
|
314
|
feat=$(echo "$feat" | xargs) # trim whitespace |
|
315
|
if [[ -n "$feat" ]]; then |
|
316
|
FEATURES_ARGS+=("--cargo-extra-args=--features=$feat") |
|
317
|
fi |
|
318
|
done |
|
319
|
log "Features: ${BOLD}${ONDE_FEATURES}${NC}" |
|
320
|
fi |
|
321
|
|
|
322
|
# ── Step 5: Build / develop / publish ───────────────────────────────────────── |
|
323
|
|
|
324
|
cd "$PYTHON_DIR" |
|
325
|
|
|
326
|
if [[ "$MODE" == "develop" ]]; then |
|
327
|
# ── Development install ─────────────────────────────────────────────── |
|
328
|
log "Running ${BOLD}maturin develop${NC} (editable install into current venv)..." |
|
329
|
echo "" |
|
330
|
|
|
331
|
$MATURIN_BIN develop \ |
|
332
|
--manifest-path "$CRATE_DIR/Cargo.toml" \ |
|
333
|
"${FEATURES_ARGS[@]+"${FEATURES_ARGS[@]}"}" |
|
334
|
|
|
335
|
echo "" |
|
336
|
ok "Development install complete." |
|
337
|
ok "The ${BOLD}onde_inference${NC} package is now available in your current Python environment." |
|
338
|
|
|
339
|
elif [[ "$MODE" == "publish" ]]; then |
|
340
|
# ── Publish to PyPI ─────────────────────────────────────────────────── |
|
341
|
# All Rust deps use git refs (no local path deps), and |
|
342
|
# uniffi-bindgen==0.31.0 is in [build-system] requires so PEP 517 |
|
343
|
# isolated builds install it automatically. |
|
344
|
log "Running ${BOLD}maturin publish${NC}..." |
|
345
|
echo "" |
|
346
|
|
|
347
|
$MATURIN_BIN publish \ |
|
348
|
--manifest-path "$CRATE_DIR/Cargo.toml" \ |
|
349
|
--skip-existing \ |
|
350
|
--compatibility pypi \ |
|
351
|
"${FEATURES_ARGS[@]+"${FEATURES_ARGS[@]}"}" |
|
352
|
|
|
353
|
echo "" |
|
354
|
ok "Published to PyPI." |
|
355
|
|
|
356
|
else |
|
357
|
# ── Release wheel build ─────────────────────────────────────────────── |
|
358
|
log "Running ${BOLD}maturin build --release${NC}..." |
|
359
|
echo "" |
|
360
|
|
|
361
|
$MATURIN_BIN build \ |
|
362
|
--release \ |
|
363
|
--manifest-path "$CRATE_DIR/Cargo.toml" \ |
|
364
|
--out "$PYTHON_DIR/dist" \ |
|
365
|
"${FEATURES_ARGS[@]+"${FEATURES_ARGS[@]}"}" |
|
366
|
|
|
367
|
echo "" |
|
368
|
|
|
369
|
# Find the built wheel(s). |
|
370
|
WHEEL_DIR="$PYTHON_DIR/dist" |
|
371
|
if [[ -d "$WHEEL_DIR" ]]; then |
|
372
|
WHEEL_FILES=() |
|
373
|
while IFS= read -r -d '' whl; do |
|
374
|
WHEEL_FILES+=("$whl") |
|
375
|
done < <(find "$WHEEL_DIR" -name '*.whl' -newer "$PYTHON_DIR" -print0 2>/dev/null || true) |
|
376
|
|
|
377
|
# If the timestamp trick didn't find any, just list all wheels. |
|
378
|
if [[ ${#WHEEL_FILES[@]} -eq 0 ]]; then |
|
379
|
while IFS= read -r -d '' whl; do |
|
380
|
WHEEL_FILES+=("$whl") |
|
381
|
done < <(find "$WHEEL_DIR" -name '*.whl' -print0 2>/dev/null || true) |
|
382
|
fi |
|
383
|
fi |
|
384
|
fi |
|
385
|
|
|
386
|
# ── Step 6: Summary ────────────────────────────────────────────────────────── |
|
387
|
|
|
388
|
echo "" |
|
389
|
ok "════════════════════════════════════════════════════════════════════" |
|
390
|
ok " Onde Python package build complete!" |
|
391
|
ok "════════════════════════════════════════════════════════════════════" |
|
392
|
echo "" |
|
393
|
|
|
394
|
if [[ "$MODE" == "develop" ]]; then |
|
395
|
log "Mode: ${BOLD}develop${NC} (editable install)" |
|
396
|
log "Python: ${BOLD}$PYTHON_VERSION${NC}" |
|
397
|
echo "" |
|
398
|
log "Verify the install:" |
|
399
|
log " ${BOLD}$PYTHON -c \"from onde_inference import __version__; print(__version__)\"${NC}" |
|
400
|
|
|
401
|
elif [[ "$MODE" == "publish" ]]; then |
|
402
|
log "Mode: ${BOLD}publish${NC} (PyPI)" |
|
403
|
log "Python: ${BOLD}$PYTHON_VERSION${NC}" |
|
404
|
echo "" |
|
405
|
log "Install from PyPI:" |
|
406
|
log " ${BOLD}pip install onde-inference${NC}" |
|
407
|
|
|
408
|
else |
|
409
|
log "Mode: ${BOLD}release${NC} (wheel)" |
|
410
|
log "Python: ${BOLD}$PYTHON_VERSION${NC}" |
|
411
|
|
|
412
|
if [[ ${#WHEEL_FILES[@]} -gt 0 ]]; then |
|
413
|
echo "" |
|
414
|
log "Built wheel(s):" |
|
415
|
for whl in "${WHEEL_FILES[@]}"; do |
|
416
|
SIZE=$(du -h "$whl" | cut -f1) |
|
417
|
ok " $(basename "$whl") (${SIZE})" |
|
418
|
log " ${BOLD}$whl${NC}" |
|
419
|
done |
|
420
|
else |
|
421
|
warn "No .whl files found in $WHEEL_DIR" |
|
422
|
fi |
|
423
|
|
|
424
|
echo "" |
|
425
|
log "Install the wheel:" |
|
426
|
if [[ ${#WHEEL_FILES[@]} -gt 0 ]]; then |
|
427
|
log " ${BOLD}pip install ${WHEEL_FILES[-1]}${NC}" |
|
428
|
else |
|
429
|
log " ${BOLD}pip install $WHEEL_DIR/<wheel-file>.whl${NC}" |
|
430
|
fi |
|
431
|
fi |
|
432
|
|
|
433
|
if [[ -n "${ONDE_FEATURES:-}" ]]; then |
|
434
|
echo "" |
|
435
|
log "Features: ${BOLD}${ONDE_FEATURES}${NC}" |
|
436
|
fi |
|
437
|
|
|
438
|
echo "" |