build.rs

  1#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")]
  2#![cfg_attr(any(not(target_os = "macos"), feature = "macos-blade"), allow(unused))]
  3
  4//TODO: consider generating shader code for WGSL
  5//TODO: deprecate "runtime-shaders" and "macos-blade"
  6
  7use std::env;
  8
  9fn main() {
 10    let target = env::var("CARGO_CFG_TARGET_OS");
 11    println!("cargo::rustc-check-cfg=cfg(gles)");
 12
 13    #[cfg(any(
 14        not(any(target_os = "macos", target_os = "windows")),
 15        all(target_os = "macos", feature = "macos-blade")
 16    ))]
 17    check_wgsl_shaders();
 18
 19    match target.as_deref() {
 20        Ok("macos") => {
 21            #[cfg(target_os = "macos")]
 22            macos::build();
 23        }
 24        Ok("windows") => {
 25            #[cfg(target_os = "windows")]
 26            windows::build();
 27        }
 28        _ => (),
 29    };
 30}
 31
 32#[cfg(any(
 33    not(any(target_os = "macos", target_os = "windows")),
 34    all(target_os = "macos", feature = "macos-blade")
 35))]
 36fn check_wgsl_shaders() {
 37    use std::path::PathBuf;
 38    use std::process;
 39    use std::str::FromStr;
 40
 41    let shader_source_path = "./src/platform/blade/shaders.wgsl";
 42    let shader_path = PathBuf::from_str(shader_source_path).unwrap();
 43    println!("cargo:rerun-if-changed={}", &shader_path.display());
 44
 45    let shader_source = std::fs::read_to_string(&shader_path).unwrap();
 46
 47    match naga::front::wgsl::parse_str(&shader_source) {
 48        Ok(_) => {
 49            // All clear
 50        }
 51        Err(e) => {
 52            println!("cargo::error=WGSL shader compilation failed:\n{}", e);
 53            process::exit(1);
 54        }
 55    }
 56}
 57#[cfg(target_os = "macos")]
 58mod macos {
 59    use std::{
 60        env,
 61        path::{Path, PathBuf},
 62    };
 63
 64    use cbindgen::Config;
 65
 66    pub(super) fn build() {
 67        generate_dispatch_bindings();
 68        #[cfg(not(feature = "macos-blade"))]
 69        {
 70            let header_path = generate_shader_bindings();
 71
 72            #[cfg(feature = "runtime_shaders")]
 73            emit_stitched_shaders(&header_path);
 74            #[cfg(not(feature = "runtime_shaders"))]
 75            compile_metal_shaders(&header_path);
 76        }
 77    }
 78
 79    fn generate_dispatch_bindings() {
 80        println!("cargo:rustc-link-lib=framework=System");
 81
 82        let bindings = bindgen::Builder::default()
 83            .header("src/platform/mac/dispatch.h")
 84            .allowlist_var("_dispatch_main_q")
 85            .allowlist_var("_dispatch_source_type_data_add")
 86            .allowlist_var("DISPATCH_QUEUE_PRIORITY_HIGH")
 87            .allowlist_var("DISPATCH_QUEUE_PRIORITY_DEFAULT")
 88            .allowlist_var("DISPATCH_QUEUE_PRIORITY_LOW")
 89            .allowlist_var("DISPATCH_TIME_NOW")
 90            .allowlist_function("dispatch_get_global_queue")
 91            .allowlist_function("dispatch_async_f")
 92            .allowlist_function("dispatch_after_f")
 93            .allowlist_function("dispatch_time")
 94            .allowlist_function("dispatch_source_merge_data")
 95            .allowlist_function("dispatch_source_create")
 96            .allowlist_function("dispatch_source_set_event_handler_f")
 97            .allowlist_function("dispatch_resume")
 98            .allowlist_function("dispatch_suspend")
 99            .allowlist_function("dispatch_source_cancel")
100            .allowlist_function("dispatch_set_context")
101            .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
102            .layout_tests(false)
103            .generate()
104            .expect("unable to generate bindings");
105
106        let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
107        bindings
108            .write_to_file(out_path.join("dispatch_sys.rs"))
109            .expect("couldn't write dispatch bindings");
110    }
111
112    fn generate_shader_bindings() -> PathBuf {
113        let output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("scene.h");
114        let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
115        let mut config = Config {
116            include_guard: Some("SCENE_H".into()),
117            language: cbindgen::Language::C,
118            no_includes: true,
119            ..Default::default()
120        };
121        config.export.include.extend([
122            "Bounds".into(),
123            "Corners".into(),
124            "Edges".into(),
125            "Size".into(),
126            "Pixels".into(),
127            "PointF".into(),
128            "Hsla".into(),
129            "ContentMask".into(),
130            "Uniforms".into(),
131            "AtlasTile".into(),
132            "PathRasterizationInputIndex".into(),
133            "PathVertex_ScaledPixels".into(),
134            "PathRasterizationVertex".into(),
135            "ShadowInputIndex".into(),
136            "Shadow".into(),
137            "QuadInputIndex".into(),
138            "Underline".into(),
139            "UnderlineInputIndex".into(),
140            "Quad".into(),
141            "BorderStyle".into(),
142            "SpriteInputIndex".into(),
143            "MonochromeSprite".into(),
144            "PolychromeSprite".into(),
145            "PathSprite".into(),
146            "SurfaceInputIndex".into(),
147            "SurfaceBounds".into(),
148            "TransformationMatrix".into(),
149        ]);
150        config.no_includes = true;
151        config.enumeration.prefix_with_name = true;
152
153        let mut builder = cbindgen::Builder::new();
154
155        let src_paths = [
156            crate_dir.join("src/scene.rs"),
157            crate_dir.join("src/geometry.rs"),
158            crate_dir.join("src/color.rs"),
159            crate_dir.join("src/window.rs"),
160            crate_dir.join("src/platform.rs"),
161            crate_dir.join("src/platform/mac/metal_renderer.rs"),
162        ];
163        for src_path in src_paths {
164            println!("cargo:rerun-if-changed={}", src_path.display());
165            builder = builder.with_src(src_path);
166        }
167
168        builder
169            .with_config(config)
170            .generate()
171            .expect("Unable to generate bindings")
172            .write_to_file(&output_path);
173
174        output_path
175    }
176
177    /// To enable runtime compilation, we need to "stitch" the shaders file with the generated header
178    /// so that it is self-contained.
179    #[cfg(feature = "runtime_shaders")]
180    fn emit_stitched_shaders(header_path: &Path) {
181        use std::str::FromStr;
182        fn stitch_header(header: &Path, shader_path: &Path) -> std::io::Result<PathBuf> {
183            let header_contents = std::fs::read_to_string(header)?;
184            let shader_contents = std::fs::read_to_string(shader_path)?;
185            let stitched_contents = format!("{header_contents}\n{shader_contents}");
186            let out_path =
187                PathBuf::from(env::var("OUT_DIR").unwrap()).join("stitched_shaders.metal");
188            std::fs::write(&out_path, stitched_contents)?;
189            Ok(out_path)
190        }
191        let shader_source_path = "./src/platform/mac/shaders.metal";
192        let shader_path = PathBuf::from_str(shader_source_path).unwrap();
193        stitch_header(header_path, &shader_path).unwrap();
194        println!("cargo:rerun-if-changed={}", &shader_source_path);
195    }
196
197    #[cfg(not(feature = "runtime_shaders"))]
198    fn compile_metal_shaders(header_path: &Path) {
199        use std::process::{self, Command};
200        let shader_path = "./src/platform/mac/shaders.metal";
201        let air_output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.air");
202        let metallib_output_path =
203            PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.metallib");
204        println!("cargo:rerun-if-changed={}", shader_path);
205
206        let output = Command::new("xcrun")
207            .args([
208                "-sdk",
209                "macosx",
210                "metal",
211                "-gline-tables-only",
212                "-mmacosx-version-min=10.15.7",
213                "-MO",
214                "-c",
215                shader_path,
216                "-include",
217                (header_path.to_str().unwrap()),
218                "-o",
219            ])
220            .arg(&air_output_path)
221            .output()
222            .unwrap();
223
224        if !output.status.success() {
225            println!(
226                "cargo::error=metal shader compilation failed:\n{}",
227                String::from_utf8_lossy(&output.stderr)
228            );
229            process::exit(1);
230        }
231
232        let output = Command::new("xcrun")
233            .args(["-sdk", "macosx", "metallib"])
234            .arg(air_output_path)
235            .arg("-o")
236            .arg(metallib_output_path)
237            .output()
238            .unwrap();
239
240        if !output.status.success() {
241            println!(
242                "cargo::error=metallib compilation failed:\n{}",
243                String::from_utf8_lossy(&output.stderr)
244            );
245            process::exit(1);
246        }
247    }
248}
249
250#[cfg(target_os = "windows")]
251mod windows {
252    use std::{
253        ffi::OsString,
254        fs,
255        io::Write,
256        path::{Path, PathBuf},
257        process::{self, Command},
258    };
259
260    pub(super) fn build() {
261        // Compile HLSL shaders
262        #[cfg(not(debug_assertions))]
263        compile_shaders();
264
265        // Embed the Windows manifest and resource file
266        #[cfg(feature = "windows-manifest")]
267        embed_resource();
268    }
269
270    #[cfg(feature = "windows-manifest")]
271    fn embed_resource() {
272        let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
273        let rc_file = std::path::Path::new("resources/windows/gpui.rc");
274        println!("cargo:rerun-if-changed={}", manifest.display());
275        println!("cargo:rerun-if-changed={}", rc_file.display());
276        embed_resource::compile(rc_file, embed_resource::NONE)
277            .manifest_required()
278            .unwrap();
279    }
280
281    /// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler.
282    fn compile_shaders() {
283        let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
284            .join("src/platform/windows/shaders.hlsl");
285        let out_dir = std::env::var("OUT_DIR").unwrap();
286
287        println!("cargo:rerun-if-changed={}", shader_path.display());
288
289        // Check if fxc.exe is available
290        let fxc_path = find_fxc_compiler();
291
292        // Define all modules
293        let modules = [
294            "quad",
295            "shadow",
296            "path_rasterization",
297            "path_sprite",
298            "underline",
299            "monochrome_sprite",
300            "polychrome_sprite",
301        ];
302
303        let rust_binding_path = format!("{}/shaders_bytes.rs", out_dir);
304        if Path::new(&rust_binding_path).exists() {
305            fs::remove_file(&rust_binding_path)
306                .expect("Failed to remove existing Rust binding file");
307        }
308        for module in modules {
309            compile_shader_for_module(
310                module,
311                &out_dir,
312                &fxc_path,
313                shader_path.to_str().unwrap(),
314                &rust_binding_path,
315            );
316        }
317
318        {
319            let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
320                .join("src/platform/windows/color_text_raster.hlsl");
321            compile_shader_for_module(
322                "emoji_rasterization",
323                &out_dir,
324                &fxc_path,
325                shader_path.to_str().unwrap(),
326                &rust_binding_path,
327            );
328        }
329    }
330
331    /// Locate `binary` in the newest installed Windows SDK.
332    pub fn find_latest_windows_sdk_binary(
333        binary: &str,
334    ) -> Result<Option<PathBuf>, Box<dyn std::error::Error>> {
335        let key = windows_registry::LOCAL_MACHINE
336            .open("SOFTWARE\\WOW6432Node\\Microsoft\\Microsoft SDKs\\Windows\\v10.0")?;
337
338        let install_folder: String = key.get_string("InstallationFolder")?; // "C:\Program Files (x86)\Windows Kits\10\"
339        let install_folder_bin = Path::new(&install_folder).join("bin");
340
341        let mut versions: Vec<_> = std::fs::read_dir(&install_folder_bin)?
342            .flatten()
343            .filter(|entry| entry.path().is_dir())
344            .filter_map(|entry| entry.file_name().into_string().ok())
345            .collect();
346
347        versions.sort_by_key(|s| {
348            s.split('.')
349                .filter_map(|p| p.parse().ok())
350                .collect::<Vec<u32>>()
351        });
352
353        let arch = match std::env::consts::ARCH {
354            "x86_64" => "x64",
355            "aarch64" => "arm64",
356            _ => Err(format!(
357                "Unsupported architecture: {}",
358                std::env::consts::ARCH
359            ))?,
360        };
361
362        if let Some(highest_version) = versions.last() {
363            return Ok(Some(
364                install_folder_bin
365                    .join(highest_version)
366                    .join(arch)
367                    .join(binary),
368            ));
369        }
370
371        Ok(None)
372    }
373
374    /// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler.
375    fn find_fxc_compiler() -> String {
376        // Check environment variable
377        if let Ok(path) = std::env::var("GPUI_FXC_PATH")
378            && Path::new(&path).exists()
379        {
380            return path;
381        }
382
383        // Try to find in PATH
384        // NOTE: This has to be `where.exe` on Windows, not `where`, it must be ended with `.exe`
385        if let Ok(output) = std::process::Command::new("where.exe")
386            .arg("fxc.exe")
387            .output()
388            && output.status.success()
389        {
390            let path = String::from_utf8_lossy(&output.stdout);
391            return path.trim().to_string();
392        }
393
394        if let Ok(Some(path)) = find_latest_windows_sdk_binary("fxc.exe") {
395            return path.to_string_lossy().into_owned();
396        }
397
398        panic!("Failed to find fxc.exe");
399    }
400
401    fn compile_shader_for_module(
402        module: &str,
403        out_dir: &str,
404        fxc_path: &str,
405        shader_path: &str,
406        rust_binding_path: &str,
407    ) {
408        // Compile vertex shader
409        let output_file = format!("{}/{}_vs.h", out_dir, module);
410        let const_name = format!("{}_VERTEX_BYTES", module.to_uppercase());
411        compile_shader_impl(
412            fxc_path,
413            &format!("{module}_vertex"),
414            &output_file,
415            &const_name,
416            shader_path,
417            "vs_4_1",
418        );
419        generate_rust_binding(&const_name, &output_file, rust_binding_path);
420
421        // Compile fragment shader
422        let output_file = format!("{}/{}_ps.h", out_dir, module);
423        let const_name = format!("{}_FRAGMENT_BYTES", module.to_uppercase());
424        compile_shader_impl(
425            fxc_path,
426            &format!("{module}_fragment"),
427            &output_file,
428            &const_name,
429            shader_path,
430            "ps_4_1",
431        );
432        generate_rust_binding(&const_name, &output_file, rust_binding_path);
433    }
434
435    fn compile_shader_impl(
436        fxc_path: &str,
437        entry_point: &str,
438        output_path: &str,
439        var_name: &str,
440        shader_path: &str,
441        target: &str,
442    ) {
443        let output = Command::new(fxc_path)
444            .args([
445                "/T",
446                target,
447                "/E",
448                entry_point,
449                "/Fh",
450                output_path,
451                "/Vn",
452                var_name,
453                "/O3",
454                shader_path,
455            ])
456            .output();
457
458        match output {
459            Ok(result) => {
460                if result.status.success() {
461                    return;
462                }
463                println!(
464                    "cargo::error=Shader compilation failed for {}:\n{}",
465                    entry_point,
466                    String::from_utf8_lossy(&result.stderr)
467                );
468                process::exit(1);
469            }
470            Err(e) => {
471                println!("cargo::error=Failed to run fxc for {}: {}", entry_point, e);
472                process::exit(1);
473            }
474        }
475    }
476
477    fn generate_rust_binding(const_name: &str, head_file: &str, output_path: &str) {
478        let header_content = fs::read_to_string(head_file).expect("Failed to read header file");
479        let const_definition = {
480            let global_var_start = header_content.find("const BYTE").unwrap();
481            let global_var = &header_content[global_var_start..];
482            let equal = global_var.find('=').unwrap();
483            global_var[equal + 1..].trim()
484        };
485        let rust_binding = format!(
486            "const {}: &[u8] = &{}\n",
487            const_name,
488            const_definition.replace('{', "[").replace('}', "]")
489        );
490        let mut options = fs::OpenOptions::new()
491            .create(true)
492            .append(true)
493            .open(output_path)
494            .expect("Failed to open Rust binding file");
495        options
496            .write_all(rust_binding.as_bytes())
497            .expect("Failed to write Rust binding file");
498    }
499}