WSL (Windows Subsystem for Linux) 上安裝 pyenv (Python 版本快速切換器),理應簡單到像吃豆腐,更別說我在 Windows、Linux 已裝過多次,但這次我搞了快一個小時,嚴格說來是自己耍笨又學藝不精,寫篇筆記留念~

WSL 是標準 Ubuntu,理論上照著官方說明下指令應可無腦搞定。

curl -fsSL https://pyenv.run | bash # 會將 pyenv 程式裝在 ~/.pyenv 目錄
# 設定啟動腳本
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(pyenv init - bash)"' >> ~/.bashrc
source ~/.bashrc
# 安裝編譯程式庫、安裝 Python 版本...

BUT! 裝完我遇上了 -bash: /mnt/c/Users/jeffrey/.pyenv/pyenv-win/bin/pyenv: /bin/sh^M: bad interpreter: No such file or directory 錯誤訊息。

會花這麼多時間,是我沒有直接參考官方說明,心想裝在 WSL 可能有點特殊吧,於是爬文查到一篇 Using Pyenv in WSL Ubuntu 22.04 LTS to install Python 3.8,心想這篇安裝指南針對 WSL 的,應該更萬無一失。其做法如下:

curl https://pyenv.run | bash
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc
echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc
echo 'eval "$(pyenv init -)"' >> ~/.zshrc
source ~/.zshrc

我承認沒看懂就照做,加上過去對 WSL 與 Windows 的交互關聯、.bashrc/.zshrc/.profile 設定,以及 pyenv 運作原理全都不求甚解,這回遇上了問題,就是償還「知識債」的時侯惹~

由錯誤訊息可知,問題出在 Ubuntu 去呼叫了之前我在 Windows 環境玩 Python 所裝的 Windows 版 pyenv,至於為什麼 Ubuntu 會吃到 Windows 檔案,是因為在 WSL 裡 PATH 環境變數會繼承 Windows 的設定,用 echo $PATH | grep pyenv 我們可以看到 WSL 的 PATH 包含了 /mnt/c/Users/jeffrey/.pyenv/pyenv-win/bin,不僅如此,甚至連 /mnt/c/Program Files (x86)/Microsoft SQL Server/160/Tools/Binn/,SQL Server 的執行檔目錄,也在其中,可以說 WSL 的 bash 會繼承 Windows 的 PATH 環境變數。

而那段 Gist 的問題在於:1) 作者是用 zsh 才這樣寫,我跑 bash 不能直接用(我天真地以為 pyenv 有什麼魔法會用到 zsh) 2) 它用一種「聰明做法」設定 PATH 路徑 command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH",若已經可以存取 pyenv,代表 PATH 已經存在 pyenv 路徑了,就跳過將 $HOME/.pyenv/bin 加入 PATH 的動作。因為沒有在 PATH 最前方加入 $HOME/.pyenv/bin,輸入 pyenv 執行的就都是 Windows 版 /mnt/c/Users/jeffrey/.pyenv/pyenv-win/bin/pyenv...

至於 $HOME 目錄(~)下的 .zshrc/.bashrc/.profile 都可以放每次登入或啟動 Shell 時環境初始化動作,我之前沒搞懂它的關係,這回稍微做個了解。.profile 長這樣:

# ~/.profile: executed by the command interpreter for login shells.
# This file is not read by bash(1), if ~/.bash_profile or ~/.bash_login
# exists.
# see /usr/share/doc/bash/examples/startup-files for examples.
# the files are located in the bash-doc package.

# the default umask is set in /etc/profile; for setting the umask
# for ssh logins, install and configure the libpam-umask package.
#umask 022

# if running bash
if [ -n "$BASH_VERSION" ]; then
    # include .bashrc if it exists
    if [ -f "$HOME/.bashrc" ]; then
        . "$HOME/.bashrc"
    fi
fi

# set PATH so it includes user's private bin if it exists
if [ -d "$HOME/bin" ] ; then
    PATH="$HOME/bin:$PATH"
fi

# set PATH so it includes user's private bin if it exists
if [ -d "$HOME/.local/bin" ] ; then
    PATH="$HOME/.local/bin:$PATH"
fi

它只做三件事,若檔案或路徑存在,就執行 ~/.bashrc、將 ~/bin 及 ~/.local/bin 加入 $PATH。(若有設定 .bash_profile 或 .bash_login,則以其為準。因此我不用 zsh,照抄 Gist 做法純屬搞笑。)

最後,pyenv 是怎麼做到動態切換 Python 版本?關鍵它在 .bashrc 裡加入的 $(pyenv init - bash),pyenv 會在 ~/.pyenv/shims 下放入名為 pip、python 的執行腳本,而 ~/.pyenv/shims 又會被放到 PATH 的第一個,意思是當你輸入 python、pip 等指令,最先找到的會是 ~/.pyenv/shims/python 及 pip:

而 ~/.pyenv/shims/python 長這樣:

#!/usr/bin/env bash
set -e # 任何命令返回非零狀態碼時,立即退出腳本
# 如果環境變數 PYENV_DEBUG 有值,顯示每個執行的命令
[ -n "$PYENV_DEBUG" ] && set -x
# $0 是腳本名稱,${0##*/} 使用參數展開,移除 */ 前綴只留檔名
program="${0##*/}"

export PYENV_ROOT="/home/jeffrey/.pyenv"
# 呼叫 ~/.pyenv/libexec/pyenv,參數分別為 exec、腳本名稱、所有原始參數
exec "/home/jeffrey/.pyenv/libexec/pyenv" exec "$program" "$@"

現在我們都知道 pyenv 的祕密了 PO PO...

「沒搞懂桯式原理,只知道這樣會動就用了」不就是現在當紅 Vibe Coding 寫照,下場也一樣,一旦結果未如預期,也沒有能力當場判斷與處理。此時你可以繼續爬文或問 AI,走神農氏嚐百草路線,繼續找來更多看不懂的東西瞎試;或是,試著搞懂原理,改用有邏輯的科學方法挖掘真相找到答案。有茶包射手魂的人,應當成為後者。

耍了一次呆,花了一些時間,但補學到 WSL 繼承行為、.bashrc/.profile 觀念、pyenv 魔法的原理這些該學到的東西,不算浪費時間,也是很棒的一次經驗。


Comments

# by Curry

推薦使用 uv ,安裝速度快,而且也可以避免這樣的問題

# by simsek

現在改用uv,一次解決版本以及套件問題

Post a comment