include_shader/
lib.rs

1//! A library to help working with shaders.
2//!
3//! Although this library works on `stable`, detection of shader file changes is not
4//! guaranteed due to caching. Therefore, it is recommended to use `nightly` along with
5//! the `track-path` feature enabled until the
6//! [`track_path`](https://doc.rust-lang.org/stable/proc_macro/tracked_path/fn.path.html)
7//! API stabilizes.
8//!
9//! ## Optional features
10//! **`relative-path`** - Resolves path relative to the current file instead of relative
11//! to the workspace root directory.
12//!
13//! **`track-path`** - Enables
14//! [`file tracking`](https://doc.rust-lang.org/stable/proc_macro/tracked_path/fn.path.html)
15//! to ensure detection of shader file changes.
16
17#![cfg_attr(feature = "track-path", feature(track_path))]
18#![cfg_attr(feature = "relative-path", feature(proc_macro_span))]
19
20mod dependency_graph;
21
22use dependency_graph::DependencyGraph;
23use lazy_static::lazy_static;
24use proc_macro::{Literal, TokenStream, TokenTree};
25use regex::Regex;
26use std::fs::{canonicalize, read_to_string};
27use std::io;
28use std::path::{Path, PathBuf};
29
30fn resolve_path(path: &str, parent_dir_path: Option<PathBuf>) -> io::Result<PathBuf> {
31    let mut path = PathBuf::from(path);
32    if let Some(p) = parent_dir_path {
33        if !path.is_absolute() {
34            path = p.join(path);
35        }
36    }
37    canonicalize(&path)
38}
39
40fn track_file(_path: &Path) {
41    #[cfg(feature = "track-path")]
42    proc_macro::tracked_path::path(_path.to_string_lossy());
43}
44
45fn process_file(path: &Path, dependency_graph: &mut DependencyGraph) -> String {
46    let content = read_to_string(path).unwrap_or_else(|e| {
47        panic!(
48            "An error occured while trying to read file: {}. Error: {}",
49            path.to_string_lossy(),
50            e
51        )
52    });
53    track_file(path);
54    process_includes(path, content, dependency_graph)
55}
56
57fn process_includes(
58    source_path: &Path,
59    source_file_content: String,
60    dependency_graph: &mut DependencyGraph,
61) -> String {
62    lazy_static! {
63        static ref INCLUDE_RE: Regex = Regex::new(r#"#include\s+"(?P<file>.*)""#).unwrap();
64    }
65    let mut result = source_file_content;
66
67    while let Some(captures) = INCLUDE_RE.captures(&result.clone()) {
68        let capture = captures.get(0).unwrap();
69        let file_path = captures.name("file").unwrap().as_str();
70
71        #[allow(unused_assignments, unused_mut)]
72        let mut include_parent_dir_path = None;
73
74        #[cfg(feature = "relative-path")]
75        {
76            let mut path = source_path.to_path_buf();
77            path.pop();
78            include_parent_dir_path = Some(path);
79        }
80
81        let include_path = match resolve_path(&file_path, include_parent_dir_path) {
82            Ok(path) => path,
83            Err(e) => {
84                panic!(
85                    r#"An error occured while trying to resolve a dependency path: "{}". Error: {}"#,
86                    &file_path,
87                    e
88                )
89            }
90        };
91
92        dependency_graph.add_edge(
93            source_path.to_string_lossy().to_string(),
94            include_path.to_string_lossy().to_string(),
95        );
96
97        if let Some(cycle) = dependency_graph.find_cycle() {
98            panic!("Circular dependency detected: {}", cycle.join(" -> "));
99        }
100
101        result.replace_range(
102            capture.start()..capture.end(),
103            &process_file(&include_path, dependency_graph),
104        );
105    }
106
107    result
108}
109
110fn expr_to_string(expr: &Literal) -> Option<String> {
111    let mut expr = expr.to_string();
112    if !expr.starts_with(r#"""#) || !expr.ends_with(r#"""#) {
113        return None;
114    }
115    expr.remove(0);
116    expr.pop();
117    Some(expr)
118}
119
120fn get_single_string_from_token_stream(token_stream: TokenStream) -> Option<String> {
121    let tokens: Vec<_> = token_stream.into_iter().collect();
122    match tokens.as_slice() {
123        [TokenTree::Literal(expr)] => expr_to_string(expr),
124        _ => None,
125    }
126}
127
128/// Includes a shader file as a string with dependencies support.
129///
130/// By default, the file is located relative to the workspace root directory.
131/// If the `relative-path` feature is enabled, then the file is located relative
132/// to the current file.
133///
134/// # Panics
135///
136/// Panics if:
137/// * A file specified cannot be found
138/// * A circular dependency is detected
139///
140/// # Examples
141///
142/// ```ignore
143/// use include_shader::include_shader;
144///
145/// fn main() {
146///    // ...
147///    let frag_shader = compile_shader(
148///        &context,
149///        WebGl2RenderingContext::FRAGMENT_SHADER,
150///        include_shader!("src/shaders/fragment_shader.glsl"),
151///    )?;
152///    // ...
153/// }
154/// ```
155///
156/// ## Dependencies
157///
158/// Dependencies are supported within shader files using the `#include` preprocessor directive.
159///
160/// `rand.glsl`:
161///
162/// ```glsl
163/// float rand(vec2 co) {
164///     return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453);
165/// }
166/// ```
167///
168/// `fragment_shader.glsl`:
169///
170/// ```glsl
171/// uniform vec2 u_resolution;
172///
173/// #include "./src/shaders/functions/rand.glsl"
174///
175/// void main() {
176///    vec2 st = gl_FragCoord.xy / u_resolution.xy;
177///
178///    gl_FragColor = vec4(vec3(rand(st)), 1.0);
179/// }
180/// ```
181#[proc_macro]
182pub fn include_shader(input: TokenStream) -> TokenStream {
183    let arg = match get_single_string_from_token_stream(input) {
184        Some(string) => string,
185        None => panic!("Takes 1 argument and the argument must be a string literal"),
186    };
187
188    #[allow(unused_assignments, unused_mut)]
189    let mut call_parent_dir_path = None;
190
191    #[cfg(feature = "relative-path")]
192    {
193        let mut path = proc_macro::Span::call_site().source_file().path();
194        path.pop();
195        call_parent_dir_path = Some(path);
196    }
197
198    let root_path = match resolve_path(&arg, call_parent_dir_path) {
199        Ok(path) => path,
200        Err(e) => {
201            panic!(
202                r#"An error occured while trying to resolve root shader path: "{}". Error: {}"#,
203                &arg,
204                e
205            )
206        }
207    };
208    let mut dependency_graph = DependencyGraph::new();
209    let result = process_file(&root_path, &mut dependency_graph);
210
211    format!("{:?}", result).parse().unwrap()
212}