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

57 KiB
Raw Blame History

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


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 Windowsreaper_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.hswell-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.cpplinuxmain.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 optionalgetModuleInfoPath (: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 dlopens 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:135CreateDialogParam + 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.cppnormalizeSlashes 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.