Skip to content

Commit 9fee888

Browse files
committed
updated, now working on macOS and Windows. Need to retest on Windows before beta and merging
1 parent da77ed3 commit 9fee888

File tree

8 files changed

+191
-61
lines changed

8 files changed

+191
-61
lines changed

.DS_Store

0 Bytes
Binary file not shown.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,6 @@ anyhow = "1"
2828

2929
[target.'cfg(windows)'.dependencies]
3030
libloading = "0.8"
31+
32+
[lints.rust]
33+
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(has_syphon)'] }

assets/output.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
22
"output_mode": "texture",
33
"syphon": {
4-
"enabled": true,
4+
"enabled": false,
55
"server_name": "shadecore"
66
},
77
"spout": {
8-
"enabled": false
8+
"enabled": true
99
},
1010
"hotkeys": {
1111
"texture": ["Digit1", "Numpad1"],

build.rs

Lines changed: 151 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,42 @@
11
// build.rs
22
//
3-
// This build script does four things (macOS only):
4-
// 1) Compiles our Objective‑C Syphon bridge (native/syphon_bridge.m) into a static lib.
5-
// 2) Links against the Syphon.framework we vendor inside this repo (vendor/Syphon.framework).
6-
// 3) Adds an LC_RPATH so the runtime loader can actually *find* Syphon.framework when you run
7-
// `cargo run` (or any raw executable build).
8-
// 4) Copies vendor/Syphon.framework into target/{debug|release}/Syphon.framework so the rpath
9-
// we add will resolve correctly.
3+
// macOS:
4+
// - optionally compiles the Syphon ObjC bridge
5+
// - links Syphon.framework if vendored
6+
// - emits cfg(has_syphon) when Syphon.framework exists
107
//
11-
// Why this is needed:
12-
// - Our Rust binary references Syphon as: @rpath/Syphon.framework/Versions/A/Syphon
13-
// - If the binary has *no* rpaths, dyld can’t resolve @rpath and you get:
14-
// "Reason: no LC_RPATH's found"
8+
// Windows:
9+
// - builds native/spout_bridge via CMake (which builds Spout + a small C-ABI bridge DLL)
10+
// - links to spout_bridge import library so Rust can resolve spout_* symbols
11+
// - copies spout_bridge.dll next to the built exe for `cargo run`
1512
//
16-
// For app-bundle distribution later, we’ll instead copy Syphon.framework into:
17-
// MyApp.app/Contents/Frameworks/Syphon.framework
18-
// and rely on the rpath @executable_path/../Frameworks (we also add that here).
13+
// Also:
14+
// - declares cfg(has_syphon) to rustc (silences unexpected_cfgs warnings on non-mac targets)
1915

2016
use std::env;
2117
use std::fs;
2218
use std::path::{Path, PathBuf};
2319

2420
fn main() {
25-
// Only do Syphon wiring on macOS. Other platforms should compile fine without it.
26-
if env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("macos") {
21+
// Tell rustc that `cfg(has_syphon)` is an allowed cfg key (silences warnings on Windows/Linux).
22+
println!("cargo:rustc-check-cfg=cfg(has_syphon)");
23+
24+
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
25+
26+
if target_os == "macos" {
27+
build_syphon_macos();
28+
return;
29+
}
30+
31+
if target_os == "windows" {
32+
build_spout_windows();
2733
return;
2834
}
2935

36+
// Other platforms: nothing special.
37+
}
38+
39+
fn build_syphon_macos() {
3040
// Rebuild if these change
3141
println!("cargo:rerun-if-changed=native/syphon_bridge.m");
3242
println!("cargo:rerun-if-changed=native/syphon_bridge.h");
@@ -36,9 +46,7 @@ fn main() {
3646
let vendor_dir = manifest_dir.join("vendor");
3747
let syphon_framework = vendor_dir.join("Syphon.framework");
3848

39-
// IMPORTANT: Syphon is OPTIONAL.
40-
// If the framework isn't vendored, we still want the project to build and run on macOS.
41-
// (You'll just lose Syphon output and fall back to Texture.)
49+
// Syphon is OPTIONAL. If missing, compile Texture-only on macOS.
4250
if !syphon_framework.exists() {
4351
println!(
4452
"cargo:warning=Syphon.framework not found at {} — building WITHOUT Syphon support (Texture-only on macOS).",
@@ -47,79 +55,167 @@ fn main() {
4755
return;
4856
}
4957

50-
// Tell Rust code that Syphon is actually available in this build.
58+
// Tell Rust code that Syphon is available in this build.
5159
println!("cargo:rustc-cfg=has_syphon");
5260

53-
// -------------------------
5461
// 1) Compile the ObjC bridge into libsyphon_bridge.a
55-
// -------------------------
5662
let mut cc_build = cc::Build::new();
5763
cc_build
5864
.file("native/syphon_bridge.m")
5965
.flag("-fobjc-arc")
6066
.flag("-ObjC")
6167
.include(syphon_framework.join("Headers"))
62-
// Some Syphon distributions also have Headers under Versions/A/Headers
6368
.include(syphon_framework.join("Versions/A/Headers"))
64-
// Allow finding frameworks via -F
6569
.flag(&format!("-F{}", vendor_dir.display()))
66-
// Silence noisy deprecation warnings (optional; remove if you want them visible)
6770
.flag("-Wno-deprecated-declarations");
6871

69-
cc_build.compile("syphon_bridge"); // -> libsyphon_bridge.a
72+
cc_build.compile("syphon_bridge");
7073

71-
// -------------------------
72-
// 2) Link against Syphon.framework + required Apple frameworks
73-
// -------------------------
74+
// 2) Link Syphon.framework + required Apple frameworks
7475
println!("cargo:rustc-link-search=framework={}", vendor_dir.display());
7576
println!("cargo:rustc-link-lib=framework=Syphon");
7677
println!("cargo:rustc-link-lib=framework=Cocoa");
7778
println!("cargo:rustc-link-lib=framework=OpenGL");
7879

79-
// -------------------------
8080
// 3) Add runtime rpaths so dyld can resolve @rpath/Syphon.framework/...
81-
// -------------------------
82-
//
83-
// We add TWO rpaths:
84-
// - @executable_path : so `cargo run` works if Syphon.framework sits next to the binary
85-
// - @executable_path/../Frameworks : so future .app bundles can place frameworks in Contents/Frameworks
86-
//
87-
// IMPORTANT: rustc-link-arg is stable and works across profiles.
8881
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
8982
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path/../Frameworks");
9083

91-
// -------------------------
9284
// 4) Copy Syphon.framework next to the built binary for `cargo run`
93-
// -------------------------
94-
//
95-
// Cargo puts the executable at:
96-
// <workspace>/target/<profile>/glsl_engine
97-
//
98-
// So we copy:
99-
// vendor/Syphon.framework -> target/<profile>/Syphon.framework
100-
//
101-
// Then @executable_path (the directory containing the binary) is also the directory
102-
// containing Syphon.framework, and dyld can resolve @rpath/Syphon.framework...
10385
let profile = env::var("PROFILE").unwrap_or_else(|_| "debug".into());
104-
105-
// Respect CARGO_TARGET_DIR if set, otherwise default to <manifest>/target
10686
let target_dir = env::var("CARGO_TARGET_DIR")
10787
.map(PathBuf::from)
10888
.unwrap_or_else(|_| manifest_dir.join("target"));
109-
11089
let dest_dir = target_dir.join(&profile).join("Syphon.framework");
11190

112-
// Copy only if missing or obviously stale (simple heuristic: missing dest)
11391
if !dest_dir.exists() {
11492
copy_dir_recursive(&syphon_framework, &dest_dir)
11593
.unwrap_or_else(|e| panic!("Failed to copy Syphon.framework -> {}: {e}", dest_dir.display()));
11694
}
11795
}
11896

97+
fn build_spout_windows() {
98+
// If these change, rerun
99+
println!("cargo:rerun-if-changed=native/spout_bridge/CMakeLists.txt");
100+
println!("cargo:rerun-if-changed=native/spout_bridge/spout_bridge.cpp");
101+
println!("cargo:rerun-if-changed=native/spout_bridge/spout_bridge.h");
102+
println!("cargo:rerun-if-changed=native/spout2");
103+
104+
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
105+
let spout2_dir = manifest_dir.join("native").join("spout2");
106+
107+
if !spout2_dir.exists() {
108+
println!(
109+
"cargo:warning=Spout2 directory not found at {} — building WITHOUT Spout support.",
110+
spout2_dir.display()
111+
);
112+
return;
113+
}
114+
115+
// Map Cargo profile -> CMake config
116+
let profile = env::var("PROFILE").unwrap_or_else(|_| "debug".into());
117+
let cmake_build_type = if profile.eq_ignore_ascii_case("release") {
118+
"Release"
119+
} else {
120+
"Debug"
121+
};
122+
123+
// Build the CMake project (spout_bridge DLL + import lib)
124+
let dst = cmake::Config::new("native/spout_bridge")
125+
.define("SPOUT2_DIR", spout2_dir.to_string_lossy().to_string())
126+
.profile(cmake_build_type)
127+
.build_target("spout_bridge") // <-- THIS is the key fix
128+
.build();
129+
130+
// Find spout_bridge import library + dll (cmake crate layouts vary by generator)
131+
let (lib_dir, dll_path) = find_spout_bridge_artifacts(&dst)
132+
.unwrap_or_else(|| panic!("Could not find spout_bridge.lib / spout_bridge.dll under {}", dst.display()));
133+
134+
// Link against the import library so Rust can resolve spout_* externs
135+
println!("cargo:rustc-link-search=native={}", lib_dir.display());
136+
println!("cargo:rustc-link-lib=dylib=spout_bridge");
137+
138+
// Copy DLL next to the exe for `cargo run`
139+
let target_dir = env::var("CARGO_TARGET_DIR")
140+
.map(PathBuf::from)
141+
.unwrap_or_else(|_| manifest_dir.join("target"));
142+
let exe_dir = target_dir.join(&profile);
143+
144+
let dest_dll = exe_dir.join("spout_bridge.dll");
145+
if let Err(e) = fs::create_dir_all(&exe_dir) {
146+
panic!("Failed to create target dir {}: {e}", exe_dir.display());
147+
}
148+
if let Err(e) = fs::copy(&dll_path, &dest_dll) {
149+
panic!(
150+
"Failed to copy {} -> {} : {e}",
151+
dll_path.display(),
152+
dest_dll.display()
153+
);
154+
}
155+
}
156+
157+
fn find_spout_bridge_artifacts(dst: &Path) -> Option<(PathBuf, PathBuf)> {
158+
// Common locations produced by cmake crate across generators:
159+
// - dst/lib + dst/bin
160+
// - dst/build/<cfg>/...
161+
// - dst/<cfg>/...
162+
let candidates = [
163+
dst.join("lib"),
164+
dst.join("bin"),
165+
dst.join("build"),
166+
dst.join("build").join("Debug"),
167+
dst.join("build").join("Release"),
168+
dst.join("Debug"),
169+
dst.join("Release"),
170+
];
171+
172+
// Helper: search recursively (limited depth) for a filename.
173+
fn find_file(root: &Path, filename: &str, depth: usize) -> Option<PathBuf> {
174+
if depth == 0 || !root.exists() {
175+
return None;
176+
}
177+
let rd = fs::read_dir(root).ok()?;
178+
for e in rd.flatten() {
179+
let p = e.path();
180+
if p.is_file() {
181+
if p.file_name().map(|s| s.to_string_lossy().eq_ignore_ascii_case(filename)) == Some(true) {
182+
return Some(p);
183+
}
184+
} else if p.is_dir() {
185+
if let Some(found) = find_file(&p, filename, depth - 1) {
186+
return Some(found);
187+
}
188+
}
189+
}
190+
None
191+
}
192+
193+
// Find lib first (import library)
194+
let mut lib_path: Option<PathBuf> = None;
195+
for c in &candidates {
196+
if let Some(p) = find_file(c, "spout_bridge.lib", 6) {
197+
lib_path = Some(p);
198+
break;
199+
}
200+
}
201+
let lib_path = lib_path.or_else(|| find_file(dst, "spout_bridge.lib", 8))?;
202+
203+
// Find dll
204+
let mut dll_path: Option<PathBuf> = None;
205+
for c in &candidates {
206+
if let Some(p) = find_file(c, "spout_bridge.dll", 6) {
207+
dll_path = Some(p);
208+
break;
209+
}
210+
}
211+
let dll_path = dll_path.or_else(|| find_file(dst, "spout_bridge.dll", 8))?;
212+
213+
Some((lib_path.parent()?.to_path_buf(), dll_path))
214+
}
215+
119216
/// Recursively copy a directory (framework bundles are directories).
120217
fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
121218
if dst.exists() {
122-
// If something exists, remove it so we don't end up with mixed versions.
123219
fs::remove_dir_all(dst)?;
124220
}
125221
fs::create_dir_all(dst)?;
@@ -135,10 +231,9 @@ fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
135231
} else if file_type.is_file() {
136232
fs::copy(&from, &to)?;
137233
} else if file_type.is_symlink() {
138-
// Preserve symlinks in framework bundles (common in Versions layout)
139-
let _target = fs::read_link(&from)?;
234+
let target = fs::read_link(&from)?;
140235
#[cfg(unix)]
141-
std::os::unix::fs::symlink(_target, &to)?;
236+
std::os::unix::fs::symlink(target, &to)?;
142237
}
143238
}
144239
Ok(())

native/.DS_Store

0 Bytes
Binary file not shown.

src/.DS_Store

-6 KB
Binary file not shown.

src/main.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,26 @@ fn find_assets_base() -> PathBuf {
437437
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets")
438438
}
439439

440+
fn pick_platform_json(assets: &Path, stem: &str) -> PathBuf {
441+
let os = if cfg!(target_os = "windows") {
442+
"windows"
443+
} else if cfg!(target_os = "macos") {
444+
"macos"
445+
} else if cfg!(target_os = "linux") {
446+
"linux"
447+
} else {
448+
"other"
449+
};
450+
451+
let platform = assets.join(format!("{stem}.{os}.json"));
452+
if platform.exists() {
453+
platform
454+
} else {
455+
assets.join(format!("{stem}.json"))
456+
}
457+
}
458+
459+
440460
fn connect_midi(pf: &ParamsFile, store: Arc<Mutex<ParamStore>>) -> Option<midir::MidiInputConnection<()>> {
441461
let mut midi_in = MidiInput::new("shadecore-midi").ok()?;
442462
midi_in.ignore(Ignore::None);
@@ -540,8 +560,8 @@ fn main() {
540560

541561
let frag_path = assets.join("shaders").join("default.frag");
542562
let present_frag_path = assets.join("shaders").join("present.frag");
543-
let params_path = assets.join("params.json");
544-
let output_cfg_path = assets.join("output.json");
563+
let params_path = pick_platform_json(&assets, "params");
564+
let output_cfg_path = pick_platform_json(&assets, "output");
545565

546566
println!("[assets] base: {}", assets.display());
547567
println!("[assets] frag: {}", frag_path.display());
@@ -625,7 +645,19 @@ fn main() {
625645

626646
let _midi_conn_in = connect_midi(&pf, store.clone());
627647

628-
let output_cfg = load_output_config(&output_cfg_path, OutputMode::Texture);
648+
let default_mode = if cfg!(target_os = "windows") {
649+
OutputMode::Spout
650+
} else if cfg!(target_os = "macos") {
651+
if cfg!(has_syphon) {
652+
OutputMode::Syphon
653+
} else {
654+
OutputMode::Texture
655+
}
656+
} else {
657+
OutputMode::Texture
658+
};
659+
660+
let output_cfg = load_output_config(&output_cfg_path, default_mode);
629661

630662
let syphon_name = output_cfg
631663
.syphon

vendor.zip

-146 KB
Binary file not shown.

0 commit comments

Comments
 (0)