包管理
包 (Packages) 使得 Move 程序员能够更轻松地复用代码并在项目间共享.Move 包管理系统支持程序员完成以下操作:
- 定义包含 Move 代码的包
- 通过命名地址参数化包
- 在其他 Move 代码中导入和使用包,并实例化命名地址
- 构建包并生成相关的编译产物
- 通过统一接口处理已编译的 Move 产物
包目录结构与清单语法
Section titled “包目录结构与清单语法”Move 包的源目录包含一个 Move.toml
包清单文件及若干子目录:
文件夹a_move_package/
- Move.toml
文件夹sources (required)/
- module.move
- *.move
- examples (optional, test & dev mode)/
- scripts (optional, can also put in sources)/
- doc_templates (optional)/
- tests (optional, test mode)/
标记为 required
的目录必须存在才能使该目录被视为 Move 包并进行编译.可选目录可以存在,若存在则会被包含在编译过程中.根据包的构建模式(test
或dev
),tests
和examples
目录也会被包含进来.
sources
目录可以同时包含 Move 模块和 Move 脚本(包括脚本函数).examples
目录可以存放仅用于开发和/或教程目的的额外代码,这些代码在非 test
或 dev
模式下不会被包含.
scripts
目录用于将 Move 脚本与模块分离(如果包作者需要).只要存在该目录,其中的内容总会被包含在编译过程中.doc_templates
目录中的文档模板会被用于构建文档.
Move.toml 文件
Section titled “Move.toml 文件”Move 包清单定义在 Move.toml
文件中,语法如下.标有*
的字段为可选,+
表示一个或多个元素:
[package]name = <string> # 例如:"MoveStdlib"version = "<uint>.<uint>.<uint>" # 例如:"0.1.1"license* = <string> # 例如:"MIT", "GPL", "Apache 2.0"authors* = [<string>] # 例如:["Joe Smith (joesmith@noemail.com)", "Jane Smith (janesmith@noemail.com)"]
[addresses] # (可选章节)声明本包中的命名地址并在包图中实例化命名地址# 一行或多行以下格式的命名地址声明<addr_name> = "_" | "<hex_address>" # 例如:std = "_" 或 my_addr = "0xC0FFEECAFE"
[dependencies] # (可选章节)依赖项路径以及对每个依赖项中命名地址的实例化或重命名# 一行或多行以下格式的依赖声明<string> = { local = <string>, addr_subst* = { (<string> = (<string> | "<hex_address>"))+ } } # 本地依赖<string> = { git = <URL ending in .git>, subdir=<path to dir containing Move.toml inside git repo>, rev=<git commit hash or branch name>, addr_subst* = { (<string> = (<string> | "<hex_address>"))+ } } # git依赖
[dev-addresses] # (可选章节)与[addresses]章节相同,但仅在"dev"和"test"模式下包含# 一行或多行以下格式的开发命名地址声明<addr_name> = "_" | "<hex_address>" # 例如:std = "_" 或 my_addr = "0xC0FFEECAFE"
[dev-dependencies] # (可选章节)与[dependencies]章节相同,但仅在"dev"和"test"模式下包含# 一行或多行以下格式的开发依赖声明<string> = { local = <string>, addr_subst* = { (<string> = (<string> | <address>))+ } }```一个包含一个本地依赖和一个git依赖的最小化包清单示例:
```toml[package]name = "AName"version = "0.0.0"
一个更标准的包清单示例,还包含了 Move 标准库,并将命名地址 Std
实例化为地址值 0x1
:
[package]name = "AName"version = "0.0.0"license = "Apache 2.0"
[addresses]address_to_be_filled_in = "_"specified_address = "0xB0B"
[dependencies]# 本地依赖LocalDep = { local = "projects/move-awesomeness", addr_subst = { "std" = "0x1" } }# Git依赖MoveStdlib = { git = "https://github.com/aptos-labs/aptos-framework", subdir="move-stdlib", rev = "mainnet" }
[dev-addresses] # 用于开发时使用address_to_be_filled_in = "0x101010101"
包清单中的大部分部分都一目了然,但命名地址可能较难理解,因此值得更详细地探讨.
编译期间的命名地址
Section titled “编译期间的命名地址”回忆一下 Move 有 命名地址 ,且命名地址不能在 Move 中声明.因此在此之前,命名地址及其值需要通过命令行传递给编译器.有了 Move 包系统后就不再需要这样了,你可以在包中声明命名地址,实例化作用域内的其他命名地址,并在 Move 包系统清单文件中重命名来自其他包的命名地址.让我们逐一了解:
假设我们在example_pkg/sources/A.move
中有如下 Move 模块:
module named_addr::A { public fun x(): address { @named_addr }}
我们可以在 example_pkg/Move.toml
中以两种不同方式声明命名地址 named_addr
.第一种:
[package]name = "ExamplePkg"# ...[addresses]named_addr = "_"
声明 named_addr
作为包 ExamplePkg
中的一个命名地址,且 该地址可以是任何有效的地址值.因此,导入包可以选择命名地址 named_addr
的值为任何它想要的地址.直观上,你可以将此视为通过命名地址 named_addr
参数化包 ExamplePkg
,然后该包稍后可以被导入包实例化.
named_addr
也可以声明为:
[package]name = "ExamplePkg"# ...[addresses]named_addr = "0xCAFE"
它声明了命名地址 named_addr
的值固定为 0xCAFE
,且不可更改.这样做的好处是,其他导入该包的包在使用这个命名地址时,无需关心它具体被赋了什么值.
由于存在这两种不同的命名地址声明方式,命名地址的信息在包依赖图中的传递也有两种方式:
第一种(“未赋值的命名地址”)允许命名地址的值从导入方流向声明方;
第二种(“已赋值的命名地址”)则允许命名地址的值从声明方向上传递到使用该地址的包.
正因为命名地址信息可以通过这两种方式在包依赖图中传递,理解作用域和重命名的规则就变得非常重要.
命名地址的作用域和重命名
Section titled “命名地址的作用域和重命名”包 P
中的命名地址 N
在以下情况下处于作用域内:
- 它声明了命名地址
N
;或 P
的某个传递依赖包声明了命名地址N
,并且在包图中P
与声明N
的包之间存在依赖路径,且在此路径上N
没有被重命名.
此外,包中的每个命名地址都会被导出.因此结合上述作用域规则,每个包可以被视为附带一组命名地址,当导入该包时这些命名地址会被带入作用域.
例如,如果导入了 ExamplePkg
包,该导入会将 named_addr
命名地址带入作用域.
因此,如果 P
导入了两个包 P1
和 和P2
,且这两个包都声明了命名地址 N
,那么在 P
中就会出现问题:当在 P
中引用 N
时,指的是哪个”N
”?来自 P1
还是 P2
的?为了防止这种关于命名地址来源的歧义,我们强制要求包中所有依赖项引入的作用域集合互不相交,并提供了一种在导入时将命名地址_重命名_的方法.
在我们的 P
,P1
和 P2
示例中,可以在导入时按如下方式重命名命名地址:
[package]name = "P"# ...[dependencies]P1 = { local = "some_path_to_P1", addr_subst = { "P1N" = "N" } }P2 = { local = "some_path_to_P2" }
通过这种重命名方式,N
将指向 P2
中的 N
,而 P1N
将指向来自 P1
的 N
:
module N::A { public fun x(): address { @P1N }}
需要注意的是 重命名不是局部的 :一旦在包 P
中将命名地址 N
重命名为 N2
,所有导入 P
的包将无法看到 N
,只能看到 N2
,除非 N
从 P
外部重新引入.
这就是为什么本节开头的作用域规则中的第二条特别指明“在包图中 P
与 N
的声明包之间的依赖路径上不能对 N
进行重命名”.
命名地址可以在包图中多次实例化,只要每次实例化的值相同即可.如果在包图中同一个命名地址(无论是否经过重命名)被赋予了不同的值,将会报错.
Move 包只有在所有命名地址都解析为具体值时才能编译.如果包希望暴露一个未实例化的命名地址,就会产生问题.这正是 [dev-addresses]
节要解决的问题.
该节可以为命名地址设置值,但不能引入新的命名地址.此外,只有根包中的 [dev-addresses]
会在 dev
模式下被包含.
例如,以下清单的根包在非 dev
模式下将无法编译,因为 named_addr
未被实例化:
[package]name = "ExamplePkg"# ...[addresses]named_addr = "_"
[dev-addresses]named_addr = "0xC0FFEE"
使用方式,产物与数据结构
Section titled “使用方式,产物与数据结构”Move 包系统作为 Move CLI 的一部分提供了命令行选项 move <flags> <command> <command_flags>
.除非指定了特定路径,所有包命令都将在当前工作目录下运行.运行 move --help
可以查看 Move CLI 的完整命令和标志列表.
包可以通过 Move CLI 命令编译,也可以通过 Rust 中的库函数 compile_package
编译.
这将创建一个 CompiledPackage
,其中包含编译后的字节码以及其他编译产物(源映射,文档,ABI).
CompiledPackage
可以转换为 OnDiskPackage
,反之亦然——后者是 CompiledPackage
数据按以下文件系统结构存放的形式:
文件夹a_move_package/
文件夹…/
- …
文件夹build/
文件夹dependency_name/
- BuildInfo.yaml
文件夹bytecode_modules/
- module_name.mv
- *.mv
文件夹source_maps/
- module_name.mvsm
- *.mvsm
文件夹bytecode_scripts/
- script_name.mv
- *.mv
文件夹abis/
- script_name.abi
- *.abi
文件夹module_name/
- function_name.abi
- *.abi
文件夹sources/
- module_name.move
- dependency_name2 …/
关于这些数据结构以及如何在 Rust 库中使用 Move 包系统的更多信息,请参阅 move-package
crate.
使用字节码作为依赖项
Section titled “使用字节码作为依赖项”当依赖项的 Move 源代码在本地不可用时,可以使用 Move 字节码作为依赖项.要使用此功能,需要将这些文件放在同级目录中,然后在相应的 Move.toml
文件中指定它们的路径.
使用本地字节码作为依赖项需要将字节码文件下载到本地,并且每个命名地址的实际地址必须在 Move.toml
中或通过 --named-addresses
指定.
请注意,aptos move prove
和 aptos move test
命令目前不支持将字节码作为依赖项.
我们通过一个示例来说明使用此功能的开发流程.假设我们要编译包 A
.包的布局如下:
文件夹A/
- Move.toml
文件夹sources/
- AModule.move
A.move
的定义如下,依赖于模块 Bar
和 Foo
:
module A::AModule { use B::Bar; use C::Foo; public fun foo(): u64 { Bar::foo() + Foo::bar() }}
假设 Bar
和 Foo
的源代码不可用,但对应的字节码文件 Bar.mv
和 Foo.mv
在本地可用.要使用它们作为依赖项,我们需要:
为 Bar
和 Foo
指定 Move.toml
文件.注意命名地址已在字节码中实例化为实际地址.在我们的示例中,C
的实际地址已绑定为 0x3
.因此,必须在 [addresses]
中将 C
指定为 0x3
,如下所示:
[package]name = "Foo"version = "0.0.0"
[addresses]C = "0x3"
将字节码文件和对应的 Move.toml
文件放在同一目录下,字节码需置于 build
子目录中.注意必须存在一个空的 sources
目录.例如,包 Bar
对应的文件夹 B
和包 Foo
对应的文件夹 C
的布局如下:
文件夹workspace/
文件夹A/
- Move.toml
文件夹sources/
- AModule.move
文件夹B/
- Move.toml
文件夹sources/
- …
文件夹build/
- Bar.mv
文件夹C/
- Move.toml
文件夹sources/
- …
文件夹build/
文件夹Foo/
文件夹bytecode_modules/
- Foo.mv
在目标(第一个)包的 Move.toml
中通过 [dependencies]
指定依赖(次级)包的位置.例如,假设三个包目录位于同一层级,A
的 Move.toml
应如下所示:
[package]name = "A"version = "0.0.0"
[addresses]A = "0x2"
[dependencies]Bar = { local = "../B" }Foo = { local = "../C" }
请注意,如果同一个包的字节码和源代码同时存在于搜索路径中,编译器会报错声明重复.
在使用第三方包时,可能会遇到引用了不同版本的 Move 和 Aptos 标准库包的问题.
这可能导致包解析失败.
"Error": "Move 编译失败: 无法解析包 'C' 的依赖: 在解析包 'C' 的依赖 'B' 时: 无法解析包依赖 'B': 在解析包 'B' 的依赖 'AptosFramework' 时: 无法解析包依赖 'AptosFramework': 发现冲突依赖: 包 'AptosFramework' 与 'AptosFramework' 冲突
要解决此问题,您可以通过命令行选项覆盖标准库包.这允许您在整个依赖关系树中强制执行特定版本的标准库.您可以将覆盖应用于诸如 aptos move compile
,aptos move run
等命令.语法如下:
--override-std <network name>
其中 network_name
可以是以下选项之一:
- devnet
- testnet
- mainnet