ccgo 3.7.2

A high-performance C++ cross-platform build CLI
# 依赖链接

两个正交维度共同决定一个依赖如何进入你的构建产物:

| 维度 | CCGO.toml 字段 | 取值 |
|---|---|---|
| ****自己产出什么 | `[build].link_type` | `static``shared``both` |
| **依赖**与你的关系 | `[[dependencies]].linkage`(项目级默认值见 `[build].default_dep_linkage`| `shared-external``static-embedded``static-external` |

## 链接取值

* **`shared-external`** —— 依赖保持为独立的 `.so`/`.dylib`/`.dll`,你的二进制记录运行时依赖(ELF 上为 `DT_NEEDED`)。当依赖能产出共享产物时,这是 shared 消费者的默认选项。最适合在多个消费者间共享同一应用内依赖 —— 避免胖归档为每个消费者都复制一份依赖。
* **`static-embedded`** —— 依赖的 `.a` 在链接时被归档进你的二进制。你的二进制变得自包含;多个消费者各自携带一份依赖代码。当依赖只发布 `.a` 时是默认回退方案。
* **`static-external`** —— 依赖保留为独立的 `.a`,你的 `.a` 仅记录该依赖关系而不合并。仅对 static 消费者有效;最终可执行文件的链接器在 exe 链接时解析符号。即"细链"模型。
* `shared-embedded` —— **不存在**。尝试设置该值会产生解析错误,并指向两个有效的备选项。`.so` 不能被归档进另一个 `.so`
## build-as 与 linkage 有效组合

三种 linkage 取值对不同消费者类型(`build-as`)有不同的有效性规则,
无效组合会在配置解析时报错。

### 通用提示字段(`linkage` / `default_dep_linkage`

当设置在 `linkage` 或 `default_dep_linkage` 字段(作用于所有消费者形态)时,
允许的取值取决于 `build-as`:

| `build-as` | `shared-external` | `static-embedded` | `static-external` |
|---|---|---|---|
| `shared` | ✅(默认)|||
| `static` ||| ✅(默认)|
| `both` ||||

`build-as = both` 取两者的交集:某个取值必须对**两种**消费者形态都有效。
`static-embedded` 违反 static 侧,`static-external` 违反 shared 侧,
因此无条件安全的只有 `shared-external`。

### 消费者专属字段(`linkage_on_shared` / `linkage_on_static`

通过这两个字段,可以为不同消费者形态分别指定提示,互不冲突:

| 取值 | `linkage_on_shared` | `linkage_on_static` |
|---|---|---|
| `shared-external` |||
| `static-embedded` |||
| `static-external` |||

`linkage_on_shared` 永不允许 `static-external`(会在 `.so` 中留下未解析符号)。
`linkage_on_static` 永不允许 `static-embedded`(静态归档不能内嵌其他静态归档)。

## 决策矩阵

| 消费者 | 依赖提供 | 提示 | 结果 |
|---|---|---|---|
| `shared` |`.a` |(任意)| `static-embedded`(强制;`shared-external` 提示报错)|
| `shared` |`.so` |(任意)| `shared-external`(强制;`static-embedded` 提示报错)|
| `shared` | 两者皆有 / 源码 | 缺省 | `shared-external` |
| `shared` | 两者皆有 / 源码 | `shared-external` | `shared-external` |
| `shared` | 两者皆有 / 源码 | `static-embedded` | `static-embedded` |
| `shared` | 任意 | `static-external` | **报错** —— shared 消费者禁用此值 |
| `static` |`.a` | 缺省 / `static-external` | `static-external` |
| `static` |`.a` | `shared-external` | **报错** —— 依赖无 `.so` |
| `static` |`.so` | 缺省 / `shared-external` | `shared-external` |
| `static` |`.so` | `static-external` | **报错** —— 依赖无 `.a` |
| `static` | 两者皆有 / 源码 | 缺省 / `static-external` | `static-external` |
| `static` | 两者皆有 / 源码 | `shared-external` | `shared-external` |
| `static` | 任意 | `static-embedded` | **报错** —— 静态归档不能内嵌其他归档 |

## 何时需要覆盖

大多数项目根本不需要设置 `linkage`。默认值
(当依赖发布 `.so` 时,shared 消费者使用 shared-external)能在多个同级消费者
之间避免膨胀,同时不带意外。

需要在某个依赖上设 `linkage = "static-embedded"` 的场景:
* 该依赖较小,且你希望保持自包含(少发布一个 `.so`)。
* 你正在通过 Maven/CocoaPods 向外部开发者发布 SDK,他们没有
  独立安装传递依赖的途径。
* 该依赖只发布 `.a`,你想消除构建日志中的自动回退提示。

## 构建期日志

ccgo 在 CMake configure 时会针对每个依赖输出一行 `STATUS`:

```
[ccgo] stdcomm:    linkage=shared-external (DT_NEEDED to dep.so)
[ccgo] tinyhelper: linkage=static-embedded (.a archived into target)
[ccgo] zstd:       linkage=static-embedded (auto, no .so available)
```

`(auto, ...)` 表示该结果来自回退,而非显式的 `linkage` 字段;
`(auto, no .so available)` 特指依赖未发布共享形式,因此只能内嵌。

## 示例

```toml
[package]
name = "logcomm"
version = "1.0.0"

[build]
link_type = "shared"                 # 我产出 .so
default_dep_linkage = "shared-external"  # 我的依赖默认值

[[dependencies]]
name = "stdcomm"
version = "25.2.9519653"
# 应用默认 linkage → shared-external(libstdcomm.so 保持独立)

[[dependencies]]
name = "tinyhelper"
version = "0.3.0"
linkage = "static-embedded"          # 显式:归档进 liblogcomm.so
```

可读为:"我是一个共享库。默认情况下我的依赖都是外部的。tinyhelper
是例外 —— 把它焊进我里面。" 三种语义、三个设置、毫无歧义。

## 仅源码依赖

当一个依赖只发布源码(`.ccgo/deps/<name>/` 目录下有 `src/` 和
`CCGO.toml`,但没有当前目标平台的 `lib/<platform>/` 产物)时,
`ccgo build` 会自动递归:在解析 linkage 之前,它会在该依赖目录内
派生 `ccgo build <platform> --build-as <derived>`,然后将
`dep/lib/<platform>/` symlink 到新的构建输出,让消费者的
`FindCCGODependencies.cmake` 巡查能找到产物。

`--build-as` 的取值由消费者对该依赖解析后的提示派生:

| 消费者对依赖的提示 | 递归 `--build-as` |
|---|---|
| `shared-external`(默认)| `shared` |
| `static-embedded` / `static-external` | `static` |
|(无提示)| `both` |

依赖自身的 `[build].link_type` 声明**不会**决定递归实例化时
产出什么 —— 由消费者的需求决定。如果你在某次构建中设置了
`--linkage stdcomm=static-embedded` 且 `stdcomm` 是仅源码依赖,
就会得到 `.a`;如果切换到 `--linkage stdcomm=shared-external`,
ccgo 会在下次构建时把 `stdcomm` 重建为 `.so`。

### 缓存

实例化步骤会在
`~/.ccgo/cache/<project-path>/<dep>/<platform>_<build_as>.fingerprint`
(位于项目目录之外,不会在工作区产生生成文件)持久化一份按平台、按 `--build-as` 隔离的指纹。指纹是
(按字典序的源码树 mtime + size + 路径)+ `CCGO.toml` 内容
+ 请求的 `--build-as` 的 SHA-256。当指纹匹配且 `lib/<platform>/`
仍存在产物时,后续构建会跳过递归 spawn。按 `build_as` 拆分 sidecar
是为了避免同一路径源码依赖的两次并行构建(一个想要
`--build-as shared`、另一个想要 `--build-as static`)在共享 sidecar 上发生竞态。

行为矩阵:

| 状态 | 动作 |
|---|---|
|`lib/<platform>/`、无 fingerprint | 派生构建,写入 fingerprint |
|`lib/<platform>/`、fingerprint 存在 | 派生构建(lib 已被删除)|
| `lib/<platform>/` 存在、fingerprint 匹配 | 跳过(缓存命中)|
| `lib/<platform>/` 存在、fingerprint 不匹配 | 派生构建(源码已变)|
| `lib/<platform>/` 存在、无 fingerprint | 信任已构建产物,写入 fingerprint |

"信任已构建产物"路径对那些自带手工策划的 `lib/<platform>/`
布局的 fixture 和项目(例如 xcframework symlink)很重要。它们在
首次调用时被打上 fingerprint,自此参与正常的源码变更失效流程。

### 哪些参数会传播到递归构建

递归 `ccgo build` 调用会继承:

* `--release`(来自父构建的 release 标志)
* `--arch <csv>`(小写化;与父构建的 `--arch` 对齐)
* `--build-as <variant>`(按上文的提示派生)

它**不会**继承:

* `--linkage` —— 依赖自己的 `[[dependencies]]` 由它自己的
  CCGO.toml 决定。消费者的逐依赖 linkage 提示只作用于该消费者
  与该依赖的关系,不作用于该依赖与其自身依赖的关系。

### 失败模式

如果递归构建失败,父 ccgo 会以依赖名加复现命令 bail:

```
recursive `ccgo build` for source-only dep 'stdcomm' (--build-as shared) failed
with exit code Some(1). The dep at .ccgo/deps/stdcomm could not be compiled —
check its CCGO.toml and try `ccgo build macos --build-as shared` inside that
directory to reproduce.
```

### 消费者 CMake 模板中的源码与二进制优先级

当一个 path-source 依赖同时发布 `src/` 和预构建的 `lib/<platform>/`
产物(后者是 bridge 在材化 spawn 成功后填入的)时,如果解析后的
linkage 为 `shared-external` 且 bridge 已在预期深度放置可用的共享
库,消费者的 CMake 模板现在会跳过内联源码编译。具体说:

* `consumer/.ccgo/deps/<name>/lib/<platform>/shared/<name>.xcframework/`
  存在(Apple)或 `lib/<platform>/shared/<arch>/lib<name>.so` 存在
  (Android/OHOS/Linux)→ 该依赖的 `src/``<consumer>-deps`
  聚合中被跳过。消费者的主共享目标改为通过
  DT_NEEDED / LC_LOAD_DYLIB 连接到 `libleaf.dylib` / `libleaf.so`* `static-embedded` linkage 仍按预期把依赖源码编入消费者归档
  (或链接到预构建的 `.a`)。

只有在没有共享产物且 linkage 提示要求或最终落到 `static-embedded`
时,才会回退到内联源码编译。这是 linkage 矩阵所期望的契约。

### 跨平台 bridge

bridge 步骤在 Windows 上以 NTFS 目录 junction(`mklink /J`)替代
Unix `symlink`。Junction 不需要管理员权限或开发者模式,且只能在
依赖所在的同一卷上工作 —— `cmake_build/` 目录树正好满足。它们
在 `FindCCGODependencies.cmake` 所做的 `EXISTS` / `file(GLOB ...)`
巡查中表现与 symlink 完全一致。

## 另请参阅

- [`dependency-resolution.zh.md`]dependency-resolution.zh.md —— ccgo 在
  做出 linkage 决策之前如何查找并拉取依赖。
- Rust 源码:`src/build/linkage.rs` —— 纯决策矩阵与文件系统扫描器。
  上文的决策表与单元测试一一对应。