47 KiB
Λ-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/ — and every line number was re-read immediately before
being written down. 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. 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 (1–3 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
across the 19 src/core/** CMake files; 46 of them are on the extension's link line.)
- Root
CMakeLists.txtis 95 lines and declares no targets at all: it owns the version string (:24), the channel fork (:40–54), theconfigure_fileforversion_generated.h(:58–61), the four vendor path variables (:65–68), the four-TULICE_SRClist (:78–83),enable_testing()(:90), and threeadd_subdirectorycalls (:92–94). - Language/toolchain settings are exactly three lines:
CMAKE_CXX_STANDARD 17,CMAKE_CXX_STANDARD_REQUIRED ON,CMAKE_POSITION_INDEPENDENT_CODE ON(CMakeLists.txt:28–30). There is noCMAKE_BUILD_TYPE, noCMAKE_CXX_FLAGS, noCMAKE_CXX_EXTENSIONS, no IPO/LTO setting, and notarget_compile_optionsanywhere in the tree — I grepped all 23 CMake files forcompile_options,find_package,pkg_check,link_directories,target_link_options, andinstall(and got zero hits outside the two files named below. - Platform conditionals exist in exactly two files. Grepping all 23
CMakeLists.txtforWIN32|APPLE|UNIX|LINUX|MSVC|GNU|Clang|CMAKE_SYSTEMreturns hits only insrc/app/CMakeLists.txt(:71if(WIN32),:76elseif(APPLE),:88else()) andsrc/shell/instrument/CMakeLists.txt(:9if(WIN32 AND EXISTS ...)). Every other CMake file is platform-neutral by construction: they call the two helpers incmake/reasampler_targets.cmake(reasampler_pure_libraryat:8–15,reasampler_testat:19–24) and nothing else.
1a. The REAPER extension (reaper_reasampler)
Declared as a MODULE library over 39 shell TUs plus ${LICE_SRC}
(src/app/CMakeLists.txt:8–51), 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 (:65–69).
The three-way platform block (:71–98) is:
| Branch | State | What it does |
|---|---|---|
if(WIN32) :71–74 |
Live and shipped | adds src/resource.rc for the RC compiler |
elseif(APPLE) :76–86 |
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) :88–97 |
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:67–68 states this is
"expected, not a bug" — the tree is honest about it.
Three build-input facts I verified in vendor/ and that hold in the extension's favour:
vendor/WDL/WDL/swell/swell-modstub-generic.cppis entirely inside#ifdef SWELL_PROVIDED_BY_APP(:21, closed at:150). SinceSWELL_LOAD_SWELL_DYLIBis not defined anywhere in this build, the#elsebranch at:133applies: the file's whole contribution is the threeSWELL_curmodule_*resource_headglobals (:31–33), the API function-pointer table built fromswell.h(:37–49), and theSWELL_dllMainexport (:135) that resolves every SWELL symbol from a host-suppliedGetFunc. That is the correct stub for a REAPER extension, and the comment atsrc/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.cppreaches SWELL only throughSWELL_DeleteGfxContext(:153,:218),SWELL_GetCtxFrameBuffer(:182,:226) andSWELL_CreateMemContext(:224);lice_textnew.cppreaches it throughSWELL_PushClipRegion/SWELL_SetClipRegion(:1027–1028) andSWELL_PopClipRegion(:1048). All six are entries in SWELL's API table (vendor/WDL/WDL/swell/swell-functions.h:857,:863,:877,:885–887), so the modstub resolves them from the host. No fifth LICE TU is implied: the_LICE_RGB2HSVtable lives insidelice.cppitself (:3040), and the one non-Win32-only helperutf8makecharis a file-static defined atlice_textnew.cpp:26and used at:241–242. CMAKE_POSITION_INDEPENDENT_CODE ON(CMakeLists.txt:30) is already set. This is the single most commonly missed setting for aMODULEtarget fed bySTATIClibraries 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:33–35 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 19-source vst3_sdk static library (:15–39) pinned to SDK tag
v3.7.9_build_61 (confirmed by git describe --tags in vendor/vst3sdk), and the
reasampler_vst MODULE (:44–81) which additionally compiles dllmain.cpp and
moduleinit.cpp directly (:78–79, with the linker-stripping rationale at :75–77) plus
${LICE_SRC} (:80). Output is a single file named <name>.vst3 (:106–111).
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 (:18–22). 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:19–24, which is
add_executable + target_link_libraries + add_test); one is longhand —
app_version_padding_tests (src/core/version/CMakeLists.txt:37–42), 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. :158–168 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:
:361–363— "Artifact per platform. The binary isreaper_*.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.":307–310— two named artifacts (stable + beta) per platform, "three platform artifacts each", explicitly handed off to dev-ops.:364–366— "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 — the
only hits are the lines above. 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:94–97 (the Linux comment block) and :83–86 (the
identical macOS one); vendor/WDL/WDL/swell/swell_resgen.php:313–314;
vendor/WDL/WDL/swell/swell-dlggen.h:22; vendor/WDL/WDL/swell/sample_project/myapp.cpp:192–193;
vendor/WDL/WDL/swell/swell-dlggen.h:214–218; vendor/WDL/WDL/swell/swell-modstub-generic.cpp:31–33;
vendor/WDL/WDL/swell/swell-functions.h:612–619; src/resource.rc:18–22;
src/shell/panel/panel_window.cpp:135.
What breaks. Three defects stack, and each alone would sink the step:
- The named script does not exist. Both comment blocks say
php ${WDL_INC}/swell/mac_resgen.php src/resource.rc(:85and:96). Listingvendor/WDL/WDL/swell/showsswell_resgen.php,swell_resgen.pl, andswell_resgen.sh— there is nomac_resgen.php.CLAUDE.mdandREADME.md:120both 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. - The named output file is not what the generator writes. Both commented
target_sourceslines (:86,:97) reference${REASAMPLER_SRC_DIR}/resource.rc_mac_dlg.h.swell_resgen.php:313–314composes its output names as$srcfn . "_mac_menu"and$srcfn . "_mac_dlg"— so the actual outputs aresrc/resource.rc_mac_dlgandsrc/resource.rc_mac_menu, with no.hsuffix. - Even the correct filename cannot be a
target_sourcesentry.swell-dlggen.h:22states 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 ofSWELL_DEFINE_DIALOG_RESOURCE_BEGIN(...)macro invocations (swell_resgen.php:117–129) that only expand afterswell-dlggen.hhas been included —swell-dlggen.h:214–218shows the macro emitting a staticSWELL_DialogRegHelperthat registers intoSWELL_curmodule_dialogresource_head. The canonical consumption pattern is the SDK's own sample:sample_project/myapp.cpp:192–193does#include "../WDL/swell/swell-dlggen.h"then#include "res.rc_mac_dlg". A CMaketarget_sourcesentry 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:606–608 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)." src/resource.rc:18–22
shows IDD_BANK_PANEL is precisely that: a WS_CHILD dialog with an empty body and zero
controls, whose comment at :8–10 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:28–30 (the complete list of language settings — I read the
whole 95-line file and there is no CMAKE_BUILD_TYPE anywhere); README.md:106–109;
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:130–131 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 rather than the documented build/Release/
path (see Λ-08).
This matters more here than the usual "forgot to set the build type", because CLAUDE.md's
own performance guardrails section states that the header-inline hot paths (peaks
envelope compute, Voice::advanceFrame, the three envelope evaluators) "presume an
optimizing build", and there is no LTO configured to recover any of it. A Linux user
following the README would ship a binary whose hottest loops did not inline.
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:78–83 (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:75–77);
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. On Linux ELF, symbols
from dlopen'd objects can participate in global interposition, so reaper_reasampler.so's
LICE_FillRect and a hypothetical reasampler_9000.so's LICE_FillRect are candidates to
bind to the same definition. That is a class of bug that presents as "the VST editor draws
with the extension's LICE build" and is invisible on Windows.
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.
Λ-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:20–24;
vendor/vst3sdk/base/thread/source/flock.cpp:71–133;
src/shell/instrument/CMakeLists.txt:15–39; 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:23includes<pthread.h>on non-Win32. I did not find a pthread call in the panel'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). That makes it Minor, and[verify — Linux]. - VST3 target.
flock.cppis unambiguous: on non-Windows it callspthread_mutexattr_init/_settype/_destroy(:71–76),pthread_mutex_init(:74),pthread_mutex_destroy(:89),_lock(:105),_unlock(:121) and_trylock(:133). It is unconditionally in thevst3_sdksource 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.
Λ-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:42–54; CMakeLists.txt:65–66;
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:8–51 and :71–98;
src/shell/actions/drag_out_win.cpp:8, :249, :282.
Mechanism. All 39 shell TUs are listed unconditionally (:8–51); the platform block at
:71–98 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.cppis the model —#ifdef _WIN32at:8with a real// ---- macOS / Linux (SWELL)branch at:249and#endifat:282.arrange_drop_win.cppandinstrument_drop_win.cppcontain 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:50–54.
What breaks. capture_paths is a pure library with its own CTest target
(src/core/capture/CMakeLists.txt:1–2), 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), but they are all in the
extension target, which shares the same fix.
Λ-08 — the artifact path and the install instruction are both wrong for a single-config generator
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:130–133; CLAUDE.md §"Install / reload";
docs/product/versioning-and-release.md:364–366.
What breaks. 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, so the README's "copy from build/Release/" instruction has no
matching directory on Linux. 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:28–30 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".
Λ-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:70–86; src/shell/instrument/CMakeLists.txt:9; CLAUDE.md §"One-time submodule setup".
Mechanism. README.md:72–75 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.txtfiles 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. Pluscmake/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 LinuxMODULE-fed-by-STATICtrap, already handled..sonaming.PREFIX ""(src/app/CMakeLists.txt:66) +SUFFIX ".so"(:93) + channel-derivedOUTPUT_NAME(:67) yieldsreaper_reasampler.so/reaper_reasampler_beta.so, matching thereaper_*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:40–54) threads throughconfigure_file(:58–61) 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:21–42) uses a function-scopedset()+ secondconfigure_file+ longhandadd_executable. Portable; no platform assumption. - The whole test corpus is platform-neutral. 91 files in
tests/, 91 declarations in CMake. Onlytest_capture_paths.cppbranches on platform and it already has POSIX expectations written; onlytest_pitch_shift.cppmentionswindows.h, in a comment. - Generator-shape handling in the docs is already correct.
README.md:93–98explains that-C Debugis required on multi-config and unnecessary on Ninja/Make.ctest --test-dir buildworks bare on Linux; the-C Debugin 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,:885–887). - The LICE slice needs no fifth TU on Linux (
lice.cpp:3040colorspace table,lice_textnew.cpp:26file-staticutf8makechar). swell-modstub-generic.cppcompiles against its own includes — it usesprintf(:61), which arrives viaswell.h→swell-types.h:29(#include <stdio.h>). Thedlopen/readlinkblock (:69–131) is behindSWELL_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:50–54definesLinux::IEventHandler,Linux::ITimerHandlerandLinux::IRunLoopunder#if SMTG_OS_LINUX, and that file is already in thevst3_sdksource 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_SYMBOLare correct in the pinned SDK (pluginterfaces/base/fplatform.h:132,:164).- The MSVC-ABI caveat is Windows-scoped.
reaper_plugin.h:22warns 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 39 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. Each item is a Blocker for the VST3 artifact —
without it there is no loadable .vst3.
-
B1 — the entry point is the wrong one, and the right one is already vendored.
src/shell/instrument/CMakeLists.txt:78compilespublic.sdk/source/main/dllmain.cppinto the module. That file does#include <windows.h>at:41with noSMTG_OS_*guard — grepping the file forSMTG_OSreturns nothing, and its only conditionals are#if defined(_MSC_VER) && defined(DEVELOPMENT)(:43,:96) and#ifdef __cplusplus(:58,:87). Linux needspublic.sdk/source/main/linuxmain.cpp, which is present in the slice and exportsModuleEntry/ModuleExit(linuxmain.cpp:47–52,:59–82). Both are mandatory — the SDK's own loader refuses the module without either (public.sdk/source/vst/hosting/module_linux.cpp:189–204, error strings "The shared library does not export the required 'ModuleEntry' function" and the same forModuleExit).GetPluginFactoryis checked at:205. Effort S. -
B2 — the artifact is a directory bundle, not a file. Today the target sets
SUFFIX ".vst3"on a MODULE (src/shell/instrument/CMakeLists.txt:106–111), producing a single file.module_linux.cpp:146–170(getSOPath) shows what a Linux host actually opens: the path must be a directory, containingContents/, containing<machine>-linux/where<machine>isuname().machine(:158–163, helper at:88–97), containing<stem>.so(:167–168). Soreasampler_9000.vst3/Contents/x86_64-linux/reasampler_9000.so. That isadd_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:282–302).Contents/Resources/moduleinfo.jsonis optional —getModuleInfoPath(:356–365) 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 exportsSWELL_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:69–131): the stub reads/proc/self/exe, strips to the directory, anddlopenslibSwell.sobeside the host binary (:81–98), then callsSWELL_set_app_main(SWELLAppMain)(:112–114). That reference makesSWELLAppMaina required definition in the plugin — it is declared atswell-types.h:137as "to be implemented by app". Whether REAPER's Linux install ships alibSwell.sobeside 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:100–104enumerates the 13 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:106–107the 8 LICE TUs it needs alongside. Link requirements at:90(-lpthread -ldl),:128–130(pkg-configgtk+-3.0orgdk-3.0),:145–151(freetype2+-lfontconfig) and:154(-lGL). This is a wholly new third-party dependency surface — GDK/GTK3, FreeType, Fontconfig, OpenGL — that the extension deliberately avoids viaSWELL_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.
- Route B3a —
-
B4 — the editor's platform contract changes. The view must report
kPlatformTypeX11EmbedWindowID(pluginterfaces/gui/iplugview.h:79) instead ofkPlatformTypeHWND(:62), and timers/file-descriptors must go throughSteinberg::Linux::IRunLoop(iplugview.h:267–279) withIEventHandler(:223–229) andITimerHandler(:239–245) — the header states the reason at:255–256: "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 wholenamespace Linuxblock 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 iseditor_platform.cppand every timer in the editor, which is T2's surface. Effort: build S, source L. -
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) andsrc/shell/instrument/CLAUDE.md(Non-goals/guardrails).src/shell/panel/CLAUDE.mdcarries 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.
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 — this is the single highest-information action available and gates most of T2's list too. |
| 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 — REAPER ships x86_64, aarch64 and armv7l Linux builds, and module_linux.cpp:158–163 keys the bundle directory off exactly this string. |
[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 is a shared macOS/Linux fix (both branches carry the identical broken comment block, src/app/CMakeLists.txt:83–86 vs :94–97), and swell_resgen.php output is shared per README.md:120. Doing both at once is cheaper than doing them serially — but that is a scope call, and macOS additionally implies signing/notarization, which versioning-and-release.md does not mention anywhere. |
| D4 | Does CI get built in this phase, and on what runner? | versioning-and-release.md:307–311 and :361–363 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. |
| D5 | 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. |
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:362–364);
whether the Linux run-loop IIDs need a new SDK TU (they do not — commoniids.cpp:50–54).
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:8–51 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 D5 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.
src/shell/panel/panel_window.cpp:135—CreateDialogParam+MAKEINTRESOURCE(IDD_BANK_PANEL). Under SWELL this isSWELL_CreateDialog(SWELL_curmodule_dialogresource_head, …). Relevant to Λ-01's resource-id-0 option, which requires the proc to be aWNDPROCreturningLRESULT, cast toDLGPROC(swell-functions.h:606–608).src/shell/actions/arrange_drop_win.cppandsrc/shell/actions/instrument_drop_win.cpp— zero_WIN32occurrences, no<windows.h>, yet_win-suffixed. Confirm they are genuinely portable, or add guards.src/shell/actions/drag_out_win.cpp:249–282— the#else"macOS / Linux (SWELL)" branch exists but has never been compiled. Whether the SWELL drag path is functionally equivalent to the Win32IDataObject/IDropSourcepath is unaudited.src/shell/persist/prune_fs.cpp:36—<windows.h>behind two_WIN32guards. Confirm the non-Windows deletion path exists and is equivalent (this file is on the only file-deletion path in the product, persrc/core/reclaim/CLAUDE.md).src/shell/panel/draw_kit.cpp:11–15,draw_kit.h:27–29,src/shell/panel/panel_state.h:18–26— thewindows.h-vs-swell/swell.hinclude switches. These look correct; whether the SWELL subset actually covers every GDI call the kit makes is the question.src/shell/panel/CLAUDE.mdrecords "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.src/core/capture/capture_paths.cpp—normalizeSlasheslowercases on_WIN32only, andtests/test_capture_paths.cpp:19–46already encodes the case-sensitive POSIX contract. The test is written; the behaviour under a case-sensitive filesystem is unverified.src/shell/instrument/reasampler_editor.h:30— unguarded#include <windows.h>. Only matters under Fork B.