# Dependency Linkage

Two orthogonal axes determine how a dependency ends up in your build product:

| Axis | CCGO.toml field | Values |
|---|---|---|
| What **you** produce | `[build].link_type` | `static`, `shared`, `both` |
| How a **dep** relates to you | `[[dependencies]].linkage` (with `[build].default_dep_linkage` as project-wide default) | `shared-external`, `static-embedded`, `static-external` |

## Linkage values

* **`shared-external`** — the dep stays as its own `.so`/`.dylib`/`.dll` and your binary records a runtime dependency (`DT_NEEDED` on ELF). Default for shared consumers when the dep can produce a shared artifact. Best for app-internal sharing across multiple consumers — eliminates the per-consumer copy of the dep that fat archives produce.
* **`static-embedded`** — the dep's `.a` is archived into your binary at link time. Your binary becomes self-contained; multiple consumers each carry their own copy of the dep code. Default fallback when the dep only ships a `.a`.
* **`static-external`** — the dep stays as a separate `.a` and your `.a` records the dependency without merging. Only valid for static consumers; the final executable's linker reconciles symbols at exe link time. The "thin chain" model.
* `shared-embedded` — **does not exist**. Trying to set it produces a parse error pointing at the two valid alternatives. A `.so` cannot be archived into another `.so`.

## Valid build-as × linkage combinations

The three linkage values have different validity rules depending on which
consumer type (`build-as`) is producing the artifact. Invalid combinations
are rejected at config parse time with a clear error message.

### General hint (`linkage` / `default_dep_linkage`)

When set on the `linkage` or `default_dep_linkage` field (which applies to
all consumer shapes), the allowed values depend on `build-as`:

| `build-as` | `shared-external` | `static-embedded` | `static-external` |
|---|---|---|---|
| `shared` | ✅ (default) | ✅ | ❌ |
| `static` | ✅ | ❌ | ✅ (default) |
| `both` | ✅ | ❌ | ❌ |

`build-as = both` is the intersection: a value must be valid for *both*
consumer shapes. `static-embedded` violates the static side; `static-external`
violates the shared side. Only `shared-external` is safe to specify
unconditionally across both shapes.

### Consumer-specific fields (`linkage_on_shared` / `linkage_on_static`)

Use these fields to set different hints per consumer shape without conflicting:

| Value | `linkage_on_shared` | `linkage_on_static` |
|---|---|---|
| `shared-external` | ✅ | ✅ |
| `static-embedded` | ✅ | ❌ |
| `static-external` | ❌ | ✅ |

`linkage_on_shared` never permits `static-external` — it would leave
unresolved symbols in the `.so`. `linkage_on_static` never permits
`static-embedded` — static archives cannot embed other archives.

## Decision matrix

| Consumer | Dep provides | Hint | Result |
|---|---|---|---|
| `shared` | only `.a` | (any) | `static-embedded` (forced; `shared-external` hint is an error) |
| `shared` | only `.so` | (any) | `shared-external` (forced; `static-embedded` hint is an error) |
| `shared` | both / source | absent | `shared-external` |
| `shared` | both / source | `shared-external` | `shared-external` |
| `shared` | both / source | `static-embedded` | `static-embedded` |
| `shared` | any | `static-external` | **error** — banned for shared consumers |
| `static` | only `.a` | absent / `static-external` | `static-external` |
| `static` | only `.a` | `shared-external` | **error** — dep has no `.so` |
| `static` | only `.so` | absent / `shared-external` | `shared-external` |
| `static` | only `.so` | `static-external` | **error** — dep has no `.a` |
| `static` | both / source | absent / `static-external` | `static-external` |
| `static` | both / source | `shared-external` | `shared-external` |
| `static` | any | `static-embedded` | **error** — static archives cannot embed |

## When to override

Most projects don't need to set `linkage` at all. The default
(shared-external for shared consumers when the dep ships a `.so`) avoids
bloat across multiple sibling consumers without surprises.

Set `linkage = "static-embedded"` per-dep when:
* The dep is small and you want self-containment (one fewer `.so` to ship).
* You're publishing an SDK to external developers via Maven/CocoaPods who
  won't have a way to install transitive deps independently.
* The dep only ships `.a` and you want to silence the auto-fallback notice
  in build logs.

## Build-time logging

ccgo emits a `STATUS` line per dep at CMake configure time:

```
[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, ...)` indicates the resolution came from a fallback rather than an
explicit `linkage` field; `(auto, no .so available)` specifically means the
dep doesn't ship a shared form, so embedding is the only choice.

## Example

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

[build]
link_type = "shared"                 # I produce a .so
default_dep_linkage = "shared-external"  # default for my deps

[[dependencies]]
name = "stdcomm"
version = "25.2.9519653"
# Default linkage applies → shared-external (libstdcomm.so stays separate)

[[dependencies]]
name = "tinyhelper"
version = "0.3.0"
linkage = "static-embedded"          # explicit: archive into liblogcomm.so
```

Reads as: "I'm a shared library. By default my deps are external. tinyhelper
is the exception — bake it into me." Three semantics, three settings, no
ambiguity.

## Source-only dependencies

When a dep ships only source code (the `.ccgo/deps/<name>/` dir contains
`src/` and `CCGO.toml` but no `lib/<platform>/` artifacts for the platform
being built), `ccgo build` automatically recurses: it spawns `ccgo build
<platform> --build-as <derived>` inside the dep before it resolves linkage,
then symlinks `dep/lib/<platform>/` to the new build output so the
consumer's `FindCCGODependencies.cmake` walks find the artifacts.

The `--build-as` value is derived from the consumer's resolved hint for
that dep:

| Consumer's hint for the dep | Recursive `--build-as` |
|---|---|
| `shared-external` (default) | `shared` |
| `static-embedded` / `static-external` | `static` |
| (no hint) | `both` |

A dep's own `[build].link_type` declaration **does not** dictate what gets
produced during a recursive materialize — the consumer's needs do. If you
set `--linkage stdcomm=static-embedded` on a build that has a source-only
`stdcomm`, you get a `.a`. If you flip to `--linkage stdcomm=shared-external`,
ccgo rebuilds `stdcomm` as a `.so` on the next build.

### Caching

The materialize step persists a per-platform, per-`--build-as` fingerprint at
`~/.ccgo/cache/<project-path>/<dep>/<platform>_<build_as>.fingerprint`
(outside the project tree, so no generated files appear in your working copy).
The fingerprint is a SHA-256 over (sorted source-tree mtimes + sizes + paths)
+ the `CCGO.toml` content + the requested `--build-as`. Subsequent builds
skip the recursive spawn when the fingerprint matches AND `lib/<platform>/`
still has artifacts. Splitting the sidecar by `build_as` prevents two
parallel builds of the same path-source dep from racing on a shared sidecar
when one wants `--build-as shared` and the other wants `--build-as static`.

Behavior matrix:

| State | Action |
|---|---|
| no `lib/<platform>/`, no fingerprint | Spawn build, write fingerprint |
| no `lib/<platform>/`, fingerprint exists | Spawn build (lib was deleted) |
| `lib/<platform>/` exists, fingerprint matches | Skip (cache hit) |
| `lib/<platform>/` exists, fingerprint mismatches | Spawn build (source changed) |
| `lib/<platform>/` exists, no fingerprint | Trust prebuilt, write fingerprint |

The "trust prebuilt" path matters for fixtures and projects that ship
hand-curated `lib/<platform>/` layouts (e.g. xcframework symlinks). Those
get a fingerprint stamped on the first invocation and then participate in
normal source-change invalidation from then on.

### What gets propagated to the recursive build

The recursive `ccgo build` call inherits:

* `--release` (from the parent's release flag)
* `--arch <csv>` (lowercased; matches the parent's `--arch`)
* `--build-as <variant>` (derived from hint as above)

It does **not** inherit:

* `--linkage` — the dep's own `[[dependencies]]` are decided by its own
  CCGO.toml. A consumer's per-dep linkage hint applies only to that
  consumer's relationship with the dep, not to the dep's relationship with
  its own deps.

### Failure mode

If the recursive build fails, the parent ccgo bails with the dep name and
a reproduction command:

```
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.
```

### Source vs binary precedence in the consumer's CMake template

When a path-source dep ships both `src/` and pre-built `lib/<platform>/`
artifacts (the latter being what the bridge populates after a successful
materialize spawn), the consumer's CMake template now skips the inline
source compilation if the resolved linkage is `shared-external` and the
bridge has placed a usable shared library at the expected depth. Concretely:

* `consumer/.ccgo/deps/<name>/lib/<platform>/shared/<name>.xcframework/`
  exists (Apple) or `lib/<platform>/shared/<arch>/lib<name>.so` exists
  (Android/OHOS/Linux) → the dep's `src/` is skipped in the
  `<consumer>-deps` aggregate. The consumer's main shared target picks up
  `libleaf.dylib` / `libleaf.so` via DT_NEEDED / LC_LOAD_DYLIB instead.
* `static-embedded` linkage continues to compile the dep's source into the
  consumer's archive (or link against the pre-built `.a`), as expected.

Falling back to inline source compilation only happens when no shared
artifacts are present and the linkage hint either asks for or ends up at
`static-embedded`. This is the contract the linkage matrix expects.

### Cross-platform bridge

The bridge step uses NTFS directory junctions (`mklink /J`) on Windows in
place of Unix `symlink`. Junctions don't require admin or Developer Mode
and work on the same volume as the dep — which the `cmake_build/` tree
always is. They behave identically to symlinks for the `EXISTS` /
`file(GLOB ...)` walks that `FindCCGODependencies.cmake` performs.

## See also

- [`dependency-resolution.md`](dependency-resolution.md) — how ccgo finds and
  fetches deps before linkage decisions get made.
- The Rust source: `src/build/linkage.rs` — pure decision matrix and
  filesystem scanner. The decision table above is mirrored exactly in the
  unit tests.
