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        // Link the AMD AGS library
255        link_amd_ags();
256
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    fn link_amd_ags() {
267        // We can not use relative paths in `cargo:rustc-link-search`, so we need to use the absolute path.
268        // See: https://stackoverflow.com/questions/41917096/how-do-i-make-rustc-link-search-relative-to-the-project-location
269        let lib_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("libs");
270        #[cfg(target_pointer_width = "64")]
271        let lib_name = "amd_ags_x64_2022_MT";
272        #[cfg(target_pointer_width = "32")]
273        let lib_name = "amd_ags_x86_2022_MT";
274
275        println!("cargo:rustc-link-lib=static={}", lib_name);
276        println!("cargo:rustc-link-search=native={}", lib_dir.display());
277        println!(
278            "cargo:rerun-if-changed={}/{}.lib",
279            lib_dir.display(),
280            lib_name
281        );
282    }
283
284    #[cfg(feature = "windows-manifest")]
285    fn embed_resource() {
286        let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
287        let rc_file = std::path::Path::new("resources/windows/gpui.rc");
288        println!("cargo:rerun-if-changed={}", manifest.display());
289        println!("cargo:rerun-if-changed={}", rc_file.display());
290        embed_resource::compile(rc_file, embed_resource::NONE)
291            .manifest_required()
292            .unwrap();
293    }
294
295    /// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler.
296    fn compile_shaders() {
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    /// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler.
333    fn find_fxc_compiler() -> String {
334        // Try to find in PATH
335        if let Ok(output) = std::process::Command::new("where").arg("fxc.exe").output() {
336            if output.status.success() {
337                let path = String::from_utf8_lossy(&output.stdout);
338                return path.trim().to_string();
339            }
340        }
341
342        // Check the default path
343        if Path::new(r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe")
344            .exists()
345        {
346            return r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe"
347                .to_string();
348        }
349
350        // Check environment variable
351        if let Ok(path) = std::env::var("GPUI_FXC_PATH") {
352            if Path::new(&path).exists() {
353                return path;
354            }
355        }
356
357        panic!("Failed to find fxc.exe");
358    }
359
360    fn compile_shader_for_module(
361        module: &str,
362        out_dir: &str,
363        fxc_path: &str,
364        shader_path: &str,
365        rust_binding_path: &str,
366    ) {
367        // Compile vertex shader
368        let output_file = format!("{}/{}_vs.h", out_dir, module);
369        let const_name = format!("{}_VERTEX_BYTES", module.to_uppercase());
370        compile_shader_impl(
371            fxc_path,
372            &format!("{module}_vertex"),
373            &output_file,
374            &const_name,
375            shader_path,
376            "vs_5_0",
377        );
378        generate_rust_binding(&const_name, &output_file, &rust_binding_path);
379
380        // Compile fragment shader
381        let output_file = format!("{}/{}_ps.h", out_dir, module);
382        let const_name = format!("{}_FRAGMENT_BYTES", module.to_uppercase());
383        compile_shader_impl(
384            fxc_path,
385            &format!("{module}_fragment"),
386            &output_file,
387            &const_name,
388            shader_path,
389            "ps_5_0",
390        );
391        generate_rust_binding(&const_name, &output_file, &rust_binding_path);
392    }
393
394    fn compile_shader_impl(
395        fxc_path: &str,
396        entry_point: &str,
397        output_path: &str,
398        var_name: &str,
399        shader_path: &str,
400        target: &str,
401    ) {
402        let output = Command::new(fxc_path)
403            .args([
404                "/T",
405                target,
406                "/E",
407                entry_point,
408                "/Fh",
409                output_path,
410                "/Vn",
411                var_name,
412                "/O3",
413                shader_path,
414            ])
415            .output();
416
417        match output {
418            Ok(result) => {
419                if result.status.success() {
420                    return;
421                }
422                eprintln!(
423                    "Pixel shader compilation failed for {}:\n{}",
424                    entry_point,
425                    String::from_utf8_lossy(&result.stderr)
426                );
427                process::exit(1);
428            }
429            Err(e) => {
430                eprintln!("Failed to run fxc for {}: {}", entry_point, e);
431                process::exit(1);
432            }
433        }
434    }
435
436    fn generate_rust_binding(const_name: &str, head_file: &str, output_path: &str) {
437        let header_content = 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 = 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}