#!/bin/bash
set -euo pipefail

# #1251：从源码构建 sherpa-onnx 的 macOS / iOS xcframework（onnxruntime 静态并入）。
#
# 为什么不用上游 release 的预编译包：上游 `*-shared-onnxruntime-static.xcframework.zip` 打开了 TTS，
# 把 espeak-ng（GPL-3.0）静态链进了同一个库。闭源 App 里不能分发它。这里跑的是上游同名的两个
# 构建脚本，只多加两个开关：
#
#   -DSHERPA_ONNX_ENABLE_TTS=OFF                  不编 TTS，espeak-ng / piper-phonemize 不进产物
#   -DSHERPA_ONNX_ENABLE_SPEAKER_DIARIZATION=OFF  不编说话人分离（hclust-cpp），用不到
#
# 其余与上游完全一致：onnxruntime 用上游脚本指定的 csukuangfj/onnxruntime-libs v1.28.2 静态库，
# 各依赖由上游 cmake 按 URL_HASH 取回。每个上游输入都钉 SHA256，构建后断言：
#   - 产物里 TTS / 说话人分离 / espeak / piper 相关符号为 0（计数，空检查不算通过）
#   - 识别用到的 C API 符号都在（正向对照，防止「什么都没编进去」也判通过）
#   - 除系统库外没有动态依赖
#
# 用法：SherpaOnnxPackage/build.sh [输出目录]   默认 SherpaOnnxPackage/Artifacts
#   DEVELOPER_DIR 默认 /Applications/Xcode_26.6.app/Contents/Developer（与提审构建同一套 Xcode）。
# 产物 zip 的 SHA256 写进 Package.swift 与 Materials/artifact-checksums.sha256 之前，先按
# Materials/distribution.md 的不可变规则上传。

package_dir="$(cd "$(dirname "$0")" && pwd)"
out_dir="${1:-$package_dir/Artifacts}"
export DEVELOPER_DIR="${DEVELOPER_DIR:-/Applications/Xcode_26.6.app/Contents/Developer}"
# 上游 macOS 脚本不设部署目标，不设就会落到构建机 SDK 的版本；取上游 CI 产物的同一个值。
export MACOSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-11.0}"

# 源码取 tag v1.13.8 的 GitHub 归档（tag 指向 11afbd009a7f8c08f4bcf2fc1b265d0df4670fbf），按 SHA256 校验。
sherpa_version="1.13.8"
ort_version="1.28.2"
ort_base="https://github.com/csukuangfj/onnxruntime-libs/releases/download/v$ort_version"
ort_macos_zip="onnxruntime-osx-universal2-static_lib-$ort_version.zip"
ort_ios_zip="onnxruntime-ios-static-xcframework-$ort_version.xcframework.zip"

work="${SHERPA_BUILD_WORK:-$(mktemp -d "${TMPDIR:-/tmp}/conflux-sherpa-build.XXXXXX")}"
mkdir -p "$work" "$out_dir"
echo "工作目录：$work"
echo "Xcode：$(xcodebuild -version | head -1)（${DEVELOPER_DIR}）"

fetch() { # <url> <file>
    local name want got
    name="$(basename "$2")"
    want="$(awk -v n="$name" '$2 == n {print $1}' "$package_dir/Materials/upstream-input-checksums.sha256")"
    [[ -n "$want" ]] || { echo "FAIL: upstream-input-checksums.sha256 里没有 $name" >&2; exit 1; }
    [[ -f "$2" ]] || curl -fsSL --retry 3 -o "$2" "$1"
    got="$(shasum -a 256 "$2" | awk '{print $1}')"
    [[ "$got" == "$want" ]] || { echo "FAIL: $name 的 SHA256 是 ${got}，期望 $want" >&2; exit 1; }
}

# ---- 上游输入 ----

fetch "https://github.com/k2-fsa/sherpa-onnx/archive/refs/tags/v$sherpa_version.tar.gz" "$work/sherpa-onnx-v$sherpa_version.tar.gz"
fetch "$ort_base/$ort_macos_zip" "$work/$ort_macos_zip"
fetch "$ort_base/$ort_ios_zip" "$work/$ort_ios_zip"

src="$work/sherpa-onnx-$sherpa_version"
# SHERPA_BUILD_RESUME=1：接着上一次中断的构建跑（上游脚本对已编好的切片跳过、其余增量编译）。
# 默认从干净的源码树开始。
if [[ "${SHERPA_BUILD_RESUME:-0}" != 1 || ! -d "$src" ]]; then
    rm -rf "$src"
    tar xzf "$work/sherpa-onnx-v$sherpa_version.tar.gz" -C "$work"
fi
grep -q "set(SHERPA_ONNX_VERSION \"$sherpa_version\")" "$src/CMakeLists.txt" \
    || { echo "FAIL: 源码归档的版本不是 $sherpa_version" >&2; exit 1; }

# 上游脚本自己用 wget 下 onnxruntime；预先放好已校验的文件，它就不再下载。
ort_macos_dir="$src/build-macos-shared-sherpa-with-static-onnxruntime/onnxruntime-static/$ort_version"
if [[ ! -f "$ort_macos_dir/lib/libonnxruntime.a" ]]; then
    mkdir -p "$ort_macos_dir"
    (cd "$ort_macos_dir" && unzip -q "$work/$ort_macos_zip" && mv "${ort_macos_zip%.zip}"/* . && rmdir "${ort_macos_zip%.zip}")
fi
ort_ios_dir="$src/build-ios-shared-sherpa-with-static-onnxruntime/ios-onnxruntime/$ort_version"
if [[ ! -d "$ort_ios_dir/onnxruntime.xcframework" ]]; then
    mkdir -p "$ort_ios_dir"
    (cd "$ort_ios_dir" && unzip -q "$work/$ort_ios_zip")
fi
ln -sfn "$ort_version/onnxruntime.xcframework" "$src/build-ios-shared-sherpa-with-static-onnxruntime/ios-onnxruntime/onnxruntime.xcframework"

# ---- 关掉 TTS 与说话人分离 ----

for script in build-macos-shared-sherpa-with-static-onnxruntime.sh build-ios-shared-sherpa-with-static-onnxruntime.sh; do
    before="$(grep -c -- '-DSHERPA_ONNX_ENABLE_BINARY=OFF' "$src/$script")"
    sed -i '' 's/-DSHERPA_ONNX_ENABLE_BINARY=OFF \\/-DSHERPA_ONNX_ENABLE_BINARY=OFF -DSHERPA_ONNX_ENABLE_TTS=OFF -DSHERPA_ONNX_ENABLE_SPEAKER_DIARIZATION=OFF \\/' "$src/$script"
    after="$(grep -c -- '-DSHERPA_ONNX_ENABLE_TTS=OFF -DSHERPA_ONNX_ENABLE_SPEAKER_DIARIZATION=OFF' "$src/$script")"
    [[ "$before" -gt 0 && "$after" -eq "$before" ]] \
        || { echo "FAIL: $script 里 cmake 调用 $before 处，改到 $after 处" >&2; exit 1; }
done

# 不给 SDKROOT 时 /usr/bin/cc 会拿本机 Command Line Tools 的 SDK（与所选 Xcode 不一致，
# 版本不同时链接探测直接失败）。iOS 由上游 toolchain 按平台选 SDK。
macos_sdk="$(xcrun --sdk macosx --show-sdk-path)"
(cd "$src" && SDKROOT="$macos_sdk" ./build-macos-shared-sherpa-with-static-onnxruntime.sh)
(cd "$src" && ./build-ios-shared-sherpa-with-static-onnxruntime.sh)

macos_zip="$src/build-macos-shared-sherpa-with-static-onnxruntime/sherpa-onnx-v$sherpa_version-macos-shared-onnxruntime-static.xcframework.zip"
ios_zip="$src/build-ios-shared-sherpa-with-static-onnxruntime/sherpa-onnx-v$sherpa_version-ios-shared-onnxruntime-static.xcframework.zip"

# ---- 断言 ----

binaries=(
    "$src/build-macos-shared-sherpa-with-static-onnxruntime/sherpa-onnx.xcframework/macos-arm64_x86_64/libsherpa-onnx-c-api.dylib"
    "$src/build-ios-shared-sherpa-with-static-onnxruntime/SherpaOnnxC.xcframework/ios-arm64/SherpaOnnxC.framework/SherpaOnnxC"
    "$src/build-ios-shared-sherpa-with-static-onnxruntime/SherpaOnnxC.xcframework/ios-arm64_x86_64-simulator/SherpaOnnxC.framework/SherpaOnnxC"
)
# 用 strings 而不是 nm：iOS 产物 strip 过，本地符号只能从字符串表里看出来（上游包里的 espeak-ng
# 就是这样被发现的）。按架构逐个查，区分大小写。
# 只查实现，不查 C API 名：关掉 TTS / 说话人分离后，c-api 仍导出 `SherpaOnnxCreateOfflineTts` 等
# 返回空的桩函数。`[^w]espeak` 排除 wespeaker（说话人嵌入模型的名字）。
# 对照：同一模式在上游官方包（打开了 TTS）上命中 macOS arm64 50 处、iOS arm64 60 处。
forbidden='(^|[^w])espeak|[Pp]iper|phontab|Kokoro|Matcha[A-Z]|Vits[A-Z]|Kitten|hclust|OfflineTtsImpl|OfflineTtsModel|DiarizationPyannote|OfflineSpeakerDiarizationImpl'
required=(SherpaOnnxCreateOfflineRecognizer SherpaOnnxCreateVoiceActivityDetector SherpaOnnxCreateLinearResampler SherpaOnnxDecodeOfflineStream)
for binary in "${binaries[@]}"; do
    [[ -f "$binary" ]] || { echo "FAIL: 缺产物 $binary" >&2; exit 1; }
    for arch in $(lipo -archs "$binary"); do
        rm -f "$work/thin"
        # 单架构的切片（ios-arm64）不是 fat 文件，`lipo -thin` 会失败；那就直接查它本身。
        if [[ "$(lipo -archs "$binary")" == "$arch" ]]; then
            cp "$binary" "$work/thin"
        else
            lipo "$binary" -thin "$arch" -output "$work/thin"
        fi
        # 先把字符串表落盘：strings 自己失败时要中止，不能让 grep -c 输出 0 冒充「没有命中」。
        strings -a "$work/thin" > "$work/strings.txt" || { echo "FAIL: strings 读不了 $binary ($arch)" >&2; exit 1; }
        [[ -s "$work/strings.txt" ]] || { echo "FAIL: $binary ($arch) 的字符串表为空" >&2; exit 1; }
        hits="$(grep -cE "$forbidden" "$work/strings.txt" || true)"
        [[ "$hits" -eq 0 ]] || {
            grep -E "$forbidden" "$work/strings.txt" | head -20 >&2
            echo "FAIL: $binary ($arch) 里仍有 ${hits} 处 TTS / 说话人分离相关字符串" >&2
            exit 1
        }
        sdk="$(otool -l "$work/thin" | awk '/LC_BUILD_VERSION/{f=1} f && $1 == "sdk" {print $2; exit}')"
        case "$binary" in
            *macos*) want_sdk="$(xcrun --sdk macosx --show-sdk-version)" ;;
            *simulator*) want_sdk="$(xcrun --sdk iphonesimulator --show-sdk-version)" ;;
            *) want_sdk="$(xcrun --sdk iphoneos --show-sdk-version)" ;;
        esac
        [[ "$sdk" == "$want_sdk" ]] || { echo "FAIL: $binary ($arch) 用 SDK ${sdk} 构建，期望所选 Xcode 的 ${want_sdk}" >&2; exit 1; }
        exported="$(nm -gU "$work/thin" | awk '{print $NF}')"
        for symbol in "${required[@]}"; do
            grep -qx "_$symbol" <<<"$exported" || { echo "FAIL: $binary ($arch) 缺导出符号 $symbol" >&2; exit 1; }
        done
        # 按架构拆开后第一行是文件名本身；再往下是 install name 与依赖
        unexpected="$(otool -L "$work/thin" | tail -n +2 | awk '{print $1}' \
            | grep -vE '^(@rpath/(libsherpa-onnx-c-api\.dylib|SherpaOnnxC\.framework/SherpaOnnxC)|/usr/lib/|/System/Library/)' || true)"
        [[ -z "$unexpected" ]] || { echo "FAIL: $binary ($arch) 有非系统动态依赖：$unexpected" >&2; exit 1; }
        echo "  ✓ $(basename "$binary") ${arch}：禁用字符串 0 处，${#required[@]} 个必需导出符号齐全，只依赖系统库"
    done
done

"$package_dir/audit-licenses.sh" "$src"

cp "$macos_zip" "$ios_zip" "$out_dir/"
echo ""
echo "产物："
(cd "$out_dir" && shasum -a 256 "$(basename "$macos_zip")" "$(basename "$ios_zip")" && ls -l "$(basename "$macos_zip")" "$(basename "$ios_zip")")
echo "构建目录保留在 ${work}（许可审计读其中的 _deps）"
