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,83 +46,176 @@ fn main() {
3646 let vendor_dir = manifest_dir. join ( "vendor" ) ;
3747 let syphon_framework = vendor_dir. join ( "Syphon.framework" ) ;
3848
49+ // Syphon is OPTIONAL. If missing, compile Texture-only on macOS.
3950 if !syphon_framework. exists ( ) {
40- panic ! (
41- "Syphon.framework not found at {}. Put a built Syphon.framework in vendor/ ." ,
51+ println ! (
52+ "cargo:warning= Syphon.framework not found at {} — building WITHOUT Syphon support (Texture-only on macOS) ." ,
4253 syphon_framework. display( )
4354 ) ;
55+ return ;
4456 }
4557
46- // -------------------------
58+ // Tell Rust code that Syphon is available in this build.
59+ println ! ( "cargo:rustc-cfg=has_syphon" ) ;
60+
4761 // 1) Compile the ObjC bridge into libsyphon_bridge.a
48- // -------------------------
4962 let mut cc_build = cc:: Build :: new ( ) ;
5063 cc_build
5164 . file ( "native/syphon_bridge.m" )
5265 . flag ( "-fobjc-arc" )
5366 . flag ( "-ObjC" )
5467 . include ( syphon_framework. join ( "Headers" ) )
55- // Some Syphon distributions also have Headers under Versions/A/Headers
5668 . include ( syphon_framework. join ( "Versions/A/Headers" ) )
57- // Allow finding frameworks via -F
5869 . flag ( & format ! ( "-F{}" , vendor_dir. display( ) ) )
59- // Silence noisy deprecation warnings (optional; remove if you want them visible)
6070 . flag ( "-Wno-deprecated-declarations" ) ;
6171
62- cc_build. compile ( "syphon_bridge" ) ; // -> libsyphon_bridge.a
72+ cc_build. compile ( "syphon_bridge" ) ;
6373
64- // -------------------------
65- // 2) Link against Syphon.framework + required Apple frameworks
66- // -------------------------
74+ // 2) Link Syphon.framework + required Apple frameworks
6775 println ! ( "cargo:rustc-link-search=framework={}" , vendor_dir. display( ) ) ;
6876 println ! ( "cargo:rustc-link-lib=framework=Syphon" ) ;
6977 println ! ( "cargo:rustc-link-lib=framework=Cocoa" ) ;
7078 println ! ( "cargo:rustc-link-lib=framework=OpenGL" ) ;
7179
72- // -------------------------
7380 // 3) Add runtime rpaths so dyld can resolve @rpath/Syphon.framework/...
74- // -------------------------
75- //
76- // We add TWO rpaths:
77- // - @executable_path : so `cargo run` works if Syphon.framework sits next to the binary
78- // - @executable_path/../Frameworks : so future .app bundles can place frameworks in Contents/Frameworks
79- //
80- // IMPORTANT: rustc-link-arg is stable and works across profiles.
8181 println ! ( "cargo:rustc-link-arg=-Wl,-rpath,@executable_path" ) ;
8282 println ! ( "cargo:rustc-link-arg=-Wl,-rpath,@executable_path/../Frameworks" ) ;
8383
84- // -------------------------
8584 // 4) Copy Syphon.framework next to the built binary for `cargo run`
86- // -------------------------
87- //
88- // Cargo puts the executable at:
89- // <workspace>/target/<profile>/glsl_engine
90- //
91- // So we copy:
92- // vendor/Syphon.framework -> target/<profile>/Syphon.framework
93- //
94- // Then @executable_path (the directory containing the binary) is also the directory
95- // containing Syphon.framework, and dyld can resolve @rpath/Syphon.framework...
9685 let profile = env:: var ( "PROFILE" ) . unwrap_or_else ( |_| "debug" . into ( ) ) ;
97-
98- // Respect CARGO_TARGET_DIR if set, otherwise default to <manifest>/target
9986 let target_dir = env:: var ( "CARGO_TARGET_DIR" )
10087 . map ( PathBuf :: from)
10188 . unwrap_or_else ( |_| manifest_dir. join ( "target" ) ) ;
102-
10389 let dest_dir = target_dir. join ( & profile) . join ( "Syphon.framework" ) ;
10490
105- // Copy only if missing or obviously stale (simple heuristic: missing dest)
10691 if !dest_dir. exists ( ) {
10792 copy_dir_recursive ( & syphon_framework, & dest_dir)
10893 . unwrap_or_else ( |e| panic ! ( "Failed to copy Syphon.framework -> {}: {e}" , dest_dir. display( ) ) ) ;
10994 }
11095}
11196
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+
112216/// Recursively copy a directory (framework bundles are directories).
113217fn copy_dir_recursive ( src : & Path , dst : & Path ) -> std:: io:: Result < ( ) > {
114218 if dst. exists ( ) {
115- // If something exists, remove it so we don't end up with mixed versions.
116219 fs:: remove_dir_all ( dst) ?;
117220 }
118221 fs:: create_dir_all ( dst) ?;
@@ -128,7 +231,6 @@ fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
128231 } else if file_type. is_file ( ) {
129232 fs:: copy ( & from, & to) ?;
130233 } else if file_type. is_symlink ( ) {
131- // Preserve symlinks in framework bundles (common in Versions layout)
132234 let target = fs:: read_link ( & from) ?;
133235 #[ cfg( unix) ]
134236 std:: os:: unix:: fs:: symlink ( target, & to) ?;
0 commit comments