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

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

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

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

# 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

# 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 \
        tar \
        openssh-client && \
    rm -rf /var/lib/apt/lists/*

# --- JDK / Node / Go / Rust, copied from the source stages ---------------------
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
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

# /usr/local/bin is already on Debian's default PATH, so node/npm/npx need no
# extra PATH entry. Everything else here (JDK, pnpm, Go, Rust) lives under a
# non-standard prefix and needs one.
ENV JAVA_HOME=/opt/java/openjdk
ENV PNPM_HOME=/opt/pnpm
# 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

# 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"

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

# 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}"

# 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

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

WORKDIR /workspace
CMD ["opencode"]
