Apple M1 system's Pyinstaller operating procedures

前言

最近將電腦換成了 MacBook Pro M1 Max,在使用 PyInstaller 轉成執行檔發現了一系列的問題,為了防止記憶隨著時間消散,在這邊特別紀錄一下,也供給其他人參考一下。

由於 M1 Chip 是 ARM 64 的架構,所以 Python 的 Libary 有許多未支援或修正的地方,所幸 Apple 早想到了這個問題,提出可同時支援這兩個平台架構的編譯程式 Universal 2,我們只要運用 Universal 2 所編譯出來的程式,即可無痛在 ARM 和 Intel 架構下執行!

接下來說明如何使用 Universal 2 編譯 Python 程式

1. 環境設置

Python 3.9.1 在 2021 年 12 月 7 日正式釋出,它是第一個支持 Universal 2 的 Python 版本,而目前在最新的 macOS Monterey 12.3.1 版本中,系統內建的 Python 版本為 3.8.9,也就是說在最新的系統內建 Python 環境下,是無法使用 Universal 2 進行編譯的。

為了解決這個問題,我使用的方法是利用 Pyenv 建立一個虛擬環境,重新打造一個支援 Universal 2 的 Pyhton 環境。

  1. 安裝 Homebrew
    在執行所有作業前,需要先安裝 Homebrew,來作為後續安裝的基礎,而 Homebrew 目前也已經支援 M1 Chip,大部分的 Bug 也陸續得到了修復,可以正常使用,其安裝指令如下

    1
    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
  2. 安裝 Pyenv 和 Pyenv-Virtualenv
    接下來利用 Homebrew 來安裝 PyenvPyenv-Virtualenv,指令分別如下

    1
    brew install pyenv

    設置環境變數如下 (由於我使用的 Shell 是 ZSH,所以我是設置在.zshrc,請依照你所使用的 Shell 設置環境參數)

    1
    2
    3
    4
    export PYENV_ROOT="$HOME/.pyenv" 
    export PATH="$PYENV_ROOT/bin:$PATH"
    eval "$(pyenv init --path)"
    eval "$(pyenv init -)"

    緊接著再來安裝 Pyenv-Virtualenv,指令如下

    1
    brew install pyenv-virtualenv
  3. 安裝 macOS 64-bit universal2 Version Python
    由於 Pyenv 預設安裝 Python 的版本會依照系統,自動選擇安裝 Intel 或是 ARM 版本,並不會主動安裝 universal2 版本,所以我們在設置虛擬環境時,需要設置 Config 強制要求制定 universal2 版本,其指令如下

    1
    2
    3
    4
    5
    6
    7
    8
    env \
    PATH="$(brew --prefix tcl-tk)/bin:$PATH" \
    LDFLAGS="-L$(brew --prefix tcl-tk)/lib" \
    CPPFLAGS="-I$(brew --prefix tcl-tk)/include" \
    PKG_CONFIG_PATH="$(brew --prefix tcl-tk)/lib/pkgconfig" \
    CFLAGS="-I$(brew --prefix tcl-tk)/include" \
    PYTHON_CONFIGURE_OPTS="--enable-framework --enable-universalsdk --with-universal-archs=universal2 --with-tcltk-includes='-I$(brew --prefix tcl-tk)/include' --with-tcltk-libs='-L$(brew --prefix tcl-tk)/lib -ltcl8.6 -ltk8.6'" \
    pyenv install 3.10.3

    對於這個指令,稍微說明一下其含義

    關於這段 PATH 說明

    1
    2
    3
    4
    5
    6
    PATH="$(brew --prefix tcl-tk)/bin:$PATH" \
    LDFLAGS="-L$(brew --prefix tcl-tk)/lib" \
    CPPFLAGS="-I$(brew --prefix tcl-tk)/include" \
    PKG_CONFIG_PATH="$(brew --prefix tcl-tk)/lib/pkgconfig" \
    CFLAGS="-I$(brew --prefix tcl-tk)/include" \
    PYTHON_CONFIGURE_OPTS="--with-tcltk-includes='-I$(brew --prefix tcl-tk)/include' --with-tcltk-libs='-L$(brew --prefix tcl-tk)/lib -ltcl8.6 -ltk8.6'" \

    首先由於我的程式是使用 Tkinter 所撰寫的,而 macOS 系統內建的 Python,預先設置的 Tkinter 為 v8.5 版本,而此版本在 Arm 系統下,會出現 UI 錯誤,轉譯出的程式 UI Background 會出現全黑的狀況,這個問題需要在 v8.5.9 才會被修復,所以我們需要在 PyenvConfig 中設置 PATH,讓 Pyenv 安裝 Python 時內建 Tkinter v8.6 版本。

    此解決辦法參考自 Stackoverflow 中的說明。

    接下來說明此段 PATH 意義

    1
    --enable-framework --enable-universalsdk  --with-universal-archs=universal2

    這段主要是作為 universal binary 的建構,否則 Python 在設置建構文件時,僅會依照所安裝使用的系統,預設安裝 IntelARM 二擇一的建構文件。

    此解決辦法參考自 Jack.Jansen Note 以及 pyenv arm64 builds confused by x86_64 libintl in /usr/local #1877

    因此如果不需要設置 Tkinter,則可以輸入以下 PATH

    1
    2
    env PYTHON_CONFIGURE_OPTS="--enable-framework --enable-universalsdk  --with-universal-archs=universal2" pyenv install -v 3.10.3

    當安裝完 Python 後,需要先將 System 環境切換成新的 Python 環境,可以直接輸入以下指令,基本上就可以直接開始使用

    1
    pyenv global 3.10.3

    但是我習慣在虛擬環境下使用,所以我會再建置一個 Virtualenv,輸入以下指令

    1
    pyenv virtualenv 3.10.3 Python3103

    最後把環境切入 Python3911 Virtualenv 做使用,相關操作指令可以參考此教學

    1
    pyenv activate Python3103

2. Pyinstaller 操作實例

接下來我們就可以開始編譯我們的程式,在上篇文章,我所使用的指令如下

1
pyinstaller -F /Users/Documents/02_Program/GitHub/test/test.py

這個指令正常使用是不會有問題的,可是他所使用的編譯環境會變成依照系統預設的環境進行轉譯,例如你現在所使用的系統為 Intel 那麼他轉譯出來檔案的 Type 就會是 Inte,所以如果想在 ARM 架構下 Run 此程式就會碰到問題,所以我們必須進一步使用 --target-arch 指令讓 pyinstaller 知道你想轉譯成什麼樣的程式。

以下可以看看我的實例,編譯指令分別對應以下圖示

1
2
3
4
5
6
7
8
9
10
11
# 預設指令
pyinstaller -F -w "/Users/Documents/GitHub/RandomValueGenerator.py"

# --target-arch arm64 指令
pyinstaller -F -w --target-arch arm64 "/Users/Documents/GitHub/RandomValueGenerator.py"

# --target-arch x86_64 指令
pyinstaller -F -w --target-arch x86_64 "/Users/Documents/GitHub/RandomValueGenerator.py"

# --target-arch universal2 指令
pyinstaller -F -w --target-arch universal2 "/Users/Documents/GitHub/RandomValueGenerator.py"

image-20220501112051849

以上就是關於 PyInstallerTkinter 目前在 Apple 晶片使用上需要注意的地方,我認為這是一個過渡期,後續應該隨著時間支援性會越來越完善,也就不需要使用這種方式了。