fix(size): the v1.9.1 residual was npm's own cache, not the platform binaries
v1.9.1's @esbuild prune fixed the 431 MB size-gate failure but still shipped
+131 MB compressed over v1.8.14, nearly all of it in the pi/extensions install
layer (87 -> 206 MB). That leftover was filed as an open item with an explicit
hypothesis — the @mariozechner/clipboard-* family, same npm 11 behaviour, a
different package — and an explicit warning that the hypothesis was not a
measured cause. Measured now, after recreating onto v1.9.1, and the hypothesis
accounted for one sixth of it:
+110 MB /root/.npm/_cacache (35.2 -> 145.3 MB) the build's npm cache
+ 21 MB clipboard foreign platform packages, both install sites
= 131 MB i.e. the whole delta, no unexplained remainder
Method, since there is no docker CLI inside the container: pulled both variant
layer blobs straight from the registry with a token + manifest + blob fetch and
listed the tarballs (29 789 vs 29 889 entries, 270.8 vs 401.9 MB uncompressed),
then aggregated per package. The file COUNT barely moved, which is what said
"few large files", not "npm installed more packages".
- purge_build_caches: npm cache clean --force + rm -rf /root/.npm, in the SAME
layer as the installs, in both the main RUN and the studio RUN. npm 11
caches every platform tarball it downloads, including the ones the prune
then deletes, so the cache grew faster than the tree. Nothing at runtime
reads it: build is root, container is developer with its own cache in $HOME.
- prune_foreign_esbuild -> prune_foreign_natives: now covers both MEASURED
families. Clipboard keeps linux-$arch-gnu AND -musl because its napi-rs
loader picks between them at runtime via its own isMusl() probe; the musl
package is a 420-byte stub. The bare @mariozechner/clipboard wrapper has no
hyphen suffix and cannot match the pattern.
Verified on arm64 before writing the glob — a widened rm -rf against a tree you
cannot inspect is the one change shape not to write blind, which is why the
order was update-then-patch. Exercised against a copy of the real trees with
foreign dirs fabricated back in (aix-ppc64, android-arm64, darwin-arm64,
win32-x64, linux-x64): all removed, host linux-arm64 kept at both sites, 21 MB
freed, require('@mariozechner/clipboard') still loads and exports all 18
functions, esbuild.transformSync still compiles TS at both sites. v1.9.1's own
arm64 validation also passed here; CI could only smoke amd64.
Two sentinel assertions, because the size gate did not catch this: it has
~225 MB of margin, so 131 MB of residue stayed green. Both were verified RED
against the running v1.9.1 image and GREEN against a pruned tree. The cache one
refuses to run as non-root: test ! -d /root/.npm on mode-700 /root would
otherwise pass for the wrong reason. Size-failure diagnostics now list cache
paths too — they previously enumerated only node_modules and /opt, where these
bytes were not.
Also fixed: -printf '%f\\n' reaches the shell with both backslashes (confirmed
from the published image's recorded created_by), so v1.9.1's progress line
printed a mangled "li ux-arm64" — find emitted a literal backslash and tr ate
the n out of the name. Single backslash now.
Deliberately not purged: /tmp/node-compile-cache (1.3 MB). The manifest RUN
calls pi --version again, so deleting it earlier only relocates those bytes into
that layer — today's manifest layer is 128 kB precisely because it finds the
cache warm.
Dockerfile.base untouched, so no base rebuild: this rides the next release.
This commit is contained in:
@@ -280,8 +280,10 @@ shipped the same image bytes); preventatively fixed for `PI_VERSION` +
|
|||||||
image. Verifies binaries, repo clones, runtime deployment (waits for
|
image. Verifies binaries, repo clones, runtime deployment (waits for
|
||||||
keybindings + mempalace bridge + ≥4 extensions before sampling — fixes
|
keybindings + mempalace bridge + ≥4 extensions before sampling — fixes
|
||||||
the parallel-build-load race documented in opencode-devbox c6f9d11
|
the parallel-build-load race documented in opencode-devbox c6f9d11
|
||||||
2026-06-08), and image size threshold (3500 MB; revisit after a few
|
2026-06-08), build-time leftovers (see below), and image size threshold
|
||||||
releases as actuals settle).
|
(3800 MB in `SIZE_THRESHOLD_MB`; revisit after a few releases as actuals
|
||||||
|
settle — this doc said 3500 until 2026-09-11, after the bar had already
|
||||||
|
moved twice).
|
||||||
|
|
||||||
If smoke fails on size threshold but build is otherwise fine: bump
|
If smoke fails on size threshold but build is otherwise fine: bump
|
||||||
`SIZE_THRESHOLD_MB` in scripts/smoke-test.sh in a follow-up commit and
|
`SIZE_THRESHOLD_MB` in scripts/smoke-test.sh in a follow-up commit and
|
||||||
@@ -289,6 +291,17 @@ re-run. The threshold exists to catch *runaway* growth (an accidental
|
|||||||
texlive bake-in, a forgotten chrome dependency), not to block ordinary
|
texlive bake-in, a forgotten chrome dependency), not to block ordinary
|
||||||
upstream bumps.
|
upstream bumps.
|
||||||
|
|
||||||
|
**The size gate is not a substitute for naming the residue.** It carries
|
||||||
|
~225 MB of deliberate margin, so v1.9.1 shipped +131 MB of pure build
|
||||||
|
residue — 110 MB of it npm's own download cache under `/root/.npm`, the
|
||||||
|
rest foreign platform packages — and stayed green. Two named assertions
|
||||||
|
now cover exactly that: no foreign npm-11 platform packages beyond the
|
||||||
|
host arch (`@esbuild/*`, `@mariozechner/clipboard-*`), and no
|
||||||
|
`/root/.npm` in the image. Note the second one refuses to run as
|
||||||
|
non-root: `test ! -d /root/.npm` on mode-700 `/root` would otherwise
|
||||||
|
pass for the wrong reason, which is the failure shape to watch for in
|
||||||
|
any assertion about a path you may not be able to read.
|
||||||
|
|
||||||
## Build pipeline notes
|
## Build pipeline notes
|
||||||
|
|
||||||
- **Two-phase**: base + variant. Base is rebuilt only when
|
- **Two-phase**: base + variant. Base is rebuilt only when
|
||||||
|
|||||||
@@ -11,6 +11,79 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
**The v1.9.1 residual is attributed and fixed: it was mostly npm's own download
|
||||||
|
cache, not the platform binaries it looked like.** v1.9.1 pruned the 26 foreign
|
||||||
|
`@esbuild/<platform>` directories that npm 11 installs, which fixed the 431 MB
|
||||||
|
size-gate failure — but the image still shipped **+131 MB compressed over
|
||||||
|
v1.8.14**, nearly all of it in the single pi/extensions install layer (87 MB →
|
||||||
|
206 MB). v1.9.1's notes recorded the leftover as an open item with an explicit
|
||||||
|
hypothesis (the `@mariozechner/clipboard-*` family, same npm 11 behaviour, a
|
||||||
|
different package) and an explicit warning that the hypothesis was **not** a
|
||||||
|
measured cause. It was measured on 2026-09-11, after recreating onto v1.9.1, and
|
||||||
|
the hypothesis accounted for only a sixth of it:
|
||||||
|
|
||||||
|
| item | v1.8.14 | v1.9.1 | delta |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/root/.npm/_cacache` — the build's npm download cache | 35.2 MB | 145.3 MB | **+110 MB** |
|
||||||
|
| `@mariozechner/clipboard-*` foreign platform packages (2 sites) | 0 MB | 21.1 MB | **+21 MB** |
|
||||||
|
| variant install layer, uncompressed total | 270.8 MB | 401.9 MB | +131 MB |
|
||||||
|
|
||||||
|
That is the whole delta with no unexplained remainder. Both items are now
|
||||||
|
deleted in the **same layer** that creates them, in both the main install RUN and
|
||||||
|
the studio RUN:
|
||||||
|
|
||||||
|
- **`purge_build_caches`** — `npm cache clean --force` plus `rm -rf /root/.npm`.
|
||||||
|
npm 11 caches every platform tarball it downloads, including the ones the
|
||||||
|
prune then deletes, so the cache grew far faster than the installed tree.
|
||||||
|
Nothing at runtime reads it: the build runs as root, the container runs as
|
||||||
|
`developer` with its own cache under `$HOME`.
|
||||||
|
- **`prune_foreign_esbuild` → `prune_foreign_natives`** — now covers both
|
||||||
|
measured families. For clipboard the keep-set is `clipboard-linux-$arch-gnu`
|
||||||
|
**and** `-musl`, because its napi-rs loader chooses between them at runtime
|
||||||
|
from its own `isMusl()` probe; the musl package is a 420-byte stub, so keeping
|
||||||
|
it is free insurance. The bare `@mariozechner/clipboard` wrapper has no
|
||||||
|
hyphen suffix and cannot match the pattern.
|
||||||
|
|
||||||
|
**Verified on arm64 before writing the patch, which is why the order was
|
||||||
|
update-then-patch:** a widened `rm -rf` glob is the worst possible change to
|
||||||
|
write against a tree you cannot inspect, and there is no docker CLI inside the
|
||||||
|
container — but after a recreate the container *is* the image. The prune was
|
||||||
|
exercised against a copy of the real trees with foreign directories fabricated
|
||||||
|
back in (aix-ppc64, android-arm64, darwin-arm64, win32-x64, linux-x64): all
|
||||||
|
removed, host `linux-arm64` kept at both sites, 21 MB freed, and
|
||||||
|
`require('@mariozechner/clipboard')` still loads and exports all 18 functions.
|
||||||
|
`esbuild.transformSync` still compiles TS at both sites. v1.9.1's own arm64
|
||||||
|
validation of the esbuild prune also passed here — CI could only smoke-test
|
||||||
|
amd64.
|
||||||
|
|
||||||
|
**Two sentinel assertions in `smoke-test.sh`, because the size gate did not
|
||||||
|
catch this.** The gate has ~225 MB of deliberate margin, so 131 MB of pure
|
||||||
|
build residue stayed green. There are now named PASS/FAIL checks for *foreign
|
||||||
|
platform packages beyond the host arch* and for *`/root/.npm` being shipped* —
|
||||||
|
the latter deliberately refuses to run as non-root, because `test ! -d
|
||||||
|
/root/.npm` on mode-700 `/root` would otherwise pass for the wrong reason. The
|
||||||
|
size-failure diagnostics now also list cache paths: they previously enumerated
|
||||||
|
only `node_modules` and `/opt`, where these bytes were not.
|
||||||
|
|
||||||
|
**Also fixed: the prune's own progress line was mangled.** `-printf '%f\\n'`
|
||||||
|
reaches the shell with both backslashes (confirmed from the published image's
|
||||||
|
recorded `created_by`), so `find` emitted a literal backslash and `tr` then ate
|
||||||
|
the `n` out of the name — v1.9.1 printed `esbuild platform dirs kept: li
|
||||||
|
ux-arm64`. Single backslash now.
|
||||||
|
|
||||||
|
Deliberately **not** changed: `/tmp/node-compile-cache` (1.3 MB). The manifest
|
||||||
|
RUN at the end of `Dockerfile.variant` calls `pi --version` again, so deleting
|
||||||
|
it earlier only relocates those bytes into that layer — today's manifest layer
|
||||||
|
is 128 kB precisely because it finds the cache warm.
|
||||||
|
|
||||||
|
Touches `Dockerfile.variant` and `scripts/smoke-test.sh` only: `Dockerfile.base`
|
||||||
|
is unchanged, so this needs no base rebuild and should **ride the next release**
|
||||||
|
rather than burn a cycle of its own.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v1.9.1 — 2026-09-10
|
## v1.9.1 — 2026-09-10
|
||||||
|
|
||||||
**v1.9.0 was tagged but never published: its own smoke gate stopped it, and it
|
**v1.9.0 was tagged but never published: its own smoke gate stopped it, and it
|
||||||
|
|||||||
+81
-25
@@ -196,24 +196,70 @@ RUN set -e && \
|
|||||||
done; \
|
done; \
|
||||||
return 1; \
|
return 1; \
|
||||||
} && \
|
} && \
|
||||||
# prune_foreign_esbuild: npm 11 (shipped with Node 24) installs EVERY
|
# prune_foreign_natives: npm 11 (shipped with Node 24) installs EVERY optional
|
||||||
# @esbuild/<platform> optional binary instead of only the one matching the
|
# platform package of a native dependency, not just the one matching the host.
|
||||||
# host — 26 platform dirs, 284 MB, for aix-ppc64/android/darwin/freebsd/
|
# TWO families are affected in this image, and BOTH have been measured — add a
|
||||||
# netbsd/openbsd/win32/s390x/riscv64/... that this image can never execute.
|
# family here only after measuring it, never by widening the pattern on a hunch:
|
||||||
# Measured on pi-fork's tree: npm 10.9.8 -> 165 MB, npm 11.19.0 -> 449 MB,
|
#
|
||||||
# and the 165 MB figure reproduces what v1.9.0's predecessor actually shipped.
|
# @esbuild/<platform> 26 dirs, 284 MB (found first, v1.9.1)
|
||||||
|
# @mariozechner/clipboard-<triple> 11 dirs, 12 MB per site, 10 MB foreign
|
||||||
|
#
|
||||||
# esbuild declares those with os/cpu constraints, but npm 11 ignores them and
|
# esbuild declares those with os/cpu constraints, but npm 11 ignores them and
|
||||||
# ALSO ignores --os/--cpu and an npmrc carrying os=/cpu= (all three measured).
|
# ALSO ignores --os/--cpu and an npmrc carrying os=/cpu= (all three measured).
|
||||||
# So prune explicitly, keeping only linux-$(node -p process.arch) so the same
|
# So prune explicitly, keeping only the host platform, computed from
|
||||||
# line is correct on amd64 and arm64. Verified after pruning that esbuild still
|
# `node -p process.arch` so one line stays correct on amd64 and arm64.
|
||||||
# works (transformSync compiles TS), i.e. this removes dead weight, not function.
|
# Measured on pi-fork's tree: npm 10.9.8 -> 165 MB, npm 11.19.0 -> 449 MB,
|
||||||
|
# and the 165 MB figure reproduces what v1.9.0's predecessor actually shipped.
|
||||||
|
#
|
||||||
|
# WHY THE CLIPBOARD FAMILY WAS ADDED (2026-09-11): v1.9.1 pruned @esbuild only
|
||||||
|
# and still shipped +131 MB compressed over v1.8.14. That residual was
|
||||||
|
# attributed by listing the PUBLISHED arm64 layer tarballs straight from the
|
||||||
|
# registry (there is no docker CLI inside the container, so `docker history`
|
||||||
|
# was not available): +110 MB /root/.npm/_cacache (purged below) and +21 MB of
|
||||||
|
# clipboard platform packages across the two install sites — 131 MB total, so
|
||||||
|
# the delta is now fully accounted for with no unexplained remainder.
|
||||||
|
#
|
||||||
|
# Keeping linux-$arch-{gnu,musl} is deliberate: clipboard's napi-rs loader
|
||||||
|
# tries ./<name>.node then the platform package, per platform in try/catch, and
|
||||||
|
# chooses gnu vs musl at runtime from its own isMusl() probe — so both host-arch
|
||||||
|
# branches must survive. The musl package is a 420-byte stub, i.e. free. The
|
||||||
|
# bare wrapper `@mariozechner/clipboard` has no hyphen suffix and therefore
|
||||||
|
# cannot match the regex below. Verified on arm64 against a copy of the real
|
||||||
|
# tree before this was written: after pruning to those two,
|
||||||
|
# require('@mariozechner/clipboard') still loads and exports all 18 functions.
|
||||||
|
# esbuild likewise still compiles TS via transformSync at both install sites.
|
||||||
|
# This removes dead weight, not function.
|
||||||
|
#
|
||||||
# MUST run in the SAME layer as the npm installs above: deleting in a later RUN
|
# MUST run in the SAME layer as the npm installs above: deleting in a later RUN
|
||||||
# leaves the bytes in this layer and shrinks the image by nothing.
|
# leaves the bytes in this layer and shrinks the image by nothing.
|
||||||
prune_foreign_esbuild() { \
|
# NOTE the single backslash in -printf '%f\n': Docker passes '\\n' through
|
||||||
keep="linux-$(node -p process.arch)"; \
|
# verbatim, so v1.9.1's doubled version printed a mangled "li ux-arm64"
|
||||||
|
# (find emitted a literal backslash, then `tr` translated the n out of the
|
||||||
|
# name). Confirmed from the published image's own recorded created_by.
|
||||||
|
prune_foreign_natives() { \
|
||||||
|
arch="$(node -p process.arch)"; \
|
||||||
find /usr/lib/node_modules /opt -type d -regex '.*/@esbuild/[^/]+' \
|
find /usr/lib/node_modules /opt -type d -regex '.*/@esbuild/[^/]+' \
|
||||||
! -name "$keep" -prune -exec rm -rf {} + ; \
|
! -name "linux-$arch" -prune -exec rm -rf {} + ; \
|
||||||
echo "esbuild platform dirs kept: $(find /usr/lib/node_modules /opt -type d -regex '.*/@esbuild/[^/]+' -printf '%f\\n' 2>/dev/null | sort -u | tr '\\n' ' ')"; \
|
find /usr/lib/node_modules /opt -type d -regex '.*/@mariozechner/clipboard-[^/]+' \
|
||||||
|
! -name "clipboard-linux-$arch-gnu" ! -name "clipboard-linux-$arch-musl" \
|
||||||
|
-prune -exec rm -rf {} + ; \
|
||||||
|
echo "native platform dirs kept: $(find /usr/lib/node_modules /opt -type d \( -regex '.*/@esbuild/[^/]+' -o -regex '.*/@mariozechner/clipboard-[^/]+' \) -printf '%f\n' 2>/dev/null | sort | uniq -c | tr '\n' ' ')"; \
|
||||||
|
} && \
|
||||||
|
# purge_build_caches: the build's own download caches are NOT free — they land
|
||||||
|
# in whichever layer created them. Measured on the published v1.9.1 arm64
|
||||||
|
# variant layer: root/.npm/_cacache was 145.2 MB of a 401.9 MB layer (35.2 MB
|
||||||
|
# in v1.8.14), the single biggest item in the +131 MB residual, because npm 11
|
||||||
|
# caches every platform tarball it fetched — including the ones just pruned.
|
||||||
|
# Nothing at runtime reads it: the build runs as root, the container runs as
|
||||||
|
# `developer` with its own cache under $HOME (and $HOME/.pi is a volume).
|
||||||
|
# DELIBERATELY NOT purged here: /tmp/node-compile-cache (1.3 MB, written by
|
||||||
|
# `pi --version` below). The manifest RUN at the end of this file calls
|
||||||
|
# `pi --version` again, so deleting it here only relocates those bytes into
|
||||||
|
# that layer instead of removing them from the image — measured, not assumed:
|
||||||
|
# today the manifest layer is 128 kB precisely because it finds the cache warm.
|
||||||
|
purge_build_caches() { \
|
||||||
|
npm cache clean --force >/dev/null 2>&1 || true; \
|
||||||
|
rm -rf /root/.npm; \
|
||||||
} && \
|
} && \
|
||||||
if [ "${PI_VERSION}" = "latest" ]; then \
|
if [ "${PI_VERSION}" = "latest" ]; then \
|
||||||
NPM_CONFIG_PREFIX=/usr npm install -g @earendil-works/pi-coding-agent ; \
|
NPM_CONFIG_PREFIX=/usr npm install -g @earendil-works/pi-coding-agent ; \
|
||||||
@@ -228,7 +274,8 @@ RUN set -e && \
|
|||||||
git_fetch_ref "${PI_ATELIER_REPO}" "${PI_ATELIER_REF}" /opt/pi-atelier && \
|
git_fetch_ref "${PI_ATELIER_REPO}" "${PI_ATELIER_REF}" /opt/pi-atelier && \
|
||||||
(cd /opt/pi-fork && npm install --omit=dev --no-audit --no-fund) && \
|
(cd /opt/pi-fork && npm install --omit=dev --no-audit --no-fund) && \
|
||||||
(cd /opt/pi-observational-memory && npm install --omit=dev --no-audit --no-fund) && \
|
(cd /opt/pi-observational-memory && npm install --omit=dev --no-audit --no-fund) && \
|
||||||
prune_foreign_esbuild && \
|
prune_foreign_natives && \
|
||||||
|
purge_build_caches && \
|
||||||
echo "pi-toolkit at $(cd /opt/pi-toolkit && git rev-parse --short HEAD)" && \
|
echo "pi-toolkit at $(cd /opt/pi-toolkit && git rev-parse --short HEAD)" && \
|
||||||
echo "pi-extensions at $(cd /opt/pi-extensions && git rev-parse --short HEAD)" && \
|
echo "pi-extensions at $(cd /opt/pi-extensions && git rev-parse --short HEAD)" && \
|
||||||
echo "pi-fork at $(cd /opt/pi-fork && git rev-parse --short HEAD)" && \
|
echo "pi-fork at $(cd /opt/pi-fork && git rev-parse --short HEAD)" && \
|
||||||
@@ -329,17 +376,25 @@ ARG PI_STUDIO_REF=main
|
|||||||
ARG PI_STUDIO_VERSION=none
|
ARG PI_STUDIO_VERSION=none
|
||||||
RUN if [ "${INSTALL_STUDIO}" = "true" ]; then \
|
RUN if [ "${INSTALL_STUDIO}" = "true" ]; then \
|
||||||
set -e; \
|
set -e; \
|
||||||
# Same esbuild prune as the main install RUN — see the comment there. It has
|
# Same prune + cache purge as the main install RUN — see the comments there.
|
||||||
# to be redefined because shell functions do not survive across layers, and
|
# They have to be redefined because shell functions do not survive across
|
||||||
# it has to run in THIS layer because pi-studio's npm install happens here:
|
# layers, and they have to run in THIS layer because pi-studio's npm install
|
||||||
# deleting in a later RUN would leave the bytes in this layer and shrink
|
# happens here: deleting in a later RUN would leave the bytes in this layer
|
||||||
# nothing. pi-studio pulls its own pi-coding-agent copy, so it is a third
|
# and shrink nothing. pi-studio pulls its own pi-coding-agent copy, so it is
|
||||||
# ~274 MB site on top of the two in the non-studio variant.
|
# a third ~274 MB site on top of the two in the non-studio variant — and its
|
||||||
prune_foreign_esbuild() { \
|
# npm install refills /root/.npm, which the main RUN emptied in ITS layer.
|
||||||
keep="linux-$(node -p process.arch)"; \
|
prune_foreign_natives() { \
|
||||||
|
arch="$(node -p process.arch)"; \
|
||||||
find /usr/lib/node_modules /opt -type d -regex '.*/@esbuild/[^/]+' \
|
find /usr/lib/node_modules /opt -type d -regex '.*/@esbuild/[^/]+' \
|
||||||
! -name "$keep" -prune -exec rm -rf {} + ; \
|
! -name "linux-$arch" -prune -exec rm -rf {} + ; \
|
||||||
echo "esbuild platform dirs kept: $(find /usr/lib/node_modules /opt -type d -regex '.*/@esbuild/[^/]+' -printf '%f\\n' 2>/dev/null | sort -u | tr '\\n' ' ')"; \
|
find /usr/lib/node_modules /opt -type d -regex '.*/@mariozechner/clipboard-[^/]+' \
|
||||||
|
! -name "clipboard-linux-$arch-gnu" ! -name "clipboard-linux-$arch-musl" \
|
||||||
|
-prune -exec rm -rf {} + ; \
|
||||||
|
echo "native platform dirs kept: $(find /usr/lib/node_modules /opt -type d \( -regex '.*/@esbuild/[^/]+' -o -regex '.*/@mariozechner/clipboard-[^/]+' \) -printf '%f\n' 2>/dev/null | sort | uniq -c | tr '\n' ' ')"; \
|
||||||
|
}; \
|
||||||
|
purge_build_caches() { \
|
||||||
|
npm cache clean --force >/dev/null 2>&1 || true; \
|
||||||
|
rm -rf /root/.npm; \
|
||||||
}; \
|
}; \
|
||||||
rm -rf /opt/pi-studio && mkdir -p /opt/pi-studio && \
|
rm -rf /opt/pi-studio && mkdir -p /opt/pi-studio && \
|
||||||
git -C /opt/pi-studio init -q && \
|
git -C /opt/pi-studio init -q && \
|
||||||
@@ -352,7 +407,8 @@ RUN if [ "${INSTALL_STUDIO}" = "true" ]; then \
|
|||||||
done; \
|
done; \
|
||||||
[ "$ok" = "1" ] && \
|
[ "$ok" = "1" ] && \
|
||||||
(cd /opt/pi-studio && npm install --omit=dev --no-audit --no-fund) && \
|
(cd /opt/pi-studio && npm install --omit=dev --no-audit --no-fund) && \
|
||||||
prune_foreign_esbuild && \
|
prune_foreign_natives && \
|
||||||
|
purge_build_caches && \
|
||||||
echo "pi-studio at $(cd /opt/pi-studio && git rev-parse --short HEAD)"; \
|
echo "pi-studio at $(cd /opt/pi-studio && git rev-parse --short HEAD)"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
+40
-2
@@ -28,6 +28,8 @@
|
|||||||
# (human, --json, --quiet)
|
# (human, --json, --quiet)
|
||||||
# - (studio variant only, auto-detected) pi-studio cloned + prebuilt
|
# - (studio variant only, auto-detected) pi-studio cloned + prebuilt
|
||||||
# client bundle present + registered via `pi install`
|
# client bundle present + registered via `pi install`
|
||||||
|
# - no foreign npm-11 platform packages (@esbuild, clipboard) beyond the host
|
||||||
|
# - no build-time npm cache (/root/.npm) shipped in the image
|
||||||
# - image size within threshold
|
# - image size within threshold
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -879,6 +881,34 @@ exec_test "pi-fork extensions floor is [] (forks cannot write to the palace)" \
|
|||||||
exec_test "/tmp/sshcm dir mode 700 (ssh ControlMaster)" \
|
exec_test "/tmp/sshcm dir mode 700 (ssh ControlMaster)" \
|
||||||
'test -d /tmp/sshcm && [ "$(stat -c %a /tmp/sshcm)" = "700" ] && echo ok'
|
'test -d /tmp/sshcm && [ "$(stat -c %a /tmp/sshcm)" = "700" ] && echo ok'
|
||||||
|
|
||||||
|
# ── Build-time leftovers (npm 11 bloat sentinels) ─────────────────────
|
||||||
|
# Both of these are worth a PASS/FAIL assertion rather than a size-gate
|
||||||
|
# diagnostic, because the size gate has ~225 MB of deliberate margin: v1.9.1
|
||||||
|
# shipped +131 MB of pure build residue and stayed green. These name the
|
||||||
|
# residue directly, so a regression is legible instead of merely "bigger".
|
||||||
|
echo ""
|
||||||
|
echo "── Build-time leftovers ──"
|
||||||
|
|
||||||
|
# npm 11 installs EVERY optional platform package of a native dependency, not
|
||||||
|
# just the host's (it ignores os/cpu, --os/--cpu and npmrc os=/cpu=). Two
|
||||||
|
# families are affected and pruned in Dockerfile.variant: @esbuild/<platform>
|
||||||
|
# and @mariozechner/clipboard-<triple>. Keep-set is the host arch only, plus
|
||||||
|
# clipboard's gnu AND musl (its napi loader picks between them at runtime).
|
||||||
|
# Runs as root because the image declares no USER; that is also what lets the
|
||||||
|
# cache assertion below read /root.
|
||||||
|
run "no foreign platform packages (npm 11 sentinel)" \
|
||||||
|
'arch=$(node -p process.arch); bad=$(find /usr/lib/node_modules /opt -type d \( -regex ".*/@esbuild/[^/]+" -o -regex ".*/@mariozechner/clipboard-[^/]+" \) ! -name "linux-$arch" ! -name "clipboard-linux-$arch-gnu" ! -name "clipboard-linux-$arch-musl" -prune -print 2>/dev/null); if [ -n "$bad" ]; then echo "foreign platform dirs shipped:" >&2; echo "$bad" >&2; du -sm $bad 2>/dev/null | sort -rn | head -5 >&2; exit 1; fi; echo ok'
|
||||||
|
|
||||||
|
# The build's own npm download cache is not free: it lands in the layer that
|
||||||
|
# created it. v1.9.1 shipped 145 MB of /root/.npm/_cacache (35 MB in v1.8.14)
|
||||||
|
# — the largest single item in its +131 MB residual, and invisible to the
|
||||||
|
# size-gate diagnostics because those only looked under node_modules and /opt.
|
||||||
|
# Nothing at runtime reads it: root's cache, while the container runs as
|
||||||
|
# `developer`. NOTE the assertion must run as root or a permission error on
|
||||||
|
# mode-700 /root would make `test ! -d` pass for the wrong reason.
|
||||||
|
run "no build-time npm cache shipped (/root/.npm)" \
|
||||||
|
'test "$(id -u)" = "0" || { echo "assertion needs root to read /root" >&2; exit 1; }; if [ -e /root/.npm ]; then echo "/root/.npm shipped: $(du -sm /root/.npm | cut -f1) MB" >&2; exit 1; fi; echo ok'
|
||||||
|
|
||||||
# ── Image size ────────────────────────────────────────────────────────
|
# ── Image size ────────────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo "── Image size ──"
|
echo "── Image size ──"
|
||||||
@@ -920,9 +950,17 @@ else
|
|||||||
docker run --rm --entrypoint sh "$IMAGE" -c \
|
docker run --rm --entrypoint sh "$IMAGE" -c \
|
||||||
'du -sm /usr/lib/node_modules/* /opt/* /usr/local/share/ms-playwright 2>/dev/null | sort -rn | head -12' \
|
'du -sm /usr/lib/node_modules/* /opt/* /usr/local/share/ms-playwright 2>/dev/null | sort -rn | head -12' \
|
||||||
2>/dev/null | sed 's/^/ /' || echo " (could not inspect directories)"
|
2>/dev/null | sed 's/^/ /' || echo " (could not inspect directories)"
|
||||||
echo " ── @esbuild platform dirs (npm 11 regression sentinel) ──"
|
echo " ── build caches that should not be in the image ──"
|
||||||
|
# v1.9.1's residual was 145 MB of npm cache under /root, and the du list
|
||||||
|
# above cannot see it: it enumerates node_modules and /opt only. A gate whose
|
||||||
|
# diagnostic looks only where the bytes were LAST time sends the next reader
|
||||||
|
# spelunking again, so name the cache paths explicitly.
|
||||||
docker run --rm --entrypoint sh "$IMAGE" -c \
|
docker run --rm --entrypoint sh "$IMAGE" -c \
|
||||||
'find /usr/lib/node_modules /opt -type d -regex ".*/@esbuild/[^/]+" -printf "%f\n" 2>/dev/null | sort | uniq -c | sort -rn | head' \
|
'du -sm /root/.npm /root/.cache /tmp/node-compile-cache /home/developer/.npm 2>/dev/null | sort -rn' \
|
||||||
|
2>/dev/null | sed 's/^/ /' || true
|
||||||
|
echo " ── foreign platform dirs (npm 11 regression sentinel) ──"
|
||||||
|
docker run --rm --entrypoint sh "$IMAGE" -c \
|
||||||
|
'find /usr/lib/node_modules /opt -type d \( -regex ".*/@esbuild/[^/]+" -o -regex ".*/@mariozechner/clipboard-[^/]+" \) -printf "%f\n" 2>/dev/null | sort | uniq -c | sort -rn | head' \
|
||||||
2>/dev/null | sed 's/^/ /' || true
|
2>/dev/null | sed 's/^/ /' || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user