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
|
||||
# does not rot.
|
||||
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
|
||||
run: |
|
||||
# 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`
|
||||
- 层内允许依赖该层的基准 crate:`foundation` 内 `apsis-core` 为基准,其余 crate 可依赖它;其余层内部以各自的门面 crate(`apsis-orbit`、`apsis-rf`、`apsis-sim` 等)作为整合点
|
||||
- 依赖只能从左向右。下层可依赖其上任意层,上层不得依赖其下任意层。
|
||||
例如 `network` 可以依赖 `orbit`(向左),但 `orbit` 不得依赖 `network`
|
||||
(向右,即反向)。左向依赖不限层数,允许跨层,因为把 `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/
|
||||
│ ├── examples/ # 对拍导出与跨层集成检查
|
||||
│ └── ...
|
||||
├── scripts/ # 仓库检查脚本
|
||||
├── validation/ # Python 对拍验证
|
||||
└── docs/design/ # 设计文档
|
||||
```
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
- [单位与坐标约定](units.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