MoonBit 构建系统教程#

Moon 是 MoonBit 语言的构建系统,目前基于 n2 项目。Moon 支持并行和增量构建。此外,moon 还在 mooncakes.io 上管理和构建第三方包。

先决条件#

在开始本教程之前,请确保你已安装以下内容:

  1. MoonBit CLI 工具:从 https://www.moonbitlang.cn/download/ 下载。这个命令行工具用于创建和管理 MoonBit 项目。

    使用 moon help 查看使用说明。

    $ moon help
    ...
    
  2. Visual Studio Code 中的 MoonBit 语言 插件:你可以从 VS Code 市场安装。此插件为 MoonBit 提供了丰富的开发环境,包括语法高亮、代码补全等功能。

一旦你满足了这些先决条件,我们就可以开始创建一个新的 MoonBit 模块。

创建一个新模块#

要创建一个新模块,在终端中输入 moon new <path> 命令,其中 path 是你想要创建项目的文件夹目录,之后项目会被创建。使用默认值,你会在 my_project 目录中创建一个名为 username/my_project 的新模块。

$ moon new my_project
Initialized empty Git repository in my_project/.git/
Created username/my_project at my_project

你也可以通过使用 --user 选项和 --name 选项分别指定用户名和模块名称。如果你已经登录,用户名将默认为你的用户名。

理解模块目录结构#

创建新模块后,你的目录结构应该如下:

my_project
├── Agents.md
├── cmd
│   └── main
│       ├── main.mbt
│       └── moon.pkg
├── LICENSE
├── moon.mod.json
├── moon.pkg
├── my_project_test.mbt
├── my_project.mbt
├── README.mbt.md
└── README.md -> README.mbt.md

备注

在 Windows 系统上,你需要管理员权限或启用开发者模式才能创建符号链接。

以下是目录结构的简要说明:

  • moon.mod.json 用于标识目录为 MoonBit 模块。它包含模块的元数据,如模块名称、版本等。

    {
      "name": "username/my_project",
      "version": "0.1.0",
      "readme": "README.md",
      "repository": "",
      "license": "Apache-2.0",
      "keywords": [],
      "description": ""
    }
    
  • .cmd/main 目录:这些是模块中的包。每个包可以包含多个 .mbt 文件,这些文件是 MoonBit 语言的源代码文件。但是,无论一个包有多少 .mbt 文件,它们都共享同一个 moon.pkg 文件。较旧的项目仍然可能使用遗留的 moon.pkg.json 格式。*_test.mbt 是包中的独立测试文件,这些文件用于黑盒测试,因此同一个包的私有成员不能直接访问。

  • moon.pkg 是包描述符。它定义了包的属性,例如它是否是 main 包以及它导入的包。

    • cmd/main/moon.pkg:

      import {
        "username/my_project" @lib,
      }
      
      options(
        "is-main": true,
      )
      

      这里,"is-main": true 表示该包包含 moon run 命令的入口。

    • moon.pkg:

      这个文件可以是空的。它的作用只是告诉构建系统这个文件夹是一个包。

  • README.mbt.md 是 README 文件。在这个文件里,mbt check 代码块会由 moon checkmoon test 进行检查和运行。

使用包#

我们的 username/my_project 模块包含两个包:username/my_projectusername/my_project/cmd/main

username/my_project 包包含 my_project.mbtmy_project_test.mbt 文件:

my_project.mbt#
///|
pub fn fib(n : Int) -> Int64 {
  for i = 0, a = 0L, b = 1L; i < n; i = i + 1, a = b, b = a + b {

  } else {
    b
  }
}
my_project_test.mbt#
///|
test "fib" {
  let array = [1, 2, 3, 4, 5].map(fib(_))

  // `inspect` is used to check the output of the function
  // Just write `inspect(value)` and execute `moon test --update`
  // to update the expected output, and verify them afterwards
  inspect(array, content="[1, 2, 3, 5, 8]")
}

备注

生成的文件名取决于包名。

username/my_project/cmd/main 包包含一个 main.mbt 文件:

///|
fn main {
  println(@lib.fib(10))
}

要执行程序,在 moon run 命令中指定文件系统路径到 username/my_project/cmd/main 包:

$ moon run cmd/main
89

你可以使用 moon test 命令进行测试:

$ moon test
Total tests: 1, passed: 1, failed: 0.

选择目标后端#

Moon 有三个与目标后端相关的配置项,它们分别负责不同的事情:

  • 命令行上的 --target 用于选择当前命令使用哪个后端

  • moon.mod.json 中的 preferred-target 用于为 moon 和语言服务器选择默认后端

  • supported-targets 用于声明一个模块或包打算支持哪些后端

例如,一个以 native 为主的 CLI 项目可以这样设置:

{
  "preferred-target": "native",
  "supported-targets": "native"
}

supported-targets 使用目标集合语法,例如 js+js+wasm-gc+all-js

如果只有部分文件与后端相关,就让模块或包的元数据保持宽泛,并使用 moon.pkg 或遗留的 moon.pkg.json 中的 targets 按后端选择文件。

包导入#

在 MoonBit 构建系统中,依赖是在包级别声明的。要在 username/my_project/cmd/main 中导入 username/my_project 包,你需要在 cmd/main/moon.pkg 中指定:

import {
  "username/my_project" @lib,
}

options(
  "is-main": true,
)

这里,"username/my_project" 表示导入根包,并为它指定别名 lib,因此你可以在 cmd/main/main.mbt 中使用 @lib.fib(10)

创建和使用新包#

首先,在模块根目录下创建一个名为 fib 的新目录:

mkdir fib

现在,你可以在 fib 下创建新文件:

slow.mbt#
pub fn fib_slow(n : Int) -> Int {
  match n {
    0 => 0
    1 => 1
    _ => fib_slow(n - 1) + fib_slow(n - 2)
  }
}
fast.mbt#
pub fn fib_fast(num : Int) -> Int {
  fn aux(n, acc1, acc2) {
    match n {
      0 => acc1
      1 => acc2
      _ => aux(n - 1, acc2, acc1 + acc2)
    }
  }

  aux(num, 0, 1)
}
moon.pkg#
// This package does not need extra options yet.

在创建这些文件后,你的目录结构应该如下:

.
├── Agents.md
├── cmd
│   └── main
│       ├── main.mbt
│       └── moon.pkg
├── fib
│   ├── fast.mbt
│   ├── moon.pkg
│   └── slow.mbt
├── LICENSE
├── moon.mod.json
├── moon.pkg
├── my_project_test.mbt
├── my_project.mbt
├── README.mbt.md
└── README.md -> README.mbt.md

cmd/main/moon.pkg 文件中,导入 username/my_project/fib 包并将其别名定制为 my_awesome_fibonacci

import {
  "username/my_project/fib" @my_awesome_fibonacci,
}

options(
  "is-main": true,
)

这会导入 fib 包。完成之后,你就可以在 cmd/main/main.mbt 中使用 fib 包了。将 cmd/main/main.mbt 的内容替换为:

fn main {
  let a = @my_awesome_fibonacci.fib_slow(10)
  let b = @my_awesome_fibonacci.fib_fast(11)
  println("fib(10) = \{a}, fib(11) = \{b}")
}

为了执行你的程序,指定 main 包的路径:

$ moon run cmd/main
fib(10) = 55, fib(11) = 89

添加测试#

让我们添加一些测试来验证我们的 fib 实现。在 fib/fib_test.mbt 中添加以下内容:

test {
  inspect(@fib.fib_slow(0))
  inspect(@fib.fib_slow(1))
  inspect(@fib.fib_slow(2))
}

这段代码测试斐波那契数列的前三项。test { ... } 定义了一个内联测试块。内联测试块中的代码在测试模式下执行。

内联测试块在非测试编译模式(moon buildmoon run)中被丢弃,因此它们不会导致生成的代码大小膨胀。

这里我们使用了快照测试。执行 moon test --update,文件应该被更改为:

test {
  inspect(@fib.fib_slow(0), content="0")
  inspect(@fib.fib_slow(1), content="1")
  inspect(@fib.fib_slow(2), content="1")
}

注意,测试代码使用 @fib 来引用 fib 包。构建系统通过使用以 _test.mbt 结尾的文件自动为黑盒测试创建一个新包。

最后,使用 moon test 命令,它会扫描整个项目,识别并运行所有测试。如果一切正常,你将看到:

$ moon test
Total tests: 2, passed: 2, failed: 0.