# Building
```bash
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
```
CMake downloads and unpacks the pinned CEF binary distribution on first configure
(~125 MB compressed, ~350 MB on disk) into `third_party/cef/current`, verifying
its SHA-1. Already have one? `-DCEF_ROOT=/path/to/cef_binary_...`.
The version is pinned in `cmake/FetchCEF.cmake` rather than resolved from
upstream at configure time, because a build that silently follows stable is a
build that breaks on someone else's Tuesday.
## Options
| Option | Default | Notes |
|---|---|---|
| `WEBLINKED_WITH_NDI` | ON | Headers vendored; nothing to install |
| `WEBLINKED_WITH_OMT` | ON | Header vendored; nothing to install |
| `WEBLINKED_WITH_DECKLINK` | ON | Needs `-DDECKLINK_SDK_DIR` |
| `WEBLINKED_WITH_AJA` | OFF | Needs `-DNTV2_DIR` and `-DNTV2_BUILD_DIR` |
| `WEBLINKED_WITH_SCREEN` | ON | Fullscreen GPU output. No SDK on macOS or Windows; on Linux see below |
| `WEBLINKED_BUILD_TESTS` | ON | Links `weblinked_core` only |
Configure output lists which backends were found. A missing SDK disables its
backend with a message; it is never a hard error, because most people building
this do not have all four.
### NDI
Nothing to install. The headers are vendored in `third_party/ndi` under the
per-file MIT grant each one carries, so this backend always builds. An installed
SDK still wins if you have one — found automatically at
`/Library/NDI SDK for Apple`, `C:/Program Files/NDI/NDI 6 SDK`, or via
`-DNDI_SDK_DIR=` — which is how you build against a newer SDK than the vendored
set.
Only headers are ever needed at build time; the library is resolved at run time
with `dlopen`. Without a runtime the NDI output reports itself unavailable and
names the download, and the rest of the app is unaffected. See
[06-ndi-distribution.md](06-ndi-distribution.md).
### OMT
`third_party/omt/libomt.h` is vendored (MIT), so this backend builds with no
setup. At *run* time you need `libomt` and `libvmx` beside the binary, from the
[OMT binary releases](https://github.com/openmediatransport/libomtnet/releases).
Published for Windows x64/arm64 and macOS 15+ arm64. **There is no Linux
release** — a Linux deployment must build `libomtnet` from source.
### DeckLink
The SDK is a free but licence-gated download from Blackmagic and is not vendored.
```bash
cmake -S . -B build -DDECKLINK_SDK_DIR="/path/to/Blackmagic DeckLink SDK 12.9"
```
Accepted layouts: `
/Mac/include/`, `/Linux/include/`,
`/Win/include/`, a bare directory containing `DeckLinkAPI.h`, or a flattened
`/Mac/` (as redistributed inside Unreal's BlackmagicMedia plugin, which is a
convenient way to build the backend without the SDK archive to hand).
On macOS and Linux the SDK's `DeckLinkAPIDispatch.cpp` is compiled in; it dlopens
the installed driver, so the binary still runs on a machine with no Desktop Video
and simply finds no devices.
### AJA
```bash
git clone https://github.com/aja-video/libajantv2
cmake -S libajantv2 -B ajantv2-build && cmake --build ajantv2-build
cmake -S . -B build -DWEBLINKED_WITH_AJA=ON \
-DNTV2_DIR=$PWD/libajantv2 -DNTV2_BUILD_DIR=$PWD/ajantv2-build
```
`NTV2_BUILD_DIR` matters: `ntv2version.h` is generated by the SDK's own CMake run
and lives in the build tree, not the source tree. Requires libajantv2 18.x.
---
## macOS
The output is `build/Release/WebLinked.app`, about 310 MB, most of it Chromium.
### The bundle is six executables
CEF on macOS needs the browser process plus five helper `.app` bundles inside
`Contents/Frameworks`, named `WebLinked Helper[ (variant)].app`. CEF finds them
*by name*; get one wrong and the app starts, shows a window, and dies the moment
it needs a renderer. `browser_subprocess_path` is deliberately not set on macOS.
### Two signing traps
**Every build is ad-hoc signed, automatically.** An unsigned `.app` that bundles
further executables does not become trusted when the user approves the outer app
— the nested helpers are SIGKILLed with no dialog and no log entry, which reads
exactly like a CEF initialisation bug.
**Ad-hoc signatures and hardened runtime do not mix.** With `--options runtime`
macOS enforces library validation, requiring every loaded library to share the
process's Team ID. An ad-hoc signature has none, so the app fails at launch with:
```
code signature ... not valid for use in process:
mapping process and mapped file (non-platform) have different Team IDs
```
which looks like a broken bundle rather than a signing policy. Ad-hoc builds are
therefore signed *without* hardened runtime. For distribution:
```bash
cmake -S . -B build -DWEBLINKED_CODESIGN_IDENTITY="Developer ID Application: You (TEAMID)"
```
That switches on hardened runtime plus `src/app/WebLinked.entitlements`, which
carries `disable-library-validation` (the framework, libndi and libomt are all
loaded at run time) and the JIT entitlements V8 needs. Notarise afterwards.
### Universal binaries
The CEF binary distribution is per-architecture, so a universal build needs two
distributions built separately and `lipo`d together. `CMAKE_OSX_ARCHITECTURES` is
set before `project()` — setting it afterwards silently produces a single-arch
binary that still claims to be universal, so verify the result with `lipo -info`
rather than trusting the build log.
---
## Windows and Linux
**Both build and run** — CI builds every release for them, and both were run on
real systems in August 2026 (§31 and §32 of
[04-verification.md](04-verification.md)). A from-source Linux build on a clean
Ubuntu 24.04 needed only the packages listed below and took one `cmake` and one
`ninja` with no source changes. Still worth knowing:
- **The DeckLink SDK and libajantv2 are the only backends a local build adds.**
NDI and OMT have vendored headers and are compiled in everywhere, including CI.
- The Windows DeckLink path uses COM (`CoCreateInstance`) rather than the
dispatch shim, and that code has still never seen a card.
- On Windows and Linux one binary is re-executed for every subprocess type, so
`CefExecuteProcess` runs first in `main()` — there are no helper bundles.
- Linux needs the usual Chromium runtime libraries (`libnss3`, `libgbm`,
`libatk`, X11 or Wayland); CEF's `FIND_LINUX_LIBRARIES` handles the link side.
- **The screen output needs X11 and EGL headers on Linux.** Without them it
disables itself at configure time with a message, the same way a missing NDI
or DeckLink SDK does — so a build that quietly has no `screen` backend is
almost always a missing package:
```bash
sudo apt-get install -y libx11-dev libegl1-mesa-dev libgles2-mesa-dev
```
The Linux backend is X11 only. A Wayland session with XWayland gets a window;
one without does not, and `open()` says so. It also indexes **X screens**, not
RandR heads, so a normal multi-monitor Linux desktop presents as one screen —
which makes `--screen=1` there not yet useful. The Linux backend has now been
run and renders correctly on software GL; the Windows one still has not.
- **The KMS output needs libdrm's headers**, and nothing else — no X, no EGL. It
is Linux-only and disables itself at configure time without them:
```bash
sudo apt-get install -y libdrm-dev
```
For a board with no X server, build with `-DWEBLINKED_WITH_SCREEN=OFF` as
well, and the binary no longer links X11, EGL or GLES at all (libcef.so still
links X11 itself; run it with `--ozone-platform=headless`).
## In a container
`docker/Dockerfile` builds the headless Linux image from this checkout — the
repository root is the build context:
```bash
docker build -f docker/Dockerfile -t weblinked .
```
It takes CEF's version and linux64 sha1 from `cmake/FetchCEF.cmake`, builds with
the screen, shared, DeckLink, AJA, KMS and stream outputs off, and runs the unit
suite as a build step. Why each is off, and how to run it, is in
[docker/README.md](../docker/README.md).
## Tests
```bash
./build/tests/weblinked_tests # all
./build/tests/weblinked_tests uyvy # substring filter
```
They link `weblinked_core` only — no CEF — so they build and run in seconds.
## The verification tool
`tools/ndi_probe.cpp` is an independent NDI receiver used to check what actually
reaches the network. It is not part of the application and is not built by CMake:
```bash
clang++ -std=c++20 -I"/Library/NDI SDK for Apple/include" tools/ndi_probe.cpp \
"/Library/NDI SDK for Apple/lib/macOS/libndi.dylib" \
-Wl,-rpath,"/Library/NDI SDK for Apple/lib/macOS" -o ndi_probe
```
`tools/mdns_probe.cpp` is the equivalent for the mDNS advertisement. It browses
for `_weblinked._tcp`, decodes the TXT record with the *platform's* parser
rather than ours, and then connects to the address each record publishes —
which is the half `dns-sd -B` and `avahi-browse` leave out, and the half that
catches an advertisement pointing somewhere unreachable.
```bash
# macOS: dns_sd is in libSystem, so there is nothing to link
clang++ -std=c++20 tools/mdns_probe.cpp -o mdns_probe
./mdns_probe --timeout 5
```
See [04-verification.md](04-verification.md) for what it proves.
## Linux: discovery needs avahi's headers
The mDNS backend is built against `avahi-client`'s headers so the compiler
checks every signature, but opens the library at run time like libndi — so the
binary still runs on a machine without avahi, it just cannot advertise.
```bash
sudo apt-get install -y libavahi-client-dev
```
Without them CMake prints `mDNS: no avahi headers` and compiles a stub. That is
not a build failure, which is exactly why it is worth noticing: the build
succeeds and the release simply cannot be discovered.