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            if Path::new(&path).exists() {
332                return path;
333            }
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        {
342            if output.status.success() {
343                let path = String::from_utf8_lossy(&output.stdout);
344                return path.trim().to_string();
345            }
346        }
347
348        // Check the default path
349        if Path::new(r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe")
350            .exists()
351        {
352            return r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe"
353                .to_string();
354        }
355
356        panic!("Failed to find fxc.exe");
357    }
358
359    fn compile_shader_for_module(
360        module: &str,
361        out_dir: &str,
362        fxc_path: &str,
363        shader_path: &str,
364        rust_binding_path: &str,
365    ) {
366        // Compile vertex shader
367        let output_file = format!("{}/{}_vs.h", out_dir, module);
368        let const_name = format!("{}_VERTEX_BYTES", module.to_uppercase());
369        compile_shader_impl(
370            fxc_path,
371            &format!("{module}_vertex"),
372            &output_file,
373            &const_name,
374            shader_path,
375            "vs_4_1",
376        );
377        generate_rust_binding(&const_name, &output_file, &rust_binding_path);
378
379        // Compile fragment shader
380        let output_file = format!("{}/{}_ps.h", out_dir, module);
381        let const_name = format!("{}_FRAGMENT_BYTES", module.to_uppercase());
382        compile_shader_impl(
383            fxc_path,
384            &format!("{module}_fragment"),
385            &output_file,
386            &const_name,
387            shader_path,
388            "ps_4_1",
389        );
390        generate_rust_binding(&const_name, &output_file, &rust_binding_path);
391    }
392
393    fn compile_shader_impl(
394        fxc_path: &str,
395        entry_point: &str,
396        output_path: &str,
397        var_name: &str,
398        shader_path: &str,
399        target: &str,
400    ) {
401        let output = Command::new(fxc_path)
402            .args([
403                "/T",
404                target,
405                "/E",
406                entry_point,
407                "/Fh",
408                output_path,
409                "/Vn",
410                var_name,
411                "/O3",
412                shader_path,
413            ])
414            .output();
415
416        match output {
417            Ok(result) => {
418                if result.status.success() {
419                    return;
420                }
421                eprintln!(
422                    "Shader compilation failed for {}:\n{}",
423                    entry_point,
424                    String::from_utf8_lossy(&result.stderr)
425                );
426                process::exit(1);
427            }
428            Err(e) => {
429                eprintln!("Failed to run fxc for {}: {}", entry_point, e);
430                process::exit(1);
431            }
432        }
433    }
434
435    fn generate_rust_binding(const_name: &str, head_file: &str, output_path: &str) {
436        let header_content = fs::read_to_string(head_file).expect("Failed to read header file");
437        let const_definition = {
438            let global_var_start = header_content.find("const BYTE").unwrap();
439            let global_var = &header_content[global_var_start..];
440            let equal = global_var.find('=').unwrap();
441            global_var[equal + 1..].trim()
442        };
443        let rust_binding = format!(
444            "const {}: &[u8] = &{}\n",
445            const_name,
446            const_definition.replace('{', "[").replace('}', "]")
447        );
448        let mut options = fs::OpenOptions::new()
449            .create(true)
450            .append(true)
451            .open(output_path)
452            .expect("Failed to open Rust binding file");
453        options
454            .write_all(rust_binding.as_bytes())
455            .expect("Failed to write Rust binding file");
456    }
457}