Skip to content

Distribution — Đóng gói và Phân phối Python Package

Tháng 3 năm 2023, một team infrastructure push release v2.4.0 của internal SDK lên PyPI chiều thứ Sáu. Thứ Hai, 14 microservice đồng loạt crash khi auto-upgrade. Nguyên nhân: wheel build thiếu file py.typed và thư mục migrations/. Install OK, import OK, nhưng runtime fail. Rollback mất 4 giờ vì PyPI không cho re-upload cùng version — phải bump v2.4.1 hotfix.

Bài học: viết code giỏi chưa đủ — đóng gói và phân phối đúng cách mới là yếu tố quyết định. Distribution là cầu nối giữa source code trên máy bạn và pip install trên máy người khác.

Tin tốt: sau bài viết này, bạn sẽ nắm vững toàn bộ quy trình — từ build format, publish lên registry, đến CI/CD tự động.

Bức tranh tư duy

Hãy nghĩ về distribution như đóng gói sản phẩm trước khi giao hàng. Bạn sản xuất ghế gỗ (source code). Để giao đến khách, bạn cần: đóng thùng đúng kích thước (build format), dán nhãn vận chuyển (metadata), gửi đến kho trung chuyển (registry), và khách đặt qua hệ thống (pip install). Gửi nguyên khối gỗ thô thì khách phải tự lắp — chậm, dễ hỏng. Gửi ghế đã lắp sẵn trong hộp chuẩn (wheel) thì chỉ cần mở ra dùng.

text
  Source Code          Build              Distribution         Registry           Install
  ┌──────────┐      ┌──────────┐       ┌──────────────┐    ┌──────────┐     ┌──────────────┐
  │ src/     │      │ python   │       │ .tar.gz      │    │  PyPI    │     │ pip install  │
  │ tests/   │ ───→ │ -m build │ ───→  │ (sdist)      │ ─→ │  Test    │ ──→ │ my-package   │
  │ pyproject│      │          │       │              │    │  PyPI    │     │              │
  │ .toml    │      │          │       │ .whl         │    │  Private │     │ → site-      │
  └──────────┘      └──────────┘       │ (wheel)      │    │  Registry│     │   packages/  │
                                       └──────────────┘    └──────────┘     └──────────────┘

  Bạn viết code  →  Đóng gói thành    →  Hai format    →  Upload lên   →  Người dùng
                     artifact chuẩn       phân phối         kho lưu trữ     cài đặt

sdist vs wheel — So sánh trực quan

text
  sdist (.tar.gz)                         wheel (.whl)
  ┌─────────────────────┐                 ┌─────────────────────┐
  │ 📦 Source archive    │                 │ 📦 Pre-built archive │
  │  pyproject.toml     │                 │  my_package/        │
  │  src/my_package/    │                 │    __init__.py      │
  │    __init__.py      │                 │    module.py        │
  │    ext.c  ← cần     │                 │  METADATA, RECORD   │
  │  MANIFEST.in        │                 │  ✅ Đã compile sẵn  │
  └─────────────────────┘                 └─────────────────────┘
  ⏱️ Chậm (cần build + compiler)          ⏱️ Nhanh (unzip → done)

📌 Khi nào cần cả hai?

Thực tế tốt nhất: luôn publish cả sdist lẫn wheel. Wheel cho tốc độ install, sdist cho trường hợp wheel không tương thích với platform đích. python -m build mặc định tạo cả hai — đừng bỏ bước này.

Cốt lõi kỹ thuật

Distribution Formats — sdist vs wheel

Source Distribution (sdist) là archive .tar.gz chứa toàn bộ source code. Khi install, pip phải chạy build system để compile. Nếu có C extensions, user cần compiler.

Wheel là archive .whl (ZIP) chứa code đã build sẵn. pip chỉ cần unzip vào site-packages/ — không cần build. Format khuyến nghị cho distribution hiện đại.

Tiêu chísdist (.tar.gz)wheel (.whl)
Nội dungSource code gốcCode đã build
Cần build khi install✅ Có❌ Không
Tốc độ installChậmNhanh (10-100x)
C extensionsCần compilerĐã compile sẵn
PlatformĐộc lậpCó thể platform-specific
ReproducibilityPhụ thuộc build envDeterministic

Wheel Naming Convention

Mỗi tên file wheel mã hóa chính xác compatibility:

text
{distribution}-{version}(-{build})?-{python}-{abi}-{platform}.whl

# Pure Python — chạy mọi nơi
analytics_sdk-2.1.0-py3-none-any.whl
#               │     │   │    │
#               │     │   │    └── Mọi architecture
#               │     │   └─────── Không ABI requirement
#               │     └─────────── Python 3.x
#               └───────────────── Version 2.1.0

# Platform-specific — có C extensions
numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.whl
#       │      │     │     │
#       │      │     │     └── Linux x86_64, glibc ≥ 2.17
#       │      │     └──────── ABI: CPython 3.12
#       │      └────────────── CPython 3.12
#       └──────────────────── Version 1.26.4

# macOS universal
cryptography-42.0.0-cp312-cp312-macosx_10_12_universal2.whl

# Windows
pillow-10.2.0-cp312-cp312-win_amd64.whl

Building Packages

Tool chuẩn: python -m build — frontend PEP 517 tương thích mọi build backend.

bash
# Cài đặt build tool
pip install build

# Build cả sdist và wheel (khuyến nghị)
python -m build

# Chỉ build wheel
python -m build --wheel

# Chỉ build sdist
python -m build --sdist

# Kiểm tra output
ls dist/
# analytics_sdk-2.1.0.tar.gz
# analytics_sdk-2.1.0-py3-none-any.whl

Verify trước khi publish — bắt buộc:

bash
pip install twine

# Kiểm tra metadata và long description rendering
twine check dist/*

# Test install trong clean environment
python -m venv /tmp/test_install
source /tmp/test_install/bin/activate
pip install dist/analytics_sdk-2.1.0-py3-none-any.whl
python -c "import analytics_sdk; print(analytics_sdk.__version__)"
deactivate && rm -rf /tmp/test_install

Publishing với Twine

Twine là tool upload chuẩn — hỗ trợ cả PyPI, TestPyPI, và private registries.

bash
# Bước 1: Upload lên TestPyPI để kiểm tra
twine upload --repository testpypi dist/*

# Bước 2: Verify install từ TestPyPI
pip install --index-url https://test.pypi.org/simple/ \
    --extra-index-url https://pypi.org/simple/ \
    analytics-sdk

# Bước 3: Mọi thứ ổn → upload lên PyPI production
twine upload dist/*

Cấu hình credentials qua ~/.pypirc (cho local development):

ini
[distutils]
index-servers = pypi, testpypi

[pypi]
username = __token__
password = pypi-AgEIcH...

[testpypi]
repository = https://test.pypi.org/legacy/
username = __token__
password = pypi-AgEIcH...

Nhớ chmod 600 ~/.pypirc để bảo mật file credentials.

Trusted Publishing (OIDC) — Không cần API Token

Trusted publishing dùng OpenID Connect (OIDC) để CI provider xác thực trực tiếp với PyPI — không cần lưu trữ API token. Thiết lập: vào PyPI → Manage project → Publishing → Add GitHub Actions publisher (điền owner, repo, workflow filename).

yaml
# .github/workflows/publish.yml
name: Publish to PyPI

on:
  release:
    types: [published]

permissions:
  id-token: write  # Bắt buộc cho OIDC

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install build && python -m build
      - uses: pypa/gh-action-pypi-publish@release/v1
        # Không cần token — OIDC tự động xác thực!

Private Registries

Khi package nội bộ không phù hợp để public, private registry là giải pháp.

AWS CodeArtifact:

bash
# Login và publish
aws codeartifact login --tool pip \
    --domain my-org --domain-owner 123456789012 --repository internal-pypi
aws codeartifact login --tool twine \
    --domain my-org --domain-owner 123456789012 --repository internal-pypi
twine upload --repository codeartifact dist/*

devpi — Self-hosted registry:

bash
pip install devpi-server devpi-client
devpi-server --init && devpi-server --start --port 4040
devpi use http://localhost:4040
devpi user -c deploy password=S3cureP@ss
devpi login deploy --password=S3cureP@ss
devpi index -c production bases=root/pypi
devpi upload dist/*

Cấu hình trong pyproject.toml (Poetry):

toml
[[tool.poetry.source]]
name = "internal"
url = "https://artifacts.company.com/pypi/internal/simple/"
priority = "primary"

Semantic Versioning (SemVer)

text
MAJOR.MINOR.PATCH[-pre_release]
  MAJOR ──→ Breaking changes: xóa/đổi public API
  MINOR ──→ Backward-compatible features: thêm hàm mới
  PATCH ──→ Backward-compatible fixes: bug fix, security patch
  VD:  2.1.3

Quy tắc bump version (PEP 440 compatible):

python
# pyproject.toml — các giai đoạn release
[project]
version = "1.0.0"       # Stable release
version = "2.0.0a1"     # Alpha — API chưa ổn định
version = "2.0.0b1"     # Beta — feature-complete, đang test
version = "2.0.0rc1"    # Release Candidate — sẵn sàng nếu không có bug

Single Source of Truth cho version — dùng setuptools-scm:

toml
# pyproject.toml
[build-system]
requires = ["setuptools>=64", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"

[project]
dynamic = ["version"]

[tool.setuptools_scm]
# Version tự derive từ git tag — không hardcode ở đâu cả
bash
# Workflow: tag → build → version tự động
git tag v2.1.0
git push origin v2.1.0
python -m build
# → analytics_sdk-2.1.0-py3-none-any.whl (version từ tag)

Thực chiến

Scenario: CI/CD Publishing Pipeline hoàn chỉnh

Bạn maintain analytics-sdk, internal package dùng bởi 8 backend services. Yêu cầu: mỗi khi tag release (v*), pipeline tự động chạy test → build → publish TestPyPI → verify → publish PyPI → tạo GitHub Release.

yaml
# .github/workflows/release.yml
name: Release Pipeline

on:
  push:
    tags:
      - "v*"

permissions:
  id-token: write
  contents: write

jobs:
  # ──────────── Stage 1: Test matrix ────────────
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.10", "3.11", "3.12"]

    steps:
      - uses: actions/checkout@v4

      - name: Setup Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install dependencies
        run: pip install -e ".[dev]"

      - run: ruff check src/
      - run: mypy src/
      - run: pytest --cov=analytics_sdk --cov-report=xml

  # ──────────── Stage 2: Build & verify ────────────
  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: Build & verify
        run: |
          pip install build twine
          python -m build
          twine check dist/*
          pip install dist/*.whl
          python -c "import analytics_sdk; print(analytics_sdk.__version__)"
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

  # ──────────── Stage 3: Publish & Verify TestPyPI ────────────
  publish-testpypi:
    needs: build
    runs-on: ubuntu-latest
    environment: testpypi
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
      - uses: pypa/gh-action-pypi-publish@release/v1
        with:
          repository-url: https://test.pypi.org/legacy/
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: Verify from TestPyPI
        run: |
          sleep 30
          pip install --index-url https://test.pypi.org/simple/ \
              --extra-index-url https://pypi.org/simple/ \
              analytics-sdk==${GITHUB_REF_NAME#v}

  # ──────────── Stage 4: Publish PyPI (production) ────────────
  publish-pypi:
    needs: publish-testpypi
    runs-on: ubuntu-latest
    environment:
      name: pypi
      url: https://pypi.org/p/analytics-sdk
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
      - uses: pypa/gh-action-pypi-publish@release/v1

  # ──────────── Stage 5: GitHub Release ────────────
  github-release:
    needs: publish-pypi
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
      - uses: softprops/action-gh-release@v2
        with:
          files: dist/*
          generate_release_notes: true

Sai lầm điển hình

1. Publish thẳng lên PyPI không qua TestPyPI

bash
# ❌ SAI: Bypass TestPyPI — "nhanh hơn mà"
python -m build
twine upload dist/*
# Phát hiện metadata sai → PyPI không cho xóa version
# → Phải bump version chỉ để fix metadata

# ✅ ĐÚNG: Luôn qua TestPyPI trước
python -m build
twine upload --repository testpypi dist/*
pip install --index-url https://test.pypi.org/simple/ \
    --extra-index-url https://pypi.org/simple/ \
    my-package==1.0.0
# Verify xong → PyPI
twine upload dist/*

2. Hardcode version ở nhiều nơi

python
# ❌ SAI: Version nằm rải rác — quên update 1 chỗ là đủ hỏng
# pyproject.toml → version = "1.2.0"
# __init__.py   → __version__ = "1.1.0"  ← QUÊN UPDATE!
# docs/conf.py  → version = "1.2.0"

# ✅ ĐÚNG: Single source of truth với setuptools-scm
# pyproject.toml
# [project]
# dynamic = ["version"]
# [tool.setuptools_scm]
# → Version derive từ git tag, không hardcode ở đâu cả

# Nếu cần truy cập version trong code:
from importlib.metadata import version
__version__ = version("analytics-sdk")

3. Thiếu package data trong distribution

toml
# ❌ SAI: Chỉ include Python files — thiếu data files
[tool.setuptools.packages.find]
where = ["src"]
# → py.typed, JSON schemas, templates KHÔNG có trong wheel!

# ✅ ĐÚNG: Khai báo rõ package data
[tool.setuptools.package-data]
analytics_sdk = [
    "py.typed",
    "schemas/*.json",
    "templates/*.html",
    "data/**/*.csv",
]

4. Không verify build trước khi publish

bash
# ❌ SAI: Build xong upload ngay
python -m build && twine upload dist/*
# wheel thiếu module → user install OK nhưng import fail!

# ✅ ĐÚNG: Verify chain đầy đủ
python -m build
twine check dist/*
python -m venv /tmp/verify && source /tmp/verify/bin/activate
pip install dist/*.whl
python -c "from analytics_sdk.core import Engine; print('OK')"
deactivate && rm -rf /tmp/verify
twine upload --repository testpypi dist/*

5. Lộ secrets trong CI/CD pipeline

yaml
# ❌ SAI: Token hardcode trong workflow
- name: Publish
  run: twine upload dist/*
  env:
    TWINE_PASSWORD: pypi-AgEIcHlwaS5vcmc...  # LEAKED trong repo!

# ✅ ĐÚNG: Dùng Trusted Publishing (OIDC) — không cần token
- name: Publish
  uses: pypa/gh-action-pypi-publish@release/v1
  # OIDC token tự động — không secret nào cần lưu trữ

# Hoặc nếu buộc phải dùng token: GitHub encrypted secrets
- name: Publish
  run: twine upload dist/*
  env:
    TWINE_USERNAME: __token__
    TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}

Under the Hood

Pip resolve và install wheel như thế nào?

Khi bạn chạy pip install analytics-sdk:

  1. Query PyPI APIGET https://pypi.org/simple/analytics-sdk/ → nhận danh sách tất cả distributions
  2. Resolve version — chọn version mới nhất thỏa mãn constraint, resolve dependency conflicts
  3. Chọn distribution — ưu tiên wheel > sdist, match Python version + ABI + platform
  4. Download & install — wheel: unzip vào site-packages/; sdist: build rồi install

PyPI Upload Process

Khi twine upload chạy:

  1. Đọc metadata từ PKG-INFO (sdist) hoặc METADATA (wheel)
  2. Tính hash SHA-256 và MD5 cho mỗi file
  3. POST multipart/form-data đến https://upload.pypi.org/legacy/
  4. PyPI validate: tên package, version chưa tồn tại, metadata hợp lệ
  5. PyPI index package — có thể mất vài phút để available qua pip install

Trade-offs: Distribution Strategies

Chiến lượcƯu điểmNhược điểmKhi nào dùng
TestPyPI → PyPIAn toàn, verify trướcThêm bước, chậm hơnMọi production release
Trusted Publishing (OIDC)Không token, audit trailChỉ hỗ trợ vài CI providersGitHub/GitLab projects
Private Registry (CodeArtifact)Kiểm soát truy cập, IAMChi phí, phức tạpInternal packages
Private Registry (devpi)Tự host, caching PyPIVận hành serverTeam nhỏ-trung bình
Git tag versioning (scm)Single source of truthPhụ thuộc git historyMọi project nên dùng

Anatomy của một Wheel file

bash
# Unzip wheel để xem bên trong
unzip -l analytics_sdk-2.1.0-py3-none-any.whl

# analytics_sdk/__init__.py
# analytics_sdk/core.py
# analytics_sdk/py.typed
# analytics_sdk/schemas/event.json
# analytics_sdk-2.1.0.dist-info/METADATA        ← Metadata (PEP 566)
# analytics_sdk-2.1.0.dist-info/WHEEL           ← Wheel tag info
# analytics_sdk-2.1.0.dist-info/RECORD          ← SHA-256 hash mỗi file
# analytics_sdk-2.1.0.dist-info/entry_points.txt

RECORD chứa hash SHA-256 mỗi file — pip dùng để verify integrity khi install. Nếu file bị sửa đổi sau khi build, hash không khớp → pip từ chối install.

Checklist ghi nhớ

✅ Checklist triển khai

Build & Verify

  • [ ] Luôn dùng python -m build để tạo cả sdist và wheel
  • [ ] Chạy twine check dist/* trước mỗi lần publish
  • [ ] Test install wheel trong clean virtual environment trước khi upload
  • [ ] Verify tất cả imports và data files có trong wheel

Publishing

  • [ ] Luôn publish TestPyPI trước, PyPI sau
  • [ ] Ưu tiên Trusted Publishing (OIDC) thay vì API token
  • [ ] Nếu dùng token: chỉ qua CI secrets, không hardcode
  • [ ] Bảo mật ~/.pypirc với chmod 600

Versioning

  • [ ] Tuân thủ SemVer: MAJOR.MINOR.PATCH
  • [ ] Single source of truth cho version (setuptools-scm hoặc importlib.metadata)
  • [ ] Không hardcode version ở nhiều file
  • [ ] Dùng pre-release versions (alpha/beta/rc) cho testing

CI/CD Pipeline

  • [ ] Pipeline tự động: test → build → verify → TestPyPI → PyPI
  • [ ] Test matrix trên nhiều Python versions (3.10, 3.11, 3.12)
  • [ ] Upload build artifacts giữa các stages
  • [ ] Tạo GitHub Release kèm distribution files

Package Data

  • [ ] Khai báo rõ package-data trong pyproject.toml
  • [ ] Include py.typed nếu package hỗ trợ type checking
  • [ ] Verify data files có mặt trong wheel (unzip và kiểm tra)

Bài tập luyện tập

Bài 1: Phân tích Wheel Naming — Foundation

Đề bài: Cho các wheel filename dưới đây, xác định package nào compatible với hệ thống CPython 3.11 trên Linux x86_64 (glibc 2.31).

text
A. cryptography-42.0.0-cp312-cp312-manylinux_2_17_x86_64.whl
B. requests-2.31.0-py3-none-any.whl
C. numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.whl
D. pillow-10.2.0-cp311-cp311-win_amd64.whl
E. pandas-2.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

🧠 Quiz

Wheel nào KHÔNG compatible với CPython 3.11 trên Linux x86_64?

  • [ ] A. Chỉ A
  • [ ] B. Chỉ D
  • [x] C. A và D — A yêu cầu CPython 3.12 (cp312), D yêu cầu Windows (win_amd64)
  • [ ] D. A, D và E Giải thích: B compatible vì py3-none-any = pure Python, chạy mọi nơi. C compatible vì match đúng cp311 + manylinux_2_17_x86_64 (glibc 2.31 > 2.17). E compatible vì cp311 + manylinux_2_17_x86_64. A fail vì cp312cp311. D fail vì win_amd64 ≠ Linux.
💡 Phân tích chi tiết
WheelTagsCompatible?Lý do
Acp312 / manylinux_2_17_x86_64Python 3.12 only
Bpy3 / none / anyPure Python — universal
Ccp311 / manylinux_2_17_x86_64Exact match
Dcp311 / win_amd64Windows only
Ecp311 / manylinux_2_17_x86_64glibc 2.31 ≥ 2.17

Bài 2: Thiết kế Release Workflow — Intermediate

Đề bài: Team bạn cần thiết lập CI/CD cho internal package data-pipeline publish lên AWS CodeArtifact. Yêu cầu:

  • Trigger khi push tag v*
  • Test trên Python 3.11 và 3.12
  • Build + verify
  • Publish lên CodeArtifact (không phải PyPI)

🧠 Quiz

Trong trusted publishing workflow, tại sao cần permissions: id-token: write?

  • [ ] A. Để GitHub Actions có quyền push code
  • [x] B. Để GitHub Actions có thể request OIDC token, dùng xác thực với PyPI mà không cần API secret
  • [ ] C. Để workflow có thể đọc repository secrets
  • [ ] D. Để tạo GitHub Release Giải thích: id-token: write cho phép workflow request JSON Web Token (JWT) từ GitHub's OIDC provider. PyPI verify token này để xác nhận request đến từ trusted workflow — không cần lưu trữ bất kỳ secret nào. Đây là cơ chế an toàn nhất hiện tại cho automated publishing.
✅ Lời giải
yaml
# .github/workflows/release-codeartifact.yml
name: Release to CodeArtifact

on:
  push:
    tags: ["v*"]

env:
  AWS_REGION: ap-southeast-1
  CODEARTIFACT_DOMAIN: my-org
  CODEARTIFACT_OWNER: "123456789012"

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.11", "3.12"]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - run: pip install -e ".[dev]" && pytest --cov=data_pipeline

  build-and-publish:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      id-token: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: Build, verify & publish
        run: |
          pip install build twine
          python -m build && twine check dist/*
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::${{ env.CODEARTIFACT_OWNER }}:role/github-publish
          aws-region: ${{ env.AWS_REGION }}
      - name: Publish to CodeArtifact
        run: |
          aws codeartifact login --tool twine \
              --domain $CODEARTIFACT_DOMAIN \
              --domain-owner $CODEARTIFACT_OWNER \
              --repository internal-pypi
          twine upload --repository codeartifact dist/*

Điểm chính: Workflow dùng OIDC (configure-aws-credentials với role-to-assume) để authenticate với AWS — không cần AWS access keys trong secrets.

Bài 3: Debug Distribution Problem — Advanced

Đề bài: User báo lỗi sau khi install package:

python
>>> import mylib
>>> mylib.load_config()
FileNotFoundError: [Errno 2] No such file or directory: '.../site-packages/mylib/defaults.yaml'

Package structure trên repo:

text
src/mylib/
├── __init__.py
├── core.py
├── defaults.yaml
└── schemas/
    └── v1.json

pyproject.toml hiện tại:

toml
[tool.setuptools.packages.find]
where = ["src"]

Tìm nguyên nhân và sửa cấu hình để defaults.yamlschemas/v1.json có mặt trong wheel.

💡 Gợi ý
  • setuptools.packages.find chỉ tìm Python packages (thư mục có __init__.py)
  • Non-Python files (YAML, JSON) cần khai báo riêng qua package-data
  • Verify bằng cách unzip wheel và kiểm tra nội dung
✅ Lời giải

Nguyên nhân: setuptools.packages.find chỉ include .py files. Các file .yaml.json bị bỏ qua khi build wheel.

Sửa pyproject.toml:

toml
[tool.setuptools.packages.find]
where = ["src"]

[tool.setuptools.package-data]
mylib = [
    "defaults.yaml",
    "schemas/*.json",
]

Verify:

bash
# Rebuild và verify
rm -rf dist/
python -m build

# Kiểm tra wheel contents
python -c "
import zipfile
with zipfile.ZipFile('dist/mylib-1.0.0-py3-none-any.whl') as zf:
    for name in sorted(zf.namelist()):
        print(name)
"
# mylib/defaults.yaml        ← Phải có
# mylib/schemas/v1.json      ← Phải có

# Test install
pip install dist/*.whl --force-reinstall
python -c "import mylib; mylib.load_config()"  # Không còn FileNotFoundError

Bài học: Luôn verify wheel contents sau khi thay đổi pyproject.toml. Unzip wheel và kiểm tra — đừng giả định file sẽ tự xuất hiện.

Liên kết học tiếp

Từ khóa glossary: Distribution, wheel, sdist, twine, PyPI, TestPyPI, trusted publishing, OIDC, private registry, CodeArtifact, devpi, semantic versioning, SemVer, setuptools-scm, MANIFEST.in, package-data

Tìm kiếm liên quan: đóng gói Python, publish PyPI, wheel vs sdist, CI/CD Python release, private Python registry, semantic versioning Python, trusted publishing GitHub Actions