mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-08-11 03:09:56 +00:00
Clean release snapshot
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
# Build Scripts
|
||||
|
||||
Unified build system for Slopsmith Desktop supporting Linux (via Docker), macOS, and Windows.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
./scripts/build-release.sh
|
||||
```
|
||||
|
||||
## Call Hierarchy
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ GitHub Actions │
|
||||
│ build.yml │
|
||||
└────────┬────────┘
|
||||
│
|
||||
│ (calls the same script everywhere)
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ build-release.sh │ Platform dispatcher
|
||||
└────────┬─────────┘
|
||||
│
|
||||
│ Detects host OS:
|
||||
│ - Linux → build-linux-docker.sh
|
||||
│ - macOS → build-macos.sh
|
||||
│ - Windows → build-windows.sh
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────┐
|
||||
│ For Linux: Docker wrapper │
|
||||
│ build-linux-docker.sh → │
|
||||
│ Docker container: │
|
||||
│ ./build-linux-ubuntu.sh │
|
||||
└──────────────────────────────────────┘
|
||||
│
|
||||
│ Sources:
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ build-common.sh │ Shared build logic (~250 lines)
|
||||
└─────────────────┘
|
||||
▲ ▲ ▲
|
||||
│ │ │
|
||||
│ │ ├─── Platform-specific implementations:
|
||||
│ │ install_system_deps()
|
||||
│ │ bundle_python_impl()
|
||||
│ │ bundle_binaries_impl()
|
||||
│ │
|
||||
│ └─ build-macos.sh
|
||||
│ build-windows.sh
|
||||
│ build-linux-ubuntu.sh
|
||||
│
|
||||
└─ platform: mac / win / linux
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **build-release.sh** - Platform dispatcher. Detects OS and routes to the right build script.
|
||||
|
||||
2. **Linux builds** - Always Docker-based for reproducibility:
|
||||
- `build-linux-docker.sh` → Docker container → `build-linux-ubuntu.sh` → packages
|
||||
|
||||
3. **Native builds** - macOS and Windows run directly on host:
|
||||
- `build-macos.sh` - Uses Homebrew dependencies
|
||||
- `build-windows.sh` - Uses Git Bash, downloads binaries
|
||||
|
||||
4. **build-common.sh** - Shared logic sourced by platform scripts:
|
||||
- Validates environment (Node.js, Python, .NET)
|
||||
- Runs npm install, builds C++ engine, bundles resources
|
||||
- Calls platform-specific functions for: dependency installation, Python bundling, binary bundling
|
||||
|
||||
## Platform-Specific Scripts
|
||||
|
||||
### Files
|
||||
|
||||
| Script | Purpose | Requirements | Output |
|
||||
|--------|---------|--------------|--------|
|
||||
| `build-linux-docker.sh` | Reproducible Docker build | Docker, adjacent slopsmith repo | `.AppImage`, `.deb` |
|
||||
| `build-linux-ubuntu.sh` | Native Ubuntu build | Ubuntu/Debian + apt | `.AppImage`, `.deb` |
|
||||
| `build-macos.sh` | Native macOS build | Homebrew, Xcode CLI | `.dmg`, `.zip` |
|
||||
| `build-windows.sh` | Native Windows build | Git Bash, Node.js, Python, .NET | `.exe` installer |
|
||||
|
||||
### Two-Layer Ubuntu Builds
|
||||
|
||||
Most Linux distributions don't have identical package versions. Using Docker ensures the build is reproducible:
|
||||
|
||||
- **Direct use**: `./scripts/build-linux-docker.sh`
|
||||
- **Inside container**: Runs `./scripts/build-linux-ubuntu.sh`
|
||||
- **Why**: Guarantees identical builds across different Linux distros
|
||||
|
||||
### Platform-Specific Functions
|
||||
|
||||
Each platform script implements four functions that `build-common.sh` calls:
|
||||
|
||||
```bash
|
||||
install_system_deps() {
|
||||
# Platform-specific: apt install, brew install, choco install, or downloads
|
||||
}
|
||||
|
||||
bundle_python_impl() {
|
||||
# Linux: copy system Python
|
||||
# macOS: download python-build-standalone
|
||||
# Windows: download embeddable Python zip
|
||||
}
|
||||
|
||||
bundle_binaries_impl() {
|
||||
# Linux: copy existing + patchelf
|
||||
# macOS: copy existing + dylibbundler + sign
|
||||
# Windows: download binaries (ffmpeg, vgmstream, fluidsynth)
|
||||
}
|
||||
|
||||
get_expected_artifacts() {
|
||||
# Globs verify_artifacts checks at the end of the build, e.g.
|
||||
# printf "%s\n" "$PROJECT_DIR/release/*.dmg" "$PROJECT_DIR/release/*.zip"
|
||||
}
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
| Platform | Requirements |
|
||||
|----------|--------------|
|
||||
| **Linux (Docker)** | Docker, adjacent slopsmith repo |
|
||||
| **Linux (native)** | Ubuntu/Debian, sudo, Node.js 22+, Python 3.12+, .NET 10+, apt dependencies |
|
||||
| **macOS** | macOS 11+, Homebrew, Xcode CLI, Node.js 22+, Python 3.12+, .NET 10+ |
|
||||
| **Windows** | Windows 10/11, Git for Windows + Bash, Node.js 22+, Python 3.12+, .NET 10+ |
|
||||
|
||||
**Windows Note:** These scripts must run in Git Bash (MSYS), not `cmd.exe` or PowerShell. They rely on MSYS-style paths such as `/tmp`, which work fine inside Git Bash but won't resolve correctly from a native Windows shell — so for local development outside GitHub Actions, run the scripts from a Git Bash terminal.
|
||||
|
||||
## GitHub Actions
|
||||
|
||||
The CI workflow is extremely simple - just calls the same script:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/build.yml
|
||||
steps:
|
||||
# Install platform-specific dependencies (apt, brew, or choco)
|
||||
- name: Install dependencies
|
||||
run: ...
|
||||
|
||||
# Build using the same script developers use locally
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: ./scripts/build-release.sh
|
||||
```
|
||||
|
||||
Result:
|
||||
- Local builds and CI use identical code paths
|
||||
- Build failures can be reproduced and debugged locally
|
||||
- Workflow is "dumb" - all logic lives in versioned scripts
|
||||
|
||||
## macOS Code Signing & Notarization
|
||||
|
||||
The macOS build signs every bundled native binary (fluidsynth, ffmpeg, vgmstream-cli, embedded Python interpreter + dylibs + extension `.so`s) with a Developer ID Application certificate, then electron-builder signs the `.app` and submits it to Apple's notary service. With signing in place, users get no Gatekeeper "app is damaged" warning on first launch.
|
||||
|
||||
### Required GitHub secrets
|
||||
|
||||
| Secret | Purpose |
|
||||
|---|---|
|
||||
| `APPLE_CERTIFICATE_P12_BASE64` | Developer ID Application cert exported as `.p12`, then `base64 -i cert.p12` |
|
||||
| `APPLE_CERTIFICATE_PASSWORD` | The `.p12` export password |
|
||||
| `APPLE_SIGNING_IDENTITY` | Full identity, e.g. `Developer ID Application: Your Name (TEAMID)` |
|
||||
| `APPLE_ID` | Apple ID email |
|
||||
| `APPLE_APP_SPECIFIC_PASSWORD` | App-specific password from appleid.apple.com (not the regular Apple ID password) |
|
||||
| `APPLE_TEAM_ID` | 10-char team ID from developer.apple.com → Membership |
|
||||
| `KEYCHAIN_PASSWORD` | Any random string — used for the temporary CI keychain |
|
||||
|
||||
When `APPLE_CERTIFICATE_P12_BASE64` is unset (forks, contributor PRs without secret access), the certificate-import step is skipped and `sign-macos-binaries.sh` exits early. The build still completes — it just produces an unsigned `.app` that will trigger Gatekeeper on macOS.
|
||||
|
||||
### Local macOS builds
|
||||
|
||||
Local builds without `APPLE_SIGNING_IDENTITY` set produce an unsigned `.app` (same as before signing was added). To produce a signed local build for testing, ensure your Developer ID Application certificate is in your login keychain and run:
|
||||
|
||||
```bash
|
||||
APPLE_SIGNING_IDENTITY="Developer ID Application: Your Name (TEAMID)" \
|
||||
./scripts/build-release.sh
|
||||
```
|
||||
|
||||
This signs the bundled binaries but does **not** notarize — notarization requires `APPLE_ID` + `APPLE_APP_SPECIFIC_PASSWORD` + `APPLE_TEAM_ID` env vars and is run by electron-builder when those are present.
|
||||
|
||||
### Local cmake-js cache
|
||||
|
||||
`build-windows.sh` only force-clears `$HOME/.cmake-js` when `$CI` is set (or `CLEAN_CMAKE_JS=1` is exported). Local Windows builds reuse the cache by default; set `CLEAN_CMAKE_JS=1` if you need a fully fresh build to mirror CI behaviour.
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# VST trace — runtime-gated diagnostic logging
|
||||
|
||||
`src/audio/VSTTrace.h` defines a `VST_TRACE(...)` macro the addon, the VST
|
||||
host code, and the sandbox subprocess all use to emit lines into
|
||||
`%TEMP%\slopsmith-vst-trace-<pid>.log` (Linux/macOS:
|
||||
`/tmp/slopsmith-vst-trace-<pid>.log`) and stderr. The filename is per-PID
|
||||
so concurrent runs (e.g. addon spawning a sandbox subprocess) each get
|
||||
their own log without interleaving. It's compiled into every build but no-ops at runtime unless the
|
||||
`SLOPSMITH_SANDBOX_DEBUG` environment variable is set to a non-empty value
|
||||
other than `"0"`.
|
||||
|
||||
The first call caches the env var, so flipping the variable mid-process has
|
||||
no effect — set it before launching the host process (`node ...`,
|
||||
`electron ...`, or the sandbox subprocess via the parent's environment).
|
||||
|
||||
## What you'll see when it's enabled
|
||||
|
||||
* `[ctrl] ...` — control-channel framing from `ControlChannel.cpp`
|
||||
(`ConnectNamedPipe`, `readFrame got N bytes`, `event: ready`, error codes).
|
||||
* Sandbox subprocess startup steps from `slopsmith-vst-host.exe`:
|
||||
`args ok`, `audio shm opened`, `control pipe connected`,
|
||||
`plugin loaded: <name>`, `sending ready event`.
|
||||
* `LoadVST: path='...'`, `SubprocessHandle.start: spawned pid=N`, the full
|
||||
CreateProcess command line.
|
||||
* `VSTHost.loadPlugin / VST3ComponentHolder.initialise` host-callback traces
|
||||
from the JUCE VST3 host context (in-process load path only).
|
||||
|
||||
The sandbox host also opens a per-PID file at
|
||||
`%TEMP%\slopsmith-vst-host-<pid>.log` **unconditionally** — this is by
|
||||
design and intentionally not gated on `SLOPSMITH_SANDBOX_DEBUG`. The
|
||||
sandbox subprocess runs hidden (no console window) and can die before
|
||||
the env var has propagated, so an always-on per-PID file is the only
|
||||
reliable way to diagnose "the subprocess died and I have no console"
|
||||
crashes in the field. The file is small (a handful of lines per session),
|
||||
written from a single process, and rotates per PID so they cap naturally.
|
||||
|
||||
## Turning it on
|
||||
|
||||
```cmd
|
||||
:: Windows — set before launching node / electron / the desktop app:
|
||||
set SLOPSMITH_SANDBOX_DEBUG=1
|
||||
node load-gr6.js
|
||||
```
|
||||
|
||||
```bash
|
||||
# macOS / Linux:
|
||||
SLOPSMITH_SANDBOX_DEBUG=1 node load-gr6.js
|
||||
```
|
||||
|
||||
## Reading the log
|
||||
|
||||
The `<pid>` portion is the OS process ID of the writer — find your most
|
||||
recent run via `dir %TEMP%\slopsmith-vst-trace-*.log` (Windows) or
|
||||
`ls -t /tmp/slopsmith-vst-trace-*.log | head` (POSIX).
|
||||
|
||||
```cmd
|
||||
:: Windows — view the most recently modified trace file:
|
||||
for /f "delims=" %f in ('dir /b /o-d %%TEMP%%\slopsmith-vst-trace-*.log') do @type "%%TEMP%%\%f" & exit /b
|
||||
```
|
||||
|
||||
```bash
|
||||
# macOS / Linux
|
||||
cat "$(ls -t /tmp/slopsmith-vst-trace-*.log | head -1)"
|
||||
```
|
||||
|
||||
Per-PID naming means files accumulate over time. Clean up periodically
|
||||
with `del %TEMP%\slopsmith-vst-trace-*.log` (Windows) or
|
||||
`rm /tmp/slopsmith-vst-trace-*.log` (POSIX) when they're no longer
|
||||
needed.
|
||||
Executable
+149
@@ -0,0 +1,149 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build the JUCE audio engine as a Node.js native addon
|
||||
# Usage: ./scripts/build-audio.sh [debug|release]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
BUILD_TYPE="${1:-Release}"
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
# Ensure JUCE submodule is available
|
||||
if [ ! -f "JUCE/CMakeLists.txt" ]; then
|
||||
echo "Initializing JUCE submodule..."
|
||||
git submodule update --init --recursive
|
||||
fi
|
||||
|
||||
# Ensure node_modules exist (for node-addon-api headers)
|
||||
if [ ! -d "node_modules" ]; then
|
||||
echo "Installing npm dependencies..."
|
||||
npm install
|
||||
fi
|
||||
|
||||
# Detect architecture
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64)
|
||||
CMAKE_ARCH="x64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
CMAKE_ARCH="arm64"
|
||||
;;
|
||||
*)
|
||||
CMAKE_ARCH="$ARCH"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Linux: NeuralAmpModelerCore's A2 (slimmable) sources use
|
||||
# std::atomic<std::shared_ptr<...>>, a C++20 library feature that libstdc++ only
|
||||
# implements from GCC 12 on. ubuntu-22.04's default g++ is 11, where the primary
|
||||
# std::atomic template fires a "trivially copyable" static_assert and the build
|
||||
# fails. Prefer a g++ >= 12 when the default is older. No-op on macOS/Windows and
|
||||
# on hosts whose default compiler is already new enough, and respects a CXX the
|
||||
# caller already set.
|
||||
if [ "$(uname -s)" = "Linux" ] && [ -z "${CXX:-}" ]; then
|
||||
default_major="$(g++ -dumpversion 2>/dev/null | cut -d. -f1)"
|
||||
if [ -n "$default_major" ] && [ "$default_major" -lt 12 ] 2>/dev/null; then
|
||||
for v in 14 13 12; do
|
||||
if command -v "g++-$v" >/dev/null 2>&1; then
|
||||
export CC="gcc-$v" CXX="g++-$v"
|
||||
echo "Default g++ is $default_major (<12, lacks std::atomic<shared_ptr>); using g++-$v for the NAM A2 sources"
|
||||
# A build/ configured earlier with the default g++ has that
|
||||
# compiler cached in CMakeCache.txt; cmake-js would reuse it and
|
||||
# ignore CC/CXX, so the A2 sources would still compile with the
|
||||
# old g++ and hit the static_assert. Drop a stale cache (one that
|
||||
# isn't already on the selected compiler) so cmake reconfigures.
|
||||
if [ -f build/CMakeCache.txt ] && ! grep -q "CMAKE_CXX_COMPILER:.*g++-$v" build/CMakeCache.txt; then
|
||||
echo "Removing stale build/ (configured with a different compiler) so cmake reconfigures"
|
||||
rm -rf build
|
||||
fi
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "${CXX:-}" ]; then
|
||||
echo "Warning: default g++ is $default_major (<12) and no g++-12+ was found." >&2
|
||||
echo " The NAM A2 sources need std::atomic<shared_ptr> (GCC 12+ libstdc++); the build will likely fail." >&2
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Get the Electron version directly from the installed Electron package.
|
||||
# Native addons MUST be built against the exact Electron ABI that ships
|
||||
# with the app — guessing a fallback (the prior `|| echo 35.7.5`) can
|
||||
# produce a .node that loads but crashes at runtime when the actual
|
||||
# Electron version differs.
|
||||
echo "Detecting Electron version..."
|
||||
ELECTRON_PKG="node_modules/electron/package.json"
|
||||
if [[ ! -f "$ELECTRON_PKG" ]]; then
|
||||
echo "Error: $ELECTRON_PKG not found. Run \`npm install\` before building native addons." >&2
|
||||
exit 1
|
||||
fi
|
||||
ELECTRON_VERSION=$(node -p "require('./$ELECTRON_PKG').version" 2>/dev/null | tr -d '\r\n')
|
||||
if [[ -z "$ELECTRON_VERSION" ]]; then
|
||||
echo "Error: failed to read Electron version from $ELECTRON_PKG." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " Electron version: $ELECTRON_VERSION"
|
||||
|
||||
# Set environment variables for cmake-js
|
||||
# CROSS-PLATFORM NOTE: cmake-js looks for these CMAKE_JS_* variables internally
|
||||
export CMAKE_JS_RUNTIME="electron"
|
||||
export CMAKE_JS_RUNTIME_VERSION="$ELECTRON_VERSION"
|
||||
export CMAKE_JS_ARCH="$CMAKE_ARCH"
|
||||
|
||||
# Also set npm_config variables for compatibility
|
||||
# CROSS-PLATFORM NOTE: These are needed because cmake-js falls back to node-gyp
|
||||
# which expects npm_config_* variables. Both sets are required for reliable
|
||||
# cross-platform builds, especially on Windows where environment handling differs.
|
||||
export npm_config_runtime="electron"
|
||||
export npm_config_target="$ELECTRON_VERSION"
|
||||
export npm_config_arch="$CMAKE_ARCH"
|
||||
export npm_config_target_arch="$CMAKE_ARCH"
|
||||
|
||||
# Optional: clear cmake-js cache on Windows (where this matters most)
|
||||
# CROSS-PLATFORM NOTE: Only clear cache in CI environments by default to avoid
|
||||
# permission issues on local Windows machines and preserve incremental builds.
|
||||
# On Windows, cmake-js downloads headers to a different location
|
||||
# (C:\Users\...\.cmake-js) than on Unix systems.
|
||||
# To force cache clearing locally, set CLEAN_CMAKE_JS=1
|
||||
if [ "${CLEAN_CMAKE_JS:-}" = "1" ] || { [ -n "$CI" ] && [ -d "$HOME/.cmake-js" ]; }; then
|
||||
echo "Clearing cmake-js cache..."
|
||||
rm -rf "$HOME/.cmake-js"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Building audio engine..."
|
||||
echo " Platform: $(uname -s)"
|
||||
echo " Arch: $CMAKE_ARCH"
|
||||
echo " Electron: $ELECTRON_VERSION"
|
||||
echo " Build type: $BUILD_TYPE"
|
||||
echo ""
|
||||
|
||||
# Debug: show what cmake-js will see
|
||||
echo "Environment for cmake-js:"
|
||||
echo " CMAKE_JS_RUNTIME=$CMAKE_JS_RUNTIME"
|
||||
echo " CMAKE_JS_RUNTIME_VERSION=$CMAKE_JS_RUNTIME_VERSION"
|
||||
echo " CMAKE_JS_ARCH=$CMAKE_JS_ARCH"
|
||||
echo " npm_config_runtime=$npm_config_runtime"
|
||||
echo " npm_config_target=$npm_config_target"
|
||||
echo ""
|
||||
|
||||
npx cmake-js build \
|
||||
--runtime electron \
|
||||
--runtime-version "$ELECTRON_VERSION" \
|
||||
--arch "$CMAKE_ARCH" \
|
||||
--CDCMAKE_BUILD_TYPE="$BUILD_TYPE"
|
||||
|
||||
echo ""
|
||||
echo "Build complete!"
|
||||
if [ -f "build/Release/slopsmith_audio.node" ]; then
|
||||
echo "Output: build/Release/slopsmith_audio.node"
|
||||
ls -lh "build/Release/slopsmith_audio.node"
|
||||
else
|
||||
echo "Warning: slopsmith_audio.node not found in expected location"
|
||||
find build -name "*.node" 2>/dev/null
|
||||
fi
|
||||
@@ -0,0 +1,590 @@
|
||||
#!/bin/bash
|
||||
# Common build logic for all platforms.
|
||||
# Platform scripts source this file and implement four functions:
|
||||
# install_system_deps() — install OS packages (apt / brew / winget)
|
||||
# bundle_python_impl() — bundle Python runtime
|
||||
# bundle_binaries_impl() — bundle system binaries (ffmpeg etc.)
|
||||
# get_expected_artifacts() — globs verify_artifacts checks at the end
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Check if this is being sourced by a platform script
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
echo "Error: build-common.sh should not be run directly" >&2
|
||||
echo "Run ./build-release.sh instead" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Script directory must be set by sourcing script
|
||||
if [[ -z "${SCRIPT_DIR:-}" ]]; then
|
||||
echo "Error: SCRIPT_DIR not set by sourcing script" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# is_skipped_lib() — glibc/loader skip list, shared verbatim with
|
||||
# bundle-binaries.sh so the bundler and the audit never disagree.
|
||||
source "$SCRIPT_DIR/bundled-lib-skiplist.sh"
|
||||
|
||||
# Colors
|
||||
if [[ -z "${RED:-}" ]]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
fi
|
||||
|
||||
# Check that required variables are set
|
||||
if [[ -z "${PROJECT_DIR:-}" ]]; then
|
||||
echo "Error: PROJECT_DIR not set by sourcing script" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${PLATFORM:-}" ]]; then
|
||||
echo "Error: PLATFORM not set by sourcing script" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure platform is lowercase
|
||||
PLATFORM="$(echo "$PLATFORM" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
# Validate platform
|
||||
if [[ ! "$PLATFORM" =~ ^(linux|macos|windows)$ ]]; then
|
||||
echo -e "${RED}Error: Invalid platform: $PLATFORM${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Configuration file
|
||||
CONFIG="$PROJECT_DIR/.build-config.json"
|
||||
PARSE_CONFIG="$SCRIPT_DIR/parse-build-config.py"
|
||||
|
||||
# Check config file
|
||||
if [[ ! -f "$CONFIG" ]]; then
|
||||
echo -e "${RED}Error: $CONFIG not found${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! python3 "$PARSE_CONFIG" "$CONFIG" >/dev/null; then
|
||||
echo -e "${RED}Error: $CONFIG is not valid JSON${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
get_cfg() { python3 "$PARSE_CONFIG" "$CONFIG" "$1"; }
|
||||
|
||||
# --- Platform functions (to be implemented by platform scripts) ---
|
||||
# Platform scripts MUST implement these three functions:
|
||||
# install_system_deps()
|
||||
# bundle_python_impl()
|
||||
# bundle_binaries_impl()
|
||||
|
||||
# --- Common Build Steps ---
|
||||
|
||||
# Clone Slopsmith and plugins (shared across all platforms)
|
||||
clone_slopsmith() {
|
||||
# RUNNER_TEMP on Windows runners is a native Windows path
|
||||
# (e.g. `D:\a\_temp`) that Git Bash / MSYS tools don't reliably
|
||||
# treat as a filesystem path. POSIX `/tmp/slopsmith` is the
|
||||
# default; if a non-POSIX environment really wants RUNNER_TEMP, it
|
||||
# can pass an explicit clone_dir argument (resolved via cygpath -u
|
||||
# on Windows if needed).
|
||||
local clone_dir="${1:-/tmp/slopsmith}"
|
||||
|
||||
# Skip if already set for local development
|
||||
if [[ -n "${SLOPSMITH_DIR:-}" ]] && [[ -d "$SLOPSMITH_DIR" ]]; then
|
||||
echo "Using existing SLOPSMITH_DIR: $SLOPSMITH_DIR"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Make the clone re-runnable: a leftover dir from a previous failed
|
||||
# build would otherwise abort `git clone`. CI runners start fresh so
|
||||
# this is purely a quality-of-life fix for local re-runs.
|
||||
if [[ -d "$clone_dir" ]]; then
|
||||
rm -rf "$clone_dir"
|
||||
fi
|
||||
|
||||
# SLOPSMITH_REF selects the core branch/tag to bundle (set by the
|
||||
# Build workflow's slopsmith_ref input). Defaults to main so local
|
||||
# builds and the push/tag CI paths behave exactly as before.
|
||||
# --branch accepts either a branch or a tag, both shallow-cloneable.
|
||||
local slopsmith_ref="${SLOPSMITH_REF:-main}"
|
||||
echo "Cloning Slopsmith repository (ref: ${slopsmith_ref})..."
|
||||
git clone --depth 1 --branch "$slopsmith_ref" https://github.com/slopsmith/slopsmith.git "$clone_dir"
|
||||
|
||||
# Remove broken symlinks from plugins dir
|
||||
find "$clone_dir/plugins" -maxdepth 1 -type l -delete 2>/dev/null || true
|
||||
|
||||
# Clone bundled plugins. Format per entry:
|
||||
# <owner>/<repo>[@<branch>][:<dirname>]
|
||||
# Dirname defaults to <repo> minus the "slopsmith-plugin-" prefix
|
||||
# with hyphens replaced by underscores (slopsmith treats plugin
|
||||
# directories as Python module names, which can't contain dashes).
|
||||
# Provide an explicit dirname after a colon for repos that don't
|
||||
# follow the slopsmith-plugin-* naming convention. An optional
|
||||
# @<branch> clones a non-default branch (used to ship in-review
|
||||
# plugin work in a feature-branch test build).
|
||||
cd "$clone_dir/plugins"
|
||||
local plugins=(
|
||||
# byrongamatos plugins
|
||||
slopsmith/slopsmith-plugin-drum-highway-3d
|
||||
slopsmith/slopsmith-plugin-drums
|
||||
slopsmith/slopsmith-plugin-editor
|
||||
slopsmith/slopsmith-plugin-flappy-bend
|
||||
slopsmith/slopsmith-plugin-fretboard
|
||||
slopsmith/slopsmith-plugin-jumpingtab
|
||||
slopsmith/slopsmith-plugin-keys-highway-3d
|
||||
slopsmith/slopsmith-plugin-lyrics-karaoke
|
||||
slopsmith/slopsmith-plugin-metronome
|
||||
slopsmith/slopsmith-plugin-midi
|
||||
slopsmith/slopsmith-plugin-multiplayer
|
||||
slopsmith/slopsmith-plugin-musicxml-import
|
||||
slopsmith/slopsmith-plugin-nam-tone
|
||||
slopsmith/slopsmith-plugin-notedetect
|
||||
slopsmith/slopsmith-plugin-piano
|
||||
slopsmith/slopsmith-plugin-practice
|
||||
slopsmith/slopsmith-plugin-profileimport
|
||||
slopsmith/slopsmith-plugin-sectionmap
|
||||
slopsmith/slopsmith-plugin-setlist
|
||||
slopsmith/slopsmith-plugin-staffview
|
||||
slopsmith/slopsmith-plugin-stepmode
|
||||
slopsmith/slopsmith-plugin-studio
|
||||
slopsmith/slopsmith-plugin-tabimport
|
||||
slopsmith/slopsmith-plugin-tabview
|
||||
slopsmith/slopsmith-plugin-tones
|
||||
slopsmith/slopsmith-plugin-tutorials
|
||||
# Community plugins
|
||||
alleexx/slopsmith-plugin-transpose-chords
|
||||
ChrisBeWithYou/slopsmith-plugin-slopscale
|
||||
DeathlySin/slopsmith-plugin-song-preview
|
||||
Jafz2001/slopsmith-plugin-nam-rig-builder
|
||||
masc0t/slopsmith-plugin-find-more
|
||||
masc0t/slopsmith-plugin-invert-highway
|
||||
masc0t/slopsmith-plugin-themes
|
||||
masc0t/slopsmith-update-manager:update_manager
|
||||
slopsmith/slopsmith-plugin-stem-mixer
|
||||
topkoa/slopsmith-plugin-guitar-theory
|
||||
topkoa/slopsmith-plugin-sloppak-converter
|
||||
topkoa/slopsmith-plugin-splitscreen
|
||||
topkoa/slopsmith-plugin-stems
|
||||
)
|
||||
|
||||
local total=0
|
||||
local cloned=0
|
||||
for entry in "${plugins[@]}"; do
|
||||
total=$((total + 1))
|
||||
# Split off an optional ":<dirname>" then an optional "@<branch>".
|
||||
# Git branch names can't contain ':' so the dirname split is safe
|
||||
# to do first; what's left is "<owner>/<repo>" or "<owner>/<repo>@<branch>".
|
||||
local spec="$entry" dirname="" branch=""
|
||||
if [[ "$spec" == *:* ]]; then
|
||||
dirname="${spec##*:}"
|
||||
spec="${spec%%:*}"
|
||||
fi
|
||||
local owner_repo="$spec"
|
||||
if [[ "$spec" == *@* ]]; then
|
||||
branch="${spec##*@}"
|
||||
owner_repo="${spec%%@*}"
|
||||
fi
|
||||
if [[ -z "$dirname" ]]; then
|
||||
dirname="${owner_repo##*/}"
|
||||
dirname="${dirname#slopsmith-plugin-}"
|
||||
dirname="${dirname//-/_}"
|
||||
fi
|
||||
local clone_args=(--depth 1)
|
||||
[[ -n "$branch" ]] && clone_args+=(--branch "$branch")
|
||||
if git clone "${clone_args[@]}" "https://github.com/${owner_repo}.git" "$dirname" 2>/dev/null; then
|
||||
cloned=$((cloned + 1))
|
||||
else
|
||||
echo " skipped ${owner_repo}${branch:+@$branch}"
|
||||
fi
|
||||
done
|
||||
|
||||
export SLOPSMITH_DIR="$clone_dir"
|
||||
echo "Cloned ${cloned} of ${total} plugins"
|
||||
cd - >/dev/null
|
||||
}
|
||||
|
||||
|
||||
step=1
|
||||
|
||||
echo_validate_env() {
|
||||
echo -e "${BLUE}Step $step: Validating environment${NC}"
|
||||
step=$((step + 1))
|
||||
}
|
||||
|
||||
echo_step() {
|
||||
echo -e "${BLUE}Step $step: $1${NC}"
|
||||
step=$((step + 1))
|
||||
}
|
||||
|
||||
echo_summary() {
|
||||
echo -e "${GREEN}✓${NC} $1"
|
||||
}
|
||||
|
||||
echo_warning() {
|
||||
echo -e "${YELLOW}!${NC} $1"
|
||||
}
|
||||
|
||||
echo_error() {
|
||||
echo -e "${RED}✗${NC} $1"
|
||||
}
|
||||
|
||||
validate_environment() {
|
||||
echo_validate_env
|
||||
|
||||
NODE_VERSION=$(get_cfg .versions.node)
|
||||
PYTHON_VERSION=$(get_cfg .versions.python)
|
||||
|
||||
echo "Platform: $PLATFORM"
|
||||
echo "Node: $NODE_VERSION"
|
||||
echo "Python: $PYTHON_VERSION"
|
||||
echo ""
|
||||
|
||||
# Check Node.js
|
||||
if command -v node &>/dev/null; then
|
||||
INSTALLED_NODE=$(node -p "process.version.replace('v', '')")
|
||||
echo_summary "Found Node.js $INSTALLED_NODE"
|
||||
else
|
||||
echo_error "Node.js not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check Python 3
|
||||
if command -v python3 &>/dev/null; then
|
||||
INSTALLED_PYTHON=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
|
||||
echo_summary "Found Python $INSTALLED_PYTHON"
|
||||
else
|
||||
echo_error "Python 3 not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
install_npm_deps() {
|
||||
echo_step "Installing npm dependencies"
|
||||
npm install
|
||||
echo_summary "npm dependencies installed"
|
||||
echo ""
|
||||
}
|
||||
|
||||
build_native_addons() {
|
||||
echo_step "Building native addons (audio engine)"
|
||||
npm run build:native
|
||||
echo_summary "Native addons built"
|
||||
echo ""
|
||||
}
|
||||
|
||||
bundle_slopsmith() {
|
||||
echo_step "Bundling Slopsmith and plugins"
|
||||
npm run bundle:slopsmith
|
||||
echo_summary "Slopsmith bundled"
|
||||
echo ""
|
||||
}
|
||||
|
||||
bundle_python() {
|
||||
mkdir -p "$PROJECT_DIR/resources"
|
||||
bundle_python_impl
|
||||
echo_summary "Python runtime bundled"
|
||||
echo ""
|
||||
}
|
||||
|
||||
bundle_binaries() {
|
||||
mkdir -p "$PROJECT_DIR/resources/bin"
|
||||
bundle_binaries_impl
|
||||
echo_summary "System binaries bundled"
|
||||
echo ""
|
||||
}
|
||||
|
||||
verify_bundled_binaries() {
|
||||
# Smoke test: verify bundled binaries are executable and can run
|
||||
local bin_dir="$PROJECT_DIR/resources/bin"
|
||||
local ext=""
|
||||
if [[ "$PLATFORM" == "windows" ]]; then
|
||||
ext=".exe"
|
||||
fi
|
||||
|
||||
echo_step "Verifying bundled binaries"
|
||||
|
||||
# Verify fluidsynth: supports --version
|
||||
local fs_path="$bin_dir/fluidsynth${ext}"
|
||||
if [[ ! -f "$fs_path" ]]; then
|
||||
echo_error "Missing bundled binary: $fs_path"
|
||||
exit 1
|
||||
fi
|
||||
if ! "$fs_path" --version >/dev/null 2>&1; then
|
||||
echo_error "Binary fluidsynth failed to execute"
|
||||
exit 1
|
||||
fi
|
||||
echo " ✓ fluidsynth"
|
||||
|
||||
# Verify ffmpeg: supports -version
|
||||
local ff_path="$bin_dir/ffmpeg${ext}"
|
||||
if [[ ! -f "$ff_path" ]]; then
|
||||
echo_error "Missing bundled binary: $ff_path"
|
||||
exit 1
|
||||
fi
|
||||
if ! "$ff_path" -version >/dev/null 2>&1; then
|
||||
echo_error "Binary ffmpeg failed to execute"
|
||||
exit 1
|
||||
fi
|
||||
echo " ✓ ffmpeg"
|
||||
|
||||
# Verify ffprobe: demucs spawns it before ffmpeg to read stream
|
||||
# metadata. Required on every platform because falling through to a
|
||||
# host-installed ffprobe makes stem splitting work on the build host
|
||||
# and silently fail on user machines without it.
|
||||
local ffp_path="$bin_dir/ffprobe${ext}"
|
||||
if [[ ! -f "$ffp_path" ]]; then
|
||||
echo_error "Missing bundled binary: $ffp_path"
|
||||
exit 1
|
||||
fi
|
||||
if ! "$ffp_path" -version >/dev/null 2>&1; then
|
||||
echo_error "Binary ffprobe failed to execute"
|
||||
exit 1
|
||||
fi
|
||||
echo " ✓ ffprobe"
|
||||
|
||||
# Verify vgmstream-cli: doesn't have --version, check it produces output with version
|
||||
local vgm_path="$bin_dir/vgmstream-cli${ext}"
|
||||
echo " Checking vgmstream-cli at: $vgm_path"
|
||||
if [[ ! -f "$vgm_path" ]]; then
|
||||
echo_error "Missing bundled binary: $vgm_path"
|
||||
exit 1
|
||||
fi
|
||||
echo " File exists, checking permissions:"
|
||||
ls -la "$vgm_path"
|
||||
# `file` is a diagnostic-only call — keep it best-effort so a minimal
|
||||
# base image (e.g. the Linux Docker builder, which doesn't ship the
|
||||
# libmagic-backed `file` binary) doesn't fail the build over a debug
|
||||
# line. The actual smoke test is the run-and-grep below.
|
||||
if command -v file >/dev/null 2>&1; then
|
||||
echo " File type:"
|
||||
file "$vgm_path"
|
||||
fi
|
||||
echo " Attempting to run vgmstream-cli..."
|
||||
local vgm_output
|
||||
local vgm_exit_code
|
||||
# vgmstream-cli with no args prints its version header then exits 1.
|
||||
# Capture the exit code via the if-branch so `set -e` doesn't trip
|
||||
# AND vgm_exit_code reflects the binary's real status (not the `|| true`
|
||||
# short-circuit that the previous form ended up reporting).
|
||||
if vgm_output=$("$vgm_path" 2>&1); then
|
||||
vgm_exit_code=0
|
||||
else
|
||||
vgm_exit_code=$?
|
||||
fi
|
||||
echo " Exit code: $vgm_exit_code"
|
||||
echo " Raw output:"
|
||||
echo "$vgm_output" | head -20
|
||||
echo " Checking if output matches expected pattern..."
|
||||
if [[ -z "$vgm_output" ]]; then
|
||||
echo_error "Binary vgmstream-cli produced no output"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! "$vgm_output" =~ vgmstream.*CLI.*decoder ]]; then
|
||||
echo_error "Binary vgmstream-cli produced unexpected output"
|
||||
echo " Expected pattern: vgmstream.*CLI.*decoder"
|
||||
echo " Actual output (first 500 chars):"
|
||||
echo "${vgm_output:0:500}"
|
||||
exit 1
|
||||
fi
|
||||
echo " ✓ vgmstream-cli"
|
||||
|
||||
# On Linux, audit each bundled ELF binary's NEEDED entries: every
|
||||
# SONAME must either be in the glibc/loader skip list (delegated to
|
||||
# the user's libc) or sit next to the binary in resources/bin/.
|
||||
# The `-version` smoke tests above can't catch this on the build
|
||||
# host because /usr/lib happens to satisfy the deps — but on a user
|
||||
# machine with a different ffmpeg ABI the load fails at runtime
|
||||
# (issue #68 on Fedora 44 / Arch).
|
||||
if [[ "$PLATFORM" == "linux" ]]; then
|
||||
if ! command -v readelf >/dev/null 2>&1; then
|
||||
echo_error "readelf not found (apt: binutils) - required to audit bundled binaries' shared library deps"
|
||||
exit 1
|
||||
fi
|
||||
for bin in fluidsynth ffmpeg ffprobe vgmstream-cli; do
|
||||
audit_bundled_deps "$bin_dir/$bin" "$bin_dir" || exit 1
|
||||
done
|
||||
# Also audit each bundled .so's own NEEDED entries. ldd-on-the-top-
|
||||
# level-binary usually resolves the full transitive closure, but
|
||||
# dlopen-resolved deps and interposer libs can slip through that
|
||||
# traversal. Auditing every bundled .so closes the gap.
|
||||
for so in "$bin_dir"/*.so*; do
|
||||
[ -f "$so" ] || continue
|
||||
audit_bundled_deps "$so" "$bin_dir" || exit 1
|
||||
done
|
||||
echo " ✓ shared-library audit"
|
||||
fi
|
||||
|
||||
echo_summary "All bundled binaries verified"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Asserts every NEEDED SONAME in $1 is either in the glibc skip list
|
||||
# or present as a file in $2 (the bundle directory). Returns non-zero
|
||||
# with a specific error if any SONAME is unsatisfied.
|
||||
audit_bundled_deps() {
|
||||
local bin_path="$1"
|
||||
local bundle_dir="$2"
|
||||
local missing=()
|
||||
local soname
|
||||
while IFS= read -r soname; do
|
||||
[ -n "$soname" ] || continue
|
||||
is_skipped_lib "$soname" && continue
|
||||
[ -f "$bundle_dir/$soname" ] && continue
|
||||
missing+=("$soname")
|
||||
done < <(readelf -d "$bin_path" 2>/dev/null | awk -F'[][]' '/\(NEEDED\)/ {print $2}')
|
||||
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
echo_error "Bundled $(basename "$bin_path") needs shared libs that are not in resources/bin/:"
|
||||
for soname in "${missing[@]}"; do
|
||||
echo " - $soname"
|
||||
done
|
||||
echo " Fix: extend scripts/bundle-binaries.sh so these SONAMEs are bundled (or add them to the glibc skip list if they MUST come from the host libc)."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
bundle_soundfont() {
|
||||
echo_step "Bundling default soundfont"
|
||||
bash "$SCRIPT_DIR/bundle-soundfont.sh"
|
||||
echo_summary "Soundfont bundled"
|
||||
echo ""
|
||||
}
|
||||
|
||||
build_typescript() {
|
||||
echo_step "Building TypeScript"
|
||||
npm run build:ts
|
||||
echo_summary "TypeScript built"
|
||||
echo ""
|
||||
}
|
||||
|
||||
package_application() {
|
||||
echo_step "Packaging application"
|
||||
# Call electron-builder directly. The package.json `dist:*` scripts
|
||||
# chain `build:native && bundle && build:ts && electron-builder`,
|
||||
# but build-common.sh's main() has already run all three of those
|
||||
# explicitly. Going through `npm run dist:*` would re-run them, which
|
||||
# on macOS is wasteful: build:native rebuilds the native audio addon
|
||||
# that build-common.sh already produced.
|
||||
#
|
||||
# `--publish never` is required: on a tag push electron-builder
|
||||
# defaults to auto-publishing to GitHub Releases and then errors out
|
||||
# without GH_TOKEN. The workflow has a dedicated `release` job that
|
||||
# publishes via softprops/action-gh-release after artifact upload —
|
||||
# the build job just needs to produce artifacts, not publish them.
|
||||
local builder_platform
|
||||
case "$PLATFORM" in
|
||||
linux) builder_platform="--linux" ;;
|
||||
macos) builder_platform="--mac" ;;
|
||||
windows) builder_platform="--win" ;;
|
||||
*)
|
||||
echo_error "Unsupported packaging platform: $PLATFORM"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
npx electron-builder "$builder_platform" --publish never
|
||||
echo_summary "Application packaged"
|
||||
echo ""
|
||||
}
|
||||
|
||||
verify_artifacts() {
|
||||
echo_step "Verifying artifacts"
|
||||
|
||||
ARTIFACTS_FOUND=0
|
||||
|
||||
# Read patterns into array (avoid process substitution for CI compatibility)
|
||||
patterns=()
|
||||
tempfile=$(mktemp)
|
||||
get_expected_artifacts > "$tempfile"
|
||||
cat "$tempfile" >&2
|
||||
while IFS= read -r line; do
|
||||
patterns+=("$line")
|
||||
done < "$tempfile"
|
||||
rm -f "$tempfile"
|
||||
for pattern in "${patterns[@]}"; do
|
||||
shopt -s nullglob
|
||||
files=($pattern)
|
||||
shopt -u nullglob
|
||||
if [ ${#files[@]} -gt 0 ]; then
|
||||
ARTIFACTS_FOUND=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $ARTIFACTS_FOUND -eq 1 ]]; then
|
||||
echo_summary "Build successful!"
|
||||
echo ""
|
||||
ls -lh "$PROJECT_DIR/release/" 2>/dev/null | grep -v "^total" | awk 'NR > 1' | head -10 || true
|
||||
else
|
||||
echo_error "No artifacts found"
|
||||
if [[ -d "$PROJECT_DIR/release" ]]; then
|
||||
echo "Contents of release/:"
|
||||
ls -la "$PROJECT_DIR/release/" 2>&1 || echo "(directory empty)"
|
||||
else
|
||||
echo "release/ directory doesn't exist"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Main entry point - platform scripts call this
|
||||
main() {
|
||||
local start_time=$(date +%s)
|
||||
|
||||
case "$PLATFORM" in
|
||||
linux|macos|windows)
|
||||
;;
|
||||
*)
|
||||
echo_error "Unsupported platform: $PLATFORM"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Verify that all required functions are defined by the sourcing platform script
|
||||
local missing_functions=()
|
||||
local required_funcs=(
|
||||
install_system_deps
|
||||
bundle_python_impl
|
||||
bundle_binaries_impl
|
||||
get_expected_artifacts
|
||||
)
|
||||
|
||||
for func in "${required_funcs[@]}"; do
|
||||
if ! type "$func" &>/dev/null; then
|
||||
missing_functions+=("$func")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#missing_functions[@]} -gt 0 ]]; then
|
||||
echo_error "Required functions not defined by platform script:"
|
||||
for func in "${missing_functions[@]}"; do
|
||||
echo " - $func"
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
validate_environment
|
||||
install_system_deps
|
||||
install_npm_deps
|
||||
# clone_slopsmith provides the slopsmith core + plugins that
|
||||
# bundle_slopsmith packages into the app; run it before the build steps
|
||||
# that consume $SLOPSMITH_DIR.
|
||||
clone_slopsmith
|
||||
build_native_addons
|
||||
bundle_slopsmith
|
||||
bundle_python
|
||||
bundle_binaries
|
||||
verify_bundled_binaries
|
||||
bundle_soundfont
|
||||
build_typescript
|
||||
package_application
|
||||
verify_artifacts
|
||||
|
||||
local end_time=$(date +%s)
|
||||
local duration=$((end_time - start_time))
|
||||
|
||||
echo -e "${GREEN}✓${NC} Build complete for $PLATFORM in ${duration}s"
|
||||
echo "Output: $PROJECT_DIR/release/"
|
||||
}
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
#!/bin/bash
|
||||
# Docker-based Linux build wrapper
|
||||
# Runs build-linux-ubuntu.sh inside a reproducible container
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
DEVCONTAINER_DIR="$PROJECT_DIR/.devcontainer"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo "=== Slopsmith Desktop Docker Build ==="
|
||||
echo ""
|
||||
echo "This script provides reproducible Linux builds by running"
|
||||
echo "build-linux-ubuntu.sh inside a Docker container."
|
||||
echo ""
|
||||
|
||||
# Check prerequisites
|
||||
echo -e "${BLUE}Checking prerequisites...${NC}"
|
||||
|
||||
if ! command -v docker &>/dev/null; then
|
||||
echo -e "${RED}Error: Docker is not installed${NC}" >&2
|
||||
echo "Install: https://docs.docker.com/get-docker/" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker info &>/dev/null; then
|
||||
echo -e "${RED}Error: Docker daemon is not running${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓${NC} Docker available"
|
||||
echo ""
|
||||
|
||||
# Build container image
|
||||
echo -e "${BLUE}Building container image...${NC}"
|
||||
echo " (This will take a few minutes on first run)"
|
||||
echo ""
|
||||
|
||||
docker build \
|
||||
-f "$DEVCONTAINER_DIR/Dockerfile" \
|
||||
-t slopsmith-ubuntu-builder \
|
||||
"$PROJECT_DIR"
|
||||
# `set -e` at the top of this script already aborts on a failed
|
||||
# `docker build` — no manual `$?` check needed (and the check that
|
||||
# was here would in practice be unreachable).
|
||||
|
||||
echo -e "${GREEN}✓${NC} Container image built"
|
||||
echo ""
|
||||
|
||||
# Clear stale CMake build cache. CMakeCache.txt bakes in the build path;
|
||||
# when the project is mounted at a different path inside the container the
|
||||
# paths don't match and cmake aborts. A clean build/ guarantees consistency.
|
||||
if [[ -d "$PROJECT_DIR/build" ]]; then
|
||||
echo -e "${BLUE}Clearing stale CMake cache...${NC}"
|
||||
rm -rf "$PROJECT_DIR/build"
|
||||
fi
|
||||
|
||||
# Generate unique container name
|
||||
CONTAINER_NAME="slopsmith-build-$(date +%s)-$$-$RANDOM"
|
||||
|
||||
echo -e "${BLUE}Running build in container...${NC}"
|
||||
echo -e "${BLUE}Container name:${NC} $CONTAINER_NAME"
|
||||
echo ""
|
||||
echo "The container will be preserved after the build to allow debugging."
|
||||
echo "Clean up when done:"
|
||||
echo " docker stop $CONTAINER_NAME && docker rm $CONTAINER_NAME"
|
||||
echo ""
|
||||
|
||||
set +e
|
||||
docker run \
|
||||
--name "$CONTAINER_NAME" \
|
||||
-v "$PROJECT_DIR:/workspace" \
|
||||
-w /workspace \
|
||||
-e ELECTRON_CACHE=/home/vscode/.cache/electron \
|
||||
-e ELECTRON_BUILDER_CACHE=/home/vscode/.cache/electron-builder \
|
||||
-e GIT_TERMINAL_PROMPT=0 \
|
||||
-t \
|
||||
slopsmith-ubuntu-builder \
|
||||
bash -c './scripts/build-linux-ubuntu.sh'
|
||||
BUILD_EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
echo ""
|
||||
if [[ $BUILD_EXIT_CODE -eq 0 ]]; then
|
||||
echo -e "${GREEN}✓${NC} Build completed successfully!"
|
||||
else
|
||||
echo -e "${RED}✗${NC} Build failed (exit code: $BUILD_EXIT_CODE)"
|
||||
echo ""
|
||||
echo "To debug:"
|
||||
echo " docker exec -it $CONTAINER_NAME /bin/bash"
|
||||
echo " docker logs $CONTAINER_NAME"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
exit $BUILD_EXIT_CODE
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
# Native Ubuntu build script
|
||||
# Assumes host is running Ubuntu Linux
|
||||
# Uses native Ubuntu packages (apt) and copies system Python
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
CONFIG="$PROJECT_DIR/.build-config.json"
|
||||
|
||||
# Platform identifier
|
||||
export PLATFORM="linux"
|
||||
|
||||
# Check we're on Linux
|
||||
if [[ ! -f /etc/os-release ]] || ! grep -q "ubuntu\|debian" /etc/os-release; then
|
||||
echo "Note: This script is optimized for Ubuntu/Debian but may work on other distributions."
|
||||
echo "For non-Ubuntu Linux, system dependencies might need manual installation."
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "=== Slopsmith Desktop Ubuntu Native Build ==="
|
||||
echo ""
|
||||
|
||||
# Color setup
|
||||
export RED='\033[0;31m'
|
||||
export GREEN='\033[0;32m'
|
||||
export YELLOW='\033[1;33m'
|
||||
export BLUE='\033[0;34m'
|
||||
export NC='\033[0m'
|
||||
|
||||
# Source common build logic
|
||||
source "$SCRIPT_DIR/build-common.sh"
|
||||
|
||||
# Platform-specific: Return expected artifact patterns
|
||||
get_expected_artifacts() {
|
||||
printf "%s\n" "$PROJECT_DIR/release/*.AppImage" "$PROJECT_DIR/release/*.deb"
|
||||
}
|
||||
|
||||
# Platform-specific: Install system dependencies
|
||||
install_system_deps() {
|
||||
if command -v sudo &>/dev/null; then
|
||||
sudo apt-get update
|
||||
PACKAGES=$(grep -v '^[[:space:]]*#' "$PROJECT_DIR/.packages/apt.txt" | grep -v '^[[:space:]]*$' | tr '\n' ' ')
|
||||
if [[ -n "$PACKAGES" ]]; then
|
||||
sudo apt-get install -y $PACKAGES
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}!${NC} sudo not available, skipping apt package installation"
|
||||
echo " Make sure build dependencies are already installed"
|
||||
fi
|
||||
}
|
||||
|
||||
# Platform-specific: Bundle Python runtime
|
||||
bundle_python_impl() {
|
||||
# Linux: use existing bundle-python.sh script
|
||||
bash "$SCRIPT_DIR/bundle-python.sh"
|
||||
}
|
||||
|
||||
# Platform-specific: Bundle system binaries
|
||||
bundle_binaries_impl() {
|
||||
# Linux: use existing bundle-binaries.sh script
|
||||
bash "$SCRIPT_DIR/bundle-binaries.sh"
|
||||
}
|
||||
|
||||
# Run the build
|
||||
main "$@"
|
||||
Executable
+419
@@ -0,0 +1,419 @@
|
||||
#!/bin/bash
|
||||
# Native macOS build script
|
||||
# Uses Homebrew for dependencies and system Python
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
CONFIG="$PROJECT_DIR/.build-config.json"
|
||||
|
||||
# Platform identifier
|
||||
export PLATFORM="macos"
|
||||
|
||||
# Check we're on macOS
|
||||
if [[ "$OSTYPE" != "darwin"* ]]; then
|
||||
echo "Error: This script is for macOS only" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Slopsmith Desktop macOS Build ==="
|
||||
echo ""
|
||||
|
||||
# Disable electron-builder's keychain-identity auto-discovery on unsigned
|
||||
# builds. Without this, electron-builder picks the first codesigning
|
||||
# identity it finds (often an "Apple Development" cert from Xcode) and
|
||||
# tries to sign with it — which both produces unusable artifacts AND
|
||||
# fails when Slopsmith.app contains paths the Apple Development cert
|
||||
# can't sign. Signed CI builds set APPLE_SIGNING_IDENTITY / CSC_NAME, so
|
||||
# this guard only triggers for local unsigned dev builds.
|
||||
if [[ -z "${APPLE_SIGNING_IDENTITY:-}" && -z "${CSC_NAME:-}" && -z "${CSC_LINK:-}" ]]; then
|
||||
export CSC_IDENTITY_AUTO_DISCOVERY=false
|
||||
fi
|
||||
|
||||
# Derive CSC_NAME (electron-builder's identity name) from
|
||||
# APPLE_SIGNING_IDENTITY. codesign accepts the full identity string with
|
||||
# "Developer ID Application:" prefix; electron-builder rejects that
|
||||
# prefix and wants the bare team-name + team-id form. Strip the prefix
|
||||
# once here so the rest of the build (sign-macos-binaries.sh and
|
||||
# electron-builder) can each consume the form they expect.
|
||||
if [[ -z "${CSC_NAME:-}" && -n "${APPLE_SIGNING_IDENTITY:-}" ]]; then
|
||||
export CSC_NAME="${APPLE_SIGNING_IDENTITY#Developer ID Application: }"
|
||||
fi
|
||||
|
||||
# Color setup
|
||||
export RED='\033[0;31m'
|
||||
export GREEN='\033[0;32m'
|
||||
export YELLOW='\033[1;33m'
|
||||
export BLUE='\033[0;34m'
|
||||
export NC='\033[0m'
|
||||
|
||||
# Source common build logic
|
||||
source "$SCRIPT_DIR/build-common.sh"
|
||||
|
||||
# Platform-specific: Install system dependencies
|
||||
install_system_deps() {
|
||||
if command -v brew &>/dev/null; then
|
||||
PACKAGES=$(grep -v '^[[:space:]]*#' "$PROJECT_DIR/.packages/brew.txt" | grep -v '^[[:space:]]*$' | tr '\n' ' ')
|
||||
if [[ -n "$PACKAGES" ]]; then
|
||||
brew install $PACKAGES
|
||||
fi
|
||||
else
|
||||
echo "Error: Homebrew not found. Install from https://brew.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Platform-specific: Bundle Python runtime
|
||||
#
|
||||
# Uses python-build-standalone (Astral) — a fully relocatable CPython
|
||||
# distribution built specifically for redistribution. Avoids every
|
||||
# hazard of trying to copy a Homebrew framework: no PEP 668 marker, no
|
||||
# install_name_tool dance, no broken site-packages symlink, sys.prefix
|
||||
# correctly resolves to the bundle's location at runtime.
|
||||
bundle_python_impl() {
|
||||
local config_py
|
||||
config_py=$(python3 "$SCRIPT_DIR/parse-build-config.py" "$PROJECT_DIR/.build-config.json" .versions.python)
|
||||
local py_mm="${config_py%.*}"
|
||||
|
||||
local arch
|
||||
arch=$(uname -m)
|
||||
local config_key
|
||||
case "$arch" in
|
||||
arm64|aarch64) config_key="python_standalone_macos_arm64" ;;
|
||||
x86_64) config_key="python_standalone_macos_x64" ;;
|
||||
*)
|
||||
echo "Error: unsupported macOS arch: $arch" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
local pbs_url pbs_sha
|
||||
pbs_url=$(python3 "$SCRIPT_DIR/parse-build-config.py" "$PROJECT_DIR/.build-config.json" ".external.${config_key}.url")
|
||||
pbs_sha=$(python3 "$SCRIPT_DIR/parse-build-config.py" "$PROJECT_DIR/.build-config.json" ".external.${config_key}.sha256")
|
||||
|
||||
local runtime="$PROJECT_DIR/resources/python/runtime"
|
||||
local tarball="/tmp/cpython-${config_py}-macos-${arch}.tar.gz"
|
||||
|
||||
mkdir -p "$PROJECT_DIR/resources/python"
|
||||
rm -rf "$runtime"
|
||||
|
||||
if [[ ! -f "$tarball" ]] || ! shasum -a 256 "$tarball" | awk '{print $1}' | grep -qx "$pbs_sha"; then
|
||||
echo " Downloading python-build-standalone ${config_py} (${arch})"
|
||||
curl -sL --fail --retry 5 --retry-delay 5 --retry-all-errors "$pbs_url" -o "$tarball"
|
||||
fi
|
||||
local actual_sha
|
||||
actual_sha=$(shasum -a 256 "$tarball" | awk '{print $1}')
|
||||
if [[ "$actual_sha" != "$pbs_sha" ]]; then
|
||||
echo "Error: python-build-standalone tarball SHA256 mismatch" >&2
|
||||
echo " expected: $pbs_sha" >&2
|
||||
echo " got: $actual_sha" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# PBS tarballs extract to a top-level `python/` dir; rename to
|
||||
# `runtime` so the rest of the build (and python.ts) finds the
|
||||
# interpreter at resources/python/runtime/bin/python3.
|
||||
local extract_dir="/tmp/pbs-extract-$$"
|
||||
rm -rf "$extract_dir"
|
||||
mkdir -p "$extract_dir"
|
||||
tar -xzf "$tarball" -C "$extract_dir"
|
||||
mv "$extract_dir/python" "$runtime"
|
||||
rm -rf "$extract_dir"
|
||||
|
||||
# PBS tarballs ship a working pip pre-installed in the bundle's
|
||||
# site-packages, and `bin/python3` is a real binary (not a symlink),
|
||||
# so the install is fully relocatable as-is.
|
||||
#
|
||||
# Install slopsmith's runtime requirements first (single source of
|
||||
# truth — drift used to silently break desktop builds whenever
|
||||
# slopsmith added a dep), then desktop-only extras. SLOPSMITH_DIR
|
||||
# is exported by clone_slopsmith() in build-common.sh; fall back
|
||||
# to local-dev paths to match bundle-slopsmith.sh's discovery so
|
||||
# this script works outside CI too.
|
||||
if [[ -z "${SLOPSMITH_DIR:-}" ]]; then
|
||||
if [[ -d "$PROJECT_DIR/../slopsmith" ]]; then
|
||||
SLOPSMITH_DIR="$PROJECT_DIR/../slopsmith"
|
||||
elif [[ -d "$HOME/Repositories/slopsmith" ]]; then
|
||||
SLOPSMITH_DIR="$HOME/Repositories/slopsmith"
|
||||
fi
|
||||
fi
|
||||
if [[ -z "${SLOPSMITH_DIR:-}" ]] || [[ ! -f "$SLOPSMITH_DIR/requirements.txt" ]]; then
|
||||
echo "ERROR: slopsmith requirements.txt not found (SLOPSMITH_DIR=${SLOPSMITH_DIR:-<unset>})." >&2
|
||||
echo " Expected SLOPSMITH_DIR to be exported by clone_slopsmith() in build-common.sh," >&2
|
||||
echo " or slopsmith cloned next to this repo." >&2
|
||||
exit 1
|
||||
fi
|
||||
"$runtime/bin/python3" -m pip install --quiet --no-cache-dir \
|
||||
-r "$SLOPSMITH_DIR/requirements.txt" 2>&1 | tail -5
|
||||
"$runtime/bin/python3" -m pip install --quiet --no-cache-dir \
|
||||
-r "$PROJECT_DIR/.packages/python.txt" 2>&1 | tail -5
|
||||
}
|
||||
|
||||
# Platform-specific: Return expected artifact patterns
|
||||
get_expected_artifacts() {
|
||||
# mac.target is "dir": electron-builder writes the unpacked
|
||||
# Slopsmith.app to release/mac-arm64/ (no .dmg/.zip). Velopack's
|
||||
# pack step turns that .app into the actual release assets. Glob
|
||||
# mac*/ so the check also passes for an x64 (mac/) or universal
|
||||
# (mac-universal/) local build — verify_artifacts expands this.
|
||||
printf "%s\n" "$PROJECT_DIR/release/mac*/Slopsmith.app"
|
||||
}
|
||||
|
||||
# Platform-specific: Bundle system binaries
|
||||
bundle_binaries_impl() {
|
||||
# macOS: copy existing binaries and bundle dependencies
|
||||
|
||||
# ffmpeg + ffprobe (static builds, NOT brew's ffmpeg).
|
||||
#
|
||||
# Homebrew's stock `ffmpeg` formula (8.1.1+) no longer ships
|
||||
# --enable-libvorbis. Sloppak conversion encodes .ogg with
|
||||
# `-c:a libvorbis`, so a brew-ffmpeg-bundled desktop app silently
|
||||
# degrades to the built-in `vorbis -strict experimental` encoder on
|
||||
# user machines. Pull the matching arch's static build instead:
|
||||
# - osxexperts.net for arm64 (Apple Silicon)
|
||||
# - evermeet.cx for x86_64 (Intel)
|
||||
# Both ship with --enable-libvorbis; URLs + SHA256 pins live in
|
||||
# .build-config.json so an upstream rebuild surfaces as a SHA
|
||||
# mismatch rather than a silent codec change. demucs needs ffprobe
|
||||
# alongside ffmpeg (it reads stream metadata before invoking the
|
||||
# encoder), so we download both from the same provider.
|
||||
local arch ff_key fp_key
|
||||
arch=$(uname -m)
|
||||
case "$arch" in
|
||||
arm64|aarch64) ff_key="ffmpeg_macos_arm64"; fp_key="ffprobe_macos_arm64" ;;
|
||||
x86_64) ff_key="ffmpeg_macos_x64"; fp_key="ffprobe_macos_x64" ;;
|
||||
*)
|
||||
echo "Error: unsupported macOS arch: $arch" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
local bin_dir="$PROJECT_DIR/resources/bin"
|
||||
mkdir -p "$bin_dir"
|
||||
|
||||
download_and_install_macos_ffmpeg_tool() {
|
||||
# $1 = tool name (ffmpeg|ffprobe), $2 = config key
|
||||
local tool="$1" key="$2"
|
||||
local url sha tarball
|
||||
url=$(python3 "$SCRIPT_DIR/parse-build-config.py" "$CONFIG" ".external.${key}.url")
|
||||
sha=$(python3 "$SCRIPT_DIR/parse-build-config.py" "$CONFIG" ".external.${key}.sha256")
|
||||
tarball="/tmp/${tool}-macos-${arch}.zip"
|
||||
|
||||
if [[ ! -f "$tarball" ]] || ! shasum -a 256 "$tarball" | awk '{print $1}' | grep -qx "$sha"; then
|
||||
echo " Downloading $tool from $url"
|
||||
curl -sL --fail --retry 5 --retry-delay 5 --retry-all-errors "$url" -o "$tarball"
|
||||
fi
|
||||
local actual_sha
|
||||
actual_sha=$(shasum -a 256 "$tarball" | awk '{print $1}')
|
||||
if [[ "$actual_sha" != "$sha" ]]; then
|
||||
echo "Error: $tool zip SHA256 mismatch — upstream rebuilt under the same URL" >&2
|
||||
echo " expected: $sha" >&2
|
||||
echo " got: $actual_sha" >&2
|
||||
echo " url: $url" >&2
|
||||
echo "Update .external.${key}.sha256 in .build-config.json after verifying the new binary." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local extract_dir="/tmp/${tool}-extract-$$"
|
||||
rm -rf "$extract_dir"
|
||||
mkdir -p "$extract_dir"
|
||||
# `find` after unzip tolerates either layout (osxexperts puts
|
||||
# the binary at the zip root, evermeet does too at the moment,
|
||||
# but the spec doesn't promise it). The osxexperts zip also
|
||||
# ships __MACOSX/._* resource forks alongside the binary; the
|
||||
# path filter avoids picking those up as the result.
|
||||
unzip -q -o "$tarball" -d "$extract_dir"
|
||||
local found
|
||||
found=$(find "$extract_dir" -type f -name "$tool" -not -path '*/__MACOSX/*' | head -1)
|
||||
if [[ -z "$found" ]]; then
|
||||
echo "Error: '$tool' binary not found after unzipping $tarball — upstream layout may have changed." >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "$found" "$bin_dir/$tool"
|
||||
chmod +x "$bin_dir/$tool"
|
||||
# Static builds from third-party sites carry the macOS quarantine
|
||||
# xattr by default; clear it so the binary can be exec'd by the
|
||||
# build steps that follow (sign-macos-binaries.sh would also
|
||||
# strip this, but verify_bundled_binaries runs the binary first).
|
||||
xattr -d com.apple.quarantine "$bin_dir/$tool" 2>/dev/null || true
|
||||
rm -rf "$extract_dir"
|
||||
}
|
||||
|
||||
download_and_install_macos_ffmpeg_tool ffmpeg "$ff_key"
|
||||
download_and_install_macos_ffmpeg_tool ffprobe "$fp_key"
|
||||
|
||||
# Sloppak conversion encodes .ogg with -c:a libvorbis. Verify the
|
||||
# downloaded ffmpeg actually has the encoder — both osxexperts and
|
||||
# evermeet build with --enable-libvorbis today, but pinning by SHA
|
||||
# already catches binary drift; this is the runtime guarantee. The
|
||||
# lib/sloppak_convert.py fallback is a safety net for unbundled
|
||||
# installs, not a license to ship a libvorbis-less binary.
|
||||
if ! "$bin_dir/ffmpeg" -hide_banner -encoders 2>/dev/null | grep -wq libvorbis; then
|
||||
echo "Error: bundled ffmpeg lacks libvorbis encoder. Sloppak conversion would fall back to the lower-quality built-in vorbis encoder on user machines." >&2
|
||||
echo "The pinned static build for arch=$arch ($ff_key) should include --enable-libvorbis; if it doesn't, pick a different upstream and update .build-config.json." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Apple Silicon only: the native arm64 ffmpeg static builds (osxexperts,
|
||||
# martin-riedl) omit --enable-librubberband, so Retune's pitch-shift step
|
||||
# has no `rubberband` filter and fails with "Filter not found". Bundle the
|
||||
# Intel evermeet ffmpeg (which HAS rubberband) as `ffmpeg-rubberband`;
|
||||
# lib/retune.py prefers it for that one step and it runs under Rosetta 2.
|
||||
# Everything else keeps using the native arm64 ffmpeg (no Rosetta needed).
|
||||
if [[ "$arch" == "arm64" || "$arch" == "aarch64" ]]; then
|
||||
local rb_url rb_sha rb_zip rb_extract rb_found rb_actual
|
||||
rb_url=$(python3 "$SCRIPT_DIR/parse-build-config.py" "$CONFIG" ".external.ffmpeg_macos_rubberband.url")
|
||||
rb_sha=$(python3 "$SCRIPT_DIR/parse-build-config.py" "$CONFIG" ".external.ffmpeg_macos_rubberband.sha256")
|
||||
rb_zip="/tmp/ffmpeg-rubberband-macos.zip"
|
||||
if [[ ! -f "$rb_zip" ]] || ! shasum -a 256 "$rb_zip" | awk '{print $1}' | grep -qx "$rb_sha"; then
|
||||
echo " Downloading ffmpeg-rubberband (Intel) from $rb_url"
|
||||
curl -sL --fail --retry 5 --retry-delay 5 --retry-all-errors "$rb_url" -o "$rb_zip"
|
||||
fi
|
||||
rb_actual=$(shasum -a 256 "$rb_zip" | awk '{print $1}')
|
||||
if [[ "$rb_actual" != "$rb_sha" ]]; then
|
||||
echo "Error: ffmpeg-rubberband zip SHA256 mismatch — upstream rebuilt under the same URL" >&2
|
||||
echo " expected: $rb_sha" >&2
|
||||
echo " got: $rb_actual" >&2
|
||||
echo " url: $rb_url" >&2
|
||||
echo "Update .external.ffmpeg_macos_rubberband.sha256 in .build-config.json after verifying the new binary." >&2
|
||||
exit 1
|
||||
fi
|
||||
rb_extract="/tmp/ffmpeg-rubberband-extract-$$"
|
||||
rm -rf "$rb_extract"; mkdir -p "$rb_extract"
|
||||
unzip -q -o "$rb_zip" -d "$rb_extract"
|
||||
rb_found=$(find "$rb_extract" -type f -name ffmpeg -not -path '*/__MACOSX/*' | head -1)
|
||||
if [[ -z "$rb_found" ]]; then
|
||||
echo "Error: 'ffmpeg' binary not found after unzipping $rb_zip — upstream layout may have changed." >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "$rb_found" "$bin_dir/ffmpeg-rubberband"
|
||||
chmod +x "$bin_dir/ffmpeg-rubberband"
|
||||
xattr -d com.apple.quarantine "$bin_dir/ffmpeg-rubberband" 2>/dev/null || true
|
||||
rm -rf "$rb_extract"
|
||||
# Verify librubberband via the embedded `configuration:` string with
|
||||
# `grep -a`, NOT by running the binary: the build host may be Apple
|
||||
# Silicon without Rosetta 2, so executing this Intel binary could fail
|
||||
# even though it's correct. The config string is arch-independent.
|
||||
if ! grep -a -q 'enable-librubberband' "$bin_dir/ffmpeg-rubberband"; then
|
||||
echo "Error: bundled ffmpeg-rubberband lacks --enable-librubberband — Retune pitch-shift would still fail on Apple Silicon." >&2
|
||||
echo "Pick an Intel ffmpeg build with librubberband and update .external.ffmpeg_macos_rubberband in .build-config.json." >&2
|
||||
exit 1
|
||||
fi
|
||||
# Retune re-encodes the shifted audio as OGG (vorbis); require libvorbis
|
||||
# here too so its output isn't downgraded to the built-in encoder.
|
||||
if ! grep -a -q 'enable-libvorbis' "$bin_dir/ffmpeg-rubberband"; then
|
||||
echo "Error: bundled ffmpeg-rubberband lacks --enable-libvorbis — Retune output OGG would use the lower-quality built-in vorbis encoder." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " ffmpeg-rubberband (Intel, for Retune via Rosetta 2) bundled and verified."
|
||||
fi
|
||||
|
||||
local fluidsynth_bin
|
||||
fluidsynth_bin="$(command -v fluidsynth || true)"
|
||||
if [[ -z "$fluidsynth_bin" ]]; then
|
||||
echo "Error: fluidsynth not found on PATH. Install it with \`brew install fluid-synth\` (and ensure /opt/homebrew/bin is on PATH)." >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "$fluidsynth_bin" "$PROJECT_DIR/resources/bin/"
|
||||
|
||||
# vgmstream: use the local Homebrew Intel build instead of the upstream mac zip.
|
||||
# The upstream vgmstream-mac.zip currently gives this Intel Mac an arm64 binary,
|
||||
# which causes "Bad CPU type in executable" and pulls /opt/homebrew dependencies.
|
||||
echo -e "${BLUE}=== Using Homebrew vgmstream-cli ===${NC}"
|
||||
VGM_BIN="$(command -v vgmstream-cli || true)"
|
||||
if [[ -z "$VGM_BIN" ]]; then
|
||||
echo -e "${RED}ERROR: vgmstream-cli not found. Install it with: brew install vgmstream${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Found vgmstream-cli at: $VGM_BIN"
|
||||
cp "$VGM_BIN" "$PROJECT_DIR/resources/bin/vgmstream-cli"
|
||||
chmod +x "$PROJECT_DIR/resources/bin/vgmstream-cli"
|
||||
|
||||
echo "Copied binary details:"
|
||||
ls -la "$PROJECT_DIR/resources/bin/vgmstream-cli"
|
||||
file "$PROJECT_DIR/resources/bin/vgmstream-cli"
|
||||
|
||||
xattr -d com.apple.quarantine "$PROJECT_DIR/resources/bin/vgmstream-cli" 2>/dev/null || true
|
||||
|
||||
echo -e "${BLUE}=== Skipping vgmstream-cli self-test ===${NC}"
|
||||
# The Homebrew Intel binary is copied and architecture-checked above.
|
||||
# vgmstream-cli returns non-zero for its info/help modes, so don't block packaging here.
|
||||
|
||||
echo -e "${GREEN}vgmstream-cli setup complete${NC}"
|
||||
|
||||
# Run dylibbundler on every bundled binary so each one's brew deps
|
||||
# (libfluidsynth, libspeex, libmpg123, libvorbis, libogg, ffmpeg
|
||||
# libs, etc.) get copied into resources/bin/ and the binaries' load
|
||||
# commands get rewritten to @executable_path/. Without this,
|
||||
# vgmstream-cli (downloaded from upstream) at runtime asks dyld for
|
||||
# /opt/homebrew/opt/speex/lib/libspeex.1.dylib — fine on the dev
|
||||
# machine, fatal on every other Mac. ffmpeg has the same problem
|
||||
# against its own brew deps. dylibbundler is idempotent and skips
|
||||
# paths it has already rewritten, so the per-binary loop is safe
|
||||
# even when binaries share dylibs.
|
||||
if command -v dylibbundler &>/dev/null; then
|
||||
for bin in fluidsynth ffmpeg ffprobe vgmstream-cli; do
|
||||
local target="$PROJECT_DIR/resources/bin/$bin"
|
||||
[[ -f "$target" ]] || continue
|
||||
echo -e "${BLUE}Bundling ${bin} dependencies...${NC}"
|
||||
dylibbundler -cd -b -of -x "$target" \
|
||||
-d "$PROJECT_DIR/resources/bin" \
|
||||
-p '@executable_path/'
|
||||
done
|
||||
fi
|
||||
|
||||
# Sign all bundled native binaries with the Developer ID Application
|
||||
# cert before verify_bundled_binaries runs them. Signing also clears
|
||||
# the macOS quarantine attribute that downloaded binaries carry, so
|
||||
# the verify step doesn't have to special-case quarantine. No-op
|
||||
# when APPLE_SIGNING_IDENTITY is unset (local dev without a cert).
|
||||
"$SCRIPT_DIR/sign-macos-binaries.sh"
|
||||
}
|
||||
|
||||
# Run the build
|
||||
main "$@"
|
||||
|
||||
# Post-build: notarize and staple the DMG. electron-builder notarizes
|
||||
# and staples the .app, then builds + signs the DMG — but the DMG
|
||||
# itself is not submitted to Apple's notary service, so it ships
|
||||
# unstapled. That's fine for online installs (Gatekeeper checks the
|
||||
# .app inside on first launch), but offline first launches and some
|
||||
# enterprise tools want a stapled DMG. notarytool with --wait blocks
|
||||
# until Apple finishes (usually 30s–3min), then stapler embeds the
|
||||
# ticket so the DMG verifies offline. No-op when signing was off.
|
||||
if [[ -n "${APPLE_SIGNING_IDENTITY:-}" && -n "${APPLE_ID:-}" \
|
||||
&& -n "${APPLE_APP_SPECIFIC_PASSWORD:-}" \
|
||||
&& -n "${APPLE_TEAM_ID:-}" ]]; then
|
||||
shopt -s nullglob
|
||||
for dmg in "$PROJECT_DIR"/release/*.dmg; do
|
||||
echo -e "${BLUE}Notarizing $(basename "$dmg") (wait for Apple)...${NC}"
|
||||
xcrun notarytool submit "$dmg" \
|
||||
--apple-id "$APPLE_ID" \
|
||||
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
|
||||
--team-id "$APPLE_TEAM_ID" \
|
||||
--wait
|
||||
echo -e "${BLUE}Stapling notarization ticket to $(basename "$dmg")...${NC}"
|
||||
# `notarytool submit --wait` returns when Apple's notary service
|
||||
# accepts the submission, but the ticket can take an extra
|
||||
# 30-60 s to propagate to CloudKit (where `stapler` reads from).
|
||||
# Stapling immediately fails with `Error 65: Record not found`
|
||||
# on CI roughly half the time. Retry with backoff.
|
||||
staple_ok=0
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if xcrun stapler staple "$dmg"; then
|
||||
staple_ok=1
|
||||
break
|
||||
fi
|
||||
echo " staple attempt $attempt failed; waiting before retry..."
|
||||
sleep $((attempt * 15))
|
||||
done
|
||||
if [[ "$staple_ok" -ne 1 ]]; then
|
||||
echo -e "${RED}Failed to staple $(basename "$dmg") after 5 attempts${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
xcrun stapler validate "$dmg"
|
||||
done
|
||||
shopt -u nullglob
|
||||
fi
|
||||
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Unified release build script that dispatches to platform-specific scripts.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# Color output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo "=== Slopsmith Desktop Build ==="
|
||||
echo ""
|
||||
|
||||
# Detect platform
|
||||
# CROSS-PLATFORM NOTE: MINGW*, MSYS*, and CYGWIN* patterns must all be checked
|
||||
# to properly detect Windows environment. Git Bash sets OSTYPE to msys, but we
|
||||
# also check for MINGW and CYGWIN to cover all Windows bash environments.
|
||||
PLATFORM=""
|
||||
case "$(uname -s)" in
|
||||
Linux*)
|
||||
PLATFORM="linux"
|
||||
;;
|
||||
Darwin*)
|
||||
PLATFORM="macos"
|
||||
;;
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
PLATFORM="windows"
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Error: Unsupported platform: $(uname -s)${NC}" >&2
|
||||
echo "Supported platforms: Linux, macOS, Windows (Git Bash)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo -e "${GREEN}Platform:${NC} $PLATFORM"
|
||||
echo ""
|
||||
|
||||
case "$PLATFORM" in
|
||||
linux)
|
||||
# Check if running on Ubuntu
|
||||
if [[ -f /etc/os-release ]] && grep -q '^ID=ubuntu' /etc/os-release; then
|
||||
# Ubuntu: Use native Ubuntu build
|
||||
if [[ -f "$SCRIPT_DIR/build-linux-ubuntu.sh" ]]; then
|
||||
bash "$SCRIPT_DIR/build-linux-ubuntu.sh"
|
||||
else
|
||||
echo -e "${RED}Error: build-linux-ubuntu.sh not found${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Other Linux: Use Docker for reproducibility across distros
|
||||
if [[ -f "$SCRIPT_DIR/build-linux-docker.sh" ]]; then
|
||||
bash "$SCRIPT_DIR/build-linux-docker.sh"
|
||||
else
|
||||
echo -e "${RED}Error: build-linux-docker.sh not found${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
macos)
|
||||
if [[ -f "$SCRIPT_DIR/build-macos.sh" ]]; then
|
||||
bash "$SCRIPT_DIR/build-macos.sh"
|
||||
else
|
||||
echo -e "${RED}Error: build-macos.sh not found${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
windows)
|
||||
if [[ -f "$SCRIPT_DIR/build-windows.sh" ]]; then
|
||||
bash "$SCRIPT_DIR/build-windows.sh"
|
||||
else
|
||||
echo -e "${RED}Error: build-windows.sh not found${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Error: Unexpected platform: $PLATFORM${NC}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# The exit code from the platform build script
|
||||
exit_code=$?
|
||||
|
||||
if [[ $exit_code -eq 0 ]]; then
|
||||
echo ""
|
||||
echo -e "${GREEN}✓${NC} Build complete!"
|
||||
echo "Artifacts: $PROJECT_DIR/release/"
|
||||
else
|
||||
echo ""
|
||||
echo -e "${RED}✗${NC} Build failed"
|
||||
fi
|
||||
|
||||
exit $exit_code
|
||||
Executable
+276
@@ -0,0 +1,276 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Native Windows build script
|
||||
# Runs in Git Bash (Git for Windows)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Use plain `pwd` (POSIX path). `pwd -W` returns a Windows-form path
|
||||
# with backslashes on MSYS / Git Bash, which would then break `dirname`
|
||||
# and `source "$SCRIPT_DIR/build-common.sh"` since those expect POSIX
|
||||
# paths. If a Windows-form path is needed downstream (e.g. for cmake-js
|
||||
# or a non-MSYS tool), convert at the point of use via
|
||||
# `cygpath -w "$SCRIPT_DIR"`.
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
CONFIG="$PROJECT_DIR/.build-config.json"
|
||||
|
||||
# Platform identifier
|
||||
export PLATFORM="windows"
|
||||
|
||||
# Check for Git Bash/MSYS
|
||||
if [[ "$OSTYPE" != "msys" ]] && [[ "$OSTYPE" != "win32" ]] && [[ -z "${MSYSTEM:-}" ]]; then
|
||||
echo "Error: This script must be run in Git Bash (Git for Windows)" >&2
|
||||
echo "Download: https://git-scm.com/download/win" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Slopsmith Desktop Windows Build ==="
|
||||
echo ""
|
||||
|
||||
# Color setup
|
||||
export RED='\033[0;31m'
|
||||
export GREEN='\033[0;32m'
|
||||
export YELLOW='\033[1;33m'
|
||||
export BLUE='\033[0;34m'
|
||||
export NC='\033[0m'
|
||||
|
||||
# Source common build logic
|
||||
source "$SCRIPT_DIR/build-common.sh"
|
||||
|
||||
# Platform-specific: Return expected artifact patterns
|
||||
# Windows target is now "dir" (electron-builder unpacked only) because Velopack
|
||||
# generates the installer via `vpk pack` on tagged CI runs. For non-tag builds
|
||||
# (PR validation, main-branch pushes) only the unpacked dir is produced.
|
||||
get_expected_artifacts() {
|
||||
printf "%s\n" "$PROJECT_DIR/release/win-unpacked/*.exe"
|
||||
}
|
||||
|
||||
# Platform-specific: Install system dependencies
|
||||
install_system_deps() {
|
||||
# Windows: install via Chocolatey if available
|
||||
if command -v choco.exe &>/dev/null || command -v choco &>/dev/null; then
|
||||
choco install cmake ffmpeg -y --installargs 'ADD_CMAKE_TO_PATH=System' || echo "Chocolatey install may have failed, continuing..."
|
||||
else
|
||||
echo_warning "Chocolatey not found - skipping system package installation"
|
||||
echo " Make sure cmake and ffmpeg are already in PATH"
|
||||
fi
|
||||
}
|
||||
|
||||
# Platform-specific: Bundle Python runtime
|
||||
bundle_python_impl() {
|
||||
# Windows: download embeddable Python
|
||||
PYTHON_VERSION=$(python3 "$SCRIPT_DIR/parse-build-config.py" "$CONFIG" .versions.python)
|
||||
PYTHON_MAJOR="${PYTHON_VERSION%%.*}"
|
||||
PYTHON_MINOR="${PYTHON_VERSION#*.}"
|
||||
PYTHON_EMBED_URL="https://www.python.org/ftp/python/${PYTHON_VERSION}/python-${PYTHON_VERSION}-embed-amd64.zip"
|
||||
|
||||
echo "Downloading Python embeddable..."
|
||||
local curl_status=0
|
||||
curl -sL --fail --retry 5 --retry-delay 5 --retry-all-errors "$PYTHON_EMBED_URL" -o /tmp/python-embed.zip || curl_status=$?
|
||||
if [[ "$curl_status" -ne 0 ]]; then
|
||||
echo_error "Failed to download Python embeddable package"
|
||||
echo " URL: $PYTHON_EMBED_URL"
|
||||
echo " curl exit code: $curl_status"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Wipe the existing python dir before extracting so a re-run doesn't
|
||||
# leave stale files (e.g. an old ._pth from a previous Python version)
|
||||
# mixed in with the freshly-extracted embeddable distribution.
|
||||
rm -rf "$PROJECT_DIR/resources/python"
|
||||
mkdir -p "$PROJECT_DIR/resources/python"
|
||||
unzip -q /tmp/python-embed.zip -d "$PROJECT_DIR/resources/python/"
|
||||
|
||||
# Enable site-packages by editing the ._pth file
|
||||
# IMPORTANT: On Windows embeddable Python, PYTHONPATH environment variable is IGNORED
|
||||
# when a ._pth file exists (isolated mode). We must add paths directly to the .pth file.
|
||||
# The embeddable zip is supposed to ship a ._pth file; if it doesn't,
|
||||
# the rest of the script's PATH-injection won't work, so fail fast.
|
||||
PTH_FILE=$(find "$PROJECT_DIR/resources/python" -name "*._pth" | head -1)
|
||||
if [[ -z "$PTH_FILE" ]]; then
|
||||
echo_error "No ._pth file found in extracted embeddable Python — upstream zip layout may have changed"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -n "$PTH_FILE" ]]; then
|
||||
# Enable site-packages
|
||||
sed -i 's/#import site/import site/' "$PTH_FILE"
|
||||
echo "Lib/site-packages" >> "$PTH_FILE"
|
||||
# Add Slopsmith paths (relative to resources/python)
|
||||
# These must be in the .pth file since PYTHONPATH is ignored in isolated mode
|
||||
echo "# Slopsmith modules (relative to resources/python)" >> "$PTH_FILE"
|
||||
echo "../slopsmith" >> "$PTH_FILE"
|
||||
echo "../slopsmith/lib" >> "$PTH_FILE"
|
||||
fi
|
||||
|
||||
# Install pip
|
||||
echo "Downloading pip..."
|
||||
if ! curl -sL --fail --retry 5 --retry-delay 5 --retry-all-errors \
|
||||
https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py; then
|
||||
echo_error "Failed to download pip installer"
|
||||
exit 1
|
||||
fi
|
||||
"$PROJECT_DIR/resources/python/python.exe" /tmp/get-pip.py --quiet --no-cache-dir
|
||||
|
||||
# Install packages
|
||||
# Install build tools first (required for building from source on Windows embeddable Python)
|
||||
"$PROJECT_DIR/resources/python/python.exe" -m pip install --quiet --no-cache-dir \
|
||||
setuptools wheel
|
||||
# Install slopsmith runtime requirements (single source of truth —
|
||||
# drift used to silently break desktop builds whenever slopsmith added
|
||||
# a dep), then desktop-only extras. SLOPSMITH_DIR is exported by
|
||||
# clone_slopsmith() in build-common.sh; fall back to local-dev paths
|
||||
# to match bundle-slopsmith.sh's discovery so this works outside CI.
|
||||
if [[ -z "${SLOPSMITH_DIR:-}" ]]; then
|
||||
if [[ -d "$PROJECT_DIR/../slopsmith" ]]; then
|
||||
SLOPSMITH_DIR="$PROJECT_DIR/../slopsmith"
|
||||
elif [[ -d "$HOME/Repositories/slopsmith" ]]; then
|
||||
SLOPSMITH_DIR="$HOME/Repositories/slopsmith"
|
||||
fi
|
||||
fi
|
||||
if [[ -z "${SLOPSMITH_DIR:-}" ]] || [[ ! -f "$SLOPSMITH_DIR/requirements.txt" ]]; then
|
||||
echo "ERROR: slopsmith requirements.txt not found (SLOPSMITH_DIR=${SLOPSMITH_DIR:-<unset>})." >&2
|
||||
echo " Expected SLOPSMITH_DIR to be exported by clone_slopsmith() in build-common.sh," >&2
|
||||
echo " or slopsmith cloned next to this repo." >&2
|
||||
exit 1
|
||||
fi
|
||||
"$PROJECT_DIR/resources/python/python.exe" -m pip install --quiet --no-cache-dir \
|
||||
-r "$SLOPSMITH_DIR/requirements.txt"
|
||||
"$PROJECT_DIR/resources/python/python.exe" -m pip install --quiet --no-cache-dir \
|
||||
-r "$PROJECT_DIR/.packages/python.txt"
|
||||
}
|
||||
|
||||
# Usage: download_with_retries <url> <output_path> <description>
|
||||
download_with_retries() {
|
||||
local url="$1"
|
||||
local output_path="$2"
|
||||
local description="$3"
|
||||
local max_attempts=3
|
||||
local attempt=1
|
||||
local delay=10
|
||||
|
||||
while [[ $attempt -le $max_attempts ]]; do
|
||||
echo " Downloading $description (attempt $attempt/$max_attempts)..."
|
||||
if curl -sL --fail --max-time 120 "$url" -o "$output_path"; then
|
||||
echo " Successfully downloaded $description"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local exit_code=$?
|
||||
echo " Download failed with exit code $exit_code"
|
||||
|
||||
if [[ $attempt -lt $max_attempts ]]; then
|
||||
echo " Retrying in ${delay}s..."
|
||||
sleep $delay
|
||||
delay=$((delay * 2))
|
||||
fi
|
||||
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
echo_error "Failed to download $description after $max_attempts attempts"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Platform-specific: Bundle system binaries
|
||||
# These binaries are REQUIRED for core functionality. The build will fail
|
||||
# if downloads don't succeed after multiple retry attempts.
|
||||
bundle_binaries_impl() {
|
||||
mkdir -p "$PROJECT_DIR/resources/bin"
|
||||
|
||||
# ffmpeg static build
|
||||
echo "Downloading ffmpeg..."
|
||||
if ! download_with_retries \
|
||||
"https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip" \
|
||||
"/tmp/ffmpeg.zip" \
|
||||
"ffmpeg"; then
|
||||
exit 1
|
||||
fi
|
||||
unzip -q /tmp/ffmpeg.zip -d /tmp/ffmpeg
|
||||
# Validate the expected layout instead of `cp ... || true` — a broken
|
||||
# / changed zip layout would otherwise drop `ffmpeg.exe` silently and
|
||||
# surface as a less-direct error in `verify_bundled_binaries`.
|
||||
FFMPEG_BIN=$(find /tmp/ffmpeg -name 'ffmpeg.exe' -type f | head -1)
|
||||
if [[ -z "$FFMPEG_BIN" ]]; then
|
||||
echo_error "ffmpeg.exe not found after extracting /tmp/ffmpeg.zip — upstream zip layout may have changed"
|
||||
exit 1
|
||||
fi
|
||||
cp "$FFMPEG_BIN" "$PROJECT_DIR/resources/bin/"
|
||||
|
||||
# Sloppak conversion encodes .ogg with -c:a libvorbis. The BtbN GPL
|
||||
# build typically ships libvorbis, but verify so a future upstream
|
||||
# change doesn't silently degrade users to the built-in vorbis
|
||||
# encoder. The lib/sloppak_convert.py fallback is a safety net for
|
||||
# unbundled installs, not a license to ship a libvorbis-less binary.
|
||||
if ! "$PROJECT_DIR/resources/bin/ffmpeg.exe" -hide_banner -encoders 2>/dev/null | grep -wq libvorbis; then
|
||||
echo_error "bundled ffmpeg lacks libvorbis encoder. Sloppak conversion would fall back to the lower-quality built-in vorbis encoder on user machines."
|
||||
echo_error "BtbN's GPL build no longer includes --enable-libvorbis; pick a different release asset (or earlier build) that ships it."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ffprobe ships in the same BtbN zip as ffmpeg. demucs's audio loader
|
||||
# spawns ffprobe before ffmpeg to read stream metadata; without it the
|
||||
# loader dies with FileNotFoundError before ffmpeg is ever invoked.
|
||||
FFPROBE_BIN=$(find /tmp/ffmpeg -name 'ffprobe.exe' -type f | head -1)
|
||||
if [[ -z "$FFPROBE_BIN" ]]; then
|
||||
echo_error "ffprobe.exe not found after extracting /tmp/ffmpeg.zip — upstream zip layout may have changed"
|
||||
exit 1
|
||||
fi
|
||||
cp "$FFPROBE_BIN" "$PROJECT_DIR/resources/bin/"
|
||||
|
||||
# vgmstream-cli
|
||||
echo "Downloading vgmstream-cli..."
|
||||
if ! download_with_retries \
|
||||
"https://github.com/vgmstream/vgmstream/releases/latest/download/vgmstream-win64.zip" \
|
||||
"/tmp/vgmstream.zip" \
|
||||
"vgmstream-cli"; then
|
||||
exit 1
|
||||
fi
|
||||
unzip -q /tmp/vgmstream.zip -d /tmp/vgmstream
|
||||
VGMSTREAM_EXE="/tmp/vgmstream/vgmstream-cli.exe"
|
||||
shopt -s nullglob
|
||||
VGMSTREAM_DLLS=("/tmp/vgmstream"/*.dll)
|
||||
shopt -u nullglob
|
||||
if [[ ! -f "$VGMSTREAM_EXE" ]]; then
|
||||
echo_error "vgmstream-cli.exe not found at $VGMSTREAM_EXE after extraction"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ${#VGMSTREAM_DLLS[@]} -eq 0 ]]; then
|
||||
echo_error "Expected vgmstream DLLs in /tmp/vgmstream after extraction but found none"
|
||||
exit 1
|
||||
fi
|
||||
cp "$VGMSTREAM_EXE" "$PROJECT_DIR/resources/bin/"
|
||||
cp "${VGMSTREAM_DLLS[@]}" "$PROJECT_DIR/resources/bin/"
|
||||
|
||||
# fluidsynth
|
||||
echo "Downloading fluidsynth..."
|
||||
# Reuse parse-build-config.py — Git Bash auto-converts the MSYS-style
|
||||
# $CONFIG path to native Windows form when it's passed as an arg, but
|
||||
# NOT when it's interpolated into an inline `python -c "...open('$CONFIG')..."`
|
||||
# string. The previous inline form silently failed with set -e on
|
||||
# Windows because Python opened a "/d/a/.../config.json" path that
|
||||
# doesn't exist as a literal Windows path.
|
||||
FS_URL=$(python3 "$SCRIPT_DIR/parse-build-config.py" "$CONFIG" .external.fluidsynth_windows.url 2>/dev/null || true)
|
||||
if [[ -n "$FS_URL" ]]; then
|
||||
if ! download_with_retries \
|
||||
"$FS_URL" \
|
||||
"/tmp/fluidsynth.zip" \
|
||||
"fluidsynth"; then
|
||||
exit 1
|
||||
fi
|
||||
unzip -q /tmp/fluidsynth.zip -d /tmp/fluidsynth
|
||||
FS_BIN=$(find /tmp/fluidsynth -name 'fluidsynth.exe' -type f | head -1)
|
||||
if [[ -n "$FS_BIN" ]]; then
|
||||
cp "$FS_BIN" "$PROJECT_DIR/resources/bin/"
|
||||
cp "$(dirname "$FS_BIN")"/*.dll "$PROJECT_DIR/resources/bin/" 2>/dev/null || true
|
||||
fi
|
||||
else
|
||||
echo_error "Fluidsynth URL not found in build config"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo_summary "All required Windows binaries downloaded and installed"
|
||||
}
|
||||
|
||||
# Run the build
|
||||
main "$@"
|
||||
Executable
+191
@@ -0,0 +1,191 @@
|
||||
#!/bin/bash
|
||||
# Bundle system binaries (ffmpeg, ffprobe, vgmstream-cli, fluidsynth)
|
||||
# into resources/bin/ along with their non-glibc shared library
|
||||
# dependencies, then set RPATH=$ORIGIN so each binary loads its
|
||||
# siblings from its own directory at runtime.
|
||||
#
|
||||
# Without this, a build host with a different ffmpeg ABI than the user
|
||||
# (e.g. Ubuntu 22.04 ffmpeg 4.x → libav*.so.58, Fedora 44 / Arch
|
||||
# ffmpeg 7.x → libav*.so.62) ships a binary the user can't load.
|
||||
#
|
||||
# Linux-only. macOS bundling runs dylibbundler inline in the CI workflow
|
||||
# because it's a different dynamic-linker story (Mach-O load paths +
|
||||
# codesign invalidation). Windows downloads pinned zip archives inline.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$(uname -s)" != "Linux" ]; then
|
||||
echo "bundle-binaries.sh is Linux-only. macOS/Windows bundling runs inline in CI." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
BIN_DIR="$PROJECT_DIR/resources/bin"
|
||||
|
||||
mkdir -p "$BIN_DIR"
|
||||
echo "=== Bundling system binaries ==="
|
||||
|
||||
# patchelf is required to set RPATH=$ORIGIN on bundled binaries and
|
||||
# libs. Without that, the runtime linker on the user's machine falls
|
||||
# back to /usr/lib and fails when the host ABI doesn't match the build
|
||||
# host's. Fail here rather than letting fluidsynth's section fail later.
|
||||
if ! command -v patchelf >/dev/null 2>&1; then
|
||||
echo "ERROR: patchelf not found on PATH (apt: patchelf) - required to set RPATH on bundled binaries." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# is_skipped_lib() — the glibc/loader skip list, shared verbatim with
|
||||
# build-common.sh's audit so the two never drift.
|
||||
source "$SCRIPT_DIR/bundled-lib-skiplist.sh"
|
||||
|
||||
# Copy every non-glibc shared library that the given binary links to
|
||||
# into resources/bin/. The final patchelf sweep (below) sets RPATH on
|
||||
# every binary and every .so so the directory becomes a single
|
||||
# self-contained load tree.
|
||||
bundle_with_deps() {
|
||||
local bin_path="$1"
|
||||
# ldd exits non-zero (and prints "not a dynamic executable") on
|
||||
# statically linked binaries like the vgmstream-cli GitHub release.
|
||||
# Trap that and treat it as "no deps to bundle" rather than letting
|
||||
# pipefail kill the build.
|
||||
local ldd_out
|
||||
if ! ldd_out=$(ldd "$bin_path" 2>/dev/null); then
|
||||
return 0
|
||||
fi
|
||||
echo "$ldd_out" | awk '/=>/ {print $3}' | while read -r lib; do
|
||||
[ -n "$lib" ] && [ -f "$lib" ] || continue
|
||||
if is_skipped_lib "$(basename "$lib")"; then
|
||||
continue
|
||||
fi
|
||||
# -L follows symlinks; -n avoids re-copying a lib already
|
||||
# contributed by an earlier binary (every bundled binary on
|
||||
# the same build host links the same /usr/lib versions, so
|
||||
# first-wins is safe). Errors are NOT suppressed: -n makes the
|
||||
# already-bundled case a no-op exit 0, so any non-zero status
|
||||
# here is a real copy failure that must fail the build.
|
||||
cp -Ln "$lib" "$BIN_DIR/"
|
||||
done
|
||||
}
|
||||
|
||||
# ffmpeg - used for WAV → OGG transcoding on GP5 imports.
|
||||
# verify_bundled_binaries treats resources/bin/ffmpeg as required and
|
||||
# will hard-fail later if it's missing — fail here with the actual cause
|
||||
# rather than letting that downstream check produce a less-direct error.
|
||||
if command -v ffmpeg >/dev/null 2>&1; then
|
||||
cp "$(which ffmpeg)" "$BIN_DIR/"
|
||||
echo " ffmpeg: $(ls -lh "$BIN_DIR/ffmpeg" | awk '{print $5}')"
|
||||
else
|
||||
echo "ERROR: ffmpeg not found on PATH; resources/bin/ffmpeg is required for the bundled build (apt: ffmpeg / brew: ffmpeg)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Sloppak conversion encodes .ogg with -c:a libvorbis. Fail the build now
|
||||
# rather than ship an ffmpeg that produces "Unknown encoder 'libvorbis'"
|
||||
# at runtime on user machines. The lib/sloppak_convert.py fallback to
|
||||
# the built-in `vorbis -strict experimental` encoder is a safety net for
|
||||
# unbundled installs, not a license to ship a libvorbis-less binary.
|
||||
if ! "$BIN_DIR/ffmpeg" -hide_banner -encoders 2>/dev/null | grep -wq libvorbis; then
|
||||
echo "ERROR: bundled ffmpeg lacks libvorbis encoder. Sloppak conversion would fall back to the lower-quality built-in vorbis encoder on user machines." >&2
|
||||
echo "Install an ffmpeg built with --enable-libvorbis (apt's ffmpeg ships it by default; check your distro's package if this fails)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bundle_with_deps "$BIN_DIR/ffmpeg"
|
||||
|
||||
# ffprobe - demucs's audio loader spawns ffprobe before ffmpeg to read
|
||||
# stream metadata; falling through to a host-installed ffprobe (or none
|
||||
# at all) means the desktop bundle behaves differently on each user's
|
||||
# machine. Ship the build host's ffprobe alongside ffmpeg so the bundle
|
||||
# is self-contained on every platform. apt's ffmpeg package includes
|
||||
# ffprobe, so this is universally available where ffmpeg already is.
|
||||
if command -v ffprobe >/dev/null 2>&1; then
|
||||
cp "$(which ffprobe)" "$BIN_DIR/"
|
||||
echo " ffprobe: $(ls -lh "$BIN_DIR/ffprobe" | awk '{print $5}')"
|
||||
else
|
||||
echo "ERROR: ffprobe not found on PATH; resources/bin/ffprobe is required so demucs can read stream metadata in stem-splitting (apt: ffmpeg / brew: ffmpeg)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bundle_with_deps "$BIN_DIR/ffprobe"
|
||||
|
||||
# vgmstream-cli - used for WEM → WAV decoding.
|
||||
# Download from GitHub releases if not in PATH (CI does this inline).
|
||||
# verify_bundled_binaries downstream treats this as required and will
|
||||
# hard-fail if it ends up missing — fail here with the actual cause
|
||||
# instead.
|
||||
if command -v vgmstream-cli >/dev/null 2>&1; then
|
||||
cp "$(which vgmstream-cli)" "$BIN_DIR/"
|
||||
echo " vgmstream-cli: $(ls -lh "$BIN_DIR/vgmstream-cli" | awk '{print $5}')"
|
||||
else
|
||||
for tool in curl unzip; do
|
||||
if ! command -v "$tool" >/dev/null 2>&1; then
|
||||
echo "ERROR: $tool not on PATH; required to download/extract vgmstream-cli." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Downloading vgmstream-cli from GitHub releases..."
|
||||
VGM_ASSET="vgmstream-linux.zip"
|
||||
if ! curl -sL --fail --retry 5 --retry-delay 5 --retry-all-errors \
|
||||
"https://github.com/vgmstream/vgmstream/releases/latest/download/${VGM_ASSET}" \
|
||||
-o /tmp/vgmstream.zip; then
|
||||
echo "ERROR: failed to download vgmstream-cli zip from upstream releases." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! unzip -q /tmp/vgmstream.zip -d /tmp/vgmstream; then
|
||||
echo "ERROR: failed to extract /tmp/vgmstream.zip — upstream archive may be malformed." >&2
|
||||
exit 1
|
||||
fi
|
||||
VGM_BIN=$(find /tmp/vgmstream -maxdepth 2 -name 'vgmstream-cli' -type f | head -1)
|
||||
if [ -z "$VGM_BIN" ]; then
|
||||
echo "ERROR: vgmstream-cli binary not found in downloaded archive — upstream zip layout may have changed." >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "$VGM_BIN" "$BIN_DIR/vgmstream-cli"
|
||||
chmod +x "$BIN_DIR/vgmstream-cli"
|
||||
echo " vgmstream-cli: $(ls -lh "$BIN_DIR/vgmstream-cli" | awk '{print $5}') (downloaded)"
|
||||
rm -rf /tmp/vgmstream /tmp/vgmstream.zip
|
||||
fi
|
||||
|
||||
bundle_with_deps "$BIN_DIR/vgmstream-cli"
|
||||
|
||||
# fluidsynth - used for MIDI → WAV in GP5 imports.
|
||||
if command -v fluidsynth >/dev/null 2>&1; then
|
||||
cp "$(which fluidsynth)" "$BIN_DIR/fluidsynth"
|
||||
echo " fluidsynth: $(ls -lh "$BIN_DIR/fluidsynth" | awk '{print $5}')"
|
||||
else
|
||||
echo "ERROR: fluidsynth not found on PATH - it is required to bundle GP5 import support. Install fluidsynth and rerun this script (apt: fluidsynth)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bundle_with_deps "$BIN_DIR/fluidsynth"
|
||||
|
||||
# Final patchelf sweep: every dynamic binary gets RPATH=$ORIGIN, and
|
||||
# every dynamic .so does too so transitive deps also load from
|
||||
# resources/bin/. Done once at the end so the order in which binaries
|
||||
# contribute their libs doesn't matter.
|
||||
#
|
||||
# Be strict about patchelf success on dynamic binaries. The downstream
|
||||
# audit only checks lib *presence*, not RPATH — so a silent patchelf
|
||||
# failure here would ship a binary whose loader falls back to /usr/lib
|
||||
# at runtime, exactly the issue #68 regression. Detect static-vs-dynamic
|
||||
# explicitly via NEEDED entries in the .dynamic section and only skip
|
||||
# patchelf on the static case (e.g. the vgmstream-cli GitHub release).
|
||||
for bin in ffmpeg ffprobe vgmstream-cli fluidsynth; do
|
||||
bin_path="$BIN_DIR/$bin"
|
||||
if readelf -d "$bin_path" 2>/dev/null | grep -q '(NEEDED)'; then
|
||||
patchelf --set-rpath '$ORIGIN' "$bin_path"
|
||||
else
|
||||
echo " $bin: statically linked, RPATH not applicable"
|
||||
fi
|
||||
done
|
||||
for so in "$BIN_DIR"/*.so*; do
|
||||
[ -f "$so" ] || continue
|
||||
if readelf -d "$so" 2>/dev/null | grep -q '(NEEDED)'; then
|
||||
patchelf --set-rpath '$ORIGIN' "$so"
|
||||
fi
|
||||
done
|
||||
|
||||
echo " Total resources/bin/: $(du -sh "$BIN_DIR" | cut -f1)"
|
||||
echo "=== Binary bundle complete ==="
|
||||
Executable
+89
@@ -0,0 +1,89 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Bundle a portable Python runtime into resources/python/runtime/ for Linux.
|
||||
#
|
||||
# Produces a relocatable interpreter + stdlib that the Electron main process
|
||||
# spawns via `python.ts`. The layout matches python.ts's expectations:
|
||||
# resources/python/runtime/bin/python3
|
||||
#
|
||||
# Downloads python-build-standalone (same source as actions/setup-python in
|
||||
# CI) so the result is identical regardless of what Python is installed on the
|
||||
# host or in the Docker container.
|
||||
#
|
||||
# Linux-only: macOS and Windows bundles are handled inline in build-common.sh.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$(uname -s)" != "Linux" ]; then
|
||||
echo "bundle-python.sh is Linux-only. macOS and Windows bundles run inline in CI." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
CONFIG="$PROJECT_DIR/.build-config.json"
|
||||
|
||||
PYTHON_FULL_VERSION=$(python3 "$SCRIPT_DIR/parse-build-config.py" "$CONFIG" .versions.python)
|
||||
PYTHON_VERSION="${PYTHON_FULL_VERSION%.*}"
|
||||
|
||||
PYTHON_STANDALONE_URL=$(python3 "$SCRIPT_DIR/parse-build-config.py" "$CONFIG" .external.python_standalone_linux_x64.url)
|
||||
PYTHON_STANDALONE_SHA256=$(python3 "$SCRIPT_DIR/parse-build-config.py" "$CONFIG" .external.python_standalone_linux_x64.sha256)
|
||||
|
||||
PYTHON_BUNDLE="$PROJECT_DIR/resources/python/runtime"
|
||||
|
||||
echo "=== Bundling Python $PYTHON_FULL_VERSION runtime ==="
|
||||
echo " Downloading python-build-standalone..."
|
||||
|
||||
TMPDIR_PBS=$(mktemp -d)
|
||||
trap 'rm -rf "$TMPDIR_PBS"' EXIT
|
||||
|
||||
curl -fsSL "$PYTHON_STANDALONE_URL" -o "$TMPDIR_PBS/python-standalone.tar.gz"
|
||||
echo "${PYTHON_STANDALONE_SHA256} $TMPDIR_PBS/python-standalone.tar.gz" | sha256sum -c -
|
||||
tar -xzf "$TMPDIR_PBS/python-standalone.tar.gz" -C "$TMPDIR_PBS"
|
||||
|
||||
PBS_PREFIX="$TMPDIR_PBS/python"
|
||||
|
||||
rm -rf "$PROJECT_DIR/resources/python"
|
||||
mkdir -p "$PYTHON_BUNDLE/bin" "$PYTHON_BUNDLE/lib"
|
||||
|
||||
cp "$PBS_PREFIX/bin/python${PYTHON_VERSION}" "$PYTHON_BUNDLE/bin/python3"
|
||||
chmod +x "$PYTHON_BUNDLE/bin/python3"
|
||||
cp -r "$PBS_PREFIX/lib/python${PYTHON_VERSION}" "$PYTHON_BUNDLE/lib/"
|
||||
cp "$PBS_PREFIX/lib"/libpython${PYTHON_VERSION}*.so* "$PYTHON_BUNDLE/lib/"
|
||||
|
||||
echo " Bootstrapping pip via ensurepip in the bundled runtime"
|
||||
LD_LIBRARY_PATH="$PYTHON_BUNDLE/lib" "$PYTHON_BUNDLE/bin/python3" -m ensurepip --upgrade --default-pip
|
||||
|
||||
# Resolve the slopsmith repo so we can pip install from its
|
||||
# requirements.txt — that's the single source of truth for runtime
|
||||
# deps. Search order matches bundle-slopsmith.sh:
|
||||
# 1. $SLOPSMITH_DIR env var (set by clone_slopsmith() in CI)
|
||||
# 2. ../slopsmith (sibling to this repo)
|
||||
# 3. ~/Repositories/slopsmith (legacy dev layout)
|
||||
if [ -z "${SLOPSMITH_DIR:-}" ]; then
|
||||
if [ -d "$PROJECT_DIR/../slopsmith" ]; then
|
||||
SLOPSMITH_DIR="$PROJECT_DIR/../slopsmith"
|
||||
elif [ -d "$HOME/Repositories/slopsmith" ]; then
|
||||
SLOPSMITH_DIR="$HOME/Repositories/slopsmith"
|
||||
fi
|
||||
fi
|
||||
if [ -z "${SLOPSMITH_DIR:-}" ] || [ ! -f "$SLOPSMITH_DIR/requirements.txt" ]; then
|
||||
echo "ERROR: slopsmith requirements.txt not found." >&2
|
||||
echo "Searched:" >&2
|
||||
echo " \$SLOPSMITH_DIR=${SLOPSMITH_DIR:-<unset>}" >&2
|
||||
echo " $PROJECT_DIR/../slopsmith" >&2
|
||||
echo " $HOME/Repositories/slopsmith" >&2
|
||||
echo "Clone slopsmith next to this repo: git clone https://github.com/slopsmith/slopsmith.git $PROJECT_DIR/../slopsmith" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " Installing slopsmith runtime requirements ($SLOPSMITH_DIR/requirements.txt)"
|
||||
LD_LIBRARY_PATH="$PYTHON_BUNDLE/lib" "$PYTHON_BUNDLE/bin/python3" -m pip install --quiet --no-cache-dir \
|
||||
-r "$SLOPSMITH_DIR/requirements.txt" 2>&1 | tail -3
|
||||
|
||||
echo " Installing desktop-only Python extras"
|
||||
LD_LIBRARY_PATH="$PYTHON_BUNDLE/lib" "$PYTHON_BUNDLE/bin/python3" -m pip install --quiet --no-cache-dir \
|
||||
-r "$PROJECT_DIR/.packages/python.txt" 2>&1 | tail -3
|
||||
|
||||
echo " Python runtime size: $(du -sh "$PYTHON_BUNDLE" | cut -f1)"
|
||||
echo "=== Python bundle complete ==="
|
||||
Executable
+189
@@ -0,0 +1,189 @@
|
||||
#!/bin/bash
|
||||
# Bundle the Slopsmith server source + plugins into resources/slopsmith/.
|
||||
#
|
||||
# Slopsmith repo location is resolved in this order:
|
||||
# 1. $SLOPSMITH_DIR env var
|
||||
# 2. ../slopsmith (sibling to this repo)
|
||||
# 3. ~/Repositories/slopsmith (legacy dev layout)
|
||||
#
|
||||
# Cross-platform: avoids `readlink -f` (not available on macOS by default)
|
||||
# by using python's os.path.realpath. `rsync` is used for the resolved-
|
||||
# symlink copy step and must be present (see .packages/apt.txt).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
BUNDLE_DIR="$PROJECT_DIR/resources/slopsmith"
|
||||
|
||||
if [ -z "${SLOPSMITH_DIR:-}" ]; then
|
||||
if [ -d "$PROJECT_DIR/../slopsmith" ]; then
|
||||
SLOPSMITH_DIR="$PROJECT_DIR/../slopsmith"
|
||||
elif [ -d "$HOME/Repositories/slopsmith" ]; then
|
||||
SLOPSMITH_DIR="$HOME/Repositories/slopsmith"
|
||||
else
|
||||
SLOPSMITH_DIR=""
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$SLOPSMITH_DIR" ] || [ ! -d "$SLOPSMITH_DIR" ]; then
|
||||
echo "ERROR: Slopsmith repository not found." >&2
|
||||
echo "Searched:" >&2
|
||||
echo " \$SLOPSMITH_DIR (unset)" >&2
|
||||
echo " $PROJECT_DIR/../slopsmith" >&2
|
||||
echo " $HOME/Repositories/slopsmith" >&2
|
||||
echo "Clone it with: git clone https://github.com/slopsmith/slopsmith.git ../slopsmith" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Portable realpath — readlink -f doesn't exist on stock macOS.
|
||||
realpath_portable() {
|
||||
python3 -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$1"
|
||||
}
|
||||
|
||||
echo "=== Bundling Slopsmith server and plugins ==="
|
||||
echo " Source: $SLOPSMITH_DIR"
|
||||
|
||||
rm -rf "$BUNDLE_DIR"
|
||||
mkdir -p "$BUNDLE_DIR/static" "$BUNDLE_DIR/plugins"
|
||||
|
||||
# Server + lib
|
||||
cp "$SLOPSMITH_DIR/server.py" "$BUNDLE_DIR/"
|
||||
cp "$SLOPSMITH_DIR/VERSION" "$BUNDLE_DIR/"
|
||||
cp -r "$SLOPSMITH_DIR/lib" "$BUNDLE_DIR/"
|
||||
rm -rf "$BUNDLE_DIR/lib/__pycache__"
|
||||
|
||||
# Bundled content (progression paths, quests, shop definitions)
|
||||
[ -d "$SLOPSMITH_DIR/data" ] && cp -r "$SLOPSMITH_DIR/data" "$BUNDLE_DIR/"
|
||||
|
||||
# Static assets — copy the whole directory. User-data dirs (art/, sloppak_cache/)
|
||||
# and generated audio_*.mp3 files are gitignored and won't exist in a clean checkout.
|
||||
cp -r "$SLOPSMITH_DIR/static/." "$BUNDLE_DIR/static/"
|
||||
# Strip any leftover user-data that may exist in a dev checkout.
|
||||
rm -rf "$BUNDLE_DIR/static/art" "$BUNDLE_DIR/static/sloppak_cache"
|
||||
find "$BUNDLE_DIR/static" -maxdepth 1 -name 'audio_*.mp3' -delete
|
||||
|
||||
# Builtin diagnostic sloppak — server._seed_builtin_diagnostic_sloppaks() copies
|
||||
# this into DLC_DIR/diagnostics-builtin/ on library scan startup.
|
||||
DIAG_SLOPPAK="$SLOPSMITH_DIR/docs/diagnostics/slopsmith-diagnostic-basic-guitar.sloppak"
|
||||
if [ -f "$DIAG_SLOPPAK" ]; then
|
||||
mkdir -p "$BUNDLE_DIR/docs/diagnostics"
|
||||
cp "$DIAG_SLOPPAK" "$BUNDLE_DIR/docs/diagnostics/"
|
||||
else
|
||||
echo "WARNING: diagnostic sloppak not found at $DIAG_SLOPPAK — builtin seeding will skip in packaged builds" >&2
|
||||
fi
|
||||
|
||||
# Cross-platform "cp -r minus .git" — Git Bash on Windows doesn't ship
|
||||
# rsync, so we can't rely on `rsync --exclude=.git`. Plain `cp -r`
|
||||
# followed by stripping any nested `.git/` directories works on
|
||||
# Linux/macOS/Git-Bash alike. The .git stripping matters because
|
||||
# plugin directories cloned by clone_slopsmith() are git working trees;
|
||||
# bundling their .git/objects/ would inflate the .app and (on macOS)
|
||||
# trip electron-builder with EACCES on read-only pack files.
|
||||
copy_plugin() {
|
||||
local src="$1"
|
||||
local dst="$2"
|
||||
mkdir -p "$dst"
|
||||
# Use cp -R rather than -r for portable symlink-following semantics.
|
||||
cp -R "$src/." "$dst/"
|
||||
find "$dst" -name '.git' -type d -prune -exec rm -rf {} +
|
||||
}
|
||||
|
||||
# Built-in plugins (real directories, not symlinks to avoid duplicates).
|
||||
for plugin_dir in "$SLOPSMITH_DIR/plugins/editor" "$SLOPSMITH_DIR/plugins/note_detect"; do
|
||||
if [ -d "$plugin_dir" ] && [ ! -L "$plugin_dir" ]; then
|
||||
name=$(basename "$plugin_dir")
|
||||
copy_plugin "$plugin_dir" "$BUNDLE_DIR/plugins/$name"
|
||||
fi
|
||||
done
|
||||
|
||||
# External plugins: resolve symlinks, skip .git
|
||||
for plugin_link in "$SLOPSMITH_DIR/plugins/"*; do
|
||||
name=$(basename "$plugin_link")
|
||||
[ "$name" = "__pycache__" ] && continue
|
||||
[ "$name" = "__init__.py" ] && continue
|
||||
target="$BUNDLE_DIR/plugins/$name"
|
||||
[ -d "$target" ] && continue # already copied
|
||||
|
||||
if [ -L "$plugin_link" ]; then
|
||||
real_dir=$(realpath_portable "$plugin_link")
|
||||
if [ -d "$real_dir" ]; then
|
||||
copy_plugin "$real_dir" "$target"
|
||||
fi
|
||||
elif [ -d "$plugin_link" ]; then
|
||||
copy_plugin "$plugin_link" "$target"
|
||||
elif [ -f "$plugin_link" ]; then
|
||||
cp "$plugin_link" "$target"
|
||||
fi
|
||||
done
|
||||
|
||||
# Plugin-discovery __init__.py
|
||||
cp "$SLOPSMITH_DIR/plugins/__init__.py" "$BUNDLE_DIR/plugins/"
|
||||
|
||||
# Desktop-specific plugins (audio_engine, plugin_manager) declared in
|
||||
# src/renderer/**/plugin.json
|
||||
for dp in "$PROJECT_DIR/src/renderer" "$PROJECT_DIR/src/renderer/plugin-manager"; do
|
||||
if [ -f "$dp/plugin.json" ]; then
|
||||
pname=$(python3 -c "import json, sys; print(json.load(open(sys.argv[1]))['id'])" "$dp/plugin.json")
|
||||
mkdir -p "$BUNDLE_DIR/plugins/$pname"
|
||||
cp "$dp"/*.html "$dp"/*.js "$dp"/plugin.json "$BUNDLE_DIR/plugins/$pname/" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
# ── Rebuild Tailwind CSS over the FULL bundled plugin set ──────────────────
|
||||
# Core's committed static/tailwind.min.css is built scanning only the in-tree
|
||||
# plugins (highway_3d, editor, note_detect, app_tour_*). Shipped as-is it would
|
||||
# leave most of the 30+ bundled plugins' classes unstyled — the Play CDN's
|
||||
# runtime JIT used to cover them, but it was removed (slopsmith#411). So
|
||||
# regenerate the sheet HERE, after every plugin is copied, so it covers the
|
||||
# whole bundled set and scales automatically as more plugins are added.
|
||||
#
|
||||
# We reuse core's tailwind.config.js for parity (same theme colors, safelist,
|
||||
# and the highway_3d exclusion — that plugin ships its own assets/plugin.css
|
||||
# via the `styles` capability). The config is copied into the bundle and run
|
||||
# from there so its relative content globs (./static/**, ./plugins/**) resolve
|
||||
# against the bundle regardless of Tailwind's cwd-vs-config-dir semantics.
|
||||
if command -v npx >/dev/null 2>&1; then
|
||||
echo "=== Rebuilding Tailwind CSS over bundled plugins ==="
|
||||
# Whole pipeline runs inside one guarded `if (...)` so ANY failure (config
|
||||
# copy, no npm cache/network, build error, or the final swap) falls back to
|
||||
# the committed sheet instead of aborting the bundle under `set -e`. Two
|
||||
# subtleties: (1) `set -e` is suppressed inside an `if` condition, so the
|
||||
# steps are &&-chained to make an early failure short-circuit; (2) the build
|
||||
# writes to a temp file that's mv'd into place only as the last link, so a
|
||||
# failed/partial `npx` can never truncate the committed fallback sheet.
|
||||
if (
|
||||
cp "$SLOPSMITH_DIR/tailwind.config.js" "$BUNDLE_DIR/tailwind.config.js" \
|
||||
&& cd "$BUNDLE_DIR" \
|
||||
&& npx -y tailwindcss@3.4.19 \
|
||||
-c tailwind.config.js \
|
||||
-i static/_tailwind.src.css \
|
||||
-o static/tailwind.min.css.new \
|
||||
--minify \
|
||||
&& mv -f static/tailwind.min.css.new static/tailwind.min.css
|
||||
); then
|
||||
# Drop the build-only inputs so they don't ship in resources/slopsmith.
|
||||
# `|| true`: cleanup is best-effort — a stray rm failure (e.g. Windows
|
||||
# file locks) must never abort the bundle under `set -e`.
|
||||
rm -f "$BUNDLE_DIR/tailwind.config.js" "$BUNDLE_DIR/static/_tailwind.src.css" || true
|
||||
echo " Tailwind CSS: $(wc -c < "$BUNDLE_DIR/static/tailwind.min.css") bytes (bundled-plugin-aware)"
|
||||
else
|
||||
# Discard any partial output; the committed sheet copied earlier stays
|
||||
# intact. Still drop the build-only input so it never ships (matches the
|
||||
# success path). `|| true` keeps cleanup non-fatal.
|
||||
rm -f "$BUNDLE_DIR/tailwind.config.js" "$BUNDLE_DIR/static/tailwind.min.css.new" "$BUNDLE_DIR/static/_tailwind.src.css" || true
|
||||
echo "WARN: Tailwind rebuild failed — shipping core's committed sheet as-is." >&2
|
||||
echo " Bundled plugins using classes outside it may render unstyled." >&2
|
||||
fi
|
||||
else
|
||||
# No rebuild engine; still drop the build-only input so bundle contents are
|
||||
# consistent regardless of whether the rebuild ran. `|| true` keeps it
|
||||
# non-fatal under `set -e`.
|
||||
rm -f "$BUNDLE_DIR/static/_tailwind.src.css" || true
|
||||
echo "WARN: npx/node not found — shipping core's committed tailwind.min.css as-is." >&2
|
||||
echo " Bundled plugins using classes outside core's sheet may render unstyled." >&2
|
||||
fi
|
||||
|
||||
echo " Slopsmith server: $(du -sh "$BUNDLE_DIR" | cut -f1)"
|
||||
echo " Plugins: $(ls -d "$BUNDLE_DIR/plugins/"*/ 2>/dev/null | wc -l)"
|
||||
echo "=== Slopsmith bundle complete ==="
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# Download + verify the default General-MIDI soundfont into
|
||||
# resources/soundfonts/. Used by both CI and local `npm run bundle` on
|
||||
# all three platforms.
|
||||
#
|
||||
# URL + SHA256 are read from .build-config.json (external.soundfont_general_user).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
CONFIG="$PROJECT_DIR/.build-config.json"
|
||||
|
||||
SF_URL=$(python3 "$SCRIPT_DIR/parse-build-config.py" "$CONFIG" .external.soundfont_general_user.url)
|
||||
SF_SHA256=$(python3 "$SCRIPT_DIR/parse-build-config.py" "$CONFIG" .external.soundfont_general_user.sha256)
|
||||
SF_DIR="$PROJECT_DIR/resources/soundfonts"
|
||||
SF_FILE="$SF_DIR/GeneralUser-GS.sf2"
|
||||
|
||||
mkdir -p "$SF_DIR"
|
||||
|
||||
if [ -f "$SF_FILE" ]; then
|
||||
echo " Existing soundfont found — verifying checksum"
|
||||
else
|
||||
echo " Downloading GeneralUser-GS.sf2 (~32 MB) from $SF_URL"
|
||||
curl -sL --fail --retry 5 --retry-delay 5 --retry-all-errors "$SF_URL" -o "$SF_FILE"
|
||||
fi
|
||||
|
||||
# macOS ships `shasum -a 256`; Linux / Windows-git-bash ship `sha256sum`.
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
echo "${SF_SHA256} ${SF_FILE}" | sha256sum -c - >/dev/null
|
||||
else
|
||||
echo "${SF_SHA256} ${SF_FILE}" | shasum -a 256 -c - >/dev/null
|
||||
fi
|
||||
|
||||
echo " Soundfont: $(ls -lh "$SF_FILE" | awk '{print $5}') (SHA256 verified)"
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# Top-level bundle delegator. Calls the per-concern modular scripts so
|
||||
# local dev matches CI. Linux-focused; macOS/Windows bundling lives in
|
||||
# the GitHub Actions workflow because those platforms have quite
|
||||
# different packaging needs (dylibbundler, zip downloads, etc.).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
echo "=== Bundling Slopsmith Desktop ==="
|
||||
|
||||
bash "$SCRIPT_DIR/bundle-slopsmith.sh"
|
||||
# Skip Python bundling on non-Linux platforms (handled inline in platform scripts)
|
||||
if [[ "$(uname -s)" == "Linux" ]]; then
|
||||
bash "$SCRIPT_DIR/bundle-python.sh"
|
||||
fi
|
||||
# Skip binary bundling on non-Linux platforms (handled inline in platform scripts)
|
||||
if [[ "$(uname -s)" == "Linux" ]]; then
|
||||
bash "$SCRIPT_DIR/bundle-binaries.sh"
|
||||
fi
|
||||
bash "$SCRIPT_DIR/bundle-soundfont.sh"
|
||||
|
||||
# Default IRs — small copy step that doesn't need its own script.
|
||||
echo "=== Copying default IRs ==="
|
||||
mkdir -p "$PROJECT_DIR/resources/default-irs"
|
||||
cp "$PROJECT_DIR/models/cabs/"*.wav "$PROJECT_DIR/resources/default-irs/" 2>/dev/null || true
|
||||
echo " Default IRs: $(ls "$PROJECT_DIR/resources/default-irs/" | wc -l) file(s)"
|
||||
|
||||
echo ""
|
||||
echo "=== Bundle complete ==="
|
||||
echo " Total resources: $(du -sh "$PROJECT_DIR/resources" | cut -f1)"
|
||||
echo ""
|
||||
echo "Ready for: npm run dist"
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
# Shared skip list for bundled-binary shared-library handling.
|
||||
#
|
||||
# Sourced by both scripts/bundle-binaries.sh (which decides which libs
|
||||
# to copy into resources/bin/) and scripts/build-common.sh (which
|
||||
# audits that every NEEDED SONAME is satisfied). Keeping the list in
|
||||
# one place prevents drift: a name present in only one copy would make
|
||||
# the bundler and the audit disagree about which libs must be present.
|
||||
#
|
||||
# These are the low-level libc / loader pieces that MUST come from the
|
||||
# user's own glibc — bundling them across distros breaks the dynamic
|
||||
# linker.
|
||||
|
||||
is_skipped_lib() {
|
||||
case "$1" in
|
||||
libc.so*|libm.so*|libpthread.so*|libdl.so*|librt.so*|\
|
||||
ld-linux*|libresolv.so*|linux-vdso*|linux-gate*|\
|
||||
libnsl.so*|libutil.so*|libgcc_s.so*)
|
||||
return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// Smoke-test harness for the Windows VST sandbox path. Loads the addon,
|
||||
// spawns the sandbox subprocess for Guitar Rig 6, opens its editor, then
|
||||
// closes and shuts down cleanly. Used by clean-rerun.cmd on the test VM.
|
||||
//
|
||||
// Run from the repo root: `node scripts/dev/load-gr6.js > load-gr6-sandbox.log`.
|
||||
// The Guitar Rig 6 path is hardcoded to its standard Win11 install location;
|
||||
// adjust the GR6 path below if your install differs.
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
const addonPath = path.join(process.cwd(), 'build', 'Release', 'slopsmith_audio.node');
|
||||
console.log('[test] loading addon from', addonPath);
|
||||
const addon = require(addonPath);
|
||||
console.log('[test] addon loaded; methods:', Object.keys(addon).slice(0, 10).join(', '), '...');
|
||||
|
||||
// Global watchdog — best-effort coverage for *asynchronous* hang paths
|
||||
// (event-loop livelocks, setTimeout-stacked cleanup). loadVST is now a
|
||||
// Napi::AsyncWorker, so the libuv event loop continues to fire timers
|
||||
// while the load runs on a worker thread; this watchdog can pre-empt
|
||||
// a hung async load. addon.shutdown still parks the event loop
|
||||
// synchronously inside dispatchOnMessageThread, so if shutdown itself
|
||||
// hangs the timer callback never fires — a proper supervisor-process
|
||||
// + SIGKILL watchdog belongs in the CI harness (test-suite follow-up).
|
||||
//
|
||||
// The timer callback hard-exits — do NOT call addon.shutdown() here,
|
||||
// it would block on the same dispatchOnMessageThread the addon is
|
||||
// already stuck in and deadlock the process.
|
||||
const WATCHDOG_MS = 60000;
|
||||
const watchdog = setTimeout(() => {
|
||||
console.error(`[test] FATAL: watchdog tripped after ${WATCHDOG_MS} ms (async hang)`);
|
||||
process.exit(1);
|
||||
}, WATCHDOG_MS);
|
||||
|
||||
function failExit(msg) {
|
||||
if (msg) console.log('[test] FAIL:', msg);
|
||||
// addon.shutdown blocks on dispatchOnMessageThread (up to 15s) if
|
||||
// JUCE init partially succeeded; an init-failure path that triggered
|
||||
// *because* the message thread never came up would then time out
|
||||
// before the process exits. Cap with a hard process.exit timer so a
|
||||
// hung shutdown can't extend the failure window beyond 3s.
|
||||
const hardKill = setTimeout(() => {
|
||||
console.error('[test] FAIL: addon.shutdown hung, force-exiting');
|
||||
process.exit(2);
|
||||
}, 3000);
|
||||
hardKill.unref();
|
||||
try { addon.shutdown(); } catch (_) {}
|
||||
try { clearTimeout(watchdog); } catch (_) {}
|
||||
try { clearTimeout(hardKill); } catch (_) {}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('[test] addon.init()');
|
||||
try {
|
||||
addon.init();
|
||||
} catch (e) {
|
||||
failExit('EXCEPTION on init: ' + e.message);
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
// Allow override for CI / dev machines whose VST3 layout differs from
|
||||
// the standard "C:\Program Files\Common Files\VST3" install location.
|
||||
// The default Native Instruments install ships "Guitar Rig 6.vst3";
|
||||
// some installer versions or FX-only variants land as
|
||||
// "Guitar Rig 6 FX.vst3". Try both before giving up.
|
||||
const fs = require('fs');
|
||||
const candidates = process.env.GR6_PATH
|
||||
? [process.env.GR6_PATH]
|
||||
: [
|
||||
// NI's own installer drops into a vendor subdir; this is the
|
||||
// most common default on a fresh GR6 install.
|
||||
'C:\\Program Files\\Native Instruments\\VST3\\Guitar Rig 6.vst3',
|
||||
'C:\\Program Files\\Native Instruments\\VST3\\Guitar Rig 6 FX.vst3',
|
||||
// Some installs (and the existing CI fixture VM) drop into the
|
||||
// shared Common Files VST3 dir; keep these as fallbacks so the
|
||||
// existing smoke harness doesn't have to flip overnight.
|
||||
'C:\\Program Files\\Common Files\\VST3\\Guitar Rig 6.vst3',
|
||||
'C:\\Program Files\\Common Files\\VST3\\Guitar Rig 6 FX.vst3',
|
||||
];
|
||||
const gr6 = candidates.find(p => { try { return fs.existsSync(p); } catch (_) { return false; } });
|
||||
if (!gr6) {
|
||||
console.error('[test] FATAL: no Guitar Rig 6 install found at any of:');
|
||||
for (const p of candidates) console.error(' - ' + p);
|
||||
console.error('Set GR6_PATH to override (e.g. GR6_PATH="C:\\path\\to\\Guitar Rig 6.vst3" node scripts\\dev\\load-gr6.js).');
|
||||
try { addon.shutdown(); } catch (_) {}
|
||||
clearTimeout(watchdog);
|
||||
process.exit(2);
|
||||
return;
|
||||
}
|
||||
console.log('[test] calling addon.loadVST(' + gr6 + ')');
|
||||
let slot;
|
||||
try {
|
||||
// addon.loadVST is now a Promise<number> (Napi::AsyncWorker); await
|
||||
// it. The enclosing setTimeout callback was made async above.
|
||||
slot = await addon.loadVST(gr6);
|
||||
console.log('[test] loadVST returned slot:', slot);
|
||||
} catch (e) {
|
||||
failExit('EXCEPTION on loadVST: ' + e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Number.isInteger(slot) || slot < 0) {
|
||||
failExit('loadVST returned invalid slot: ' + String(slot));
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
console.log('[test] calling addon.openPluginEditor(' + slot + ')');
|
||||
let ok = false;
|
||||
try {
|
||||
ok = addon.openPluginEditor(slot);
|
||||
console.log('[test] openPluginEditor returned:', ok);
|
||||
} catch (e) {
|
||||
failExit('EXCEPTION on openPluginEditor: ' + e.message);
|
||||
return;
|
||||
}
|
||||
if (!ok) {
|
||||
try { addon.closePluginEditor(slot); } catch (_) {}
|
||||
failExit('openPluginEditor returned false');
|
||||
return;
|
||||
}
|
||||
console.log('[test] sleeping 5s for editor creation + potential crash...');
|
||||
setTimeout(() => {
|
||||
console.log('[test] still alive after editor wait; closing');
|
||||
try { addon.closePluginEditor(slot); } catch (e) {}
|
||||
try { addon.shutdown(); } catch (e) {}
|
||||
clearTimeout(watchdog);
|
||||
setTimeout(() => process.exit(0), 1000);
|
||||
}, 5000);
|
||||
}, 1500);
|
||||
}, 2000);
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
# Normalise ONNX Runtime install names on macOS (slopsmith#818).
|
||||
#
|
||||
# The prebuilt onnxruntime macOS dylib bakes the *build machine's absolute
|
||||
# extraction path* (e.g. /Users/runner/work/.../_deps/onnxruntime/.../lib/
|
||||
# libonnxruntime.1.20.1.dylib) into its LC_ID_DYLIB. At link time the linker
|
||||
# copies that absolute path into slopsmith_audio.node's LC_LOAD_DYLIB. The
|
||||
# .node carries the correct `@loader_path` rpath (set in src/audio/
|
||||
# CMakeLists.txt) and the runtime is staged right beside it — but dyld never
|
||||
# consults the rpath, because the load command is an absolute path, not an
|
||||
# `@rpath/...` one. Result: the addon loads fine *only* on the CI runner;
|
||||
# every other Mac fails to find onnxruntime, MlNoteDetector can't initialise,
|
||||
# and the engine silently falls back to YIN ("ML detection: OFF").
|
||||
#
|
||||
# Fix: rewrite the install names to be `@rpath`-relative so the addon's
|
||||
# existing `@loader_path` rpath resolves the co-located runtime everywhere.
|
||||
# Idempotent — re-running is a no-op once the names are already `@rpath/...`.
|
||||
#
|
||||
# Usage: fix-onnxruntime-install-names.sh <slopsmith_audio.node> <runtime-lib-name>
|
||||
# $1 absolute path to the built slopsmith_audio.node
|
||||
# $2 runtime lib filename, e.g. libonnxruntime.1.20.1.dylib
|
||||
#
|
||||
# Invoked as a POST_BUILD step on Apple platforms only. install_name_tool
|
||||
# invalidates any existing (adhoc) code signature, so we re-adhoc-sign the
|
||||
# touched binaries afterwards; electron-builder overrides this with the real
|
||||
# Developer ID signature when it packages + signs the .app.
|
||||
set -euo pipefail
|
||||
|
||||
node="${1:?path to slopsmith_audio.node required}"
|
||||
libname="${2:?onnxruntime runtime lib name required}"
|
||||
dir="$(cd "$(dirname "$node")" && pwd)"
|
||||
runtime="$dir/$libname"
|
||||
providers="$dir/libonnxruntime_providers_shared.dylib"
|
||||
|
||||
# Re-adhoc-sign a binary after rewriting its load commands. `install_name_tool`
|
||||
# strips the lightweight adhoc signature the linker attaches on Apple Silicon;
|
||||
# without one, dyld refuses to load the addon on a local (unsigned) dev build.
|
||||
resign() {
|
||||
codesign --remove-signature "$1" 2>/dev/null || true
|
||||
codesign --force --sign - "$1" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Discover the addon's current (absolute) reference to the runtime, if any.
|
||||
# Skip when it is already `@rpath/...` so re-runs / already-correct builds are
|
||||
# no-ops. Match the exact runtime filename to avoid touching unrelated entries.
|
||||
old_ref="$(otool -L "$node" | awk -v L="$libname" 'index($1,L) && $1 !~ /^@rpath\// {print $1; exit}')"
|
||||
if [[ -n "${old_ref:-}" ]]; then
|
||||
install_name_tool -change "$old_ref" "@rpath/$libname" "$node"
|
||||
resign "$node"
|
||||
echo "fix-onnxruntime-install-names: $node -> @rpath/$libname"
|
||||
fi
|
||||
|
||||
# Normalise the runtime's own id so future links (and any consumer that reads
|
||||
# LC_ID_DYLIB) get `@rpath/...` instead of an absolute build path.
|
||||
if [[ -f "$runtime" ]]; then
|
||||
cur_id="$(otool -D "$runtime" | sed -n '2p')"
|
||||
if [[ "$cur_id" != "@rpath/$libname" ]]; then
|
||||
install_name_tool -id "@rpath/$libname" "$runtime"
|
||||
resign "$runtime"
|
||||
echo "fix-onnxruntime-install-names: id $runtime -> @rpath/$libname"
|
||||
fi
|
||||
fi
|
||||
|
||||
# providers_shared is dlopen()'d by the runtime at session-init and links the
|
||||
# main runtime by the same baked absolute path; rewrite it too so a build that
|
||||
# ships the providers stub doesn't reintroduce the absolute dependency.
|
||||
if [[ -f "$providers" ]]; then
|
||||
prov_name="$(basename "$providers")"
|
||||
cur_pid="$(otool -D "$providers" | sed -n '2p')"
|
||||
if [[ "$cur_pid" != "@rpath/$prov_name" ]]; then
|
||||
install_name_tool -id "@rpath/$prov_name" "$providers"
|
||||
fi
|
||||
prov_ref="$(otool -L "$providers" | awk -v L="$libname" 'index($1,L) && $1 !~ /^@rpath\// {print $1; exit}')"
|
||||
if [[ -n "${prov_ref:-}" ]]; then
|
||||
install_name_tool -change "$prov_ref" "@rpath/$libname" "$providers"
|
||||
fi
|
||||
resign "$providers"
|
||||
fi
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Parse `.build-config.json` and emit a value (or the whole doc).
|
||||
|
||||
Usage:
|
||||
parse-build-config.py <path> # pretty-print the whole document
|
||||
parse-build-config.py <path> .versions.node # print a single value
|
||||
|
||||
Keys are dot-delimited, e.g. `.external.rs2014net.commit`. Plain JSON
|
||||
only — if a future config needs comments, add a proper JSONC parser
|
||||
(naive regex-based `//` stripping breaks on URLs like `https://...`).
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) < 2:
|
||||
print(f"Usage: {sys.argv[0]} <path> [.json.path]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
config_file = sys.argv[1]
|
||||
json_path = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
|
||||
try:
|
||||
with open(config_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
except FileNotFoundError:
|
||||
print(f"Error: file not found: {config_file}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error: invalid JSON in {config_file}: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if json_path is None:
|
||||
print(json.dumps(data, indent=2))
|
||||
return
|
||||
|
||||
value = data
|
||||
for key in json_path.lstrip('.').split('.'):
|
||||
try:
|
||||
value = value[key]
|
||||
except (KeyError, TypeError):
|
||||
print(f"Error: key {json_path!r} not found in {config_file}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print(value)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+158
@@ -0,0 +1,158 @@
|
||||
#!/bin/bash
|
||||
# Development environment setup
|
||||
# Installs all dependencies and verifies the build chain
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
echo "=== Slopsmith Desktop Development Setup ==="
|
||||
echo ""
|
||||
|
||||
# Check prerequisites
|
||||
echo "Checking prerequisites..."
|
||||
|
||||
check_command() {
|
||||
if command -v "$1" &>/dev/null; then
|
||||
echo " [OK] $1"
|
||||
else
|
||||
echo " [MISSING] $1 — $2"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Verify the media chain: ffmpeg (WEM/OGG transcode), ffprobe (demucs probes
|
||||
# stream metadata before invoking ffmpeg), and ffmpeg's libvorbis encoder
|
||||
# (Sloppak conversion encodes .ogg with `-c:a libvorbis`).
|
||||
# $1 — install hint shown when ffmpeg/ffprobe are missing.
|
||||
# $2 — platform-specific remediation shown when libvorbis is absent.
|
||||
check_media_chain() {
|
||||
local install_hint="$1"
|
||||
local libvorbis_hint="$2"
|
||||
command -v ffmpeg >/dev/null 2>&1 && echo " [OK] ffmpeg" || echo " [MISSING] ffmpeg ($install_hint)"
|
||||
command -v ffprobe >/dev/null 2>&1 && echo " [OK] ffprobe" || echo " [MISSING] ffprobe (ships with ffmpeg — $install_hint)"
|
||||
if command -v ffmpeg >/dev/null 2>&1; then
|
||||
if ffmpeg -hide_banner -encoders 2>/dev/null | grep -wq libvorbis; then
|
||||
echo " [OK] ffmpeg libvorbis encoder"
|
||||
else
|
||||
echo " [WARN] ffmpeg lacks the libvorbis encoder — Sloppak conversion"
|
||||
echo " falls back to the lower-quality built-in vorbis encoder."
|
||||
echo " $libvorbis_hint"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
check_command node "Install Node.js 20+" || exit 1
|
||||
check_command npm "Comes with Node.js" || exit 1
|
||||
check_command cmake "Install cmake (apt/brew/pacman)" || exit 1
|
||||
check_command python3 "Install Python 3.12+" || exit 1
|
||||
check_command git "Install git" || exit 1
|
||||
|
||||
# Platform-specific checks
|
||||
case "$(uname -s)" in
|
||||
Linux)
|
||||
echo ""
|
||||
echo "Checking Linux build dependencies..."
|
||||
pkg-config --exists alsa 2>/dev/null && echo " [OK] ALSA" || echo " [MISSING] ALSA dev headers (apt: libasound2-dev / pacman: alsa-lib)"
|
||||
pkg-config --exists jack 2>/dev/null && echo " [OK] JACK" || echo " [MISSING] JACK dev headers (apt: libjack-jackd2-dev / pacman: jack2)"
|
||||
pkg-config --exists freetype2 2>/dev/null && echo " [OK] freetype2" || echo " [MISSING] freetype2 (apt: libfreetype-dev / pacman: freetype2)"
|
||||
pkg-config --exists x11 2>/dev/null && echo " [OK] X11" || echo " [MISSING] X11 dev headers"
|
||||
pkg-config --exists xrandr 2>/dev/null && echo " [OK] Xrandr" || echo " [MISSING] Xrandr dev headers"
|
||||
pkg-config --exists xcursor 2>/dev/null && echo " [OK] Xcursor" || echo " [MISSING] Xcursor dev headers"
|
||||
pkg-config --exists xinerama 2>/dev/null && echo " [OK] Xinerama" || echo " [MISSING] Xinerama dev headers"
|
||||
check_media_chain "apt: ffmpeg / pacman: ffmpeg" \
|
||||
"Most distro ffmpeg packages enable libvorbis — reinstall your distro's ffmpeg if this build does not."
|
||||
command -v vgmstream-cli >/dev/null 2>&1 && echo " [OK] vgmstream-cli" || echo " [MISSING] vgmstream-cli (AUR: yay -S vgmstream-cli-bin / or github.com/vgmstream/vgmstream/releases)"
|
||||
;;
|
||||
Darwin)
|
||||
echo ""
|
||||
echo "Checking macOS dependencies..."
|
||||
xcode-select -p &>/dev/null && echo " [OK] Xcode Command Line Tools" || echo " [MISSING] Run: xcode-select --install"
|
||||
check_media_chain "brew install ffmpeg" \
|
||||
"Homebrew's ffmpeg 8.1.1+ omits libvorbis — install a static ffmpeg build instead (packaged builds bundle one)."
|
||||
command -v vgmstream-cli >/dev/null 2>&1 && echo " [OK] vgmstream-cli" || echo " [MISSING] vgmstream-cli (brew install vgmstream)"
|
||||
;;
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
echo ""
|
||||
echo "Checking Windows (Git Bash) dependencies..."
|
||||
check_media_chain "install ffmpeg and add it to PATH (e.g. winget install Gyan.FFmpeg)" \
|
||||
"Most prebuilt Windows ffmpeg builds (e.g. Gyan) include libvorbis — pick one that does."
|
||||
command -v vgmstream-cli >/dev/null 2>&1 && echo " [OK] vgmstream-cli" || echo " [MISSING] vgmstream-cli (github.com/vgmstream/vgmstream/releases — add to PATH)"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
|
||||
# Initialize submodules
|
||||
echo "Initializing git submodules..."
|
||||
git submodule update --init --recursive 2>/dev/null || echo " Note: Run 'git submodule update --init --recursive' manually if this is a fresh clone"
|
||||
|
||||
# Install npm dependencies
|
||||
echo ""
|
||||
echo "Installing npm dependencies..."
|
||||
npm install
|
||||
|
||||
# Locate Slopsmith. Matches the build scripts (bundle-slopsmith.sh,
|
||||
# bundle-python.sh, build-macos.sh): an explicit $SLOPSMITH_DIR is honoured
|
||||
# verbatim — a typo or partial checkout there is surfaced, never silently
|
||||
# masked by a sibling or legacy checkout. Only when $SLOPSMITH_DIR is unset
|
||||
# do we fall back to ../slopsmith then ~/Repositories/slopsmith, and a
|
||||
# fallback candidate only counts if it actually contains server.py.
|
||||
SLOPSMITH_DIR_ENV="${SLOPSMITH_DIR:-}"
|
||||
if [ -z "${SLOPSMITH_DIR:-}" ]; then
|
||||
if [ -f "$PROJECT_DIR/../slopsmith/server.py" ]; then
|
||||
SLOPSMITH_DIR="$PROJECT_DIR/../slopsmith"
|
||||
elif [ -f "$HOME/Repositories/slopsmith/server.py" ]; then
|
||||
SLOPSMITH_DIR="$HOME/Repositories/slopsmith"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "${SLOPSMITH_DIR:-}" ] && [ -f "$SLOPSMITH_DIR/server.py" ]; then
|
||||
SLOPSMITH_DIR="$(cd "$SLOPSMITH_DIR" && pwd)"
|
||||
echo ""
|
||||
echo "Slopsmith found at: $SLOPSMITH_DIR"
|
||||
|
||||
# On Windows/Git Bash a bash check passes for an MSYS path like
|
||||
# /c/src/slopsmith, but `npm run dev` (Electron) resolves $SLOPSMITH_DIR
|
||||
# with Node, which needs a native Windows path. Warn before setup
|
||||
# reports a config that dev mode would still fail to start.
|
||||
case "$(uname -s)" in
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
if [ -n "$SLOPSMITH_DIR_ENV" ] && [ "${SLOPSMITH_DIR_ENV#/}" != "$SLOPSMITH_DIR_ENV" ]; then
|
||||
echo ""
|
||||
echo " NOTE: \$SLOPSMITH_DIR is an MSYS/Git-Bash path. 'npm run dev' needs a"
|
||||
echo " native Windows path. Re-export it as:"
|
||||
if command -v cygpath >/dev/null 2>&1; then
|
||||
echo " export SLOPSMITH_DIR='$(cygpath -w "$SLOPSMITH_DIR_ENV")'"
|
||||
else
|
||||
echo " a native path such as C:\\src\\slopsmith"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
PYTHON="${PROJECT_DIR}/.venv/bin/python3"
|
||||
[ -x "$PYTHON" ] || PYTHON="python3"
|
||||
echo "Checking Python dependencies (\"$PYTHON\")..."
|
||||
"$PYTHON" -c "import fastapi" 2>/dev/null && echo " [OK] fastapi" || echo " [MISSING] \"$PYTHON\" -m pip install -r \"$SLOPSMITH_DIR/requirements.txt\""
|
||||
"$PYTHON" -c "import uvicorn" 2>/dev/null && echo " [OK] uvicorn" || echo " [MISSING] \"$PYTHON\" -m pip install -r \"$SLOPSMITH_DIR/requirements.txt\""
|
||||
elif [ -n "${SLOPSMITH_DIR:-}" ]; then
|
||||
echo ""
|
||||
echo "WARNING: \$SLOPSMITH_DIR is set to '$SLOPSMITH_DIR' but no server.py was found there."
|
||||
echo " Fix the path or unset \$SLOPSMITH_DIR to fall back to ../slopsmith or ~/Repositories/slopsmith."
|
||||
else
|
||||
echo ""
|
||||
echo "WARNING: Slopsmith not found. Set \$SLOPSMITH_DIR, clone to $PROJECT_DIR/../slopsmith, or use ~/Repositories/slopsmith"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Setup Complete ==="
|
||||
echo ""
|
||||
echo "Build commands:"
|
||||
echo " npm run build:audio # Build JUCE native addon"
|
||||
echo " npm run build:ts # Compile TypeScript"
|
||||
echo " npm run dev # Run in development mode"
|
||||
echo " npm run dist:linux # Build Linux package"
|
||||
Executable
+98
@@ -0,0 +1,98 @@
|
||||
#!/bin/bash
|
||||
# Sign every native binary inside resources/bin and resources/python/runtime
|
||||
# with the Developer ID Application certificate so the .app passes
|
||||
# notarization. Runs as part of the macOS bundle step, after binaries
|
||||
# are downloaded/copied but before verify_bundled_binaries runs them
|
||||
# (signing also clears the quarantine attribute that GitHub release
|
||||
# downloads carry by default).
|
||||
#
|
||||
# Skips silently when APPLE_SIGNING_IDENTITY is unset — local builds
|
||||
# without a Developer ID cert still produce a (Gatekeeper-rejected)
|
||||
# unsigned .app, same as before this script existed.
|
||||
#
|
||||
# Required env (set by CI):
|
||||
# APPLE_SIGNING_IDENTITY Full identity string, e.g.
|
||||
# "Developer ID Application: Name (TEAMID)"
|
||||
# Must already be present in the active keychain.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -z "${APPLE_SIGNING_IDENTITY:-}" ]]; then
|
||||
echo "[sign-macos] APPLE_SIGNING_IDENTITY not set — skipping (unsigned build)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$OSTYPE" != "darwin"* ]]; then
|
||||
echo "[sign-macos] not on macOS — skipping" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
ENTITLEMENTS="$PROJECT_DIR/resources/entitlements.mac.plist"
|
||||
|
||||
if [[ ! -f "$ENTITLEMENTS" ]]; then
|
||||
echo "[sign-macos] entitlements file missing: $ENTITLEMENTS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sign_one() {
|
||||
local target="$1"
|
||||
[[ -e "$target" ]] || return 0
|
||||
# Skip symlinks — they get followed during bundling and signing the
|
||||
# link target separately is enough.
|
||||
[[ -L "$target" ]] && return 0
|
||||
echo " $target"
|
||||
codesign --force --options runtime --timestamp \
|
||||
--sign "$APPLE_SIGNING_IDENTITY" \
|
||||
--entitlements "$ENTITLEMENTS" \
|
||||
"$target"
|
||||
}
|
||||
|
||||
echo "[sign-macos] signing bundled binaries with: $APPLE_SIGNING_IDENTITY"
|
||||
|
||||
# 1. Top-level executables and bundled dylibs in resources/bin.
|
||||
# dylibbundler copies fluidsynth's deps into this directory and
|
||||
# rewrites their install names to @executable_path/, so each one
|
||||
# has to be signed individually before the app bundle is built.
|
||||
BIN_DIR="$PROJECT_DIR/resources/bin"
|
||||
if [[ -d "$BIN_DIR" ]]; then
|
||||
while IFS= read -r -d '' f; do
|
||||
sign_one "$f"
|
||||
done < <(find "$BIN_DIR" -maxdepth 1 -type f \( -perm -u+x -o -name '*.dylib' \) -print0)
|
||||
fi
|
||||
|
||||
# 2. Embedded CPython runtime — interpreter + libpython dylib + every
|
||||
# compiled extension. Notarization rejects the bundle if any of
|
||||
# these is unsigned.
|
||||
PY_RUNTIME="$PROJECT_DIR/resources/python/runtime"
|
||||
if [[ -d "$PY_RUNTIME" ]]; then
|
||||
# Interpreter binaries (python3, python3.x, etc.)
|
||||
while IFS= read -r -d '' f; do
|
||||
sign_one "$f"
|
||||
done < <(find "$PY_RUNTIME/bin" -maxdepth 1 -type f -perm -u+x -print0 2>/dev/null || true)
|
||||
|
||||
# libpython dylib(s) and any other shared libs
|
||||
while IFS= read -r -d '' f; do
|
||||
sign_one "$f"
|
||||
done < <(find "$PY_RUNTIME/lib" -maxdepth 2 -type f -name '*.dylib' -print0 2>/dev/null || true)
|
||||
|
||||
# Compiled extension modules — both stdlib lib-dynload and any
|
||||
# site-packages C extensions installed via pip.
|
||||
while IFS= read -r -d '' f; do
|
||||
sign_one "$f"
|
||||
done < <(find "$PY_RUNTIME/lib" -type f \( -name '*.so' -o -name '*.dylib' \) -print0 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
echo "[sign-macos] done"
|
||||
|
||||
# Spot-check a couple of binaries so a botched sign call fails the
|
||||
# build here rather than mid-notarization 2 minutes later.
|
||||
for probe in "$BIN_DIR/fluidsynth" "$BIN_DIR/ffmpeg" "$BIN_DIR/vgmstream-cli"; do
|
||||
if [[ -f "$probe" ]]; then
|
||||
codesign --verify --strict "$probe" || {
|
||||
echo "[sign-macos] verification failed: $probe" >&2
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
done
|
||||
Reference in New Issue
Block a user