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        if let Ok(output) = std::process::Command::new("where").arg("fxc.exe").output() {
326            if output.status.success() {
327                let path = String::from_utf8_lossy(&output.stdout);
328                return path.trim().to_string();
329            }
330        }
331
332        // Check the default path
333        if Path::new(r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe")
334            .exists()
335        {
336            return r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe"
337                .to_string();
338        }
339
340        panic!("Failed to find fxc.exe");
341    }
342
343    fn compile_shader_for_module(
344        module: &str,
345        out_dir: &str,
346        fxc_path: &str,
347        shader_path: &str,
348        rust_binding_path: &str,
349    ) {
350        // Compile vertex shader
351        let output_file = format!("{}/{}_vs.h", out_dir, module);
352        let const_name = format!("{}_VERTEX_BYTES", module.to_uppercase());
353        compile_shader_impl(
354            fxc_path,
355            &format!("{module}_vertex"),
356            &output_file,
357            &const_name,
358            shader_path,
359            "vs_5_0",
360        );
361        generate_rust_binding(&const_name, &output_file, &rust_binding_path);
362
363        // Compile fragment shader
364        let output_file = format!("{}/{}_ps.h", out_dir, module);
365        let const_name = format!("{}_FRAGMENT_BYTES", module.to_uppercase());
366        compile_shader_impl(
367            fxc_path,
368            &format!("{module}_fragment"),
369            &output_file,
370            &const_name,
371            shader_path,
372            "ps_5_0",
373        );
374        generate_rust_binding(&const_name, &output_file, &rust_binding_path);
375    }
376
377    fn compile_shader_impl(
378        fxc_path: &str,
379        entry_point: &str,
380        output_path: &str,
381        var_name: &str,
382        shader_path: &str,
383        target: &str,
384    ) {
385        let output = Command::new(fxc_path)
386            .args([
387                "/T",
388                target,
389                "/E",
390                entry_point,
391                "/Fh",
392                output_path,
393                "/Vn",
394                var_name,
395                "/O3",
396                shader_path,
397            ])
398            .output();
399
400        match output {
401            Ok(result) => {
402                if result.status.success() {
403                    return;
404                }
405                eprintln!(
406                    "Pixel shader compilation failed for {}:\n{}",
407                    entry_point,
408                    String::from_utf8_lossy(&result.stderr)
409                );
410                process::exit(1);
411            }
412            Err(e) => {
413                eprintln!("Failed to run fxc for {}: {}", entry_point, e);
414                process::exit(1);
415            }
416        }
417    }
418
419    fn generate_rust_binding(const_name: &str, head_file: &str, output_path: &str) {
420        let header_content = fs::read_to_string(head_file).expect("Failed to read header file");
421        let const_definition = {
422            let global_var_start = header_content.find("const BYTE").unwrap();
423            let global_var = &header_content[global_var_start..];
424            let equal = global_var.find('=').unwrap();
425            global_var[equal + 1..].trim()
426        };
427        let rust_binding = format!(
428            "const {}: &[u8] = &{}\n",
429            const_name,
430            const_definition.replace('{', "[").replace('}', "]")
431        );
432        let mut options = fs::OpenOptions::new()
433            .create(true)
434            .append(true)
435            .open(output_path)
436            .expect("Failed to open Rust binding file");
437        options
438            .write_all(rust_binding.as_bytes())
439            .expect("Failed to write Rust binding file");
440    }
441}