The device-source parser assumed pactl list short sources is strictly tab-separated, true for stock PulseAudio but not guaranteed for a TV's own heavily customized audio stack (this one names sources tpcm_output/tpmedia/tptts/... — clearly not vanilla). A different separator would have silently produced zero parsed sources with no error, and the picker's own `when` guard would then just hide the row entirely rather than show anything broken. Split on any whitespace run instead of a literal tab; source names never contain embedded whitespace, so this is strictly more permissive with no new failure mode. Confirmed end to end on real hardware: tptts.monitor lit up during the accessibility voice guide and reached HyperHDR. Also: the System panel now shows the app's actual running version, read from a <meta> tag substituted at package time (tools/build.sh stage()) from frontend/appinfo.json — not hand-maintained, so it can't drift from what was actually built. Requested after a version bump alone wasn't enough to tell whether a reinstall had truly picked up new files versus served something cached along the way; this settles that question by inspection instead of by inference. Bumped to 1.0.2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
237 lines
7.6 KiB
Bash
Executable File
237 lines
7.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Builds the native service, assembles the package layout and produces the ipk.
|
|
#
|
|
# ./tools/build.sh # native + stage + package
|
|
# ./tools/build.sh native # cross-compile the service only
|
|
# ./tools/build.sh package # assemble and run ares-package
|
|
# ./tools/build.sh install # ares-install the ipk on the TV
|
|
# ./tools/build.sh launch # ares-launch the app
|
|
# ./tools/build.sh logs # tail the service log over ssh
|
|
# ./tools/build.sh clean
|
|
#
|
|
# Needs the webOS NDK (arm-webos-linux-gnueabi buildroot SDK) for the native
|
|
# part and ares-cli for the packaging part. Point WEBOS_SDK at the SDK if it is
|
|
# not in the usual place; set DEVICE to the ares device name (default: tv).
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
cd "$ROOT"
|
|
|
|
# `npm install` puts ares-cli here rather than on PATH, so a local install works
|
|
# without also needing the global one.
|
|
[ -d "$ROOT/node_modules/.bin" ] && PATH="$ROOT/node_modules/.bin:$PATH"
|
|
|
|
APP_ID=org.webosbrew.audiocap
|
|
SERVICE_ID=$APP_ID.service
|
|
BINARY=audiocap-service
|
|
|
|
WEBOS_SDK="${WEBOS_SDK:-$HOME/arm-webos-linux-gnueabi_sdk-buildroot}"
|
|
DEVICE="${DEVICE:-tv}"
|
|
BUILD_DIR="$ROOT/build"
|
|
STAGE_APP="$BUILD_DIR/stage/app"
|
|
STAGE_SERVICE="$BUILD_DIR/stage/service"
|
|
OUT_DIR="$ROOT/out"
|
|
|
|
say() { printf '%s\n' "$*"; }
|
|
step() { printf '\n== %s\n' "$*"; }
|
|
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
|
|
|
|
version() {
|
|
python3 - "$ROOT/frontend/appinfo.json" <<'EOF'
|
|
import json, sys
|
|
print(json.load(open(sys.argv[1]))["version"])
|
|
EOF
|
|
}
|
|
|
|
check_sdk() {
|
|
local toolchain="$WEBOS_SDK/share/buildroot/toolchainfile.cmake"
|
|
if [ ! -f "$toolchain" ]; then
|
|
cat >&2 <<EOF
|
|
error: no webOS SDK at $WEBOS_SDK
|
|
|
|
Download and unpack the buildroot NDK, then point WEBOS_SDK at it:
|
|
|
|
https://github.com/openlgtv/buildroot-nc4/releases
|
|
tar xf arm-webos-linux-gnueabi_sdk-buildroot.tar.gz -C \$HOME
|
|
\$HOME/arm-webos-linux-gnueabi_sdk-buildroot/relocate-sdk.sh
|
|
|
|
WEBOS_SDK=\$HOME/arm-webos-linux-gnueabi_sdk-buildroot ./tools/build.sh
|
|
|
|
The SDK only runs on Linux. On macOS or Windows, compile in a container
|
|
instead and then come back here to package:
|
|
|
|
./tools/docker-build.sh && ./tools/build.sh package
|
|
EOF
|
|
exit 1
|
|
fi
|
|
command -v cmake >/dev/null 2>&1 || die "cmake is not installed"
|
|
echo "$toolchain"
|
|
}
|
|
|
|
PACKAGER_NODE_IMAGE="${PACKAGER_NODE_IMAGE:-node:18-bookworm-slim}"
|
|
|
|
check_ares() {
|
|
[ -f "$ROOT/node_modules/@webosose/ares-cli/bin/ares-package.js" ] && return 0
|
|
command -v ares-package >/dev/null 2>&1 && return 0
|
|
cat >&2 <<'EOF'
|
|
error: ares-cli is not installed
|
|
|
|
npm install
|
|
|
|
Then register the TV once (developer mode or the Homebrew Channel's ssh):
|
|
|
|
ares-setup-device --add tv --info "{'host':'192.168.1.20','port':9922,'username':'root'}"
|
|
EOF
|
|
return 1
|
|
}
|
|
|
|
# ares-package's own packaging code (ar-async/fstream/tar, all last touched
|
|
# around 2017-2019) silently mishandles file metadata on very new Node
|
|
# releases: every mtime in the ipk comes out as 1970-01-01 instead of the real
|
|
# date. The archive still parses, so nothing here errors — the TV's installer
|
|
# is what eventually rejects it, as "ipk verify failed" with no clue why.
|
|
# Running the same ares-cli under a pinned, known-good Node avoids the whole
|
|
# class of bug regardless of what's on the host.
|
|
run_ares_package() {
|
|
local ares_js="$ROOT/node_modules/@webosose/ares-cli/bin/ares-package.js"
|
|
if [ -f "$ares_js" ] && command -v docker >/dev/null 2>&1; then
|
|
# Arguments are host paths under $ROOT; rewrite them to the container's
|
|
# mount point since nothing outside $ROOT is visible in there.
|
|
local args=() a
|
|
for a in "$@"; do
|
|
args+=("${a/#$ROOT//src}")
|
|
done
|
|
docker run --rm \
|
|
--volume "$ROOT:/src" \
|
|
--workdir /src \
|
|
--user "$(id -u):$(id -g)" \
|
|
--env HOME=/tmp \
|
|
"$PACKAGER_NODE_IMAGE" \
|
|
node /src/node_modules/@webosose/ares-cli/bin/ares-package.js "${args[@]}"
|
|
return
|
|
fi
|
|
if [ -f "$ares_js" ]; then
|
|
say "warning: no docker, running ares-package on the host's own Node ($(node --version 2>/dev/null))"
|
|
say " if the ipk fails to install with a vague error, re-run with docker installed"
|
|
node "$ares_js" "$@"
|
|
return
|
|
fi
|
|
ares-package "$@"
|
|
}
|
|
|
|
build_native() {
|
|
local toolchain
|
|
toolchain="$(check_sdk)"
|
|
|
|
step "Cross-compiling the service"
|
|
cmake -S native -B "$BUILD_DIR/native" \
|
|
-DCMAKE_TOOLCHAIN_FILE="$toolchain" \
|
|
-DCMAKE_BUILD_TYPE=Release
|
|
cmake --build "$BUILD_DIR/native" --parallel
|
|
|
|
local binary="$BUILD_DIR/native/$BINARY"
|
|
[ -f "$binary" ] || die "the build produced no $BINARY"
|
|
say "built $binary"
|
|
file "$binary" 2>/dev/null | sed 's/^/ /' || true
|
|
}
|
|
|
|
stage() {
|
|
step "Staging the package"
|
|
rm -rf "$BUILD_DIR/stage"
|
|
mkdir -p "$STAGE_APP" "$STAGE_SERVICE"
|
|
|
|
cp -R "$ROOT/frontend/." "$STAGE_APP/"
|
|
# The mock only exists so the UI can be opened in a desktop browser.
|
|
rm -f "$STAGE_APP/js/mock.js"
|
|
python3 - "$STAGE_APP/index.html" "$ROOT/frontend/appinfo.json" <<'EOF'
|
|
import json, re, sys
|
|
html_path, appinfo_path = sys.argv[1], sys.argv[2]
|
|
html = open(html_path).read()
|
|
html = re.sub(r'\s*<script src="js/mock\.js"></script>', '', html)
|
|
version = json.load(open(appinfo_path))["version"]
|
|
html = html.replace("__APP_VERSION__", version)
|
|
open(html_path, "w").write(html)
|
|
EOF
|
|
|
|
cp "$ROOT/servicefiles/services.json" "$STAGE_SERVICE/"
|
|
cp "$ROOT/servicefiles/package.json" "$STAGE_SERVICE/"
|
|
cp "$ROOT/servicefiles/audiocapautostart" "$STAGE_SERVICE/"
|
|
chmod +x "$STAGE_SERVICE/audiocapautostart"
|
|
|
|
local binary="$BUILD_DIR/native/$BINARY"
|
|
[ -f "$binary" ] || die "no service binary; run './tools/build.sh native' first"
|
|
cp "$binary" "$STAGE_SERVICE/$BINARY"
|
|
chmod +x "$STAGE_SERVICE/$BINARY"
|
|
|
|
say "app: $STAGE_APP"
|
|
say "service: $STAGE_SERVICE"
|
|
}
|
|
|
|
package() {
|
|
check_ares || exit 1
|
|
step "Packaging"
|
|
mkdir -p "$OUT_DIR"
|
|
rm -f "$OUT_DIR"/${APP_ID}_*.ipk
|
|
run_ares_package "$STAGE_APP" "$STAGE_SERVICE" -o "$OUT_DIR"
|
|
|
|
local ipk
|
|
ipk="$(ls -t "$OUT_DIR"/${APP_ID}_*.ipk | head -1)"
|
|
say ""
|
|
say "$ipk"
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
sha256sum "$ipk" | sed 's/^/ sha256 /'
|
|
elif command -v shasum >/dev/null 2>&1; then
|
|
shasum -a 256 "$ipk" | sed 's/^/ sha256 /'
|
|
fi
|
|
}
|
|
|
|
latest_ipk() {
|
|
ls -t "$OUT_DIR"/${APP_ID}_*.ipk 2>/dev/null | head -1
|
|
}
|
|
|
|
install_ipk() {
|
|
local ipk
|
|
ipk="$(latest_ipk)" || true
|
|
[ -n "$ipk" ] || die "no ipk in $OUT_DIR; run './tools/build.sh' first"
|
|
step "Installing $ipk on device '$DEVICE'"
|
|
ares-install --device "$DEVICE" "$ipk"
|
|
say ""
|
|
say "The service needs root to reach the TV's audio devices. Either open the"
|
|
say "app and press 'Grant root access', or run it here:"
|
|
say " ares-shell --device $DEVICE -r \\"
|
|
say " '/media/developer/apps/usr/palm/services/org.webosbrew.hbchannel.service/elevate-service $SERVICE_ID'"
|
|
}
|
|
|
|
launch() {
|
|
step "Launching on '$DEVICE'"
|
|
ares-launch --device "$DEVICE" "$APP_ID"
|
|
}
|
|
|
|
logs() {
|
|
step "Service log from '$DEVICE' (ctrl-c to stop)"
|
|
# The service keeps its own ring buffer, but journald/pmlog has the crashes.
|
|
ares-shell --device "$DEVICE" -r \
|
|
"tail -f /var/log/messages 2>/dev/null | grep -i audiocap || journalctl -f | grep -i audiocap"
|
|
}
|
|
|
|
clean() {
|
|
step "Cleaning"
|
|
rm -rf "$BUILD_DIR" "$OUT_DIR"
|
|
say "removed build/ and out/"
|
|
}
|
|
|
|
case "${1:-all}" in
|
|
all) build_native; stage; package ;;
|
|
native) build_native ;;
|
|
stage) stage ;;
|
|
package) stage; package ;;
|
|
install) install_ipk ;;
|
|
launch) launch ;;
|
|
logs) logs ;;
|
|
clean) clean ;;
|
|
version) version ;;
|
|
-h|--help)
|
|
awk 'NR == 1 { next } /^#/ { sub(/^# ?/, ""); print; next } { exit }' "$0" ;;
|
|
*) die "unknown command '$1' (try --help)" ;;
|
|
esac
|