build.rs

  1#![cfg_attr(any(not(target_os = "macos"), feature = "macos-blade"), allow(unused))]
  2
  3//TODO: consider generating shader code for WGSL
  4//TODO: deprecate "runtime-shaders" and "macos-blade"
  5
  6use std::env;
  7
  8fn main() {
  9    let target = env::var("CARGO_CFG_TARGET_OS");
 10    println!("cargo::rustc-check-cfg=cfg(gles)");
 11
 12    #[cfg(any(
 13        not(any(target_os = "macos", target_os = "windows")),
 14        feature = "macos-blade"
 15    ))]
 16    check_wgsl_shaders();
 17
 18    match target.as_deref() {
 19        Ok("macos") => {
 20            #[cfg(target_os = "macos")]
 21            macos::build();
 22        }
 23        Ok("windows") => {
 24            #[cfg(target_os = "windows")]
 25            windows::build();
 26        }
 27        _ => (),
 28    };
 29}
 30
 31#[allow(dead_code)]
 32fn check_wgsl_shaders() {
 33    use std::path::PathBuf;
 34    use std::process;
 35    use std::str::FromStr;
 36
 37    let shader_source_path = "./src/platform/blade/shaders.wgsl";
 38    let shader_path = PathBuf::from_str(shader_source_path).unwrap();
 39    println!("cargo:rerun-if-changed={}", &shader_path.display());
 40
 41    let shader_source = std::fs::read_to_string(&shader_path).unwrap();
 42
 43    match naga::front::wgsl::parse_str(&shader_source) {
 44        Ok(_) => {
 45            // All clear
 46        }
 47        Err(e) => {
 48            eprintln!("WGSL shader compilation failed:\n{}", e);
 49            process::exit(1);
 50        }
 51    }
 52}
 53#[cfg(target_os = "macos")]
 54mod macos {
 55    use std::{
 56        env,
 57        path::{Path, PathBuf},
 58    };
 59
 60    use cbindgen::Config;
 61
 62    pub(super) fn build() {
 63        generate_dispatch_bindings();
 64        #[cfg(not(feature = "macos-blade"))]
 65        {
 66            let header_path = generate_shader_bindings();
 67
 68            #[cfg(feature = "runtime_shaders")]
 69            emit_stitched_shaders(&header_path);
 70            #[cfg(not(feature = "runtime_shaders"))]
 71            compile_metal_shaders(&header_path);
 72        }
 73    }
 74
 75    fn generate_dispatch_bindings() {
 76        println!("cargo:rustc-link-lib=framework=System");
 77
 78        let bindings = bindgen::Builder::default()
 79            .header("src/platform/mac/dispatch.h")
 80            .allowlist_var("_dispatch_main_q")
 81            .allowlist_var("_dispatch_source_type_data_add")
 82            .allowlist_var("DISPATCH_QUEUE_PRIORITY_HIGH")
 83            .allowlist_var("DISPATCH_TIME_NOW")
 84            .allowlist_function("dispatch_get_global_queue")
 85            .allowlist_function("dispatch_async_f")
 86            .allowlist_function("dispatch_after_f")
 87            .allowlist_function("dispatch_time")
 88            .allowlist_function("dispatch_source_merge_data")
 89            .allowlist_function("dispatch_source_create")
 90            .allowlist_function("dispatch_source_set_event_handler_f")
 91            .allowlist_function("dispatch_resume")
 92            .allowlist_function("dispatch_suspend")
 93            .allowlist_function("dispatch_source_cancel")
 94            .allowlist_function("dispatch_set_context")
 95            .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
 96            .layout_tests(false)
 97            .generate()
 98            .expect("unable to generate bindings");
 99
100        let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
101        bindings
102            .write_to_file(out_path.join("dispatch_sys.rs"))
103            .expect("couldn't write dispatch bindings");
104    }
105
106    fn generate_shader_bindings() -> PathBuf {
107        let output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("scene.h");
108        let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
109        let mut config = Config {
110            include_guard: Some("SCENE_H".into()),
111            language: cbindgen::Language::C,
112            no_includes: true,
113            ..Default::default()
114        };
115        config.export.include.extend([
116            "Bounds".into(),
117            "Corners".into(),
118            "Edges".into(),
119            "Size".into(),
120            "Pixels".into(),
121            "PointF".into(),
122            "Hsla".into(),
123            "ContentMask".into(),
124            "Uniforms".into(),
125            "AtlasTile".into(),
126            "PathRasterizationInputIndex".into(),
127            "PathVertex_ScaledPixels".into(),
128            "PathRasterizationVertex".into(),
129            "ShadowInputIndex".into(),
130            "Shadow".into(),
131            "QuadInputIndex".into(),
132            "Underline".into(),
133            "UnderlineInputIndex".into(),
134            "Quad".into(),
135            "BorderStyle".into(),
136            "SpriteInputIndex".into(),
137            "MonochromeSprite".into(),
138            "PolychromeSprite".into(),
139            "PathSprite".into(),
140            "SurfaceInputIndex".into(),
141            "SurfaceBounds".into(),
142            "TransformationMatrix".into(),
143        ]);
144        config.no_includes = true;
145        config.enumeration.prefix_with_name = true;
146
147        let mut builder = cbindgen::Builder::new();
148
149        let src_paths = [
150            crate_dir.join("src/scene.rs"),
151            crate_dir.join("src/geometry.rs"),
152            crate_dir.join("src/color.rs"),
153            crate_dir.join("src/window.rs"),
154            crate_dir.join("src/platform.rs"),
155            crate_dir.join("src/platform/mac/metal_renderer.rs"),
156        ];
157        for src_path in src_paths {
158            println!("cargo:rerun-if-changed={}", src_path.display());
159            builder = builder.with_src(src_path);
160        }
161
162        builder
163            .with_config(config)
164            .generate()
165            .expect("Unable to generate bindings")
166            .write_to_file(&output_path);
167
168        output_path
169    }
170
171    /// To enable runtime compilation, we need to "stitch" the shaders file with the generated header
172    /// so that it is self-contained.
173    #[cfg(feature = "runtime_shaders")]
174    fn emit_stitched_shaders(header_path: &Path) {
175        use std::str::FromStr;
176        fn stitch_header(header: &Path, shader_path: &Path) -> std::io::Result<PathBuf> {
177            let header_contents = std::fs::read_to_string(header)?;
178            let shader_contents = std::fs::read_to_string(shader_path)?;
179            let stitched_contents = format!("{header_contents}\n{shader_contents}");
180            let out_path =
181                PathBuf::from(env::var("OUT_DIR").unwrap()).join("stitched_shaders.metal");
182            std::fs::write(&out_path, stitched_contents)?;
183            Ok(out_path)
184        }
185        let shader_source_path = "./src/platform/mac/shaders.metal";
186        let shader_path = PathBuf::from_str(shader_source_path).unwrap();
187        stitch_header(header_path, &shader_path).unwrap();
188        println!("cargo:rerun-if-changed={}", &shader_source_path);
189    }
190
191    #[cfg(not(feature = "runtime_shaders"))]
192    fn compile_metal_shaders(header_path: &Path) {
193        use std::process::{self, Command};
194        let shader_path = "./src/platform/mac/shaders.metal";
195        let air_output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.air");
196        let metallib_output_path =
197            PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.metallib");
198        println!("cargo:rerun-if-changed={}", shader_path);
199
200        let output = Command::new("xcrun")
201            .args([
202                "-sdk",
203                "macosx",
204                "metal",
205                "-gline-tables-only",
206                "-mmacosx-version-min=10.15.7",
207                "-MO",
208                "-c",
209                shader_path,
210                "-include",
211                (header_path.to_str().unwrap()),
212                "-o",
213            ])
214            .arg(&air_output_path)
215            .output()
216            .unwrap();
217
218        if !output.status.success() {
219            eprintln!(
220                "metal shader compilation failed:\n{}",
221                String::from_utf8_lossy(&output.stderr)
222            );
223            process::exit(1);
224        }
225
226        let output = Command::new("xcrun")
227            .args(["-sdk", "macosx", "metallib"])
228            .arg(air_output_path)
229            .arg("-o")
230            .arg(metallib_output_path)
231            .output()
232            .unwrap();
233
234        if !output.status.success() {
235            eprintln!(
236                "metallib compilation failed:\n{}",
237                String::from_utf8_lossy(&output.stderr)
238            );
239            process::exit(1);
240        }
241    }
242}
243
244#[cfg(target_os = "windows")]
245mod windows {
246    use std::{
247        fs,
248        io::Write,
249        path::{Path, PathBuf},
250        process::{self, Command},
251    };
252
253    pub(super) fn build() {
254        // Compile HLSL shaders
255        #[cfg(not(debug_assertions))]
256        compile_shaders();
257
258        // Embed the Windows manifest and resource file
259        #[cfg(feature = "windows-manifest")]
260        embed_resource();
261    }
262
263    #[cfg(feature = "windows-manifest")]
264    fn embed_resource() {
265        let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
266        let rc_file = std::path::Path::new("resources/windows/gpui.rc");
267        println!("cargo:rerun-if-changed={}", manifest.display());
268        println!("cargo:rerun-if-changed={}", rc_file.display());
269        embed_resource::compile(rc_file, embed_resource::NONE)
270            .manifest_required()
271            .unwrap();
272    }
273
274    /// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler.
275    fn compile_shaders() {
276        let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
277            .join("src/platform/windows/shaders.hlsl");
278        let out_dir = std::env::var("OUT_DIR").unwrap();
279
280        println!("cargo:rerun-if-changed={}", shader_path.display());
281
282        // Check if fxc.exe is available
283        let fxc_path = find_fxc_compiler();
284
285        // Define all modules
286        let modules = [
287            "quad",
288            "shadow",
289            "path_rasterization",
290            "path_sprite",
291            "underline",
292            "monochrome_sprite",
293            "polychrome_sprite",
294        ];
295
296        let rust_binding_path = format!("{}/shaders_bytes.rs", out_dir);
297        if Path::new(&rust_binding_path).exists() {
298            fs::remove_file(&rust_binding_path)
299                .expect("Failed to remove existing Rust binding file");
300        }
301        for module in modules {
302            compile_shader_for_module(
303                module,
304                &out_dir,
305                &fxc_path,
306                shader_path.to_str().unwrap(),
307                &rust_binding_path,
308            );
309        }
310    }
311
312    /// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler.
313    fn find_fxc_compiler() -> String {
314        // Check environment variable
315        if let Ok(path) = std::env::var("GPUI_FXC_PATH") {
316            if Path::new(&path).exists() {
317                return path;
318            }
319        }
320
321        // Try to find in PATH
322        if let Ok(output) = std::process::Command::new("where").arg("fxc.exe").output() {
323            if output.status.success() {
324                let path = String::from_utf8_lossy(&output.stdout);
325                return path.trim().to_string();
326            }
327        }
328
329        // Check the default path
330        if Path::new(r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe")
331            .exists()
332        {
333            return r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe"
334                .to_string();
335        }
336
337        panic!("Failed to find fxc.exe");
338    }
339
340    fn compile_shader_for_module(
341        module: &str,
342        out_dir: &str,
343        fxc_path: &str,
344        shader_path: &str,
345        rust_binding_path: &str,
346    ) {
347        // Compile vertex shader
348        let output_file = format!("{}/{}_vs.h", out_dir, module);
349        let const_name = format!("{}_VERTEX_BYTES", module.to_uppercase());
350        compile_shader_impl(
351            fxc_path,
352            &format!("{module}_vertex"),
353            &output_file,
354            &const_name,
355            shader_path,
356            "vs_5_0",
357        );
358        generate_rust_binding(&const_name, &output_file, &rust_binding_path);
359
360        // Compile fragment shader
361        let output_file = format!("{}/{}_ps.h", out_dir, module);
362        let const_name = format!("{}_FRAGMENT_BYTES", module.to_uppercase());
363        compile_shader_impl(
364            fxc_path,
365            &format!("{module}_fragment"),
366            &output_file,
367            &const_name,
368            shader_path,
369            "ps_5_0",
370        );
371        generate_rust_binding(&const_name, &output_file, &rust_binding_path);
372    }
373
374    fn compile_shader_impl(
375        fxc_path: &str,
376        entry_point: &str,
377        output_path: &str,
378        var_name: &str,
379        shader_path: &str,
380        target: &str,
381    ) {
382        let output = Command::new(fxc_path)
383            .args([
384                "/T",
385                target,
386                "/E",
387                entry_point,
388                "/Fh",
389                output_path,
390                "/Vn",
391                var_name,
392                "/O3",
393                shader_path,
394            ])
395            .output();
396
397        match output {
398            Ok(result) => {
399                if result.status.success() {
400                    return;
401                }
402                eprintln!(
403                    "Pixel shader compilation failed for {}:\n{}",
404                    entry_point,
405                    String::from_utf8_lossy(&result.stderr)
406                );
407                process::exit(1);
408            }
409            Err(e) => {
410                eprintln!("Failed to run fxc for {}: {}", entry_point, e);
411                process::exit(1);
412            }
413        }
414    }
415
416    fn generate_rust_binding(const_name: &str, head_file: &str, output_path: &str) {
417        let header_content = fs::read_to_string(head_file).expect("Failed to read header file");
418        let const_definition = {
419            let global_var_start = header_content.find("const BYTE").unwrap();
420            let global_var = &header_content[global_var_start..];
421            let equal = global_var.find('=').unwrap();
422            global_var[equal + 1..].trim()
423        };
424        let rust_binding = format!(
425            "const {}: &[u8] = &{}\n",
426            const_name,
427            const_definition.replace('{', "[").replace('}', "]")
428        );
429        let mut options = fs::OpenOptions::new()
430            .create(true)
431            .append(true)
432            .open(output_path)
433            .expect("Failed to open Rust binding file");
434        options
435            .write_all(rust_binding.as_bytes())
436            .expect("Failed to write Rust binding file");
437    }
438}