Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .containerignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
build/
td-install/
vendor/td/build/
vendor/libyuv/build/
tdlib_db/
recordings/
.envrc
54 changes: 42 additions & 12 deletions .github/workflows/build-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,24 @@ 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
- uses: actions/checkout@v5
with:
submodules: recursive

Expand All @@ -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
54 changes: 45 additions & 9 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
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. 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)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
Expand All @@ -11,8 +14,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) --

Expand Down Expand Up @@ -88,14 +99,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)
Expand All @@ -113,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")
"$<$<COMPILE_LANGUAGE:C,CXX>:SHELL:-include stdint.h>"
"$<$<COMPILE_LANGUAGE:C,CXX>:SHELL:-include stddef.h>")
target_compile_options(usrsctp PRIVATE
"SHELL:-include stdint.h" "SHELL:-include stddef.h")
target_compile_options(srtp PRIVATE
Expand Down Expand Up @@ -231,7 +260,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()
54 changes: 46 additions & 8 deletions Containerfile
Original file line number Diff line number Diff line change
@@ -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 libavformat-dev libavutil-dev \
zlib1g-dev libmp3lame-dev \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /src

Expand All @@ -11,14 +19,27 @@ 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) && \
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.)
Expand All @@ -30,14 +51,31 @@ 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 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{} cp -L {} /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 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

# /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 .

ENTRYPOINT ["./call_service"]
7 changes: 5 additions & 2 deletions Containerfile.tools
Original file line number Diff line number Diff line change
@@ -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"]
16 changes: 12 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ IMAGE ?= docker.io/nikicat/tg-echo-service
# (e.g. --layers --cache-from/--cache-to <registry>); 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
Expand Down Expand Up @@ -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
5 changes: 4 additions & 1 deletion stub/crc32c/crc32c_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_