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        io::Write,
248        path::{Path, PathBuf},
249        process::{self, Command},
250    };
251
252    pub(super) fn build() {
253        // Link the AMD AGS library
254        link_amd_ags();
255
256        // Compile HLSL shaders
257        #[cfg(not(debug_assertions))]
258        compile_shaders();
259
260        // Embed the Windows manifest and resource file
261        #[cfg(feature = "windows-manifest")]
262        embed_resource();
263    }
264
265    fn link_amd_ags() {
266        // We can not use relative paths in `cargo:rustc-link-search`, so we need to use the absolute path.
267        // See: https://stackoverflow.com/questions/41917096/how-do-i-make-rustc-link-search-relative-to-the-project-location
268        let lib_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("libs");
269        #[cfg(target_pointer_width = "64")]
270        let lib_name = "amd_ags_x64_2022_MT";
271        #[cfg(target_pointer_width = "32")]
272        let lib_name = "amd_ags_x86_2022_MT";
273        println!("cargo:rustc-link-lib=static={}", lib_name);
274        println!("cargo:rustc-link-search=native={}", lib_dir.display());
275        println!(
276            "cargo:rerun-if-changed={}/{}.lib",
277            lib_dir.display(),
278            lib_name
279        );
280    }
281
282    #[cfg(feature = "windows-manifest")]
283    fn embed_resource() {
284        let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
285        let rc_file = std::path::Path::new("resources/windows/gpui.rc");
286        println!("cargo:rerun-if-changed={}", manifest.display());
287        println!("cargo:rerun-if-changed={}", rc_file.display());
288        embed_resource::compile(rc_file, embed_resource::NONE)
289            .manifest_required()
290            .unwrap();
291    }
292
293    /// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler.
294    fn compile_shaders() {
295        use std::fs;
296
297        let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
298            .join("src/platform/windows/shaders.hlsl");
299        let out_dir = std::env::var("OUT_DIR").unwrap();
300
301        println!("cargo:rerun-if-changed={}", shader_path.display());
302
303        // Check if fxc.exe is available
304        let fxc_path = find_fxc_compiler();
305
306        // Define all modules
307        let modules = [
308            "quad",
309            "shadow",
310            "paths",
311            "underline",
312            "monochrome_sprite",
313            "polychrome_sprite",
314        ];
315
316        let rust_binding_path = format!("{}/shaders_bytes.rs", out_dir);
317        if Path::new(&rust_binding_path).exists() {
318            fs::remove_file(&rust_binding_path)
319                .expect("Failed to remove existing Rust binding file");
320        }
321        for module in &modules {
322            compile_shader_for_module(
323                module,
324                &out_dir,
325                &fxc_path,
326                shader_path.to_str().unwrap(),
327                &rust_binding_path,
328            );
329        }
330    }
331
332    fn find_fxc_compiler() -> String {
333        // Try to find in PATH
334        if let Ok(output) = std::process::Command::new("where").arg("fxc.exe").output() {
335            if output.status.success() {
336                let path = String::from_utf8_lossy(&output.stdout);
337                return path.trim().to_string();
338            }
339        }
340
341        // Check the default path
342        if Path::new(r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe")
343            .exists()
344        {
345            return r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe"
346                .to_string();
347        }
348
349        // Check environment variable
350        if let Ok(path) = std::env::var("GPUI_FXC_PATH") {
351            if Path::new(&path).exists() {
352                return path;
353            }
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_5_0",
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_5_0",
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                    "Pixel 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 =
437            std::fs::read_to_string(head_file).expect("Failed to read header file");
438        let const_definition = {
439            let global_var_start = header_content.find("const BYTE").unwrap();
440            let global_var = &header_content[global_var_start..];
441            let equal = global_var.find('=').unwrap();
442            global_var[equal + 1..].trim()
443        };
444        let rust_binding = format!(
445            "const {}: &[u8] = &{}\n",
446            const_name,
447            const_definition.replace('{', "[").replace('}', "]")
448        );
449        let mut options = std::fs::OpenOptions::new()
450            .write(true)
451            .create(true)
452            .append(true)
453            .open(output_path)
454            .expect("Failed to open Rust binding file");
455        options
456            .write_all(rust_binding.as_bytes())
457            .expect("Failed to write Rust binding file");
458    }
459}