# Build system architecture How Slopsmith Desktop is built — the flow of scripts, config, and packaging across local dev and CI. ## Philosophy 1. **npm scripts are the API.** Every build step is callable via `npm run `. CI, local dev, and the DevContainer all use the same entry points. 2. **Shell scripts implement.** Complex bundling lives in `scripts/bundle-*.sh`; npm just invokes them. 3. **Configuration is centralized.** Tool versions + external pins live in `.build-config.json`; per-OS system deps in `.packages/`. 4. **Reproducibility is pinned, not floating.** External dependencies (FluidR3/GeneralUser soundfonts, fluidsynth Windows zip) are tied to specific commits or SHA256s; CI and local see the same bytes. ## High-level flow ``` ┌─────────────────────────────────────────────────────────────────────┐ │ Build environments │ │ │ │ GitHub Actions DevContainer (Docker) Developer CLI │ │ │ │ │ │ │ └─────────────────────┼───────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────┐ │ │ │ npm scripts │ │ │ │ (package.json) │ ← single-source entry │ │ └─────────────────────┘ │ │ │ │ │ ┌──────────────────┼──────────────────┐ │ │ ▼ ▼ ▼ │ │ ┌────────────┐ ┌──────────────┐ ┌────────────┐ │ │ │build:native│ │ bundle │ │ build:ts │ │ │ │ │ │ │ │ │ │ │ │• build:audio│ │• bundle: │ │ (tsc) │ │ │ │ │ │ slopsmith │ │ │ │ │ └────────────┘ │• bundle: │ └────────────┘ │ │ │ python │ │ │ │• bundle: │ │ │ │ binaries │ │ │ │• bundle: │ │ │ │ soundfont │ │ │ └──────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────┐ │ │ │ electron-builder │ │ │ │ (dist:linux etc.) │ │ │ └─────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────┐ │ Configuration files │ │ │ │ .build-config.json .packages/ │ │ ├── versions ├── apt.txt (Ubuntu/Debian) │ │ │ ├── node ├── brew.txt (macOS) │ │ │ ├── python ├── choco.txt (Windows) │ │ │ ├── electron └── README.md │ │ │ ├── cmake │ │ │ └── ubuntu │ │ └── external │ │ ├── fluidsynth_windows (version + URL) │ │ └── soundfont_general_user (URL + SHA256) │ └─────────────────────────────────────────────────────────────────────┘ ``` ## Detailed flow for `npm run dist:linux` ``` npm run dist:linux └── npm run dist -- --linux └── npm run bundle && npm run build:ts && electron-builder --linux └── bundle ├── bundle-slopsmith.sh — server.py + lib + static + plugins ├── bundle-python.sh — portable Python 3.12 runtime + pip + app deps ├── bundle-binaries.sh — ffmpeg + vgmstream-cli + fluidsynth + its .so chain └── bundle-soundfont.sh — GeneralUser-GS.sf2 (SHA256-verified) └── build:ts └── tsc └── electron-builder └── AppImage + .deb ``` For `build:native` (run separately, not part of `dist`): ``` npm run build:native └── build:audio — cmake-js builds the JUCE C++ native addon ``` ## Scripts directory ``` scripts/ ├── bundle.sh — top-level Linux delegator (→ bundle-*.sh) ├── bundle-slopsmith.sh — copy server + plugins ├── bundle-python.sh — portable Python runtime (Linux) ├── bundle-binaries.sh — ffmpeg + vgmstream + fluidsynth + libs (Linux) ├── bundle-soundfont.sh — GeneralUser-GS.sf2 download + verify (cross-platform) ├── build-linux-release.sh — Docker wrapper for reproducible Linux AppImage ├── parse-build-config.py — JSON value extractor └── setup-dev.sh — local prerequisite checker ``` ### Why modular - **Composability**: debug or re-run a single step (`npm run bundle:python`) - **Readability**: each script does one thing and has a single reason to change - **CI/local parity**: CI calls the same scripts where platform-independent; platform-specific inline bash stays in `build.yml` for macOS/Windows (dylibbundler, zip downloads) only because those tools don't fit the Linux shell-script model ### Platform scope per script | Script | Linux | macOS | Windows | |---|---|---|---| | `bundle.sh` | yes | n/a | n/a | | `bundle-slopsmith.sh` | yes | yes (untested) | yes (untested) | | `bundle-python.sh` | yes | no (CI inline) | no (CI inline) | | `bundle-binaries.sh` | yes | no (CI uses dylibbundler inline) | no (CI downloads zips) | | `bundle-soundfont.sh` | yes | yes | yes | ## Configuration ### `.build-config.json` Tool versions pinned once, read by the CI workflow (via `node -p`) and by the DevContainer (via `parse-build-config.py`). External pins (soundfont SHA256, fluidsynth Windows zip URL) live under `external.*`. Update tool versions in one place; every consumer picks them up. ### `.packages/` Platform system-dep lists. One package per line; `#` comments and blank lines are filtered by consumers. See `.packages/README.md` for manual install snippets. ### Electron version One source of truth: `devDependencies.electron` in `package.json`. `build:audio` derives `--runtime-version` via `node -p "require('electron/package.json').version"` so cmake-js rebuilds the native addon against the currently-installed Electron. ## CI vs local parity | Aspect | GitHub Actions | DevContainer | |---|---|---| | Entry | `npm run dist:linux` (via checkout + setup-* + install) | `npm run dist:linux` (inside container) | | Node / Python | From `.build-config.json` | From `.build-config.json` | | System deps | `.packages/apt.txt` | `.packages/apt.txt` | | Slopsmith source | Cloned fresh to `$RUNNER_TEMP` | Bind-mounted from `../slopsmith` | | Python bundle | `scripts/bundle-python.sh` | `scripts/bundle-python.sh` | | Binary bundle | Inline bash (ffmpeg / vgmstream / fluidsynth lib chain) | `scripts/bundle-binaries.sh` (invoked by `npm run bundle`) | | Soundfont bundle | `scripts/bundle-soundfont.sh` | `scripts/bundle-soundfont.sh` | | Smoke test | `fluidsynth --version` + `ffmpeg -version` + `vgmstream-cli --help` | (run manually) | ## Adding a new build step 1. Create a script in `scripts/` (e.g. `bundle-foo.sh`) 2. Register an npm script in `package.json` (e.g. `"bundle:foo": "bash scripts/bundle-foo.sh"`) 3. Wire into the appropriate chain (`bundle`, `build:native`, etc.) 4. Test locally: `npm run bundle:foo` 5. Open a PR — CI consumes the same npm scripts ## Troubleshooting ### Run individual steps ```bash npm run bundle:python # rebuild just the Python runtime npm run bundle:binaries # rebuild just the binary chain npm run bundle:soundfont # re-verify the soundfont ``` ### Read the current config ```bash python3 scripts/parse-build-config.py .build-config.json python3 scripts/parse-build-config.py .build-config.json .versions.electron ``` ### Local vs CI divergence If local and CI builds differ: 1. `.build-config.json` — pinned versions identical? 2. `.packages/apt.txt` — are you running in the DevContainer? 3. The CI workflow inline-bundles some things differently on macOS/Windows (dylibbundler, zip downloads). Local linux dev won't reproduce those. ## See also - [`../.build-config.json`](../.build-config.json) - [`../.packages/`](../.packages/) - [`../.devcontainer/`](../.devcontainer/) - [`../.github/workflows/build.yml`](../.github/workflows/build.yml)