2026-07-16 19:40:21 +03:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
set -Eeuo pipefail
|
|
|
|
|
|
|
|
|
|
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd -P)"
|
|
|
|
|
cd -- "$repo_root"
|
|
|
|
|
|
2026-07-16 22:09:38 +03:00
|
|
|
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
|
|
|
|
|
|
2026-07-16 19:40:21 +03:00
|
|
|
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"
|
2026-07-16 22:09:38 +03:00
|
|
|
mkdir -p -- \
|
2026-07-17 14:41:10 +03:00
|
|
|
"$package_dir/assets/icon" \
|
2026-07-16 22:09:38 +03:00
|
|
|
"$package_dir/lib" \
|
|
|
|
|
"$package_dir/scripts" \
|
|
|
|
|
"$package_dir/third-party-licenses"
|
2026-07-16 19:40:21 +03:00
|
|
|
|
|
|
|
|
# The Sherpa Go module embeds an absolute build-machine RPATH. Add a portable
|
2026-07-16 22:09:38 +03:00
|
|
|
# build-time fallback; the final paths are normalized after dependency copying.
|
2026-07-17 19:35:08 +03:00
|
|
|
# gtk4-layer-shell must precede libwayland-client so its Wayland shim installs.
|
2026-07-16 19:40:21 +03:00
|
|
|
# shellcheck disable=SC2016 # $ORIGIN must remain literal for the ELF loader.
|
2026-07-17 19:35:08 +03:00
|
|
|
CGO_LDFLAGS='-Wl,--no-as-needed -lgtk4-layer-shell -Wl,-rpath,$ORIGIN/lib' \
|
2026-07-16 22:09:38 +03:00
|
|
|
go build -trimpath -o "$package_dir/captioneer" ./src/cmd/captioneer
|
|
|
|
|
# shellcheck disable=SC2016 # $ORIGIN must remain literal for the ELF loader.
|
2026-07-17 19:35:08 +03:00
|
|
|
CGO_LDFLAGS='-Wl,--no-as-needed -lgtk4-layer-shell -Wl,-rpath,$ORIGIN/lib' \
|
2026-07-16 22:09:38 +03:00
|
|
|
go build -trimpath -tags gtk \
|
|
|
|
|
-o "$package_dir/captioneer-desktop" ./src/cmd/captioneer-desktop
|
2026-07-16 19:40:21 +03:00
|
|
|
|
|
|
|
|
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"
|
2026-07-16 22:09:38 +03:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2026-07-16 19:40:21 +03:00
|
|
|
cp -- README.md LICENSE "$package_dir/"
|
2026-07-17 14:41:10 +03:00
|
|
|
cp -- assets/icon/icon.png "$package_dir/assets/icon/"
|
2026-07-16 19:40:21 +03:00
|
|
|
cp -- scripts/download-models.sh "$package_dir/scripts/"
|
|
|
|
|
|
|
|
|
|
for executable in captioneer captioneer-desktop; do
|
2026-07-16 22:09:38 +03:00
|
|
|
dynamic_section="$(readelf -d "$package_dir/$executable")"
|
2026-07-16 19:40:21 +03:00
|
|
|
# shellcheck disable=SC2016 # Verify the literal ELF-loader token.
|
2026-07-16 22:09:38 +03:00
|
|
|
if [[ "$dynamic_section" != *'$ORIGIN/lib'* ]]; then
|
|
|
|
|
printf 'captioneer: %s is missing its private-runtime RUNPATH\n' \
|
|
|
|
|
"$executable" >&2
|
2026-07-16 19:40:21 +03:00
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
done
|
2026-07-16 22:09:38 +03:00
|
|
|
[[ -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
|
2026-07-16 19:40:21 +03:00
|
|
|
|
|
|
|
|
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"
|