Julia 版本管理和环境管理
Julia 的版本管理和环境管理,通常可以分成两部分来看:
juliaup:Julia 版本管理Pkg:项目环境和包管理
和 Python 中常见的 pyenv + venv + pip 组合(以及其他各种替代方案)相比,Julia 直接从语言角度就考虑,设计更加统一优雅:
- Julia 版本推荐由
juliaup管理;(如果只使用一个版本,也可以绕过juliaup) - 项目依赖和环境由 Julia 自带的
Pkg标准库管理; - 环境本质上不是一套独立拷贝的运行时,只是两个文本文件,对应依赖声明与解析结果。
Julia 的版本和环境管理机制和 Rust 很相似,可以说是现代编程语言设计的最佳方案。
Julia 运行时和版本
Julia 运行时
Julia 运行时指的是使 Julia 程序能够执行的一组核心文件,通常包括:
julia可执行文件- Julia 自带标准库(standard libraries)
- Julia 的系统镜像(system image)
- 动态库和底层运行时组件
第三方包并不直接属于 Julia 自带运行时,它们通常存放在 depot 中,由当前激活的环境决定是否可用。
Julia 运行时的抽象结构如下:
1 | Julia Runtime |
在 Linux / macOS 中一个典型的 Julia 安装目录大致如下:
1 | julia-1.12.x/ |
说明:
| 目录 | 作用 |
|---|---|
bin/ |
Julia 可执行文件 |
share/julia/base/ |
Julia Base 源码 |
share/julia/stdlib/ |
标准库源码 |
lib/julia/ |
系统镜像和底层动态库 |
include/julia/ |
嵌入 Julia 或开发扩展所需头文件 |
包查找规则
Julia 启动后,决定“能 using 到哪些包”的关键是两组路径:
LOAD_PATH:决定当前会在哪些环境里查找包DEPOT_PATH:决定包缓存、注册表、预编译文件、共享环境等存放在哪里,通常是~/.julia中的文件夹,如果不是通过 juliaup 安装的,那么还包括 julia 的安装目录
可以在 REPL 中直接查看:
1 | LOAD_PATH |
一个常见的 LOAD_PATH 形如:
1 | ["@", |
含义分别为:
@:当前激活的项目环境@v#.#:当前 Julia 主次版本对应的默认全局环境,例如~/.julia/environments/v1.12@stdlib:Julia 自带标准库
这个路径查找机制表明:
- 如果当前项目环境中有某个包,Julia 会优先使用该环境中的依赖解析结果;
- 如果当前项目没有,但是默认全局环境中有,也会成功加载;(这里的 fallback 机制和 Python 很不相同,Python 虚拟环境的
site-packages通常不会使用 base 环境的site-packages) - 标准库始终可通过
@stdlib找到。
depot
Julia 中非常关键的概念是 depot。默认情况下,用户级 depot 往往在 ~/.julia 中,也可以通过配置选项指定。
julia 运行时的安装目录和 depot 可以是分开的,也可以合并在一起,例如使用 juliaup 时,julia 运行时会安装
~/.julia/juliaup中。
depot 的典型结构如下
1 | ~/.julia/ |
说明:
| 目录 | 作用 |
|---|---|
registries/ |
包注册表,例如 General |
packages/ |
包源码缓存 |
compiled/ |
预编译缓存 |
artifacts/ |
二进制依赖或数据工件 |
environments/ |
共享环境,例如默认环境 @v1.12 |
这里和 Python 的设计很不一样:
- Python 的虚拟环境通常有自己独立的
site-packages/,包含完整的第三方依赖的内容; - Julia 的项目环境通常只需要在项目文件夹下保存两个文本文件,分别是项目环境需要的依赖声明和锁定结果,真正的包源码、缓存、工件都是全局共享的,存储在 depot 中的。
所以 Julia 环境更像是“依赖视图”或“解析上下文”,而不是一整套独立复制的运行时。
由于 Julia 默认把绝大部分内容都存放在
~/.julia,卸载时直接删除这个目录和安装目录即可。
Julia 版本管理
juliaup 是 Julia 官方推荐的版本管理工具,可以理解为 Julia 的多版本启动器和安装器。
它负责:
- 安装不同版本的 Julia
- 管理不同版本通道(channel),通常选择
release或lts即可 - 设置默认版本
- 在不同目录下覆盖默认版本
可以把它理解为:
1 | juliaup = Julia 版本下载 + 多版本切换 + 默认版本控制 |
安装 juliaup
官网推荐的安装方式:
1 | # Linux / macOS |
实际不推荐这两种安装方式,直接下载 juliaup 的 Github Release 的预编译版本。安装完成后会得到两个可执行文件:
juliajuliaup
注意:
- juliaup 仓库 的 Release 的
julia实际上并不是真正的 julia 可执行文件,而是juliaup提供的一个统一入口,最终会通过juliaup选择对应版本来启动。 - julia 仓库 的 Release 不能直接下载使用,如果希望绕过 juliaup 直接下载 julia,也得去 Julia 官网 下载
基本使用
juliaup 主要使用 channel 概念:
release:当前稳定版lts:长期支持版1.12:某个主次版本系列中的最新补丁版本
查看当前可用通道:
1 | juliaup list |
安装某个版本或通道:
1 | juliaup add release |
查看当前已安装版本:
1 | juliaup status |
设置默认版本:
1 | juliaup default 1.12 |
更新已安装版本:
1 | juliaup update |
删除某个版本:
1 | juliaup remove 1.11 |
可以在启动 julia 时指定 channel:
1 | julia +release |
这里的 +1.12 表示临时使用 1.12 通道启动 Julia,而不改变全局默认版本。
这种做法适合临时测试不同版本兼容性,或者某些项目固定依赖特定的 Julia 版本。
juliaup 还支持目录级版本切换,与 Python 通过 .python-version 固定项目 Python 版本的思路不同,Juliaup 对版本的要求应该是记录 .julia/ 中的某个文件中,而且目前并不需要,因此略过。
小结:
juliaup管的是 Julia 解释器版本;Pkg环境管的是项目依赖;
Julia 环境管理
Julia 环境本质
Julia 环境本质上是一组依赖信息,由两个文本文件负责描述:
Project.toml:记录项目本身信息和直接依赖,表明项目的依赖意图,类似 Python 的pyproject.toml。Manifest.toml:记录完整依赖树和精确解析结果,是相当于确环境快照,类似 Python uv 的uv.lock。
一个简单的 Project.toml 可能形如:
1 | name = "ExampleApp" |
其中:
[deps]记录直接依赖包及其 UUID;[compat]记录版本兼容要求;julia = "1.12"用于限制项目可用的 Julia 版本范围。
Manifest.toml 会记录完整依赖树、每个包的精确版本、来源和解析结果。(内容可能很长,因为一个直接依赖可能连带着几十个间接依赖)
通常会把这两个文件都纳入版本管理,或者至少把 Project.toml 纳入版本管理。如果希望别人能完全复现环境,应该同时保留这两个文件。
激活环境
在 Julia 中,进入某个项目目录后,可以直接激活当前目录环境:
1 | using Pkg |
在包管理模式下也可以写成:
1 | pkg> activate . |
如果当前目录还没有 Project.toml,激活后可以进一步初始化或添加依赖。
对于日常项目开发,更常见的做法是在项目根目录直接启动 Julia:
1 | julia --project=. |
这表示使用当前目录作为项目环境。
也可以写成:
1 | julia --project |
此时会自动查找当前目录中的项目文件。
创建环境和添加依赖
在一个新目录中:
1 | using Pkg |
执行后通常会发生这些事情:
- 当前目录下生成
Project.toml - 生成或更新
Manifest.toml - 在 depot 中下载包源码、工件和预编译缓存
注意:
- 包并不是安装到当前项目目录下面的某个
site-packages/; - 当前目录主要保存环境描述文件;
- 实际包内容通常共享存放在
~/.julia/中。 - 即使是标准库中自带的某个模块,如果使用
add手动添加,也会记录在Project.toml中。
使用包管理模式
Julia REPL 中按 ] 可以进入 Pkg 模式,例如:
1 | (@v1.12) pkg> |
常见命令:
1 | pkg> activate . |
说明:
| 命令 | 作用 |
|---|---|
activate |
切换当前环境 |
add |
添加依赖 |
rm |
删除依赖 |
update |
更新依赖 |
status |
查看依赖状态 |
instantiate |
根据环境文件恢复环境 |
precompile |
手动预编译 |
环境导出和恢复
Julia 的环境导出和恢复比 Python 中的 requirements.txt 更结构化。
如果项目中已经有:
Project.tomlManifest.toml
那么在其他机器或其它目录下恢复环境通常只需要:
1 | using Pkg |
或者在 Pkg 模式中:
1 | pkg> activate . |
其中 instantiate 会根据 Manifest.toml 尽可能精确地恢复依赖;如果没有 Manifest.toml,则会根据 Project.toml 重新解析一个可用环境。
Julia 的项目环境在设计上通常可以跨平台共享:Project.toml 基本是跨平台的,Manifest.toml 也往往可以在不同平台上复用;但如果项目依赖平台相关的 artifact、系统库或构建步骤,实际恢复结果仍可能因平台不同而有差异。
共享环境
共享环境是指不依赖具体项目目录,可以被多个项目使用的环境。
为了便于与一般的环境区分,共享环境会在 REPL 显示名称时在前面加上 @。
Julia 会给每个版本准备一个同名的默认环境,如果没有显式激活环境,就会进入默认环境,例如 @v1.12,对应的 Project.toml 和 Manifest.toml 会保存在:
1 | ~/.julia/environments/v1.12 |
在这个环境里也可以添加包:
1 | pkg> add IJulia |
这会把包加到该版本 Julia 对应的默认环境中。
由于 Julia 项目环境存在 fallback 机制,默认环境中的包是全局可见的,在各个项目环境中都是可用的,但是通常不建议把依赖都加到默认环境里,因为不利于依赖管理,而且容易引入依赖冲突。
除了默认的全局环境 @v#.# 之外,Julia 也支持自定义共享环境,例如
1 | using Pkg |
等价于在 pkg 模式下执行
1 | pkg> activate --shared myenv |
这会创建或进入一个名为 @myenv 的共享环境,对应的 Project.toml 和 Manifest.toml 会保存在:
1 | ~/.julia/environments/myenv |
共享环境适合放一些经常在 REPL 中使用的通用工具包,例如:
IJuliaReviseBenchmarkTools
但是对于具体项目依赖,通常仍然更推荐使用项目本地环境。
临时环境
Julia 支持临时环境,适合一次性实验:
1 | using Pkg |
或者在 pkg 模式下:
1 | pkg> activate --temp |
此时会创建一个临时环境,Julia 退出后环境也会失效,但包缓存本身通常仍然留在 depot 中以便复用。
这里的临时环境和 Python uv 临时创建一个一次性的虚拟环境的用途类似。
Julia 包开发、本地包和注册表
包的基本结构和创建
一个标准的 Julia 包通常也是一个普通项目目录,只是它需要满足 Julia 包的组织约定。
一个典型结构如下:
1 | MyPkg/ |
其中:
| 路径 | 作用 |
|---|---|
Project.toml |
包的元信息、依赖和兼容要求 |
src/MyPkg.jl |
包的主入口文件 |
test/runtests.jl |
测试入口 |
通常包名是 MyPkg,主模块也写成:
1 | module MyPkg |
并放在 src/MyPkg.jl 文件中。
生成新包
最简单的做法是手动创建目录和文件,也可以借助社区工具生成模板。
如果手动创建,一个最小可用包通常至少包括:
Project.tomlsrc/MyPkg.jltest/runtests.jl
一个简单的 Project.toml 可以形如:
1 | name = "MyPkg" |
一个最简单的 src/MyPkg.jl 可以写成:
1 | module MyPkg |
本地开发、测试和版本控制
开发 Julia 包时,通常直接在包目录中启动 Julia:
1 | julia --project=. |
此时当前包目录本身就是一个项目环境,可以继续添加依赖:
1 | using Pkg |
如果这个依赖只在测试中使用,也可以切换到 test/Project.toml 一类的专门测试环境,但很多小项目会先直接使用单一环境。
本地包接入当前项目
如果你在开发一个应用项目,同时希望引用本地正在开发的 Julia 包,常见做法是 develop 而不是普通 add。
例如:
1 | using Pkg |
或者:
1 | pkg> develop ../MyPkg |
这和普通 add 的区别是:
add更偏向安装某个已发布版本或某个 Git 来源的快照;develop更偏向把一个本地工作目录以开发模式接入当前环境。
这样做之后,当前环境会直接使用这个本地目录中的代码变化,非常适合联调开发。
add 本地路径和 develop 本地路径的区别
对于本地路径,Julia 里容易混淆的就是 add path 和 develop path。
可以简单理解为:
add path/to/pkg:把该路径视为一个来源,记录当前解析结果,更偏向“依赖一个包”develop path/to/pkg:明确把这个目录作为开发工作副本,更偏向“我要直接改这个包”
在实际开发中,如果你会继续修改这个本地包,通常优先使用:
1 | pkg> develop path/to/pkg |
通过 Git 仓库使用包
Julia 也支持直接从 Git 仓库添加包,例如:
1 | Pkg.add(url="https://github.com/user/MyPkg.jl") |
也可以在 Pkg 模式中写成:
1 | pkg> add https://github.com/user/MyPkg.jl |
如果需要指定分支或提交,也可以写:
1 | Pkg.add(url="https://github.com/user/MyPkg.jl", rev="main") |
这里和 Python 的 pip install git+https://... 很接近。
测试包
Julia 包通常使用 test/runtests.jl 作为统一测试入口。
在包目录中可以执行:
1 | using Pkg |
或者在 Pkg 模式中:
1 | pkg> test |
Pkg.test() 会在合适的包测试环境中执行测试,而不是简单地直接运行某个脚本文件。
构建和预编译
有些包会提供构建步骤,例如下载资源、探测系统库等,此时可能会用到:
1 | pkg> build |
而常规的预编译则可以使用:
1 | pkg> precompile |
其中:
build更偏向包安装后的额外初始化动作;precompile更偏向生成缓存,加快后续加载速度。
注册表
Julia 中的注册表(registry)可以理解为“包索引数据库”。
它记录了:
- 有哪些包
- 包名对应哪个 UUID
- 各个版本对应的源码来源
- 版本兼容信息
默认最常见的是官方通用注册表 General。
当执行:
1 | pkg> add DataFrames |
Pkg 实际上会先在注册表中查找 DataFrames 这个名字对应的包,再根据版本约束解析依赖。
查看和管理注册表
常见命令:
1 | pkg> registry status |
这些命令分别用于:
- 查看当前注册表
- 添加注册表
- 删除注册表
- 更新注册表索引
通常普通用户只需要保留 General 就够了,但在公司内部或团队内,也可能会额外维护私有注册表。
私有注册表和未注册包
如果某个包没有发布到 General,通常有两种常见使用方式:
- 直接通过 Git URL 添加
- 放到私有注册表中统一管理
直接通过 Git URL 适合:
- 临时依赖
- 小团队内部使用
- 还没准备正式发布的包
私有注册表更适合:
- 多个内部包统一分发
- 需要稳定版本管理
- 希望像公开包一样通过名称直接
add
注册表、本地包和 depot 的关系
这三者分别解决不同问题:
- 注册表:回答“有哪些包、有哪些版本、去哪里下载”
- 本地包:回答“当前开发时直接使用哪个工作目录”
- depot:回答“包源码、工件、缓存实际放在哪里”
也就是说:
- 注册表偏向索引和发布;
develop path=...偏向本地开发联调;- depot 偏向底层存储。
开发本地包
如果一个本地 Julia 包需要以开发模式加入当前环境,可以使用:
1 | Pkg.develop(path="path/to/MyPkg") |
或者在 Pkg 模式中:
1 | pkg> develop path/to/MyPkg |
这类似于 Python 中的可编辑安装 pip install -e .。
固定与解除固定版本
Julia 也支持固定依赖版本:
1 | pkg> pin DataFrames |
含义:
pin:固定某个包,避免被update升级free:解除固定,恢复正常解析
删除环境
Julia 项目环境本身通常只是一两个配置文件,因此如果不再需要某个项目环境:
- 删除项目目录中的
Project.toml - 删除项目目录中的
Manifest.toml
即可视为删除该环境。
但注意:
- 这不会自动删除 depot 中已经缓存的包源码和工件;
- 共享缓存会继续保留,供其它环境复用。
如果要清理长期不用的包缓存,通常需要额外做 depot 清理,而不是仅删除项目目录。
在 Julia 中调用 Python
Julia 中调用 Python,最常见的方案主要有两种:
PyCall.jlPythonCall.jl
它们都能让 Julia 导入 Python 模块、调用 Python 函数、读写 Python 对象,但底层思路和环境管理方式略有不同。
基本思路
无论使用哪一个库,本质上都是:
- 在 Julia 进程中嵌入一个 Python 解释器
- 让 Julia 可以导入 Python 模块
- 在 Julia 和 Python 对象之间做类型转换
因此这里会同时涉及两套环境:
- Julia 自己的项目环境
- 被调用的 Python 环境
也就是说:
Pkg.add(...)管的是 Julia 侧依赖;pip install ...、uv add ...、conda 等管的是 Python 侧依赖。
PyCall.jl 和 PythonCall.jl
PyCall.jl 是 Julia 中较早也非常常见的 Python 互操作方案。
安装:
1 | using Pkg |
基本使用:
1 | using PyCall |
也可以直接调用 Python 内置或第三方库,例如:
1 | using PyCall |
PyCall 使用哪个 Python
PyCall 最大的关键点是:它在构建时会绑定一个 Python 解释器。
也就是说,PyCall 不是每次运行时随便找一个 Python,而是通常在 build PyCall 时决定使用哪个 Python。
例如可以在启动 Julia 前设置:
1 | export PYTHON=/path/to/python |
然后在 Julia 中执行:
1 | using Pkg |
此后 PyCall 就会绑定到这个 Python。
因此如果你后来切换了 Python 虚拟环境,PyCall 往往不会自动跟着变,通常需要重新 build。
可以在 Julia 中查看它当前绑定的是哪个 Python:
1 | using PyCall |
PythonCall.jl
PythonCall.jl 是相对更新的一套方案,设计上通常更现代,也更强调和 Julia 项目环境的配合。
安装:
1 | using Pkg |
基本使用:
1 | using PythonCall |
调用 Python 第三方库:
1 | using PythonCall |
PythonCall 的环境管理
PythonCall 的一个特点是,它通常可以更方便地管理或定位自己的 Python 环境。
常见理解方式是:
PyCall更像“先选定一个 Python,再把它嵌进 Julia”PythonCall更像“由 Julia 侧更主动地协调 Python 环境”
在很多情况下,PythonCall 会自动准备一个可用的 Python 运行环境;也可以显式要求它使用系统里已有的 Python。
因此:
- 如果你想快速用 Python 库,
PythonCall往往更省心; - 如果你已经有一个明确的 Python 环境,并且希望 Julia 严格绑定它,
PyCall也很常见。
一个简单例子
例如想在 Julia 中调用 Python 的 json 模块:
1 | using PythonCall |
如果使用 PyCall,写法也很相似:
1 | using PyCall |
类型转换
Julia 和 Python 的对象系统不同,因此调用过程中会发生类型转换。
常见情况:
- Julia 的数字、字符串、数组、字典可以转换给 Python
- Python 返回的列表、字典、
numpy.ndarray等对象,也可以转换或包装成 Julia 可操作对象
但这种转换并不总是“完全自动且零成本”,尤其是:
- 大数组
- 可变对象
- 自定义类实例
在实际使用中,需要注意到底是“复制了一份数据”,还是“共享/包装了一个 Python 对象”。
安装 Python 侧依赖
要成功调用某个 Python 包,除了 Julia 里装好 PyCall 或 PythonCall,还必须确保对应 Python 环境里真的安装了那个包。
例如你想调用 numpy,那么 Python 侧也必须有:
1 | pip install numpy |
或者在使用 uv 的项目里:
1 | uv add numpy |
否则即使 Julia 侧已经安装了桥接库,下面的语句仍然会失败:
1 | pyimport("numpy") |
和 Python 虚拟环境配合时的注意事项
如果你的 Python 项目使用:
venvuv- conda
那么 Julia 调 Python 时最关键的问题就是:当前桥接库到底连接到了哪一个 Python。
对于 PyCall,要特别注意:
- 它通常绑定某个固定 Python
- 切换 Python 虚拟环境后可能需要重新
Pkg.build("PyCall")
对于 PythonCall,则更要关注:
- 它当前使用的是自动管理的 Python,还是你指定的外部 Python
否则很容易出现这种问题:
- shell 里
python能导入某个包 - 但 Julia 里的
pyimport("xxx")却失败
本质原因往往不是包没装,而是“Julia 连接的不是同一个 Python 环境”。
什么时候用哪一个
可以简单理解为:
PyCall:历史更久,使用广泛,适合已经有固定 Python 环境的情况PythonCall:设计更新,环境管理通常更灵活,新的项目里经常更推荐优先考虑
如果只是偶尔调用少量 Python 功能,两者都够用;如果项目里 Julia 和 Python 需要长期混合协作,最好从一开始就明确:
- 哪个库负责桥接
- Python 解释器由谁管理
- Python 依赖安装在哪个环境里
Julia 和 Python 环境模型的区别
1. Julia 环境不是独立复制的运行时
Python 的 venv 往往表现为:
- 一套独立的解释器入口
- 一套独立的
site-packages/
而 Julia 的项目环境更像:
- 当前项目的依赖声明
- 当前项目的依赖解析结果
- 背后共享同一份 depot 中的包缓存
所以 Julia 环境通常更轻量。
2. Julia 环境依赖 Project.toml / Manifest.toml
Python 中很多项目历史上依赖:
requirements.txt- 或
pyproject.toml + lockfile
Julia 则天然围绕:
Project.tomlManifest.toml
来组织依赖管理和环境复现。
3. Julia 版本和项目环境天然分层
在 Julia 中通常是:
- 用
juliaup切换解释器版本 - 用
Pkg.activate()切换项目环境
这两层的职责边界相对清楚。
推荐工作流
新项目
- 使用合适版本启动 Julia
1 | julia +1.12 --project=. |
- 在 REPL 中激活当前目录并添加依赖
1 | using Pkg |
- 将
Project.toml和Manifest.toml纳入版本控制
打开已有项目
如果仓库中已经有 Project.toml 和 Manifest.toml:
1 | julia --project=. |
进入后执行:
1 | using Pkg |
临时实验
如果只是一次性测试:
1 | using Pkg |
小结
关键结论:
- Julia 版本管理通常由
juliaup完成 - Julia 项目环境管理通常由
Pkg完成 Project.toml记录直接依赖和兼容约束Manifest.toml记录完整依赖树和精确版本- Julia 环境本质上不是独立复制的运行时,而是依赖解析上下文
- 包源码、工件和预编译结果通常共享存放在
~/.juliadepot 中 - 项目开发时应尽量使用项目级环境,而不是长期堆在默认全局环境中
