From 3f91108b35d6e5b37a93422de4651891d1afa9ae Mon Sep 17 00:00:00 2001 From: Nikolay Bryskin Date: Tue, 23 Jun 2026 13:23:20 +0300 Subject: [PATCH 1/8] Build multi-arch image (amd64 + arm64) on an Ubuntu base Adds arm64 alongside amd64. Emulation is not viable for a from-source WebRTC build, so each arch builds natively: amd64 on ubuntu-24.04, arm64 on the ubuntu-24.04-arm hosted runner. A manifest job stitches the per-arch tags into a multi-arch :latest for both the image and the tools image. Switches the container base from Arch to Ubuntu. Official Arch is x86_64-only and the community Arch-Linux-ARM images are stale (keyring rot on pacman -Syu), whereas Ubuntu's official image is glibc + multi-arch and continuously rebuilt. glibc keeps the WebRTC/tgcalls compile low-risk (Alpine/musl would need source patching). The cmake build steps are unchanged; only the package layer and base differ. libyuv isn't packaged on Ubuntu, so it's built from the existing vendored submodule as a static PIC lib (the project finds it via find_library(yuv) and uses the vendored headers). The runtime stage copies the binary's shared-library closure via ldd instead of hand-listing version-suffixed Debian runtime packages, keeping the image minimal and arch-agnostic. The Makefile gains BASE_IMAGE and TAG knobs so CI builds/pushes per-arch tags through `make image`/`make push`; local `make image` is unchanged (defaults to ubuntu:24.04, :latest). Co-Authored-By: Claude Opus 4.8 (1M context) --- .containerignore | 1 + .github/workflows/build-image.yml | 52 ++++++++++++++++++++++++------- Containerfile | 45 +++++++++++++++++++++----- Containerfile.tools | 7 +++-- Makefile | 16 +++++++--- 5 files changed, 96 insertions(+), 25 deletions(-) diff --git a/.containerignore b/.containerignore index f5b3c61..0d8572a 100644 --- a/.containerignore +++ b/.containerignore @@ -1,6 +1,7 @@ build/ td-install/ vendor/td/build/ +vendor/libyuv/build/ tdlib_db/ recordings/ .envrc diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 9c1038f..36d9bb3 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -20,14 +20,21 @@ env: # GHCR image, derived from the repo (e.g. ghcr.io/nikicat/tg-echo-service). # Assumes the repo owner/name are lowercase, as GHCR requires. IMAGE: ghcr.io/${{ github.repository }} - # Registry-backed layer cache. podman has no GHA cache backend, so it - # pushes/pulls intermediate layers to this repo instead, letting the - # expensive TDLib/tgcalls/WebRTC layers survive across runs. - CACHE_REPO: ghcr.io/${{ github.repository }}/buildcache jobs: + # Build each architecture natively on its own runner (emulation is not viable + # for a from-source WebRTC build). The Ubuntu base is multi-arch, so both legs + # share one Containerfile with no per-arch base. build: - runs-on: ubuntu-24.04 # podman 4.9 — supports --layers --cache-to/--cache-from + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runner: ubuntu-24.04 + - arch: arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} timeout-minutes: 360 steps: - uses: actions/checkout@v4 @@ -47,12 +54,35 @@ jobs: - name: Build image if: github.event_name != 'push' || github.ref != 'refs/heads/master' run: | - make image IMAGE="$IMAGE" \ - IMAGE_BUILD_ARGS="--layers --cache-from $CACHE_REPO" + make image \ + IMAGE="$IMAGE" TAG="${{ matrix.arch }}" \ + IMAGE_BUILD_ARGS="--layers --cache-from $IMAGE/buildcache-${{ matrix.arch }}" - # master: build, populate the cache, and push the image + tools image. - - name: Build and push image + # master: build, populate the per-arch cache, and push the per-arch tag + # (image + tools image). The manifest job stitches them into :latest. + - name: Build and push per-arch image if: github.event_name == 'push' && github.ref == 'refs/heads/master' run: | - make push IMAGE="$IMAGE" \ - IMAGE_BUILD_ARGS="--layers --cache-from $CACHE_REPO --cache-to $CACHE_REPO" + make push \ + IMAGE="$IMAGE" TAG="${{ matrix.arch }}" \ + IMAGE_BUILD_ARGS="--layers --cache-from $IMAGE/buildcache-${{ matrix.arch }} --cache-to $IMAGE/buildcache-${{ matrix.arch }}" + + # Combine the per-arch tags into multi-arch manifests for the image and the + # tools image. Manifests are arch-agnostic metadata, so this runs on amd64. + manifest: + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + runs-on: ubuntu-24.04 + steps: + - name: Log in to GHCR + run: echo "${{ secrets.GITHUB_TOKEN }}" | podman login ghcr.io -u "${{ github.actor }}" --password-stdin + + - name: Create and push multi-arch manifests + run: | + for repo in "$IMAGE" "$IMAGE-tools"; do + podman manifest create "$repo:latest" + podman manifest add "$repo:latest" "$repo:amd64" + podman manifest add "$repo:latest" "$repo:arm64" + podman manifest push --all "$repo:latest" "docker://$repo:latest" + podman manifest rm "$repo:latest" + done diff --git a/Containerfile b/Containerfile index 5b19d8b..722e406 100644 --- a/Containerfile +++ b/Containerfile @@ -1,8 +1,16 @@ +# Base image. Ubuntu's official image is glibc + multi-arch (amd64/arm64), so a +# single base serves both architectures. Declared before FROM so both stages +# pick it up; override BASE_IMAGE to pin a different tag. +ARG BASE_IMAGE=docker.io/ubuntu:24.04 + # Stage 1: build -FROM docker.io/archlinux:latest AS builder +FROM ${BASE_IMAGE} AS builder -RUN pacman -Syu --noconfirm \ - base-devel git cmake openssl opus libvpx libyuv ffmpeg zlib gperf lame +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential git cmake pkg-config gperf ca-certificates \ + libssl-dev libopus-dev libvpx-dev libavcodec-dev libavutil-dev \ + zlib1g-dev libmp3lame-dev \ + && rm -rf /var/lib/apt/lists/* WORKDIR /src @@ -16,9 +24,17 @@ RUN cmake -B vendor/td/build -S vendor/td -DCMAKE_BUILD_TYPE=Release && \ cmake --build vendor/td/build -j$(nproc) && \ cmake --install vendor/td/build --prefix /src/td-install +# libyuv isn't packaged on Ubuntu, so build the vendored submodule as a static, +# position-independent lib that links into the (PIE) executable. The project +# locates it via find_library(yuv) and uses the vendored headers directly. +RUN cmake -B vendor/libyuv/build -S vendor/libyuv -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_POSITION_INDEPENDENT_CODE=ON && \ + cmake --build vendor/libyuv/build -j$(nproc) --target yuv && \ + install -Dm644 vendor/libyuv/build/libyuv.a /usr/local/lib/libyuv.a + # Custom tgcalls platform source (compiled into the tgcalls lib, so it must -# exist at configure time). Copied after TDLib so editing it doesn't bust the -# cached TDLib layer, but before configure since CMakeLists.txt references it. +# exist at configure time). Copied after the deps so editing it doesn't bust the +# cached TDLib/libyuv layers, but before configure since CMakeLists.txt uses it. COPY video_platform.cpp . # Configure and build all deps (tgcalls pulls in webrtc, absl, etc.) @@ -30,14 +46,27 @@ COPY main.cpp . RUN cmake --build build -j$(nproc) --target call_service RUN strip -s build/call_service +# Collect the binary's shared-library closure (minus the glibc core the runtime +# base already ships) into /rootfs. This keeps the runtime image minimal without +# hand-listing version-suffixed Debian runtime packages, and works identically +# on amd64 and arm64. +RUN mkdir -p /rootfs && \ + ldd build/call_service | awk '/=> \//{print $3}' | sort -u | \ + grep -vE '/(ld-linux.*|libc|libm|libdl|libpthread|librt|libresolv|libgcc_s)\.so' | \ + xargs -I{} install -D {} /rootfs{} + # Stage 2: runtime -FROM docker.io/archlinux:latest +FROM ${BASE_IMAGE} -RUN pacman -Syu --noconfirm openssl opus libvpx libyuv ffmpeg zlib lame && \ - pacman -Scc --noconfirm +# ca-certificates for TLS to Telegram; the shared libs come from /rootfs above. +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* WORKDIR /app +COPY --from=builder /rootfs/ / +RUN ldconfig COPY --from=builder /src/build/call_service . ENTRYPOINT ["./call_service"] diff --git a/Containerfile.tools b/Containerfile.tools index 21fc078..0a7a711 100644 --- a/Containerfile.tools +++ b/Containerfile.tools @@ -1,5 +1,8 @@ -FROM docker.io/archlinux:latest -RUN pacman -Syu --noconfirm make ffmpeg curl && pacman -Scc --noconfirm +ARG BASE_IMAGE=docker.io/ubuntu:24.04 +FROM ${BASE_IMAGE} +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + make ffmpeg curl ca-certificates \ + && rm -rf /var/lib/apt/lists/* COPY Makefile /Makefile WORKDIR /out ENTRYPOINT ["make", "-f", "/Makefile"] diff --git a/Makefile b/Makefile index 5ae2a2f..57a7a9e 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,14 @@ IMAGE ?= docker.io/nikicat/tg-echo-service # (e.g. --layers --cache-from/--cache-to ); empty for local builds. IMAGE_BUILD_ARGS ?= +# Container base image. Ubuntu's official image is glibc + multi-arch, so the +# same base builds on both amd64 and arm64. +BASE_IMAGE ?= docker.io/ubuntu:24.04 + +# Image tag. CI uses per-arch tags (amd64/arm64) and combines them into a +# multi-arch :latest manifest; local builds just use :latest. +TAG ?= latest + .PHONY: all submodules tdlib configure build clean run run-only prompt glados-prompt image image-tools push push-tools all: build @@ -73,16 +81,16 @@ run-only: ./build/call_service image-tools: - podman build -t $(IMAGE)-tools -f Containerfile.tools . + podman build --build-arg BASE_IMAGE=$(BASE_IMAGE) -t $(IMAGE)-tools:$(TAG) -f Containerfile.tools . image: image-tools submodules - podman build $(IMAGE_BUILD_ARGS) -t $(IMAGE) . + podman build --build-arg BASE_IMAGE=$(BASE_IMAGE) $(IMAGE_BUILD_ARGS) -t $(IMAGE):$(TAG) . push-tools: image-tools - podman push $(IMAGE)-tools + podman push $(IMAGE)-tools:$(TAG) push: image push-tools - podman push $(IMAGE) + podman push $(IMAGE):$(TAG) clean: rm -rf build vendor/td/build From a66ed7c8e771fe239ec66a8e41ff75f22ad928c9 Mon Sep 17 00:00:00 2001 From: Nikolay Bryskin Date: Tue, 23 Jun 2026 14:07:58 +0300 Subject: [PATCH 2/8] Fix arm64 build: gate x86 SIMD on arch; bump checkout to v5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The aarch64 leg failed with `unrecognized command-line option '-mavx'`: the vendored Build*.cmake files select arch-specific sources, SIMD flags, and defines off ANDROID_ABI, but CMakeLists.txt hardcoded it to "x86_64". The WebRTC/openh264 cmake already have full arm64-v8a branches (NEON sources, WEBRTC_ARCH_ARM64, HAVE_NEON_AARCH64), so mapping ANDROID_ABI to the real target arch clears the -mavx/-mavx2/-mfma injection automatically. Remaining un-gated x86 assumptions in CMakeLists.txt, now per-arch: - crc32c: the stub crc32c_config.h sets HAVE_SSE42 on x86_64 and HAVE_ARM64_CRC32C on aarch64, so compile the matching hardware impl (crc32c_sse42.cc with -msse4.2 / crc32c_arm64.cc with -march=armv8-a+crc+crypto) or the dispatcher hits an undefined reference. - HAVE_SSE2 → WEBRTC_ARCH_ARM64 + WEBRTC_HAS_NEON on arm64. Also bumps actions/checkout v4 → v5 to clear the Node 20 deprecation warning (v4 runs on the now-deprecated Node 20; v5 uses Node 24). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build-image.yml | 2 +- CMakeLists.txt | 44 ++++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 36d9bb3..c374656 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -37,7 +37,7 @@ jobs: runs-on: ${{ matrix.runner }} timeout-minutes: 360 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: recursive diff --git a/CMakeLists.txt b/CMakeLists.txt index dcd4e08..4f5d9ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,8 +11,16 @@ set(VENDOR_DIR "${CMAKE_SOURCE_DIR}/vendor") set(TGCALLS_DEPS_DIR "${VENDOR_DIR}") set(STUB_DIR "${CMAKE_SOURCE_DIR}/stub") -# Hack: BuildWebRTC.cmake uses ANDROID_ABI for platform-conditional sources -set(ANDROID_ABI "x86_64") +# Hack: the vendored Build*.cmake files use ANDROID_ABI to select +# platform-conditional sources, SIMD flags, and defines. Map the real target +# arch onto the ABI names they understand. +if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64") + set(ANDROID_ABI "arm64-v8a") +elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64|AMD64") + set(ANDROID_ABI "x86_64") +else() + message(FATAL_ERROR "Unsupported architecture: ${CMAKE_SYSTEM_PROCESSOR}") +endif() # -- System library targets (must exist before Build*.cmake includes) -- @@ -88,14 +96,29 @@ list(REMOVE_ITEM _webrtc_sources "${WEBRTC_DIR}/modules/audio_device/audio_device_impl.cc" ) list(APPEND _webrtc_sources - "${TGCALLS_DEPS_DIR}/crc32c/src/crc32c_sse42.cc" "${STUB_DIR}/audio_device_stub.cc" ) + +# Hardware-accelerated crc32c, per arch. The stub crc32c_config.h sets +# HAVE_SSE42 on x86_64 and HAVE_ARM64_CRC32C on aarch64, so the matching impl +# must be compiled in or the dispatcher hits an undefined reference. +if(ANDROID_ABI STREQUAL "x86_64") + list(APPEND _webrtc_sources "${TGCALLS_DEPS_DIR}/crc32c/src/crc32c_sse42.cc") +elseif(ANDROID_ABI STREQUAL "arm64-v8a") + list(APPEND _webrtc_sources "${TGCALLS_DEPS_DIR}/crc32c/src/crc32c_arm64.cc") +endif() + set_target_properties(webrtc PROPERTIES SOURCES "${_webrtc_sources}") -# crc32c_sse42.cc needs SSE4.2 -set_source_files_properties("${TGCALLS_DEPS_DIR}/crc32c/src/crc32c_sse42.cc" - PROPERTIES COMPILE_FLAGS "-msse4.2") +# crc32c_sse42.cc needs SSE4.2; crc32c_arm64.cc needs the ARMv8 CRC (__crc32cd) +# and PMULL (vmull_p64) extensions. +if(ANDROID_ABI STREQUAL "x86_64") + set_source_files_properties("${TGCALLS_DEPS_DIR}/crc32c/src/crc32c_sse42.cc" + PROPERTIES COMPILE_FLAGS "-msse4.2") +elseif(ANDROID_ABI STREQUAL "arm64-v8a") + set_source_files_properties("${TGCALLS_DEPS_DIR}/crc32c/src/crc32c_arm64.cc" + PROPERTIES COMPILE_FLAGS "-march=armv8-a+crc+crypto") +endif() # Add pthread (Linux needs it explicitly) target_link_libraries(webrtc PUBLIC pthread) @@ -231,7 +254,14 @@ target_include_directories(call_service PRIVATE target_compile_definitions(call_service PRIVATE WEBRTC_POSIX WEBRTC_LINUX - HAVE_SSE2 NDEBUG OPENSSL_SUPPRESS_DEPRECATED ) + +# Arch-specific WebRTC defines, mirroring what BuildWebRTC.cmake sets on the +# webrtc target so inline header code compiles the same way in this TU. +if(ANDROID_ABI STREQUAL "x86_64") + target_compile_definitions(call_service PRIVATE HAVE_SSE2) +elseif(ANDROID_ABI STREQUAL "arm64-v8a") + target_compile_definitions(call_service PRIVATE WEBRTC_ARCH_ARM64 WEBRTC_HAS_NEON) +endif() From 6e383ee832b1008241339597598dc1d78e798870 Mon Sep 17 00:00:00 2001 From: Nikolay Bryskin Date: Tue, 23 Jun 2026 14:17:25 +0300 Subject: [PATCH 3/8] Provide ffmpeg headers for the Debian/Ubuntu build WebRTC includes ffmpeg as "third_party/ffmpeg/libav{codec,format,util}/...", resolved through the vendored vendor/third_party/ffmpeg symlink that targets /usr/include (Arch's flat header layout). Debian/Ubuntu keep these headers under a multiarch triplet dir and split libavformat into its own -dev package, so the amd64 build failed with: fatal error: third_party/ffmpeg/libavcodec/avcodec.h: No such file or directory Add libavformat-dev (libavcodec/libavutil were already present) and repoint the ffmpeg symlink to /usr/include/ inside the build. avformat is only included for types, not linked, so the runtime closure is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- Containerfile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Containerfile b/Containerfile index 722e406..054a381 100644 --- a/Containerfile +++ b/Containerfile @@ -8,7 +8,7 @@ FROM ${BASE_IMAGE} AS builder RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ build-essential git cmake pkg-config gperf ca-certificates \ - libssl-dev libopus-dev libvpx-dev libavcodec-dev libavutil-dev \ + libssl-dev libopus-dev libvpx-dev libavcodec-dev libavformat-dev libavutil-dev \ zlib1g-dev libmp3lame-dev \ && rm -rf /var/lib/apt/lists/* @@ -19,6 +19,11 @@ COPY vendor/ vendor/ COPY stub/ stub/ COPY CMakeLists.txt ./ +# WebRTC includes ffmpeg as "third_party/ffmpeg/libav*/...". The vendored +# symlink targets /usr/include (Arch's flat layout); Debian/Ubuntu keep those +# headers under a multiarch triplet dir, so repoint it for this build. +RUN ln -sfn "/usr/include/$(uname -m)-linux-gnu" vendor/third_party/ffmpeg + # Build TDLib RUN cmake -B vendor/td/build -S vendor/td -DCMAKE_BUILD_TYPE=Release && \ cmake --build vendor/td/build -j$(nproc) && \ From 7db386d9a8a1d10501994d1213f6630e8ea2c96b Mon Sep 17 00:00:00 2001 From: Nikolay Bryskin Date: Tue, 23 Jun 2026 14:54:44 +0300 Subject: [PATCH 4/8] Fix empty HAVE_WEAK_GETAUXVAL breaking arm64 crc32c build On aarch64 the stub crc32c_config.h enables HAVE_ARM64_CRC32C, which makes crc32c_arm64_check.h compile `#if defined(__linux__) && (HAVE_STRONG_GETAUXVAL || HAVE_WEAK_GETAUXVAL)`. HAVE_WEAK_GETAUXVAL was defined empty, so the arm64 build failed with "operator '||' has no right operand". Give it a value (0; glibc provides the strong getauxval). x86 was unaffected since the whole block is HAVE_ARM64_CRC32C-gated. Co-Authored-By: Claude Opus 4.8 (1M context) --- stub/crc32c/crc32c_config.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/stub/crc32c/crc32c_config.h b/stub/crc32c/crc32c_config.h index 259e11d..b7316c3 100644 --- a/stub/crc32c/crc32c_config.h +++ b/stub/crc32c/crc32c_config.h @@ -17,6 +17,9 @@ #endif #define HAVE_STRONG_GETAUXVAL 1 -#define HAVE_WEAK_GETAUXVAL +// Must have a value: crc32c_arm64_check.h tests it arithmetically as +// `#if (HAVE_STRONG_GETAUXVAL || HAVE_WEAK_GETAUXVAL)`, so an empty define +// expands to a dangling `||`. glibc provides the strong getauxval anyway. +#define HAVE_WEAK_GETAUXVAL 0 #endif // CRC32C_CRC32C_CONFIG_H_ From f5f623d970c9b4ffb4645525795c63803d5dacd6 Mon Sep 17 00:00:00 2001 From: Nikolay Bryskin Date: Tue, 23 Jun 2026 15:26:50 +0300 Subject: [PATCH 5/8] Enable ASM language so openh264 arm64 NEON assembles The arm64 link failed with undefined references to openh264's aarch64 NEON routines (WelsDctT4_AArch64_neon, SampleVariance16x16_AArch64_neon, ...). HAVE_NEON_AARCH64 makes the C++ call them, and BuildOpenH264.cmake adds the matching .S sources, but project() only enabled C/CXX, so CMake silently skipped every .S file and their symbols never entered the archive. Add ASM to the project languages. x86 is unaffected (openh264 has no x86 assembly branch; WebRTC's arm64 NEON paths are .c/.cc, already compiled). Co-Authored-By: Claude Opus 4.8 (1M context) --- CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f5d9ab..01421f7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,7 @@ cmake_minimum_required(VERSION 3.16) -project(call_service LANGUAGES C CXX) +# ASM is needed to assemble openh264's aarch64 NEON .S files on arm64 (unused on +# x86, where openh264 has no assembly sources). +project(call_service LANGUAGES C CXX ASM) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) From 8d0f210c32d3186308ab1bf9c69e771b963c7aa2 Mon Sep 17 00:00:00 2001 From: Nikolay Bryskin Date: Tue, 23 Jun 2026 15:35:55 +0300 Subject: [PATCH 6/8] Correct misleading comment about x86 openh264 assembly openh264 does ship x86 SIMD, but as NASM/YASM .asm files this build doesn't wire up; x86_64 uses the C++ fallbacks. The old comment claimed there were no x86 assembly sources at all. Co-Authored-By: Claude Opus 4.8 (1M context) --- CMakeLists.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 01421f7..f6e1183 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,7 @@ cmake_minimum_required(VERSION 3.16) -# ASM is needed to assemble openh264's aarch64 NEON .S files on arm64 (unused on -# x86, where openh264 has no assembly sources). +# ASM is needed to assemble openh264's aarch64 NEON .S files on arm64. openh264 +# also ships x86 SIMD, but as NASM/YASM .asm (needs a separate assembler) which +# this build doesn't wire up, so x86_64 falls back to the portable C++ paths. project(call_service LANGUAGES C CXX ASM) set(CMAKE_CXX_STANDARD 20) From b2d1290dc587973deff238c893d5fa32d2dac085 Mon Sep 17 00:00:00 2001 From: Nikolay Bryskin Date: Tue, 23 Jun 2026 15:56:46 +0300 Subject: [PATCH 7/8] Keep C force-includes off openh264 arm64 .S assembly With ASM enabled, openh264's aarch64 .S files failed to assemble: bits/stdint-least.h: Error: unknown mnemonic `typedef' target_compile_options applies to every language, so the GCC-compat "-include stdint.h/stddef.h" force-includes were prepended to the assembler input, feeding C typedefs to the assembler. Gate them to C/CXX via a COMPILE_LANGUAGE generator expression so the .S sources assemble clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- CMakeLists.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f6e1183..c541731 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -139,8 +139,11 @@ target_compile_options(webrtc PRIVATE -Wno-error -Wno-deprecated-declarations "SHELL:-include stdint.h" "SHELL:-include stddef.h") target_compile_options(absl PRIVATE -Wno-error "SHELL:-include stdint.h" "SHELL:-include stddef.h") +# Force-includes are for the C/C++ sources only — openh264's arm64 .S assembly +# sources must not get them (the assembler can't parse stdint.h's typedefs). target_compile_options(openh264 PRIVATE - "SHELL:-include stdint.h" "SHELL:-include stddef.h") + "$<$:SHELL:-include stdint.h>" + "$<$:SHELL:-include stddef.h>") target_compile_options(usrsctp PRIVATE "SHELL:-include stdint.h" "SHELL:-include stddef.h") target_compile_options(srtp PRIVATE From 2a0d376f3893e492f335e07eba1edf15402370ed Mon Sep 17 00:00:00 2001 From: Nikolay Bryskin Date: Tue, 23 Jun 2026 16:21:47 +0300 Subject: [PATCH 8/8] Stage runtime libs in /usr/local/lib to avoid usrmerge breakage The arm64 runtime stage failed at `RUN ldconfig` with "exec /bin/sh: No such file or directory (missing dynamic library?)". The shared-lib closure was copied path-preserving (install -D {} /rootfs{} then COPY /rootfs/ /). On arm64 ldd reports some libs under /lib/aarch64-linux-gnu, so this recreated a real /lib and copied it over the base image's /lib -> /usr/lib usrmerge symlink, breaking the dynamic linker. amd64 only escaped because its libs all resolved under /usr/lib. Copy the closure flat (cp -L) and drop it into /usr/local/lib in the runtime stage, then ldconfig. /usr/local/lib is already on the default loader path, so no system lib dirs (or the /lib symlink) are touched. Works the same on both arches. Co-Authored-By: Claude Opus 4.8 (1M context) --- Containerfile | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Containerfile b/Containerfile index 054a381..57d0dbb 100644 --- a/Containerfile +++ b/Containerfile @@ -52,25 +52,29 @@ RUN cmake --build build -j$(nproc) --target call_service RUN strip -s build/call_service # Collect the binary's shared-library closure (minus the glibc core the runtime -# base already ships) into /rootfs. This keeps the runtime image minimal without -# hand-listing version-suffixed Debian runtime packages, and works identically -# on amd64 and arm64. +# base already ships) into a flat dir. The runtime stage drops these into +# /usr/local/lib; copying flat avoids recreating /lib as a real directory, which +# (via Ubuntu's /lib -> /usr/lib usrmerge symlink) a path-preserving copy onto / +# would clobber, breaking the dynamic linker. This keeps the runtime minimal +# without hand-listing version-suffixed Debian packages, identically on amd64/arm64. RUN mkdir -p /rootfs && \ ldd build/call_service | awk '/=> \//{print $3}' | sort -u | \ grep -vE '/(ld-linux.*|libc|libm|libdl|libpthread|librt|libresolv|libgcc_s)\.so' | \ - xargs -I{} install -D {} /rootfs{} + xargs -I{} cp -L {} /rootfs/ # Stage 2: runtime FROM ${BASE_IMAGE} -# ca-certificates for TLS to Telegram; the shared libs come from /rootfs above. +# ca-certificates for TLS to Telegram; the app's shared libs come from /rootfs. RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ ca-certificates \ && rm -rf /var/lib/apt/lists/* WORKDIR /app -COPY --from=builder /rootfs/ / +# /usr/local/lib is on the default loader path; ldconfig registers the copied +# libs (and their soname links) without touching system lib dirs. +COPY --from=builder /rootfs/ /usr/local/lib/ RUN ldconfig COPY --from=builder /src/build/call_service .