Veloris.
返回索引
工具环境 2026-02-14

Python项目打包与发布:PyInstaller打包exe,pyproject.toml发布到PyPI

2 分钟
454 words

Python项目打包与发布:PyInstaller打包exe,pyproject.toml发布到PyPI

将Python项目打包可以方便地分发和部署。本篇介绍如何将Python项目打包为可安装的包、可执行文件,以及发布到PyPI。


1. 项目结构

my_package/
├── pyproject.toml          # 项目配置(推荐)
├── setup.py                # 传统安装脚本(可选)
├── setup.cfg               # 配置文件(可选)
├── README.md               # 项目说明
├── LICENSE                 # 许可证
├── CHANGELOG.md            # 更新日志
├── requirements.txt        # 依赖列表

├── src/                    # 源代码目录
│   └── my_package/
│       ├── __init__.py
│       ├── main.py
│       ├── utils.py
│       └── cli.py

├── tests/                  # 测试目录
│   ├── __init__.py
│   └── test_main.py

└── docs/                   # 文档目录
    └── index.md
# src/my_package/__init__.py
"""My Package - 一个示例Python包"""

__version__ = "0.1.0"
__author__ = "Your Name"

from .main import main_function
from .utils import helper_function

__all__ = ['main_function', 'helper_function']

2. pyproject.toml配置

# pyproject.toml - 现代Python项目配置文件

[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "my-package"
version = "0.1.0"
description = "一个示例Python包"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.8"
authors = [
    {name = "Your Name", email = "[email protected]"}
]
keywords = ["example", "package", "python"]
classifiers = [
    "Development Status :: 3 - Alpha",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.8",
    "Programming Language :: Python :: 3.9",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
]

# 依赖
dependencies = [
    "requests>=2.28.0",
    "click>=8.0.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0.0",
    "black>=23.0.0",
    "flake8>=6.0.0",
    "mypy>=1.0.0",
]
docs = [
    "sphinx>=5.0.0",
    "sphinx-rtd-theme>=1.0.0",
]

# 命令行入口点
[project.scripts]
my-cli = "my_package.cli:main"

# GUI入口点
[project.gui-scripts]
my-gui = "my_package.gui:main"

# 插件入口点
[project.entry-points."my_package.plugins"]
plugin1 = "my_package.plugins:Plugin1"

[project.urls]
Homepage = "https://github.com/username/my-package"
Documentation = "https://my-package.readthedocs.io"
Repository = "https://github.com/username/my-package"
Issues = "https://github.com/username/my-package/issues"

# setuptools配置
[tool.setuptools]
package-dir = {"" = "src"}

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

# 包含数据文件
[tool.setuptools.package-data]
my_package = ["data/*.json", "templates/*.html"]

# 工具配置
[tool.black]
line-length = 100
target-version = ['py38', 'py39', 'py310', 'py311']

[tool.isort]
profile = "black"
line_length = 100

[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_ignores = true

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"

3. 构建包

# 安装构建工具
pip install build twine

# 构建包
python -m build

# 构建结果在 dist/ 目录
# dist/
# ├── my_package-0.1.0-py3-none-any.whl  # wheel包
# └── my_package-0.1.0.tar.gz            # 源码包

# 本地安装测试
pip install dist/my_package-0.1.0-py3-none-any.whl

# 开发模式安装(可编辑)
pip install -e .

# 带可选依赖安装
pip install -e ".[dev]"
pip install -e ".[dev,docs]"
# 传统 setup.py(如果需要)
from setuptools import setup, find_packages

setup(
    name="my-package",
    version="0.1.0",
    packages=find_packages(where="src"),
    package_dir={"": "src"},
    install_requires=[
        "requests>=2.28.0",
        "click>=8.0.0",
    ],
    entry_points={
        "console_scripts": [
            "my-cli=my_package.cli:main",
        ],
    },
    python_requires=">=3.8",
)

4. 发布到PyPI

# 1. 注册PyPI账号
# https://pypi.org/account/register/

# 2. 创建API Token
# https://pypi.org/manage/account/token/

# 3. 配置认证
# 创建 ~/.pypirc 文件
# ~/.pypirc
[distutils]
index-servers =
    pypi
    testpypi

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

[testpypi]
repository = https://test.pypi.org/legacy/
username = __token__
password = pypi-AgENdGVzdC5weXBp...
# 4. 上传到TestPyPI(测试)
python -m twine upload --repository testpypi dist/*

# 从TestPyPI安装测试
pip install --index-url https://test.pypi.org/simple/ my-package

# 5. 上传到PyPI(正式)
python -m twine upload dist/*

# 6. 安装验证
pip install my-package
# 自动化发布脚本
# scripts/release.py
import subprocess
import sys
from pathlib import Path

def run(cmd):
    print(f"运行: {cmd}")
    result = subprocess.run(cmd, shell=True)
    if result.returncode != 0:
        sys.exit(1)

def release():
    # 清理旧构建
    for path in Path("dist").glob("*"):
        path.unlink()
    
    # 运行测试
    run("pytest")
    
    # 构建
    run("python -m build")
    
    # 上传
    run("python -m twine upload dist/*")
    
    print("发布完成!")

if __name__ == "__main__":
    release()

5. 打包为可执行文件

PyInstaller

pip install pyinstaller
# 基本打包(单文件)
pyinstaller --onefile main.py

# 带图标
pyinstaller --onefile --icon=app.ico main.py

# 无控制台窗口(GUI程序)
pyinstaller --onefile --noconsole --icon=app.ico main.py

# 指定名称
pyinstaller --onefile --name myapp main.py

# 添加数据文件
pyinstaller --onefile --add-data "data;data" main.py

# 使用spec文件
pyinstaller myapp.spec
# myapp.spec - PyInstaller配置文件
# -*- mode: python ; coding: utf-8 -*-

block_cipher = None

a = Analysis(
    ['src/main.py'],
    pathex=[],
    binaries=[],
    datas=[
        ('data/*.json', 'data'),
        ('templates/*.html', 'templates'),
    ],
    hiddenimports=['pkg_resources.py2_warn'],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)

pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.zipfiles,
    a.datas,
    [],
    name='MyApp',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=False,  # GUI程序设为False
    disable_windowed_traceback=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
    icon='app.ico',
)
# 处理数据文件路径
import sys
from pathlib import Path

def get_resource_path(relative_path):
    """获取资源文件路径(兼容PyInstaller)"""
    if hasattr(sys, '_MEIPASS'):
        # PyInstaller打包后的路径
        base_path = Path(sys._MEIPASS)
    else:
        # 开发环境路径
        base_path = Path(__file__).parent
    
    return base_path / relative_path

# 使用
config_path = get_resource_path('data/config.json')

Nuitka(编译为C)

pip install nuitka
# 基本编译
nuitka --standalone --onefile main.py

# Windows GUI程序
nuitka --standalone --onefile --windows-disable-console main.py

# 带图标
nuitka --standalone --onefile --windows-icon-from-ico=app.ico main.py

6. 创建安装脚本

# install.py - Windows安装脚本
import os
import sys
import subprocess
from pathlib import Path

def install():
    """安装程序"""
    print("=" * 50)
    print("开始安装...")
    print("=" * 50)
    
    # 检查Python版本
    if sys.version_info < (3, 8):
        print("错误: 需要Python 3.8或更高版本")
        sys.exit(1)
    
    # 创建虚拟环境
    venv_path = Path("venv")
    if not venv_path.exists():
        print("创建虚拟环境...")
        subprocess.run([sys.executable, "-m", "venv", "venv"])
    
    # 确定pip路径
    if os.name == 'nt':
        pip_path = venv_path / "Scripts" / "pip.exe"
    else:
        pip_path = venv_path / "bin" / "pip"
    
    # 安装依赖
    print("安装依赖...")
    subprocess.run([str(pip_path), "install", "-r", "requirements.txt"])
    
    # 安装包
    print("安装程序...")
    subprocess.run([str(pip_path), "install", "-e", "."])
    
    print("=" * 50)
    print("安装完成!")
    print("=" * 50)
    
    # 显示使用说明
    if os.name == 'nt':
        activate = r"venv\Scripts\activate"
    else:
        activate = "source venv/bin/activate"
    
    print(f"\n激活虚拟环境: {activate}")
    print("运行程序: my-cli --help")

if __name__ == "__main__":
    install()
@echo off
REM install.bat - Windows批处理安装脚本

echo ========================================
echo 安装程序
echo ========================================

REM 检查Python
python --version >nul 2>&1
if errorlevel 1 (
    echo 错误: 未找到Python
    pause
    exit /b 1
)

REM 创建虚拟环境
if not exist venv (
    echo 创建虚拟环境...
    python -m venv venv
)

REM 激活虚拟环境并安装
call venv\Scripts\activate
pip install -r requirements.txt
pip install -e .

echo ========================================
echo 安装完成!
echo ========================================
pause

7. Docker打包

# Dockerfile
FROM python:3.11-slim

# 设置工作目录
WORKDIR /app

# 复制依赖文件
COPY requirements.txt .

# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt

# 复制源代码
COPY src/ ./src/
COPY pyproject.toml .

# 安装包
RUN pip install --no-cache-dir .

# 设置环境变量
ENV PYTHONUNBUFFERED=1

# 暴露端口(如果是Web应用)
EXPOSE 8000

# 运行命令
CMD ["my-cli", "run"]
# Dockerfile.multi-stage - 多阶段构建(更小的镜像)
# 构建阶段
FROM python:3.11-slim as builder

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt

COPY src/ ./src/
COPY pyproject.toml .
RUN pip install --no-cache-dir --user .

# 运行阶段
FROM python:3.11-slim

WORKDIR /app

# 从构建阶段复制安装的包
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH

# 复制必要文件
COPY --from=builder /app/src ./src

CMD ["my-cli", "run"]
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8000:8000"
    volumes:
      - ./data:/app/data
    environment:
      - DEBUG=false
      - DATABASE_URL=sqlite:///data/db.sqlite
    restart: unless-stopped
# Docker命令
docker build -t my-app .
docker run -p 8000:8000 my-app
docker-compose up -d

8. 常见问题

❌ 问题1:找不到模块

# 确保__init__.py存在
# 确保包结构正确

# 检查MANIFEST.in(如果使用)
# MANIFEST.in
include README.md
include LICENSE
recursive-include src *.py
recursive-include data *

❌ 问题2:数据文件未包含

# pyproject.toml
[tool.setuptools.package-data]
my_package = ["data/*.json", "templates/*.html"]

# 或使用MANIFEST.in

❌ 问题3:PyInstaller打包后找不到文件

# 使用get_resource_path函数
# 在spec文件中添加datas

❌ 问题4:版本号管理

# 使用单一来源的版本号
# src/my_package/__init__.py
__version__ = "0.1.0"

# pyproject.toml中动态读取
[project]
dynamic = ["version"]

[tool.setuptools.dynamic]
version = {attr = "my_package.__version__"}

9. 总结

🔑 核心要点

任务工具/方法
项目配置pyproject.toml
构建包python -m build
发布PyPItwine upload
可执行文件PyInstaller, Nuitka
容器化Docker

✅ 学习检查清单

  • 了解项目结构规范
  • 会配置pyproject.toml
  • 能构建和发布Python包
  • 能使用PyInstaller打包
  • 了解Docker打包

📖 下一步学习

掌握了项目打包后,让我们学习学习资源与进阶路线:


常见问题 FAQ

💬 PyInstaller打包的exe太大怎么办?

默认打包会包含整个Python环境。解决:1)用虚拟环境只安装必要依赖;2)用--exclude-module排除不需要的模块;3)用UPX压缩。通常能从200MB降到50MB以下。

💬 打包的exe在别人电脑上运行报错?

常见原因:缺少运行时依赖(如VC++ Redistributable)、路径问题、缺少数据文件。用--add-data打包数据文件,测试时在干净的虚拟机上验证。


系列导航

End of file.