Julia 笔记系列:

唠唠闲话

上篇介绍了模块开发的基础知识,本篇介绍一些远程开发相关的内容,包括 GitHub 部署,测试文档覆盖率和包的注册等。

概要

本篇内容包括:

模板生成

PkgTemplate

推荐阅读:PkgTemplates 官方文档

一个完整的 Julia 模块应包括:主代码,代码测试和帮助文档等内容,而这些用 PkgTemplates 就能“一键生成”。

  1. 调用模块

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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("<模块名>") 创建模块
    深度截图_选择区域_20221026161913

  4. 查看模块文件结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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/

    1
    2
    3
    4
    5
    TestPackage/.github/
    └── workflows
    ├── CI.yml
    ├── CompatHelper.yml
    └── TagBot.yml

只用 Template(;<参数设置>)("包名") 这一行命令,上篇提到的内容就都生成了,包括:

  • Git 环境及初始 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 涉及参数较多,且通常需要根据需求修改,下边着重介绍。

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 (源文件)为例,分段理解,边用边学,且只介绍比较重要或可能需要修改的部分。

第一部分,头部内容:

1
2
3
4
5
6
7
8
9
10
11
12
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,要根据仓库实际情况修改。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 子项的名称由,如下图
深度截图_选择区域_20221026170249

第三部分,测试内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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 服务,生成覆盖率报告

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

第四部分,文档参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#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 分支的读写
  • 其他参数与上一部分类似

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

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

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

DocumentTools

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

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

    1
    2
    3
    using <包名称>
    using DocumenterTools
    DocumenterTools.genkeys(<包名称>)

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

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

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

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

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

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

docs/make.jl 和文档 docs/index.md 的内容,下一章节一块介绍。

测试,文档和覆盖率

代码测试

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

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

    深度截图_选择区域_20220424162332

  2. 设置测试环境,也可以在 Project.tomlextras 中添加相应依赖

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

    如果在包环境下执行 test 报错 cannot merge projects,可能是测试环境的 Manifest.toml 与主环境重复了,删除即可,参考 GitHub 的讨论

帮助文档

文档通常放在 docs/ 目录下,Julia 用得最广的文档工具为 Documenter.jl,其他工具暂不介绍。

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

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

    1
    2
    3
    ] # 进入包管理模式
    activate docs # 切换环境到 docs 目录
    add Documenter

    如果包未注册,可能需要添加 dev . 将开发中的包加入到 doc 环境依赖中,并添加 Manifest.toml 到 Git 记录

  3. QRDecoders 为例,在 docs/make.jl 中添加如下内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    DocMeta.setdocmeta!(QRDecoders, :DocTestSetup, :(using QRDecoders); recursive=true)

    makedocs(;
    modules=[QRDecoders],
    sitename="QRDecoders.jl"
    )

    deploydocs(;
    repo="github.com/JuliaImages/QRDecoders.jl",
    )

    其中 deploydocs 设置文档部署的仓库地址

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

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

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

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

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

    1
    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 触发 。如果覆盖率降低会发出提示:
深度截图_选择区域_20221029091036

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

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

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

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

除此之外还有性能测试 bench.jl 等等很多可选内容,后续接触再进一步学习。
深度截图_选择区域_20221027120500

包注册

推荐阅读
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,并回复注册信息
    20221028220206

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

  3. 方法三:在 JuliaHub 中点击注册
    深度截图_选择区域_20221028220433
    这种方式注册的包可以不局限 GitHub 仓库
    20221028220519

如果触发机器人后,因为测试问题没有通过,则在个人仓库修改密码并重新输入 @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 标签会记录该版本的状态
20221028223040

GitHub 右侧也可以看到 Release 的状态
深度截图_选择区域_20221028223128

小结

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


附录-函数文档

这部分参考了 Document 官网教程,可以看成 Documenter 前几节的翻译,更建议直接看官方文档。

  1. Julia 允许包开发人员能轻松地为函数、类型和其他对象编写文档。基本语法很简单:出现对象(函数、宏、类型或实例)顶部编写的字符串将被解释为文档(docstrings)。请注意,文档字符串和文档对象之间不能有空行或注释。这是一个基本示例:

    1
    2
    "Tell whether there are too foo items in the array."
    foo(xs::Array) = ...
  2. 这是一个更复杂的例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    """
    bar(x[, y])

    Compute the Bar index between `x` and `y`.

    If `y` is unspecified, compute the Bar index between all pairs of columns of `x`.

    # Examples
    ```julia-repl
    julia> bar([1, 2], [1, 2])
    1
    ```(排版原因忽略这个括号及内容)
    """
    function bar(x, y=1) end

    显示效果如下:
    20220507195430

  3. 补充说明:

    • 顶部前边放四个空格,可将函数名高亮,当第二参数可选时,应写为 bar(x[, y])
    • 在函数签名后,用单行句描述函数用途,如果需要的话,在一个空行之后,在第二段提供更详细的信息。撰写函数的文档时,单行语句应使用祈使结构(比如「Do this」、「Return that」)而非第三人称(不要写「Returns the length…」),并且应以句号结尾。如果函数的意义不能简单地总结,更好的方法是分成分开的组合句
    • 不要自我重复:因为签名给出了函数名,所以没有必要用「The function bar…」开始文档:直接说要点。类似地,如果签名指定了参数的类型,在描述中提到这些是多余的。
  4. # Arguments 下插入参数列表:只在确实必要时提供参数列表,对于简单函数,直接在函数目的中描述更合适。但对于拥有多个参数的(特别是含有关键字参数的)复杂函数来说,提供一个参数列表是个好主意,比如

    1
    2
    3
    # Arguments
    - `n::Integer`: the number of elements to compute.
    - `dim::Integer=1`: the dimensions along which to perform the computation.

    深度截图_选择区域_20220507200629

  5. See also: 提供关联函数的引用,比如

    1
    See also [`bar!`](@ref), [`baz`](@ref), [`baaz`](@ref).
  6. # Examples 中包含一些代码例子,示例应尽可能按 doctest 来写,具体在 Documenter 中一并介绍,示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    """
    Some nice documentation here.

    # Examples
    ```jldoctest
    julia> a = [1 2; 3 4]
    2×2 Array{Int64,2}:
    1 2
    3 4
    ```(排版原因忽略这个括号及内容)
    """

    深度截图_选择区域_20220507202119

  7. 使用反引号表示代码和方程,LaTeX 方程使用两个反引号,比如

    1
    2
    `a = 1`
    ``α = 1``

    深度截图_选择区域_20220507202804

  8. 结尾的 """ 应单独一行(字符串末尾的空白字符会被自动 strip 掉)

  9. 代码中应遵守单行长度限制,即建议 92 个字符后换行

  10. 对长文档字符串,可以考虑使用 # Extend help 来拆分文档

  11. 函数存在多个方法时,每个方法会用函数 catdoc 拼接

以上。