Julia 教程 / 构建与发布(Package 指南)
构建与发布(Package 指南)
Julia 的包管理系统 Pkg 是其核心优势之一。本文详细介绍如何创建、测试、文档化并发布一个 Julia 包到 General Registry。
1. Pkg 模块详解
1.1 进入 Pkg 模式
# 在 REPL 中按 ] 进入 Pkg 模式
# (v1.11) pkg>
# 或在代码中使用
using Pkg
1.2 常用 Pkg 命令
| 命令 | 说明 | 示例 |
|---|---|---|
add | 安装包 | Pkg.add("DataFrames") |
rm | 移除包 | Pkg.rm("DataFrames") |
up | 更新包 | Pkg.update() |
status | 查看已安装包 | Pkg.status() |
instantiate | 安装依赖 | Pkg.instantiate() |
resolve | 解析依赖冲突 | Pkg.resolve() |
test | 运行测试 | Pkg.test("MyPkg") |
build | 构建包 | Pkg.build("MyPkg") |
develop | 开发模式 | Pkg.develop("MyPkg") |
generate | 创建新包 | Pkg.generate("MyPkg") |
1.3 项目环境
# 激活项目环境
Pkg.activate(".")
# 查看当前环境
Pkg.status()
# 添加依赖到当前项目
Pkg.add(["DataFrames", "CSV", "Plots"])
# 添加开发依赖(仅测试/文档使用)
Pkg.add("Test"; target=:test)
2. 创建新包
2.1 使用 Pkg.generate
using Pkg
# 在指定路径创建新包
Pkg.generate("MyAwesomePkg")
# 创建 MyAwesomePkg/ 目录,包含 Project.toml 和 src/MyAwesomePkg.jl
2.2 推荐的包结构
MyAwesomePkg/
├── Project.toml # 项目元数据和依赖
├── Manifest.toml # 锁定的依赖版本(不提交到 git)
├── src/
│ ├── MyAwesomePkg.jl # 主模块文件
│ ├── core.jl # 核心功能
│ └── utils.jl # 工具函数
├── test/
│ └── runtests.jl # 测试文件
├── docs/
│ ├── make.jl # 文档构建脚本
│ └── src/
│ ├── index.md # 文档首页
│ └── api.md # API 参考
├── .gitignore
├── README.md
├── LICENSE
└── .github/
└── workflows/
└── CI.yml # GitHub Actions
2.3 主模块文件
# src/MyAwesomePkg.jl
module MyAwesomePkg
using LinearAlgebra
using Statistics
# 导出公共 API
export compute, analyze, MyResult
# 包含子文件
include("core.jl")
include("utils.jl")
# 文档字符串
"""
compute(x::AbstractVector)
计算向量的统计数据。
# 示例
```julia
result = compute([1.0, 2.0, 3.0])
""" function compute(x::AbstractVector) return MyResult(mean(x), std(x), length(x)) end
""" 存储计算结果的结构体。 """ struct MyResult mean::Float64 std::Float64 n::Int end
预编译提示
function init() println(“MyAwesomePkg loaded!”) end
end # module
---
## 3. Project.toml 详解
### 3.1 完整的 Project.toml
```toml
name = "MyAwesomePkg"
uuid = "12345678-1234-1234-1234-123456789abc"
version = "1.0.0"
authors = ["Your Name <[email protected]>"]
[deps]
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
[compat]
DataFrames = "1"
julia = "1.9"
[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[targets]
test = ["Test"]
3.2 各字段说明
| 字段 | 说明 | 必需 |
|---|---|---|
name | 包名 | ✅ |
uuid | 唯一标识符 | ✅ |
version | 语义化版本号 | ✅ |
authors | 作者信息 | ❌ |
[deps] | 依赖列表 | ❌ |
[compat] | 版本兼容性 | 推荐 |
[extras] | 测试依赖 | ❌ |
[targets] | 测试目标 | ❌ |
3.3 UUID 生成
using UUIDs
uuid4() # 生成随机 UUID
4. Manifest.toml
4.1 作用
Manifest.toml 记录了所有依赖(包括间接依赖)的精确版本和 Git commit hash。它是可重现构建的关键。
# Manifest.toml 片段(自动生成,不要手动编辑)
[[DataFrames]]
deps = ["Compat", "DataAPI", "InvertedIndices", "LinearAlgebra", ...]
git-tree-sha1 = "abc123..."
uuid = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
version = "1.6.1"
[[Compat]]
git-tree-sha1 = "def456..."
uuid = "34da2185-b29b-5c13-b0c7-acf172513d20"
version = "4.10.0"
4.2 何时提交 Manifest.toml
| 项目类型 | 提交 Manifest.toml? | 原因 |
|---|---|---|
| 库(被其他包依赖) | ❌ | 让使用者自行解析版本 |
| 应用(独立运行) | ✅ | 确保部署环境一致 |
| 文档项目 | ✅ | 确保文档构建可重现 |
5. 本地开发
5.1 开发模式
# 将本地包添加为开发依赖
Pkg.develop(path="/path/to/MyAwesomePkg")
# 或使用 URL
Pkg.develop(url="https://github.com/user/MyAwesomePkg.jl.git")
# 开发模式下,代码修改立即生效(无需重新安装)
5.2 Revise.jl — 热重载
using Revise
using MyAwesomePkg
# 修改 MyAwesomePkg 的代码后,更改自动加载
# 无需重启 Julia 或重新 import
5.3 工作流
# 1. 激活项目
Pkg.activate(".")
# 2. 开发模式添加本地包
Pkg.develop(path="../MyAwesomePkg")
# 3. 启用 Revise
using Revise
using MyAwesomePkg
# 4. 编辑代码,更改自动生效
# 5. 运行测试验证
Pkg.test("MyAwesomePkg")
6. 测试
6.1 编写测试
# test/runtests.jl
using MyAwesomePkg
using Test
@testset "MyAwesomePkg.jl" begin
@testset "compute" begin
# 基本功能
result = compute([1.0, 2.0, 3.0])
@test result.mean ≈ 2.0
@test result.n == 3
# 边界情况
result_single = compute([5.0])
@test result_single.mean ≈ 5.0
@test result_single.std ≈ 0.0
end
@testset "MyResult" begin
r = MyResult(1.0, 0.5, 10)
@test r.mean == 1.0
@test r.std == 0.5
@test r.n == 10
end
@testset "类型稳定性" begin
@inferred compute([1.0, 2.0, 3.0])
end
end
6.2 运行测试
# 在 Pkg 模式下
# (v1.11) pkg> test
# 或代码中
Pkg.test("MyAwesomePkg")
# 带覆盖率
Pkg.test("MyAwesomePkg"; coverage=true)
6.3 测试最佳实践
| 实践 | 说明 |
|---|---|
| 测试边界值 | 空数组、单元素、极大值 |
| 测试类型稳定性 | 使用 @inferred |
| 测试异常 | 使用 @test_throws |
| 测试近似值 | 使用 @test ... ≈ ... |
| 组织测试集 | 使用 @testset 嵌套 |
| 避免依赖外部资源 | Mock 数据库/网络调用 |
7. 文档
7.1 Documenter.jl 设置
# docs/make.jl
using Documenter
using MyAwesomePkg
makedocs(
sitename = "MyAwesomePkg.jl",
modules = [MyAwesomePkg],
pages = [
"首页" => "index.md",
"快速入门" => "getting-started.md",
"API 参考" => "api.md",
],
format = Documenter.HTML(
prettyurls = get(ENV, "CI", nothing) == "true"
)
)
# 部署到 GitHub Pages
deploydocs(
repo = "github.com/user/MyAwesomePkg.jl.git",
devbranch = "main"
)
7.2 文档字符串
"""
compute(x::AbstractVector; corrected::Bool=true) -> MyResult
计算向量的均值和标准差。
# 参数
- `x`: 输入数据向量
- `corrected`: 是否使用修正的样本标准差(默认 true)
# 返回值
返回 `MyResult` 结构体,包含 `mean`、`std`、`n` 字段。
# 示例
```julia
data = [1.0, 2.0, 3.0, 4.0, 5.0]
result = compute(data)
result.mean # 3.0
result.std # 1.5811...
另见
MyResult, analyze
"""
function compute(x::AbstractVector; corrected::Bool=true)
# 实现…
end
---
## 8. 注册包到 General Registry
### 8.1 注册流程
```julia
# 1. 确保包已推送到 GitHub
# git remote add origin https://github.com/user/MyAwesomePkg.jl.git
# git push -u origin main
# 2. 使用 Registrator 注册
# 访问 https://github.com/JuliaRegistries/Registrator.jl
# 在 GitHub issue 中 @JuliaRegistrator 注册
# 或本地使用
using Pkg
Pkg.Registry.add(RegistrySpec(url="https://github.com/JuliaRegistries/General"))
8.2 注册前检查清单
- 包名不与已有包冲突(搜索 JuliaHub)
-
Project.toml包含正确的name、uuid、version -
[compat]中指定了 Julia 版本要求 - 所有依赖的
[compat]已指定 - 测试通过
- 有 README.md
- 有 LICENSE 文件
- 代码已推送到 GitHub
8.3 版本更新
# 1. 修改版本号
# Project.toml: version = "1.1.0"
# 2. 提交并推送
# git commit -am "Bump version to 1.1.0"
# git tag v1.1.0
# git push --tags
# 3. 重新注册
# @JuliaRegistrator register
9. 语义化版本 (Semver)
9.1 版本号格式
MAJOR.MINOR.PATCH
│ │ └── 修复版本(向后兼容的 bug 修复)
│ └──────── 次版本(向后兼容的新功能)
└─────────────── 主版本(不兼容的 API 变更)
9.2 版本范围语法
| 表达式 | 含义 | 匹配版本 |
|---|---|---|
"1.0" | 兼容 1.0 | ≥1.0.0, <2.0.0 |
"1.2.3" | 兼容 1.2.3 | ≥1.2.3, <2.0.0 |
"~1.2" | 补丁级别 | ≥1.2.0, <1.3.0 |
"=1.2.3" | 精确版本 | 仅 1.2.3 |
">=1.0, <2" | 范围 | ≥1.0.0, <2.0.0 |
9.3 依赖版本管理
[compat]
julia = "1.9" # 支持 Julia 1.9 及以上
DataFrames = "1" # 支持 DataFrames 1.x
Plots = "1.38" # 支持 Plots 1.38.x 及以上(<2.0)
SomePackage = "=2.1.0" # 精确版本
⚠️ 注意:主版本 0 时,次版本表示不兼容变更。
"0.2"只匹配 0.2.x,不匹配 0.3.0。
10. 包模板 — PkgTemplates.jl
10.1 使用模板创建包
using Pkg
Pkg.add("PkgTemplates")
using PkgTemplates
t = Template(;
user="yourname",
plugins=[
License(; name="MIT"),
Git(; manifest=false),
GitHubActions(; coverage=true),
Documenter{GitHubActions}(),
Tests(; project=true),
Citation(; key="mykey"),
],
)
t("MyNewPkg")
10.2 可用插件
| 插件 | 功能 |
|---|---|
License | 添加 LICENSE 文件 |
Git | 初始化 Git 仓库 |
GitHubActions | CI 配置 |
Documenter | 文档系统 |
Tests | 测试框架 |
Citation | 引用文件 |
SrcDir | 源码目录 |
TagBot | 自动打标签 |
10.3 自定义模板
t = Template(;
user="yourname",
dir="~/julia-dev",
plugins=[
License(; name="MIT"),
Git(; manifest=false, ssh=true),
GitHubActions(;
coverage=true,
extra_versions=["1.10", "nightly"],
),
Documenter{GitHubActions}(),
Tests(; project=true),
Codecov(),
Coveralls(),
],
)
常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 依赖冲突 | 版本范围不兼容 | Pkg.resolve() 或调整 [compat] |
| 包加载失败 | 路径或名称错误 | 检查 LOAD_PATH 和模块名 |
| 测试失败 | 依赖未添加到测试 | 在 [extras] 中添加测试依赖 |
| 预编译慢 | 依赖过多 | 使用 PrecompileTools.jl |
| 注册失败 | 版本号未更新 | 递增 Project.toml 中的 version |
业务场景
场景一:开源数值计算库
开发一个数值积分库,使用 PkgTemplates.jl 创建项目骨架。配置 GitHub Actions 进行多版本测试,Documenter.jl 生成 API 文档,注册到 General Registry 供社区使用。
场景二:内部工具包
创建团队内部工具包,发布到私有 Registry。通过 [compat] 锁定依赖版本,确保团队成员使用一致的环境。
场景三:研究代码包
将研究代码封装为 Julia 包,添加测试和文档。通过 DOI 发布到 Zenodo,配合 Citation.jl 提供引用信息。
总结
| 主题 | 关键要点 |
|---|---|
| 创建包 | Pkg.generate() 或 PkgTemplates.jl |
| Project.toml | 包含 name/uuid/version/deps/compat |
| 开发模式 | Pkg.develop() + Revise.jl |
| 测试 | test/runtests.jl,使用 @testset |
| 文档 | Documenter.jl,docstring |
| 注册 | 推送 GitHub + @JuliaRegistrator |
| 版本号 | 遵循 semver 规范 |