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