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            "subpixel_sprite",
301            "polychrome_sprite",
302        ];
303
304        let rust_binding_path = format!("{}/shaders_bytes.rs", out_dir);
305        if Path::new(&rust_binding_path).exists() {
306            fs::remove_file(&rust_binding_path)
307                .expect("Failed to remove existing Rust binding file");
308        }
309        for module in modules {
310            compile_shader_for_module(
311                module,
312                &out_dir,
313                &fxc_path,
314                shader_path.to_str().unwrap(),
315                &rust_binding_path,
316            );
317        }
318
319        {
320            let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
321                .join("src/platform/windows/color_text_raster.hlsl");
322            compile_shader_for_module(
323                "emoji_rasterization",
324                &out_dir,
325                &fxc_path,
326                shader_path.to_str().unwrap(),
327                &rust_binding_path,
328            );
329        }
330    }
331
332    /// Locate `binary` in the newest installed Windows SDK.
333    pub fn find_latest_windows_sdk_binary(
334        binary: &str,
335    ) -> Result<Option<PathBuf>, Box<dyn std::error::Error>> {
336        let key = windows_registry::LOCAL_MACHINE
337            .open("SOFTWARE\\WOW6432Node\\Microsoft\\Microsoft SDKs\\Windows\\v10.0")?;
338
339        let install_folder: String = key.get_string("InstallationFolder")?; // "C:\Program Files (x86)\Windows Kits\10\"
340        let install_folder_bin = Path::new(&install_folder).join("bin");
341
342        let mut versions: Vec<_> = std::fs::read_dir(&install_folder_bin)?
343            .flatten()
344            .filter(|entry| entry.path().is_dir())
345            .filter_map(|entry| entry.file_name().into_string().ok())
346            .collect();
347
348        versions.sort_by_key(|s| {
349            s.split('.')
350                .filter_map(|p| p.parse().ok())
351                .collect::<Vec<u32>>()
352        });
353
354        let arch = match std::env::consts::ARCH {
355            "x86_64" => "x64",
356            "aarch64" => "arm64",
357            _ => Err(format!(
358                "Unsupported architecture: {}",
359                std::env::consts::ARCH
360            ))?,
361        };
362
363        if let Some(highest_version) = versions.last() {
364            return Ok(Some(
365                install_folder_bin
366                    .join(highest_version)
367                    .join(arch)
368                    .join(binary),
369            ));
370        }
371
372        Ok(None)
373    }
374
375    /// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler.
376    fn find_fxc_compiler() -> String {
377        // Check environment variable
378        if let Ok(path) = std::env::var("GPUI_FXC_PATH")
379            && Path::new(&path).exists()
380        {
381            return path;
382        }
383
384        // Try to find in PATH
385        // NOTE: This has to be `where.exe` on Windows, not `where`, it must be ended with `.exe`
386        if let Ok(output) = std::process::Command::new("where.exe")
387            .arg("fxc.exe")
388            .output()
389            && output.status.success()
390        {
391            let path = String::from_utf8_lossy(&output.stdout);
392            return path.trim().to_string();
393        }
394
395        if let Ok(Some(path)) = find_latest_windows_sdk_binary("fxc.exe") {
396            return path.to_string_lossy().into_owned();
397        }
398
399        panic!("Failed to find fxc.exe");
400    }
401
402    fn compile_shader_for_module(
403        module: &str,
404        out_dir: &str,
405        fxc_path: &str,
406        shader_path: &str,
407        rust_binding_path: &str,
408    ) {
409        // Compile vertex shader
410        let output_file = format!("{}/{}_vs.h", out_dir, module);
411        let const_name = format!("{}_VERTEX_BYTES", module.to_uppercase());
412        compile_shader_impl(
413            fxc_path,
414            &format!("{module}_vertex"),
415            &output_file,
416            &const_name,
417            shader_path,
418            "vs_4_1",
419        );
420        generate_rust_binding(&const_name, &output_file, rust_binding_path);
421
422        // Compile fragment shader
423        let output_file = format!("{}/{}_ps.h", out_dir, module);
424        let const_name = format!("{}_FRAGMENT_BYTES", module.to_uppercase());
425        compile_shader_impl(
426            fxc_path,
427            &format!("{module}_fragment"),
428            &output_file,
429            &const_name,
430            shader_path,
431            "ps_4_1",
432        );
433        generate_rust_binding(&const_name, &output_file, rust_binding_path);
434    }
435
436    fn compile_shader_impl(
437        fxc_path: &str,
438        entry_point: &str,
439        output_path: &str,
440        var_name: &str,
441        shader_path: &str,
442        target: &str,
443    ) {
444        let output = Command::new(fxc_path)
445            .args([
446                "/T",
447                target,
448                "/E",
449                entry_point,
450                "/Fh",
451                output_path,
452                "/Vn",
453                var_name,
454                "/O3",
455                shader_path,
456            ])
457            .output();
458
459        match output {
460            Ok(result) => {
461                if result.status.success() {
462                    return;
463                }
464                println!(
465                    "cargo::error=Shader compilation failed for {}:\n{}",
466                    entry_point,
467                    String::from_utf8_lossy(&result.stderr)
468                );
469                process::exit(1);
470            }
471            Err(e) => {
472                println!("cargo::error=Failed to run fxc for {}: {}", entry_point, e);
473                process::exit(1);
474            }
475        }
476    }
477
478    fn generate_rust_binding(const_name: &str, head_file: &str, output_path: &str) {
479        let header_content = fs::read_to_string(head_file).expect("Failed to read header file");
480        let const_definition = {
481            let global_var_start = header_content.find("const BYTE").unwrap();
482            let global_var = &header_content[global_var_start..];
483            let equal = global_var.find('=').unwrap();
484            global_var[equal + 1..].trim()
485        };
486        let rust_binding = format!(
487            "const {}: &[u8] = &{}\n",
488            const_name,
489            const_definition.replace('{', "[").replace('}', "]")
490        );
491        let mut options = fs::OpenOptions::new()
492            .create(true)
493            .append(true)
494            .open(output_path)
495            .expect("Failed to open Rust binding file");
496        options
497            .write_all(rust_binding.as_bytes())
498            .expect("Failed to write Rust binding file");
499    }
500}