build.rs

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