-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDockerfile.nanoserver
More file actions
441 lines (406 loc) · 25.8 KB
/
Copy pathDockerfile.nanoserver
File metadata and controls
441 lines (406 loc) · 25.8 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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
# escape=`
# The `# escape=`` parser directive above (which must be line 1) is documented here.
# It sets the line-continuation / escape character to a backtick instead of the default
# `\`, so backslashes in Windows paths (C:\..., trailing `C:\`) stay literal and lines
# continue with a backtick.
# Suppress two false ShellCheck errors hadolint raises on this file's Windows cmd RUNs.
# hadolint runs ShellCheck (POSIX sh) over every RUN, but these are Windows cmd,
# so it false-errors on cmd `if` (SC1072) and on `\x` in paths like C:\gh_token
# (SC1001). hadolint/hadolint#645. Suppress just those two for this file (a file-
# scoped pragma, so the Linux Dockerfiles keep full ShellCheck coverage).
# hadolint global ignore=SC1072,SC1001
# Staged to mirror the Linux Dockerfile so the layers cache and target the same way.
# The stage order is: vcruntime -> build-minimal -> build -> prefetch -> precompile ->
# test / server. Each stage `FROM`s the previous, so installed tools, fetched deps and
# built module pkg/ carry forward; `--target <stage>` stops early. The
# Windows-only bootstrap (VC++ runtime, mise.exe, HOME/TEMP, the gnu-host rust
# flip) lives in build-minimal. Where Linux stops at a clean build/test/serve,
# Windows is still proving the toolchain installs at all -- the stages past
# `build` are the CI frontier (see "Known unknowns" at the foot).
#
# Base: Nano Server (~120 MB vs Server Core's ~1.25 GB), kept on Nano per the
# CLAUDE.md non-negotiable. Same MSI/PowerShell-installer-avoidance posture as
# any minimal Windows image -- nothing is installed the Windows way; mise (a
# single .exe the workflow stages in the build context) is copied in, and
# `mise install` pulls the rest.
#
# gpg workaround (Nano-only). The workstation Windows path uses the msys2
# `conda:m2-gnupg` from config.windows.toml -- that loads fine on Windows
# 10/11/Server because their kernel32 has the symbols msys-2.0.dll's lazy
# autoload calls. On Nano Server it doesn't: msys-2.0.dll resolves additional
# kernel32 symbols at first-use via GetProcAddress (`.kernel32_autoload_text`
# section), and when one (e.g. kernel32!IdnToAscii, hit by gpg's UID/email
# IDN normalization) is absent from Nano's stripped kernel32, the autoload
# deliberately exits the process with STATUS_ENTRYPOINT_NOT_FOUND
# (0xC0000139) -- the same code a static load failure produces, which is
# what the CI log surfaced as `gpg exited with non-zero status: exit code
# -1073741511`. Same autoload trap lurks behind every other conda:m2-*
# package (m2-git, m2-make), which happen not to hit IDN today.
#
# Nano carve-out: MISE_DISABLE_TOOLS skips `conda:m2-gnupg` here, and the
# `gpgbin` donor stage below stages the GnuPG simple Windows installer
# (gnupg-w32-*.exe -- a clean MinGW-w64 native gpg.exe with no msys runtime,
# no msys-2.0.dll, no IDN code path) at C:\gnupg\bin, which the build-minimal
# PATH below prepends so `mise install node`'s gpg lookup finds it. NSIS
# installer extraction needs an installer stack Nano lacks, so the donor
# stage runs on Server Core like the VC++ runtime donor.
# vcruntime: donor stage for the VC++ runtime DLLs.
# Nano Server omits the VC++ runtime that msvc-built executables (mise.exe, and
# the rust/cargo mise installs) need just to *start* -- without it mise.exe dies
# with 0xC0000135 (DLL not found). No base Windows image ships it (not even
# Server Core), and mise can't install it (mise needs it to run), so we install
# the official VC++ redistributable on a Server Core donor -- which has the
# installer stack Nano Server lacks -- and copy the runtime DLLs forward. This
# plus mise are the only non-mise bootstrap bits; the rest is mise.
# Windows base version. Both FROMs below must use the same value so the VC++
# runtime DLLs copied from servercore match nanoserver's C runtime. Override
# with `--build-arg WINDOWS_VERSION=ltsc2025` to build against newer Nano
# Server (runner host must match -- ltsc2022 pairs with windows-2022, ltsc2025
# with windows-2025; mismatched versions need Hyper-V isolation).
ARG WINDOWS_VERSION=ltsc2022
FROM mcr.microsoft.com/windows/servercore:${WINDOWS_VERSION} AS vcruntime
SHELL ["cmd", "/S", "/C"]
# `|| ver>nul` swallows the redistributable's 3010 ("reboot required") exit code.
RUN curl -fsSL -o vc_redist.exe https://aka.ms/vs/17/release/vc_redist.x64.exe && `
(vc_redist.exe /install /quiet /norestart || ver>nul) && `
del vc_redist.exe
# gpgbin: donor stage for the GnuPG simple-installer payload.
# GnuPG's official simple Windows installer is an NSIS .exe that installs a
# MinGW-w64 native gpg.exe (no msys runtime); Nano Server can't run NSIS but
# Server Core can, so we install silently here (`/S` NSIS silent mode, `/D=`
# target dir -- must be the last arg, no quotes) and COPY the result to the
# Nano stage below. SHA256 pin: an upstream republish under the same URL
# fails the build instead of silently changing the gpg we ship.
FROM mcr.microsoft.com/windows/servercore:${WINDOWS_VERSION} AS gpgbin
SHELL ["cmd", "/S", "/C"]
RUN curl -fsSL -o gnupg.exe https://gnupg.org/ftp/gcrypt/binary/gnupg-w32-2.5.20_20260513.exe && `
certutil -hashfile gnupg.exe SHA256 | findstr /i `
"ca26cd20602581b2ce05e95b16f6249f6fb6c4dcf32304165fc90519328d7981" >nul && `
start /wait "" gnupg.exe /S /D=C:\gnupg && `
del gnupg.exe && `
if not exist C:\gnupg\bin\gpg.exe exit /b 1
# build-minimal: mise + the always-loaded toolchain (config.toml only).
# Mirrors the Linux build-minimal: copies just .mise/config.toml + installs the
# default tools, so this layer is reused until the always-loaded toolset changes,
# not when a guest config or the source does. All the Windows-only bootstrap
# (VC++ DLLs, mise.exe, HOME, TEMP) and the gnu-host rust flip live here too,
# since every later stage builds on it.
FROM mcr.microsoft.com/windows/nanoserver:${WINDOWS_VERSION} AS build-minimal
COPY --from=vcruntime C:\Windows\System32\vcruntime140.dll C:\Windows\System32\vcruntime140.dll
COPY --from=vcruntime C:\Windows\System32\vcruntime140_1.dll C:\Windows\System32\vcruntime140_1.dll
COPY --from=vcruntime C:\Windows\System32\msvcp140.dll C:\Windows\System32\msvcp140.dll
# Bring in the GnuPG simple-installer payload (see gpgbin stage above).
# mise's node-tarball gpg-verify path runs `gpg --import` + `gpg --verify`, so gpg.exe
# must be on PATH before the first `mise install node` below; C:\gnupg\bin is
# added to PATH in the ENV PATH= line later in this stage.
COPY --from=gpgbin C:\gnupg C:\gnupg
# Run as cmd (the only shell Nano Server has) and as ContainerAdministrator.
# ContainerAdministrator is so we can write under C:\ (the backtick line continuations
# below rely on the escape directive documented at the top of this file).
USER ContainerAdministrator
SHELL ["cmd", "/S", "/C"]
# Diagnostic (informational -- never fails the build): detect whether this base image ships a bash.
# The `bash -euo pipefail` mise tasks need one, and Nano Server doesn't
# bundle it. Record which it is in the build log so the "is bash present" unknown
# is answered up front.
RUN where bash >nul 2>&1 && (echo [bash-check] pre-installed: & where bash) || `
echo [bash-check] bash is not pre-installed on this base image
# Set HOME to the admin profile so the container has a sane home, which Nano Server leaves unset.
# This also renders the config's `{{ env.HOME }}` paths; note
# mise still resolves its *global config* to C:\.config\mise on Windows
# regardless of HOME (trusted explicitly below).
ENV HOME=C:\Users\ContainerAdministrator
# Sanity-check that HOME is the running user's real profile.
# It must exist and end with the current %USERNAME%. USERPROFILE is echoed for diagnostics.
# Also sanity-check %APPDATA% (gnupg 2.5's homedir is %APPDATA%\gnupg; Nano may
# not pre-create %USERPROFILE%\AppData\Roaming for ContainerAdministrator, in
# which case gpg's first homedir-create later fails -- catch it here, where the
# fix is a one-line `mkdir` rather than at the mise-runs-gpg invocation site).
RUN echo HOME=[%HOME%] USERPROFILE=[%USERPROFILE%] USERNAME=[%USERNAME%] APPDATA=[%APPDATA%] & `
if not exist "%HOME%\" exit /b 1 & `
echo %HOME%| findstr /i /e /c:"%USERNAME%" >nul & `
if errorlevel 1 exit /b 1 & `
if "%APPDATA%"=="" (echo APPDATA is unset & exit /b 1) & `
if not exist "%APPDATA%\" (echo %APPDATA% dir does not exist & exit /b 1)
# Point TEMP/TMP at a writable dir on C: (same volume as mise's installs).
# Nano Server's default temp dir can be missing/unwritable, which fails rustup's
# "persist temporary file" step (Access denied) installing toolchains.
RUN if not exist C:\Temp mkdir C:\Temp
ENV TEMP=C:\Temp `
TMP=C:\Temp
# Set WASM_PACK_CACHE so wasm-pack's binary cache doesn't depend on a Nano-missing shell API.
# wasm-pack's binary cache (binary-install's Cache::new) resolves its dir via
# dirs_next, which on Windows calls the SHGetKnownFolderPath shell API that Nano
# lacks (the same gap that stops pipx) -- so it fails with "couldn't find your
# home directory, is $HOME not set?" (build-ws-wasm-agent's `-Z build-std` hits
# it first). WASM_PACK_CACHE makes wasm-pack use Cache::at(<path>) instead, which
# stores the path verbatim and never calls dirs (the dir is created on first
# use). It's a transient build cache, so it lives under the temp dir above.
ENV WASM_PACK_CACHE=C:\Temp\wasm-pack
# Serialize mise's installs (MISE_JOBS=1).
# On Nano Server the parallel cargo: source builds -- no gnu binstall prebuilts exist, so they fall back to
# `cargo install` -- race on rustup's shared .rustup\downloads dir: one build's component .partial vanishes
# before another rustup can rename it ("cannot find the file", os error 2). Windows CI doesn't hit this (msvc
# binstall prebuilts mean no source builds); the gnu Docker path does. One job at a time avoids it.
ENV MISE_JOBS=1
# Full Rust backtraces so any panic surfaces in the CI log.
ENV RUST_BACKTRACE=full
# Nano-only carve-out: disable mise's gpg-verify of tool tarballs.
# The cross-platform [settings] in .mise/config.toml has `gpg_verify = true`, and the
# gpgbin donor stage above does stage a working gpg.exe at C:\gnupg\bin (the
# gpg-check diagnostic earlier confirms it loads + lists keys) -- but mise's
# pipe-driven `gpg --import` / `gpg --verify` flow still fails on Nano, with
# the runner subprocess crashing as:
# mise ERROR gpg failed
# The application panicked (crashed).
# Message: called `Result::unwrap()` on an `Err` value:
# Os { code: 109, kind: BrokenPipe, message: "The pipe has been ended." }
# Location: src\cmd.rs:616
# Until that mise-on-Nano pipe behavior is fixed upstream, override via env
# (MISE_GPG_VERIFY takes precedence over the [settings] entry per mise's
# settings.toml). Keep the donor stage + PATH so re-enabling is a one-line
# revert once mise stops panicking.
ENV MISE_GPG_VERIFY=false
# Verbose mise is disabled because it floods the log with tool/task resolution and the exact bash it launches.
# Re-enable it (noisy but invaluable) when diagnosing a
# Windows build failure on the bare-Nano-Server path:
# ENV MISE_VERBOSE=1 `
# MISE_LOG_LEVEL=debug
# mise.zip is staged in the build context by the docker.yaml windows job and copied in here.
# That job (.github/workflows/docker.yaml) pins the version. We
# don't curl a versioned URL inside the build: the classic Windows builder
# doesn't substitute build-args into RUN, and mise's `mise-latest-windows-x64.zip`
# prebuilt is stale (2026.3.0, which predates the nested `task.run_auto_install`
# in .mise/config.toml and errors on it). Extract with Nano Server's tar.exe (zip
# is mise\bin\mise.exe) and put that on PATH so the steps below can call `mise`.
COPY mise.zip C:\mise.zip
RUN tar -xf C:\mise.zip -C C:\ && del C:\mise.zip
# Build PATH explicitly: mise's bin + shims, the re-listed System32/Windows dirs, and llvm-mingw's bin.
# Prepend mise's bin and its shims dir (mirrors the Linux Dockerfile's
# /root/.local/share/mise/shims), and re-list System32 + Windows explicitly:
# the base image exposes its search path as `Path`, which Docker's case-
# sensitive `${PATH}` doesn't match, so it expands empty -- dropping System32
# (findstr, net, sc, ...) off PATH for every step below. Also put llvm-mingw's
# bin on PATH so clang-sys can find its libclang.dll: config.windows.toml's
# LIBCLANG_PATH names a system LLVM dir absent on Nano Server, so clang-sys
# falls back to a PATH search and llvm-mingw's bin is where libclang.dll lives
# here. (The gnullvm linker and llvm-dlltool are absolute paths in
# config.windows.toml, so PATH isn't needed for them.) The version segment
# must match the github:mstorsjo/llvm-mingw pin in config.windows.toml; the
# dir doesn't exist until preinstall installs llvm-mingw, which is fine --
# the builds that need it run afterwards.
ENV MISESHIMS=C:\Users\ContainerAdministrator\AppData\Local\mise\shims
ENV LLVMBIN=C:\Users\ContainerAdministrator\AppData\Local\mise\installs\github-mstorsjo-llvm-mingw\20260602\bin
ENV PATH="C:\mise\bin;C:\sha-shim;${MISESHIMS};C:\Windows\System32;C:\Windows;${LLVMBIN};C:\gnupg\bin;${PATH}"
# Diagnostic for gpg-on-Nano (fail-fast): check gpg three ways before mise touches it.
# With gpg only newly here via the
# gpgbin donor stage, the earliest known failure mode was the bare gpg invoke
# crashing -- so check it three ways before mise touches it:
# (1) dir listing of C:\gnupg\bin proves the silent install populated bin/
# (2) `gpg --version` proves gpg.exe loads + its supporting DLLs resolve
# (3) `gpg --list-keys` proves the keyring + keyboxd spawn work on Nano
# %APPDATA% existence is checked earlier with HOME. gpg writes its homedir to
# %APPDATA%\gnupg and --list-keys creates it on first use. If the next
# mise-on-Nano failure is "gpg failed" after these all pass, the failure is in
# mise's pipe-to-stdin flow, not in gpg load/init.
RUN dir C:\gnupg\bin & `
echo [gpg-check] gpg --version: & `
gpg --version & `
if errorlevel 1 (echo [gpg-check] FAILED gpg --version: errorlevel=%ERRORLEVEL% & exit /b 1) & `
echo [gpg-check] gpg --list-keys: & `
gpg --list-keys & `
if errorlevel 1 (echo [gpg-check] FAILED gpg --list-keys: errorlevel=%ERRORLEVEL% & exit /b 1) & `
echo [gpg-check] OK
WORKDIR C:\workspace
# Copy the always-loaded mise configs; the guest-language configs come in the build stage below.
# config.toml (always-loaded) + config.windows.toml (the Windows tools/env/task)
# + .miserc.toml (auto_env=true, which makes mise load config.windows.toml on
# Windows).
COPY .mise/config.toml .mise/config.toml
COPY .mise/config.windows.toml .mise/config.windows.toml
COPY .miserc.toml .miserc.toml
# gh_token (a GitHub token) is OPTIONAL.
# With it, mise's GitHub fetches use the
# authenticated rate limit; without it they fall back to the lower anonymous one,
# which can rate-limit `install`. The classic Windows builder has no
# BuildKit secrets, so the token is read per-RUN from a file in the build context
# (it can't be a build-arg substituted into RUN). The glob makes it optional:
# paired with an always-present file (.miserc.toml) so COPY still succeeds when
# gh_token is absent; both land in C:\token (a dir off the WORKDIR ancestor chain,
# so the paired file is inert). WARNING: when a token IS supplied it bakes into
# this image layer -- do NOT publish an image built with one (see the README).
COPY .miserc.toml gh_token* C:/token/
# Apply the Docker-only mise settings, written inline in cmd (no bash needed).
# `mise settings` writes the global config (C:\.config\mise on Windows, not
# auto-trusted -- hence the explicit trust); experimental enables the conda/cargo
# backends; cargo.binstall lets cargo: tools use prebuilt binaries where they
# exist; aqua signature checks (attestations/cosign/slsa) are off (their
# sigstore/TUF step can't find a cache dir here); pipx.uvx=false makes the pipx:
# backend use pip not uv (uv's PE-resource trampoline isn't implemented on Nano).
RUN (if exist C:\token\gh_token set /p GITHUB_TOKEN=<C:\token\gh_token) & `
mise trust && `
mise settings experimental=true && `
mise settings set cargo.binstall true && `
mise settings set aqua.github_attestations false && `
mise settings set aqua.cosign false && `
mise settings set aqua.slsa false && `
mise settings set pipx.uvx false && `
mise trust C:\.config\mise\config.toml
# Install busybox-w32 `sh`, the shell the bash-tasks run on (defined in config.windows.toml).
# It is a prebuilt native exe, just downloaded (no build), so
# it's available immediately and preinstall below can run on it.
# MISE_BASH_PATH (config.windows.toml [env]) points the task shell here.
RUN (if exist C:\token\gh_token set /p GITHUB_TOKEN=<C:\token\gh_token) & `
mise install http:busybox
# Smoke test (informational -- never fails the build): confirm busybox runs and finds `mise` on PATH.
# This runs before the bash-shell tasks below rely on it.
ENV WINSH=C:\Users\ContainerAdministrator\AppData\Local\mise\installs\http-busybox\1.37.0\ash.exe
RUN dir "%LOCALAPPDATA%\mise\installs\http-busybox\1.37.0" & `
("%WINSH%" -euo pipefail -c "command -v mise && echo [smoke] OK || echo [smoke] NOMISE") & `
ver>nul
# Nano-safe sha256sum shim.
# mise's `src/hash.rs` switches from in-process
# Rust SHA256 to a subprocess `sha256sum` for files > 50 MB, and the
# uutils-coreutils sha256sum.exe mise installs later (via `coreutils` in
# _setup_all) fails to load on Nano with verbatim:
# mise ERROR Failed to install conda:m2-git@latest: command ["sha256sum",
# "...conda-packages\m2-git-2.49.0.1-hc364b38_6.tmp.<pid>.<n>"] exited
# with code -1073741511
# (-1073741511 = 0xC0000139 = STATUS_ENTRYPOINT_NOT_FOUND; same load-fail
# signature as the msys2 autoload trap). Busybox-w32's sha256sum applet IS
# native Win32 + loads on Nano (the smoke test above already proved busybox
# runs here), and it dispatches by argv[0] -- copy busybox under the name
# `sha256sum.exe` into a dir that PATH below resolves *before* the mise
# shims dir, and the call becomes a busybox applet invocation.
RUN if not exist C:\sha-shim mkdir C:\sha-shim & `
copy /Y "%WINSH%" C:\sha-shim\sha256sum.exe & `
if not exist C:\sha-shim\sha256sum.exe exit /b 1
# Pin CARGO_HOME/RUSTUP_HOME to the dirs mise's rust tool uses, C:\.cargo and C:\.rustup.
# See its exec_env. preinstall sets the gnullvm default
# toolchain in C:\.rustup, but when mise's cargo backend builds a `cargo:` tool it
# spawns `cargo install` WITHOUT rust's exec_env, so rustup would otherwise look
# in the empty %USERPROFILE%\.rustup and fail with "no default is configured".
# Setting these globally makes every cargo/rustup invocation find the default.
ENV CARGO_HOME=C:\.cargo `
RUSTUP_HOME=C:\.rustup
# The shared Windows preinstall pip-installs pipx, the backend for the pipx:* mise tools.
# On a workstation it picks up the system CPython
# on PATH; on Nano there's no CPython, so it falls back to the
# `http:et-rp` rustpython binary (config.toml's [tools."http:et-rp"]
# ships an x86_64-pc-windows-msvc tarball). The pipx that lands isn't
# invoked here -- every pipx:* tool is in MISE_DISABLE_TOOLS on this
# image (pipx/platformdirs needs SHGetFolderPathW, which Nano lacks).
# Run preinstall on busybox sh (via MISE_BASH_PATH), already bootstrapped above,
# so there's no chicken-and-egg with the shell.
RUN (if exist C:\token\gh_token set /p GITHUB_TOKEN=<C:\token\gh_token) & `
mise run preinstall
# Skip tools this image never runs.
# `pipx:cowsay` is the canary: it stays
# OUT of MISE_DISABLE_TOOLS so the next `mise install` exercises the
# end-to-end pipx-on-Nano path (rustpython interpreter, pip-26.0.1
# RUSTPYTHONPATH workaround, pre-created shared-libs venv). If pipx ever
# regresses on Nano the failure surfaces here, not silently months later
# when a pipx:* guest tool gets added. The other pipx tools stay listed
# because their installers do platform-incompatible things even with a
# working pipx (semgrep needs a real CPython, etc.).
# conda:m2-gnupg -- the workstation Windows path uses this msys2 gnupg (declared
# in config.windows.toml); on Nano its msys-2.0.dll trips the kernel32 autoload
# trap (see the comment block at the top of this file), so the gpgbin donor stage
# above provides a clean MinGW gpg.exe at C:\gnupg\bin instead. All install fine
# elsewhere; dropping them here just keeps the image lean.
# `cargo:dart-typegen` is os-scoped to non-Windows in config.dart.toml --
# the Windows lane (incl. Nano) uses `http:dart-typegen` from the
# upstream-cache release instead.
# Two intermediate ARGs so each line fits 120 chars without backslash
# continuations (Dockerfile ENV doesn't support comment-aware joining and
# the value is too long for a single line otherwise). The ARGs don't
# persist into the runtime env; only the joined ENV does.
ARG MISE_DT_BASE=conda:m2-gnupg,lychee,pipx:cowsay,pipx:cpplint,pipx:semgrep
ARG MISE_DT_DOTNET=dotnet:roslynator.dotnet.cli
ARG MISE_DT_PY=pipx:componentize-py,pipx:datamodel-code-generator,pipx:openapi-python-client,pipx:pytest,pipx:torch
ENV MISE_DISABLE_TOOLS=${MISE_DT_BASE},${MISE_DT_DOTNET},${MISE_DT_PY}
# Wire mise's pipx backend at the bootstrapped pipx.
# preinstall above installed it via rustpython into target/preinstall-pipx. PATH points at
# the script the rustpython arm of preinstall just laid down; PYTHONPATH
# + RUSTPYTHONPATH point both pip-26.0.1 and pipx's site-packages so the
# script's `from pipx.main import cli` resolves; PIPX_SHARED_LIBS points
# at the pre-created shared venv so pipx's first run skips `ensurepip`
# (which would trip the same audit-hook crash). PIPX_DEFAULT_BACKEND=pip
# keeps pipx off the uv backend (uv strips env when spawning the
# interpreter probe, so RUSTPYTHONPATH would vanish). Workspace-relative
# paths because the Nano build's WORKDIR is C:\workspace.
#
# WIN_PD_OVERRIDE_* short-circuits platformdirs's `get_win_folder` so it
# returns these strings instead of calling SHGetKnownFolderPath via ctypes
# (Nano's shell32 is missing the entrypoint). Three CSIDLs cover every
# code path pipx.paths can reach at import: LOCAL_APPDATA (cache/data),
# APPDATA (user_data_dir roaming branch), COMMON_APPDATA (site dirs).
# Values just need to be writable; pipx's real data goes to PIPX_HOME.
ENV PIPX_DEFAULT_BACKEND=pip `
PIPX_HOME=C:\workspace\target\preinstall-pipx\pipx-home `
PIPX_BIN_DIR=C:\workspace\target\preinstall-pipx\pipx-bin `
PIPX_SHARED_LIBS=C:\workspace\target\preinstall-pipx\shared `
PYTHONPATH=C:\workspace\target\preinstall-pipx\site `
RUSTPYTHONPATH=C:\workspace\target\preinstall-pipx\pip-extracted;C:\workspace\target\preinstall-pipx\site `
WIN_PD_OVERRIDE_LOCAL_APPDATA=C:\workspace\target\preinstall-pipx\winpd\local `
WIN_PD_OVERRIDE_APPDATA=C:\workspace\target\preinstall-pipx\winpd\roaming `
WIN_PD_OVERRIDE_COMMON_APPDATA=C:\workspace\target\preinstall-pipx\winpd\common `
PATH="C:\workspace\target\preinstall-pipx\site\bin;${PATH}"
# Install the remaining always-loaded tools now that rust links via gnullvm.
# These are the cargo: tools; python is already installed above.
RUN (if exist C:\token\gh_token set /p GITHUB_TOKEN=<C:\token\gh_token) & `
mise install
# build: add the guest-language toolchains (config.<lang>.toml).
# Nano can only install a subset of the standard guest set: dotnet is excluded
# (its installer is a PowerShell script, and Nano has no PowerShell) and python
# is excluded (its tools install via pipx; the rustpython-pipx fallback fails
# with STATUS_DLL_NOT_FOUND / STATUS_ENTRYPOINT_NOT_FOUND on the Nano API set).
# The accompanying ARG accepts the workflow-level MISE_ENV (the standard set)
# for documentation, but the ENV that follows hardcodes the Nano-installable
# subset so an inherited fuller value can't override it. Use `mise install`
# rather than install-all (which forces $ALL_LANGS) so this restricted
# MISE_ENV is honored; it persists to later stages.
FROM build-minimal AS build
COPY .mise/ .mise/
RUN mise trust
ARG MISE_ENV
ENV MISE_ENV=dart,java,js,rust,zig
RUN (if exist C:\token\gh_token set /p GITHUB_TOKEN=<C:\token\gh_token) & `
mise install
# prefetch: download all dependencies + ONNX models.
# The full source is needed from here on (module builds, cargo fetch, pnpm).
# `prefetch-ci` (mnist only) instead of full `prefetch` (mnist + face1 +
# har-motion1) because face1 / har-motion1's checksum-verify steps invoke
# `coreutils sha256sum`, and mise's coreutils install on Nano Server has
# a broken bin path -- the WARN "bin path does not exist:
# ...installs\coreutils\0.9.0\coreutils-0.9.0-x86_64-pc-windows-msvc\coreutils.exe"
# means `mise exec -- coreutils` fails. docker-windows CI only builds
# the `precompile` target (never the `test` stage, which is what would
# need the face / har models at runtime), so the lighter prefetch is
# sufficient. Switch back once the Nano-shim TODO in CLAUDE.md lands.
FROM build AS prefetch
COPY . .
RUN (if exist C:\token\gh_token set /p GITHUB_TOKEN=<C:\token\gh_token) & `
mise run prefetch-ci
# precompile: build the WASM/JS modules (needed by test and server).
# Mirrors Linux; drops target/ in the same layer so the multi-GB cargo
# intermediates don't bake in (module outputs live in each module's pkg/).
FROM prefetch AS precompile
RUN mise run build-modules && if exist target rmdir /s /q target
# test: the full suite. Compiled + run at `docker run` time.
# Mirrors Linux's `mise run test`; no host GPU is attachable at build time, so it
# runs in the container. (Whether wgpu finds a usable adapter on a Windows
# container host is itself unknown -- part of the frontier below.)
FROM precompile AS test
CMD ["mise", "run", "test"]
# server: release build of et-ws-server, the default image (final stage).
# A plain `docker build` produces this. CARGO_BUILD_TARGET puts the binary under
# target\<triple>\release\; copy it onto PATH (C:\mise\bin) and drop target/ in
# the same layer so the build intermediates don't bloat the image. The server
# serves each module from its pkg/ (none of which live in target/).
FROM precompile AS server
RUN mise exec -- cargo build --release -p et-ws-server && `
copy target\x86_64-pc-windows-gnullvm\release\et-ws-server.exe C:\mise\bin\ && `
if exist target rmdir /s /q target
EXPOSE 8080 8443
CMD ["et-ws-server"]