Build Reason Rack Extensions on Linux.
Reason Studios only ships the Jukebox SDK build toolchain for Windows and macOS: the
compiler (clang.exe), the LLVM tools (llc.exe, opt.exe, llvm-link.exe) and the
linker (link.exe) are all Windows binaries. This project wraps them in a Docker image
that runs them under Wine, and drives the whole thing from a small set of Python
scripts.
The MSVC linker and the handful of Windows SDK libraries it needs are vendored directly in
.docker/msvc-files/ — trimmed to the ~50 files this build actually touches, with a small
link shell wrapper that translates Unix paths into Wine paths. The approach is inspired by
msvc-wine, which solves the general problem of
running MSVC under Wine; this repo doesn't use it or require it.
The result: ./build.sh local45 Debugging on your Linux box produces a signed-for-local-use
Rack Extension you can drop into Reason.
- How it works
- Prerequisites
- First-time setup
- Building
- Build configurations
- Build targets
- Project structure
- Configuration
- Bringing your own Rack Extension
- Troubleshooting
- Known rough edges
The build is a five-stage pipeline. Everything except build.sh runs inside the
container, and every Windows .exe runs through Wine.
vst/*.cpp + ShimABI/JukeboxABI.cpp
│
│ 1. clang.exe (-ccdsp, target phdsp64)
▼
*.o.bc (LLVM bitcode, one per source file)
│
│ 2. opt.exe -ph-disable-globals → rejects global state
│ llvm-link.exe + libcpp.bc/libc.bc → one bitcode blob
▼
MyRackExtension_static_library.bc
│
│ 3. opt.exe → Propellerhead instrumentation
│ (timeout checks for Testing/Deployment, -strip for Deployment)
▼
...Inst.bc
│
│ 4. llc.exe -mtriple=x86_64-pc-win32-jbcrt -filetype=obj
▼
...Inst.obj
│
│ 5. link.exe (MSVC, via Wine) /DLL /ENTRY:DSPMain
│ + RackExtWrapperLib.lib + clang_rt.builtins + picolibc shim
▼
MyRackExtension64.dll ──┐
├──► output/ (+ lua files, GUI images, version.txt)
vst/*.lua, GUI/Output/ ──┘
Two things are worth knowing about stage 5. The linker runs twice: first against a
dummy libc (dependencytest.dll) purely to prove your code pulls in no unexpected C
runtime symbols, then for real against a wrapper libc. And /ENTRY:DSPMain is what makes
the DLL a Rack Extension rather than an ordinary Windows library.
Stage 2's -ph-disable-globals pass is the one that most often surprises newcomers — the
Jukebox DSP environment forbids mutable global state, and this pass is where that rule is
enforced.
| Requirement | Notes |
|---|---|
| Docker + Docker Compose | The build never runs on the host directly |
| ~5 GB disk | Ubuntu base + Wine (64 and 32 bit) |
| The Jukebox SDK | Already vendored under src/JukeboxSDK/ |
| MSVC linker + Windows SDK libs | Already vendored under .docker/msvc-files/ (~60 MB) |
You do not need Wine, Python, Lua or clang on the host, and there is no toolchain to
download — everything lives in the image or in the repo. Clone, docker compose up, build.
cd .docker
docker compose up -d --buildThe first build takes a while — it installs Wine (64 and 32 bit), runs wineboot under a
virtual X server, and initialises the Wine prefix.
Verify it is up; the container name must be exactly jukebox-sdk-linux, because that is
what build.sh looks for:
docker ps --format '{{.Names}}' # expect: jukebox-sdk-linuxcd ..
./build.sh local45 Debugging./build.sh <target> [platform] <configuration># Everyday development build
./build.sh local45 Debugging
# With timeout instrumentation, still unoptimised
./build.sh local45 Testing
# Optimised, stripped, NDEBUG
./build.sh local45 Deployment
# Platform is optional and currently only ever 64
./build.sh local45 64 Debuggingbuild.sh is a thin wrapper: it checks the container is running, then
docker execs python3 /src/build.py inside it with the right environment variables.
It passes your arguments straight through, so any usage error is reported by
src/build.py.
VS Code users get the same three builds as launch configurations in
.vscode/launch.json (Run and Debug → Build with bash script (…)).
build.sh sets OUTPUT_DIR=/output/MyRackExtension, so the finished extension appears on
the host in output/MyRackExtension/:
output/MyRackExtension/
├── MyRackExtension64.dll # the Rack Extension itself
├── MyRackExtension64.pdb # debug symbols
├── info.lua # copied from vst/
├── motherboard_def.lua
├── realtime_controller.lua
├── display.lua
├── gui.lua # from vst/GUI/Output/
├── *.png # device panels, icons, filmstrips
├── English/texts.lua # from vst/Resources/
└── version.txt # SDK version + llc/clang --version output
The output directory is wiped at the start of each install step, so don't keep anything of your own in there.
The three configurations are genuinely different — this is where the real behaviour lives.
| Debugging | Testing | Deployment | |
|---|---|---|---|
| Optimisation | -O0 |
-O0 |
-O2 (clang + llc) |
| Preprocessor | DEBUG=1 |
DEBUG=1 |
NDEBUG=1 |
| Debug info | CodeView, standalone, unwind tables | — | — |
| Timeout checks | no | yes (1000 / 100) |
yes (1000 / 100) |
| Bitcode stripped | no | no | yes (-strip) |
| Linker folding | no | no | /OPT:REF /OPT:ICF |
Use Debugging while you develop: timeout checks abort your DSP code when a callback runs long, which makes stepping through a debugger impossible. Switch to Testing to find out whether your DSP actually fits its time budget, and Deployment for anything you intend to ship.
Static analysis (clang --analyze) runs before every compile when STATIC_ANALYSIS=True,
which is the default. It roughly doubles compile time; set it to False in
build.sh for faster iteration.
| Target | Status | Description |
|---|---|---|
local45 |
Working | Local development build, installed straight into output/ |
universal45 |
Not implemented | Platform-independent build for uploading to Reason Studios |
optimized45 |
Not implemented | Per-platform optimised build produced from a universal45 |
universal45 and optimized45 are accepted by the argument parser in
src/build.py and then silently do nothing — only local45 reaches
src/local.py. Distribution builds still need the official Windows or macOS SDK.
.
├── build.sh # Host entry point — docker exec wrapper
├── .docker/
│ ├── Dockerfile # Ubuntu 24.04 + wine64/wine32 + python3 + lua5.3 + xvfb
│ ├── docker-compose.yml # Volume mounts and container name
│ └── msvc-files/ # Vendored MSVC linker + Windows SDK libs (~60 MB)
│ ├── bin/x64/link # Wrapper: Unix→Wine path translation, noise filtering
│ ├── vc/tools/msvc/… # link.exe + its DLLs, libcmt/libvcruntime/oldnames
│ └── kits/10/lib/… # kernel32, Uuid, ucrt
│
├── src/ # Build system — mounted at /src
│ ├── build.py # Entry point: argument parsing, target dispatch
│ ├── local.py # local45 target implementation
│ ├── common.py # The pipeline: bitcode, instrumentation, linking, install
│ ├── clang.py # clang.exe flags for the phdsp64 target
│ ├── llc.py # llc.exe — bitcode → Windows .obj
│ ├── llvm.py # llvm-link.exe — bitcode merging, output naming
│ ├── lua.py # Reads product_id / development_version from info.lua
│ ├── command_runner.py # subprocess + wine wrappers
│ ├── constants.py # All paths and env-var handling
│ ├── utils.py # File helpers (copy, chmod, cleanup)
│ ├── debug.py # verbose_print
│ ├── .env # Defaults for docker compose
│ └── JukeboxSDK/ # Vendored SDK
│ ├── API/ # Jukebox.h, JukeboxTypes.h
│ ├── version.txt
│ ├── getproductid.lua # Lua helpers run against your info.lua
│ ├── getdevelopmentversion.lua
│ └── Tools/
│ ├── LLVM/
│ │ ├── Win/bin # clang.exe, llc.exe, opt.exe, llvm-link.exe
│ │ └── Jukebox/ # libc / libcxx bitcode + headers for phdsp64
│ └── Libs/
│ ├── Jukebox/ShimABI/ # JukeboxABI.cpp — always compiled in
│ ├── RackExtensionWrapper/ # RackExtWrapperLib.lib + WinDLLExports.txt
│ └── VisualStudio/ # picolibc.c and per-config libs
│
├── vst/ # YOUR Rack Extension — mounted at /vst
│ ├── *.cpp, *.h # DSP and GUI sources (all *.cpp are compiled)
│ ├── info.lua # Device metadata: product_id, names, version
│ ├── motherboard_def.lua # Properties, audio/CV sockets
│ ├── realtime_controller.lua # RT bindings and diff handling
│ ├── display.lua # Display/remote configuration
│ ├── GUI/Output/ # Pre-rendered panels, gui.lua, filmstrips
│ └── Resources/English/ # texts.lua — UI strings
│
├── output/ # Build output — mounted at /output
└── tmp/ # Mounted at /tmp/vst (see Known rough edges)
The sample device currently in vst/ is Daimyo, a polyphonic synth
(com.soundhubb.Daimyo) — voice pool, envelopes, LFO, waveform tables and an operator
graph. It is a useful reference for how the pieces fit together.
Every path is a container path, because the build only ever runs inside Docker.
| Variable | Value set by build.sh |
Meaning |
|---|---|---|
JUKEBOX_SDK_DIR |
/src/JukeboxSDK |
Vendored SDK root |
SYNTH_DIR |
/vst |
Your source — scanned for *.cpp and *.lua |
OUTPUT_DIR |
/output/MyRackExtension |
Install destination (wiped each build) |
INTERMEDIATE_DIR |
/tmp/IntermediateLLVM |
Bitcode and objects; deleted before and after |
RACK_EXTENSION_NAME |
MyRackExtension |
Drives the DLL filename |
PLATFORM_NAME |
x64 |
Only x64 is supported |
STATIC_ANALYSIS |
True |
Run clang --analyze before each compile |
VERBOSE_BUILD |
False |
Print every command line as it runs |
Two files define these, and it matters which one wins:
- build.sh passes them explicitly with
docker exec -e. These are the ones that apply to a build, and they override everything else. - src/.env is loaded by docker compose into the container environment. It
applies if you run
python3 /src/build.pyby hand inside the container. Note that it setsOUTPUT_DIR=/output(not/output/MyRackExtension), so a manual build installs one directory higher.
To rename your extension, change RACK_EXTENSION_NAME in both places.
- Drop your
*.cpp/*.hintovst/. Every*.cppat the top level ofvst/is compiled — there is no file list to maintain, but there are also no subdirectories scanned. - Edit
vst/info.lua:product_id(reverse-DNS, must match your Reason Studios registration),long_name,medium_name,short_name,version_number,device_type. - Describe your device in
vst/motherboard_def.lua— properties, audio and CV sockets. - Wire up
vst/realtime_controller.luafor RT-side bindings. - Put your rendered GUI into
vst/GUI/Output/(panels,gui.lua, filmstrips, device icons). This build system does not render 2D GUI assets —vst/GUI2D/is referenced inconstants.pybut nothing processes it. Render with the official RE2DRender tooling and commit the results. - Translate UI strings in
vst/Resources/English/texts.lua. ./build.sh local45 Debugging, then copyoutput/MyRackExtension/into your Reason local Rack Extension folder.
A few constraints the toolchain will enforce whether you like it or not:
- No mutable globals in DSP code — the
-ph-disable-globalspass rejects them. - No C/C++ standard library beyond what Jukebox ships. You link against Jukebox's
libc.bc/libcpp.bc, and the dependency-test link will fail loudly if you reach for anything else. - C++17,
-ffreestanding,-fno-builtin, no aligned allocation. -Werror=uninitialized— uninitialised variables are a build failure, not a warning.
Error: Docker container 'jukebox-sdk-linux' is not running
build.sh matches the container name exactly. Start it with cd .docker && docker compose up -d
and confirm with docker ps --format '{{.Names}}'. If you run the image under a different
name, point the wrapper at it: DOCKER_IMAGE=my-container ./build.sh local45 Debugging.
Wrong number of arguments / Unknown configuration
The configuration is case-sensitive and must be exactly Debugging, Testing or
Deployment. Valid forms: local45 <Config> or local45 64 <Config>.
Link fails on kernel32.lib or a /LIBPATH: directory
The vendored toolchain is deliberately minimal — only the libraries this build links against
are present. If you add code that needs another Windows library, you have to add the .lib
to .docker/msvc-files/kits/10/lib/10.0.19041.0/um/x64/ yourself.
If instead you replaced or upgraded the vendored toolchain, its version must be updated in
two places or the link will fail: the three *_lib_path variables in
src/common.py, and MSVCVER / SDKVER in
.docker/msvc-files/bin/x64/link. Both currently pin
Windows SDK 10.0.19041.0 and MSVC 14.29.30133.
Wine errors, or clang.exe won't start
Rebuild the image so wineboot re-initialises the prefix:
cd .docker && docker compose up -d --build --force-recreate. The Wine prefix lives in the
container, not in a volume, so recreating is the fix.
The dependency-test link fails but your code compiles
You've pulled in a C runtime symbol Jukebox doesn't provide. The failing symbol name in the
dependencytest.dll link output tells you which one — usually a <cmath>, <cstdio> or
allocation call that isn't available in the DSP sandbox.
WARNING: No HD GUI folder / HD GUI folder is empty
vst/GUI/Output/ is missing or empty. The build continues and produces a working DLL, but
the device will have no panel graphics.
Seeing what actually ran
Set VERBOSE_BUILD=True in build.sh to echo every clang/llc/opt/link command
line. You can also work interactively:
docker exec -it jukebox-sdk-linux bash
python3 /src/build.py local45 DebuggingWorth knowing before they cost you an afternoon:
tmp/is mounted at/tmp/vst, but intermediates go to/tmp/IntermediateLLVM. The mount is effectively unused, and bitcode/object files are not visible on the host. They are also deleted before and after every build, so there is nothing to inspect after a successful run. To keep them, comment out theremove_temporary_folders()calls in src/local.py..gitignoreexcludesvst/*andoutput/*(keeping only the.gitkeepfiles). Your Rack Extension source is deliberately not tracked by this repository — keep it in its own repo, or unignore it if you want it versioned here.build.shusesdocker exec -it, which needs a TTY. In CI, drop the-t.- The MSVC version is pinned in two unrelated files —
src/common.pyand the.docker/msvc-files/bin/x64/linkwrapper. They have to be changed together. - Only x64 is supported.
platformis still threaded throughlocal.buildandclang.get_clang_libs(it selects thephdsp64lib folder), butphdsp64andx86_64are hardcoded throughoutclang.py,llc.pyandcommon.py. - Static analysis writes
.plistfiles into the source tree.clang --analyzeinclang.pyis invoked without-o, so one analyzer report per source file lands next to your code on every build. They are safe to delete; setSTATIC_ANALYSIS=Falseto stop producing them.
The Jukebox SDK and the vendored MSVC/Windows SDK components are covered by their respective vendors' licenses — check that redistributing them suits your situation before publishing a fork. This repository provides only the Linux build harness around them.