-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDockerfile.universal
More file actions
214 lines (203 loc) · 11 KB
/
Copy pathDockerfile.universal
File metadata and controls
214 lines (203 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# syntax=docker/dockerfile:1.7
###############################################################################
# Dockerfile.universal — optional fourth layer of the NeoLabHQ sandbox chain.
#
# Chain: base -> agents -> final -> universal.
# - Dockerfile.base -> neolabhq/sandbox:base
# - Dockerfile.agents -> neolabhq/sandbox:agents
# - Dockerfile -> neolabhq/sandbox:latest
# - Dockerfile.universal -> neolabhq/sandbox:universal (this file)
#
# Purpose: extends :latest with the broader language stack that the legacy
# devcontainers/universal image shipped, for downstream consumers who need a
# drop-in universal replacement.
#
# Languages added on top of :latest (do NOT re-install C++ — it is already
# inherited from Dockerfile.base via build-essential):
# - Java — via mise (first-class temurin plugin; hedged `@temurin-25`
# selector resolves to the current Eclipse Temurin 25 at build
# time per the version-claims rule)
# - Rust — via mise (first-class plugin; `cargo` lands via mise shims)
# - Zig — via mise (first-class plugin)
# - .NET — via mise (first-class plugin; wraps Microsoft's official
# `dotnet-install.sh`, which downloads prebuilt arch-specific
# binaries and works on both linux/amd64 and linux/arm64)
# - PHP — via apt (php-cli + project-relevant extensions); mise's PHP
# plugin requires building from source which is slow and heavy
# - Composer — via the official PHP installer (piped through php)
#
# What is NOT duplicated (intentionally inherited from earlier layers):
# - C++ toolchain (Dockerfile.base → build-essential)
# - Node, Python, Go (Dockerfile.base → mise use --global)
# - mise, nix, devbox, Homebrew, gh CLI (Dockerfile.base)
# - AI agents, LSPs, codemap, docker-mcp (Dockerfile.agents)
# - entrypoint, configure-claude, install-mcps (Dockerfile / :latest)
# - ENTRYPOINT, CMD, WORKDIR (inherited from :latest, not overridden)
#
# Authoritative spec (rationale, language-manager split, hedging policy):
# .specs/tasks/draft/switch-base-image.md
# Step 4: Create Dockerfile.universal
#
# Per /workspaces/sandbox/.claude/rules/dockerfile-curl-pipe-pipefail.md the
# SHELL directive below switches every subsequent RUN to bash with pipefail
# so any `curl ... | php` or `curl ... | bash` pipeline aborts the layer on
# failure instead of silently producing an empty install.
###############################################################################
ARG FINAL_IMAGE=neolabhq/sandbox:latest
FROM ${FINAL_IMAGE}
###############################################################################
# Re-declare bash+pipefail SHELL.
#
# Per /workspaces/sandbox/.claude/rules/dockerfile-curl-pipe-pipefail.md
# (hadolint DL4006), every Dockerfile that contains a pipe-fed installer MUST
# switch the RUN shell to bash with `-o pipefail` BEFORE any such line. The
# SHELL directive does NOT carry across `FROM`, so it must be re-declared here
# even though every prior stage in the chain already set it. The default
# `/bin/sh` is dash on Debian, which does not support pipefail and silently
# produces a successful-but-empty layer when curl fails mid-stream.
###############################################################################
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
###############################################################################
# OCI image annotations.
###############################################################################
LABEL org.opencontainers.image.source="https://github.com/NeoLabHQ/sandbox"
LABEL org.opencontainers.image.description="NeoLabHQ sandbox universal: drop-in replacement for devcontainers/universal — adds Java, Rust, Zig, .NET SDK (via mise), PHP + Composer (via apt/official installer), and the jdtls Java language server (co-located with the Temurin JVM) on top of :latest"
LABEL org.opencontainers.image.licenses="MIT"
###############################################################################
# Switch to root for apt installs and Composer. (.NET is installed later via
# mise as the vscode user — no root step required for it.)
###############################################################################
USER root
###############################################################################
# 1. PHP from apt (Debian trixie's php-cli is current stable; verify the
# exact PHP version that ships in trixie at build time via
# `apt-cache show php-cli` in a transient base:trixie container).
#
# Extensions chosen for parity with the devcontainers/universal set:
# php-cli — CLI runtime (provides the `php` binary)
# php-common — shared files required by most extensions
# php-mbstring — multibyte string support (required by many frameworks)
# php-xml — XML/DOM/SimpleXML support
# php-curl — cURL HTTP client (required by Composer itself)
# php-zip — ZIP archive support (required by Composer for package install)
###############################################################################
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
php-cli \
php-common \
php-mbstring \
php-xml \
php-curl \
php-zip \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
###############################################################################
# 2. Composer — official PHP installer.
#
# The installer is piped through `php` (not `bash`) which is valid with the
# bash+pipefail SHELL declared above (curl failure still aborts the layer).
# The installer script performs its own integrity check (hash comparison)
# before writing the phar, so no explicit checksum step is required.
#
# --install-dir=/usr/local/bin — system-wide location; on PATH for every user
# --filename=composer — invoced as `composer` (not composer.phar)
#
# Verify the installer URL is still canonical at build time:
# https://getcomposer.org/doc/faqs/how-to-install-composer-programmatically.md
###############################################################################
RUN curl -fsSL https://getcomposer.org/installer \
| php -- --install-dir=/usr/local/bin --filename=composer
###############################################################################
# 3. Java, Rust, Zig, and .NET via mise (first-class plugins).
#
# These are the remaining languages devcontainers/universal shipped that
# :latest does not include. mise resolves each `@latest` specifier at build
# time, so security and patch updates flow in automatically. The exact resolved
# versions are recorded by the Step 7 verification commands (`java --version`,
# `rustc --version`, `zig version`, `dotnet --version`) and surfaced in the
# CI build summary — they are NOT pinned in this Dockerfile per the
# version-claims rule.
#
# Why mise for .NET (and not Microsoft's Debian apt repo): Microsoft's
# packages.microsoft.com Debian 13 suite has sparse arm64 coverage — `apt-get
# install dotnet-sdk-*` on linux/arm64 selects the amd64 variant and fails on
# unsatisfiable `libc6:amd64` deps, breaking the multi-arch build. mise's
# dotnet plugin wraps Microsoft's official `dotnet-install.sh`, which
# downloads arch-specific prebuilt binaries and works on both linux/amd64
# and linux/arm64. (Unlike the PHP plugin, the dotnet plugin does NOT build
# from source — it installs Microsoft-published binaries directly.)
# Plugin docs: https://mise.jdx.dev/lang/dotnet.html
#
# Run as the vscode user — mise's data dir (/usr/local/share/mise) is owned by
# vscode (set by Dockerfile.base's chown), so running mise as root would
# trigger mise's "wrong owner" refusal. Switching back to vscode here keeps
# consistent ownership and matches the pattern in Dockerfile.base.
#
# `mise reshim` makes `java`, `dotnet` (and `rustc`/`cargo`/`zig`) discoverable
# on PATH for the vscode user via the system-wide shims dir at
# /usr/local/share/mise/shims, which Dockerfile.base already prepends to PATH
# via both ENV and /etc/profile.d/mise.sh.
###############################################################################
USER vscode
RUN mise use --global java@temurin-25 rust@latest zig@latest dotnet@latest \
&& mise install \
&& mise reshim \
&& java --version \
&& dotnet --version
###############################################################################
# jdtls — Eclipse JDT language server for Java.
#
# Installed in :universal (not :agents) because jdtls needs a Java runtime to
# launch, and the JVM lives in this layer (mise-managed Temurin LTS via the
# `java` shim at /usr/local/share/mise/shims/java). Installing jdtls in any
# earlier layer would produce a binary that fails at startup with "java: not
# found" because :base/:agents/:latest do not ship a JVM.
#
# Strategy:
# 1. Discover the latest milestone version by scraping the milestones
# directory listing (https://download.eclipse.org/jdtls/milestones/).
# Directory names are pure semver (e.g. `1.58.0`); sort -V picks the
# newest.
# 2. Discover the actual tarball name inside that version directory
# (`jdt-language-server-<version>-<timestamp>.tar.gz`) the same way.
# 3. Download + extract into /opt/jdtls.
# 4. Symlink the launcher (`bin/jdtls` inside the tarball) onto
# /usr/local/bin/jdtls so it is on PATH for every user.
#
# We use the milestones channel (not snapshots) per the task spec — milestones
# are intentional releases; snapshots are unstable CI artifacts.
#
# Version is NOT pinned in the Dockerfile per the version-claims rule; the
# resolved version is logged at build time and recorded in the CI build
# summary.
###############################################################################
USER root
RUN set -eux; \
base_url="https://download.eclipse.org/jdtls/milestones"; \
version="$( \
curl -fsSL "${base_url}/?d" \
| grep -oE "href='/jdtls/milestones/[0-9]+\.[0-9]+(\.[0-9]+)?'" \
| grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' \
| sort -V \
| tail -n 1 \
)"; \
[ -n "${version}" ] || { echo "Failed to resolve latest jdtls milestone version"; exit 1; }; \
tarball="$( \
curl -fsSL "${base_url}/${version}/?d" \
| grep -oE "jdt-language-server-${version}-[0-9]+\.tar\.gz" \
| sort -u \
| tail -n 1 \
)"; \
[ -n "${tarball}" ] || { echo "Failed to resolve jdtls tarball for ${version}"; exit 1; }; \
echo "Installing jdtls ${version} (${tarball})"; \
mkdir -p /opt/jdtls; \
curl -fsSL "${base_url}/${version}/${tarball}" \
| tar -xz -C /opt/jdtls; \
ln -sf /opt/jdtls/bin/jdtls /usr/local/bin/jdtls; \
chmod +x /opt/jdtls/bin/jdtls
###############################################################################
# Final user — remain as vscode (non-root) for runtime safety.
# The inherited ENTRYPOINT, CMD, and WORKDIR from :latest are preserved
# unchanged (no override necessary or intended).
###############################################################################
USER vscode