6.3 發布到 PyPI
6.3 發布到 PyPI
本章介紹如何將套件發布到 PyPI。
本章目標
學完本章後,你將能夠:
- 建構 sdist 和 wheel
- 使用 twine 上傳到 PyPI
- 設定 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/v1Trusted 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/v1cibuildwheel 設定
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思考題
- 為什麼 PyPI 不允許刪除或覆蓋已發布的版本?
- Trusted Publishing 相比 API token 有什麼優勢?
- 在什麼情況下應該同時發布 sdist 和 wheel?
實作練習
- 建立一個簡單套件並發布到 TestPyPI
- 設定 GitHub Actions 自動發布流程
- 使用 cibuildwheel 建構多平台 wheel