yams/Dockerfile
trvon 8250b061fc
build: release buildtype with split debug symbols
Release used buildtype=debugoptimized (-O2 -g, strip=false) and relied on
scripts/prune-runtime-install.sh to strip the staged payload, discarding
the debug info. Crash reports from shipped binaries could not be
symbolicated and the build type label did not match what shipped.

- setup.sh/setup.ps1 Release and the Meson project default use
  buildtype=release. setup.sh adds -Ddebug=true for Release so -O3 code
  keeps DWARF for the split, and passes -Db_pie=true explicitly (also a
  project default) so reconfigured build dirs pick it up. Windows keeps
  release without -g so no PDBs land in the MSI stage. Debug/Fuzzing are
  unchanged (-O0 -g, assertions on).
- scripts/split-debug-symbols.sh replaces prune-runtime-install.sh: ELF
  files get objcopy --only-keep-debug (compressed) into <debug-dir>/
  <path>.debug plus .build-id/xx/yyyy.debug links, strip
  --strip-unneeded and --add-gnu-debuglink; Mach-O files get dsymutil
  dSYMs and strip -S -x. Without a debug dir it only strips.
- The release job splits into $BUILD_DIR/debug-symbols and uploads it as
  a debug-symbols-<suffix> workflow artifact (30 days) instead of
  shipping or dropping it. build-deb.sh, the Dockerfile, PKGBUILD and
  build-arch-pkg.sh call the split script in strip-only mode.

Measured on grim (Linux x86_64, clang 21, stripped staged payload):

  file                         debugoptimized  release -O3   delta
  bin/yams-cli                     35,528,968   36,733,304   +3.4%
  bin/yams-daemon                  29,731,312   30,685,792   +3.2%
  bin/yams-mcp-server              19,504,928   19,865,472   +1.8%
  libyams_onnx_resource.so            477,280      481,488   +0.9%
  libyams_object_storage_s3.so      8,226,608    8,239,016   +0.2%
  yams_zyp.so                          79,936       80,040   +0.1%

Debug symbols: 309 MiB (compressed .debug files); gdb resolves
`info line main` on the stripped daemon through the .build-id tree.
check_install_surface: no debug sections or .symtab remain; the
remaining violations are the export leaks fixed later in this series.

Signed-off-by: trvon <me@trevon.dev>
2026-10-01 16:53:23 -06:00

301 lines
11 KiB
Docker

# syntax=docker/dockerfile:1.7-labs
# Stage 0: base build dependencies & toolchain
# Using Ubuntu 24.04 for GCC 13+ which supports C++23 <format> header
FROM ubuntu:24.04 AS deps
# FROM debian:trixie-slim AS deps
ARG DEBIAN_FRONTEND=noninteractive
RUN set -eux; \
for i in 1 2 3; do \
apt-get update && \
apt-get install -y --no-install-recommends --fix-missing \
build-essential git curl pkg-config ca-certificates xz-utils \
libssl-dev libsqlite3-dev libsqlite3-0 protobuf-compiler libprotobuf-dev \
libcurl4-openssl-dev \
python3 python3-venv python3-pip \
gcc g++ ninja-build openssl lld llvm clang \
libc++-dev libc++abi-dev \
liburing-dev ccache \
gfortran libomp-18-dev wget \
libopenblas-dev \
cmake && \
rm -rf /var/lib/apt/lists/* && break || \
{ echo "Attempt $i failed, retrying after 5s..."; sleep 5; apt-get clean; rm -rf /var/lib/apt/lists/*; }; \
done
# Install Zig 0.15.2 for zyp PDF plugin (wget -c resumes partial downloads)
ARG ZIG_VERSION=0.15.2
RUN set -eux; \
ARCH=$(uname -m); \
case "$ARCH" in \
x86_64) ZIG_ARCH="x86_64" ;; \
aarch64) ZIG_ARCH="aarch64" ;; \
*) echo "Unsupported architecture: $ARCH"; exit 1 ;; \
esac; \
case "${ZIG_VERSION}" in \
0.15.*) ;; \
*) echo "Unsupported Zig version ${ZIG_VERSION}"; exit 1 ;; \
esac; \
wget -q --show-progress --tries=20 --retry-connrefused -c \
"https://ziglang.org/download/${ZIG_VERSION}/zig-${ZIG_ARCH}-linux-${ZIG_VERSION}.tar.xz" \
-O /tmp/zig.tar.xz && \
tar -xJf /tmp/zig.tar.xz -C /opt && \
mv /opt/zig-${ZIG_ARCH}-linux-${ZIG_VERSION} /opt/zig && \
ln -sf /opt/zig/zig /usr/local/bin/zig && \
rm /tmp/zig.tar.xz && \
zig version
# Install Rust toolchain for libsql build (pinned version to avoid cross-device link issues)
ENV RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo \
PATH=/usr/local/cargo/bin:$PATH
RUN set -eux; \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain 1.83.0 --profile minimal && \
rustup set auto-self-update disable && \
rustc --version && cargo --version
RUN python3 -m venv /opt/venv \
&& /opt/venv/bin/pip install --upgrade pip \
&& /opt/venv/bin/pip install "conan==2.5.*" "meson==1.4.*" "ninja==1.11.*"
ENV PATH="/opt/venv/bin:${PATH}"
WORKDIR /src
# Stage 1: builder - use setup.sh for consistent build process
FROM deps AS builder
ARG DOCKERFILE_CONF_REV=5
ARG YAMS_VERSION=dev
ARG GIT_COMMIT=""
ARG GIT_TAG=""
ARG BUILD_TESTS=false
ARG BUILD_BENCHMARKS=false
ARG BUILD_DOCS=false
ARG YAMS_CPPSTD=20
ARG BUILD_JOBS=2
COPY . .
# Initialize git submodules (sqlite-vec-cpp, etc.)
RUN git init 2>/dev/null || true && \
git config --global url."https://github.com/".insteadOf "git@github.com:" && \
git submodule update --init --recursive 2>/dev/null || \
echo "Submodule init skipped (not a git repo or no submodules)"
# Use setup.sh with retry logic for Conan remote issues
# Configure Conan to retry downloads more aggressively for transient network failures
RUN --mount=type=cache,target=/root/.conan2 \
--mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/src/subprojects/libsql/target \
set -eux; \
# Prevent rustup from auto-updating when rust-toolchain.toml requests different targets
export RUSTUP_TOOLCHAIN=1.83.0 && \
mkdir -p /root/.conan2 && \
printf 'core.net.http:timeout=120\ncore.net.http:max_retries=10\ncore.download:retry=10\ncore.download:retry_wait=5\n' > /root/.conan2/global.conf && \
conan --version && \
conan profile detect --force && \
echo '=== Conan remotes ==='; conan remote list || true && \
if ! conan remote list | grep -q 'conancenter'; then \
conan remote add conancenter https://center2.conan.io; \
fi && \
conan remote update conancenter --url https://center2.conan.io || true && \
export YAMS_COMPILER=gcc; \
export YAMS_CPPSTD=${YAMS_CPPSTD}; \
export YAMS_EXTRA_MESON_FLAGS="-Drequire-sqlite-vec=false"; \
sed -i 's/\r$//' setup.sh && chmod +x setup.sh && \
for attempt in 1 2 3; do \
echo "=== Build attempt $attempt ==="; \
if [ "${BUILD_TESTS}" = "true" ]; then \
echo "BUILD_TESTS=true: enabling Release tests"; \
SETUP_ARGS="Release --with-tests --with-onnx"; \
else \
SETUP_ARGS="Release --with-onnx"; \
fi; \
# shellcheck disable=SC2086
if ./setup.sh ${SETUP_ARGS}; then \
break; \
else \
echo "setup.sh attempt $attempt failed"; \
if [ $attempt -lt 3 ]; then \
echo "Cleaning cache and retrying after 30s..."; \
rm -rf /src/build/release; \
rm -rf /root/.conan2/p/*/metadata.json 2>/dev/null || true; \
conan cache clean "*" || true; \
sleep 30; \
else \
echo "All attempts failed"; \
exit 1; \
fi; \
fi; \
done; \
CONAN_BUILD_ENV="/src/build/release/build-release/conan/conanbuild.sh"; \
if [ -f "${CONAN_BUILD_ENV}" ]; then set +u; . "${CONAN_BUILD_ENV}"; set -u; fi && \
meson compile -C build/release -j${BUILD_JOBS} && \
meson install -C build/release --destdir /opt/yams && \
# Ensure Conan-provided runtime libs are present as real files (not only symlinks)
# so that Meson tests can run in later stages without access to /root/.conan2.
python3 - <<'PY'
import glob
import os
import platform
import shutil
import re
destinations = ['/src/build/release', '/opt/yams/usr/local/lib']
for dst in destinations:
os.makedirs(dst, exist_ok=True)
copied = set()
created_links = set()
elf_machine_codes = {
'x86_64': 0x3E,
'aarch64': 0xB7,
'arm64': 0xB7,
}
expected_machine = elf_machine_codes.get(platform.machine().lower())
def elf_machine(path: str):
try:
with open(path, 'rb') as fh:
header = fh.read(20)
except OSError:
return None
if len(header) < 20 or header[:4] != b'\x7fELF':
return None
endian = header[5]
if endian == 1:
return int.from_bytes(header[18:20], 'little')
if endian == 2:
return int.from_bytes(header[18:20], 'big')
return None
for pat in [
'/src/build/release/**/libtbb*.so*',
'/src/build/release/**/libonnxruntime*.so*',
'/root/.conan2/p/**/p/lib/libtbb*.so*',
'/root/.conan2/p/**/p/lib/libtbbbind*.so*',
'/root/.conan2/p/**/p/lib/libonnxruntime*.so*',
'/root/.conan2/p/**/lib/libtbb*.so*',
'/root/.conan2/p/**/lib/libtbbbind*.so*',
'/root/.conan2/p/**/lib/libonnxruntime*.so*',
]:
for p in glob.glob(pat, recursive=True):
base = os.path.basename(p)
rp = os.path.realpath(p)
if not os.path.isfile(rp):
continue
machine = elf_machine(rp)
if expected_machine is not None and machine is not None and machine != expected_machine:
continue
real_base = os.path.basename(rp)
if real_base not in copied:
for dst in destinations:
dest_path = os.path.join(dst, real_base)
if not (os.path.exists(dest_path) and os.path.samefile(rp, dest_path)):
shutil.copy2(rp, dest_path)
copied.add(real_base)
if base != real_base:
for dst in destinations:
link_path = os.path.join(dst, base)
if not os.path.lexists(link_path):
os.symlink(real_base, link_path)
created_links.add((dst, base))
def ensure_soname_links(directory: str) -> int:
created = 0
# If we have libfoo.so.M.N or libfoo.so.M.N.K, create libfoo.so.M -> that file.
rx = re.compile(r'^(?P<stem>lib.+\.so\.(?P<maj>\d+))\.(?P<rest>\d+(?:\.\d+)*)$')
for name in os.listdir(directory):
m = rx.match(name)
if not m:
continue
target = name
link_name = m.group('stem')
link_path = os.path.join(directory, link_name)
if os.path.lexists(link_path):
continue
os.symlink(target, link_path)
created += 1
return created
links = sum(ensure_soname_links(dst) for dst in destinations)
print(
f'Copied {len(copied)} runtime libs into {", ".join(destinations)}; '
f'preserved {len(created_links)} package symlinks; created {links} SONAME symlinks'
)
PY
RUN set -eux; \
bash scripts/split-debug-symbols.sh /opt/yams/usr/local
# Stage 1.25: smoke tests (optional)
# Note: kept separate so Linux builder images can be produced even when
# unrelated tests are failing.
FROM builder AS smoke-tests
ARG BUILD_TESTS=false
ARG BUILD_JOBS=2
RUN set -eux; \
if [ "${BUILD_TESTS}" != "true" ]; then \
echo "BUILD_TESTS=false: skipping smoke tests"; \
exit 0; \
fi; \
# Conan runtime_deploy copies shared libs into build dir; add it to loader path.
export LD_LIBRARY_PATH="/opt/yams/usr/local/lib:/src/build/release:${LD_LIBRARY_PATH:-}"; \
meson test -C build/release -j${BUILD_JOBS} --no-rebuild --print-errorlogs mcp_submodule cli_submodule
# Stage 1.3: full test suite (optional)
FROM builder AS tests
ARG BUILD_TESTS=false
ARG BUILD_JOBS=2
RUN set -eux; \
if [ "${BUILD_TESTS}" != "true" ]; then \
echo "BUILD_TESTS=false: skipping full test suite"; \
exit 0; \
fi; \
export LD_LIBRARY_PATH="/opt/yams/usr/local/lib:/src/build/release:${LD_LIBRARY_PATH:-}"; \
meson test -C build/release -j${BUILD_JOBS} --no-rebuild --print-errorlogs
# Stage 1.5: runtime self-test (fails the build if runtime deps are missing)
FROM builder AS runtime-test
RUN set -eux; \
export LD_LIBRARY_PATH="/opt/yams/usr/local/lib:${LD_LIBRARY_PATH:-}"; \
/opt/yams/usr/local/bin/yams --version >/dev/null; \
if [ -x /opt/yams/usr/local/bin/yams-mcp-server ]; then /opt/yams/usr/local/bin/yams-mcp-server --help >/dev/null; fi
FROM debian:trixie-slim AS runtime
ARG DEBIAN_FRONTEND=noninteractive
RUN set -eux; \
for i in 1 2 3; do \
apt-get update && \
apt-get install -y --no-install-recommends --fix-missing ca-certificates liburing2 && \
rm -rf /var/lib/apt/lists/* && break || \
{ echo "Attempt $i failed, retrying after 5s..."; sleep 5; apt-get clean; rm -rf /var/lib/apt/lists/*; }; \
done
RUN groupadd -r yams && useradd -r -g yams -s /bin/false yams
COPY --from=builder /opt/yams /opt/yams
ENV YAMS_PREFIX=/opt/yams/usr/local
ENV PATH="${YAMS_PREFIX}/bin:${PATH}"
ENV LD_LIBRARY_PATH="${YAMS_PREFIX}/lib:${LD_LIBRARY_PATH}"
# Backward compatibility: retain standalone yams symlink
RUN ln -sf ${YAMS_PREFIX}/bin/yams /usr/local/bin/yams && \
if [ -f ${YAMS_PREFIX}/bin/yams-daemon ]; then ln -sf ${YAMS_PREFIX}/bin/yams-daemon /usr/local/bin/yams-daemon; fi && \
mkdir -p /usr/local/share/yams/data
# Copy data files if they exist (optional)
# Note: Disabled because /src/data/magic_numbers.json doesn't exist yet
# RUN --mount=type=bind,from=builder,source=/src/data,target=/tmp/data \
# if [ -f /tmp/data/magic_numbers.json ]; then \
# cp /tmp/data/magic_numbers.json /usr/local/share/yams/data/; \
# fi
RUN mkdir -p /home/yams/.local/share/yams /home/yams/.config/yams && chown -R yams:yams /home/yams
RUN mknod /dev/tty c 5 0 2>/dev/null || true && \
chmod 666 /dev/tty 2>/dev/null || true
USER yams
WORKDIR /home/yams
ENV YAMS_STORAGE="/home/yams/.local/share/yams" \
XDG_DATA_HOME="/home/yams/.local/share" \
XDG_CONFIG_HOME="/home/yams/.config"
LABEL org.opencontainers.image.description="YAMS server & daemon. Run 'yams serve' for MCP or 'yams-daemon' for socket daemon."
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD yams --version || exit 1
ENTRYPOINT ["yams"]
CMD ["--help"]