
pnpmとWindowsが、このブログのビルドを2度壊した — シャドウ化したChocolateyインストールと、ブロックされたシンボリックリンク
pnpmのバージョンスイッチャーは、シャドウ化したChocolateyインストールの裏でキャッシュを破損させました。Windowsは別途、ビルドが依存関係をシンボリックリンクするのをブロックしました。
目次
はじめに
このリポジトリにある別の記事のために新しいサムネイル画像を正規化していたとき、pnpm thumbnails:fixが完全に壊れました。単なる通常のコマンド失敗ではなく、pnpmがどのシェルでもどのコマンドに対しても一切動かなくなるという壊れ方でした。これにより、このブログ自身のパイプラインがドラフトをチェック済みとみなす前に要求する検証ゲートを含め、後続のすべてがブロックされました。最初に思いついたのは古い、あるいは壊れたインストールという説明でしたが、その推測は示唆に富む形で外れていました。実際の原因は、pnpm自身のセルフマネージドなバージョン切り替え機能が、自分自身のインストールを完了できずにいたことでした。それに加えて、たった今修復したばかりのコピーよりも先にあるシェルのPATH上に、忘れられていた2つ目のpnpmインストールが座っていたことも重なりました。本記事では、そのセルフスイッチの失敗がどう見えるか、それがこのマシン特有の問題ではなく既知の上流バグである証拠、シャドウ化したインストールがどのように一行で済むはずの修正を複数ステップの診断に変えたか、その修正がなぜ次のセッションまで持たなかったか、そして同じマシンで後日、別の記事を公開している最中に遭遇した、もう1つの無関係なWindowsの失敗、すなわちビルドが自身の依存関係のシンボリックリンク作成をブロックされた件について整理します。
スイッチャー自体が壊れていた
pnpm thumbnails:fixは、裏側のスクリプトを一行も実行しないうちに失敗しました。
[WARN] Unsupported engine: wanted: {"node":">=24"} (current: {"node":"v22.12.0","pnpm":"11.3.0"}) ERROR Failed to switch pnpm to v11.3.0. Looks like pnpm CLI is missing at "C:\Users\<user>\AppData\Local\pnpm\.tools\@pnpm+win-x64\11.3.0\bin" or is incorrectspawnSync C:\Users\<user>\AppData\Local\pnpm\.tools\@pnpm+win-x64\11.3.0\bin\pnpm ENOENTこのリポジトリのpackage.jsonは"packageManager": "pnpm@11.3.0"を固定しています。最近のpnpmはこのフィールドを自分で読み取り、指定されたバージョンがユーザーごとのツールキャッシュにまだなければ、その場でダウンロードしてインストールしてから、そちらへ再実行します。このキャッシュはPNPM_HOME/.tools/@pnpm+win-x64/<version>/にあり、これはpnpmが自分自身のバージョンを管理しているのであって、Corepackのような別のツールがやっているわけではありません。
11.3.0のキャッシュフォルダ自体は存在していました。ただbinサブフォルダを含んでいなかっただけです。
total 15drwxr-xr-x 1 <user> 197609 0 Aug 22 10:11 .drwxr-xr-x 1 <user> 197609 0 Aug 22 10:11 ..drwxr-xr-x 1 <user> 197609 0 Aug 22 10:11 node_modules-rw-r--r-- 1 <user> 197609 43 Aug 22 10:11 package.json-rw-r--r-- 1 <user> 197609 438 Aug 22 10:11 pnpm-lock.yaml-rw-r--r-- 1 <user> 197609 39 Aug 22 10:11 pnpm-workspace.yamlセルフインストールが実行ファイルを置く前に書き込むはずのものはすべて揃っていましたが、そのインストールを使えるものにする1つのファイルだけが欠けていました。 pnpm --versionを再実行しても同じエラーが再現され、これにより一時的なネットワークの不調である可能性は排除されました。セルフマネージドなスイッチャーは、自分では回復できない状態に自身のキャッシュを残してしまっており、以降どの物理的なpnpmバイナリから呼び出されても、すべての呼び出しが同じ欠けたパスに突き当たりました。
たまたまでも、このマシンのせいでもない
この時点での直感は、インストール自体を疑うことでした。その直感のまま動く前に軽く検索してみたところ、診断は変わりました。**このエラーは一言一句そのまま、互いに何の共通点もない環境の間で繰り返し報告されています。**RenovateのDockerイメージ、Render.comのデプロイ、そしていくつかの独立したプロジェクトが、いずれも同一のFailed to switch pnpm to v<X>. Looks like pnpm CLI is missing... ENOENTというメッセージを報告しており、これは複数のポイントリリースにまたがる、pnpmのオープンなissueで追跡されています。
**壊れやすいのはpnpm自身のセルフマネージドなバージョンスイッチャーであって、pnpmの特定のインストール方法ではありません。**Dockerイメージ、CIランナー、そして個人のWindowsマシンは、インストール方法もOSも履歴も何一つ共有していませんが、それでも同じ文言の同じ失敗にぶつかります。それは、これらすべてが唯一共有しているもの、つまりピン留めされたバージョンをダウンロードして展開するスイッチャー自身のロジックのためです。
修正と、それでも消えなかったシャドウ
エージェントが壊れた状態を、事前の説明なしに環境変数によるワークアラウンドで回避しようと試み始めたとき、私はそれを止めて理由を尋ねました。その問いかけが修正の方向を変えました。壊れたpnpmを回避するのではなく、pnpmを正しく再インストールするようエージェントに求め、公式インストーラーを直接実行することを承認しました。
iwr https://get.pnpm.io/install.ps1 -useb | iexこれにより新しいシムと、クリーンなPNPM_HOMEが作られました。更新されたPATHを読み込んだPowerShellセッションでは、pnpm --versionが11.3.0ときれいに表示され、そのシェルでは修正は機能しました。
続けて別のBashセッションで実行したpnpm installは、まったく何も再インストールされていないかのように、まったく同じENOENTエラーを、まったく同じ壊れたキャッシュパスを参照して出しました。この時点での反射的な反応は、再インストールがうまくいっていなかったのではないか、あるいはキャッシュが2度目に自壊したのではないかと疑うことです。どちらも違いました。
which pnpm# /c/ProgramData/chocolatey/bin/pnpmBashが実行していたのは、たった今修復したばかりのpnpmではありませんでした。Chocolatey経由でインストールされた、まったく別の古いインストールを実行しており、それはBashのPATH上で新しいものより先に位置していて、なおかつ同じ壊れたスイッチャーの挙動にさらされ続けるほど古いものでした。この証拠は、実は最初の診断の時点で一度すでに現れていました。最初のエラーと並んで実行したwhich pnpmのチェックの中にです。当時はそれと気づかれないまま読み飛ばされました。うるさいENOENTのブロックが反応すべき明白な対象であり、その隣にある地味な1本のファイルパスはそうではなかったからです。
原因が見えてしまえば、修正はたった1行でした。そのシェルのPATHで、正しいインストールのbinフォルダをChocolateyのものより前に置くことです。
export PATH="/c/Users/<user>/AppData/Local/pnpm/bin:$PATH"which pnpm# /c/Users/<user>/AppData/Local/pnpm/bin/pnpm動作確認
正しいバイナリが解決されるようになると、pnpm installが完了してこのマシンで初めてnode_modulesが生成され、pnpm thumbnails:fixも開始前に失敗することなく正規化スクリプトを実行しました。両方のシェルでpnpm --versionは今や11.3.0で一致しており、毎回同じインストールから返ってきます。
もう1つの無関係なWindowsの失敗:ビルドが自身の依存関係のシンボリックリンク作成をブロックされる
後日、同じマシン上で、別の記事を公開している最中に、これとは無関係な失敗が表面化しました。pnpm buildのVercelアダプターが、サーバーバンドル自体はすでに正常にビルドし終えた後の途中でクラッシュしたのです。
EPERM: operation not permitted, symlink '.pnpm\sharp@0.35.3_@types+node@24.13.3\node_modules\sharp' -> 'C:\repositories\personal\oharu-tech-blog\.vercel\output\functions\_render.func\node_modules\sharp'このアダプターは、サーバーレス関数の依存関係をpnpmのコンテンツアドレス方式のストアからシンボリックリンクすることでバンドルしており、シンボリックリンクの作成はまさに、Windowsが標準アカウントに対してデフォルトでブロックしている操作です。**修正はDeveloper Modeであって、昇格されたシェルではありません。**設定 → プライバシーとセキュリティ → 開発者向け → 開発者モードは、非管理者セッションには本来ないSeCreateSymbolicLinkPrivilegeを付与し、そこでオンにするだけで十分でした(管理者として何かを実行する必要はありませんでした)。それが設定するレジストリキーHKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock\AllowDevelopmentWithoutDevLicenseは、シンボリックリンクの失敗にフル昇格セッションが必要だと決めつける前に確認する価値があります。存在しないか0であれば開発者モードはオフであることを意味し、それだけでこのビルドを再現できました。
同じビルドは、クラッシュの直前に、混同しやすいもう1つの警告を出していました。
The local Node.js version (26) is not supported by Vercel Serverless Functions.Your project will use Node.js 24 as the runtime instead.Consider switching your local version to 24.Node 26は、このマシンのCurrentリリースラインであって、Vercelの関数が実際に動くLTSラインではありません。この警告はビルドを止めませんでしたが、その直後のEPERMエラーは止めました。そのため、両者を無関係な別々の失敗ではなく1つの失敗として読んでしまいやすくなっていました。Nodeのミスマッチは、AstroのVercelアダプターがダウングレードして回避するランタイムサポートの警告であり、シンボリックリンクのエラーは、どのNodeバージョンが動いているかとは無関係なWindowsの権限の問題です。
その修正は定着しなかった
シャドウ化したインストールを直したあのexportは、それを実行したシェルだけを直し、それ以外には及びませんでした。 シェルスコープの環境変数は新しいターミナルには引き継がれないため、翌日、新しいセッションで同一のエラーが再び発生しました。
ERROR Failed to switch pnpm to v11.3.0. Looks like pnpm CLI is missing at "C:\Users\<user>\AppData\Local\pnpm\.tools\@pnpm+win-x64\11.3.0\bin" or is incorrectspawnSync C:\Users\<user>\AppData\Local\pnpm\.tools\@pnpm+win-x64\11.3.0\bin\pnpm ENOENTエージェントは、これが新たな途中失敗のダウンロードなのか、それとも同じ決定論的な失敗なのかを確認するため、以前の診断を最初からやり直しました。壊れた.tools\@pnpm+win-x64\11.3.0フォルダを削除して再試行したのです。結果は同じ失敗でした。上書き設定を持たないシェルでのwhere pnpmは、再びChocolateyのバイナリを先頭に挙げ、AppDataのインストールをフルパスで直接実行すると11.3.0へきれいに切り替わりました。動作するインストールは一度も壊れていませんでした。順位を下げられていただけであり、以前の修正はその順位を1つのシェルについてしか変えていませんでした。
今回の修正は、競合するインストール自体を対象にしました。choco uninstall pnpm -yです。これも最初の試行は失敗し、UnauthorizedAccessException: Access to the path 'C:\ProgramData\chocolatey\bin\pnpm.exe' is deniedというエラーが出ました。ChocolateyのファイルはC:\ProgramData配下にあり、削除するにはエージェントが自分では開けない昇格されたシェルが必要です。私自身が、管理者として開いたPowerShellからアンインストールを実行しました。Chocolateyのパッケージがなくなったことで、where pnpmはどのシェルでも常に1つのパスだけを返すようになり、持ち越すべきexportもなくなりました。
まとめ
「古いChocolateyインストールが原因だった」という見立ては誤りであり、それを声に出して間違いだったと認める価値があります。 実際に壊れていたのは、pnpm自身のセルフマネージドなバージョンスイッチャーが、ピン留めされたバージョンのインストールを完了できずにいたことで、これは同一のエラー文言で、互いに無関係な環境をまたいで報告されているバグです。修正が1回で済まず2回かかった原因は、あるシェルのPATHでより先に座っていた、もう1つの古いpnpmであり、その証拠はすでに最初の診断コマンドの時点で表面化していながら読まれていませんでした。pnpmを再インストールしたことでスイッチャーは修正されました。シェルが報告するバージョン番号を信用する前に、まずwhich pnpmを確認していれば、シャドウ化したインストールを2回目ではなく1回目で見つけられていたはずです。**PATHの並べ替えは、それを実行したシェルの範囲に閉じているため、恒久的な修正には最初からなり得ませんでした。**恒久的な修正だったのは、昇格されたシェルでシャドウ化していたインストールをまるごと削除することでした。



