本章介紹如何將套件發布到 PyPI。

本章目標

學完本章後,你將能夠:

  1. 建構 sdist 和 wheel
  2. 使用 twine 上傳到 PyPI
  3. 設定 CI/CD 自動發布

【原理層】套件分發格式

sdist vs wheel

 1sdist(Source Distribution):
 2├── 格式:.tar.gz
 3├── 內容:原始碼 + pyproject.toml
 4├── 安裝時需要建構
 5├── 可能需要編譯器(C 擴展)
 6└── 作為備用方案
 7
 8wheel(Built Distribution):
 9├── 格式:.whl(實際上是 zip)
10├── 內容:已編譯的套件
11├── 安裝時直接解壓
12├── 不需要編譯器
13└── 安裝速度快

wheel 命名規則

 1{distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl
 2
 3範例:
 4my_package-1.0.0-py3-none-any.whl
 5├── my_package: 套件名稱
 6├── 1.0.0: 版本
 7├── py3: Python 3
 8├── none: 無特定 ABI
 9└── any: 任何平台
10
11numpy-1.26.0-cp311-cp311-manylinux_2_17_x86_64.whl
12├── numpy: 套件名稱
13├── 1.26.0: 版本
14├── cp311: CPython 3.11
15├── cp311: CPython 3.11 ABI
16└── manylinux_2_17_x86_64: Linux x86_64

常見的 platform tag

 1純 Python:
 2├── py3-none-any(Python 3,任何平台)
 3├── py2.py3-none-any(Python 2 和 3)
 4
 5有 C 擴展:
 6├── cp311-cp311-manylinux_2_17_x86_64
 7├── cp311-cp311-macosx_11_0_arm64
 8├── cp311-cp311-win_amd64
 9
10使用 abi3(穩定 ABI):
11└── cp38-abi3-manylinux_2_17_x86_64

【實作層】建構套件

使用 build 工具

 1# 安裝 build
 2pip install build
 3
 4# 建構 sdist 和 wheel
 5python -m build
 6
 7# 只建構 wheel
 8python -m build --wheel
 9
10# 只建構 sdist
11python -m build --sdist
12
13# 建構結果
14ls dist/
15# my_package-1.0.0.tar.gz     (sdist)
16# my_package-1.0.0-py3-none-any.whl  (wheel)

檢查建構結果

 1# 安裝 twine
 2pip install twine
 3
 4# 檢查套件
 5twine check dist/*
 6
 7# 檢查 wheel 內容
 8unzip -l dist/my_package-1.0.0-py3-none-any.whl
 9
10# 或使用 wheel 工具
11pip install wheel
12wheel unpack dist/my_package-1.0.0-py3-none-any.whl

測試安裝

 1# 在新的虛擬環境中測試
 2python -m venv test_env
 3source test_env/bin/activate
 4
 5# 從本地檔案安裝
 6pip install dist/my_package-1.0.0-py3-none-any.whl
 7
 8# 測試匯入
 9python -c "import my_package; print(my_package.__version__)"
10
11# 清理
12deactivate
13rm -rf test_env

【實作層】發布到 PyPI

註冊帳號

11. 前往 https://pypi.org/account/register/
22. 建立帳號並驗證 email
33. 啟用雙因素認證(強烈建議)
44. 建立 API Token
5   - Account Settings → API tokens → Add API token
6   - Scope: Entire account(首次)或特定專案
7   - 複製 token(只會顯示一次)

設定認證

 1# 方法 1:使用 .pypirc 檔案
 2cat > ~/.pypirc << 'EOF'
 3[pypi]
 4username = __token__
 5password = pypi-xxxxxxxxxxxx
 6
 7[testpypi]
 8username = __token__
 9password = pypi-xxxxxxxxxxxx
10EOF
11
12# 設定檔案權限
13chmod 600 ~/.pypirc
14
15# 方法 2:使用環境變數
16export TWINE_USERNAME=__token__
17export TWINE_PASSWORD=pypi-xxxxxxxxxxxx
18
19# 方法 3:使用 keyring
20pip install keyring
21keyring set https://upload.pypi.org/legacy/ __token__

發布到 TestPyPI(測試)

 1# TestPyPI 是 PyPI 的測試環境
 2# 用於在正式發布前測試
 3
 4# 註冊 TestPyPI 帳號
 5# https://test.pypi.org/account/register/
 6
 7# 上傳到 TestPyPI
 8twine upload --repository testpypi dist/*
 9
10# 從 TestPyPI 安裝測試
11pip install --index-url https://test.pypi.org/simple/ my-package

發布到 PyPI

 1# 確認一切就緒
 2twine check dist/*
 3
 4# 上傳
 5twine upload dist/*
 6
 7# 或指定檔案
 8twine upload dist/my_package-1.0.0*
 9
10# 驗證
11pip install my-package

使用其他工具發布

 1# Poetry
 2poetry publish
 3
 4# Hatch
 5hatch publish
 6
 7# Flit
 8flit publish
 9
10# Maturin(Rust 擴展)
11maturin publish

【實作層】版本管理

語義化版本

 1MAJOR.MINOR.PATCH
 2
 3範例:1.2.3
 4├── MAJOR (1): 不相容的 API 變更
 5├── MINOR (2): 新增功能,向後相容
 6└── PATCH (3): 修復 bug,向後相容
 7
 8預發布版本:
 9├── 1.0.0a1  (alpha)
10├── 1.0.0b1  (beta)
11├── 1.0.0rc1 (release candidate)
12
13開發版本:
14├── 1.0.0.dev1
15├── 1.0.0.post1 (post-release)

PEP 440 版本格式

 1合法的版本號:
 2├── 1.0
 3├── 1.0.0
 4├── 1.0.0a1
 5├── 1.0.0b2
 6├── 1.0.0rc1
 7├── 1.0.0.dev1
 8├── 1.0.0.post1
 9└── 1.0.0+local
10
11不合法(會被正規化):
12├── 1.0.0-alpha1 → 1.0.0a1
13├── v1.0.0 → 1.0.0
14└── 1.0.0.RELEASE → 1.0.0

自動版本管理

 1# 使用 setuptools-scm
 2# 從 git tag 自動產生版本
 3
 4# 安裝
 5pip install setuptools-scm
 6
 7# pyproject.toml 設定
 8# [tool.setuptools_scm]
 9
10# 建立 tag
11git tag v1.0.0
12git push --tags
13
14# 建構(版本自動從 tag 取得)
15python -m build
16
17# 使用 hatch
18hatch version minor  # 1.0.0 → 1.1.0
19hatch version patch  # 1.1.0 → 1.1.1
20
21# 使用 poetry
22poetry version minor
23poetry version patch

【實作層】CI/CD 自動發布

GitHub Actions

 1# .github/workflows/publish.yml
 2name: Publish to PyPI
 3
 4on:
 5  release:
 6    types: [published]
 7
 8jobs:
 9  build:
10    runs-on: ubuntu-latest
11    steps:
12      - uses: actions/checkout@v4
13
14      - name: Set up Python
15        uses: actions/setup-python@v5
16        with:
17          python-version: '3.11'
18
19      - name: Install build tools
20        run: pip install build twine
21
22      - name: Build package
23        run: python -m build
24
25      - name: Check package
26        run: twine check dist/*
27
28      - name: Upload artifacts
29        uses: actions/upload-artifact@v4
30        with:
31          name: dist
32          path: dist/
33
34  publish-testpypi:
35    needs: build
36    runs-on: ubuntu-latest
37    environment: testpypi
38    permissions:
39      id-token: write  # 用於 Trusted Publishing
40    steps:
41      - uses: actions/download-artifact@v4
42        with:
43          name: dist
44          path: dist/
45
46      - name: Publish to TestPyPI
47        uses: pypa/gh-action-pypi-publish@release/v1
48        with:
49          repository-url: https://test.pypi.org/legacy/
50
51  publish-pypi:
52    needs: [build, publish-testpypi]
53    runs-on: ubuntu-latest
54    environment: pypi
55    permissions:
56      id-token: write
57    steps:
58      - uses: actions/download-artifact@v4
59        with:
60          name: dist
61          path: dist/
62
63      - name: Publish to PyPI
64        uses: pypa/gh-action-pypi-publish@release/v1

Trusted Publishing(推薦)

 1Trusted Publishing:
 2├── 不需要儲存 API token
 3├── 使用 OIDC(OpenID Connect)
 4├── 更安全的發布方式
 5└── GitHub Actions 原生支援
 6
 7設定步驟:
 81. 在 PyPI 專案設定中新增 Publisher
 92. 選擇 GitHub Actions
103. 填入 repository 和 workflow 資訊
114. 在 workflow 中使用 id-token: write

在 PyPI 設定 Trusted Publisher:

1PyPI → Your Project → Settings → Publishing
2
3新增 publisher:
4├── Owner: your-github-username
5├── Repository: your-repo-name
6├── Workflow name: publish.yml
7└── Environment name: pypi (選填)

完整的 CI/CD 流程

 1# .github/workflows/ci.yml
 2name: CI/CD
 3
 4on:
 5  push:
 6    branches: [main]
 7  pull_request:
 8  release:
 9    types: [published]
10
11jobs:
12  test:
13    runs-on: ${{ matrix.os }}
14    strategy:
15      matrix:
16        os: [ubuntu-latest, macos-latest, windows-latest]
17        python-version: ['3.9', '3.10', '3.11', '3.12']
18    steps:
19      - uses: actions/checkout@v4
20
21      - name: Set up Python
22        uses: actions/setup-python@v5
23        with:
24          python-version: ${{ matrix.python-version }}
25
26      - name: Install dependencies
27        run: |
28          pip install -e ".[dev]"
29
30      - name: Run tests
31        run: pytest --cov
32
33  build:
34    needs: test
35    runs-on: ubuntu-latest
36    steps:
37      - uses: actions/checkout@v4
38        with:
39          fetch-depth: 0  # 完整 history(用於 setuptools-scm)
40
41      - name: Build
42        run: |
43          pip install build
44          python -m build
45
46      - name: Upload
47        uses: actions/upload-artifact@v4
48        with:
49          name: dist
50          path: dist/
51
52  publish:
53    if: github.event_name == 'release'
54    needs: build
55    runs-on: ubuntu-latest
56    environment: pypi
57    permissions:
58      id-token: write
59    steps:
60      - uses: actions/download-artifact@v4
61        with:
62          name: dist
63          path: dist/
64
65      - uses: pypa/gh-action-pypi-publish@release/v1

【進階】多平台 wheel 建構

使用 cibuildwheel

 1# .github/workflows/wheels.yml
 2name: Build wheels
 3
 4on:
 5  release:
 6    types: [published]
 7
 8jobs:
 9  build_wheels:
10    name: Build wheels on ${{ matrix.os }}
11    runs-on: ${{ matrix.os }}
12    strategy:
13      matrix:
14        os: [ubuntu-latest, windows-latest, macos-13, macos-14]
15
16    steps:
17      - uses: actions/checkout@v4
18
19      - name: Build wheels
20        uses: pypa/cibuildwheel@v2.17
21        env:
22          # 建構 Python 3.9-3.12
23          CIBW_BUILD: cp39-* cp310-* cp311-* cp312-*
24          # 跳過 32-bit 和 musl
25          CIBW_SKIP: "*-win32 *-manylinux_i686 *-musllinux*"
26          # 測試命令
27          CIBW_TEST_COMMAND: pytest {project}/tests
28
29      - uses: actions/upload-artifact@v4
30        with:
31          name: wheels-${{ matrix.os }}
32          path: ./wheelhouse/*.whl
33
34  build_sdist:
35    name: Build source distribution
36    runs-on: ubuntu-latest
37    steps:
38      - uses: actions/checkout@v4
39
40      - name: Build sdist
41        run: |
42          pip install build
43          python -m build --sdist
44
45      - uses: actions/upload-artifact@v4
46        with:
47          name: sdist
48          path: dist/*.tar.gz
49
50  publish:
51    needs: [build_wheels, build_sdist]
52    runs-on: ubuntu-latest
53    environment: pypi
54    permissions:
55      id-token: write
56    steps:
57      - uses: actions/download-artifact@v4
58        with:
59          pattern: wheels-*
60          merge-multiple: true
61          path: dist/
62
63      - uses: actions/download-artifact@v4
64        with:
65          name: sdist
66          path: dist/
67
68      - uses: pypa/gh-action-pypi-publish@release/v1

cibuildwheel 設定

 1# pyproject.toml
 2[tool.cibuildwheel]
 3# 建構的 Python 版本
 4build = "cp39-* cp310-* cp311-* cp312-*"
 5
 6# 跳過的組合
 7skip = [
 8    "*-win32",
 9    "*-manylinux_i686",
10    "*-musllinux*",
11]
12
13# 測試設定
14test-command = "pytest {project}/tests"
15test-requires = "pytest"
16
17# 環境變數
18[tool.cibuildwheel.environment]
19MY_VAR = "value"
20
21# 平台特定設定
22[tool.cibuildwheel.linux]
23archs = ["x86_64", "aarch64"]
24manylinux-x86_64-image = "manylinux2014"
25
26[tool.cibuildwheel.macos]
27archs = ["x86_64", "arm64"]
28
29[tool.cibuildwheel.windows]
30archs = ["AMD64"]

【常見問題】疑難排解

上傳失敗

 1問題:HTTPError 403 Forbidden
 2原因:認證失敗
 3解決:
 41. 確認 token 正確
 52. 確認使用 __token__ 作為使用者名稱
 63. 確認 token 的 scope 包含該專案
 7
 8問題:File already exists
 9原因:該版本已經上傳過
10解決:
111. 升級版本號
122. PyPI 不允許覆蓋已發布的版本
13
14問題:Invalid distribution file
15原因:套件格式錯誤
16解決:
171. 執行 twine check dist/*
182. 確認 pyproject.toml 設定正確

安裝問題

 1問題:No matching distribution found
 2原因:沒有相容的 wheel
 3解決:
 41. 確認有發布 sdist
 52. 確認有對應平台的 wheel
 63. 檢查 requires-python 設定
 7
 8問題:Could not build wheels
 9原因:建構失敗(通常是 C 擴展)
10解決:
111. 安裝編譯器(gcc, MSVC)
122. 安裝 python-dev 套件
133. 提供預編譯的 wheel

思考題

  1. 為什麼 PyPI 不允許刪除或覆蓋已發布的版本?
  2. Trusted Publishing 相比 API token 有什麼優勢?
  3. 在什麼情況下應該同時發布 sdist 和 wheel?

實作練習

  1. 建立一個簡單套件並發布到 TestPyPI
  2. 設定 GitHub Actions 自動發布流程
  3. 使用 cibuildwheel 建構多平台 wheel

延伸閱讀


上一章:建構系統比較 下一章:最佳實踐