From 2f16be993e396f3cc0f2c2a284910432bdb8e09a Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Mon, 3 Aug 2026 19:23:42 -0400 Subject: [PATCH 1/2] perf: eliminate emulated ARM release compilation --- .dockerignore | 19 ++ .github/workflows/docker-publish.yml | 20 +- .gitignore | 1 + Dockerfile | 186 ++++++++++++++----- cmake/toolchains/armv7-linux-gnueabihf.cmake | 33 ++++ docs/RELEASE_PROCESS.md | 9 +- scripts/build.sh | 8 +- 7 files changed, 216 insertions(+), 60 deletions(-) create mode 100644 .dockerignore create mode 100644 cmake/toolchains/armv7-linux-gnueabihf.cmake diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..63d4e5fbc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +.git +.github +.devcontainer + +build +build-* +cmake-build-* +**/CMakeFiles +**/CMakeCache.txt +**/cmake_install.cmake + +**/node_modules +web/dist +playwright-report +test-results + +*.deb +*.docx +homeassistant diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 14542b571..815143ea9 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -18,14 +18,20 @@ env: jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: - platform: - - linux/amd64 - - linux/arm64 - - linux/arm/v7 + include: + - platform: linux/amd64 + runner: ubuntu-latest + emulated: false + - platform: linux/arm64 + runner: ubuntu-24.04-arm + emulated: false + - platform: linux/arm/v7 + runner: ubuntu-latest + emulated: true steps: - name: Checkout @@ -61,6 +67,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Set up QEMU + if: matrix.emulated uses: docker/setup-qemu-action@v4 - name: Set up Buildx @@ -75,6 +82,8 @@ jobs: platforms: ${{ matrix.platform }} pull: true labels: ${{ steps.meta.outputs.labels }} + cache-from: type=registry,ref=${{ env.REGISTRY_IMAGE }}:buildcache-${{ env.PLATFORM_PAIR }} + cache-to: type=registry,ref=${{ env.REGISTRY_IMAGE }}:buildcache-${{ env.PLATFORM_PAIR }},mode=max outputs: type=image,push-by-digest=true,name=${{ env.REGISTRY_IMAGE }},name-canonical=true,push=true - name: Save digest @@ -161,4 +170,3 @@ jobs: - name: Inspect final image run: | docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }} - diff --git a/.gitignore b/.gitignore index d7dce5b02..939606c64 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,7 @@ CMakeFiles/ cmake_install.cmake Makefile *.cmake +!cmake/toolchains/*.cmake !CMakeLists.txt # IDE files diff --git a/Dockerfile b/Dockerfile index a0aaec377..8bed830e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ ARG LLHTTP_VERSION=9.3.1 ARG NODE_MAJOR=24 ARG DEB_BUILD=false -FROM debian:${DEBIAN_SUITE}-slim AS builder +FROM --platform=$BUILDPLATFORM debian:${DEBIAN_SUITE}-slim AS builder ARG DEBIAN_SUITE ARG SQLITE_YEAR @@ -16,35 +16,57 @@ ARG LIBUV_VERSION ARG LLHTTP_VERSION ARG NODE_MAJOR ARG DEB_BUILD +ARG BUILDARCH +ARG TARGETARCH +ARG TARGETVARIANT # Set non-interactive mode ENV DEBIAN_FRONTEND=noninteractive -# Install build dependencies including Node.js and FFmpeg dev libraries. +# Install build dependencies including Node.js and target FFmpeg dev libraries. # Node.js comes from NodeSource so every Debian suite uses the Node 24 LTS # baseline required by the Babel 8 web test toolchain. # sid ships Go 1.26+/FFmpeg 8.x; trixie ships Go 1.24+/FFmpeg 7.x. # +# ARMv7 is cross-compiled on the x86_64 runner. Compiling LiteRT/XNNPACK under +# QEMU accounted for nearly three hours of each release; Debian multiarch gives +# the native compiler the same armhf headers and libraries without emulation. +# # Pre-install systemd-standalone-sysusers to satisfy the sysusers virtual # dependency without pulling in the full systemd package. The full systemd # postinst crashes under QEMU ARM emulation (SIGSEGV in systemd 260.x), # breaking all cross-architecture builds. -RUN apt-get update && \ +RUN if [ "$TARGETARCH/$TARGETVARIANT" = "arm/v7" ]; then \ + test "$BUILDARCH" = "amd64" || { \ + echo "linux/arm/v7 cross-compilation requires an amd64 builder"; exit 1; \ + }; \ + dpkg --add-architecture armhf; \ + elif [ "$TARGETARCH" != "$BUILDARCH" ]; then \ + echo "Unsupported cross-build: $BUILDARCH -> $TARGETARCH/$TARGETVARIANT"; \ + exit 1; \ + fi && \ + apt-get update && \ apt-get install -y --no-install-recommends \ systemd-standalone-sysusers curl ca-certificates gpg && \ curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" \ -o /tmp/nodesource_setup.sh && \ bash /tmp/nodesource_setup.sh && \ rm /tmp/nodesource_setup.sh && \ - apt-get install -y \ - git cmake build-essential pkg-config file \ - libavcodec-dev libavformat-dev libavutil-dev libswscale-dev \ - libcurl4-openssl-dev \ - libmbedtls-dev wget libcjson-dev \ - libmosquitto-dev \ - libyaml-dev \ - nodejs \ - golang-go && \ + apt-get install -y --no-install-recommends \ + git cmake build-essential pkg-config file wget nodejs golang-go && \ + if [ "$TARGETARCH/$TARGETVARIANT" = "arm/v7" ]; then \ + apt-get install -y --no-install-recommends \ + gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf \ + libavcodec-dev:armhf libavformat-dev:armhf \ + libavutil-dev:armhf libswscale-dev:armhf \ + libcurl4-openssl-dev:armhf libmbedtls-dev:armhf \ + libcjson-dev:armhf libmosquitto-dev:armhf libyaml-dev:armhf; \ + else \ + apt-get install -y --no-install-recommends \ + libavcodec-dev libavformat-dev libavutil-dev libswscale-dev \ + libcurl4-openssl-dev libmbedtls-dev libcjson-dev \ + libmosquitto-dev libyaml-dev; \ + fi && \ # Verify installation node --version && \ npm --version && \ @@ -55,16 +77,23 @@ RUN apt-get update && \ # This ensures the binary links against system SONAMEs so that libuv, libsqlite3, # and libllhttp can be proper package dependencies instead of bundled libraries. RUN if [ "$DEB_BUILD" = "true" ]; then \ + if [ "$TARGETARCH/$TARGETVARIANT" = "arm/v7" ]; then \ + TARGET_DEB_ARCH=armhf; \ + LIBDIR=/usr/lib/arm-linux-gnueabihf; \ + else \ + TARGET_DEB_ARCH=""; \ + case "$TARGETARCH" in \ + amd64) LIBDIR=/usr/lib/x86_64-linux-gnu ;; \ + arm64) LIBDIR=/usr/lib/aarch64-linux-gnu ;; \ + *) echo "Unsupported target architecture: $TARGETARCH/$TARGETVARIANT"; exit 1 ;; \ + esac; \ + fi && \ apt-get update && apt-get install -y --no-install-recommends \ - libuv1-dev libsqlite3-dev libllhttp-dev sqlite3 && \ + "libuv1-dev${TARGET_DEB_ARCH:+:$TARGET_DEB_ARCH}" \ + "libsqlite3-dev${TARGET_DEB_ARCH:+:$TARGET_DEB_ARCH}" \ + "libllhttp-dev${TARGET_DEB_ARCH:+:$TARGET_DEB_ARCH}" \ + "sqlite3${TARGET_DEB_ARCH:+:$TARGET_DEB_ARCH}" && \ rm -rf /var/lib/apt/lists/* && \ - ARCH=$(uname -m) && \ - case $ARCH in \ - x86_64) LIBDIR="/usr/lib/x86_64-linux-gnu" ;; \ - aarch64) LIBDIR="/usr/lib/aarch64-linux-gnu" ;; \ - armv7l) LIBDIR="/usr/lib/arm-linux-gnueabihf" ;; \ - *) echo "Unsupported architecture: $ARCH"; exit 1 ;; \ - esac && \ cp -a ${LIBDIR}/libuv.so* /usr/lib/ && \ cp -a ${LIBDIR}/libsqlite3.so* /usr/lib/ && \ cp -a ${LIBDIR}/libllhttp.so* /usr/lib/; \ @@ -76,10 +105,19 @@ RUN if [ "$DEB_BUILD" != "true" ]; then \ wget -q "https://www.sqlite.org/${SQLITE_YEAR}/sqlite-autoconf-${SQLITE_AUTOCONF_VERSION}.tar.gz" && \ tar -xzf "sqlite-autoconf-${SQLITE_AUTOCONF_VERSION}.tar.gz" && \ cd "sqlite-autoconf-${SQLITE_AUTOCONF_VERSION}" && \ - ./configure --prefix=/usr --disable-static && \ + if [ "$TARGETARCH/$TARGETVARIANT" = "arm/v7" ]; then \ + CONFIGURE_TARGET="--host=arm-linux-gnueabihf"; \ + else \ + CONFIGURE_TARGET=""; \ + fi && \ + ./configure $CONFIGURE_TARGET --prefix=/usr --libdir=/usr/lib --disable-static && \ make -j"$(nproc)" && \ make install && \ - sqlite3 --version; \ + if [ "$TARGETARCH/$TARGETVARIANT" = "arm/v7" ]; then \ + file /usr/bin/sqlite3 | grep -q "ARM"; \ + else \ + sqlite3 --version; \ + fi; \ fi # Build upstream libuv (skipped for .deb builds which use system libuv) @@ -88,17 +126,15 @@ RUN if [ "$DEB_BUILD" != "true" ]; then \ wget -q "https://github.com/libuv/libuv/archive/refs/tags/v${LIBUV_VERSION}.tar.gz" -O libuv.tar.gz && \ tar -xzf libuv.tar.gz && \ cd "libuv-${LIBUV_VERSION}" && \ - cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF -DCMAKE_INSTALL_PREFIX=/usr && \ + if [ "$TARGETARCH/$TARGETVARIANT" = "arm/v7" ]; then \ + UV_CROSS_ARGS="-DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=armv7 -DCMAKE_C_COMPILER=arm-linux-gnueabihf-gcc -DCMAKE_CXX_COMPILER=arm-linux-gnueabihf-g++"; \ + else \ + UV_CROSS_ARGS=""; \ + fi && \ + cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF \ + -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=lib $UV_CROSS_ARGS && \ cmake --build build -j"$(nproc)" && \ cmake --install build && \ - ARCH=$(uname -m) && \ - case $ARCH in \ - x86_64) LIBUV_DIR="/usr/lib/x86_64-linux-gnu" ;; \ - aarch64) LIBUV_DIR="/usr/lib/aarch64-linux-gnu" ;; \ - armv7l) LIBUV_DIR="/usr/lib/arm-linux-gnueabihf" ;; \ - *) echo "Unsupported architecture: $ARCH"; exit 1 ;; \ - esac && \ - cp -a "$LIBUV_DIR"/libuv.so* /usr/lib/ && \ pkg-config --modversion libuv; \ fi @@ -109,10 +145,15 @@ RUN if [ "$DEB_BUILD" != "true" ]; then \ wget -q "https://raw.githubusercontent.com/nodejs/llhttp/release/src/llhttp.c" -O /tmp/llhttp/src/llhttp.c && \ wget -q "https://raw.githubusercontent.com/nodejs/llhttp/release/src/api.c" -O /tmp/llhttp/src/api.c && \ wget -q "https://raw.githubusercontent.com/nodejs/llhttp/release/src/http.c" -O /tmp/llhttp/src/http.c && \ - cc -fPIC -I/tmp/llhttp/include -c /tmp/llhttp/src/llhttp.c -o /tmp/llhttp/llhttp.o && \ - cc -fPIC -I/tmp/llhttp/include -c /tmp/llhttp/src/api.c -o /tmp/llhttp/api.o && \ - cc -fPIC -I/tmp/llhttp/include -c /tmp/llhttp/src/http.c -o /tmp/llhttp/http.o && \ - cc -shared -Wl,-soname,libllhttp.so.9 -o /usr/lib/libllhttp.so.${LLHTTP_VERSION} /tmp/llhttp/llhttp.o /tmp/llhttp/api.o /tmp/llhttp/http.o && \ + if [ "$TARGETARCH/$TARGETVARIANT" = "arm/v7" ]; then \ + TARGET_CC=arm-linux-gnueabihf-gcc; \ + else \ + TARGET_CC=cc; \ + fi && \ + "$TARGET_CC" -fPIC -I/tmp/llhttp/include -c /tmp/llhttp/src/llhttp.c -o /tmp/llhttp/llhttp.o && \ + "$TARGET_CC" -fPIC -I/tmp/llhttp/include -c /tmp/llhttp/src/api.c -o /tmp/llhttp/api.o && \ + "$TARGET_CC" -fPIC -I/tmp/llhttp/include -c /tmp/llhttp/src/http.c -o /tmp/llhttp/http.o && \ + "$TARGET_CC" -shared -Wl,-soname,libllhttp.so.9 -o /usr/lib/libllhttp.so.${LLHTTP_VERSION} /tmp/llhttp/llhttp.o /tmp/llhttp/api.o /tmp/llhttp/http.o && \ ln -sf /usr/lib/libllhttp.so.${LLHTTP_VERSION} /usr/lib/libllhttp.so.9 && \ ln -sf /usr/lib/libllhttp.so.${LLHTTP_VERSION} /usr/lib/libllhttp.so && \ install -m 644 /tmp/llhttp/include/llhttp.h /usr/include/llhttp.h && \ @@ -129,20 +170,33 @@ RUN mkdir -p /opt/external && \ cd /opt/external && \ git clone https://github.com/benhoyt/inih.git +# LiteRT requires a build-host flatc binary while cross-compiling. Build the +# exact FlatBuffers revision selected by LiteRT, in a layer that changes only +# when its CMake dependency definitions change. +COPY third_party/litert/tflite/tools/cmake /tmp/tflite-cmake +RUN if [ "$TARGETARCH/$TARGETVARIANT" = "arm/v7" ]; then \ + cmake -S /tmp/tflite-cmake/native_tools/flatbuffers \ + -B /tmp/flatc-build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/opt/host-tools && \ + cmake --build /tmp/flatc-build -j"$(nproc)" && \ + test -x /opt/host-tools/bin/flatc && \ + /opt/host-tools/bin/flatc --version; \ + fi + # Copy current directory contents into container WORKDIR /opt COPY . . # Create pkg-config files for MbedTLS with architecture-specific paths RUN mkdir -p /usr/lib/pkgconfig && \ - ARCH=$(uname -m) && \ - MBEDTLS_VERSION=$(dpkg-query -W -f='${Version}' libmbedtls-dev | cut -d- -f1) && \ - case $ARCH in \ - x86_64) LIB_DIR="/usr/lib/x86_64-linux-gnu" ;; \ - aarch64) LIB_DIR="/usr/lib/aarch64-linux-gnu" ;; \ - armv7l) LIB_DIR="/usr/lib/arm-linux-gnueabihf" ;; \ - *) echo "Unsupported architecture: $ARCH"; exit 1 ;; \ + case "$TARGETARCH/$TARGETVARIANT" in \ + amd64/) LIB_DIR="/usr/lib/x86_64-linux-gnu"; MBEDTLS_PACKAGE=libmbedtls-dev ;; \ + arm64/) LIB_DIR="/usr/lib/aarch64-linux-gnu"; MBEDTLS_PACKAGE=libmbedtls-dev ;; \ + arm/v7) LIB_DIR="/usr/lib/arm-linux-gnueabihf"; MBEDTLS_PACKAGE=libmbedtls-dev:armhf ;; \ + *) echo "Unsupported target architecture: $TARGETARCH/$TARGETVARIANT"; exit 1 ;; \ esac && \ + MBEDTLS_VERSION=$(dpkg-query -W -f='${Version}' "$MBEDTLS_PACKAGE" | cut -d- -f1) && \ echo "prefix=/usr\nexec_prefix=\${prefix}\nlibdir=$LIB_DIR\nincludedir=\${prefix}/include\n\nName: mbedtls\nDescription: MbedTLS Library\nVersion: $MBEDTLS_VERSION\nLibs: -L\${libdir} -lmbedtls\nCflags: -I\${includedir}" > /usr/lib/pkgconfig/mbedtls.pc && \ echo "prefix=/usr\nexec_prefix=\${prefix}\nlibdir=$LIB_DIR\nincludedir=\${prefix}/include\n\nName: mbedcrypto\nDescription: MbedTLS Crypto Library\nVersion: $MBEDTLS_VERSION\nLibs: -L\${libdir} -lmbedcrypto\nCflags: -I\${includedir}" > /usr/lib/pkgconfig/mbedcrypto.pc && \ echo "prefix=/usr\nexec_prefix=\${prefix}\nlibdir=$LIB_DIR\nincludedir=\${prefix}/include\n\nName: mbedx509\nDescription: MbedTLS X509 Library\nVersion: $MBEDTLS_VERSION\nLibs: -L\${libdir} -lmbedx509\nCflags: -I\${includedir}" > /usr/lib/pkgconfig/mbedx509.pc && \ @@ -154,7 +208,14 @@ RUN mkdir -p /bin /etc/lightnvr/go2rtc && \ # Build go2rtc from local submodule (already copied by COPY . .) cd /opt/go2rtc && \ GOTOOLCHAIN=auto go mod tidy && \ - GOTOOLCHAIN=auto CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath -o /bin/go2rtc . && \ + case "$TARGETARCH/$TARGETVARIANT" in \ + amd64/) GOARCH=amd64; GOARM= ;; \ + arm64/) GOARCH=arm64; GOARM= ;; \ + arm/v7) GOARCH=arm; GOARM=7 ;; \ + *) echo "Unsupported target architecture: $TARGETARCH/$TARGETVARIANT"; exit 1 ;; \ + esac && \ + GOTOOLCHAIN=auto CGO_ENABLED=0 GOOS=linux GOARCH="$GOARCH" GOARM="$GOARM" \ + go build -ldflags "-s -w" -trimpath -o /bin/go2rtc . && \ chmod +x /bin/go2rtc && \ # Create basic configuration file echo "# go2rtc configuration file" > /etc/lightnvr/go2rtc/go2rtc.yaml && \ @@ -201,17 +262,40 @@ RUN mkdir -p /etc/lightnvr /var/lib/lightnvr/data /var/log/lightnvr /var/run/lig # Clean any existing build files rm -rf build/ && \ # Determine architecture-specific pkgconfig path - ARCH=$(uname -m) && \ - case $ARCH in \ - x86_64) PKG_CONFIG_ARCH_PATH="/usr/lib/x86_64-linux-gnu/pkgconfig" ;; \ - aarch64) PKG_CONFIG_ARCH_PATH="/usr/lib/aarch64-linux-gnu/pkgconfig" ;; \ - armv7l) PKG_CONFIG_ARCH_PATH="/usr/lib/arm-linux-gnueabihf/pkgconfig" ;; \ - *) echo "Unsupported architecture: $ARCH"; exit 1 ;; \ + case "$TARGETARCH/$TARGETVARIANT" in \ + amd64/) PKG_CONFIG_ARCH_PATH="/usr/lib/x86_64-linux-gnu/pkgconfig"; TOOLCHAIN_FILE="" ;; \ + arm64/) PKG_CONFIG_ARCH_PATH="/usr/lib/aarch64-linux-gnu/pkgconfig"; TOOLCHAIN_FILE="" ;; \ + arm/v7) PKG_CONFIG_ARCH_PATH="/usr/lib/arm-linux-gnueabihf/pkgconfig"; \ + TOOLCHAIN_FILE="/opt/cmake/toolchains/armv7-linux-gnueabihf.cmake" ;; \ + *) echo "Unsupported target architecture: $TARGETARCH/$TARGETVARIANT"; exit 1 ;; \ esac && \ + # Cross-installing armhf packages does not run the target ldconfig, so + # recreate the SONAME link that libmosquitto needs for the final link. + if [ "$TARGETARCH/$TARGETVARIANT" = "arm/v7" ]; then \ + PICOHTTP_LIB=$(find /usr/lib/arm-linux-gnueabihf -maxdepth 1 \ + -name 'libpicohttpparser.so.1.*' -print -quit) && \ + if [ -n "$PICOHTTP_LIB" ]; then \ + ln -sf "$(basename "$PICOHTTP_LIB")" \ + /usr/lib/arm-linux-gnueabihf/libpicohttpparser.so.1; \ + fi; \ + fi && \ # Build the application with go2rtc and SOD dynamic linking + CMAKE_TOOLCHAIN_FILE="$TOOLCHAIN_FILE" \ PKG_CONFIG_PATH=/usr/lib/pkgconfig:$PKG_CONFIG_ARCH_PATH:$PKG_CONFIG_PATH \ - ./scripts/build.sh --release --with-sod --sod-dynamic --with-go2rtc --go2rtc-binary=/bin/go2rtc --go2rtc-config-dir=/etc/lightnvr/go2rtc --go2rtc-api-port=1984 && \ - ./scripts/install.sh --prefix=/ --with-go2rtc --go2rtc-config-dir=/etc/lightnvr/go2rtc --without-systemd + PKG_CONFIG_LIBDIR=/usr/lib/pkgconfig:$PKG_CONFIG_ARCH_PATH:/usr/share/pkgconfig \ + ./scripts/build.sh --release --without-tests --with-sod --sod-dynamic --with-go2rtc --go2rtc-binary=/bin/go2rtc --go2rtc-config-dir=/etc/lightnvr/go2rtc --go2rtc-api-port=1984 && \ + if [ "$TARGETARCH/$TARGETVARIANT" = "arm/v7" ]; then \ + ./scripts/install.sh --prefix=/ --with-go2rtc --go2rtc-config-dir=/etc/lightnvr/go2rtc --without-systemd --without-ldconfig; \ + else \ + ./scripts/install.sh --prefix=/ --with-go2rtc --go2rtc-config-dir=/etc/lightnvr/go2rtc --without-systemd; \ + fi + +# Fail the build if a cross-compiler silently produced host binaries. +RUN file /bin/lightnvr /bin/go2rtc && \ + if [ "$TARGETARCH/$TARGETVARIANT" = "arm/v7" ]; then \ + file /bin/lightnvr | grep -q "ELF 32-bit.*ARM" && \ + file /bin/go2rtc | grep -q "ELF 32-bit.*ARM"; \ + fi # Stage 2: Minimal runtime image FROM debian:${DEBIAN_SUITE}-slim AS runtime diff --git a/cmake/toolchains/armv7-linux-gnueabihf.cmake b/cmake/toolchains/armv7-linux-gnueabihf.cmake new file mode 100644 index 000000000..dd8c3c691 --- /dev/null +++ b/cmake/toolchains/armv7-linux-gnueabihf.cmake @@ -0,0 +1,33 @@ +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_PROCESSOR armv7) + +set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc) +set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++) +set(CMAKE_ASM_COMPILER arm-linux-gnueabihf-gcc) + +# Match the linux/arm/v7 image contract. LiteRT and XNNPACK both require NEON +# and an explicit IEEE fp16 format when cross-compiling their ARMv7 kernels. +set(_LIGHTNVR_ARMV7_FLAGS + "-march=armv7-a -mfpu=neon-vfpv4 -mfloat-abi=hard -mfp16-format=ieee") +set(CMAKE_C_FLAGS_INIT "${_LIGHTNVR_ARMV7_FLAGS}") +set(CMAKE_CXX_FLAGS_INIT "${_LIGHTNVR_ARMV7_FLAGS}") + +# GNU ld does not automatically search Debian's multiarch directories for +# transitive shared-library dependencies. Mosquitto, for example, depends on +# libpicohttpparser; make those target directories available at link time +# without embedding builder paths in the resulting executable. +set(_LIGHTNVR_ARMV7_RPATH_LINK + "-Wl,-rpath-link,/usr/lib/arm-linux-gnueabihf -Wl,-rpath-link,/lib/arm-linux-gnueabihf") +set(CMAKE_EXE_LINKER_FLAGS_INIT "${_LIGHTNVR_ARMV7_RPATH_LINK}") +set(CMAKE_SHARED_LINKER_FLAGS_INIT "${_LIGHTNVR_ARMV7_RPATH_LINK}") + +# LiteRT generates FlatBuffer schemas during its target build. This binary is +# built natively in the Docker host-tools layer and must not be cross-compiled. +set(TFLITE_HOST_TOOLS_DIR "/opt/host-tools" CACHE PATH "LiteRT host tools") + +# Programs such as flatc must execute on the x86_64 build host. Libraries and +# headers are resolved through Debian's arm-linux-gnueabihf multiarch paths. +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/docs/RELEASE_PROCESS.md b/docs/RELEASE_PROCESS.md index e97d88f56..41c4888c7 100644 --- a/docs/RELEASE_PROCESS.md +++ b/docs/RELEASE_PROCESS.md @@ -99,8 +99,13 @@ When you push a tag, GitHub Actions automatically: 1. **Builds Docker images** for multiple architectures: - linux/amd64 - - linux/arm64 - - linux/arm/v7 + - linux/arm64 on a native GitHub-hosted ARM64 runner + - linux/arm/v7, cross-compiled on x86_64 (QEMU is used only to assemble the + target runtime layer) + + Each platform also imports and updates a platform-specific BuildKit cache in + GHCR. This avoids rebuilding unchanged package and dependency layers between + releases. 2. **Builds web assets** during the Docker build: - Installs Node.js 24.x LTS diff --git a/scripts/build.sh b/scripts/build.sh index 2f4804bae..8f8f8fec3 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -285,7 +285,13 @@ cd "$BUILD_DIR" # Use our custom module path CMAKE_MODULE_PATH="$(pwd)/../../cmake/modules" -cmake -DCMAKE_BUILD_TYPE="$BUILD_TYPE" $SOD_OPTION $TEST_OPTION "${GO2RTC_OPTIONS[@]}" $HTTP_BACKEND_OPTION $FFMPEG_CMAKE_OPTIONS \ +TOOLCHAIN_OPTION=() +if [ -n "${CMAKE_TOOLCHAIN_FILE:-}" ]; then + TOOLCHAIN_OPTION=("-DCMAKE_TOOLCHAIN_FILE=$CMAKE_TOOLCHAIN_FILE") + echo "Using CMake toolchain: $CMAKE_TOOLCHAIN_FILE" +fi + +cmake -DCMAKE_BUILD_TYPE="$BUILD_TYPE" $SOD_OPTION $TEST_OPTION "${GO2RTC_OPTIONS[@]}" $HTTP_BACKEND_OPTION $FFMPEG_CMAKE_OPTIONS "${TOOLCHAIN_OPTION[@]}" \ -DCMAKE_MODULE_PATH="$CMAKE_MODULE_PATH" ../.. # Return to project root From df8ac18d80e1aab3df54177f2d6e6bbf8c8db5ae Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Mon, 3 Aug 2026 20:08:21 -0400 Subject: [PATCH 2/2] fix: preserve release commit metadata --- .dockerignore | 1 + .github/workflows/container-scan.yml | 3 ++- .github/workflows/debian-package.yml | 2 +- .github/workflows/docker-publish.yml | 2 ++ CMakeLists.txt | 30 +++++++++++++++++++--------- Dockerfile | 4 +++- scripts/extract_version.js | 28 ++++++++++++++++---------- scripts/extract_version.sh | 13 ++++++++++-- 8 files changed, 59 insertions(+), 24 deletions(-) diff --git a/.dockerignore b/.dockerignore index 63d4e5fbc..1fbd4658d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ +# Release workflows inject github.sha through the GIT_COMMIT build argument. .git .github .devcontainer diff --git a/.github/workflows/container-scan.yml b/.github/workflows/container-scan.yml index 6820a52c6..e6b782df5 100644 --- a/.github/workflows/container-scan.yml +++ b/.github/workflows/container-scan.yml @@ -34,6 +34,8 @@ jobs: push: false load: true tags: lightnvr:scan + build-args: | + GIT_COMMIT=${{ github.sha }} - name: Install Trivy run: | @@ -77,4 +79,3 @@ jobs: name: trivy-scan-results path: trivy-results.sarif retention-days: 30 - diff --git a/.github/workflows/debian-package.yml b/.github/workflows/debian-package.yml index bcebdbf1b..271977391 100644 --- a/.github/workflows/debian-package.yml +++ b/.github/workflows/debian-package.yml @@ -76,6 +76,7 @@ jobs: build-args: | DEBIAN_SUITE=${{ matrix.debian_suite }} DEB_BUILD=true + GIT_COMMIT=${{ github.sha }} - name: Extract files and create .deb package run: | @@ -333,4 +334,3 @@ jobs: fail_on_unmatched_files: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 815143ea9..aa492bad3 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -82,6 +82,8 @@ jobs: platforms: ${{ matrix.platform }} pull: true labels: ${{ steps.meta.outputs.labels }} + build-args: | + GIT_COMMIT=${{ github.sha }} cache-from: type=registry,ref=${{ env.REGISTRY_IMAGE }}:buildcache-${{ env.PLATFORM_PAIR }} cache-to: type=registry,ref=${{ env.REGISTRY_IMAGE }}:buildcache-${{ env.PLATFORM_PAIR }},mode=max outputs: type=image,push-by-digest=true,name=${{ env.REGISTRY_IMAGE }},name-canonical=true,push=true diff --git a/CMakeLists.txt b/CMakeLists.txt index 60e206486..f86bcff81 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -728,15 +728,27 @@ endif() # Obtain git short commit hash for version stamping set(GIT_COMMIT "unknown") -find_package(Git QUIET) -if(GIT_FOUND) - execute_process( - COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - OUTPUT_VARIABLE GIT_COMMIT - OUTPUT_STRIP_TRAILING_WHITESPACE - ERROR_QUIET - ) +if(DEFINED ENV{LIGHTNVR_GIT_COMMIT} AND NOT "$ENV{LIGHTNVR_GIT_COMMIT}" STREQUAL "") + set(GIT_COMMIT_INPUT "$ENV{LIGHTNVR_GIT_COMMIT}") + string(LENGTH "${GIT_COMMIT_INPUT}" GIT_COMMIT_LENGTH) + if(NOT GIT_COMMIT_INPUT MATCHES "^[0-9a-fA-F]+$" OR + GIT_COMMIT_LENGTH LESS 7 OR GIT_COMMIT_LENGTH GREATER 40) + message(FATAL_ERROR + "Invalid LIGHTNVR_GIT_COMMIT: expected a 7-40 character hexadecimal commit") + endif() + string(SUBSTRING "${GIT_COMMIT_INPUT}" 0 7 GIT_COMMIT) + string(TOLOWER "${GIT_COMMIT}" GIT_COMMIT) +else() + find_package(Git QUIET) + if(GIT_FOUND) + execute_process( + COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_COMMIT + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + ) + endif() endif() # Build date in ISO-8601 diff --git a/Dockerfile b/Dockerfile index 8bed830e1..37da707b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -187,6 +187,7 @@ RUN if [ "$TARGETARCH/$TARGETVARIANT" = "arm/v7" ]; then \ # Copy current directory contents into container WORKDIR /opt COPY . . +ARG GIT_COMMIT # Create pkg-config files for MbedTLS with architecture-specific paths RUN mkdir -p /usr/lib/pkgconfig && \ @@ -240,7 +241,7 @@ RUN if grep -q "systemctl" scripts/install.sh; then \ fi # Generate version.js before building web assets (it is not checked into git) -RUN ./scripts/extract_version.sh +RUN LIGHTNVR_GIT_COMMIT="$GIT_COMMIT" ./scripts/extract_version.sh # Build web assets using Vite RUN echo "Building web assets..." && \ @@ -280,6 +281,7 @@ RUN mkdir -p /etc/lightnvr /var/lib/lightnvr/data /var/log/lightnvr /var/run/lig fi; \ fi && \ # Build the application with go2rtc and SOD dynamic linking + LIGHTNVR_GIT_COMMIT="$GIT_COMMIT" \ CMAKE_TOOLCHAIN_FILE="$TOOLCHAIN_FILE" \ PKG_CONFIG_PATH=/usr/lib/pkgconfig:$PKG_CONFIG_ARCH_PATH:$PKG_CONFIG_PATH \ PKG_CONFIG_LIBDIR=/usr/lib/pkgconfig:$PKG_CONFIG_ARCH_PATH:/usr/share/pkgconfig \ diff --git a/scripts/extract_version.js b/scripts/extract_version.js index 8e5d64633..f6de94ef2 100755 --- a/scripts/extract_version.js +++ b/scripts/extract_version.js @@ -120,17 +120,25 @@ function main() { const version = versionMatch[1]; console.log(`Extracted version: ${version}`); - // Extract git short commit hash + // Prefer an explicit source revision when the build context excludes .git. let gitCommit = 'unknown'; - try { - const { execSync } = require('child_process'); - gitCommit = execSync('git rev-parse --short HEAD', { - cwd: projectRoot, - encoding: 'utf8', - stdio: ['pipe', 'pipe', 'pipe'], - }).trim(); - } catch { - console.warn('Could not determine git commit hash'); + const commitOverride = process.env.LIGHTNVR_GIT_COMMIT; + if (commitOverride) { + if (!/^[0-9a-f]{7,40}$/i.test(commitOverride)) { + throw new Error('Invalid LIGHTNVR_GIT_COMMIT: expected a 7-40 character hexadecimal commit'); + } + gitCommit = commitOverride.slice(0, 7).toLowerCase(); + } else { + try { + const { execSync } = require('child_process'); + gitCommit = execSync('git rev-parse --short HEAD', { + cwd: projectRoot, + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + }).trim(); + } catch { + console.warn('Could not determine git commit hash'); + } } console.log(`Git commit: ${gitCommit}`); diff --git a/scripts/extract_version.sh b/scripts/extract_version.sh index 2ec8611cc..0ca1f3076 100755 --- a/scripts/extract_version.sh +++ b/scripts/extract_version.sh @@ -64,8 +64,17 @@ fi echo "Extracted version: $VERSION" -# Extract git short commit hash -GIT_COMMIT=$(cd "$PROJECT_ROOT" && git rev-parse --short HEAD 2>/dev/null || echo "unknown") +# Prefer an explicit source revision when the build context excludes .git. +if [ -n "${LIGHTNVR_GIT_COMMIT:-}" ]; then + if [[ ! "$LIGHTNVR_GIT_COMMIT" =~ ^[0-9a-fA-F]{7,40}$ ]]; then + echo "Invalid LIGHTNVR_GIT_COMMIT: expected a 7-40 character hexadecimal commit" + exit 1 + fi + GIT_COMMIT="${LIGHTNVR_GIT_COMMIT:0:7}" + GIT_COMMIT="${GIT_COMMIT,,}" +else + GIT_COMMIT=$(cd "$PROJECT_ROOT" && git rev-parse --short HEAD 2>/dev/null || echo "unknown") +fi echo "Git commit: $GIT_COMMIT" # Create output directory if it doesn't exist