feat(image): publish universal image flavor with toolchains + customize docs (#891)
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
# Kandev Universal Dockerfile
|
||||
#
|
||||
# Extends the vanilla kandev image with language toolchains and developer
|
||||
# tools so agents can build/test code in the most common stacks without
|
||||
# extra setup. Built and published alongside vanilla in the release
|
||||
# workflow. Inclusion policy: see docs/images.md.
|
||||
#
|
||||
# Build:
|
||||
# docker build -f Dockerfile.universal -t kandev:latest-universal .
|
||||
#
|
||||
# Run (drop-in replacement for the vanilla image):
|
||||
# docker run -p 38429:38429 -v kandev-data:/data ghcr.io/kdlbs/kandev:universal
|
||||
#
|
||||
# The BASE_IMAGE arg lets the release workflow point at a freshly built
|
||||
# vanilla image inside the same run (so universal-of-X.Y.Z is built on top
|
||||
# of vanilla-X.Y.Z, not on whatever :latest happens to be).
|
||||
|
||||
ARG BASE_IMAGE=ghcr.io/kdlbs/kandev:latest
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
USER root
|
||||
|
||||
# Pinned versions. Bump these deliberately; CI's size gate will catch growth.
|
||||
#
|
||||
# The Playwright Chromium apt deps below are hand-derived against Playwright
|
||||
# 1.48.0. When that target moves, the apt-get install line may need to
|
||||
# add/drop packages - check the upstream list at
|
||||
# https://github.com/microsoft/playwright/blob/v<version>/packages/playwright-core/src/server/registry/nativeDeps.ts
|
||||
# or run `npx playwright@<version> install-deps --dry-run` to diff. (Tracked
|
||||
# only via this comment, not an ARG, since the apt package names are hardcoded
|
||||
# below and a build-arg override would silently disagree with what's installed.)
|
||||
ARG GO_VERSION=1.26.0
|
||||
ARG GOLANGCI_LINT_VERSION=1.62.0
|
||||
ARG PNPM_VERSION=9.15.9
|
||||
|
||||
# Single RUN to minimize image layers and clean apt lists in the same layer.
|
||||
# Tools grouped by purpose; comments explain the why for each cluster so
|
||||
# future maintainers can prune deliberately rather than guess.
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
# Native compile (CGO, pip wheels with C extensions, native npm modules)
|
||||
build-essential \
|
||||
pkg-config \
|
||||
python3-dev \
|
||||
libssl-dev \
|
||||
# Useful CLI tools agents reach for. Small, broadly applicable.
|
||||
ripgrep \
|
||||
fd-find \
|
||||
jq \
|
||||
# Playwright Chromium runtime deps. Browsers themselves are NOT
|
||||
# preinstalled - users run `pnpm exec playwright install chromium`
|
||||
# once and the download persists at ~/.cache/ms-playwright on the PV.
|
||||
# This list mirrors `npx playwright@1.48.0 install-deps chromium`.
|
||||
libnss3 \
|
||||
libnspr4 \
|
||||
libatk1.0-0 \
|
||||
libatk-bridge2.0-0 \
|
||||
libcups2 \
|
||||
libdrm2 \
|
||||
libxkbcommon0 \
|
||||
libxcomposite1 \
|
||||
libxdamage1 \
|
||||
libxfixes3 \
|
||||
libxrandr2 \
|
||||
libgbm1 \
|
||||
libpango-1.0-0 \
|
||||
libcairo2 \
|
||||
libasound2 \
|
||||
fonts-liberation \
|
||||
fonts-noto-color-emoji \
|
||||
; \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Go toolchain - tarball install to /usr/local/go (the standard layout the
|
||||
# go.dev installer creates). Add to PATH below.
|
||||
#
|
||||
# Integrity: go.dev publishes a per-tarball `.sha256` file (a bare hex hash,
|
||||
# no filename), fetched alongside the tarball and verified before extraction.
|
||||
# This is TOFU on first fetch; if the go.dev CDN itself is compromised both
|
||||
# files are bad. Strongest available option without bumping the hash inline
|
||||
# every Go release.
|
||||
RUN set -eux; \
|
||||
arch="$(dpkg --print-architecture)"; \
|
||||
case "$arch" in \
|
||||
amd64) goarch=amd64 ;; \
|
||||
arm64) goarch=arm64 ;; \
|
||||
*) echo "unsupported arch: $arch" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${goarch}.tar.gz" -o /tmp/go.tar.gz; \
|
||||
curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${goarch}.tar.gz.sha256" -o /tmp/go.tar.gz.sha256; \
|
||||
echo "$(cat /tmp/go.tar.gz.sha256) /tmp/go.tar.gz" | sha256sum -c -; \
|
||||
tar -C /usr/local -xzf /tmp/go.tar.gz; \
|
||||
rm /tmp/go.tar.gz /tmp/go.tar.gz.sha256
|
||||
|
||||
# golangci-lint - prebuilt binary, no compilation needed.
|
||||
# Integrity: each release publishes a single `*-checksums.txt` file with
|
||||
# `<sha256> <filename>` lines for every artifact. Grep for our tarball,
|
||||
# pipe to `sha256sum -c -`. Download into /tmp and cd there so the relative
|
||||
# filename in the checksum line resolves.
|
||||
RUN set -eux; \
|
||||
arch="$(dpkg --print-architecture)"; \
|
||||
case "$arch" in \
|
||||
amd64) lintarch=amd64 ;; \
|
||||
arm64) lintarch=arm64 ;; \
|
||||
*) echo "unsupported arch: $arch" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
tarball="golangci-lint-${GOLANGCI_LINT_VERSION}-linux-${lintarch}.tar.gz"; \
|
||||
base="https://github.com/golangci/golangci-lint/releases/download/v${GOLANGCI_LINT_VERSION}"; \
|
||||
curl -fsSL "${base}/${tarball}" -o "/tmp/${tarball}"; \
|
||||
curl -fsSL "${base}/golangci-lint-${GOLANGCI_LINT_VERSION}-checksums.txt" -o /tmp/golangci-lint-checksums.txt; \
|
||||
(cd /tmp && grep " ${tarball}$" golangci-lint-checksums.txt | sha256sum -c -); \
|
||||
tar -xz -C /tmp -f "/tmp/${tarball}"; \
|
||||
mv "/tmp/golangci-lint-${GOLANGCI_LINT_VERSION}-linux-${lintarch}/golangci-lint" /usr/local/bin/; \
|
||||
rm -rf "/tmp/${tarball}" "/tmp/golangci-lint-${GOLANGCI_LINT_VERSION}-linux-${lintarch}" /tmp/golangci-lint-checksums.txt
|
||||
|
||||
# Rust toolchain via rustup. RUSTUP_HOME/CARGO_HOME live in /usr/local so the
|
||||
# install is system-wide and read-only for non-root users at runtime -
|
||||
# matching the rest of the image. Users who need writable per-user state
|
||||
# (e.g. `cargo install <tool>`, `rustup toolchain install nightly`) should
|
||||
# override CARGO_HOME and RUSTUP_HOME to a path on the PV, e.g.:
|
||||
# CARGO_HOME=~/.cargo RUSTUP_HOME=~/.rustup cargo install <tool>
|
||||
#
|
||||
# `--profile minimal` keeps the install lean (rustc + cargo + rust-std); we
|
||||
# then explicitly add clippy and rustfmt, which agents working on Rust
|
||||
# projects almost always reach for. `--profile default` would also work but
|
||||
# pulls in rust-docs which we don't need.
|
||||
#
|
||||
# Integrity: download rustup-init for our arch and verify its SHA-256
|
||||
# against the upstream .sha256 sidecar before executing - matches the
|
||||
# pattern used for Go/golangci-lint/yq above, rather than `curl | sh`.
|
||||
ENV RUSTUP_HOME=/usr/local/rustup \
|
||||
CARGO_HOME=/usr/local/cargo
|
||||
RUN set -eux; \
|
||||
arch="$(dpkg --print-architecture)"; \
|
||||
case "$arch" in \
|
||||
amd64) triplet=x86_64-unknown-linux-gnu ;; \
|
||||
arm64) triplet=aarch64-unknown-linux-gnu ;; \
|
||||
*) echo "unsupported arch: $arch" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
base="https://static.rust-lang.org/rustup/dist/${triplet}"; \
|
||||
curl -fsSL "${base}/rustup-init" -o /tmp/rustup-init; \
|
||||
curl -fsSL "${base}/rustup-init.sha256" -o /tmp/rustup-init.sha256; \
|
||||
(cd /tmp && sha256sum -c rustup-init.sha256); \
|
||||
chmod +x /tmp/rustup-init; \
|
||||
/tmp/rustup-init -y --default-toolchain stable --no-modify-path --profile minimal; \
|
||||
/usr/local/cargo/bin/rustup component add --toolchain stable clippy rustfmt; \
|
||||
rm /tmp/rustup-init /tmp/rustup-init.sha256; \
|
||||
chmod -R a+rX "$RUSTUP_HOME" "$CARGO_HOME"
|
||||
|
||||
# pnpm via corepack - install system-wide so users don't need
|
||||
# `corepack enable` themselves. Drops shims into /usr/local/bin.
|
||||
#
|
||||
# No separate sha256 step: corepack downloads from registry.npmjs.org and
|
||||
# verifies the tarball against the registry's `integrity` field (SHA-512)
|
||||
# before activating. The other tools above pin upstream `.sha256` sidecars
|
||||
# because they're served from CDNs that don't expose package integrity at
|
||||
# the API level; npm does. Decision is deliberate, not an oversight.
|
||||
RUN set -eux; \
|
||||
corepack enable; \
|
||||
corepack prepare "pnpm@${PNPM_VERSION}" --activate
|
||||
|
||||
# fd is installed as `fdfind` on Debian to avoid clashing with the legacy
|
||||
# fd package - symlink so agents calling plain `fd` still work.
|
||||
RUN ln -s /usr/bin/fdfind /usr/local/bin/fd
|
||||
|
||||
# yq is not in Debian stable; install the prebuilt binary from upstream
|
||||
# (Mike Farah's yq, the Go implementation - the one most scripts assume).
|
||||
#
|
||||
# Integrity: mikefarah/yq publishes a `checksums` file with one row per
|
||||
# artifact and multiple hash columns, plus a `checksums_hashes_order` file
|
||||
# that lists which hash algorithm corresponds to each column. We compute the
|
||||
# SHA-256 column index dynamically (defensive against upstream reordering)
|
||||
# and verify the binary against that column before installing.
|
||||
ARG YQ_VERSION=4.44.3
|
||||
RUN set -eux; \
|
||||
arch="$(dpkg --print-architecture)"; \
|
||||
target="yq_linux_${arch}"; \
|
||||
base="https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}"; \
|
||||
curl -fsSL "${base}/checksums" -o /tmp/yq-checksums; \
|
||||
curl -fsSL "${base}/checksums_hashes_order" -o /tmp/yq-hashes-order; \
|
||||
sha256_col="$(awk '/^SHA-256$/ {print NR + 1; exit}' /tmp/yq-hashes-order)"; \
|
||||
if [ -z "$sha256_col" ]; then echo "SHA-256 not in upstream checksums_hashes_order" >&2; exit 1; fi; \
|
||||
expected="$(awk -v t="$target" -v c="$sha256_col" '$1==t {print $c}' /tmp/yq-checksums)"; \
|
||||
if [ -z "$expected" ]; then echo "no checksum row for $target" >&2; exit 1; fi; \
|
||||
curl -fsSL "${base}/${target}" -o /tmp/yq; \
|
||||
echo "${expected} /tmp/yq" | sha256sum -c -; \
|
||||
mv /tmp/yq /usr/local/bin/yq; \
|
||||
chmod +x /usr/local/bin/yq; \
|
||||
rm /tmp/yq-checksums /tmp/yq-hashes-order
|
||||
|
||||
# Append Go's toolchain bin and cargo's bin to whatever PATH the base image
|
||||
# set. Appending rather than hardcoding the full list keeps the agent CLI
|
||||
# dir (/data/.npm-global/bin) first - as the base image already arranges -
|
||||
# and stays correct if the base image's PATH ever changes.
|
||||
#
|
||||
# Note: $GOPATH/bin (defaults to ~/go/bin, i.e. /data/home/go/bin in this
|
||||
# image) is NOT added here - it's user-specific and lives on the PV. Users
|
||||
# who `go install` tools and want them on PATH should add it to their shell
|
||||
# profile (e.g. `export PATH=$PATH:$(go env GOPATH)/bin`).
|
||||
ENV PATH=$PATH:/usr/local/go/bin:/usr/local/cargo/bin
|
||||
|
||||
# Drop back to the unprivileged user. Matches the vanilla image's runtime
|
||||
# user; the docker-entrypoint.sh in the base image handles root → kandev
|
||||
# privilege drop via gosu for users who run the container as root.
|
||||
USER kandev
|
||||
Reference in New Issue
Block a user