Julia 模块开发 - 远程开发

上个帖子介绍了模块开发的基础知识,本篇介绍 PkgTemplates.jl 和远程开发相关知识,包括 GitHub 部署,测试文档覆盖率和包的注册等。

PkgTemplate

推荐阅读:PkgTemplates 官方文档

一个完整的 Julia 模块应包括:主代码测试文档等,实际上,上篇介绍的这些内容用 PkgTemplates 就能“一键生成”。

  1. 调用模块

    # using Pkg; Pkg.add("PkgTemplates")
    using PkgTemplates
    
  2. Template 函数设置模板参数,并存入变量 t

    t = Template(;
        dir=".",
        plugins=[
          License(; name="MIT"),
          Git(; manifest=false, ssh=true),
          GitHubActions(; x86=true, coverage=true),
          Codecov(),
          Documenter{GitHubActions}()
        ])
    

    参数释义:

    • dir 指定创建模块的位置,默认为 ~/.julia/dev
    • plugins 设置接口,也即常用的开发工具
      • License 通过 name 参数选择了 MIT 许可证
      • Git 设置忽略 Mainfest.toml,且用 ssh 链接 GitHub 仓库,默认方式是 https
      • GitHubActions 启用对 x86 机器的支持,并启用 coverage 计算测试覆盖率
      • Codecov() 对应上一选项的 coverage=true
      • Documenter 指定通过 GitHubActions 部署文档

    每个参数更细致的用法参考 PkgTemplates 文档的 这一节

  3. 输入 t("<模块名>") 创建模块
    image

  4. 查看模块文件结构

    TestPackage/
    ├── docs
    │   ├── make.jl
    │   ├── Manifest.toml
    │   ├── Project.toml
    │   └── src
    │       └── index.md
    ├── LICENSE
    ├── Manifest.toml
    ├── Project.toml
    ├── README.md
    ├── src
    │   └── TestPackage.jl
    └── test
       └── runtests.jl
    

    以及隐藏文件夹 .github/

    TestPackage/.github/
    └── workflows
       ├── CI.yml
       ├── CompatHelper.yml
       └── TagBot.yml
    

Template(;<参数设置>)("包名") 一行命令,生成内容包括:

  • Git 环境,初始 commit 和模板 commit
  • 仓库常用的 README.mdLICENSE.gitignore
  • 代码文档测试 src/docs/test/
  • 代码环境文件 Project.tomlManifest.toml
  • GitHub Actions 的配置文件 .github/workflows/

其他文件已介绍过就不再赘述,重点是 GitHub Actions 的配置文件:

  • CompatHelper.yml:自动检查和更新依赖版本,其会根据依赖环境的版本变化,向仓库提交 PR
  • TagBot.yml:自动打包并发布新版本,在注册包的时候发挥作用
  • CI.yml:CI 全称 Continuous Integration 的缩写,即持续集成

前两个文件一般不需要修改,最后一个 CI.yml 涉及参数较多,通常需要根据需求修改,我们放在附录详细介绍。

测试,文档和覆盖率

代码测试

  1. Julia 规定测试代码放在 test/ 目录下的文件 runtests.jl 中,在包模式下执行 test 会自动运行 test/runtest.jl 文件

    ; # 进入 shell 模式
    mkdir test
    touch test/runtests.jl
    

    image

  2. 编写测试样例,用 @testset 打包测试样例

    using Test
    @testset "test 1" begin
        @test 1/0 == Inf # 测试正确
        @test_throws DivideError 1 ÷ 0 # 报错测试
        @test 1 == 2 # 测试出错
    end
    

  3. 测试通常需要调包,比如 Test,所以也需要配置环境:可以在 test 目录下生成 Project.toml 文件

    ] # 进入包管理模式
    activate test # 切换环境到 test 目录
    add Test # 添加测试库 Test
    

    也可以在主目录 Project.tomlextras 中添加相应依赖,比如

  4. 在包模式下执行 test 报错,排除代码错误后,很大可能是 Manifest.toml 问题,如非必须删除即可,比如cannot merge projects错误

帮助文档

文档通常放在 docs/ 目录下,Julia 用得最广的文档工具为 Documenter.jl

  1. Documenter.jl 约定用 docs/make.jl 作为文档的主代码文件

    ; # 进入 shell 模式
    mkdir docs
    touch docs/make.jl
    
  2. 类似地,为文档设置开发环境

    ] # 进入包管理模式
    activate docs # 切换环境到 docs 目录
    add Documenter
    
  3. QRDecoders 为例,在 docs/make.jl 中添加如下内容

    DocMeta.setdocmeta!(QRDecoders, :DocTestSetup, :(using QRDecoders); recursive=true)
    
    makedocs(;
        modules=[QRDecoders],
        sitename="QRDecoders.jl"
    )
    
    deploydocs(;
        repo="github.com/JuliaImages/QRDecoders.jl",
    )
    

    其中 deploydocs 设置文档自动部署,对应第一部分 PkgTemplates 生成 CI.yml 文件的 docs 字段。此外文档部署涉及写入操作还需要设置密钥,同样也放在附录介绍。

  4. 文档默认主页面根据 docs/src 目录下的 index.md 进行渲染。文件内容支持许多规则,比如用 @docs 和函数名可以将函数的帮助文档加入到页面中,如下图

    对应到网页上,每个函数每个派发的文档都会展示出来

  5. 除了通过 GitHub 部署网页,也可以在本地生成预览,命令如下

    julia --project=docs/  docs/make.jl
    

    激活 docs 环境并执行 make.jl 文件,执行后将在 docs/build 目录下生成网页,执行下边命令并打开浏览器

    python3 -m http.server --directory docs/build/ 8181
    

    其中 8181 为自定义的端口号,在网页中输入 http://localhost:8181 即可预览

网页是基于 Markdown 语法编写的,相关规则以及 Julia 中的特殊用法推荐看社区的帖子:Markdown.jl 使用总结或者 Julia 官方的 Markdown 手册

代码覆盖率

CodeCov 是非常实用的开发工具,很多开源仓库都用它来查看主代码文件被测试覆盖的比例,其对应由 CI.yml 文件中的 julia-actions/julia-processcoverage@v1 触发 。如果覆盖率降低会发出提示:
image

提示显示了主文件覆盖率的变化
深度截图_选择区域_20221029091711

点击查看代码,标红说明该行代码未被测试覆盖,也即存在错误的可能但未被测试捕捉
深度截图_选择区域_20221029090639

上图说明 add! 函数的测试样例不够全面,没有处理 val = 0 的情况。如果这里 deleteat! 错写成 delete!,错误也不会被发现。尽管bug 可能在之后被发现,但用 CodeCov 能提前规避潜在的错误。

一般地,代码覆盖率更高,说明作者代码认真做了测试,仓库可靠性也更高

除此之外还有性能测试 bench.jl 等等很多可选内容,视需要再进一步学习。

包注册

推荐阅读
Julia 包注册说明:General registry README
自动合并说明: AutoMerge guidelines
注册机器人:Registrator
模块命名规范:Package naming guidelines

这部分强烈建议看官方文档,包括常见 FAQ,以及自动合并的规则,帖子只简单介绍注册流程,遇到问题再查阅官方说明就行了。

JuliaRegistries/General 是 Julia 包的注册表,其维护关于 Julia 包的信息,例如版本、依赖项和兼容性限制。

一般地,我们通过 JuliaRegistrator 向仓库提交 PR,更新包相关的 .toml 文件。相关文件并入主分支后,用户就能通过 Pkg.add 安装这个包了。

目前等待期如下:

  • 新的 Julia 包:3 天(这让社区有时间反馈)
  • 现有软件包的新版本:15 分钟
  • JLL 包(二进制依赖项):15 分钟,对于新包或新版本

总之,包在第一次注册如果满足自动合并的要求,等待三天后就会自动合并仅仓库,此后升级小版本只需要等待 15 分钟。

注册机器人

进入包所在 GitHub 仓库,通过 @JuliaRegistrator 提出注册请求。

  1. 方法一:在 issue 中输入 @JuliaRegistrator register,机器人就会根据 Project.toml 内容,自动生成 PR,并回复注册信息

  2. 方法二:打开最近的一个 commit,选择一行或者在底部输入消息:
    image

  3. 方法三:在 JuliaHub 中点击注册


    这种方式注册的包可以不局限 GitHub 仓库

如果触发机器人后,因为测试问题没有通过,则在个人仓库修改密码并重新输入 @JuliaRegistrator register 触发机器人。

自动合并

官方列举了自动合并的要求,这里挑几个例子:

  1. 包名称应以大写字母开头,仅包含 ASCII 字母数字字符,并且至少包含一个小写字母。
  2. 名称长度至少为 5 个字符。
  3. 名称不包括“julia”或以“Ju”开头。
  4. 有一个上限 [compat] 条目 julia 版本,它只包括有限数量的 Julia 重大版本。
  5. 依赖项:所有依赖项都应具有 [compat]上限条目
  6. 许可证:包应该有一个 OSI 批准的软件许可证,位于包代码的顶级目录中,例如在一个名为LICENSE或的文件中LICENSE.md
  7. 为防止名称相似的包之间的混淆,新包的名称还必须满足以下三项检查:
    • 包名称与任何现有包的名称之间的最小编辑距离必须至少为 3
    • 包名称的小写版本与任何现有包名称的小写版本之间的最小编辑距离必须至少为 2。
    • 包名称和任何现有包之间的VisualStringDistances.jl 的视觉距离必须超过某个手动选择的阈值(当前为 2.5)

值得留意的几点,包需要符合命名规范;确保不会有相近命名的包;确保包的依赖项都有 [compat] 条目(自带库比如 SparseArray 等不需要指定)。

如果自动合并失败,可以在 PR 中说明原因,然后等待人工审核。

TagBot

包注册成功后,最开始用 PkgTemplates.jl 生成的 TagBot.yml 会在仓库自动生成 Release。此时,仓库的 tag 标签会记录该版本的状态

GitHub 右侧也可以看到 Release 的状态

小结

一般场景中,先用 PkgTemplates.jl 生成代码文件和部署文件,然后 DocumenterTools 生成密钥并添加到远端,剩下就是常规的代码编写了,非常简单。
这些是最基础的用法,比如 PkgTemplates.jl 还能制作模板,文档测试还有其他工具等等,未来可以再一边摸索。

附录

CI 文件

CI/CD 全称为 Continuous Integration/ Continuous Deployment,连续集成与连续部署,常见平台

文件 平台
gitlab-ci.yml gitlab
.github/workflows/xxx.yml github
.travis.yml travis CI
.appveyor.yml appveyor CI

个人仅接触过 GitHub 平台,其他暂不讨论。下边以 QRDecoders (源文件)为例,分段理解,边用边学,且只介绍比较重要或可能需要修改的部分。

第一部分,头部内容:

name: CI
on:
  push:
    branches:
      - master
    tags: '*'
  pull_request:
concurrency:
  # Skip intermediate builds: always.
  # Cancel intermediate builds: only if it is a pull request build.
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }}
  • name 是当前 GitHub Action 的小标题
  • on 的参数项 push: branches: 指定会触发测试的分支,即当我们向 branches 中的分支 push commit 时,将触发 CI。特别地,此处向 master 分支 push 会触发 CI。特别留意,PkgTemplates 生成 CI 文件的默认主分支名为 main,要根据仓库实际情况修改。

第二部分:模块测试的环境配置

jobs:
  test:
    name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }}
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        version:
          - '1.6'
          - '1.7'
        os:
          - ubuntu-latest
        arch:
          - x64
        include:
          - os: windows-latest
            version: '1'
            arch: x64
          - os: macOS-latest
            version: '1'
            arch: x64
          - os: ubuntu-latest
            version: '1'
            arch: x86

jobs: test 参数项下的 strategy,指定了测试环境,这里用了两种方式:

  • 前三个参数 version, os, arch 设置在 x64 架构的 ubuntu-latest 系统上,对 Julia 1.61.7 版本分别执行测试,共 2 * 1 * 1 = 2 个测试
  • 最后一个参数 include 的每条子项对应一个测试,分别指明了 Julia 版本和系统架构,version 默认取最新版本,比如目前 1 等同于 1.8.21.6 等同于 1.6.7
  • 测试系统设置越多,每次触发 CI 需要等待的时间可能就越长,所以通常还要根据实际情况调整

此外, name 字段指定了 GitHub Actions 子项的名称由,如下图
image

第三部分,测试内容:

#jobs:
#  test:
    steps:
      - uses: actions/checkout@v2
      - uses: julia-actions/setup-julia@v1
        with:
          version: ${{ matrix.version }}
          arch: ${{ matrix.arch }}
      - uses: julia-actions/cache@v1
      - uses: julia-actions/julia-buildpkg@v1
      - uses: julia-actions/julia-runtest@v1
      - uses: julia-actions/julia-processcoverage@v1
      - uses: codecov/codecov-action@v2
        with:
          files: lcov.info

jobs: test: steps 部分参数释义:

  • julia-actions/setup-julia@v1 启动 Julia
  • julia-actions/julia-buildpkg@v1 编译 Julia 依赖环境
  • julia-actions/julia-runtest@v1 执行测试,也即仓库下的 test/runtests.jl 文件
  • julia-actions/julia-processcoverage@v1 计算覆盖率,主要为 Codecov 服务提供数据
  • codecov/codecov-action@v2 触发 Codecov 服务,生成覆盖率报告

对应到仓库中,显示信息如下

第四部分,文档参数:

#jobs:
#  test:
  docs:
    name: Documentation
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v2
      - uses: julia-actions/setup-julia@v1
        with:
          version: '1.6'
      - uses: julia-actions/julia-buildpkg@v1
      - uses: julia-actions/julia-docdeploy@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - run: |
          julia --project=docs -e '
            using Documenter: DocMeta, doctest
            using QRDecoders
            DocMeta.setdocmeta!(QRDecoders, :DocTestSetup, :(using QRDecoders); recursive=true)
            doctest(QRDecoders)'

jobs: docs: 部分参数释义:

  • name 指定 GitHub Actions 子项的名称
  • runs-on: ubuntu-latest 指定在 ubuntu-latest 系统上生成文档
  • julia-actions/julia-docdeploy@v1 这部分应该是在执行 run 参数的内容,实际执行内容与 docs/make.jl 有关
  • 特别留意一项 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }},由于文档部署涉及对仓库的读写,这里要设置 GITHUB_TOKEN,允许 Documentergh-pages 分支的读写
  • 其他参数与上一部分类似

对应到仓库中,显示信息如下

总之,留意 push 的默认分支,根据需求修改测试数目,其他基本不需要改动。

关于 Documenter 的使用,以及 GITHUB_TOKEN 的设置,我们接下来进行介绍。

DocumentTools

推荐阅读:Documenter 文档 以及 DocumenterTools 文档

  1. 激活当前环境,输入模块名称,生成密钥

    using <包名称>
    using DocumenterTools
    DocumenterTools.genkeys(<包名称>)
    

    注意不是输入字符串,而是将当前环境的 模块名 作为参数

  2. 根据提示内容复制第一部分密钥,建议先用 git 连接远端的 GitHub 仓库,这一来可以点击 Info 的链接直接跳转

  3. 进入仓库,点击设置,找到 Deploy keys 粘贴刚刚复制的内容

    注意 ssh 密钥会赋予 Documenter 写入权限,所以注意是设置仓库密钥,不要设置成个人账号的密钥

  4. 其他参数参见 DocumenterTools 文档,比如修改 user 参数

    DocumenterTools.genkeys(; user="用户名", repo="仓库名")
    

以上,为模块开发的第二部分内容。

8 个赞

:+1:,楼主辛苦了,最近正疑惑这些东西呢

我之前也困惑过,然后发了帖子:Julia 开发包的基本流程

通过项目开发,慢慢就懂了,可以自己建一个包,或者看看置顶帖里提到的项目: 开源培训计划 (GSoC/JSoC/OSPP)

补充例子

上个帖子介绍基础知识,实际开发并不需要从头构建,直接用 PkgTemplates 就行了,补一个快速上手的例子。

准备工作

创建 Github 仓库,比如 BadApples.jl。如果已有仓库,可以创建模板后,再将环境文件移动到已有仓库中。设置 Git 默认信息

git config --global github.user 用户名
git config --global user.name 用户名
git config --global user.email 邮箱

后两项将被自动填入 LICENSE,github.user 可选,若不设置则需在定义模板时指明 user

创建模块

  1. 设置模板

    # using Pkg; Pkg.add("PkgTemplates")
    using PkgTemplates
    t = Template(;
         dir=".",
         user="RexWzh", # Github 账号,默认读取 git config
         host="github.com" # 默认情况
         plugins=[
             License(; name="MIT"),
             Git(; manifest=false, ssh=true),
             GitHubActions(; coverage=true),
             Codecov(),
             Documenter{GitHubActions}()
         ])
    


    模板概要:

    • 在当前目录下创建模块
    • 使用 MIT 协议
    • 通过 ssh 链接远端仓库,默认忽略 Manifest.toml 文件
    • 启用 CodeCov 计算代码覆盖率
    • 使用 GitHub Action 部署文档
  2. 创建模块

    t("BadApples")
    

  3. 查看创建结果

    cd BadApples
    tree # 查看目录
    git log --oneline # 查看提交历史
    git remote -v # 查看远程链接
    

设置远端仓库

  1. 提交更改到远程仓库,这一步要确保 GitHub 远程已设置 ssh 密钥

    git push --set-upstream origin main
    

    提交触发了 GitHub Action

  2. 配置文档密钥

    # using Pkg; Pkg.add("DocumentTools")
    using DocumentTools
    DocumenterTools.genkeys(; user="RexWzh", repo="BadApples")
    

    按提示打开链接 https://github.com/用户名/仓库名/settings/keys 设置仓库密钥,粘贴 rsa 密钥,给予 Documenter 写入权限

  3. 点击设置,选择 pages 分支设为 gh-pages

剩下就是常规代码开发了,包的注册同前边讨论

3 个赞