use std::env;
use std::fs;
use std::path::{Path, PathBuf};
fn main() {
if env::var("CARGO_CFG_TARGET_ARCH").as_deref() == Ok("wasm32") {
build_wasm();
} else {
build_native();
}
}
fn supercollider_source() -> PathBuf {
let dir = resolve_pinned_source("SCSYNTH_SYS_SUPERCOLLIDER_DIR", "supercollider", true);
assert!(
dir.join("CMakeLists.txt").is_file(),
"SuperCollider source missing/incomplete at {} - set SCSYNTH_SYS_SUPERCOLLIDER_DIR to a \
checkout, or allow the build to fetch it (needs `git` + network)",
dir.display(),
);
dir
}
fn build_native() {
let sc_src = supercollider_source();
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let sc = out_dir.join("sc-src");
if !sc.join("CMakeLists.txt").is_file() {
copy_tree(&sc_src, &sc);
patch_audioapi_external(&sc);
}
sync_file(
&PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("csrc/SC_ExternalDriver.cpp"),
&sc.join("server/scsynth/SC_ExternalDriver.cpp"),
);
let inc_server = sc.join("include/server");
let inc_common = sc.join("include/common");
let inc_plugin = sc.join("include/plugin_interface");
let inc_common_dir = sc.join("common");
let dst = cmake::Config::new(&sc)
.define("CMAKE_BUILD_TYPE", "Release")
.define("LIBSCSYNTH", "OFF")
.define("STATIC_PLUGINS", "ON")
.define("SUPERNOVA", "OFF")
.define("SC_QT", "OFF")
.define("SC_IDE", "OFF")
.define("SCLANG_SERVER", "OFF")
.define("NO_X11", "ON")
.define("NO_AVAHI", "ON")
.define("SC_HIDAPI", "OFF")
.define("SC_ABLETON_LINK", "OFF")
.define("ENABLE_TESTSUITE", "OFF")
.define("INSTALL_HELP", "OFF")
.define("AUDIOAPI", "external")
.build_target("libscsynth")
.build();
let build_dir = dst.join("build");
let scsynth_a = find_file(&build_dir, "libscsynth.a")
.unwrap_or_else(|| panic!("libscsynth.a not found under {}", build_dir.display()));
let tlsf_a = find_file(&build_dir, "libtlsf.a")
.unwrap_or_else(|| panic!("libtlsf.a not found under {}", build_dir.display()));
link_search(scsynth_a.parent().unwrap());
link_search(tlsf_a.parent().unwrap());
println!("cargo:rustc-link-lib=static=scsynth");
println!("cargo:rustc-link-lib=static=tlsf");
cc::Build::new()
.cpp(true)
.std("c++17")
.include(&inc_server)
.include(&inc_common)
.include(&inc_plugin)
.include(&inc_common_dir)
.include(sc.join("external_libraries/boost"))
.include(sc.join("external_libraries/boost_sync/include"))
.define("SC_MEMORY_ALIGNMENT", "32")
.file("csrc/shim.cpp")
.compile("scsynth_shim");
pkg_config::probe_library("sndfile").expect("libsndfile (sndfile.pc)");
pkg_config::probe_library("fftw3f").expect("fftw3f (fftw3f.pc)");
for lib in ["pthread", "dl", "rt", "m"] {
println!("cargo:rustc-link-lib=dylib={lib}");
}
println!("cargo:rustc-link-lib=dylib=stdc++");
let bindings = bindgen::Builder::default()
.header(inc_server.join("SC_WorldOptions.h").to_string_lossy())
.clang_args(["-x", "c++", "-std=c++17"])
.clang_arg(format!("-I{}", inc_server.display()))
.clang_arg(format!("-I{}", inc_common.display()))
.clang_arg(format!("-I{}", inc_plugin.display()))
.clang_arg(format!("-I{}", inc_common_dir.display()))
.clang_arg("-DSC_MEMORY_ALIGNMENT=32");
write_bindings(allowlist(bindings));
println!("cargo:rerun-if-changed=csrc/shim.cpp");
println!("cargo:rerun-if-changed=csrc/SC_ExternalDriver.cpp");
println!("cargo:rerun-if-changed=build.rs");
}
fn build_wasm() {
let sc_src = supercollider_source()
.canonicalize()
.expect("SuperCollider source not found");
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let sc = out_dir.join("sc");
if !sc.join("server/scsynth/SC_World.cpp").is_file() {
copy_tree(&sc_src, &sc);
patch_sc_sources(&sc);
generate_version_header(&sc);
}
let libc_includes: Vec<String> = env::var("DEP_WASM32_LIBC_INCLUDE")
.unwrap_or_default()
.split(':')
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect();
let libcxx_include = env::var("DEP_WASM32_LIBCXX_INCLUDE").unwrap_or_default();
assert!(
!libc_includes.is_empty() && !libcxx_include.is_empty(),
"DEP_WASM32_LIBC_INCLUDE / DEP_WASM32_LIBCXX_INCLUDE not set - the from-source libc/libc++ \
deps must export them via their own build scripts",
);
let inc = [
sc.join("include/server"),
sc.join("include/common"),
sc.join("include/plugin_interface"),
sc.join("common"),
sc.join("server/scsynth"),
sc.join("server/plugins"),
sc.join("external_libraries/boost"),
sc.join("external_libraries/boost_sync/include"),
sc.join("external_libraries/nova-simd"),
sc.join("external_libraries/nova-tt"),
sc.join("external_libraries/TLSF-2.4.6/src"),
];
let core = [
"server/scsynth/SC_BufGen.cpp",
"server/scsynth/SC_Graph.cpp",
"server/scsynth/SC_GraphDef.cpp",
"server/scsynth/SC_Group.cpp",
"server/scsynth/SC_Lib_Cintf.cpp",
"server/scsynth/SC_Lib.cpp",
"server/scsynth/SC_MiscCmds.cpp",
"server/scsynth/SC_Node.cpp",
"server/scsynth/SC_Rate.cpp",
"server/scsynth/SC_SequencedCommand.cpp",
"server/scsynth/SC_Str4.cpp",
"server/scsynth/SC_Unit.cpp",
"server/scsynth/SC_UnitDef.cpp",
"server/scsynth/SC_World.cpp",
"server/scsynth/SC_CoreAudio.cpp",
];
let common = [
"common/SC_fftlib.cpp",
"common/SC_AllocPool.cpp",
"common/SC_Errors.cpp",
"common/SC_StringBuffer.cpp",
"common/SC_StringParser.cpp",
"common/Samp.cpp",
];
let c_sources = [
"common/fftlib.c",
"external_libraries/TLSF-2.4.6/src/tlsf.c",
];
let plugins = [
"server/plugins/IOUGens.cpp",
"server/plugins/OscUGens.cpp",
"server/plugins/DelayUGens.cpp",
"server/plugins/BinaryOpUGens.cpp",
"server/plugins/FilterUGens.cpp",
"server/plugins/GendynUGens.cpp",
"server/plugins/LFUGens.cpp",
"server/plugins/NoiseUGens.cpp",
"server/plugins/MulAddUGens.cpp",
"server/plugins/GrainUGens.cpp",
"server/plugins/PanUGens.cpp",
"server/plugins/ReverbUGens.cpp",
"server/plugins/TriggerUGens.cpp",
"server/plugins/UnaryOpUGens.cpp",
"server/plugins/PhysicalModelingUGens.cpp",
"server/plugins/TestUGens.cpp",
"server/plugins/DemandUGens.cpp",
"server/plugins/DynNoiseUGens.cpp",
"server/plugins/FFTInterfaceTable.cpp",
"server/plugins/FFT_UGens.cpp",
"server/plugins/PV_UGens.cpp",
"server/plugins/PartitionedConvolution.cpp",
];
let mut c = cc::Build::new();
for d in &libc_includes {
c.include(d);
}
c.flag("-Wno-builtin-requires-header")
.file("csrc/libc_gap.c")
.compile("libc_gap");
let mut sc_c = cc::Build::new();
sc_c.std("c11")
.flag("-w")
.define("SC_FFT_GREEN", None)
.define("SC_WASM", None)
.define("NDEBUG", None);
for d in &libc_includes {
sc_c.include(d);
}
for d in &inc {
sc_c.include(d);
}
for f in &c_sources {
sc_c.file(sc.join(f));
}
sc_c.compile("scsynth_wasm_c");
let mut sc_build = cc::Build::new();
sc_build
.cpp(true)
.std("c++17")
.cpp_link_stdlib(None)
.flag("-fwasm-exceptions")
.flag("-w") .define("NO_LIBSNDFILE", None)
.define("NO_X11", None) .define("STATIC_PLUGINS", None)
.define("SC_MEMORY_ALIGNMENT", "32")
.define("SC_FFT_GREEN", None)
.define("SC_WASM", None)
.define("SC_AUDIO_API", "SC_AUDIO_API_WEBAUDIO")
.define("NDEBUG", None);
sc_build.include(&libcxx_include);
for d in &libc_includes {
sc_build.include(d);
}
for d in &inc {
sc_build.include(d);
}
for f in core.iter().chain(common.iter()).chain(plugins.iter()) {
sc_build.file(sc.join(f));
}
sc_build.file("csrc/SC_WasmPump.cpp");
sc_build.file("csrc/shim.cpp");
sc_build.compile("scsynth_wasm");
println!("cargo::rustc-link-lib=wasm32-libcxx");
println!("cargo::rustc-link-lib=wasm32-libc");
let mut bindings = bindgen::Builder::default()
.header(
sc.join("include/server/SC_WorldOptions.h")
.to_string_lossy(),
)
.clang_args(["-x", "c++", "-std=c++17"])
.clang_arg("-target")
.clang_arg("wasm32-unknown-unknown")
.clang_arg("-DSC_MEMORY_ALIGNMENT=32")
.clang_arg("-DSC_WASM")
.clang_arg("-DSC_FFT_GREEN")
.clang_arg("-DSC_AUDIO_API=SC_AUDIO_API_WEBAUDIO");
for d in &inc {
bindings = bindings.clang_arg(format!("-I{}", d.display()));
}
bindings = bindings.clang_arg(format!("-I{libcxx_include}"));
for d in &libc_includes {
bindings = bindings.clang_arg(format!("-I{d}"));
}
write_bindings(allowlist(bindings));
println!("cargo::rerun-if-changed=csrc/SC_WasmPump.cpp");
println!("cargo::rerun-if-changed=csrc/shim.cpp");
println!("cargo::rerun-if-changed=csrc/libc_gap.c");
println!("cargo::rerun-if-changed=build.rs");
}
fn allowlist(builder: bindgen::Builder) -> bindgen::Builder {
builder
.allowlist_function("World_.*")
.allowlist_function("scprintf")
.allowlist_function("SetPrintFunc")
.allowlist_type("WorldOptions")
.allowlist_type("ReplyFunc")
.allowlist_type("PrintFunc")
.allowlist_type("World")
.allowlist_type("HiddenWorld")
.opaque_type("World")
.opaque_type("HiddenWorld")
.opaque_type("ReplyAddress")
.opaque_type("SndBuf")
.blocklist_type("std::.*")
.blocklist_type("boost::.*")
}
fn write_bindings(builder: bindgen::Builder) {
let bindings = builder.generate().expect("failed to generate bindings");
let out = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out.join("bindings.rs"))
.expect("failed to write bindings");
}
fn resolve_pinned_source(env_var: &str, name: &str, recurse_submodules: bool) -> PathBuf {
println!("cargo::rerun-if-env-changed={env_var}");
if let Some(dir) = env::var_os(env_var) {
return PathBuf::from(dir);
}
let (url, rev) = read_pin();
let dir = source_cache_dir(name, &rev);
if !dir.join(".fetched").is_file() {
fetch_pinned_source(&url, &rev, &dir, recurse_submodules);
fs::write(dir.join(".fetched"), &rev).unwrap();
}
dir
}
fn read_pin() -> (String, String) {
let path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("source.toml");
println!("cargo::rerun-if-changed={}", path.display());
let text = fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
let field = |key: &str| -> String {
for line in text.lines() {
let line = line.trim();
if line.starts_with('#') {
continue;
}
if let Some((k, v)) = line.split_once('=')
&& k.trim() == key
{
return v.trim().trim_matches('"').to_string();
}
}
panic!("`{key}` not found in {}", path.display());
};
(field("url"), field("rev"))
}
fn source_cache_dir(name: &str, rev: &str) -> PathBuf {
let base = env::var_os("SCSYNTH_SRC_CACHE_DIR")
.map(PathBuf::from)
.or_else(|| env::var_os("XDG_CACHE_HOME").map(|c| PathBuf::from(c).join("scsynth-rs")))
.or_else(|| env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache/scsynth-rs")))
.unwrap_or_else(|| PathBuf::from(env::var("OUT_DIR").unwrap()).join("scsynth-rs-src"));
base.join(name).join(rev)
}
fn fetch_pinned_source(url: &str, rev: &str, dir: &Path, recurse_submodules: bool) {
let _ = fs::remove_dir_all(dir);
fs::create_dir_all(dir).unwrap();
git(dir, &["init", "-q"]);
git(dir, &["remote", "add", "origin", url]);
let target = if try_git(dir, &["fetch", "-q", "--depth", "1", "origin", rev]) {
"FETCH_HEAD".to_string()
} else {
git(dir, &["fetch", "-q", "origin"]);
rev.to_string()
};
git(dir, &["checkout", "-q", "--detach", &target]);
if recurse_submodules {
git(dir, &["submodule", "update", "--init", "--recursive"]);
}
}
fn git(dir: &Path, args: &[&str]) {
assert!(
try_git(dir, args),
"git {args:?} failed in {}",
dir.display()
);
}
fn try_git(dir: &Path, args: &[&str]) -> bool {
std::process::Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn link_search(dir: &Path) {
println!("cargo:rustc-link-search=native={}", dir.display());
}
fn copy_tree(src: &Path, dst: &Path) {
fs::create_dir_all(dst).unwrap();
for entry in fs::read_dir(src).unwrap() {
let entry = entry.unwrap();
if entry.file_name() == ".git" {
continue;
}
let from = entry.path();
let to = dst.join(entry.file_name());
let file_type = entry.file_type().unwrap();
if file_type.is_dir() {
copy_tree(&from, &to);
} else if file_type.is_symlink() {
let target = fs::read_link(&from).unwrap();
let _ = std::os::unix::fs::symlink(target, &to);
} else {
fs::copy(&from, &to).unwrap();
use std::os::unix::fs::PermissionsExt;
let perms = fs::metadata(&to).unwrap().permissions();
let mode = perms.mode();
if mode & 0o200 == 0 {
fs::set_permissions(&to, fs::Permissions::from_mode(mode | 0o200)).unwrap();
}
}
}
}
fn sync_file(src: &Path, dst: &Path) {
let new = fs::read(src).unwrap();
if fs::read(dst).is_ok_and(|old| old == new) {
return;
}
fs::create_dir_all(dst.parent().unwrap()).unwrap();
fs::write(dst, new).unwrap();
}
fn find_file(root: &Path, name: &str) -> Option<PathBuf> {
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
let entries = fs::read_dir(&dir).ok()?;
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else if path.file_name().is_some_and(|n| n == name) {
return Some(path);
}
}
}
None
}
fn patch_audioapi_external(sc: &Path) {
for rel in ["server/scsynth/CMakeLists.txt", "lang/CMakeLists.txt"] {
let path = sc.join(rel);
let regex_old = "^(jack|coreaudio|portaudio|bela|webaudio)$";
let regex_new = "^(jack|coreaudio|portaudio|bela|webaudio|external)$";
let mut text = fs::read_to_string(&path).unwrap();
assert!(
text.contains(regex_old),
"AUDIOAPI validation regex not found in {rel} - SuperCollider CMake layout changed"
);
text = text.replace(regex_old, regex_new);
fs::write(&path, text).unwrap();
}
let path = sc.join("server/scsynth/CMakeLists.txt");
let mut text = fs::read_to_string(&path).unwrap();
let anchor = "add_definitions(\"-DSC_AUDIO_API=SC_AUDIO_API_JACK\")";
let injected = concat!(
"add_definitions(\"-DSC_AUDIO_API=SC_AUDIO_API_JACK\")\n",
"elseif(AUDIOAPI STREQUAL external)\n",
"\tlist(APPEND scsynth_sources ${CMAKE_SOURCE_DIR}/server/scsynth/SC_ExternalDriver.cpp)\n",
"\tadd_definitions(\"-DSC_AUDIO_API_EXTERNAL=8\" \"-DSC_AUDIO_API=SC_AUDIO_API_EXTERNAL\")",
);
assert!(
text.contains(anchor),
"jack AUDIOAPI branch not found - SuperCollider CMake layout changed"
);
text = text.replace(anchor, injected);
fs::write(&path, text).unwrap();
}
fn patch_sc_sources(sc: &Path) {
patch(
sc,
"include/common/SC_Endian.h",
&[(
"#elif defined(__EMSCRIPTEN__)\n\n# include <endian.h>\n# include <netinet/in.h>\n",
"#elif defined(__EMSCRIPTEN__)\n\n# include <endian.h>\n# include <netinet/in.h>\n\n\
#elif defined(SC_WASM) // SC_WASM: wasm is little-endian; no <netinet/in.h>.\n\n\
# define LITTLE_ENDIAN 1234\n# define BIG_ENDIAN 4321\n\
# define BYTE_ORDER LITTLE_ENDIAN\n# define SC_NO_ENDIAN_FUNCTIONS\n",
)],
);
patch(
sc,
"common/SC_ReplyImpl.hpp",
&[
(
"#ifndef __EMSCRIPTEN__\n# include <boost/asio.hpp>\n#endif",
"#if !defined(__EMSCRIPTEN__) && !defined(SC_WASM)\n# include <boost/asio.hpp>\n#endif",
),
(
"#ifndef __EMSCRIPTEN__\n boost::asio::ip::address mAddress;\n#endif",
"#if !defined(__EMSCRIPTEN__) && !defined(SC_WASM)\n boost::asio::ip::address mAddress;\n#endif",
),
],
);
patch(
sc,
"server/scsynth/SC_World.cpp",
&[(
"#include <filesystem>\n\nnamespace fs = std::filesystem;",
"#ifndef SC_WASM // SC_WASM: <filesystem>/World_LoadGraphDefs unused \
(mLoadGraphDefs=false).\n#include <filesystem>\n\nnamespace fs = std::filesystem;\n#endif",
)],
);
patch(
sc,
"server/scsynth/SC_World.cpp",
&[(
"void World_LoadGraphDefs(World* world) {\n GraphDef* list = nullptr;",
"void World_LoadGraphDefs(World* world) {\n#ifndef SC_WASM // SC_WASM: never called \
(mLoadGraphDefs=false); body uses excluded SC_Filesystem.\n GraphDef* list = nullptr;",
)],
);
patch(
sc,
"server/scsynth/SC_World.cpp",
&[(
" list = GraphDef_LoadDir(world, path, list);\n GraphDef_Define(world, list);\n }\n}",
" list = GraphDef_LoadDir(world, path, list);\n GraphDef_Define(world, list);\n }\n#endif\n}",
)],
);
patch(
sc,
"server/scsynth/SC_GraphDef.cpp",
&[(
"#include \"SC_Filesystem.hpp\"",
"#ifndef SC_WASM // SC_WASM: SC_Filesystem.cpp TU excluded; file loaders stubbed below.\n\
#include \"SC_Filesystem.hpp\"\n#endif",
)],
);
patch(
sc,
"server/scsynth/SC_GraphDef.cpp",
&[(
"#include <filesystem>\n#include <fstream>",
"#include <filesystem>\n#ifndef SC_WASM\n#include <fstream>\n#endif",
)],
);
let loaders_old =
"GraphDef* GraphDef_LoadGlob(World* inWorld, const char* pattern, GraphDef* inList) {";
let loaders_guard_open =
"#ifndef SC_WASM // SC_WASM: filesystem loaders excluded (no /d_load on wasm).\n";
patch(
sc,
"server/scsynth/SC_GraphDef.cpp",
&[(loaders_old, &format!("{loaders_guard_open}{loaders_old}"))],
);
let after_loaders = "void UnitSpec_Free(UnitSpec* inUnitSpec);";
let noops = concat!(
"#else\n",
"// SC_WASM no-op loaders: /d_load and /d_loadDir are never sent on wasm, but the command\n",
"// handlers reference these symbols, so provide list-passthrough definitions.\n",
"GraphDef* GraphDef_LoadGlob(World*, const char*, GraphDef* inList) { return inList; }\n",
"GraphDef* GraphDef_Load(World*, const std::filesystem::path&, GraphDef* inList) { return inList; }\n",
"GraphDef* GraphDef_LoadDir(World*, const std::filesystem::path&, GraphDef* inList) { return inList; }\n",
"#endif\n",
"void UnitSpec_Free(UnitSpec* inUnitSpec);",
);
patch(
sc,
"server/scsynth/SC_GraphDef.cpp",
&[(after_loaders, noops)],
);
}
fn generate_version_header(sc: &Path) {
let scver = fs::read_to_string(sc.join("SCVersion.txt")).expect("read SCVersion.txt");
let field = |name: &str| -> String {
let needle = format!("set({name} ");
let line = scver
.lines()
.find(|l| l.trim_start().starts_with(&needle))
.unwrap_or_else(|| panic!("{name} not found in SCVersion.txt"));
let val = line.trim().trim_start_matches(&needle);
val.trim_end_matches(')')
.trim()
.trim_matches('"')
.to_string()
};
let template = sc.join("common/SC_Version.hpp.in");
let mut text = fs::read_to_string(&template).expect("read SC_Version.hpp.in");
for (ph, val) in [
("@SC_VERSION_MAJOR@", field("SC_VERSION_MAJOR")),
("@SC_VERSION_MINOR@", field("SC_VERSION_MINOR")),
("@SC_VERSION_PATCH@", field("SC_VERSION_PATCH")),
("@SC_VERSION_TWEAK@", field("SC_VERSION_TWEAK")),
("@GIT_REF_TYPE@", "na".to_string()),
("@GIT_BRANCH_OR_TAG@", "na".to_string()),
("@GIT_COMMIT_HASH@", "na".to_string()),
] {
assert!(
text.contains(ph),
"version placeholder {ph} missing - template changed"
);
text = text.replace(ph, &val);
}
fs::write(sc.join("common/SC_Version.hpp"), &text).unwrap();
}
fn patch(sc: &Path, rel: &str, edits: &[(&str, &str)]) {
let path = sc.join(rel);
let mut text = fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {rel}: {e}"));
for (old, new) in edits {
assert!(
text.contains(old),
"SC_WASM guard anchor not found in {rel} (upstream layout changed):\n{old}",
);
text = text.replace(old, new);
}
fs::write(&path, text).unwrap();
}