From 2a122e52509f21d6d3a74a3061ca06a673d75b34 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 23:19:04 +0000 Subject: [PATCH] Fixed arm32v7 builds by installing with pnpm 11 refs https://github.com/TryGhost/docker-library-ghost/issues/485 - pnpm 12 is a Rust binary that publishes no 32-bit ARM target, and unlike every pnpm before it there is no JavaScript implementation to fall back to: the npm package is only a downloader for "@pnpm/exe.-". corepack therefore cannot produce the version "packageManager" pins when building for arm32v7, which is what broke bookworm and next-bookworm - pnpm 11 is the last JavaScript implementation, is still released alongside 12 ("latest-11"), and reads the same lockfile format, so arm32v7 installs with it while every other architecture keeps corepack and the pinned pnpm 12 - set pnpm_config_pm_on_fail=ignore wherever pnpm runs: pnpm 11 otherwise honours "packageManager" and switches itself straight back to the binary that arch has none of. Exported rather than passed per invocation because Ghost-CLI spawns pnpm itself, and inert wherever corepack is in charge since corepack owns version switching - pinned the fallback in versions.json so the update workflow refreshes it like every other version - kept yarn on corepack in the -next variant so "rm -rf /opt/yarn-*" does not leave its symlinks dangling on arm32v7 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0148usZsXVFxtb8bPj9LXcJo --- 6-next/alpine3.23/Dockerfile | 16 ++++++++++++++-- 6-next/bookworm/Dockerfile | 16 ++++++++++++++-- 6/alpine3.23/Dockerfile | 16 +++++++++++++++- 6/bookworm/Dockerfile | 16 +++++++++++++++- Dockerfile-next.template | 16 ++++++++++++++-- Dockerfile.template | 16 +++++++++++++++- versions.json | 6 ++++++ versions.sh | 14 +++++++++++++- 8 files changed, 106 insertions(+), 10 deletions(-) diff --git a/6-next/alpine3.23/Dockerfile b/6-next/alpine3.23/Dockerfile index 40e0bcf7..59e3961b 100644 --- a/6-next/alpine3.23/Dockerfile +++ b/6-next/alpine3.23/Dockerfile @@ -50,8 +50,20 @@ ENV GHOST_TARBALL=https://github.com/TryGhost/Ghost/releases/download/v6.63.0/gh ENV GHOST_SHA256=20b2ac6e936299d5b88b37d2a7175712ad35e6ade78a594ab6d495256ab54292 RUN set -eux; \ -# Ghost pins its pnpm by hash in "packageManager", so corepack fetches exactly that one - corepack enable; \ +# Ghost pins its pnpm by hash in "packageManager", so corepack fetches exactly that one -- except on +# arm32v7, where pnpm 12 is a Rust binary with no 32-bit ARM target and no JavaScript fallback. +# pnpm 11 is the last JavaScript implementation and reads the same lockfile format + if [ "$(node -p 'process.arch')" = 'arm' ]; then \ +# yarn still goes to corepack so "rm -rf /opt/yarn-*" below does not leave its symlinks dangling + corepack enable yarn; \ + npm install -g 'pnpm@11.26.0'; \ + else \ + corepack enable; \ + fi; \ + \ +# pnpm 11 would otherwise honour "packageManager" and switch back to the binary it cannot run. +# Inert under corepack, which owns version switching; exported so it survives the "gosu ghost" drop + export pnpm_config_pm_on_fail=ignore; \ \ # everything under the install dir is created by "ghost" rather than chowned afterwards: a recursive # chown would copy the whole tree into a new layer diff --git a/6-next/bookworm/Dockerfile b/6-next/bookworm/Dockerfile index 25fa0e0d..20c24901 100644 --- a/6-next/bookworm/Dockerfile +++ b/6-next/bookworm/Dockerfile @@ -67,8 +67,20 @@ RUN set -eux; \ python3 \ ; \ \ -# Ghost pins its pnpm by hash in "packageManager", so corepack fetches exactly that one - corepack enable; \ +# Ghost pins its pnpm by hash in "packageManager", so corepack fetches exactly that one -- except on +# arm32v7, where pnpm 12 is a Rust binary with no 32-bit ARM target and no JavaScript fallback. +# pnpm 11 is the last JavaScript implementation and reads the same lockfile format + if [ "$(node -p 'process.arch')" = 'arm' ]; then \ +# yarn still goes to corepack so "rm -rf /opt/yarn-*" below does not leave its symlinks dangling + corepack enable yarn; \ + npm install -g 'pnpm@11.26.0'; \ + else \ + corepack enable; \ + fi; \ + \ +# pnpm 11 would otherwise honour "packageManager" and switch back to the binary it cannot run. +# Inert under corepack, which owns version switching; exported so it survives the "gosu ghost" drop + export pnpm_config_pm_on_fail=ignore; \ \ # everything under the install dir is created by "ghost" rather than chowned afterwards: a recursive # chown would copy the whole tree into a new layer diff --git a/6/alpine3.23/Dockerfile b/6/alpine3.23/Dockerfile index 6f8fda2f..4314282e 100644 --- a/6/alpine3.23/Dockerfile +++ b/6/alpine3.23/Dockerfile @@ -52,7 +52,18 @@ RUN set -eux; \ \ apk add --no-cache --virtual .ghost-cli-deps git; \ \ - corepack enable; \ +# pnpm 12 is a Rust binary with no 32-bit ARM target and no JavaScript fallback, so corepack cannot +# produce the version "packageManager" pins on arm32v7. pnpm 11 is the last JavaScript +# implementation and reads the same lockfile format, so that arch installs with it instead + if [ "$(node -p 'process.arch')" = 'arm' ]; then \ + npm install -g 'pnpm@11.26.0'; \ + else \ + corepack enable; \ + fi; \ + \ +# pnpm 11 would otherwise honour "packageManager" and switch back to the binary it cannot run; +# exported because Ghost-CLI spawns pnpm itself. Inert under corepack, which owns version switching + export pnpm_config_pm_on_fail=ignore; \ \ # corepack's downloads, pnpm's metadata cache and pnpm's store all derive their location # from these, so pointing them at /tmp keeps every throwaway byte in one place @@ -92,6 +103,9 @@ RUN set -eux; \ mkdir -p "$GHOST_INSTALL"; \ chown node:node "$GHOST_INSTALL"; \ \ +# "ghost install" and "pnpm store prune" below both run pnpm; see the note in the Ghost-CLI stage + export pnpm_config_pm_on_fail=ignore; \ + \ apk add --no-cache --virtual .build-deps-ghost g++ linux-headers make python3 py3-setuptools; \ \ gosu node ghost install "$GHOST_VERSION" --db mysql --dbhost mysql --no-prompt --no-stack --no-setup --dir "$GHOST_INSTALL"; \ diff --git a/6/bookworm/Dockerfile b/6/bookworm/Dockerfile index 452e2ea5..57e33f64 100644 --- a/6/bookworm/Dockerfile +++ b/6/bookworm/Dockerfile @@ -51,7 +51,18 @@ RUN set -eux; \ apt-get update; \ apt-get install -y --no-install-recommends ca-certificates git; \ \ - corepack enable; \ +# pnpm 12 is a Rust binary with no 32-bit ARM target and no JavaScript fallback, so corepack cannot +# produce the version "packageManager" pins on arm32v7. pnpm 11 is the last JavaScript +# implementation and reads the same lockfile format, so that arch installs with it instead + if [ "$(node -p 'process.arch')" = 'arm' ]; then \ + npm install -g 'pnpm@11.26.0'; \ + else \ + corepack enable; \ + fi; \ + \ +# pnpm 11 would otherwise honour "packageManager" and switch back to the binary it cannot run; +# exported because Ghost-CLI spawns pnpm itself. Inert under corepack, which owns version switching + export pnpm_config_pm_on_fail=ignore; \ \ # corepack's downloads, pnpm's metadata cache and pnpm's store all derive their location # from these, so pointing them at /tmp keeps every throwaway byte in one place @@ -94,6 +105,9 @@ RUN set -eux; \ mkdir -p "$GHOST_INSTALL"; \ chown node:node "$GHOST_INSTALL"; \ \ +# "ghost install" and "pnpm store prune" below both run pnpm; see the note in the Ghost-CLI stage + export pnpm_config_pm_on_fail=ignore; \ + \ savedAptMark="$(apt-mark showmanual)"; \ apt-get update; \ apt-get install -y --no-install-recommends g++ make python3; \ diff --git a/Dockerfile-next.template b/Dockerfile-next.template index 3bca6203..c734c596 100644 --- a/Dockerfile-next.template +++ b/Dockerfile-next.template @@ -86,8 +86,20 @@ RUN set -eux; \ ; \ \ {{ ) end -}} -# Ghost pins its pnpm by hash in "packageManager", so corepack fetches exactly that one - corepack enable; \ +# Ghost pins its pnpm by hash in "packageManager", so corepack fetches exactly that one -- except on +# arm32v7, where pnpm 12 is a Rust binary with no 32-bit ARM target and no JavaScript fallback. +# pnpm 11 is the last JavaScript implementation and reads the same lockfile format + if [ "$(node -p 'process.arch')" = 'arm' ]; then \ +# yarn still goes to corepack so "rm -rf /opt/yarn-*" below does not leave its symlinks dangling + corepack enable yarn; \ + npm install -g 'pnpm@{{ .pnpm.fallbackVersion }}'; \ + else \ + corepack enable; \ + fi; \ + \ +# pnpm 11 would otherwise honour "packageManager" and switch back to the binary it cannot run. +# Inert under corepack, which owns version switching; exported so it survives the "gosu ghost" drop + export pnpm_config_pm_on_fail=ignore; \ \ # everything under the install dir is created by "ghost" rather than chowned afterwards: a recursive # chown would copy the whole tree into a new layer diff --git a/Dockerfile.template b/Dockerfile.template index f717c17a..d0e03a83 100644 --- a/Dockerfile.template +++ b/Dockerfile.template @@ -78,7 +78,18 @@ RUN set -eux; \ apt-get install -y --no-install-recommends ca-certificates git; \ {{ ) end -}} \ - corepack enable; \ +# pnpm 12 is a Rust binary with no 32-bit ARM target and no JavaScript fallback, so corepack cannot +# produce the version "packageManager" pins on arm32v7. pnpm 11 is the last JavaScript +# implementation and reads the same lockfile format, so that arch installs with it instead + if [ "$(node -p 'process.arch')" = 'arm' ]; then \ + npm install -g 'pnpm@{{ .pnpm.fallbackVersion }}'; \ + else \ + corepack enable; \ + fi; \ + \ +# pnpm 11 would otherwise honour "packageManager" and switch back to the binary it cannot run; +# exported because Ghost-CLI spawns pnpm itself. Inert under corepack, which owns version switching + export pnpm_config_pm_on_fail=ignore; \ \ # corepack's downloads, pnpm's metadata cache and pnpm's store all derive their location # from these, so pointing them at /tmp keeps every throwaway byte in one place @@ -125,6 +136,9 @@ RUN set -eux; \ mkdir -p "$GHOST_INSTALL"; \ chown node:node "$GHOST_INSTALL"; \ \ +# "ghost install" and "pnpm store prune" below both run pnpm; see the note in the Ghost-CLI stage + export pnpm_config_pm_on_fail=ignore; \ + \ {{ if is_alpine then ( -}} apk add --no-cache --virtual .build-deps-ghost g++ linux-headers make python3 py3-setuptools; \ {{ ) else ( -}} diff --git a/versions.json b/versions.json index 616ae914..1e872e4a 100644 --- a/versions.json +++ b/versions.json @@ -8,6 +8,9 @@ "node": { "version": "22" }, + "pnpm": { + "fallbackVersion": "11.26.0" + }, "variants": { "bookworm": { "arches": [ @@ -36,6 +39,9 @@ "node": { "version": "22" }, + "pnpm": { + "fallbackVersion": "11.26.0" + }, "variants": { "bookworm": { "arches": [ diff --git a/versions.sh b/versions.sh index bb7921b0..ab2aec22 100755 --- a/versions.sh +++ b/versions.sh @@ -73,6 +73,17 @@ if [ -z "$cliSha" ]; then exit 1 fi +# pnpm 12 is a Rust binary and publishes no 32-bit ARM target, and unlike every pnpm before it +# there is no JavaScript implementation to fall back to -- the npm package is a downloader that +# fetches "@pnpm/exe.-". So corepack cannot produce the version "packageManager" +# pins when building for arm32v7. pnpm 11 is the last JavaScript implementation, is still released +# alongside 12 ("latest-11"), and reads the same lockfile format, so that architecture builds with +# it instead. Pinned here so it is refreshed by the update workflow like every other version. +pnpmFallbackVersion="$( + fetch 'https://registry.npmjs.org/pnpm' '."dist-tags"."latest-11" // empty' \ + | jq --raw-output '."dist-tags"."latest-11"' +)" + for version in "${versions[@]}"; do rcVersion="${version%-rc}" rcGrepV='-v' @@ -195,7 +206,7 @@ for version in "${versions[@]}"; do '{ cli: { version: $version, sha: $sha } }')" fi - export fullVersion nodeVersion + export fullVersion nodeVersion pnpmFallbackVersion json="$(jq <<<"$json" --compact-output --argjson doc "$doc" --argjson source "$sourceJson" ' env.nodeVersion as $nodeVersion | .[env.version] = ( @@ -203,6 +214,7 @@ for version in "${versions[@]}"; do + $source + { node: { version: $nodeVersion }, + pnpm: { fallbackVersion: env.pnpmFallbackVersion }, variants: ( $doc | with_entries(