#!/usr/bin/env bash set -Eeuo pipefail repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd -P)" cd -- "$repo_root" for command in go ldd patchelf readelf rpm; do command -v "$command" >/dev/null 2>&1 || { printf 'captioneer: %s is required to build a release archive\n' "$command" >&2 exit 1 } done tag=${1:-} if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then printf 'captioneer: release tag must match vMAJOR.MINOR.BUG, got %q\n' "$tag" >&2 exit 1 fi platform=linux-amd64 archive_name="captioneer-${tag}-${platform}.tar.gz" package_name="captioneer-${tag}-${platform}" package_dir="dist/$package_name" rm -rf -- "$package_dir" rm -f -- "dist/$archive_name" "dist/$archive_name.sha256" mkdir -p -- \ "$package_dir/assets/icon" \ "$package_dir/lib" \ "$package_dir/scripts" \ "$package_dir/third-party-licenses" # The Sherpa Go module embeds an absolute build-machine RPATH. Add a portable # build-time fallback; the final paths are normalized after dependency copying. # shellcheck disable=SC2016 # $ORIGIN must remain literal for the ELF loader. CGO_LDFLAGS='-Wl,-rpath,$ORIGIN/lib' \ go build -trimpath -o "$package_dir/captioneer" ./src/cmd/captioneer # shellcheck disable=SC2016 # $ORIGIN must remain literal for the ELF loader. CGO_LDFLAGS='-Wl,-rpath,$ORIGIN/lib' \ go build -trimpath -tags gtk \ -o "$package_dir/captioneer-desktop" ./src/cmd/captioneer-desktop sherpa_dir="$(go list -m -f '{{.Dir}}' github.com/k2-fsa/sherpa-onnx-go-linux)" sherpa_lib_dir="$sherpa_dir/lib/x86_64-unknown-linux-gnu" declare -A bundled_libraries=() declare -A copied_license_packages=() manifest_entries="$(mktemp)" trap 'rm -f -- "$manifest_entries"' EXIT is_host_runtime_library() { case "$1" in ld-linux*.so* | libc.so.* | libm.so.* | libmvec.so.* | libdl.so.* | \ libpthread.so.* | librt.so.* | libresolv.so.* | libanl.so.* | \ libutil.so.* | libnss_*.so.* | libthread_db.so.* | libBrokenLocale.so.*) return 0 ;; esac return 1 } copy_package_licenses() { local package=$1 local license_path relative_path destination [[ -z "${copied_license_packages[$package]:-}" ]] || return 0 copied_license_packages["$package"]=1 while IFS= read -r license_path; do [[ -f "$license_path" && "$license_path" == /usr/share/licenses/* ]] || continue relative_path="${license_path#/usr/share/licenses/}" destination="$package_dir/third-party-licenses/$relative_path" mkdir -p -- "$(dirname -- "$destination")" cp -L -- "$license_path" "$destination" done < <(rpm -ql "$package" 2>/dev/null || true) } bundle_library() { local source=$1 local provider=${2:-} local library_name resolved_source destination package library_name="$(basename -- "$source")" is_host_runtime_library "$library_name" && return resolved_source="$(readlink -f -- "$source")" [[ -f "$resolved_source" ]] || { printf 'captioneer: runtime library not found: %s\n' "$source" >&2 exit 1 } if [[ -n "${bundled_libraries[$library_name]:-}" ]]; then if ! cmp -s -- "$resolved_source" "${bundled_libraries[$library_name]}"; then printf 'captioneer: conflicting runtime libraries named %s\n' "$library_name" >&2 exit 1 fi return fi destination="$package_dir/lib/$library_name" cp -L -- "$resolved_source" "$destination" bundled_libraries["$library_name"]="$resolved_source" if [[ -z "$provider" ]] && command -v rpm >/dev/null 2>&1; then package="$(rpm -qf --queryformat '%{NAME}\n' "$resolved_source" 2>/dev/null | head -n 1 || true)" if [[ -n "$package" ]]; then provider="$(rpm -q --queryformat '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}' "$package")" copy_package_licenses "$package" fi fi provider=${provider:-unidentified-system-package} printf '%s\t%s\n' "$library_name" "$provider" >> "$manifest_entries" } bundle_library "$sherpa_lib_dir/libsherpa-onnx-c-api.so" \ 'github.com/k2-fsa/sherpa-onnx-go-linux' bundle_library "$sherpa_lib_dir/libonnxruntime.so" \ 'github.com/k2-fsa/sherpa-onnx-go-linux' # ldd reports the complete linked dependency closure. Copy everything needed by # either command except glibc and its loader, which must match the host system. for executable in \ "$package_dir/captioneer" \ "$package_dir/captioneer-desktop"; do ldd_output="$(ldd "$executable")" if grep -Fq 'not found' <<< "$ldd_output"; then printf 'captioneer: unresolved release dependencies for %s:\n%s\n' \ "$executable" "$ldd_output" >&2 exit 1 fi while IFS= read -r library_path; do [[ -n "$library_path" ]] || continue if [[ "$library_path" == "$repo_root/$package_dir/lib/"* ]]; then continue fi bundle_library "$library_path" done < <( awk ' $2 == "=>" && $3 ~ /^\// { print $3 } $1 ~ /^\// { print $1 } ' <<< "$ldd_output" ) done { printf 'Library\tProvider\n' sort -u -- "$manifest_entries" } > "$package_dir/RUNTIME-LIBRARIES.tsv" # Give every object a location-relative path. Patching each library is required # because an upstream RUNPATH (for example, Tinysparql's Fedora path) otherwise # prevents the executable's path from resolving that library's own children. # This avoids LD_LIBRARY_PATH, which would leak into spawned pactl/parec tools. for library in "$package_dir"/lib/*; do chmod u+w -- "$library" # shellcheck disable=SC2016 # $ORIGIN must remain literal for the ELF loader. patchelf --set-rpath '$ORIGIN' "$library" chmod 0755 -- "$library" done for executable in captioneer captioneer-desktop; do # shellcheck disable=SC2016 # $ORIGIN must remain literal for the ELF loader. patchelf --set-rpath '$ORIGIN/lib' "$package_dir/$executable" done cp -- README.md LICENSE "$package_dir/" cp -- assets/icon/icon.png "$package_dir/assets/icon/" cp -- scripts/download-models.sh "$package_dir/scripts/" for executable in captioneer captioneer-desktop; do dynamic_section="$(readelf -d "$package_dir/$executable")" # shellcheck disable=SC2016 # Verify the literal ELF-loader token. if [[ "$dynamic_section" != *'$ORIGIN/lib'* ]]; then printf 'captioneer: %s is missing its private-runtime RUNPATH\n' \ "$executable" >&2 exit 1 fi done [[ -f "$package_dir/lib/libgtk4-layer-shell.so.0" ]] || { printf 'captioneer: GTK4 Layer Shell was not included in the release runtime\n' >&2 exit 1 } for library in "$package_dir"/lib/*; do # shellcheck disable=SC2016 # Verify the literal ELF-loader token. [[ "$(patchelf --print-rpath "$library")" == '$ORIGIN' ]] || { printf 'captioneer: %s is missing its private dependency RUNPATH\n' \ "$(basename -- "$library")" >&2 exit 1 } done # Verify the archive itself contains every linked non-glibc dependency instead # of allowing the build container's installed libraries to mask an omission. for object in \ "$package_dir/captioneer" \ "$package_dir/captioneer-desktop" \ "$package_dir"/lib/*; do while IFS= read -r needed_library; do is_host_runtime_library "$needed_library" && continue [[ -f "$package_dir/lib/$needed_library" ]] || { printf 'captioneer: %s requires unbundled library %s\n' \ "$(basename -- "$object")" "$needed_library" >&2 exit 1 } done < <(patchelf --print-needed "$object") done # --help exits before display/model initialization while still exercising the # dynamic loader through the same executable release users invoke. "$package_dir/captioneer-desktop" --help >/dev/null 2>&1 tar -C dist -czf "dist/$archive_name" "$package_name" ( cd -- dist sha256sum "$archive_name" > "$archive_name.sha256" ) printf '%s\n' "$repo_root/dist/$archive_name" printf '%s\n' "$repo_root/dist/$archive_name.sha256"