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