包管理
包 (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