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