Julia 模块开发 - 保姆级教程

今年接触了 Julia 的模块开发,趁热乎把相关知识和踩坑点整理下来。

论坛相关教程:

论坛相关讨论:

推荐阅读:

概要

第一部分是些比较基础的知识,主要介绍仓库常见文件,包括 Project.tomlManifest.toml 等,并用 ]generate 命令演示如何新建一个包。

第二部分介绍远程开发,包括 PkgTemplates.jl 的使用,测试部署和包注册等,考虑篇幅问题,拆分到下个帖子

项目开发通常用 Git 工具,所以这里假定已经有了一定的 Git 基础。如果不熟悉 Git,可以参考 廖雪峰的 Git 教程

注:如果有表述不严谨的地方,欢迎指正~


文件结构及介绍

仓库文件可以分三部分:

  • 开源项目常用文件,比如 README.mdLICENSE
  • 项目代码,比如 src, docstest
  • 环境文件Project.tomlManifest.toml

其中环境文件是讨论重点,下边以 QRCoders.jlQRDecoders.jl 作为例子。

打开一个 Julia 包,查看文件结构,比如

# tree QRCoders.jl/ -L 2
QRCoders.jl/
├── docs
│   ├── make.jl
│   ├── Manifest.toml
│   ├── Project.toml
│   └── src
├── LICENSE
├── Manifest.toml
├── Project.toml
├── README.md
├── src
│   ├── QRCoders.jl
│   └── tables.jl
└── test
    ├── runtests.jl
    └── tst_overall.jl

最外层是这几个文件(夹):

文件(夹) 作用
LICENSE 开源协议
README.md 包的介绍说明
docs 包的使用文档,用于生成网页
src 存放源代码的地方
test 测试代码
Manifest.toml 项目依赖的具体版本
Project.toml 项目依赖的包
  1. LICENSEREADME.md 一般开源仓库都有。LICENSE 为开源协议,用得比较广泛的是 MIT LICENSE;README.md 用于介绍项目信息,使用方法、贡献者等等,打开 GitHub 仓库,直接看到的页面就由 README.md 展示。

    仓库通常还有 .gitignore 文件,用于忽略不需要跟踪的文件,比如测试过程产生的临时文件,或者使用 Jupyter-notebook 产生的 .ipynb_checkpoints 文件等。

  2. src/test/docs 是 Julia 包约定或规定要有的文件夹:

    • src 存放源代码,且要求必须有模块的同名文件;假设模块名为 QRCoders,则必须存在文件 src/QRCoders.jl;当我们使用 using/import 导入 Julia 包时,背后是在执行 src/<模块名>.jl 文件
    • test 可选,存放测试代码;test 目录下需存放 runtests.jl 文件;当使用包管理模式执行 test 命令时,会自动执行 test/runtests.jl 的内容
      20221002172213
    • docs 可选,用于生成使用文档;通常配合 Documenter.jl 使用,并在 docs 目录下存放 make.jl 文件,用于生成网页
    • 关于测试 test 和文档 docs,我们在下篇单独展开介绍。
  3. 剩下两个文件 Manifest.tomlProject.toml,用于管理包依赖的,比较重要,接下来进行介绍。

环境文件

Project.tomlManifest.toml 的几点说明和比较:

  1. Pkg 模式下执行 instanitate,将在当前环境所在目录生成 Manifest.tomlProject.toml,用于记录包的依赖信息

    每次执行包的安装、删除、更新等操作时,Pkg 会自动更新这两个文件

  2. 模块主目录必须有 Project.toml 文件,而 Manifest.toml 是可选的

  3. Mainifest.toml 可读性较差,通常只通过执行 Pkg 命令自动更改,不建议手动修改;而 Project.toml 内容要简洁很多,可以手动维护

  4. Project.toml 记录项目的基本信息,一般来说是 Pkg 和程序员共同控制的内容Manifest.toml 是包管理器 Pkg 基于 Project.toml 生成的内容(以及执行包操作触发的修改),其记录了执行这个项目所需要的全部依赖的信息

  5. 多数项目只提供 Project.toml ,就足以指定环境和依赖了,但一些特殊情况,可能需要提供 Manifest.toml,后边会介绍几个例子

  6. 文档 docs 和测试 test 也可以设置环境,通过在相应目录添加 Project.toml(和 Manifest.toml)来设置

  7. 从可复现性的角度来说, 从高可复现至低可复现依次是:

    • 两个文件均提供:常用于各种一次性项目
    • 只提供 Project.toml:常用于工具箱开发
    • 啥也没有:单纯的代码库,不能被 using 调用

Project.toml

实践通常用 PkgTemplates 生成这些环境文件,而不需要自己手动编辑,但了解参数含义还是很有必要

推荐阅读:Pkg 文档 - Project.toml and Manifest.toml

QRCoders.jl 为例,查看 Project.toml 的参数:

name = "QRCoders"
uuid = "f42e9828-16f3-11ed-2883-9126170b272d"
authors = ["Jérémie Gillet <jie.gillet@gmail.com> and contributors"]
version = "1.0.1"

[deps]
FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534"
ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19"

[compat]
FileIO = "1"
ImageCore = "0.8, 0.9"
ImageIO = "0.4, 0.5, 0.6"
julia = "1.3"

[extras]
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test", "Random"]

文件包括了五个部分:

  1. 顶部是项目的基本信息

    • name:模块名称,Julia 通过识别 name 来确定当前环境名称;此外 src/ 目录下必须有name 内容命名的 .jl 文件
    • uuid 全称 Universally Unique Identifier(通用唯一识别码),用于标识 Julia 包
    • authors 仓库作者信息
    • version 当前模块的版本号,使用 Semantic Versioning(语义化版本)
  2. deps 是当前项目依赖的包,使用 pkg> add 添加包或 pkg> remove 删除包时会相应更新

  3. compat 是项目依赖包的版本要求,需手动设置。按开发规范应为非自带包设置版本要求,并指定兼容的 Julia 版本

  4. extras 是额外的依赖,比如测试环境依赖的包。例如 QRCoders 在 extras 这栏添加了测试依赖的 Test, Random,因而仓库 test 目录下可以省去 Project.toml 文件

  5. targets 似乎是按 extras 对应出现的,其他了解不多

通常,只有 compat 要手动指定依赖版本,其他的都是自动生成,或者在用包管理器时自动更新。

语义化版本

在注册包时,可能会遇到关于版本号的相关问题,这里简单介绍一下,内容摘自 短课笔记-8

  1. 代码工程中很容易遇到的一个问题就是版本冲突,例如: A 与 B 都是项目中需要的包, 而 A 与 B 都依赖于一个共同的包 C, 这时候如果 A 仅支持 C == 0.4.0 而 B 仅支持 C == 1.0.0 的话,那么就出现了版本冲突现象。

  2. 为了降低版本冲突的可能性,包管理器 Pkg 采用了语义化版本 Semantic Versioning。大概意思是说,所有版本分为四个主要类别: 主版本 major, 小版本 minor, 补丁版本 patch 以及构建版本 build。在 Julia 中大部分时候仅使用前三个版本。

  3. 版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

  • 主版本号:当你做了不兼容的 API 修改
  • 次版本号:当你做了向下兼容的功能性新增
  • 修订号:当你做了向下兼容的问题修正

先行版本号及版本编译信息可以加到 主版本号.次版本号.修订号的后面,作为延伸。

基于 SemVer,我们可以做出类似于这样的假设:

如果 A 兼容 C == 1.0.0, 那么 A 应该也兼容 C == 1.0.1C == 1.1.0,但未必兼容 C == 2.0.0

为此,Project.toml 引入了 [compat] 块,例如:

  • FileIO = "1" 代表仅兼容 1.x.y 版本的 FileIO
  • ImageCore = "0.8, 0.9" 代表仅兼容 0.8.x0.9.x
  • 版本号还支持形如 "≥ 1.2.3" 或正则表达式等语法,参看 Version specifier format

此外,以 0 开始的版本通常代表在开发阶段,此时次版本不一定能向下兼容,比如 0.2 不一定能兼容 0.1

Manifest.toml

Mainfest.toml 由一系列格式如下的片段构成,记录了项目详细的依赖信息

[[QRCoders]]
deps = ["FileIO", "ImageCore", "ImageIO"]
git-tree-sha1 = "a7a56a2550dbea3b603b357adf81710385d1d3c7"
uuid = "f42e9828-16f3-11ed-2883-9126170b272d"
version = "1.0.1"

其中 git-tree-sha1 是标记当前 commit 的哈希值,其他参数与前边讨论类似。

多数时候,我们并不需要添加 Manifest.toml 来记录环境。但一些情况,比如依赖的模块未注册,或者依赖模块是在本地开发,就得提供 Manifest.toml 来保证环境的可复现。比较常见的两种情况:

  1. 安装未注册的 Github 包,比如

    pkg> add https://github.com/JuliaImages/QRCoders.jl
    

    Manifest.toml 中会出现类似的片段

    [[QRCoders]]
    deps = ["FileIO", "ImageCore", "ImageIO"]
    git-tree-sha1 = "a7a56a2550dbea3b603b357adf81710385d1d3c7"
    repo-rev = "master"
    repo-url = "https://github.com/JuliaImages/QRCoders.jl"
    uuid = "f42e9828-16f3-11ed-2883-9126170b272d"
    version = "1.0.1"
    

    其中 repo-rev 记录模块所在分支,repo-url 记录模块所在地址。当然不只是 GitHub 仓库,本地仓库或其他 Git 仓库也能用这种方式添加

  2. 安装本地开发的包

    # dev /path/to/your/package
    pkg> dev /home/rex/ospp/QRDecoders.jl
    

    Manifest.toml 对应出现如下片段

    [[QRDecoders]]
    deps = ["FileIO", "ImageIO", "ImageTransformations", "QRCoders"]
    path = "/home/rex/ospp/QRDecoders.jl/"
    uuid = "d4999880-6331-4276-8b7d-7ee1f305cff8"
    version = "0.1.0"
    

以上两种情况均需要使用 Manifest.toml 来完整记录环境。

小结

本节介绍了 Julia 包的文件结构,包括

  1. GitHub 仓库常用的

    • README.md 项目介绍
    • LICENSE 开源协议
    • .gitignore 忽略无关文件
  2. Julia 代码文件:

    • src 存放源代码
    • test 存放测试代码
    • docs 存放文档
  3. Julia 环境文件

    • Project.toml 项目依赖
    • Manifest.toml 项目详细依赖

对多数包来说,只使用 Project.toml 记录环境,而将 Manifest.toml 放在 .gitignore 中忽略。但当使用未注册的包时,或有更严格的复现需求时,就得提供 Manifest.toml 来记录环境。

此外,如果 Manifest.toml 已被 .gitignore 设置了忽略,可以通过 git add -f Manifest.toml 强制添加;相反的,如果 Manifest.toml 已被添加,可以通过 git rm --cached Manifest.toml 来取消添加。

简易教程

下边演示如何用包管理命令创建模板,借此熟悉前边介绍的知识。

PkgTemplates.jl 能一键生成环境,而不必逐个构建。但理解这些文件结构能帮助更灵活地使用模板。

Demo

作为示例,新建目录 TestPackage,后续操作都在这里进行。

  1. 创建 git 环境

    cd TestPackage # 进入目录
    git init # 初始化 git
    

    以下为 Git 仓库常用文件(可选)

    # 取消对某些文件的追踪
    touch .gitignore 
    # 仓库介绍
    touch README.md
    # 开源协议
    touch LICENSE # 粘贴合适的协议
    
  2. 以当前目录作为开发环境启动 Julia

    # julia --project=<环境位置>
    julia --project=.
    

    等价地,可以先启动 Julia ,然后在包管理模式中使用 activate 切换开发环境

    # 先启动 Julia
    ] # 输入 ] 进入包管理模式
    activate . # 启用当前目录作为开发环境
    

  3. 包管理模式下,使用 generate + 模块名 创建模板

    pkg> generate TestPackage
    

    如下图,操作后 Project.tomlsrc 文件夹已创建
    image

    打开文件并查看,Project.toml 已自动生成了 nameuuid 等字段,其中 authors 由当前环境的 Git 信息生成

    name = "TestPackage"
    uuid = "83012822-a8b3-402d-b3e2-a8f809b7e3a3"
    authors = ["rex <1073853456@qq.com>"]
    version = "0.1.0"
    

    src 代码内容

    module TestPackage
        greet() = print("Hello World!")
    end # module
    
  4. 我们需要将 TestPackage 文件夹中的文件挪到模块主目录

    mv TestPackage/* ./
    rmdir TestPackage
    
  5. 包管理模式下,使用 instantiate 命令初始化

    pkg> instantiate
    

    该命令会根据 Project.toml 文件生成 Manifest.toml 文件;当 Project.toml 文件不存在时,会生成空白的 Project.tomlManifest.toml

  6. 提交更改

    git add --all
    git commit -m "initial commit"
    

大功告成,一个简单的包已经创建好了!

包管理命令

包管理模式常用命令:

命令 说明
add <模块名> 安装模块;若模块已安装,则根据环境文件执行更新操作
add <链接> 通过链接(地址)安装模块
rm/remove <模块名> 删除模块依赖
instantiate 实例化,也即更新 Project.toml, Manifest.toml 文件
test 执行 test/ 目录下的 runtests.jl
st/status 查看依赖信息
activate <环境目录> 将开发环境切换到指定目录,不加参数则切换到默认环境
generate <模块名称> 生成模板
update 更新环境依赖中的模块
dev <模块目录> 本地开发,添加本地模块作为依赖
build 构建依赖
precompile 环境预编译

简单来说,如果仓库没有 Project.toml 文件,则执行 instatiate 命令初始化。之后在执行 add, rm, update, dev 等操作时,仓库会自动更新 Project.tomlManifest.toml 文件。

注:一些环境错误,比如执行 test 提示的文件错误,可能是 Manifest.toml 的问题,如该文件非必要可将其删除再运行

其他知识点

  1. 模块定义

    module ExamplePackage
        export greet, notdefine
        greet() = print("Hello World!")
    end
    

    其中 export 导出符号。当使用 using <模块名> 调包时,这些符号会被导入到当前环境。特别注意,export 只是导入符号而不做检查,哪怕符号对应的变量没有定义也能正常进行,且能够触发代码补全功能
    image

  2. 导入模块除了 using,也可以用 import,区别参看之前回答的一个帖子

  3. Revise.jl

    • Julia 提供了 Revise 包,可以实现模块热更新,即修改模块后无需使用 using 重新导入,也会自动更新模块变化。
    • 需注意的是,Revise 只影响在它之后导入的模块,因此建议将其添加到 startup.jl,每次启动 Julia 都会先执行这个脚本。
    • startup.jl 添加方式:直接编辑 ~/.julia/config/startup.jl 文件或者使用命令行
      echo "using Revise" >> ~/.julia/config/startup.jl
      
  4. 项目代码通常不止一个文件,当 src 下写了多个文件时,可以用 include 命令将它们导入到主文件中

    ## 主文件内
    module ExamplePackage
        greet() = print("Hello World!")
        include("temp.jl") # 导入文件
    end
    

    include 命令不仅适用于模块开发,普通场景的代码导入也是有效的

  5. 在模块 module 内,引用内部模块可以略写模块名,比如

    module Outer
        module Inner
            hi() = print("hi")
        end
        # 引用子模块
        using .Inner
        # 等价效果 using Outer.Inner
    end
    
  6. 变量及所在模块

    • 使用 @which 可以查看变量所在模块
    • 在 REPL 中定义的变量和函数属于全局环境 Main
      image
  7. 查看源码技巧:

    • 在 Jupyter 下,用 @which 能获取函数源码的 GitHub 链接
    • @edit func(para..) 可以查看 func 关于该派发的源码
    • @edit 默认使用 vim 作为编辑器,但可以修改为 vscode
      ENV["EDITOR"] = "code"
      
      这段代码建议写在 startup.jl 中,每次启动 Julia 都会先执行

模块开发还有些其他内容,比如 __init__ 函数等,后续接触再进行补充。


以上是模块开发的第一部分知识,PkgTemplates.jl 的使用及远程开发参阅下篇

21 个赞

给楼主点赞!!!

期待更多更新,mark

楼主,Project.toml 文件[compat]部分如果出现:
JuMP = “^0.21, 1.0”
这里是JuMP版本需要0.21及以下或1.0版本的意思吗

JuMP = "^0.21, 1.0" 指 JuMP 包的版本是 0.21.0 及以上(包括所有 0.21.x 版本和所有 1.x 版本),直到但不包括 2.0.0 版本。


链接的文档喂给 GPT4 后,返回的提炼(准确性√):

在Julia的Project.toml文件中,[compat]部分用于指定项目依赖的版本兼容性。通过设定不同的版本号和符号,可以精细地控制兼容的依赖版本范围。以下是主要的版本控制语法规则概述:

  1. 语义化版本(SemVer):Julia的包管理遵循语义化版本控制,即通过版本号的主要(major)、次要(minor)、补丁(patch)三个数字来表达不同级别的更改。

  2. 插入符号规范(Caret Specifiers):默认情况下,使用^符号或不使用任何符号都表示允许升级到与当前指定版本兼容的最新版本。兼容性是基于不改变最左边的非零数字。例如:

    • ^1.2.31.2.3 表示兼容范围 [1.2.3, 2.0.0)
    • ^0.2.3 表示兼容范围 [0.2.3, 0.3.0)
  3. 波浪号规范(Tilde Specifiers):使用~符号限制升级范围更严格。主要用于需要锁定次要版本或补丁版本时。

    • ~1.2.3 表示兼容范围 [1.2.3, 1.3.0)
    • ~0.2.3 表示兼容范围 [0.2.3, 0.3.0)
  4. 等于规范(Equality Specifier):使用=符号指定精确的版本号。

    • =1.2.3 表示仅兼容版本 1.2.3
  5. 不等式规范:可以使用>>=<<=来指定一个版本范围。

    • >=1.2.3 表示兼容范围 [1.2.3, ∞)
  6. 连字符规范(Hyphen Specifiers):使用连字符-和空格来指定一个明确的版本范围。

    • 1.2.3 - 4.5.6 表示兼容范围 [1.2.3, 4.5.6]
  7. 版本与前导零:对于前导零的版本(如0.x.y),Julia有特殊的兼容性规则。例如,0.2.1会被视为与0.2.00.3.0之间的版本兼容,而非遵循严格的语义化版本控制规则。

通过上述规则,开发者可以在Project.toml文件中精确控制项目依赖的版本,以确保软件的稳定运行。

2 个赞

明白了,谢谢楼主!