Sign in

    moon_shadow

    Read a MoonBit module's own moon.mod and git metadata at runtime, baked in at build time.

    shadow
    build-info
    metadata
    Download zip
    Author
    Version
    0.2.2
    License
    Apache-2.0
    Last updated
    18 hours ago
    Downloads
    57

    #moon_shadow

    在运行时读取 MoonBit 模块自己的元数据——moon.mod 的字段,以及构建那次 checkout 的 git 状态。两者都在构建期采集一次、烘焙进产物,运行时没有文件 I/O、没有 git、没有环境变量嗅探。

    let info = @moon_shadow.current()
    println(info.name) // "PaiGack/moon_shadow"
    println(info.version) // "0.1.1"
    println(info.long_version()) // "PaiGack/moon_shadow@0.1.1"
    println(info.dep("moonbitlang/moon_config")) // Some(...)

    let git = @moon_shadow.git()
    println(git.branch) // "main"
    println(git.to_version_string()) // "main-dirty 50a396b-dirty"

    #为什么 git 信息不是运行时采集

    你发布的软件跑在别人的电脑上:那台机器大概率没有 git、没有 .git,cwd 落在 哪里也不关你的事。运行时采集等于把不确定性外包给终端用户,最后只能拿到 available = false。想要稳定的 git 信息,只在构建这台机器、这次 checkout 上采集一次,然后编进产物。

    moon.mod 也是同样的道理,所以两条链共用同一份生成物:

    moon.mod ----(gen rule)---> shadow/generated.mbt --(compile)--> current() git 命令 ----(gen rule)---> --(compile)--> git()

    与运行时方案相比还有一个额外好处:烘焙进产物就是纯常量,wasm-gc / js / native 一样能用,不需要为读 git 限制 target。

    #在自己的模块中使用

    1. 依赖已发布的包:

    moon add PaiGack/moon_shadow

    1. 创建一个 shadow 包来实现该虚拟包,并让实现始终与你的 moon.mod 保持同步。只需要一个文件,shadow/moon.pkg:

    import { "PaiGack/moon_shadow/mooninfo", "PaiGack/moon_shadow/gitinfo", } virtual(implement: "PaiGack/moon_shadow/shadow") rule( name: "gen", command: "moon run --target js --target-dir _build/gen .mooncakes/PaiGack/moon_shadow/gen/main.mbt -- --meta-path PaiGack/moon_shadow/mooninfo --git-path PaiGack/moon_shadow/gitinfo \"$input\" \"$output\"", ) dev_build(rule: "gen", input: "../moon.mod", output: "generated.mbt")

    gen 规则在每次 moon check / moon build / moon run 时都会运行—— 包括 release 构建,pre-build 规则并非 debug 专用——它会读你的 moon.mod、再看一眼当前 checkout 的 git,把两者写进 shadow/generated.mbt。生成器直接从 .mooncakes 运行,moon add 会把依赖暂存在那里,因此模块旁边不需要检出任何东西;单独的 --target-dir 让生成器自己的构建产物不进入主 _build。

    也可以不手写:把 moon_shadow_example/shadow/ 整个复制到你的模块里。

    1. 让读取元数据的包指向你的实现——在每一个构建路径会经过 @moon_shadow.current() / @moon_shadow.git() 的 moon.pkg 里加上 import 和 override:

    import { "PaiGack/moon_shadow", } options( overrides: [ "your/module/shadow" ], )

    #生成器跑在哪个后端

    native。prebuild 里那条命令是 moon run --target native gen,生成器直接 用 libc 的 popen 去问 git,装什么运行时都无所谓——只要这台机器上有 /bin/sh,以及(想拿到 git 信息时)git。

    没有 git 不要紧:生成器照常写出 git_info().available = false,构建不会因此 失败。

    gen 这个包同时给 js / wasm-gc 备了 shim(js 走 process.binding, wasm-gc 没有进程模型、直接降级),所以你 override 想换成别的后端也能跑, 只是 js 那条路要机器上有 node。

    #工作原理

    moon.mod 和 git 状态在构建时各被读取一次,值随后被编译进产物。运行时没有 文件 I/O、没有 git、没有环境变量嗅探——只有结构体字段读取。

    元数据通过虚拟包 PaiGack/moon_shadow/shadow 跨越构建边界,它暴露两个访问 函数:

    pub fn module_info() -> @PaiGack/moon_shadow/mooninfo.ModuleInfo
    pub fn git_info() -> @PaiGack/moon_shadow/gitinfo.GitInfo

    两个类型都写完整包路径而不是别名:虚拟包的实现要按这份 pkg.mbti 检查, 而实现包自己的 moon.pkg 把这两个包叫什么别名,使用者说了算。gen 的 --meta-path / --git-path 就是这两个路径,生成的代码与 pkg.mbti 因此总能对上。

    shadow 包自带一份从本模块的 moon.mod 和本仓库的 git 生成的默认实现。 使用者通过 options(overrides: [...]) 指向自己的实现——即从它自己的 moon.mod 和它自己的 checkout 生成的那份——current() / git() 便会转而 报告该模块的元数据。采集规则在消费者模块里执行,cwd 是消费者的模块目录, git rev-parse 往上找正好落在消费者自己的仓库根上,这就是想要的语义。

    moon.mod 的 DSL 由 moonbitlang/moon_config 解析

    #API

    名称说明
    current()被构建模块的 ModuleInfo
    git()构建它的那次 checkout 的 GitInfo
    ModuleInfo::dep按模块名查找依赖
    ModuleInfo::long_version"name@version",moon add 接受的格式
    ModuleInfo::to_long_version_string"name version description",用于 --version 输出
    GitInfo::describe_version分支 / tag / describe 里最具体的那个,脏时带 -dirty
    GitInfo::commit_versioncommit(可要短 hash),脏时带 -dirty
    GitInfo::to_version_string上面两个拼起来;available = false 时是 "unavailable"
    GitInfo::is_dirtyavailable && dirty
    ModuleInfo 字段name version description license repository readme keywords preferred_target supported_targets deps
    GitInfo 字段available branch tag describe commit short_commit commit_date author email dirty ahead behind

    #仓库结构

    mooninfo/ ModuleInfo / Dependency、moon_config 解析、moon.mod 侧渲染 gitinfo/ GitInfo 类型、"怎么把一段输出解释成字段"的纯逻辑 gen/ 生成器可执行文件:读 moon.mod、跑 git、写出实现文件 shadow/ 虚拟包及其生成的默认实现、gen 规则 moon_shadow.mbt 公共 API:current()、git() moon_shadow_example/ 一个使用者模块,即上述配置的完整示例

    #需要了解的行为

    以下都源于 moon 处理虚拟包和 pre-build 规则的方式,它们决定了本库 能做出哪些承诺:

    • moon.pkg 声明的虚拟包必须暴露函数,不能是值。 moon 会用 pkg.mbti 类型检查虚拟包的各个实现,而一个来自其他包类型的 pub let 会通不过这个检查——尽管它在实现包里能编译。因此是 module_info() 而不是 module_info。
    • current() / git() 描述的是提供实现的人,不是调用它的人。 moon_shadow_example/ 展示了两种情况:没有 override 时得到库 baked-in 的值,有 override 时得到你自己的。
    • 模块不能 override 自己的虚拟包。 这就是示例之所以是独立模块、 而不是 moon_shadow 内另一个包的原因。
    • 来自 registry 的依赖不会运行构建规则。 MoonBit 会跳过 registry 依赖被消费时的 pre-build 步骤,所以本库无法替你完成模块的配置: 上面的 shadow 包需要你自己创建,里面的 gen 规则负责让 generated.mbt 保持新鲜。库在 shadow/ 里附带的只是给从不 override 的使用者的默认实现——也就是说 moon add 完,git() 拿到的是 本库烘焙进去的值,不是你的仓库。想要自己仓库的 git 信息,就得自己建 shadow 包和 gen 规则。
    • overrides 是 per-package 的。 每个构建路径会到达虚拟包的包 都需要自己的 options(overrides: [...])——因此示例在根包和 cmd/main 里各写了一份。
    • gen 规则通过 .mooncakes 找到生成器,即 moon 为 registry 依赖准备的暂存布局——registry 使用者磁盘上本库的唯一副本,且路径 不含版本号,升级不受影响。它是内部布局:如果未来的 moon 改动了它, 规则下次运行时会以 No such file or directory 失败——在任何构建上, 包括 release,因为 pre-build 规则并非 debug 专用。提交 generated.mbt 可以把损失限制在「克隆之后又去改 moon.mod」 的场景。
    • dirty 只看已跟踪文件。 git status --porcelain --untracked-files=no。 generated.mbt 是入库的,用默认行为的话「生成」这个动作本身就会让工作区 变脏,然后永远是脏的。
    • git 不在规则输入里。 dev_build(input: "../moon.mod") 只看 moon.mod, 这是有意的:否则每次 commit 都会让生成物失效、触发重编。代价是 git 变了 不会自动重新生成——想让 git_info() 跟上新 commit,moon clean 一下让 generated.mbt 重新生成即可。发布构建本来就在全新的 checkout 上跑,不受 影响。
    • 生成内容对同一份输入完全确定(不写时间戳、不写随机值),所以 「重新生成 = 无 diff」,CI 里那句 git diff --exit-code 才有意义。
    • 非 git 环境不让构建失败。 不是仓库、没装 git,生成器照常写文件, available = false、其余字段为空串 / 0,并在 stdout 上说一句。
    • 所有 git 调用带 GIT_OPTIONAL_LOCKS=0,避免抢 index.lock 卡住用户 自己的 git commit。
    • 转义只解码一次。 moon_config 返回的字符串字面量是源码原始 形式("a \"b\"" 得到 a \"b\"),因此 mooninfo 会在重新生成前 做一次反转义;否则每次重新生成都会让反斜杠翻倍。unescape 与 mooninfo::quote 互为逆运算,\u{...} / \xNN / \oNNN / 续行 都在内,所以「重新生成 = 无 diff」对含转义的 moon.mod 也成立。
    • 生成物不带尾逗号。 早先的渲染器刻意复刻 moon fmt 的尾逗号, 但 moon fmt 对字面量的输出并不带它;生成的代码现在就是格式化后的样子, 提交前不再需要手工格式化。
    • 包名。 mooninfo 装的是 moon.mod 的一半,gitinfo 装 git 的一半, 命名对齐;0.1.x 里的 core 包在 0.2.0 改名为 mooninfo,升级时把 moon.pkg 里的 PaiGack/moon_shadow/core 换成 PaiGack/moon_shadow/mooninfo 并重新生成一次 generated.mbt 即可。

    current

    moon.mod metadata of the module whose build compiled this package.

    The value crosses the build boundary through the shadow virtual package, which a consumer overrides with its own implementation, so no moon.mod is read and no file is touched at runtime.

    git

    Git state of the checkout that built this package.

    Captured once by the gen rule and compiled in, for the same reason as current(): the machine running the software has no .git, maybe no git, and its cwd has nothing to do with your repository.

    available = false when the build itself happened outside a git work tree.

    Source Files