2026-05-31 18:14:59 -04:00
#!/usr/bin/env bash
# ============================================================================
# install-driver.sh <vendor>
# ----------------------------------------------------------------------------
# Idempotent HOST installer for capture-card runtime drivers / SDKs.
#
# Runs ON the cluster node's HOST kernel. The node-agent invokes it inside a
# one-shot PRIVILEGED ubuntu container that bind-mounts this repo plus the host
# paths needed to affect the host kernel (/lib/modules, /usr/src, /boot, /dev,
# and the host apt/dpkg via the mounted root). dkms / modprobe / ldconfig
# therefore operate against the running host kernel.
#
# Reads the proprietary vendor file(s) from sdk/<vendor>/ (in this repo).
# NO binaries are committed — if the expected file is missing the script exits
# non-zero with a clear message telling the operator what to drop in.
#
# Vendors (ALLOWLIST — nothing else is accepted):
# blackmagic Desktop Video .deb (DKMS kernel module)
# aja NTV2 driver source/SDK (built kernel module)
# deltacast VideoMaster installer (kernel module)
# ndi redistributable runtime libs (user-space only, no module)
#
# Exit codes:
# 0 installed (or already present / up to date)
# 2 bad usage / unknown vendor
# 3 expected vendor file missing in sdk/<vendor>/
# 4 missing kernel headers (cannot build DKMS / module)
# 5 build / install / module-load failure
#
# `bash -n` must pass. set -euo pipefail with `|| true` guarding every probe.
# ============================================================================
set -euo pipefail
# ---------------------------------------------------------------------------
# Resolve our own repo dir (deploy/ -> repo root), regardless of CWD.
# ---------------------------------------------------------------------------
SCRIPT_DIR = " $( cd " $( dirname " ${ BASH_SOURCE [0] } " ) " >/dev/null 2>& 1 && pwd ) "
REPO_DIR = " $( cd " $SCRIPT_DIR /.. " >/dev/null 2>& 1 && pwd ) "
SDK_ROOT = " $REPO_DIR /sdk "
VENDOR = " ${ 1 :- } "
KVER = " $( uname -r 2>/dev/null || echo unknown) "
REBOOT_REQUIRED = 0
log( ) { echo " [install-driver] $* " ; }
warn( ) { echo " [install-driver] WARN: $* " >& 2; }
die( ) { echo " [install-driver] ERROR: $* " >& 2; exit " ${ 2 :- 5 } " ; }
usage( ) {
echo "Usage: install-driver.sh <blackmagic|aja|deltacast|ndi>" >& 2
exit 2
}
[ -n " $VENDOR " ] || usage
case " $VENDOR " in
blackmagic| aja| deltacast| ndi) : ; ;
*) echo " [install-driver] ERROR: unknown vendor ' $VENDOR ' (allowed: blackmagic aja deltacast ndi) " >& 2; exit 2 ; ;
esac
VENDOR_DIR = " $SDK_ROOT / $VENDOR "
log " vendor= $VENDOR kernel= $KVER repo= $REPO_DIR "
log " reading vendor files from $VENDOR_DIR "
[ -d " $VENDOR_DIR " ] || die " vendor dir $VENDOR_DIR does not exist (repo not mounted?) " 3
# Pick the newest file matching a glob; echo its path or empty.
newest_match( ) {
# shellcheck disable=SC2012
ls -1t $1 2>/dev/null | head -n1 || true
}
ensure_headers( ) {
if [ -d " /lib/modules/ $KVER /build " ] || dpkg -s " linux-headers- $KVER " >/dev/null 2>& 1; then
log " kernel headers for $KVER present "
return 0
fi
log " installing linux-headers- $KVER ... "
apt-get update -y >/dev/null 2>& 1 || true
if ! apt-get install -y " linux-headers- $KVER " >/dev/null 2>& 1; then
# Fall back to the generic meta-package; still may not match a custom kernel.
apt-get install -y linux-headers-generic >/dev/null 2>& 1 || true
fi
if [ -d " /lib/modules/ $KVER /build " ] || dpkg -s " linux-headers- $KVER " >/dev/null 2>& 1; then
log "kernel headers ready"
return 0
fi
die " kernel headers for $KVER unavailable — cannot build module. Install linux-headers- $KVER on the host. " 4
}
# ===========================================================================
# blackmagic — Desktop Video .deb (DKMS) + modprobe + restart desktopvideo
# ===========================================================================
install_blackmagic( ) {
# Detect existing state first (idempotent skip).
if lsmod 2>/dev/null | grep -q '^blackmagic' && [ -e /dev/blackmagic ] ; then
log "blackmagic module loaded and /dev/blackmagic present — already installed"
if command -v dkms >/dev/null 2>& 1; then dkms status 2>/dev/null | grep -i blackmagic || true; fi
return 0
fi
local deb
deb = " $( newest_match " $VENDOR_DIR /desktopvideo_*_amd64.deb " ) "
[ -n " $deb " ] && [ -f " $deb " ] || die \
" no desktopvideo_*_amd64.deb in $VENDOR_DIR — download Desktop Video for Ubuntu 22.04 and drop the .deb there (see sdk/blackmagic/README.md). " 3
log " using package: $( basename " $deb " ) "
ensure_headers
log "apt-get install (DKMS build) ..."
apt-get update -y >/dev/null 2>& 1 || true
apt-get install -y " $deb " || die " apt-get install of $( basename " $deb " ) failed (DKMS build?) " 5
log "depmod + modprobe blackmagic ..."
depmod -a " $KVER " 2>/dev/null || true
if ! modprobe blackmagic 2>/dev/null; then
warn "modprobe blackmagic failed — a reboot may be required for the DKMS module to bind"
REBOOT_REQUIRED = 1
fi
# Restart the DesktopVideoHelper daemon if present.
if command -v systemctl >/dev/null 2>& 1; then
systemctl restart desktopvideo 2>/dev/null \
|| systemctl restart DesktopVideoHelper 2>/dev/null || true
fi
if lsmod 2>/dev/null | grep -q '^blackmagic' || [ -e /dev/blackmagic ] ; then
log "blackmagic installed (module loaded / device present)"
else
warn "blackmagic installed but module not yet loaded — reboot required"
REBOOT_REQUIRED = 1
fi
}
# ===========================================================================
# aja — build ntv2 kernel module from source archive + modprobe
# ===========================================================================
install_aja( ) {
if lsmod 2>/dev/null | grep -q 'ajantv2' ; then
log "ajantv2 module already loaded — already installed"
return 0
fi
local src
src = " $( newest_match " $VENDOR_DIR /ntv2sdk*.zip " ) "
[ -n " $src " ] || src = " $( newest_match " $VENDOR_DIR /libajantv2*.tar.gz " ) "
[ -n " $src " ] && [ -f " $src " ] || die \
" no ntv2sdk*.zip / libajantv2*.tar.gz in $VENDOR_DIR — download the AJA NTV2 Linux SDK and drop it there (see sdk/aja/README.md). " 3
log " using source: $( basename " $src " ) "
ensure_headers
apt-get install -y build-essential unzip >/dev/null 2>& 1 || true
local work; work = " $( mktemp -d) "
log " extracting into $work ... "
case " $src " in
*.zip) unzip -q -o " $src " -d " $work " || die " failed to unzip $( basename " $src " ) " 5 ; ;
*.tar.gz) tar -xzf " $src " -C " $work " || die " failed to untar $( basename " $src " ) " 5 ; ;
esac
# Find the linux driver dir (contains a Makefile producing ajantv2.ko).
local drvdir
drvdir = " $( dirname " $( find " $work " -type d -path '*driver/linux' -print -quit 2>/dev/null || true ) /. " 2>/dev/null) "
[ -d " $drvdir " ] || drvdir = " $( dirname " $( find " $work " -name 'ajantv2.c' -print -quit 2>/dev/null || true ) " 2>/dev/null) "
[ -n " $drvdir " ] && [ -d " $drvdir " ] || die "could not locate AJA driver/linux source inside the archive" 5
log " building module in $drvdir ... "
make -C " $drvdir " >/dev/null 2>& 1 || die "AJA module build failed" 5
local ko
ko = " $( find " $drvdir " -name 'ajantv2.ko' -print -quit 2>/dev/null || true ) "
if [ -n " $ko " ] ; then
install -D -m 0644 " $ko " " /lib/modules/ $KVER /extra/ajantv2.ko " 2>/dev/null || true
depmod -a " $KVER " 2>/dev/null || true
fi
if ! modprobe ajantv2 2>/dev/null; then
# Fall back to the SDK's own load script if shipped.
local loader; loader = " $( find " $work " -name 'load_ajantv2' -print -quit 2>/dev/null || true ) "
if [ -n " $loader " ] ; then bash " $loader " 2>/dev/null || true; fi
fi
rm -rf " $work " 2>/dev/null || true
if lsmod 2>/dev/null | grep -q 'ajantv2' ; then
log "ajantv2 installed and loaded"
else
warn "ajantv2 built but not loaded — a reboot may be required (old in-tree module wedged?)"
REBOOT_REQUIRED = 1
fi
}
# ===========================================================================
# deltacast — VideoMaster installer + module load
# ===========================================================================
install_deltacast( ) {
if lsmod 2>/dev/null | grep -q 'videomaster' || ls /dev/deltacast* >/dev/null 2>& 1; then
log "Deltacast module loaded / device present — already installed"
2026-06-01 13:34:07 -04:00
install_deltacast_udev_rule
2026-05-31 18:14:59 -04:00
return 0
fi
local pkg
pkg = " $( newest_match " $VENDOR_DIR /VideoMaster*.run " ) "
[ -n " $pkg " ] || pkg = " $( newest_match " $VENDOR_DIR /VideoMaster*.tar.gz " ) "
[ -n " $pkg " ] && [ -f " $pkg " ] || die \
" no VideoMaster*.run / VideoMaster*.tar.gz in $VENDOR_DIR — obtain the Deltacast VideoMaster Linux installer and drop it there (see sdk/deltacast/README.md). " 3
log " using installer: $( basename " $pkg " ) "
ensure_headers
apt-get install -y build-essential dkms >/dev/null 2>& 1 || true
local work; work = " $( mktemp -d) "
case " $pkg " in
*.run)
log "extracting self-extractor ..."
chmod +x " $pkg " 2>/dev/null || true
" $pkg " --noexec --target " $work " >/dev/null 2>& 1 \
|| cp " $pkg " " $work /installer.run "
; ;
*.tar.gz)
tar -xzf " $pkg " -C " $work " || die " failed to untar $( basename " $pkg " ) " 5
; ;
esac
local installer
installer = " $( find " $work " -name 'install.sh' -print -quit 2>/dev/null || true ) "
[ -n " $installer " ] || installer = " $( find " $work " -name 'installer.run' -print -quit 2>/dev/null || true ) "
[ -n " $installer " ] && [ -f " $installer " ] || die "Deltacast installer (install.sh) not found inside the package" 5
log " running vendor installer: $( basename " $installer " ) ... "
chmod +x " $installer " 2>/dev/null || true
( cd " $( dirname " $installer " ) " && bash " $installer " ) || die "Deltacast VideoMaster installer failed" 5
depmod -a " $KVER " 2>/dev/null || true
modprobe videomasterhd 2>/dev/null || modprobe videomaster 2>/dev/null || true
rm -rf " $work " 2>/dev/null || true
if lsmod 2>/dev/null | grep -q 'videomaster' || ls /dev/deltacast* >/dev/null 2>& 1; then
log "Deltacast VideoMaster installed"
fi
2026-06-01 13:34:07 -04:00
# Install our own udev rule that creates 8 /dev/deltacast symlinks (ports 0-7)
# pointing at the single real device node. Kept separate from the SDK's own
# rule so a driver reinstall won't clobber it.
install_deltacast_udev_rule
2026-05-31 18:14:59 -04:00
# First-time VideoMaster installs lay down udev rules + firmware that need a reboot.
warn "Deltacast: a REBOOT is recommended after a first-time VideoMaster install (udev + firmware)"
REBOOT_REQUIRED = 1
}
2026-06-01 13:34:07 -04:00
# Copy the repo's 99-wild-dragon-deltacast.rules into /etc/udev/rules.d/ and
# reload. Idempotent. Creates /dev/deltacast0..7 -> /dev/delta-x3700 so the
# node-agent advertises all 8 RX channels.
install_deltacast_udev_rule( ) {
local rule_src = " $REPO_DIR /deploy/udev/99-wild-dragon-deltacast.rules "
local rule_dst = "/etc/udev/rules.d/99-wild-dragon-deltacast.rules"
if [ ! -f " $rule_src " ] ; then
warn " Deltacast: udev rule $rule_src not found in repo — skipping symlink rule install "
return 0
fi
if [ -f " $rule_dst " ] && cmp -s " $rule_src " " $rule_dst " ; then
log " Deltacast: udev rule already up to date at $rule_dst "
else
log " installing Deltacast udev rule -> $rule_dst "
install -D -m 0644 " $rule_src " " $rule_dst " 2>/dev/null \
|| { warn "Deltacast: failed to install udev rule (continuing)" ; return 0; }
udevadm control --reload-rules 2>/dev/null || true
udevadm trigger --action= add /dev/delta-x3700 2>/dev/null || true
fi
}
2026-05-31 18:14:59 -04:00
# ===========================================================================
# ndi — copy redistributable runtime libs to /usr/local/lib + ldconfig
# ===========================================================================
install_ndi( ) {
local target = "/opt/ndi-lib"
local found = 0
# shellcheck disable=SC2231
for f in " $VENDOR_DIR " /libndi*.so*; do
[ -e " $f " ] || continue
found = 1
break
done
[ " $found " = 1 ] || die \
" no libndi*.so* in $VENDOR_DIR — drop the NDI runtime redistributable libs there (see sdk/ndi/README.md). " 3
log " copying NDI runtime libs to $target ... "
mkdir -p " $target "
cp -av " $VENDOR_DIR " /libndi*.so* " $target " / 2>/dev/null || die "failed copying NDI libs" 5
# Recreate the libndi.so dev symlink if only versioned libs were shipped.
if [ ! -e " $target /libndi.so " ] ; then
local versioned
versioned = " $( newest_match " $target /libndi.so.* " ) "
if [ -n " $versioned " ] ; then
ln -sf " $( basename " $versioned " ) " " $target /libndi.so " 2>/dev/null || true
fi
fi
echo " $target " > /etc/ld.so.conf.d/ndi.conf
ldconfig 2>/dev/null || true
if ldconfig -p 2>/dev/null | grep -q 'libndi' ; then
log "NDI runtime registered with the dynamic linker"
else
die "NDI libs copied but ldconfig did not resolve libndi" 5
fi
log "NDI: no kernel module and no reboot required."
log "NDI: restart any process that already loaded an older libndi to pick up the new version."
}
# ---------------------------------------------------------------------------
case " $VENDOR " in
blackmagic) install_blackmagic ; ;
aja) install_aja ; ;
deltacast) install_deltacast ; ;
ndi) install_ndi ; ;
esac
if [ " $REBOOT_REQUIRED " = 1 ] ; then
log " RESULT: $VENDOR install completed — REBOOT REQUIRED "
echo "[install-driver] REBOOT_REQUIRED=1"
else
log " RESULT: $VENDOR install completed — no reboot required "
echo "[install-driver] REBOOT_REQUIRED=0"
fi
exit 0