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            "paths",
290            "underline",
291            "monochrome_sprite",
292            "polychrome_sprite",
293        ];
294
295        let rust_binding_path = format!("{}/shaders_bytes.rs", out_dir);
296        if Path::new(&rust_binding_path).exists() {
297            fs::remove_file(&rust_binding_path)
298                .expect("Failed to remove existing Rust binding file");
299        }
300        for module in modules {
301            compile_shader_for_module(
302                module,
303                &out_dir,
304                &fxc_path,
305                shader_path.to_str().unwrap(),
306                &rust_binding_path,
307            );
308        }
309
310        {
311            let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
312                .join("src/platform/windows/color_text_raster.hlsl");
313            compile_shader_for_module(
314                "emoji_rasterization",
315                &out_dir,
316                &fxc_path,
317                shader_path.to_str().unwrap(),
318                &rust_binding_path,
319            );
320        }
321    }
322
323    /// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler.
324    fn find_fxc_compiler() -> String {
325        // Check environment variable
326        if let Ok(path) = std::env::var("GPUI_FXC_PATH") {
327            if Path::new(&path).exists() {
328                return path;
329            }
330        }
331
332        // Try to find in PATH
333        if let Ok(output) = std::process::Command::new("where").arg("fxc.exe").output() {
334            if output.status.success() {
335                let path = String::from_utf8_lossy(&output.stdout);
336                return path.trim().to_string();
337            }
338        }
339
340        // Check the default path
341        if Path::new(r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe")
342            .exists()
343        {
344            return r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe"
345                .to_string();
346        }
347
348        panic!("Failed to find fxc.exe");
349    }
350
351    fn compile_shader_for_module(
352        module: &str,
353        out_dir: &str,
354        fxc_path: &str,
355        shader_path: &str,
356        rust_binding_path: &str,
357    ) {
358        // Compile vertex shader
359        let output_file = format!("{}/{}_vs.h", out_dir, module);
360        let const_name = format!("{}_VERTEX_BYTES", module.to_uppercase());
361        compile_shader_impl(
362            fxc_path,
363            &format!("{module}_vertex"),
364            &output_file,
365            &const_name,
366            shader_path,
367            "vs_5_0",
368        );
369        generate_rust_binding(&const_name, &output_file, &rust_binding_path);
370
371        // Compile fragment shader
372        let output_file = format!("{}/{}_ps.h", out_dir, module);
373        let const_name = format!("{}_FRAGMENT_BYTES", module.to_uppercase());
374        compile_shader_impl(
375            fxc_path,
376            &format!("{module}_fragment"),
377            &output_file,
378            &const_name,
379            shader_path,
380            "ps_5_0",
381        );
382        generate_rust_binding(&const_name, &output_file, &rust_binding_path);
383    }
384
385    fn compile_shader_impl(
386        fxc_path: &str,
387        entry_point: &str,
388        output_path: &str,
389        var_name: &str,
390        shader_path: &str,
391        target: &str,
392    ) {
393        let output = Command::new(fxc_path)
394            .args([
395                "/T",
396                target,
397                "/E",
398                entry_point,
399                "/Fh",
400                output_path,
401                "/Vn",
402                var_name,
403                "/O3",
404                shader_path,
405            ])
406            .output();
407
408        match output {
409            Ok(result) => {
410                if result.status.success() {
411                    return;
412                }
413                eprintln!(
414                    "Pixel shader compilation failed for {}:\n{}",
415                    entry_point,
416                    String::from_utf8_lossy(&result.stderr)
417                );
418                process::exit(1);
419            }
420            Err(e) => {
421                eprintln!("Failed to run fxc for {}: {}", entry_point, e);
422                process::exit(1);
423            }
424        }
425    }
426
427    fn generate_rust_binding(const_name: &str, head_file: &str, output_path: &str) {
428        let header_content = fs::read_to_string(head_file).expect("Failed to read header file");
429        let const_definition = {
430            let global_var_start = header_content.find("const BYTE").unwrap();
431            let global_var = &header_content[global_var_start..];
432            let equal = global_var.find('=').unwrap();
433            global_var[equal + 1..].trim()
434        };
435        let rust_binding = format!(
436            "const {}: &[u8] = &{}\n",
437            const_name,
438            const_definition.replace('{', "[").replace('}', "]")
439        );
440        let mut options = fs::OpenOptions::new()
441            .create(true)
442            .append(true)
443            .open(output_path)
444            .expect("Failed to open Rust binding file");
445        options
446            .write_all(rust_binding.as_bytes())
447            .expect("Failed to write Rust binding file");
448    }
449}