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