为什么需要包
学习这门语言或者试一段代码时,一个 .0 文件就够了。可一旦项目超过一个文件,你就需要一个包:一个带清单文件、采用工具链能识别的固定结构的目录。
从零散文件转到包,有这些好处:
- 项目的名称、版本和元数据有一个统一的存放位置。
- 一棵源代码树里可以有多个入口(可执行文件、库、测试)。
- 结构可预测:工具无需配置就能找到你的源代码。
- 可以对整棵源代码树运行
zero check/zero build,而不用一个文件一个文件地处理。
生成包的脚手架
最快的开始方式是 zero new:
zero new cli hello
这会创建一个 hello/ 目录,结构如下:
hello/
├── zero.json
└── src/
└── main.0
cli 是模板名,它会生成一个可执行的命令行程序。其他模板(库、系统程序)结构相同,只是默认设置不同。
用 cd 进入新目录,运行它,就可以开始了:
cd hello
zero run
在包目录里运行 zero run 而不指定文件时,它会从 zero.json 中读取默认目标并运行。
zero.json 清单文件
用 cli 模板生成的包,清单文件是这样的:
{
"package": { "name": "hello", "version": "0.1.0" },
"targets": { "cli": { "kind": "exe", "main": "src/main.0" } }
}
它有两个顶层键:package 和 targets。前者标识这个包,后者告诉编译器要构建什么。
package
"package": {
"name": "hello",
"version": "0.1.0"
}
name:标识包的短名称。使用小写字母和连字符。version:语义化版本字符串。1.0 之前的包使用0.x.y。
可能还支持其他元数据字段(描述、作者、许可证、代码仓库)。清单格式仍在演进,权威的结构请以当前的 Zero 文档为准。
targets
"targets": {
"cli": { "kind": "exe", "main": "src/main.0" }
}
键(这里是 cli)是你自己起的目标名称。值描述每个目标:
kind:目标的类型。exe表示可执行文件。其他类型(库、测试)结构相同。main:入口源文件,路径相对于包的根目录。
同一个包里可以声明多个目标:
{
"package": { "name": "image-tools", "version": "0.1.0" },
"targets": {
"convert": { "kind": "exe", "main": "src/convert.0" },
"resize": { "kind": "exe", "main": "src/resize.0" },
"lib": { "kind": "lib", "main": "src/lib.0" }
}
}
在 CLI 中写上目标名称,就能构建指定的目标:
zero build convert
zero run resize
src/ 目录
所有源文件都放在 src/ 下。编译器会自动遍历这个目录,你不需要在清单文件中列出每个文件。每个目标的 main 字段指向它的入口文件,编译器从那里顺着导入找到需要的其他所有内容。
一个带有几个辅助模块的包可能是这样的:
image-tools/
├── zero.json
└── src/
├── convert.0
├── resize.0
├── lib.0
└── internal/
├── decoder.0
└── encoder.0
internal/ 子目录只是一种约定,清单文件里不会提到这些文件。convert.0 中的导入可以直接访问 internal/decoder.0。
构建与运行
进入包之后,常用的操作有:
zero check # type-check the whole tree
zero run # build and run the default target
zero run convert # build and run a specific named target
zero build # build the default target
zero build --all # build every target (when supported)
zero test # run every test target
CLI 会读取 zero.json,弄清楚要做什么,然后执行。在包里工作时,你很少需要写出完整路径。
多个源文件:一个简单示例
假设 src/main.0 调用了 src/math.0 中的一个辅助函数。辅助函数所在的文件:
pub fun double(value: i32) -> i32 {
return value * 2
}
入口文件:
pub fun main(world: World) -> Void raises {
let result = double(21)
if result == 42 {
check world.out.write("forty two\n")
}
}
用 zero run 运行它。在这个简单的例子里,即使没有显式的导入声明,编译器也会在源代码树的其他文件中解析对 double 的引用。包变大以后,跨模块的可见性由显式的导入系统处理。导入语法请参考当前的 Zero 文档,这是 1.0 之前最可能发生变化的部分之一。
哪些东西不该提交到 git
Zero 包的 .gitignore 通常需要包含:
# build artifacts and caches
/build/
/target/
# editor cruft
.DS_Store
*.swp
构建输出目录的具体名称可能不同,请查看当前的工具链文档。但规则不变:源代码提交进去,构建产物留在外面。
共享包
Zero 还没到 1.0,包注册中心还不属于稳定的功能。目前共享包的实用方式有:
- Git:克隆仓库,然后对它运行
zero check。 - 内置副本(vendoring):把源代码复制一份放进另一个项目。
等注册中心推出后,包的引用很可能会移到 zero.json 中的依赖字段里。把这当作未来的功能,现在不要写脚本依赖它。
下一篇:语言基础
现在你已经掌握了组织一个真实 Zero 项目所需的一切。下一章将聚焦语言本身,从 let 绑定 开始,也就是 Zero 中的值如何被命名。
常见问题
Zero 包是什么?
Zero 包是一个目录,里面有一个 zero.json 清单文件和一个存放 .0 源文件的 src/ 文件夹。清单文件声明包的名称、版本,以及一个或多个“目标”(target):每个目标告诉编译器如何从源代码构建出某样东西(可执行文件、库或测试程序)。
如何创建一个新的 Zero 包?
运行 zero new <template> <name>,例如 zero new cli hello。CLI 会生成一个目录,里面有 zero.json、src/main.0,以及所选模板需要的其他文件。之后你就可以在包里运行 zero check、zero run 和 zero build。
zero.json 里有什么?
至少包含一个带 name 和 version 的 package 对象,以及一个描述包所构建内容的 targets 对象。每个目标有一个 kind(例如表示可执行文件的 exe)和一个指向入口源文件的 main。一个清单文件里可以声明多个目标。
一个 Zero 包可以有多个目标吗?
可以。一个包可以声明任意数量的目标,例如一个用于命令行工具的 exe 目标、一个用于可复用库的 lib 目标,以及一个或多个测试目标。每个目标在 src/ 下都有自己的入口,你可以通过 CLI 单独构建或运行它们。
编译器把构建输出放在哪里?
构建产物会放在包内的一个构建目录里(具体路径由实现决定,在 Zero 1.0 之前可能会变)。src/ 下的源代码树永远不会被修改。把构建目录当作可以随时删除的东西,把它提交到 git 不是个好主意。