近黑色卡片上的 pnpm 標誌,白色外框的正方形分割成 3x3 網格,由橘色與深灰色方塊組成,其中一格留白,旁邊是白色的 pnpm 字樣

pnpm 和 Windows 讓這個部落格的建置壞了兩次 — 先是 Chocolatey 安裝被蓋掉,後是符號連結被擋下

pnpm 的版本切換器,在一個被蓋掉的 Chocolatey 安裝背後弄壞了快取;Windows 另外也擋下了建置時對依賴項目建立符號連結。

更新

本頁目錄

引言

在為這個儲存庫裡另一篇文章正規化一張新的縮圖時,pnpm thumbnails:fix 徹底壞掉了:不是一般的指令失敗,而是 pnpm 在任何 shell 裡、對任何指令都完全拒絕執行。這連帶擋住了後續的一切,包括這個部落格自己的流程在把草稿視為已檢查之前要求的驗證關卡。第一個直覺的猜測是安裝過期或損壞,而這個猜測錯得很有啟發性:真正的原因是 pnpm 自己那套自我管理的版本切換機制,沒能完成自身的安裝,再加上某個 shell 的 PATH 裡,還有一個被遺忘、排在剛修好的那份之前的第二個 pnpm 安裝。本文說明那次自我切換失敗長什麼樣子、它是一個已知上游臭蟲而非這台機器特有問題的證據、被蓋掉的安裝如何把原本一行就能解決的修正變成一場多步驟的診斷、那次修正為什麼沒能撐過下一個工作階段,以及後來在同一台機器上、公開另一篇文章時遇到的第二個、與此無關的 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 incorrect
spawnSync 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 15
drwxr-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

自我安裝在放上執行檔之前會寫入的每一樣東西都在,唯獨讓這次安裝真正可用的那一個檔案不在。 重新執行 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 機器,安裝方式、作業系統、歷史背景沒有一項相同,卻撞上了同樣措辭的同樣失敗,原因是它們唯一共有、也是真正重要的那一件事:切換器自己下載並解壓縮一個釘選版本的那套邏輯。

修正,以及沒清掉的殘影

當代理程式開始嘗試用環境變數的權宜做法,在沒有先說明的情況下試圖繞過這個壞掉的狀態時,我讓它停下來,問了為什麼。這個提問改變了修正的方向:與其繞過壞掉的 pnpm,我要求代理程式好好重新安裝 pnpm,並核准直接執行官方安裝程式:

終端機視窗
iwr https://get.pnpm.io/install.ps1 -useb | iex

這產生了一個全新的 shim 和乾淨的 PNPM_HOME。在一個已經載入更新後 PATH 的 PowerShell 工作階段裡,pnpm --version 乾淨地印出了 11.3.0。在那個 shell 裡,修正是有效的。

接著在另一個 Bash 工作階段執行的 pnpm install,卻出現一模一樣的 ENOENT 錯誤,參照的是完全相同的損壞快取路徑,彷彿什麼都沒重新安裝過一樣。這時候的反射動作,是懷疑重新安裝沒有生效,或是快取第二次自己壞掉了。兩者都不是:

終端機視窗
which pnpm
# /c/ProgramData/chocolatey/bin/pnpm

Bash 執行的並不是剛剛修好的那個 pnpm。它執行的是一個完全不同、透過 Chocolatey 安裝的舊版本,在 Bash 的 PATH 上排在新版本之前,而且舊到仍然會被同樣壞掉的切換器行為影響。這個證據其實早就出現過一次,就在最初的診斷階段,出現在和原始錯誤一起執行的 which pnpm 檢查裡。當時它被讀過去卻沒被注意到,因為又大聲又醒目的 ENOENT 區塊是理所當然該回應的對象,旁邊那條不起眼的檔案路徑則不是。

為什麼 PowerShell 和 Bash 解析到兩個不同的 pnpm 執行檔左右並排兩張卡片。左側卡片是 PowerShell,其 PATH 搜尋順序中全新安裝的 pnpm 資料夾排在 Chocolatey 資料夾之前,因此 pnpm 解析到快取正常、版本為 11.3.0 的全新安裝。右側卡片是 Bash,同樣兩個資料夾順序相反,Chocolatey 資料夾排在前面,因此 pnpm 解析到快取損壞的舊版 Chocolatey 安裝。兩個 shell 搜尋的是同樣兩個資料夾,差別只在順序,而這個順序決定了每個 shell 實際執行的是哪一個 pnpm 執行檔。同一個 PATH 變數,搜尋順序卻不同PowerShell$env:Path 搜尋順序...\pnpm\bin(全新安裝)...\chocolatey\binpnpm 解析到全新安裝,11.3.0 正常Bash(Git Bash)$PATH 搜尋順序/c/ProgramData/chocolatey/bin.../pnpm/bin(全新安裝)pnpm 解析到Chocolatey 安裝,快取損壞
兩個 shell 搜尋的都是 PATH 上同樣的兩個資料夾。差別只在順序,而這個順序決定了每一個 shell 實際執行的是哪一個 pnpm 執行檔。

一旦看清楚原因,修正就只有一行:把正確安裝的 bin 資料夾,在那個 shell 的 PATH 裡排到 Chocolatey 的前面。

終端機視窗
export PATH="/c/Users/<user>/AppData/Local/pnpm/bin:$PATH"
which pnpm
# /c/Users/<user>/AppData/Local/pnpm/bin/pnpm

驗證

解析到正確的執行檔之後,pnpm install 順利完成,在這台機器上第一次填滿了 node_modulespnpm thumbnails:fix 也真正執行了正規化指令碼,而不是在開始前就失敗。兩個 shell 裡的 pnpm --version 現在一致回報 11.3.0,而且每次都是來自同一個安裝。

另一個無關的 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 對標準帳戶預設會擋下的那個操作。修正的方法是開發人員模式,不是提升權限的 shell:設定 → 隱私權與安全性 → 開發人員專用 → 開發人員模式,會授予一個非管理員工作階段原本沒有的 SeCreateSymbolicLinkPrivilege,而在那裡把它打開(完全不必以系統管理員身分執行任何東西)就足夠了。它所設定的登錄檔機碼 HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock\AllowDevelopmentWithoutDevLicense,值得在認定符號連結失敗需要完整提升權限的工作階段之前先確認一下:不存在或為 0,就代表開發人員模式是關閉的,光是這一點就重現了這次建置失敗。

同一次建置在當機前,還印出了第二個容易被混為一談的警告:

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 錯誤才讓它停下來,這讓人很容易把兩者讀成同一個失敗,但其實它們毫不相干。Node 版本不符是一個 Astro 的 Vercel 轉接器會自動降級繞過的執行環境支援警告;符號連結錯誤則是一個 Windows 權限上的落差,跟目前執行的是哪一個 Node 版本完全無關。

那次修正沒能撐住

先前修好被蓋掉安裝的那個 export,修好的只是執行它的那個 shell,再往後就沒有作用。 shell 範圍內的環境變數不會延續到新的終端機,所以隔天,一個全新的工作階段又撞上了完全相同的錯誤:

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 incorrect
spawnSync C:\Users\<user>\AppData\Local\pnpm\.tools\@pnpm+win-x64\11.3.0\bin\pnpm ENOENT

代理程式把先前的診斷從頭做了一遍:刪掉損壞的 .tools\@pnpm+win-x64\11.3.0 資料夾再重試,藉此確認這究竟是一次新的下載中斷,還是同一個決定論式的失敗。結果是同一個失敗。在一個沒有覆寫設定的 shell 裡,where pnpm 再次把 Chocolatey 的執行檔列在最前面,而直接用完整路徑執行 AppData 底下的安裝,則乾淨地切換成了 11.3.0能正常運作的那份安裝從來沒有壞過。它只是一直被排到後面,而先前的修正也只改了其中一個 shell 的排序。

這一次的修正把目標對準了那個互相競爭的安裝本身:choco uninstall pnpm -y。第一次嘗試同樣失敗,出現了 UnauthorizedAccessException: Access to the path 'C:\ProgramData\chocolatey\bin\pnpm.exe' is denied。Chocolatey 的檔案放在 C:\ProgramData 底下,要移除它們需要一個提升權限的 shell,而這是代理程式自己開不了的。我自己從以系統管理員身分開啟的 PowerShell 執行了這次解除安裝。Chocolatey 的套件移除之後,where pnpm 現在在每一個 shell 裡都只會回傳一條路徑,不再需要記得帶著 export 走。

總結

「是一個過期的 Chocolatey 安裝搞的鬼」這個說法是錯的,而且值得把這個錯誤大聲說出來。 真正壞掉的,是 pnpm 自己那套自我管理的版本切換器,沒能完成一個釘選版本的安裝,這是一個橫跨互不相干的各種環境、以完全相同錯誤文字被記錄下來的臭蟲。讓這次修正花了兩輪而不是一輪的原因,是某個 shell 的 PATH 上排得更前面的另一個舊版 pnpm,而這個證據其實早在第一個診斷指令就已經浮現,只是沒被讀到。重新安裝 pnpm 修好了切換器。如果在相信 shell 回報的版本號之前先確認一下 which pnpm,本來在第一輪就能抓到那個被蓋掉的安裝,而不必等到第二輪。**PATH 的重新排序只作用在執行它的那個 shell,所以它從一開始就不可能是持久的修正。**真正持久的修正,是在一個提升權限的 shell 裡,把蓋掉它的那個安裝整個移除。

參考連結

分享這篇文章