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
2016use std:: env;
2117use std:: fs;
2218use std:: path:: { Path , PathBuf } ;
2319
2420fn 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).
120217fn 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 ( ( ) )
0 commit comments