workspace: enforce the layer dependency rule
README stated that a crate may only depend on layers to its left, but nothing checked it, and the wording was self contradicting: the example given for a cross layer jump was a reverse dependency. Clarify the rule, add scripts/check-layers.sh to enforce it, run it in CI, and record the boundary cases in docs/design/layers.md.
This commit is contained in:
parent
26ee3d72e9
commit
8f74f8f97a
|
|
@ -37,6 +37,11 @@ jobs:
|
||||||
# meaningful on a quiet machine, but it must still compile so that it
|
# meaningful on a quiet machine, but it must still compile so that it
|
||||||
# does not rot.
|
# does not rot.
|
||||||
run: cargo build -p apsis-rs --features full --example bench
|
run: cargo build -p apsis-rs --features full --example bench
|
||||||
|
- name: Check the layer rule
|
||||||
|
# README.md states that a crate may only depend on layers to its
|
||||||
|
# left. Nothing else in the build enforces that, so a reverse
|
||||||
|
# dependency would otherwise pass CI silently.
|
||||||
|
run: bash scripts/check-layers.sh
|
||||||
- name: Verify dependency freedom
|
- name: Verify dependency freedom
|
||||||
run: |
|
run: |
|
||||||
# Every package in the graph must be a workspace crate. `--prefix
|
# Every package in the graph must be a workspace crate. `--prefix
|
||||||
|
|
|
||||||
13
README.md
13
README.md
|
|
@ -17,9 +17,15 @@ foundation -> physics -> orbit -> rf -> network -> ops -> sim -> apsis-rs
|
||||||
|
|
||||||
约束:
|
约束:
|
||||||
|
|
||||||
- 依赖只能从左向右,禁止反向依赖
|
- 依赖只能从左向右。下层可依赖其上任意层,上层不得依赖其下任意层。
|
||||||
- 禁止跨层跳跃,例如 `orbit` 不得直接依赖 `network`
|
例如 `network` 可以依赖 `orbit`(向左),但 `orbit` 不得依赖 `network`
|
||||||
- 层内允许依赖该层的基准 crate:`foundation` 内 `apsis-core` 为基准,其余 crate 可依赖它;其余层内部以各自的门面 crate(`apsis-orbit`、`apsis-rf`、`apsis-sim` 等)作为整合点
|
(向右,即反向)。左向依赖不限层数,允许跨层,因为把 `orbit` 的值
|
||||||
|
传到 `network` 时中间层未必提供中转类型。
|
||||||
|
- `foundation` 的 `apsis-core` 是全局基准,任何层任何 crate 均可依赖它。
|
||||||
|
`apsis-math` 同理,因为线性代数被各层共用。
|
||||||
|
- 层内以该层的门面 crate(`apsis-orbit`、`apsis-rf`、`apsis-sim`)作为整合点。
|
||||||
|
|
||||||
|
这套规则由 `scripts/check-layers.sh` 强制,CI 会运行它。
|
||||||
|
|
||||||
各层内容:
|
各层内容:
|
||||||
|
|
||||||
|
|
@ -70,6 +76,7 @@ apsis-rs/
|
||||||
│ └── apsis-rs/
|
│ └── apsis-rs/
|
||||||
│ ├── examples/ # 对拍导出与跨层集成检查
|
│ ├── examples/ # 对拍导出与跨层集成检查
|
||||||
│ └── ...
|
│ └── ...
|
||||||
|
├── scripts/ # 仓库检查脚本
|
||||||
├── validation/ # Python 对拍验证
|
├── validation/ # Python 对拍验证
|
||||||
└── docs/design/ # 设计文档
|
└── docs/design/ # 设计文档
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
- [单位与坐标约定](units.md)
|
- [单位与坐标约定](units.md)
|
||||||
- [错误与容差策略](errors.md)
|
- [错误与容差策略](errors.md)
|
||||||
|
- [分层边界](layers.md)
|
||||||
|
|
||||||
## 各层设计
|
## 各层设计
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
# 分层边界
|
||||||
|
|
||||||
|
本文件记录工作区的分层规则,以及为什么某些跨层依赖是允许的。README 的
|
||||||
|
"分层"一节是规则本身,这里是它的判定标准与边界情况。
|
||||||
|
|
||||||
|
## 规则
|
||||||
|
|
||||||
|
层自左向右排列:
|
||||||
|
|
||||||
|
```text
|
||||||
|
foundation -> physics -> orbit -> rf -> network -> ops -> sim -> apsis-rs
|
||||||
|
```
|
||||||
|
|
||||||
|
一条依赖边从 crate A 指向 crate B。把 A 所在的层与 B 所在的层分别映射为
|
||||||
|
下标,规则是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
index(B) <= index(A) 允许
|
||||||
|
index(B) > index(A) 违规(反向依赖)
|
||||||
|
```
|
||||||
|
|
||||||
|
即**只能向左依赖,不能向右依赖**。`scripts/check-layers.sh` 对工作区每个
|
||||||
|
清单执行这条判定,CI 会运行它。
|
||||||
|
|
||||||
|
## 允许跨层
|
||||||
|
|
||||||
|
规则只看方向,不看距离。从 `network` 直接依赖 `orbit` 是允许的,尽管中间
|
||||||
|
隔着 `rf`:`orbit` 在 `network` 左边,方向正确。
|
||||||
|
|
||||||
|
允许向左跨层是有意的。若强制只能依赖紧邻的左层,会产生两类问题:
|
||||||
|
|
||||||
|
- **中转类型不存在。** 把 `orbit` 层的位置传给 `network` 层时,`rf` 层并
|
||||||
|
非必然提供一个承载该位置的类型。强行要求经过 `rf` 只会制造只是转发的
|
||||||
|
空壳类型。
|
||||||
|
- **无谓的耦合。** `network` 的星座计算需要 `apsis-kepler` 的开普勒要素,
|
||||||
|
它并不需要 `rf`。若禁止跨层,`network` 就得依赖 `rf` 只为拿到一个它不
|
||||||
|
使用的类型。
|
||||||
|
|
||||||
|
因此规则约束的是**方向**,因为方向才对应真正的分层语义:上层的概念可以
|
||||||
|
引用下层的概念,反之则说明分层错了。
|
||||||
|
|
||||||
|
## 全局基准
|
||||||
|
|
||||||
|
`apsis-core`(错误、常量、单位标记)与 `apsis-math`(`Vec3`、`Mat3`、
|
||||||
|
`Quat`)被各层共用,包括最左的 `foundation` 之外的所有层。它们本身在
|
||||||
|
`foundation` 中,所以"只向左依赖"已经允许任何层使用它们,无需特例。
|
||||||
|
|
||||||
|
这与"层内基准"不同:`apsis-core` 也是 `foundation` 内的基准,但它的可用
|
||||||
|
范围是整个工作区,而不只是同层。
|
||||||
|
|
||||||
|
## 层内门面
|
||||||
|
|
||||||
|
每层以一个门面 crate 作为整合点:
|
||||||
|
|
||||||
|
| 层 | 门面 |
|
||||||
|
| --- | --- |
|
||||||
|
| orbit | `apsis-orbit` |
|
||||||
|
| rf | `apsis-rf` |
|
||||||
|
| sim | `apsis-sim` |
|
||||||
|
| 对外 | `apsis-rs` |
|
||||||
|
|
||||||
|
门面把同层的能力组合成少量类型,并再导出同层的其余 crate。`foundation`、
|
||||||
|
`physics`、`network`、`ops` 没有单独门面:它们的 crate 各自独立有用,且
|
||||||
|
`apsis-rs` 会把它们一并再导出。
|
||||||
|
|
||||||
|
## 边界情况
|
||||||
|
|
||||||
|
- **facade `apsis-rs` 不在层序列中。** 它依赖全部层,因为它就是出口。
|
||||||
|
`scripts/check-layers.sh` 通过层目录名判定,`crates/apsis-rs` 不是
|
||||||
|
`crates/<layer>/<crate>` 形状,因此被跳过。
|
||||||
|
- **`dev-dependencies` 也计入检查。** 一条测试里的反向依赖同样说明分层
|
||||||
|
错了,应当修正代码而不是放宽测试。
|
||||||
|
- **门面再导出不构成新依赖。** `apsis-rs` 把 `apsis-orbit` 再导出,是
|
||||||
|
门面行为,不是某层对 `orbit` 的依赖。
|
||||||
|
|
||||||
|
## 检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/check-layers.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本输出检查过的依赖边数,并在发现反向依赖时逐条列出后以退出码 1 结束。
|
||||||
|
对当前工作区,它检查 141 条边,全部方向正确。
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Checks the layer dependency rule stated in README.md.
|
||||||
|
#
|
||||||
|
# The workspace is organized into dependency ordered layers:
|
||||||
|
#
|
||||||
|
# foundation -> physics -> orbit -> rf -> network -> ops -> sim
|
||||||
|
#
|
||||||
|
# A crate may depend on any crate in a layer to its left, but never on one to
|
||||||
|
# its right. This script reads every workspace manifest, works out which
|
||||||
|
# layer each dependency belongs to, and fails if any dependency points to the
|
||||||
|
# right.
|
||||||
|
#
|
||||||
|
# `apsis-core` and `apsis-math` are exempt as global prerequisites: they sit
|
||||||
|
# in `foundation` and every layer may use them, which the left-only rule
|
||||||
|
# already permits, so no special case is needed for them.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
#
|
||||||
|
# scripts/check-layers.sh
|
||||||
|
#
|
||||||
|
# Exit status is zero when the rule holds and one otherwise.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# The layer order, left to right. A dependency to a layer with a lower index
|
||||||
|
# is allowed; to a higher index is a reverse dependency and fails.
|
||||||
|
LAYERS=(foundation physics orbit rf network ops sim)
|
||||||
|
|
||||||
|
# The root of the workspace, taken from this script's location so that the
|
||||||
|
# check works from any working directory.
|
||||||
|
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$root"
|
||||||
|
|
||||||
|
# Returns the index of a layer name, or an empty string when the name is not
|
||||||
|
# a layer. A crate outside these directories is not part of the layer scheme
|
||||||
|
# and is skipped rather than reported.
|
||||||
|
layer_index() {
|
||||||
|
local name="$1"
|
||||||
|
local index=0
|
||||||
|
for layer in "${LAYERS[@]}"; do
|
||||||
|
if [ "$layer" = "$name" ]; then
|
||||||
|
echo "$index"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
index=$((index + 1))
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
failures=0
|
||||||
|
checked=0
|
||||||
|
|
||||||
|
for manifest in $(find crates -name Cargo.toml | sort); do
|
||||||
|
directory="$(dirname "$manifest")"
|
||||||
|
# The layer directory is the second path component, as in
|
||||||
|
# `crates/<layer>/<crate>/Cargo.toml`.
|
||||||
|
layer="$(echo "$directory" | cut -d/ -f2)"
|
||||||
|
source_index="$(layer_index "$layer")"
|
||||||
|
[ -z "$source_index" ] && continue
|
||||||
|
|
||||||
|
# Every `apsis-...` name in the manifest is a dependency or a package
|
||||||
|
# name. The package name is the crate itself, which resolves to the same
|
||||||
|
# layer and is skipped by the index comparison below.
|
||||||
|
for dependency in $(grep -oE 'apsis-[a-z-]+' "$manifest" | sort -u); do
|
||||||
|
dependency_directory="$(find crates -maxdepth 2 -type d -name "$dependency" 2>/dev/null | head -1)"
|
||||||
|
[ -z "$dependency_directory" ] && continue
|
||||||
|
dependency_layer="$(echo "$dependency_directory" | cut -d/ -f2)"
|
||||||
|
dependency_index="$(layer_index "$dependency_layer")"
|
||||||
|
[ -z "$dependency_index" ] && continue
|
||||||
|
|
||||||
|
if [ "$dependency_index" -gt "$source_index" ]; then
|
||||||
|
echo "reverse dependency: $layer -> $dependency_layer ($(basename "$directory") -> $dependency)" >&2
|
||||||
|
failures=$((failures + 1))
|
||||||
|
fi
|
||||||
|
checked=$((checked + 1))
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$failures" -gt 0 ]; then
|
||||||
|
echo "layer check failed: $failures reverse dependency(ies)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "layer check passed: $checked dependency edge(s), none reversed"
|
||||||
Loading…
Reference in New Issue