上个帖子介绍了模块开发的基础知识,本篇介绍 PkgTemplates.jl 和远程开发相关知识,包括 GitHub 部署,测试文档覆盖率和包的注册等。
PkgTemplate
推荐阅读:PkgTemplates 官方文档
一个完整的 Julia 模块应包括:主代码,测试和文档等,实际上,上篇介绍的这些内容用 PkgTemplates
就能“一键生成”。
-
调用模块
# using Pkg; Pkg.add("PkgTemplates") using PkgTemplates
-
用
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 仓库,默认方式是 httpsGitHubActions
启用对x86
机器的支持,并启用coverage
计算测试覆盖率Codecov()
对应上一选项的coverage=true
Documenter
指定通过 GitHubActions 部署文档
每个参数更细致的用法参考 PkgTemplates 文档的 这一节。
-
输入
t("<模块名>")
创建模块
-
查看模块文件结构
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.md
,LICENSE
和.gitignore
- 代码文档测试
src/
,docs/
和test/
- 代码环境文件
Project.toml
和Manifest.toml
- GitHub Actions 的配置文件
.github/workflows/
其他文件已介绍过就不再赘述,重点是 GitHub Actions 的配置文件:
CompatHelper.yml
:自动检查和更新依赖版本,其会根据依赖环境的版本变化,向仓库提交 PRTagBot.yml
:自动打包并发布新版本,在注册包的时候发挥作用CI.yml
:CI 全称 Continuous Integration 的缩写,即持续集成
前两个文件一般不需要修改,最后一个 CI.yml
涉及参数较多,通常需要根据需求修改,我们放在附录详细介绍。
测试,文档和覆盖率
代码测试
-
Julia 规定测试代码放在
test/
目录下的文件runtests.jl
中,在包模式下执行test
会自动运行test/runtest.jl
文件; # 进入 shell 模式 mkdir test touch test/runtests.jl
-
编写测试样例,用
@testset
打包测试样例using Test @testset "test 1" begin @test 1/0 == Inf # 测试正确 @test_throws DivideError 1 ÷ 0 # 报错测试 @test 1 == 2 # 测试出错 end
-
测试通常需要调包,比如
Test
,所以也需要配置环境:可以在test
目录下生成Project.toml
文件] # 进入包管理模式 activate test # 切换环境到 test 目录 add Test # 添加测试库 Test
也可以在主目录
Project.toml
的extras
中添加相应依赖,比如
-
在包模式下执行
test
报错,排除代码错误后,很大可能是Manifest.toml
问题,如非必须删除即可,比如cannot merge projects错误
帮助文档
文档通常放在 docs/
目录下,Julia 用得最广的文档工具为 Documenter.jl
。
-
Documenter.jl
约定用docs/make.jl
作为文档的主代码文件; # 进入 shell 模式 mkdir docs touch docs/make.jl
-
类似地,为文档设置开发环境
] # 进入包管理模式 activate docs # 切换环境到 docs 目录 add Documenter
-
以
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
字段。此外文档部署涉及写入操作还需要设置密钥,同样也放在附录介绍。 -
文档默认主页面根据
docs/src
目录下的index.md
进行渲染。文件内容支持许多规则,比如用@docs
和函数名可以将函数的帮助文档加入到页面中,如下图
对应到网页上,每个函数每个派发的文档都会展示出来
-
除了通过 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
触发 。如果覆盖率降低会发出提示:
提示显示了主文件覆盖率的变化
点击查看代码,标红说明该行代码未被测试覆盖,也即存在错误的可能但未被测试捕捉
上图说明 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
提出注册请求。
-
方法一:在 issue 中输入
@JuliaRegistrator register
,机器人就会根据Project.toml
内容,自动生成 PR,并回复注册信息
-
方法二:打开最近的一个 commit,选择一行或者在底部输入消息:
-
方法三:在 JuliaHub 中点击注册
这种方式注册的包可以不局限 GitHub 仓库
如果触发机器人后,因为测试问题没有通过,则在个人仓库修改密码并重新输入 @JuliaRegistrator register
触发机器人。
自动合并
官方列举了自动合并的要求,这里挑几个例子:
- 包名称应以大写字母开头,仅包含 ASCII 字母数字字符,并且至少包含一个小写字母。
- 名称长度至少为 5 个字符。
- 名称不包括“julia”或以“Ju”开头。
- 有一个上限
[compat]
条目 julia 版本,它只包括有限数量的 Julia 重大版本。 - 依赖项:所有依赖项都应具有
[compat]
上限条目 - 许可证:包应该有一个 OSI 批准的软件许可证,位于包代码的顶级目录中,例如在一个名为LICENSE或的文件中LICENSE.md
- 为防止名称相似的包之间的混淆,新包的名称还必须满足以下三项检查:
- 包名称与任何现有包的名称之间的最小编辑距离必须至少为 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
系统上,对 Julia1.6
和1.7
版本分别执行测试,共2 * 1 * 1 = 2
个测试 - 最后一个参数
include
的每条子项对应一个测试,分别指明了 Julia 版本和系统架构,version
默认取最新版本,比如目前1
等同于1.8.2
,1.6
等同于1.6.7
- 测试系统设置越多,每次触发 CI 需要等待的时间可能就越长,所以通常还要根据实际情况调整
此外, name
字段指定了 GitHub Actions 子项的名称由,如下图
第三部分,测试内容:
#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
启动 Juliajulia-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
,允许Documenter
对gh-pages
分支的读写 - 其他参数与上一部分类似
对应到仓库中,显示信息如下
总之,留意 push
的默认分支,根据需求修改测试数目,其他基本不需要改动。
关于 Documenter
的使用,以及 GITHUB_TOKEN
的设置,我们接下来进行介绍。
DocumentTools
推荐阅读:Documenter 文档 以及 DocumenterTools 文档
-
激活当前环境,输入模块名称,生成密钥
using <包名称> using DocumenterTools DocumenterTools.genkeys(<包名称>)
注意不是输入字符串,而是将当前环境的
模块名
作为参数 -
根据提示内容复制第一部分密钥,建议先用 git 连接远端的
GitHub
仓库,这一来可以点击Info
的链接直接跳转
-
进入仓库,点击设置,找到
Deploy keys
粘贴刚刚复制的内容
注意 ssh 密钥会赋予
Documenter
写入权限,所以注意是设置仓库密钥,不要设置成个人账号的密钥 -
其他参数参见 DocumenterTools 文档,比如修改
user
参数DocumenterTools.genkeys(; user="用户名", repo="仓库名")
以上,为模块开发的第二部分内容。