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