This repository has been archived on 2026-10-10. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
opencode-sandbox/Containerfile

287 lines
16 KiB
Text
Raw Normal View History

2026-09-28 09:59:14 +02:00
# OpenCode dev sandbox: rootless-Podman container with JDK 25, Node 24, Go,
# Rust, pnpm, git, curl, and a hash-pinned build of OpenCode
# (github.com/anomalyco/opencode).
2026-09-27 10:59:40 +02:00
#
2026-09-28 09:59:14 +02:00
# Built as multi-stage: JDK, Node, Go, and Rust toolchains are pulled from
# their official upstream images and copied (not apt-installed) into a slim
# Debian final stage, to keep the image reasonably small while staying on a
# mainstream glibc distro.
#
# LSP support for OpenCode's built-in LSP client (off by default -- needs
# `"lsp": true` in opencode.json to actually turn on):
# - Go -> gopls, built from source against the pinned Go
# toolchain below.
# - Rust -> rust-analyzer, installed as a rustup
# component (same trust chain as the rest of the
# copied Rust toolchain). OpenCode has no
# auto-download fallback for this one, unlike
# gopls, so it must already be on PATH.
# - TypeScript/JavaScript -> typescript + typescript-language-server,
# installed as global npm packages; NODE_PATH
# below makes the global `typescript` package
# resolvable from any mounted project dir, even
# ones with no local node_modules.
# - Java -> jdtls, pre-baked (OpenCode has NO auto-download
# fallback for Java at all -- it must already be
# on PATH). No bin/jdtls ships upstream, so
# scripts/jdtls is a hand-written launcher
# wrapper, the same convention every other jdtls
# consumer (nvim-jdtls, mason.nvim, coc-java) uses.
# - Kotlin -> kotlin-lsp (JetBrains' official server),
# pre-baked. Ships its own bundled JetBrains
# Runtime, so it needs none of this image's JDK.
2026-09-27 10:59:40 +02:00
# ---- Stage: JDK source ------------------------------------------------------
# JDK 24 is EOL (Adoptium stopped building it Sept 2025; no 24-jdk* images
# exist). JDK 25 is the current LTS. Built on Ubuntu Noble (glibc 2.39), which
# is older than Debian Trixie's glibc (2.40) below -- copying a binary linked
# against an older glibc into a newer-glibc environment is safe.
FROM eclipse-temurin:25-jdk-noble AS jdk-source
# ---- Stage: Node source ------------------------------------------------------
# Node 24 is the current Active LTS. Matched to the same Debian release as the
# final base so glibc versions are identical -- no compatibility risk at all.
FROM node:24-trixie-slim AS node-source
2026-09-28 09:59:14 +02:00
# ---- Stage: Go source --------------------------------------------------------
# Full toolchain, not just a standalone LSP binary: gopls shells out to `go`
# itself and needs the standard library's source/type info to be useful --
# the same reason the JDK above is copied in whole rather than just a jar.
# Matched to Debian Trixie for the same glibc-alignment reason as node-source.
FROM golang:1.27.1-trixie AS go-source
# ---- Stage: Rust source -------------------------------------------------------
# Full toolchain, for the same reason as Go above -- rust-analyzer needs a
# real sysroot (std source + metadata) to do anything beyond bare syntax.
FROM rust:1.98.1-trixie AS rust-source
2026-09-27 10:59:40 +02:00
# ---- Final stage --------------------------------------------------------------
FROM debian:trixie-slim AS final
# Re-declared here: build ARGs don't cross stage boundaries implicitly.
# Auto-populated by podman/buildah from the build host's native platform on a
# plain `podman build` (no --platform needed) for same-arch builds, which
# covers building on either Apple Silicon or Intel Macs. Cross-arch builds
# would need an explicit --platform flag (out of scope for this project).
ARG TARGETARCH
# OpenCode version + per-arch SHA256 pins. Re-verified by hand against the
# actual release assets (curl + sha256sum), not just copied from a summary:
# curl -fsSL -o oc.tar.gz https://github.com/anomalyco/opencode/releases/download/v<ver>/opencode-linux-<x64|arm64>.tar.gz
# sha256sum oc.tar.gz
# Re-run this whenever OPENCODE_VERSION is bumped, and update the two hashes.
ARG OPENCODE_VERSION=1.18.32
ARG OPENCODE_SHA256_AMD64=3046e0404fdc60fb80307e7a47824ba07477364178a4d09baa8548496dd6d43b
ARG OPENCODE_SHA256_ARM64=568461b7d4d8c19865c97e9a1102e613049c6039d01fe772154de873c1865840
# pnpm version, installed via pnpm's own standalone script (not Corepack --
# the pnpm team doesn't recommend Corepack, and Node 25+ dropped bundling it).
ARG PNPM_VERSION=12.6.0
2026-09-28 09:59:14 +02:00
# gopls version, built from source at image-build time against the pinned Go
# toolchain (FROM golang:... above) via `go install`. Go's module system
# verifies this against the public checksum database (sum.golang.org)
# automatically, so no separate hash-pinning step is needed here.
ARG GOPLS_VERSION=0.23.0
# typescript + typescript-language-server versions, installed as global npm
# packages. Pinned to typescript's LAST JavaScript-based release (6.0.x) on
# purpose, NOT "latest" (7.x): TypeScript 7 is a native Go rewrite that no
# longer ships tsserver.js, which both typescript-language-server and
# OpenCode's own LSP client (which resolves "typescript/lib/tsserver.js"
# directly) require. Re-evaluate this pin if typescript-language-server ever
# ships a tsgo-native mode.
ARG TYPESCRIPT_VERSION=6.0.3
ARG TS_LS_VERSION=6.0.1
# jdtls: NOT distributed via GitHub Releases -- real distribution is
# Eclipse's own download server. Architecture-independent (pure JVM). Hash
# hand-verified against Eclipse's own published .sha256 file at
# https://download.eclipse.org/jdtls/milestones/<version>/<asset>.sha256 --
# re-run that check whenever JDTLS_VERSION/JDTLS_BUILD are bumped.
ARG JDTLS_VERSION=1.61.0
ARG JDTLS_BUILD=202609031315
ARG JDTLS_SHA256=338e7e73d61836651ba2453919a0d34fa763eb4e7c03342092309bffb8934c64
# kotlin-lsp: JetBrains' official server (github.com/Kotlin/kotlin-lsp), not
# the stale, long-unmaintained fwcd/kotlin-language-server. Per-arch native
# binary; JetBrains publishes no checksum, so these are hand-computed from
# the downloaded assets themselves, same as OPENCODE_SHA256_* above.
ARG KOTLIN_LSP_VERSION=263.4702.0
ARG KOTLIN_LSP_SHA256_AMD64=1e11d2e5fefbf9ea215ad8dd6be95f2222897cd086e8cb7a661a52084a590405
ARG KOTLIN_LSP_SHA256_ARM64=ec7cb254a6662a07fff9f10e4365226afab6c40008f8a974c10ac5e785d6510f
2026-09-27 10:59:40 +02:00
# ca-certificates : TLS roots for curl/git/npm/pnpm/JVM.
# fontconfig,
# libfreetype6 : Temurin's font subsystem needs these even headless.
# libstdc++6 : native runtime needed by libjvm.so and native Node addons.
# git, curl : required tools.
# tar : needed to extract the OpenCode release archive; not
# guaranteed present on a trimmed slim base.
# openssh-client : without it, `git clone git@...` silently fails.
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates \
fontconfig \
libfreetype6 \
libstdc++6 \
git \
curl \
make \
python3 \
2026-09-27 10:59:40 +02:00
tar \
openssh-client && \
rm -rf /var/lib/apt/lists/*
2026-09-28 09:59:14 +02:00
# --- JDK / Node / Go / Rust, copied from the source stages ---------------------
2026-09-27 10:59:40 +02:00
COPY --from=jdk-source /opt/java/openjdk /opt/java/openjdk
COPY --from=node-source /usr/local/bin /usr/local/bin
COPY --from=node-source /usr/local/lib/node_modules /usr/local/lib/node_modules
2026-09-28 09:59:14 +02:00
COPY --from=go-source /usr/local/go /usr/local/go
COPY --from=rust-source /usr/local/cargo /usr/local/cargo
COPY --from=rust-source /usr/local/rustup /usr/local/rustup
2026-09-27 10:59:40 +02:00
# /usr/local/bin is already on Debian's default PATH, so node/npm/npx need no
2026-09-28 09:59:14 +02:00
# extra PATH entry. Everything else here (JDK, pnpm, Go, Rust) lives under a
# non-standard prefix and needs one.
2026-09-27 10:59:40 +02:00
ENV JAVA_HOME=/opt/java/openjdk
ENV PNPM_HOME=/opt/pnpm
2026-09-28 09:59:14 +02:00
# GOPATH: writable install/cache dir for `go install` -- both the build-time
# gopls install below and anything `go`/gopls itself does at runtime (e.g.
# module downloads while analyzing a mounted project). Made world-writable
# further down, for the same --userns=keep-id reason /home/dev is.
ENV GOPATH=/go
ENV GOROOT=/usr/local/go
# CARGO_HOME/RUSTUP_HOME: matches the layout already baked into the
# rust-source image, so the copied toolchain and rustup metadata (including
# the recorded default toolchain) keep working unmodified.
ENV CARGO_HOME=/usr/local/cargo
ENV RUSTUP_HOME=/usr/local/rustup
ENV PATH="${JAVA_HOME}/bin:${PNPM_HOME}/bin:${GOROOT}/bin:${GOPATH}/bin:${CARGO_HOME}/bin:${PATH}"
# Makes the global `typescript` package (installed below) resolvable via
# Node's module resolution from *any* project directory OpenCode mounts at
# /workspace, even ones with no local node_modules of their own -- needed
# because OpenCode's TypeScript LSP support resolves
# "typescript/lib/tsserver.js" relative to the project directory, and
# NODE_PATH is the fallback once that climb reaches the filesystem root.
ENV NODE_PATH=/usr/local/lib/node_modules
2026-09-27 10:59:40 +02:00
2026-09-28 11:56:17 +02:00
# GRADLE_OPTS: bounds the Gradle daemon's own heap. Gradle's default heap
# auto-sizing goes off whatever memory it sees as "available" -- with no
# cap it sizes against the Makefile's --memory container ceiling in full,
# leaving nothing for the separate Kotlin-compiler daemon Gradle forks
# alongside it (see the RUN_FLAGS comment in the Makefile for the full
# story: an uncapped build here doesn't just OOM the container, it can
# starve the whole podman-machine VM). This only bounds the Gradle daemon
# itself -- a project needing the Kotlin daemon capped too should set
# `kotlin.daemon.jvmargs` in its own gradle.properties.
ENV GRADLE_OPTS="-Xmx1024m -XX:MaxMetaspaceSize=256m"
2026-09-27 10:59:40 +02:00
# Fixed HOME: the container is run with --userns=keep-id, so the runtime UID
# is whatever host user invoked `podman run`, which has no /etc/passwd entry
# inside the image. A plain ENV HOME is unaffected by that UID mapping, so we
# pin it here and make it world-writable (sticky bit, like /tmp) so any mapped
# UID can use it.
ENV HOME=/home/dev
# Belt-and-suspenders on top of OpenCode's default (~/.config/opencode):
# OPENCODE_CONFIG_DIR is OpenCode's one confirmed config-location override.
ENV OPENCODE_CONFIG_DIR=/home/dev/.config/opencode
RUN mkdir -p /home/dev && chmod 1777 /home/dev
2026-09-28 09:59:14 +02:00
# GOPATH needs to be writable by any --userns=keep-id-mapped UID too, for the
# same reason /home/dev is (see above).
RUN mkdir -p /go && chmod 1777 /go
2026-09-27 10:59:40 +02:00
# pnpm: standalone installer, pinned version. SHELL must be set explicitly --
# the installer can't infer a shell type inside a minimal `RUN` environment
# with no interactive shell, and errors out (ERR_PNPM_UNKNOWN_SHELL) otherwise.
RUN curl -fsSL https://get.pnpm.io/install.sh | env PNPM_VERSION="${PNPM_VERSION}" SHELL=/bin/bash sh - && \
chmod -R a+rX "${PNPM_HOME}"
2026-09-28 09:59:14 +02:00
# rust-analyzer: installed as a rustup component, same trust chain (signed,
# checksum-verified by rustup itself) as the rest of the copied Rust
# toolchain. Unlike gopls below, OpenCode has no auto-download fallback for
# rust-analyzer -- it must already be resolvable on PATH.
RUN rustup component add rust-analyzer
# gopls: built from source via `go install`, pinned to GOPLS_VERSION (see the
# ARG comment above for why this needs no separate hash-pinning step).
RUN go install golang.org/x/tools/gopls@v${GOPLS_VERSION} && \
chmod -R a+rwX "${GOPATH}"
# typescript + typescript-language-server: global npm install, pinned (see
# the ARG comment above for why TYPESCRIPT_VERSION deliberately isn't "latest").
RUN npm install -g \
"typescript@${TYPESCRIPT_VERSION}" \
"typescript-language-server@${TS_LS_VERSION}"
# jdtls: architecture-independent (pure JVM) tarball, fetched directly from
# Eclipse's own download server (not GitHub Releases). No bin/jdtls launcher
# ships in it -- scripts/jdtls (copied in below) is the hand-written wrapper.
RUN set -eu; \
curl -fsSL -o /tmp/jdtls.tar.gz \
"https://download.eclipse.org/jdtls/milestones/${JDTLS_VERSION}/jdt-language-server-${JDTLS_VERSION}-${JDTLS_BUILD}.tar.gz"; \
printf '%s /tmp/jdtls.tar.gz\n' "$JDTLS_SHA256" | sha256sum -c -; \
mkdir -p /opt/jdtls; \
tar -xzf /tmp/jdtls.tar.gz -C /opt/jdtls; \
rm -f /tmp/jdtls.tar.gz
COPY scripts/jdtls /usr/local/bin/jdtls
RUN chmod +x /usr/local/bin/jdtls
# kotlin-lsp: arch-aware fetch (same pattern as the OpenCode section below),
# SHA256 verified by hand (see the ARG comment above). The tarball extracts
# to a versioned top-level dir, flattened here via --strip-components=1.
# bin/intellij-server is the real binary -- kotlin-lsp.sh (not copied) is
# just a deprecated wrapper that execs it with no extra flags, confirmed by
# reading that script directly. Symlinked under both plausible command names
# since it's unconfirmed which one OpenCode's Kotlin LSP integration expects.
RUN set -eu; \
case "$TARGETARCH" in \
amd64) KLS_ASSET="kotlin-server-${KOTLIN_LSP_VERSION}.tar.gz"; KLS_SHA256="$KOTLIN_LSP_SHA256_AMD64" ;; \
arm64) KLS_ASSET="kotlin-server-${KOTLIN_LSP_VERSION}-aarch64.tar.gz"; KLS_SHA256="$KOTLIN_LSP_SHA256_ARM64" ;; \
*) echo "ERROR: unsupported TARGETARCH='$TARGETARCH'" >&2; exit 1 ;; \
esac; \
curl -fsSL -o /tmp/kotlin-lsp.tar.gz \
"https://download.jetbrains.com/language-server/kotlin-server/${KOTLIN_LSP_VERSION}/${KLS_ASSET}"; \
printf '%s /tmp/kotlin-lsp.tar.gz\n' "$KLS_SHA256" | sha256sum -c -; \
mkdir -p /opt/kotlin-lsp; \
tar -xzf /tmp/kotlin-lsp.tar.gz -C /opt/kotlin-lsp --strip-components=1; \
rm -f /tmp/kotlin-lsp.tar.gz; \
ln -s /opt/kotlin-lsp/bin/intellij-server /usr/local/bin/kotlin-lsp; \
ln -s /opt/kotlin-lsp/bin/intellij-server /usr/local/bin/kotlin-language-server
2026-09-27 10:59:40 +02:00
# OpenCode: arch-aware fetch, SHA256 verification, install.
# Asset naming uses "x64" for the amd64 arch, not "amd64".
# Confirmed layout: both tarballs contain the binary flat at top level as
# "opencode" (no subdirectory nesting).
RUN set -eu; \
case "$TARGETARCH" in \
amd64) OC_ARCH=x64; OC_SHA256="$OPENCODE_SHA256_AMD64" ;; \
arm64) OC_ARCH=arm64; OC_SHA256="$OPENCODE_SHA256_ARM64" ;; \
*) echo "ERROR: unsupported TARGETARCH='$TARGETARCH'" >&2; exit 1 ;; \
esac; \
curl -fsSL -o /tmp/opencode.tar.gz \
"https://github.com/anomalyco/opencode/releases/download/v${OPENCODE_VERSION}/opencode-linux-${OC_ARCH}.tar.gz"; \
printf '%s /tmp/opencode.tar.gz\n' "$OC_SHA256" | sha256sum -c -; \
mkdir -p /tmp/opencode-extract; \
tar -xzf /tmp/opencode.tar.gz -C /tmp/opencode-extract; \
install -m 0755 /tmp/opencode-extract/opencode /usr/local/bin/opencode; \
rm -rf /tmp/opencode.tar.gz /tmp/opencode-extract
# Build-time smoke tests: fail here, not at first `make opencode`.
RUN { ldd /usr/local/bin/opencode || true; } | tee /tmp/opencode-ldd.txt && \
if grep -qi "not found" /tmp/opencode-ldd.txt; then \
echo "ERROR: opencode binary has unresolved shared library dependencies" >&2; \
exit 1; \
fi && \
rm -f /tmp/opencode-ldd.txt
2026-09-28 09:59:14 +02:00
# jdtls/kotlin-lsp aren't invoked here (both block waiting on stdio LSP
# input with no --version-style flag) -- just checked for presence instead.
RUN java -version && node --version && pnpm --version && git --version && curl --version && \
go version && rustc --version && rust-analyzer --version && gopls version && \
tsc --version && typescript-language-server --version && \
test -x /usr/local/bin/jdtls && \
ls /opt/jdtls/plugins/org.eclipse.equinox.launcher_*.jar >/dev/null && \
test -x /usr/local/bin/kotlin-lsp && test -x /opt/kotlin-lsp/bin/intellij-server
2026-09-27 10:59:40 +02:00
WORKDIR /workspace
CMD ["opencode"]