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