Files
reasampler/docs/product/audit-notes/lambda-w1-t1-build-toolchain.md
T
daniel f57bf4fb6d docs: fix audit citations — bad line ranges, misattributed evidence, overclaimed severity
Corrects a false unguarded-include claim, re-scopes hot-path evidence to what's actually linked, replaces a nonexistent Fork-A collision scenario, fixes several miscounted TUs/libraries, re-grades two Fork-B items from Blocker to Major/non-blocker, and adds missing owners/done-conditions.
2026-08-02 06:39:31 -04:00

814 lines
57 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Λ-W1 Track 1 — Linux build/toolchain audit (build-system lens)
Static analysis of the build surface only — the 23 `CMakeLists.txt` files, `cmake/`,
`src/resource.rc`/`src/resource.h` + the SWELL resgen pipeline, the three vendored submodules
as *build inputs*, the `ctest` harness, toolchain-level flags, and the packaging story in
`docs/product/versioning-and-release.md`. 2026-08-02, branch `pl-w1-t1-build-toolchain-audit`
(cut from `dev` at 6e937b9).
**Source portability is explicitly NOT audited here** — no Win32/GDI/LICE API usage, path
handling, threading, drag/drop, dialog code, fonts, or instrument runtime code. A parallel
track (T2) owns that; what I noticed in passing is listed under "Referred to T2" at the end
and nowhere else.
**Verification posture.** I am on Windows and cannot build or run anything on Linux. Every
claim below is grounded in a file I opened in this worktree — the tree's own CMake, or a
vendored header/source under `vendor/` — not asserted from memory of similar SDKs. Where a
claim depends on runtime behaviour I cannot observe (does REAPER's Linux build call
`SWELL_dllMain`? does GCC accept this TU?), it is marked `[verify — Linux]` with the exact
check, and it is **not** stated as fact. A subsequent citation pass caught several
line-number and attribution errors in this draft (corrected in place, disclosed at the point
each occurs) — grounded-in-a-file is a floor, not a guarantee against a mis-copied line range.
To get the vendored citations I ran `git submodule update --init` for all three submodules
inside this worktree; nothing in the tracked tree was modified.
Severity vocabulary, per the dispatch: **Blocker** = the Linux build cannot produce a loadable
artifact; **Major** = it builds but is degraded or unshippable; **Minor** = ergonomics,
warnings, CI polish. Effort bands are S (< half a day), M (13 days), L (a week+), and each
names the reason for the band rather than asserting a number.
---
## 1. Baseline — what the build does today, per platform
**Two loadable modules, 84 pure libraries declared under `src/core/`, 91 CTest targets,
three vendored submodules.** (84 = the `reasampler_pure_library` + bare `add_library` count,
grep-verified library-by-library across the 18 `src/core/**` CMake files that declare any —
`src/core/CMakeLists.txt` and `src/core/instrument/CMakeLists.txt` declare none, only
`add_subdirectory` calls (plus one test in the latter); there are 20 CMake files under
`src/core/**` total, matching the 19 figure in §1d below, which counts *test* declarations
across the whole tree, not `src/core/**` alone. 46 of the 84 are on the extension's link
line.)
- Root `CMakeLists.txt` is 95 lines and declares **no targets at all**: it owns the version
string (`:24`), the channel fork (`:4054`), the `configure_file` for
`version_generated.h` (`:5861`), the four vendor path variables (`:6568`), the
four-TU `LICE_SRC` list (`:7883`), `enable_testing()` (`:90`), and three
`add_subdirectory` calls (`:9294`).
- Language/toolchain settings are exactly three lines: `CMAKE_CXX_STANDARD 17`,
`CMAKE_CXX_STANDARD_REQUIRED ON`, `CMAKE_POSITION_INDEPENDENT_CODE ON`
(`CMakeLists.txt:2830`). There is **no** `CMAKE_BUILD_TYPE`, no `CMAKE_CXX_FLAGS`, no
`CMAKE_CXX_EXTENSIONS`, no IPO/LTO setting, and no `target_compile_options` anywhere in
the tree — I grepped all 23 CMake files for `compile_options`, `find_package`,
`pkg_check`, `link_directories`, `target_link_options`, and `install(` and got zero hits
outside the two files named below.
- **Platform conditionals exist in exactly two files.** Grepping all 23 `CMakeLists.txt`
for `WIN32|APPLE|UNIX|LINUX|MSVC|GNU|Clang|CMAKE_SYSTEM` returns hits only in
`src/app/CMakeLists.txt` (`:71` `if(WIN32)`, `:76` `elseif(APPLE)`, `:88` `else()`) and
`src/shell/instrument/CMakeLists.txt` (`:9` `if(WIN32 AND EXISTS ...)`). Every other
CMake file is platform-neutral by construction: they call the two helpers in
`cmake/reasampler_targets.cmake` (`reasampler_pure_library` at `:815`,
`reasampler_test` at `:1924`) and nothing else.
### 1a. The REAPER extension (`reaper_reasampler`)
Declared as a `MODULE` library over 39 listed TUs (38 shell TUs + `app/main.cpp` itself)
plus `${LICE_SRC}` (`src/app/CMakeLists.txt:851`), linking 46 pure static libraries (`:52`) with
`${SDK_INC} ${WDL_INC}` on the include path (`:56`). Output naming is
`PREFIX ""` + channel-derived `OUTPUT_NAME`, pinned to the top of the build tree by
`LIBRARY_OUTPUT_DIRECTORY`/`ARCHIVE_OUTPUT_DIRECTORY` (`:6569`).
The three-way platform block (`:7198`) is:
| Branch | State | What it does |
|---|---|---|
| `if(WIN32)` `:7174` | **Live and shipped** | adds `src/resource.rc` for the RC compiler |
| `elseif(APPLE)` `:7686` | **Partly aspirational** | adds `swell-modstub.mm`, defines `SWELL_PROVIDED_BY_APP`, links `-framework AppKit`, forces `.dylib`. The resgen `target_sources` line at `:86` is **commented out**. |
| `else()` (Linux) `:8897` | **Partly aspirational** | adds `swell-modstub-generic.cpp`, defines `SWELL_PROVIDED_BY_APP`, forces `.so`. The resgen `target_sources` line at `:97` is **commented out**. |
So on Linux the build system today produces `reaper_reasampler.so` with the SWELL modstub
compiled in and **no dialog resource of any kind**. `README.md:6768` states this is
"expected, not a bug" — the tree is honest about it.
**Open question, macOS-only, unresolved by this doc.** The APPLE branch adds
`${SWELL}/swell-modstub.mm` (`:79`), an Objective-C++ TU, but `CMakeLists.txt:26` is
`project(reaper_reasampler VERSION 1.4.0 LANGUAGES CXX)` — OBJCXX is never enabled anywhere
in the tree. Whether CMake resolves a `.mm` source against a plain CXX-only project (some
Apple-toolchain configurations infer Objective-C++ from the extension regardless; others do
not) is unverified from this tree and untestable without a macOS box. `[verify — macOS]`.
Three build-input facts I verified in `vendor/` and that hold in the extension's favour:
- `vendor/WDL/WDL/swell/swell-modstub-generic.cpp` is entirely inside
`#ifdef SWELL_PROVIDED_BY_APP` (`:21`, closed at `:150`). Since
`SWELL_LOAD_SWELL_DYLIB` is not defined anywhere in this build, the `#else` branch at
`:133` applies: the file's whole contribution is the three
`SWELL_curmodule_*resource_head` globals (`:3133`), the API function-pointer table
built from `swell.h` (`:3749`), and the `SWELL_dllMain` export (`:135`) that resolves
every SWELL symbol from a host-supplied `GetFunc`. That is the correct stub for a
REAPER extension, and the comment at `src/app/CMakeLists.txt:90` ("With
SWELL_PROVIDED_BY_APP you can drop pkg-config / -lX11 entirely") is **correct**: no SWELL
implementation TU, no X11, no GDK is compiled or linked into the extension.
- The four-TU LICE slice is self-contained on Linux. `lice.cpp` reaches SWELL only through
`SWELL_DeleteGfxContext` (`:153`, `:218`), `SWELL_GetCtxFrameBuffer` (`:182`, `:226`) and
`SWELL_CreateMemContext` (`:224`); `lice_textnew.cpp` reaches it through
`SWELL_PushClipRegion`/`SWELL_SetClipRegion` (`:10271028`) and `SWELL_PopClipRegion`
(`:1048`). All six are entries in SWELL's API table
(`vendor/WDL/WDL/swell/swell-functions.h:857`, `:863`, `:877`, `:885887`), so the
modstub resolves them from the host. No fifth LICE TU is implied: the `_LICE_RGB2HSV_invtab`
table lives inside `lice.cpp` itself (`:3040`), and the one non-Win32-only helper
`utf8makechar` is a file-static defined at `lice_textnew.cpp:26` and used at `:241242`.
This is a link-success inference, not a from-first-principles proof — the actual proof is
V1, a successful build. One loose end: SWELL's own `Makefile:106107` lists
`lice_colorspace.o` among the LICE objects *its* build needs — a fifth TU we don't compile.
It stays unreconciled but harmless: `lice_colorspace.cpp` defines the real `LICE_RGB2HSV`
function (declared `lice.h:577`, distinct from the `_invtab` table above), and grepping our
four LICE_SRC TUs and all of `src/` for a call to it returns nothing — SWELL needs the TU
for its own generic-GDK backend, which our four-TU slice never invokes into.
- `CMAKE_POSITION_INDEPENDENT_CODE ON` (`CMakeLists.txt:30`) is already set. This is the
single most commonly missed setting for a `MODULE` target fed by `STATIC` libraries on
Linux, and it is correct today.
### 1b. The VST3 instrument (`reasampler_vst`)
Hard-gated to Windows: `if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp")`
(`src/shell/instrument/CMakeLists.txt:9`). The gate is a conjunction — even with the
`vendor/vst3sdk` slice fully initialised, a Linux configure silently omits the target.
`README.md:3335` states this explicitly. Two module-level `CLAUDE.md` files record the
underlying product decision (D5) as *settled*, with "a mac/Linux build" named as a
non-goal: `src/core/instrument/CLAUDE.md` (Non-goals/guardrails) and
`src/shell/instrument/CLAUDE.md` (same). `src/shell/panel/CLAUDE.md` carries a third
instance ("Windows-only (D5) — font/GDI/HFONT choices assume Windows").
Inside the gate: a 20-source `vst3_sdk` static library (`:1638`, grep-counted; lines 28/29/37
are comments) pinned to SDK tag
`v3.7.9_build_61` (confirmed by `git describe --tags` in `vendor/vst3sdk`), and the
`reasampler_vst` MODULE (`:4481`) which additionally compiles `dllmain.cpp` and
`moduleinit.cpp` directly (`:7879`, with the linker-stripping rationale at `:7577`) plus
`${LICE_SRC}` (`:80`). Output is a single **file** named `<name>.vst3` (`:106111`).
### 1c. Resources
`src/resource.rc` declares exactly one dialog — `IDD_BANK_PANEL`, a bare `WS_CHILD`
`DIALOG` with an empty `BEGIN`/`END` body and no controls (`:1822`). `src/resource.h`
defines the single id `IDD_BANK_PANEL 1000` (`:8`). It is consumed by
`CreateDialogParam(g_hInst, MAKEINTRESOURCE(IDD_BANK_PANEL), …)` at
`src/shell/panel/panel_window.cpp:135`.
### 1d. Test harness
91 test declarations across 19 `CMakeLists.txt` files (grep count), matching 91 files in
`tests/`. 90 go through `reasampler_test` (`cmake/reasampler_targets.cmake:1924`, which is
`add_executable` + `target_link_libraries` + `add_test`); one is longhand —
`app_version_padding_tests` (`src/core/version/CMakeLists.txt:3742`), which recompiles
`app_version.cpp` against a second `configure_file` output so the include-dir substitution
works. Neither shape carries anything platform-specific.
Only two test files mention a platform at all: `tests/test_capture_paths.cpp` (five
`#ifdef _WIN32` blocks at `:19`, `:41`, `:87`, `:152`, `:205`, each with a POSIX `#else`
branch already written — e.g. `:158168` asserts `resolveBankFile("/home/d/proj", …)`), and
`tests/test_pitch_shift.cpp:3`, where the string is inside a comment explaining why the
target links nothing that would drag `<windows.h>` in. No test includes `reaper_plugin.h`,
`windows.h`, or any SWELL header. **The test harness is the healthiest part of this
surface**; see §3 for the one generator-shape caveat.
### 1e. Packaging / release, as already committed
`docs/product/versioning-and-release.md` already commits to Linux as a first-class target:
- `:361363` — "**Artifact per platform.** The binary is `reaper_*.dll|.dylib|.so`; the
macOS/Linux builds need the SWELL resgen step (CLAUDE.md §SWELL dialog resources) baked
into the pipeline. Three platform artifacts per channel per release."
- `:307310` — two named artifacts (stable + beta) per platform, "three platform artifacts
each", explicitly handed off to dev-ops.
- `:364366` — "Install is copy-in + restart… there is no server-side rollout."
What that document does **not** contain: any mention of code signing, macOS notarization, a
CI runner, or a Linux install path. I grepped it for `sign|notariz|CI|platform|linux` — one
hit is not cited above (`:393`, "artifacts (stable + beta) per platform", the same dev-ops
handoff as `:307310`); the rest are the lines above. The substantive conclusion — no
signing/notarization/CI/Linux-install-path mention anywhere — holds regardless. There is no
CI configuration anywhere in the repo (`ls -a` on
the root shows `.git`, `.gitignore`, `.gitmodules`, `CLAUDE.md`, `CMakeLists.txt`, `LICENSE`,
`README.md`, `cmake`, `docs`, `src`, `tests`, `vendor` — no `.github`, no `.gitlab-ci.yml`),
and no `install()` rule in any of the 23 CMake files. So "the pipeline" is entirely notional
today, on every platform.
---
## 2. Findings
### Λ-01 — the Linux/macOS dialog-resource path is broken in three independent ways, and the fix may not need resgen at all
**Severity: Major. Effort: S (mechanical) to M (if the resgen route is taken and automated).**
**Citations.** `src/app/CMakeLists.txt:9497` (the Linux comment block) and `:8386` (the
identical macOS one); `vendor/WDL/WDL/swell/swell_resgen.php:313314`;
`vendor/WDL/WDL/swell/swell-dlggen.h:22`; `vendor/WDL/WDL/swell/sample_project/myapp.cpp:192193`;
`vendor/WDL/WDL/swell/swell-dlggen.h:214218`; `vendor/WDL/WDL/swell/swell-modstub-generic.cpp:3133`;
`vendor/WDL/WDL/swell/swell-functions.h:612619`; `src/resource.rc:1822`;
`src/shell/panel/panel_window.cpp:135`.
**What breaks.** Three defects stack, and each alone would sink the step:
1. **The named script does not exist.** Both comment blocks say
`php ${WDL_INC}/swell/mac_resgen.php src/resource.rc` (`:85` and `:96`). Listing
`vendor/WDL/WDL/swell/` shows `swell_resgen.php`, `swell_resgen.pl`, and
`swell_resgen.sh` — there is no `mac_resgen.php`. `CLAUDE.md` and `README.md:120` both
name the correct script; only the CMake comments are wrong, which is exactly the file an
implementer will be reading when they do the work.
2. **The named output file is not what the generator writes.** Both commented
`target_sources` lines (`:86`, `:97`) reference
`${REASAMPLER_SRC_DIR}/resource.rc_mac_dlg.h`. `swell_resgen.php:313314` composes its
output names as `$srcfn . "_mac_menu"` and `$srcfn . "_mac_dlg"` — so the actual outputs
are `src/resource.rc_mac_dlg` and `src/resource.rc_mac_menu`, with **no** `.h` suffix.
3. **Even the correct filename cannot be a `target_sources` entry.** `swell-dlggen.h:22`
states the contract outright: "Use swell_resgen.pl or swell_resgen.php on the .rc file,
which will produce .rc_mac_dlg, **include that**." The generated body is a run of
`SWELL_DEFINE_DIALOG_RESOURCE_BEGIN(...)` macro invocations
(`swell_resgen.php:117129`) that only expand after `swell-dlggen.h` has been included —
`swell-dlggen.h:214218` shows the macro emitting a static `SWELL_DialogRegHelper` that
registers into `SWELL_curmodule_dialogresource_head`. The canonical consumption pattern
is the SDK's own sample: `sample_project/myapp.cpp:192193` does
`#include "../WDL/swell/swell-dlggen.h"` then `#include "res.rc_mac_dlg"`. A CMake
`target_sources` entry for an extensionless file would not even get a language assigned.
**The runtime consequence, and why this is Major rather than Blocker.** The `.so` still
builds and still loads: the extension's whole action family, capture pipeline, and persist
layer are untouched. What fails is the dock panel. `swell-functions.h:615` defines
`CreateDialog(hinst,resid,par,dlgproc)` as
`SWELL_CreateDialog(SWELL_curmodule_dialogresource_head,(resid),par,dlgproc,0)`.
`swell-modstub-generic.cpp:32` *defines* that head but nothing ever populates it, because
the resgen'd registration TU does not exist — so it stays null and
`panel_window.cpp:135`'s `CreateDialogParam` gets a null resource list. The panel is the
product's primary surface, so a Linux build without it is not shippable; it is not a
Blocker only because the binary loads and the actions work.
**The route worth considering first.** `swell-functions.h:606608` documents an escape
hatch: *"If you are using CreateDialog() and creating a child window, you can use a resource
ID of 0, which creates an opaque child window. Instead of passing a DLGPROC, you should pass
a (WNDPROC) routine that returns LRESULT (and cast it to DLGPROC)."* The header comment is
not just documentation — the implementation confirms both halves of it:
`swell-dlg-generic.cpp:291292` is `SWELL_DialogResourceIndex *p=resById(reshead,resid); if
(!p&&resid) return 0;`, and `resById` (`:3443`) walks from a (here, null) `reshead` and
returns 0 on a miss. That confirms the graceful-failure claim behind "Major rather than
Blocker" above (a null `reshead` plus a non-zero `resid` returns 0, not a crash — matching
`panel_window.cpp:137`'s `if (!g_panel.hwnd) return;`), **and** that `resid == 0` skips the
lookup entirely (`&&resid` short-circuits), which is the escape hatch this paragraph is about.
`src/resource.rc:1822`
shows `IDD_BANK_PANEL` is precisely that: a `WS_CHILD` dialog with an empty body and zero
controls, whose comment at `:810` says "the bank_panel shell owns every pixel and draws the
sample grid with LICE in `WM_PAINT`". If the panel is created with resource id 0 on
non-Windows, **the entire resgen pipeline — the PHP dependency (`README.md:54`), the
generated-file-in-source-tree problem, and the per-platform regeneration story — disappears
from the Linux and macOS builds.** The cost is a source change in `panel_window.cpp` and the
DLGPROC→WNDPROC return-type contract; that half is T2's call, not mine, and I am naming the
option rather than choosing it. This is a shared macOS/Linux fix either way.
**If the resgen route is taken instead**, the build work is: a `find_program(PHP …)`, an
`add_custom_command` generating into `${CMAKE_CURRENT_BINARY_DIR}` (not the source tree —
`.gitignore` has no pattern matching `resource.rc_mac_dlg`, so today's manual step would
leave an untracked generated file sitting in `src/`), a new one-line shim TU that includes
`swell-dlggen.h` then the generated file, and that shim added to `target_sources`. That is
the M-effort half of the band.
---
### Λ-02 — `--config Release` is a silent no-op on Linux, so the documented ship command produces an unoptimized binary
**Severity: Major. Effort: S — one `if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)` block plus a doc correction.**
**Citations.** `CMakeLists.txt:2830` (the complete list of language settings — I read the
whole 95-line file and there is no `CMAKE_BUILD_TYPE` anywhere); `README.md:106109`;
`CLAUDE.md` §"Build and test" and §"Install / reload".
**What breaks.** Both docs prescribe `cmake --build build --config Release` as the
optimize-and-ship incantation, and `README.md:130131` tells the user to copy from
`build/Release/`. `--config` is a multi-config-generator flag; on Ninja or Unix Makefiles it
is accepted and ignored. Because no `CMAKE_BUILD_TYPE` default is set, the resulting build
passes **no `-O` flag at all** to GCC/Clang — not `-O0` explicitly, just the compiler's
unoptimized default. The failure is silent in both directions: the command succeeds, and the
artifact lands at `build/reaper_reasampler.so` — a path the docs never state for a
single-config generator (a gap, not an error; see Λ-08).
This matters more here than the usual "forgot to set the build type" — but only for `peaks`.
`CLAUDE.md`'s §"Build and test" (not §"Performance guardrails") states, at `CLAUDE.md:54`,
that the header-inline hot paths "presume an **optimizing** build", with no LTO configured to
recover it if they don't. `peaks`' `computeEnvelope`/`lastFrameAboveThreshold` is one of
those paths and IS linked into the extension (`src/app/CMakeLists.txt:52`), so the finding
holds for Fork A as stated. It does **not** extend to `Voice::advanceFrame` or the three
`envelopes.h` evaluators (named at `src/core/instrument/CLAUDE.md:342`, not in root
`CLAUDE.md`) — those belong to `sampler_core`, which `src/app/CMakeLists.txt:5355` states is
deliberately NOT linked into the extension and which only enters a build at all inside the
Windows-gated `reasampler_vst` target (§1b). On Fork A that code never compiles into
`reaper_reasampler` in the first place; the unoptimized-build risk to it is a Fork B concern,
not this extension-only one. A Linux user following the README's `--config Release` would
ship an extension binary whose `peaks` hot path did not inline.
**Done when:** `cmake --build build` on Linux (Ninja/Make, no `--config`) produces a build
whose compile commands (`compile_commands.json` or verbose build log) show an explicit `-O`
flag, and the README/CLAUDE.md ship instruction names the single-config-generator path.
**Sequencing note.** This is not strictly Linux-specific — Ninja on Windows has the same
shape — but it becomes load-bearing the moment Linux is a shipped platform, because Makefiles
and Ninja are the default there.
---
### Λ-03 — no symbol-visibility control, and two modules in one process both export a full copy of LICE
**Severity: Major. Effort: S — one `CXX_VISIBILITY_PRESET hidden` + `VISIBILITY_INLINES_HIDDEN` pair, plus a `[verify — Linux]` load test.**
**Citations.** `CMakeLists.txt:7883` (`LICE_SRC`); `src/app/CMakeLists.txt:35` and
`src/shell/instrument/CMakeLists.txt:80` (the same four LICE TUs compiled into *both*
modules — the rationale for the duplication is at `CMakeLists.txt:7577`);
`vendor/reaper-sdk/sdk/reaper_plugin.h:52`; `src/app/main.cpp:251`;
`vendor/vst3sdk/pluginterfaces/base/fplatform.h:164`;
`vendor/WDL/WDL/swell/Makefile:20`.
**What breaks.** Nothing in the build sets a visibility preset, so GCC/Clang default to
`-fvisibility=default` and every non-static symbol in both modules — all of LICE, all the
WDL helpers, every pure library's exported functions, and the whole C++ standard-library
instantiation set — lands in the module's dynamic symbol table. On Windows this is a
non-issue: each DLL has its own symbol table and per-module resolution. `[verify — Linux]`
on ELF, symbols from `dlopen`'d objects CAN participate in global interposition — but whether
they actually do here depends on REAPER's own `dlopen` flags (`RTLD_GLOBAL` vs `RTLD_LOCAL`)
and whether REAPER's executable is linked `--export-dynamic`, neither of which this tree can
answer. The scenario this doc originally named — a hypothetical `reasampler_9000.so` — does
not exist on Fork A: per Λ-10 and `src/shell/instrument/CMakeLists.txt:9`, the VST3 target is
gated `WIN32 AND EXISTS ...` and is never built on Linux unless D1 selects Fork B. The
Fork-A-real risk is different: any **second REAPER extension** loaded in the same REAPER
process (a third-party `.so` in `UserPlugins/`) that links its own copy of LICE/WDL and also
defaults to `-fvisibility=default` is a candidate to interpose with `reaper_reasampler.so`'s
`LICE_FillRect` and the rest of the exported LICE/WDL symbol set. That is a class of bug that
presents as "one extension's LICE draws with another extension's LICE build" and is invisible
on Windows, where each DLL keeps its own symbol table.
**Why hiding is safe here.** The two symbols that genuinely must be exported already say so
explicitly: `reaper_plugin.h:52` defines
`REAPER_PLUGIN_DLL_EXPORT __attribute__((visibility("default")))` on non-Windows, and
`main.cpp:251` uses it (`extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(...)`).
On the VST3 side, `fplatform.h:164` defines `SMTG_EXPORT_SYMBOL` the same way. SWELL's own
build already uses `-fvisibility=hidden` (`Makefile:20`), which is the precedent. The one
thing to confirm on a Linux box is that `swell-modstub-generic.cpp:135`'s `SWELL_dllMain`
survives — it carries its own `__attribute__ ((visibility ("default")))` at that line, so it
should, but the modstub is compiled by *our* build, not SWELL's.
*Shared macOS/Linux — the visibility default and the LICE-TU duplication (`CMakeLists.txt:7577`)
apply the same way under the APPLE branch's Mach-O two-level namespace, though the concrete
Fork-A-real interposition mechanism above is ELF-specific; macOS's own dynamic-linker symbol
resolution is a separate `[verify]` this doc does not attempt.*
---
### Λ-04 — nothing links a thread library, and `<pthread.h>` is in the extension's include graph
**Severity: Major (VST3 target) / Minor (extension). Effort: S — `find_package(Threads)` + `Threads::Threads`.**
**Citations.** `src/shell/panel/panel_state.h:2024`;
`vendor/vst3sdk/base/thread/source/flock.cpp:71133`;
`src/shell/instrument/CMakeLists.txt:1539`; `vendor/WDL/WDL/swell/Makefile:90`.
**What breaks.** No CMake file in the tree calls `find_package(Threads)` or links
`Threads::Threads`/`-lpthread`/`-ldl` (grep-verified across all 23). Two separate exposures:
- **Extension.** `panel_state.h:23` includes `<pthread.h>` on non-Win32 — and it is not the
only entry point: `vendor/reaper-sdk/sdk/reaper_plugin.h:50` includes `<pthread.h>`
unconditionally in its non-Windows branch, so it is in **every** shell TU's include graph
(every `.cpp` that includes `reaper_plugin.h`/`reaper_plugin_functions.h`), not just the
one panel file. This strengthens rather than changes the finding: I still did not find a
pthread *call* in `src/shell/`'s own code, so this may be an include-only dependency that
needs no link flag on a modern glibc (where the pthread symbols are in libc proper). Stays
Minor, and `[verify — Linux]`.
- **VST3 target.** `flock.cpp` is unambiguous: on non-Windows it calls
`pthread_mutexattr_init`/`_settype`/`_destroy` (`:7176`), `pthread_mutex_init` (`:74`),
`pthread_mutex_destroy` (`:89`), `_lock` (`:105`), `_unlock` (`:121`) and `_trylock`
(`:133`). It is unconditionally in the `vst3_sdk` source list
(`src/shell/instrument/CMakeLists.txt:27`). Any Linux VST3 target must link a thread
library. SWELL's own Makefile uses `-lpthread -ldl` (`:90`) for the same reason.
Adding `find_package(Threads REQUIRED)` + `target_link_libraries(... Threads::Threads)` is
correct on all three platforms and costs nothing on Windows.
**Done when:** the extension links with `Threads::Threads` removed (proving the include-only
half needs no flag, or naming the symbol that forces it — see V5), and, if Fork B is taken,
`reasampler_vst` links clean with `Threads::Threads` present.
*Shared macOS/Linux — `reaper_plugin.h:50`'s non-Windows `<pthread.h>` include and
`flock.cpp`'s non-Windows pthread calls apply identically to the APPLE branch.*
---
### Λ-05 — `reaper_plugin.h`'s SWELL include resolves only by coincidence of the `-I` line
**Severity: Minor. Effort: S — a comment, or an `INTERFACE` target that carries both include dirs as one unit.**
**Citations.** `vendor/reaper-sdk/sdk/reaper_plugin.h:4254`; `CMakeLists.txt:6566`;
`src/app/CMakeLists.txt:56`; `src/shell/instrument/CMakeLists.txt:97`.
**What breaks — and, today, does not.** On non-Windows, `reaper_plugin.h:49` does
`#include "../WDL/swell/swell.h"`. Relative to the including file's own directory
(`vendor/reaper-sdk/sdk/`) that resolves to `vendor/reaper-sdk/WDL/swell/swell.h`, which does
not exist — listing `vendor/reaper-sdk/` shows only `README`, `reaper-plugins`, and `sdk`.
The include is nevertheless satisfiable, because the quoted-include fallback searches the
`-I` path, and `WDL_INC` is `${CMAKE_CURRENT_SOURCE_DIR}/vendor/WDL/WDL` (`CMakeLists.txt:66`) —
so `WDL_INC` + `../WDL/swell/swell.h` normalizes to `vendor/WDL/WDL/swell/swell.h`, which
exists. Both module targets pass `${WDL_INC}` (`src/app:56`, `src/shell/instrument:97`), so it
holds today.
The hazard is that this is a load-bearing coincidence with no comment anywhere, and it is
**invisible on Windows**`reaper_plugin.h:43` takes `<windows.h>` instead, so dropping
`${WDL_INC}` from any target that includes `reaper_plugin.h` would pass CI on Windows and
fail only on Linux/macOS. Worth pinning before more targets are added.
---
### Λ-06 — the build has no platform partition of its source list, so every T2 finding lands here as a CMake change
**Severity: Minor (structural). Effort: S per TU once T2 says which ones need it.**
**Citations.** `src/app/CMakeLists.txt:851` and `:7198`;
`src/shell/actions/drag_out_win.cpp:8`, `:249`, `:282`.
**Mechanism.** All 38 shell TUs (plus `app/main.cpp`, 39 listed TUs total) are listed unconditionally (`:851`); the platform block at
`:7198` only *adds* sources, never removes or substitutes any. Every TU therefore enters a
Linux compile, and the only thing standing between the build and a compile error is each
file's own `#ifdef` discipline. Today that discipline is uneven in a way worth recording:
- `drag_out_win.cpp` is the model — `#ifdef _WIN32` at `:8` with a real
`// ---- macOS / Linux (SWELL)` branch at `:249` and `#endif` at `:282`.
- `arrange_drop_win.cpp` and `instrument_drop_win.cpp` contain **zero** occurrences of
`_WIN32` (grep count 0 each) and include no `<windows.h>` — they are `_win`-suffixed for
the surface they serve, not for a platform dependency, and appear portable by inspection.
This is not itself a defect; it is the reason the ordering sketch in §5 puts "read T2's
output, then partition the source list" as a distinct step rather than folding it into the
resgen work.
---
### Λ-07 — `std::filesystem` in a pure library, with no stated minimum compiler
**Severity: Minor. Effort: S — a `cmake_minimum_required`-adjacent compiler check or a documented floor.**
**Citations.** `src/core/capture/capture_paths.cpp:5` (`#include <filesystem>`) and `:95`
(`std::filesystem::path(rppPath).parent_path().string()`); `README.md:5054`.
**What breaks.** `capture_paths` is a *pure* library with its own CTest target
(`src/core/capture/CMakeLists.txt:12`), so this is on the test-harness path as well as the
extension's. GCC 8 requires an explicit `-lstdc++fs` for `std::filesystem`; GCC ≥ 9 and
libstdc++ ≥ 9 do not. `README.md:51` states only "A C++17 compiler" as the prerequisite, and
`cmake_minimum_required(VERSION 3.19)` (`CMakeLists.txt:1`) says nothing about the compiler.
On any current distro toolchain this is a non-issue; it becomes one on an older CI image.
Twelve other files in `src/shell/` also use `<filesystem>` (grep-verified), but one —
`src/shell/instrument/instrument_bake.cpp` — is in the `reasampler_vst` target, not the
extension: eleven in `reaper_reasampler`, sharing the fix below; the twelfth shares it only
if/when Fork B builds that target on Linux.
**Done when:** `capture_paths_tests` links on the oldest toolchain the phase targets without
`-lstdc++fs` added explicitly, or that flag is added and documented as the floor.
*Shared macOS/Linux — `capture_paths` and the eleven extension-side `<filesystem>` users
compile identically under the APPLE branch; whether Apple's libc++ needs the same kind of
minimum-toolchain floor as libstdc++ does is a separate `[verify]` this doc has not checked,
but the coverage-hole shape (undocumented compiler floor) is identical either way.*
---
### Λ-08 — the install instruction is silent about single-config generators, and no path is documented for Linux at all
**Severity: Minor. Effort: S — a doc fix, or an `install()` rule if packaging is ever automated.**
**Citations.** `src/app/CMakeLists.txt:68` (`LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}"`);
`README.md:130131`; `CLAUDE.md` §"Install / reload";
`docs/product/versioning-and-release.md:364366`.
**What breaks — a gap, not an error.** `README.md:130131` and `CLAUDE.md` §"Install /
reload" both already carry the multi-config qualifier verbatim ("`build/Release/` on a
multi-config generator — not the default `Debug/` output") — so the instruction is not
wrong, it is simply silent about the other case. On a multi-config generator CMake appends
the config directory, giving the documented `build/Release/`. On Ninja/Make the module lands
directly at `build/reaper_reasampler.so`, and neither doc names that path. Compounding it,
there is no `install()` rule anywhere in the 23 CMake files, and no target directory is
documented for Linux at all — `CLAUDE.md` and `README.md:132` say "REAPER's `UserPlugins/`
folder (Options → Show REAPER resource path)", which is a correct user-facing instruction but
not a path a pipeline can use. I could not confirm REAPER's Linux resource-path root from the
SDK: grepping `vendor/reaper-sdk/sdk/` for `UserPlugins` and `reaper_*.so` returns nothing —
the `reaper_*.dll|dylib|so` filename convention stated in `CLAUDE.md` is not documented in the
vendored headers. `[verify — Linux]`.
---
### Λ-09 — no warning flags anywhere, so the first GCC/Clang build surfaces a diagnostic set nobody has ever seen
**Severity: Minor. Effort: S to add flags; **unknowable** to fix what they reveal — that is the honest band.**
**Citations.** grep for `target_compile_options` across all 23 CMake files: zero hits (the
only `compile_definitions` hits are `SWELL_PROVIDED_BY_APP` at `src/app/CMakeLists.txt:80`,
`:92` and the SDK's `DEVELOPMENT`/`RELEASE` selector at
`src/shell/instrument/CMakeLists.txt:42`). `CMakeLists.txt:2830` for the absence of
`CMAKE_CXX_EXTENSIONS`.
**Mechanism.** The codebase has been compiled by exactly one compiler family. MSVC's default
is `/W1`; GCC/Clang's default is essentially silent. Adding `-Wall -Wextra` on the first
Linux build is the right move, but the size of the resulting diagnostic set over ~39k LOC is
not estimable from Windows, which is why the effort band is split. Do **not** add `-Werror`
in the same change.
Secondary, in the same family: `CMAKE_CXX_EXTENSIONS` is unset, so it defaults to `ON` and
GCC/Clang compile as `-std=gnu++17` rather than `-std=c++17`. Not a bug; a hygiene knob
worth flipping while the flags are being set, since it is the difference between "portable
C++17" and "whatever GNU extensions happened to be used".
**Coverage hole: the link-succeeds-but-load-fails asymmetry is never stated.** GNU `ld` does
not error on undefined symbols in a shared object by default — MSVC's linker does. For a
`MODULE` REAPER `dlopen()`s, that means a Linux build can link `reaper_reasampler.so` clean
and only discover a missing symbol when REAPER tries to load it (`dlopen` failing, or a
partially-resolved symbol table if a satisfying-by-coincidence symbol exists elsewhere in the
process — a real risk given Λ-03's global-interposition finding above). Neither `Threads`
(Λ-04) nor visibility (Λ-03) named this asymmetry, and it undercuts §5's framing of V1
(`cmake --build build`) as "the single highest-information action available" — a clean build
on Linux does not carry the same guarantee a clean Windows build does. `-Wl,--no-undefined`
(GNU ld) would restore the MSVC-equivalent fail-at-link-time behavior; whether it is needed
here specifically depends on whether `SWELL_PROVIDED_BY_APP`'s function-pointer design (§1a)
already accounts for every symbol the modstub doesn't itself define — that is `[verify —
Linux]`, since it requires an actual GCC/Clang link to observe.
**Done when:** the Linux build either adds `-Wl,--no-undefined` (or the equivalent) so an
undefined-symbol build failure surfaces at link time, or a stated reason is recorded for why
`SWELL_PROVIDED_BY_APP` makes the gap moot.
*Shared macOS/Linux — `-Wall -Wextra`, `CMAKE_CXX_EXTENSIONS OFF`, and the GNU-ld-vs-MSVC-ld
asymmetry (macOS's linker also does not default to erroring on undefined symbols) apply
identically to the APPLE branch.*
---
### Λ-10 — the submodule story costs a Linux developer a large download for a target that is gated off
**Severity: Minor. Effort: S — a doc paragraph.**
**Citations.** `.gitmodules` (three entries, no `shallow`, no `branch`);
`README.md:7086`; `src/shell/instrument/CMakeLists.txt:9`; `CLAUDE.md` §"One-time submodule setup".
**Mechanism.** `README.md:7275` already warns that `git clone --recursive` pulls all of
`vendor/vst3sdk`'s nested submodules (`vstgui4`, `tutorials`, `doc`, `cmake`) that this
project never links, and prescribes the narrow init instead. What neither the README nor
`CLAUDE.md` says is that on Linux the `vendor/vst3sdk` submodule is **pure cost**: the
`WIN32 AND EXISTS` gate at `src/shell/instrument/CMakeLists.txt:9` omits the target
regardless, so `git submodule update --init vendor/reaper-sdk vendor/WDL` is the complete
Linux prerequisite. One sentence in the platform-support table would save every Linux
contributor the download.
---
## 3. Surfaces checked and found clean
Recorded so the plan author does not re-spend effort here. Each was read or grepped in this
pass.
- **All 23 `CMakeLists.txt` files were read in full**, not sampled: root; `src/app`;
`src/core`; `src/core/{audio,capture,instrument,json,model,reclaim,tracking,ui,util,version,view,wire}`;
`src/core/instrument/{bake,engine,map,note,ui}`; `src/core/instrument/engine/{filter,loop}`;
`src/shell/instrument`. Plus `cmake/reasampler_targets.cmake`. Of these, 21 are entirely
platform-neutral — they declare libraries and tests through the two helpers and nothing
else.
- **`CMAKE_POSITION_INDEPENDENT_CODE ON`** (`CMakeLists.txt:30`) — the usual Linux
`MODULE`-fed-by-`STATIC` trap, already handled.
- **`.so` naming.** `PREFIX ""` (`src/app/CMakeLists.txt:66`) + `SUFFIX ".so"` (`:93`) +
channel-derived `OUTPUT_NAME` (`:67`) yields `reaper_reasampler.so` /
`reaper_reasampler_beta.so`, matching the `reaper_*` convention. `ARCHIVE_OUTPUT_DIRECTORY`
(`:69`) is a Windows import-lib concern and harmlessly inert on Linux.
- **The channel fork is platform-independent.** `REASAMPLER_CHANNEL` (`CMakeLists.txt:4054`)
threads through `configure_file` (`:5861`) into names only; nothing in it is
Windows-specific, so beta builds fork identically on Linux.
- **The version padding canary** (`src/core/version/CMakeLists.txt:2142`) uses a
function-scoped `set()` + second `configure_file` + longhand `add_executable`. Portable;
no platform assumption.
- **The whole test corpus is platform-neutral.** 91 files in `tests/`, 91 declarations in
CMake. Only `test_capture_paths.cpp` branches on platform and it already has POSIX
expectations written; only `test_pitch_shift.cpp` mentions `windows.h`, in a comment.
- **Generator-shape handling in the docs is already correct.** `README.md:9398` explains
that `-C Debug` is required on multi-config and unnecessary on Ninja/Make. `ctest --test-dir
build` works bare on Linux; the `-C Debug` in the README's example is harmless there.
- **SWELL API coverage for the LICE slice** — all six SWELL GDI entry points LICE needs are
in the modstub-resolved table (`swell-functions.h:857`, `:863`, `:877`, `:885887`).
- **The LICE slice needs no fifth TU on Linux** (`lice.cpp:3040` `_LICE_RGB2HSV_invtab`
table, `lice_textnew.cpp:26` file-static `utf8makechar`) — an inference from V1's link
success plus these two spot checks, not exhaustive; see §1a's fuller caveat, including the
unreconciled `lice_colorspace.o` in SWELL's own `Makefile:106107`.
- **`swell-modstub-generic.cpp` compiles against its own includes** — it uses `printf`
(`:61`), which arrives via `swell.h``swell-types.h:29` (`#include <stdio.h>`). The
`dlopen`/`readlink` block (`:69131`) is behind `SWELL_LOAD_SWELL_DYLIB`, which this build
never defines.
- **The Linux run-loop IIDs are already in the vendored VST3 slice.**
`public.sdk/source/common/commoniids.cpp:5054` defines `Linux::IEventHandler`,
`Linux::ITimerHandler` and `Linux::IRunLoop` under `#if SMTG_OS_LINUX`, and that file is
already in the `vst3_sdk` source list (`src/shell/instrument/CMakeLists.txt:36`). A Linux
VST3 port needs no additional SDK translation unit for the run loop.
- **`SMTG_OS_LINUX` / `SMTG_EXPORT_SYMBOL` are correct in the pinned SDK**
(`pluginterfaces/base/fplatform.h:132`, `:164`).
- **The MSVC-ABI caveat is Windows-scoped.** `reaper_plugin.h:22` warns that the C++
interfaces "require MSVC on win32, or at least the MSVC-compatible C++ ABI" — that is a
constraint on Windows toolchains (mingw), not on Linux, where REAPER and the extension both
use the Itanium ABI.
---
## 4. The VST3-on-Linux question, stated as a fork
Daniel rules on this. Below is the cost of each side with the evidence, and nothing else.
### Fork A — extension only on Linux
Scope: everything in §2 (Λ-01 through Λ-10). The VST3 gate at
`src/shell/instrument/CMakeLists.txt:9` stays exactly as it is; a Linux configure omits the
target and the `vendor/vst3sdk` submodule becomes optional there (Λ-10).
**Cost profile:** one Major that must be solved (Λ-01, the panel), two Majors that are
one-line-ish but load-bearing (Λ-02 optimization, Λ-03 visibility), one Major/Minor
(Λ-04 threads), and six Minors. No new third-party dependency. No new artifact format. No
change to any product decision on record.
**The unknown that dominates the band:** none of the extension's source portability is
audited here. The build-system work is small; whether the 38 shell TUs *compile and behave*
against SWELL's Win32 subset is T2's answer, and it, not this list, sets the phase's size.
### Fork B — extension + ReaSampler 9000 on Linux
Everything in Fork A, plus the following. Three of five are Blockers under the severity
vocabulary defined above ("the Linux build cannot produce a loadable artifact"): B1, B2, B3.
B4 downgrades to Major — the module still loads; only the editor fails to attach without it.
B5 is not a technical blocker at all — it is a documentation act, though one that must
precede the others in sequence (see §6 Stage 5).
- **B1 — the entry point is the wrong one, and the right one is already vendored.**
`src/shell/instrument/CMakeLists.txt:78` compiles
`public.sdk/source/main/dllmain.cpp` into the module. That file does
`#include <windows.h>` at `:41` with no `SMTG_OS_*` guard — grepping the file for
`SMTG_OS` returns nothing, and its only conditionals are
`#if defined(_MSC_VER) && defined(DEVELOPMENT)` (`:43`, `:96`) and `#ifdef __cplusplus`
(`:58`, `:87`). Linux needs
`public.sdk/source/main/linuxmain.cpp`, which is present in the slice and exports
`ModuleEntry`/`ModuleExit` (`extern "C"` declarations `linuxmain.cpp:4752`; definitions
`:5967` and `:7282` respectively). Both are **mandatory** — the
SDK's own loader refuses the module without either
(`public.sdk/source/vst/hosting/module_linux.cpp:189204`, error strings "The shared
library does not export the required 'ModuleEntry' function" and the same for `ModuleExit`).
`GetPluginFactory` is checked at `:205`. **Effort S** — a `CMakeLists.txt` source swap
(`dllmain.cpp``linuxmain.cpp`) plus the platform `if()`; both files are already vendored,
nothing to write from scratch.
- **B2 — the artifact is a directory bundle, not a file.** Today the target sets
`SUFFIX ".vst3"` on a MODULE (`src/shell/instrument/CMakeLists.txt:106111`), producing a
single file. `module_linux.cpp:146170` (`getSOPath`) shows what a Linux host actually
opens: the path must be a **directory**, containing `Contents/`, containing
`<machine>-linux/` where `<machine>` is `uname().machine` (`:158163`, helper at `:8897`),
containing `<stem>.so` (`:167168`). So `reasampler_9000.vst3/Contents/x86_64-linux/reasampler_9000.so`.
That is `add_custom_command(POST_BUILD)` work plus an install rule. Install roots are
`$HOME/.vst3/`, `/usr/lib/vst3/`, `/usr/local/lib/vst3/` and `$APPFOLDER/vst3/`
(`module_linux.cpp:282302`). `Contents/Resources/moduleinfo.json` is **optional**
`getModuleInfoPath` (`:356365`) returns empty when the file is absent rather than failing.
**Effort M** — mechanical, but it is a new artifact shape with an install story and a
per-architecture directory name.
- **B3 — the drawing stack is the real cost, and it is where Fork B stops being a build task.**
The VST3 module compiles the same four LICE TUs (`src/shell/instrument/CMakeLists.txt:80`),
and on non-Win32 LICE resolves its GDI through SWELL (§1a). But **a VST3 plugin is not a
REAPER extension**: `swell-modstub-generic.cpp`'s default branch exports `SWELL_dllMain`
(`:135`), which only REAPER's *extension* loader calls. Nothing hands a VST3 plugin the
SWELL function table. That leaves two routes:
- **Route B3a — `SWELL_LOAD_SWELL_DYLIB`** (`swell-modstub-generic.cpp:69131`): the stub
reads `/proc/self/exe`, strips to the directory, and `dlopen`s `libSwell.so` beside the
host binary (`:8198`), then calls `SWELL_set_app_main(SWELLAppMain)` (`:112114`). That
reference makes `SWELLAppMain` a **required definition in the plugin** — it is declared at
`swell-types.h:137` as "to be implemented by app (if using swellappmain.mm)". Whether
REAPER's Linux install ships a
`libSwell.so` beside its executable is `[verify — Linux]`, and this route makes the VST3
load-bearing on being hosted by REAPER specifically, which is consistent with D5's
REAPER-only stance but is a new runtime coupling to state explicitly.
- **Route B3b — build SWELL into the module.** `vendor/WDL/WDL/swell/Makefile:100104`
enumerates the 14 SWELL TUs (`swell.o swell-ini.o swell-miscdlg-generic.o
swell-wnd-generic.o swell-menu-generic.o swell-kb-generic.o swell-dlg-generic.o
swell-gdi-generic.o swell-misc-generic.o swell-gdi-lice.o swell-generic-headless.o
swell-generic-gdk.o swell-appstub-generic.o swell-modstub-generic.o`) and `:106107` the 8
LICE TUs it needs alongside. Link requirements at `:90` (`-lpthread -ldl`), `:128130`
(pkg-config `gtk+-3.0` or `gdk-3.0`), `:145151` (`freetype2` + `-lfontconfig`) and
`:154` (`-lGL`).
**This is a wholly new third-party dependency surface** — GDK/GTK3, FreeType, Fontconfig,
OpenGL — that the extension deliberately avoids via `SWELL_PROVIDED_BY_APP`. Flagging it
as a scope expansion, per the dispatch's constraint; it is not something to slip in.
**Effort L either way**, and B3b is the larger of the two by a wide margin.
- **B4 — the editor's platform contract changes.** The view must report
`kPlatformTypeX11EmbedWindowID` (`pluginterfaces/gui/iplugview.h:79`) instead of
`kPlatformTypeHWND` (`:62`), and timers/file-descriptors must go through
`Steinberg::Linux::IRunLoop` (`iplugview.h:267279`) with `IEventHandler` (`:223229`) and
`ITimerHandler` (`:239245`) — the header states the reason at `:255256`: "On Linux the
host has to provide this interface to the plug-in as there's no global event run loop
defined as on other platforms." The whole `namespace Linux` block is `#if SMTG_OS_LINUX`
(`:209`, closed `:284`), so it compiles in automatically. The **build-side** cost is nil
(the IIDs are already vendored — see §3); the **source-side** cost is `editor_platform.cpp`
and every timer in the editor, which is T2's surface. **Effort: build S, source L** — build
is S because no new CMake source is needed (the SDK block compiles in automatically); source
is L because it is a genuine window/run-loop rewrite (an X11 embed contract with no Win32
analogue, not a mechanical port) and outside this doc's own portability scope to size
further.
- **B5 — a settled product decision has to be reversed, in writing.** D5 ("Windows-only,
VST3-only, REAPER-only") is recorded as a locked decision with "a mac/Linux build" named
as a non-goal in `src/core/instrument/CLAUDE.md` (Invariants → the three locked decisions;
and again under Non-goals/guardrails) and `src/shell/instrument/CLAUDE.md`
(Non-goals/guardrails). `src/shell/panel/CLAUDE.md` carries a third dependent statement.
Fork B is not just more work — it invalidates three recorded invariants and needs Daniel's
explicit reversal before any code moves. **Effort S** — no code or CMake change of its own,
a rewrite of the three `CLAUDE.md` passages naming D5 plus Daniel's sign-off; the L-sized
cost sits in B3/B4, not here.
**The asymmetry worth naming.** Fork A's build work is small and its unknown is source
portability. Fork B's build work is genuinely large (B2, B3) *and* carries an unbounded
source-portability tail (B3b's dependency surface, B4's window/run-loop rewrite) *and*
requires reversing a settled decision. The two are not "the same phase, one bigger" — they
are different-shaped commitments.
---
## 5. Open questions
### `[verify — Linux]` — answerable by trying it on a Linux box
| # | Question | Exact check |
|---|---|---|
| V1 | Does the extension compile at all under GCC/Clang? | `cmake -B build -S . -G Ninja && cmake --build build 2>&1 \| tee build.log` — the single highest-information action available and gates most of T2's list too, with one caveat (Λ-09): a clean link is not the same guarantee here as on MSVC, since GNU `ld` does not error on undefined symbols in a `MODULE` by default — V1 answers "does it compile and link", not "does it load". |
| V2 | Does `ctest` pass all 91 targets? | `cmake -B build -S . -G Ninja && cmake --build build && ctest --test-dir build --output-on-failure` (no `-C` needed on Ninja). |
| V3 | Does REAPER's Linux build call `SWELL_dllMain` in a `reaper_*.so`, populating the API table? | Drop the built `.so` in `~/.config/REAPER/UserPlugins/`, restart, and check the extension's actions appear. A null table shows as immediate crash or dead actions. |
| V4 | Where exactly is `UserPlugins/` on Linux, and is `reaper_*.so` the right filename glob? | Options → Show REAPER resource path in a Linux REAPER. The SDK headers do not document either — grep of `vendor/reaper-sdk/sdk/` for `UserPlugins` and `reaper_*.so` returns nothing. |
| V5 | Does `panel_state.h:23`'s `<pthread.h>` actually require a link flag on the target glibc? | Build without `Threads::Threads`; if it links, it does not. |
| V6 | Does `-fvisibility=hidden` leave `ReaperPluginEntry` and `SWELL_dllMain` exported? | `nm -D --defined-only reaper_reasampler.so \| grep -E 'ReaperPluginEntry\|SWELL_dllMain'` after adding the preset. |
| V7 | With Λ-01 fixed by the resource-id-0 route, does `SWELL_CreateDialog` return a usable child HWND for the docker? | Panel toggle action in a Linux REAPER. |
| V8 | Does `std::filesystem` need `-lstdc++fs` on the chosen toolchain? | Build `capture_paths_tests` alone; the link error names it. |
| V9 | Does REAPER's Linux install ship a `libSwell.so` beside its executable (Fork B, route B3a)? | `ls $(dirname $(readlink -f $(which reaper)))/libSwell.so`. |
| V10 | Which `uname -m` values must the VST3 bundle carry (Fork B, B2)? | `uname -m` on each target. `module_linux.cpp:158163` keys the bundle directory off exactly this string (tree-grounded and verified); which architectures REAPER itself ships for Linux (x86_64, aarch64, armv7l is the claim) is an external fact with no source in this tree — `[verify]` against REAPER's own download page before relying on it. |
### `[Daniel]` — genuine product/scope calls
| # | Question | Why it is not answerable by reading code |
|---|---|---|
| D1 | **Fork A or Fork B?** Extension only, or extension + ReaSampler 9000 on Linux? | §4 states both costs. The choice is scope, and D5 makes it a reversal of a recorded decision, not a technical lookup. |
| D2 | If Fork B: **route B3a (dlopen the host's `libSwell.so`, requires defining `SWELLAppMain`) or B3b (vendor and build full SWELL, adds GDK/GTK3 + FreeType + Fontconfig + OpenGL)?** | B3b is a dependency-surface expansion; the dispatch requires that be flagged rather than chosen. B3a is cheaper but couples the plugin's UI to being hosted by REAPER specifically. |
| D3 | Is **macOS in or out** of the same phase? | Λ-01, Λ-03, Λ-04, Λ-07 and Λ-09 are all marked *shared macOS/Linux* at their own sections above (Λ-01: identical broken comment block, `src/app/CMakeLists.txt:8386` vs `:9497`, and shared `swell_resgen.php` output per `README.md:120`; Λ-03/04/07/09: same mechanism under the APPLE branch, detailed at each). Doing both at once is cheaper than doing them serially — but that is a scope call, and macOS additionally implies signing/notarization (not mentioned anywhere in `versioning-and-release.md`) plus its own open question this doc does not resolve: whether `swell-modstub.mm` (`src/app/CMakeLists.txt:79`) even compiles under a CXX-only `project()` (`CMakeLists.txt:26`, no `OBJCXX`) — see §1a. |
| D4 | Does **CI get built in this phase**, and on what runner? | `versioning-and-release.md:307311` and `:361363` hand "the pipeline" to dev-ops and commit to three platform artifacts per channel, but no CI exists in the repo on any platform. Adding it is infrastructure and needs an explicit ask. |
| D6 | Is the Linux artifact **shipped or developer-only** for 1.x? | Determines whether Λ-02 (Release build) and Λ-08 (install path) are must-fix or nice-to-have, and whether a Linux `install()` rule is worth authoring. (Numbered D6, not D5, to avoid colliding with the settled product decision D5 — "Windows-only, VST3-only, REAPER-only" — cited above at §1b and B5.) |
| D7 | Λ-01's fork: **resource-id-0 escape hatch, or the resgen route?** §6 Stage 2 gates on T2's read of `panel_window.cpp`, and V7 presupposes the route is already picked. | It is a source-code + build-pipeline tradeoff (a `panel_window.cpp` DLGPROC→WNDPROC change vs. a PHP build step + generated-file handling) with no code-only right answer — needs Daniel's call once T2 reports, same as D1/D2. |
Deliberately **not** listed as `[Daniel]`, because reading more would settle them: whether
`arrange_drop_win.cpp`/`instrument_drop_win.cpp` need platform gating (read the files — T2);
whether the SDK's `moduleinfo.json` is required (it is not — `module_linux.cpp:362364`);
whether the Linux run-loop IIDs need a new SDK TU (they do not — `commoniids.cpp:5054`).
---
## 6. Ordering sketch
Dependency order only — this is sequencing input for a plan author, not the plan.
**Stage 0 — the one action that reprices everything.** Run V1 and V2 on a Linux box against
the tree as-is. Configure will succeed (nothing in the CMake requires Windows); the compile
will produce a diagnostic set that is the actual size of this phase. Every band below is
provisional until this is done. Λ-10's doc fix rides along free, since the person doing this
will hit it first.
**Stage 1 — toolchain floor, no source dependency.** Λ-02 (`CMAKE_BUILD_TYPE` default),
Λ-03 (visibility presets), Λ-04 (`Threads::Threads`), Λ-09 (warning flags + `CXX_EXTENSIONS
OFF`), Λ-05 (pin the `WDL_INC` coincidence), Λ-07 (compiler floor). All are root- or
target-property edits with no ordering constraint among them and none touching source. Doing
them *before* Stage 2 means the resource work is done under warnings rather than retrofitted
into them. Ship as one change.
**Stage 2 — the panel resource decision.** Λ-01. This forks on the resource-id-0 question
(§2, Λ-01) and therefore needs T2's read on `panel_window.cpp` first — so it is gated on T2
output, not on Stage 1. Whichever route wins, it is a shared macOS/Linux fix (see D3).
**Stage 3 — source partition.** Λ-06. Once T2 names which TUs need a platform branch or a
non-Windows sibling, the source list in `src/app/CMakeLists.txt:851` gains its
partition. Strictly after T2 and after Stage 2 (the resgen/shim TU, if that route is taken,
is one of the entries).
**Stage 4 — packaging.** Λ-08, plus whatever D6 decides. Depends on Stage 1 (there is no
point documenting an install path for an unoptimized artifact) and on V4.
**Stage 5 — Fork B, only if D1 says so.** Internally ordered: B1 (entry point swap, S) →
B3 (SWELL strategy per D2 — this gates everything downstream, because a module that cannot
draw cannot be tested) → B2 (bundle layout + install, needs V10) → B4 (X11 embed + run loop,
mostly T2's source work). B5 (the D5 reversal) precedes all of them as a documentation act.
---
## 7. Referred to T2
Source-level items I noticed while reading build files. **Recorded, not analyzed** — the
parallel track owns each of these and I have deliberately not investigated further.
1. `src/shell/panel/panel_window.cpp:135``CreateDialogParam` + `MAKEINTRESOURCE(IDD_BANK_PANEL)`.
Under SWELL this is `SWELL_CreateDialog(SWELL_curmodule_dialogresource_head, …)`. Relevant
to Λ-01's resource-id-0 option, which requires the proc to be a `WNDPROC` returning
`LRESULT`, cast to `DLGPROC` (`swell-functions.h:606608`).
2. `src/shell/actions/arrange_drop_win.cpp` and `src/shell/actions/instrument_drop_win.cpp`
zero `_WIN32` occurrences, no `<windows.h>`, yet `_win`-suffixed. Confirm they are
genuinely portable, or add guards.
3. `src/shell/actions/drag_out_win.cpp:249282` — the `#else` "macOS / Linux (SWELL)" branch
exists but has never been compiled. Whether the SWELL drag path is functionally equivalent
to the Win32 `IDataObject`/`IDropSource` path is unaudited.
4. `src/shell/persist/prune_fs.cpp:36``<windows.h>` behind two `_WIN32` guards. Confirm the
non-Windows deletion path exists and is equivalent (this file is on the only
file-deletion path in the product, per `src/core/reclaim/CLAUDE.md`).
5. `src/shell/panel/draw_kit.cpp:1115`, `draw_kit.h:2729`, `src/shell/panel/panel_state.h:1826`
the `windows.h`-vs-`swell/swell.h` include switches. These look correct; whether the
SWELL subset actually covers every GDI call the kit makes is the question.
6. `src/shell/panel/CLAUDE.md` records "Windows-only (D5) — font/GDI/HFONT choices assume
Windows; no cross-platform font-fallback concern" as an invariant. Font selection on
Linux SWELL (FreeType/Fontconfig-backed) is the concrete instance.
7. `src/core/capture/capture_paths.cpp``normalizeSlashes` lowercases on `_WIN32` only, and
`tests/test_capture_paths.cpp:1946` already encodes the case-sensitive POSIX contract.
The test is written; the behaviour under a case-sensitive filesystem is unverified.