惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
A
About on SuperTechFans
IT之家
IT之家
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Blog — PlanetScale
Blog — PlanetScale
aimingoo的专栏
aimingoo的专栏
云风的 BLOG
云风的 BLOG
The GitHub Blog
The GitHub Blog
Vercel News
Vercel News
G
Google Developers Blog
J
Java Code Geeks
宝玉的分享
宝玉的分享
T
Tailwind CSS Blog
Cloudbric
Cloudbric
L
LINUX DO - 最新话题
MyScale Blog
MyScale Blog
H
Heimdal Security Blog
PCI Perspectives
PCI Perspectives
Attack and Defense Labs
Attack and Defense Labs
S
Security @ Cisco Blogs
Latest news
Latest news
I
Intezer
L
Lohrmann on Cybersecurity
C
CXSECURITY Database RSS Feed - CXSecurity.com
月光博客
月光博客
T
Threatpost
博客园 - 【当耐特】
S
Schneier on Security
P
Privacy International News Feed
G
GRAHAM CLULEY
T
Tenable Blog
AWS News Blog
AWS News Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
雷峰网
雷峰网
博客园 - Franky
Engineering at Meta
Engineering at Meta
美团技术团队
S
Secure Thoughts
T
Troy Hunt's Blog
Microsoft Security Blog
Microsoft Security Blog
SecWiki News
SecWiki News
V
Visual Studio Blog
人人都是产品经理
人人都是产品经理
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Cisco Talos Blog
Cisco Talos Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Martin Fowler
Martin Fowler
Webroot Blog
Webroot Blog
Google DeepMind News
Google DeepMind News
H
Hackread – Cybersecurity News, Data Breaches, AI and More

Leo’s blog

RISC-V 函数调用约定 [译] Understanding Incremental Decoding in fairseq 将化学分子的 ChEMBL ID 转化为 SMILES 的两种方法 华为云踩坑:由 URL 编码导致 yum 安装时的 No such file or directory 错误 为友治藏书印一方 忙忙碌碌中匆匆过眼我的 2023 年 Cloudflare + Backblaze 实现免费的博客图床方案 新服务器必做的基本设置——服务器迁移之记录 把博客站点交给了 Cloudflare 托管 RIME 脚本食用方法举隅:以输入苏州码为例 通过 SSH 在 Pycharm 上使用 Docker 容器中的 Python 解释器 如何在 X86 设备上使用 Docker 构建 ARM 镜像 文献总结|结构诱导的预训练 文献总结|MTGL-ADMET:一种通过地位理论与最大流增强并用于 ADMET 预测的多任务图学习框架 文献总结|探测图表示 四月十二奉新纸一试 文献总结|可以同时完成分子语言序列回归和生成的 Regression Transformer 旧书市场淘书记 文献总结|一种用于基于结构药物设计的 3D 生成模型
通过 PDM 和 GitHub Actions 在 PyPI 上自动化发布你的 Python 包吧
Leo · 2024-04-25 · via Leo’s blog

最近换用 PDM 作为主要的 Python 环境管理工具,虽然使用细节上还不太熟悉,但终究是搭配着 Anaconda 用起来了。PDM 是一款轻巧的工具,但它却涵盖了 Python 开发中的各种场景,例如自动生成项目的 pyproject.toml,自动解决 package 的版本依赖问题,就算我还未使用很久,也已经为之着迷了。

n

  PDM 提供的各种功能

很值得注意的是 PDM 提供了 build 的功能,能够将源码构建为 Python package 并发布到 PyPI,不再需要其他繁琐的工具,我也抱着极大的兴趣探索了结合 PDM 与 GitHub Actions 发布 Python 包的方法。PDM 提供了多平台的多种安装方法,读者可以根据自己的要求安装,本文在 Windows 上使用 pdm==2.15.0 作为演示。

初始化项目

安装好 PDM 后,新建项目文件夹,用终端进入文件夹并执行 pdm init 初始化项目,在这里我使用的是 PowerShell。PDM 会通过几个问答来指引用户初始化项目:

> pdm init
Creating a pyproject.toml for PDM...
Please enter the Python interpreter to use
 0. cpython@3.9 (F:\Miniconda\python.EXE)
 1. cpython@3.12 (C:\Users\Leo\scoop\apps\python\current\python.exe)
 2. cpython@3.12 (C:\Users\Leo\scoop\shims\python3.exe)
 3. cpython@3.9 (D:\Python39\python.exe)
 4. cpython@3.7 (D:\Python37\python37.exe)
 5. cpython@3.7 (D:\Python37\python.exe)
Please select (0): 3
Virtualenv is created successfully at D:\Code\gh-action-demo\.venv
Project name (gh-action-demo):leo-gh-action-demo
Project version (0.1.0):
Do you want to build this project for distribution(such as wheel)?
If yes, it will be installed by default when running `pdm install`. [y/n] (n): y
Project description ():
Which build backend to use?
0. pdm-backend
1. setuptools
2. flit-core
3. hatchling
Please select (0):
License(SPDX name) (MIT):
Author name (Leo):
Author email (im.yczeng@foxmail.com):
Python requires('*' to allow any) (>=3.9):
Project is initialized successfully

大部分设置项可以直接回车,选用括号中的默认值即可,比较重要的两项是

  • PDM 会自动搜寻设备上的 Python 解释器,需要根据需求选择项目的 Python 版本,如果输出中没有列出目标的 Python,就要检查 PDM 还是 Python 没有正确安装;
  • 选择项目 build backend 为 pdm-backend,这一点没有特别的原因,只是因为我们在用 PDM 所以就都用 PDM 的 toolkit 好啦。

以上步骤都完成后,项目文件夹的结构会是

D:\CODE\GH-ACTION-DEMO
│  .gitignore
│  .pdm-python
│  pyproject.toml
│  README.md
├─.venv
├─src
│  └─gh_action_demo
└─tests

.venv 是项目的虚拟环境,src 是项目源码目录,tests 用于存放测试文件。pyproject.toml 保存了项目的配置项,也包括 PDM 初始化时的配置,里面的内容也可以根据开发的需求手动修改:

[project]
name = "leo-gh-action-demo"
version = "0.1.0"
description = "Default template for PDM package"
authors = [
    {name = "Leo", email = "im.yczeng@foxmail.com"},
]
dependencies = []
requires-python = ">=3.9"
readme = "README.md"
license = {text = "MIT"}

[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"

[tool.pdm]
distribution = true

从零开始的开发

如果在项目开始之前就选用了 PDM 管理环境那当然最好不过,直接在 src/<project_name> 目录中写入第一行代码吧。完成后执行 pdm build 开始构建,这个过程中 PDM 会生成以下内容:

D:\CODE\GH-ACTION-DEMO
├─.pdm-build
│  │  .gitignore
│  └─gh_action_demo-0.1.0.dist-info
│          METADATA
│          WHEEL
├─dist
│      gh_action_demo-0.1.0-py3-none-any.whl
│      gh_action_demo-0.1.0.tar.gz
...

.pdm-build 是构建过程产生的中间文件,dist 目录中就是可以用于正式发布的 Python package 压缩包了。让我们查看一下 PDM 帮助打包了项目中的哪些内容:

> tar -tf .\dist\gh_action_demo-0.1.0.tar.gz
gh_action_demo-0.1.0/README.md
gh_action_demo-0.1.0/pyproject.toml
gh_action_demo-0.1.0/src/gh_action_demo/__init__.py
gh_action_demo-0.1.0/src/gh_action_demo/main.py
gh_action_demo-0.1.0/tests/__init__.py
gh_action_demo-0.1.0/PKG-INFO

可以看到压缩包是很规范的结构,唯一不太妙的地方在于 PDM 将 tests 目录也打包在内,测试文件中可能会包含体积比较大的数据文件,这对于使用 package 的用户是没有必要的。因此可以在 pyproject.toml 中添加以下的配置项,手动指定不纳入 package 的目录或文件:

[tool.pdm.build]
excludes = ["tests/"]

再次运行 pdm build,可以发现新构建的压缩包已经把 tests/ 排除在外了。

从已有项目迁移

很多情况下,我们并不是一开始就使用 PDM 的项目结构,例如我的项目也是开发了一半时才换用 PDM 管理。如果项目比较简单,可以调整项目的结构,在 src/ 中组织源码,那么构建的方法就和前文一致了。如果项目结构比较复杂或是有特殊需求而不能将源码调整至 src/ 中,那么就需要在 pyproject.toml 中手动设置打包的源码目录,例如:

[tool.pdm.build]
includes = ["main/", "plugins/"]
excludes = ["tests/"]

运行 pdm build 即可将 main/plugins/ 目录打包。

我们在本地完成了 package 的构建,dist 目录中生成的 *.tar.gz*.whl 就已经可以借助各种工具发布到 PyPI。不过更优的方法是完成开发后直接将代码推送至 GitHub 仓库,借助 GitHub Actions 的功能在云上完成构建并自动发布。

不论何种方法,在发布之前都要先注册 PyPI 和 TestPyPI 的账号。PyPI 是安装 Python package 的常用仓库,不用作多余的介绍。TestPyPI 是 PyPI 的测试仓库,主要用于测试发布、安装 package 是否正常,可视作在 PyPI 正式发布前的「预览版」。二者的流程完全相同,唯一不同在于数据、账号并不通用,必须分别注册。二者注册过程中包括开启二次验证等操作,步骤稍有些复杂,但很容易能够查找到详尽的教程,在此不再赘述。

 Note 因为本文用于演示发布的流程,后文主要以 TestPyPI 作为示例,设置项中与 PyPI 的细微区别以提示的方式给出。

登录 TestPyPI 账号后进入发布页面,我们选用 Trusted Publisher 的方式发布。

n

拖动页面至最底部,在「Add a new pending publisher」中选择「GitHub」,按要求填入 Publisher 的信息:

n

必填项包括以下 4 项:

  • PyPI Project Name:在 TestPyPI(PyPI)中索引的 package 名称,必须与 pyproject.toml 中的 name 一致;
  • Owner:GitHub 用户名;
  • Repository name:GitHub 仓库名;
  • Workflow name:GitHub Actions 文件名,GitHub Actions 通过 YAML 文件配置,所以后缀应为 .yml.yaml

完成后选择「Add」,就能看到准备中的 Publisher:

n

添加 GitHub Actions

在 GitHub 中创建仓库,注意仓库名应与 Publisher 中设置的仓库名完全一致。在推送代码之前,我们还需要在项目中添加 GitHub Actions 的配置。

在项目文件夹中添加 .github/workflows/ 文件夹,在其中新建一个 YAML 文件,注意文件名应与 Publisher 中设置的「Workflow name」一致。在 YAML 文件中添加配置,对 PDM 官方文档提供的模版稍作修改并逐行添加了注释:

# 显示在 GitHub UI 中的 workflow 名称
name: Publish Python distributions to TestPyPI

# 触发条件,在任何分支有 push 时触发该 workflow
on:
  push

# workflow 由若干个 job 组成,job 之间并行运行,此处只有 testpypi-publish 1 个 job
jobs:
  # job 标识名称
  testpypi-publish:
    # 显示在 GitHub UI 中的 job 名称
    name: upload release to TestPyPI
    # 运行 job 的计算机平台
    runs-on: ubuntu-latest
    # PDM 所需的权限
    permissions:
      # This permission is needed for private repositories.
      contents: read
      # IMPORTANT: this permission is mandatory for trusted publishing
      id-token: write
    # 每个 job 由若干 step 组成
    steps:
      # PDM 所需的环境配置
      - uses: actions/checkout@v3

      - uses: pdm-project/setup-pdm@v3

      # 运行命令行程序,通过 PDM 发布到 TestPyPI
      # `pdm publish` 命令先完成 `pdm build` 再将 `dist/` 中的内容发布
      - name: Publish package distributions to TestPyPI
        run: pdm publish --repository testpypi

 Note 若要将 package 发布到 PyPI 只需要将 workflow 中的 pdm publish --repository testpypi 改为 pdm publish

完成以上步骤后,再检查一下项目的文件结构,应当为

D:\CODE\GH-ACTION-DEMO
│  .gitignore
│  .pdm-python
│  pyproject.toml
│  README.md
├─.github
│  └─workflows
│          testpypi_publish.yml
├─.pdm-build
├─.venv
├─dist
├─src
│  └─gh_action_demo
│          main.py
└─tests

确认无误后就可以把项目推送到 GitHub 仓库了。

 Note 需要推送到 GitHub 仓库的仅有 .gitignore pyproject.toml README.md .github/ src/ tests/ 这几个文件与目录,.pdm-build/ .venv dist 都不应当上传,很方便的是 PDM 生成的 .gitignore 已经自动排除了这些留在本地的项目。

如果上述配置全部正确,打开 GitHub 的仓库中的「Actions」界面就能看到创建的 workflow 是否成功执行:

n

如果提示 workflow 全部完成,转至 TestPyPI 的项目界面就能找到刚发布的 package:

n

根据页面的提示,这时候通过 pip install -i https://test.pypi.org/simple/ leo-gh-action-demo 就能将发布的 package 安装到 Python 坏境中。

更复杂一些

好了,经过以上的步骤,我们就完成将 Python package 发布到 PyPI 的基本目标了。不过在实际中会有更复杂的 workflow,考虑这样的开发场景:

  1. 每个分支的每次 push 都要执行 pdm build 检查项目是否能构建;
  2. 每个打上版本号 tag(如 v1.2.1rc1)的 commit 都是准备发布的版本,需要自动创建 pre-release 并将其发布到 TestPyPI,用于测试各种功能;
  3. 确认无误后手动从 tag 创建正式 release,同时自动发布到 PyPI。

分析以上需求,可以划分为 2 个 workflow 实现:

  1. 测试构建 workflow
    • 由 push 触发 pdb build
      • 如果 push 中有版本 tag,则进一步执行 pdm publish,将构建文件发布到 GitHub release 和 TestPyPI;
      • 若无版本 tag,则结束 workflow。
  2. 正式发布 workflow
    • 由手动 release 触发 pdm build,自动下载 release 中的文件并执行 pdm publish,正式发布到 PyPI。

测试构建 workflow

按上文的步骤在 TestPyPI 中创建新的 Publisher,填入 workflow 的文件名,例如 build_py_dist.yml。接着在 .github/workflows 中创建同名文件,写入 workflow 内容:

name: Build Python distributions

# 由 push 触发
on:
  push

jobs:
  # 分别在不同平台构建 .whl 安装包
  build_whl:
    strategy:
      # 使用 matrix 组合在多种 OS 平台上完成 wheel 的构建
      matrix:
        os: [ubuntu-22.04, windows-2022]
        # 给对应的 OS 添加一个新变量 plat_name
        # Linux plat_name 中的 2_35 来自于 GNU libc 版本
        include:
          - os: ubuntu-22.04
            plat_name: manylinux_2_35_x86_64
          - os: windows-2022
            plat_name: win_amd64

    name: Build wheels on ${{ matrix.os }}
    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v3
      - uses: pdm-project/setup-pdm@v3
        with:
          architecture: x64

      # 查看 GNU libc 版本
      - run: ldd --version
      # 通过 PDM 构建 wheel
      - name: PDM build wheels
        # 指定 `--no-sdist` 参数时只生成二进制 wheel 文件
        run: pdm build --no-sdist --config-setting="--plat-name=${{ matrix.plat_name }}"

      # 将 dist/ 目录中的所有 .whl 文件上传暂存
      - uses: actions/upload-artifact@v4
        with:
          name: pdm-build-wheel-${{ matrix.plat_name }}
          path: dist/*.whl

  # 将源码打包为 tar.gz
  build_sdist:
    name: Build source distribution
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - uses: pdm-project/setup-pdm@v3

      # 通过 PDM 打包源码
      - name: PDM build source dist
        # 指定 `--no-wheel` 参数时只生成打包的源码
        run: pdm build --no-wheel

      # 将 dist/ 目录中的打包的源码上传暂存
      - uses: actions/upload-artifact@v4
        with:
          name: pdm-build-sdist
          path: dist/*.tar.gz

  # 使用构建文件完成 pre-release
  pre_release:
    name: Pre-release package distributions to GitHub
    # 只有 push 的 tag 以 "v" 起始时才运行该 job
    if: startsWith(github.event.ref, 'refs/tags/v')
    # 且在 build_whl 和 build_sdist 两个 job 完成的情况下执行
    needs: [build_whl, build_sdist]
    runs-on: ubuntu-latest
    permissions:
      contents: write

    steps:
    - uses: actions/checkout@v3
    # 每个 job 都会使用新的容器,需要将上传暂存的构件下载到 dist/ 目录
    - uses: actions/download-artifact@v4
      with:
        pattern: pdm-build-*
        path: dist
        merge-multiple: true
    # 使用 dist/ 目录中的文件创建一个 pre-release
    - uses: ncipollo/release-action@v1
      with:
        artifacts: "dist/*"
        prerelease: true

  # 将构建文件发布到 TestPyPI
  publish_pkg:
    name: Publish package to TestPyPI
    # 只有 push 的 tag 以 "v" 起始时才运行该 job
    if: startsWith(github.event.ref, 'refs/tags/v')
    needs: [build_whl, build_sdist]
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write

    steps:
    # 使用 PDM 发布已构建的文件
    - uses: actions/checkout@v3
    - uses: pdm-project/setup-pdm@v3
    - uses: actions/download-artifact@v4
      with:
        pattern: pdm-build-*
        path: dist
        merge-multiple: true
    # `pdm publish --no-build` 会自动发布 `dist` 中预先构建好的文件
    - name: Publish package distributions to TestPyPI
      run: pdm publish --no-build --repository testpypi

这个 workflow 需要执行的任务比较多,而且涉及了逻辑判断,看起来就比较复杂。各个步骤的介绍已经放在注释中,我想再简单介绍一下其中用到有意思的功能和要注意的细节。

strategy.matrix 可以对 n 组变量做笛卡尔积,组合变量创建出子任务;使用 include 可以为特定条件下的子任务引入新变量。将二者放在一起使用,就能实现一个简单的字典,通过键值对来指定变量。在上面的 workflow 中使用它们就是为了得到 {"ubuntu-22.04": "manylinux_2_35_x86_64", "windows-2022": "win_amd64"},根据不同的平台为 wheel 文件分配不同的后缀。

前文提过,每个 job 并行执行,所以需要使用 needs 来设定执行先决的 job,就能按需组织成任务序列。此外,每个 job 都会使用新的容器,也就是不同 job 间生成的文件是不互通的,因此我通过 upload-artifactdownload-artifact 两个 action 上传和下载文件,实现不同容器间文件的传输。

测试一下构建的情景,具体的步骤是:

  1. 修改 pyproject.toml 中的 version,例如 0.1.1b1;
  2. 通过 git push 推送 commit 到远端仓库。

n

  每次的 push 都会自动检查是否能构成功构建,因为没有 tag 条件并不执行 publish 任务

测试发布具体的步骤是:

  1. 修改 pyproject.toml 中的 version,例如 0.1.1b1;
  2. 通过 git tag v0.1.1b1 为当前分支最新 commit 打上版本 tag;
  3. 通过 git push origin v0.1.1b1 推送 tag 到远端仓库。

n

  每次的 push tag 都会在构建完成后自动发布到 pre-release

n

  同时也会自动发布到 TestPyPI

 NotePEP 440 制定了 Python package 的版本号规范,在 PyPI 上发布自己的 package 时理应也要按该规范设定版本。

正式发布 workflow

相比之下,正式发布的工作流程就简单了许多,就不作过多介绍了。同样需要在 PyPI 中添加 Publisher,在 .github/workflows 中创建 publish_pypi.yml,写入以下内容:

name: Publish distributions to PyPI

# 由正式 release 触发
on:
  release:
    types: [released]
jobs:
  # 将构建文件发布到 PyPI
  publish_pkg:
    name: Publish package to PyPI
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write

    steps:
    - uses: actions/checkout@v3
    - uses: pdm-project/setup-pdm@v3
    # 从 release 中下载所有文件到 dist/ 目录
    - uses: robinraju/release-downloader@v1.10
      with:
        latest: true
        fileName: "*"
        out-file-path: "dist"
    # 使用 PDM 将下载的文件发布到 PyPI
    - name: Publish package distributions to PyPI
      run: pdm publish --no-build

正式发布的具体步骤就是:

  1. 在 GitHub 仓库的 release 中找到正式版本的 pre-release;
  2. 编辑 pre-release,将其改为 release,提交后就会自动发布到 PyPI。

References