stereokit_rust/tools/
build_tools.rs

1use std::{
2    env,
3    ffi::OsStr,
4    fs::{self, File, create_dir},
5    io::{self, BufRead, Error},
6    path::{Path, PathBuf},
7    process::Command,
8};
9
10use crate::tools::os_api::{get_assets_dir, get_shaders_sks_dir, get_shaders_source_dir};
11
12/// Reaching the skshaderc of this platform.
13/// * `bin_dir` - The directory of the binaries.
14/// * `with_wine` - Whether to use wine to run skshaderc.exe on linux.
15///
16/// Returns the path to the skshaderc executable.
17///
18/// # Examples
19/// ```
20/// use std::path::PathBuf;
21/// use stereokit_rust::tools::build_tools::get_skshaderc;
22/// let bin_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
23/// let skshaderc_path = get_skshaderc(bin_dir.clone(), false);
24/// assert!(skshaderc_path.is_ok());
25///
26/// let skshaderc_exe_path = get_skshaderc(bin_dir, true);
27/// assert!(skshaderc_exe_path.is_ok());
28/// assert!(skshaderc_exe_path.unwrap().ends_with("skshaderc.exe"));
29/// ```
30pub fn get_skshaderc(bin_dir: PathBuf, with_wine: bool) -> Result<PathBuf, io::Error> {
31    let mut skshaderc = bin_dir.clone();
32    let target_dir = env::var("CARGO_TARGET_DIR").unwrap_or("target".into());
33    skshaderc.push(target_dir);
34
35    if !skshaderc.exists() {
36        return Err(io::Error::new(
37            io::ErrorKind::NotFound,
38            format!("{} not found. Please run 'cargo build' first.", skshaderc.display()),
39        ));
40    }
41
42    let target_os = if with_wine {
43        "win32"
44    } else if cfg!(target_os = "linux") {
45        "linux"
46    } else if cfg!(target_os = "windows") {
47        "win32"
48    } else if cfg!(target_os = "macos") {
49        "mac"
50    } else {
51        ""
52    };
53    let target_arch = if cfg!(target_arch = "x86_64") {
54        "x64"
55    } else if cfg!(target_arch = "aarch64") {
56        "arm64"
57    } else {
58        ""
59    };
60    let exe_type = target_os.to_string() + "_" + target_arch;
61
62    skshaderc.push(r"tools");
63    skshaderc.push(exe_type);
64    if cfg!(windows) || with_wine {
65        skshaderc.push("skshaderc.exe");
66    } else {
67        skshaderc.push("skshaderc");
68    }
69    Ok(skshaderc)
70}
71
72/// Compile hsls file to sks. Use variables `SK_RUST_SHADERS_SOURCE_DIR`  `SK_RUST_ASSETS_DIR` and `SK_RUST_SHADERS_SKS_DIR`
73/// to change the default values.
74/// * `project_dir` - The directory of the project. By default it's  the current directory where `shaderc_src` directory
75///   is.
76/// * `target_dir` - The directory where the sks files will be generated. By default it's the `assets/shaders/`
77///   directory.
78/// * `options` - The options to pass to skshaderc except -i and -o  that are `project_dir` and `target_dir`.
79/// * `with_wine` - If true, use wine to run `skshaderc.exe` on linux.
80///
81/// Returns `Ok(true)` if the compilation was successful, `Ok(false)` if there was no shaders_src directory and `Err` if
82/// there was an error.
83pub fn compile_hlsl(
84    project_dir: PathBuf,
85    target_dir: Option<PathBuf>,
86    options: &[&str],
87    with_wine: bool,
88) -> Result<bool, io::Error> {
89    //we get the dir from StereoKit-rust (not from here)
90    let bin_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
91
92    let skshaderc = get_skshaderc(project_dir.clone(), with_wine)?;
93
94    let mut shaders_source_path = project_dir.clone();
95
96    shaders_source_path.push(get_shaders_source_dir());
97
98    if !shaders_source_path.exists() || !shaders_source_path.is_dir() {
99        println!(
100            "No shaders to compile. Current directory does not see {shaders_source_path:?} directory. \n---The name of the directory may be change with SK_RUST_SHADERS_SOURCE_DIR"
101        );
102        return Ok(false);
103    }
104
105    let shaders_path = match target_dir {
106        Some(path) => String::from(path.to_str().expect("shader_path can't be a &str!")) + "/",
107        None => {
108            let mut shaders_path = project_dir.clone();
109            shaders_path.push(get_assets_dir());
110            if !shaders_path.exists() || !shaders_path.is_dir() {
111                return Err(Error::other(format!("Current directory do not see {shaders_path:?} directory")));
112            }
113
114            shaders_path.push(get_shaders_sks_dir());
115            if !shaders_path.exists() || !shaders_path.is_dir() {
116                create_dir(&shaders_path)?
117            }
118            String::from(shaders_path.to_str().expect("shader_path can't be a &str!")) + "/"
119        }
120    };
121
122    let mut shaders_include = bin_dir.clone();
123    shaders_include.push("StereoKit");
124    shaders_include.push("tools");
125    shaders_include.push("include");
126
127    println!("skshaderc executable used :  {:?}", &skshaderc);
128    println!("Shaders sources are here : {:?}", &shaders_source_path);
129    println!("Shaders compiled there : {:?}", &shaders_path);
130
131    let excluded_extensions = [OsStr::new("hlsli"), OsStr::new("sks"), OsStr::new("txt"), OsStr::new("md")];
132    if let Ok(entries) = shaders_source_path.read_dir() {
133        for entry in entries {
134            let file = entry?.path();
135            println!("Compiling file : {:?}", &file);
136            if file.is_file()
137                && let Some(extension) = file.extension()
138                && !excluded_extensions.contains(&extension)
139            {
140                let mut cmd = if with_wine {
141                    let mut c = Command::new("wine");
142                    c.arg(skshaderc.clone());
143                    c
144                } else {
145                    Command::new(OsStr::new(skshaderc.to_str().unwrap_or("NOPE")))
146                };
147                cmd.arg("-f").arg("-e").arg("-i").arg(&shaders_include).arg("-o").arg(&shaders_path);
148                for arg in options {
149                    cmd.arg(arg);
150                }
151                let output = cmd.arg(file).output().expect("failed to run shader compiler");
152                let out = String::from_utf8(output.clone().stdout).unwrap_or(format!("{output:#?}"));
153                if !out.is_empty() {
154                    println!("{out}")
155                }
156                let err = String::from_utf8(output.clone().stderr).unwrap_or(format!("{output:#?}"));
157                if !err.is_empty() {
158                    println!("{err}")
159                }
160            }
161        }
162    }
163    Ok(true)
164}
165
166/// Recursive fn to copy all the content of a directory to another one.
167/// * `src` - The source directory.
168/// * `dst` - The destination directory.
169pub fn copy_tree(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
170    if let Err(_err) = fs::create_dir(&dst) {}
171    for entry in fs::read_dir(src)?.flatten() {
172        let path_type = entry.file_type()?;
173        if path_type.is_dir() {
174            copy_tree(entry.path(), dst.as_ref().join(entry.file_name()))?;
175        } else {
176            fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
177        }
178    }
179    Ok(())
180}
181
182/// Reading Cargo.toml file of the current dir, looking for a \[package\]/name field and returning its value.
183///
184/// Returns the name of the package as a String or an Error.
185/// ### Examples
186/// ```
187/// use stereokit_rust::tools::build_tools::get_cargo_name;
188/// // Create a dummy Cargo.toml file for testing
189/// let name = get_cargo_name().expect("name should be found");
190/// assert_eq!(name, "stereokit-rust");
191/// ```
192pub fn get_cargo_name() -> Result<String, Error> {
193    // File Cargo.toml must exist in the current path
194    let lines = {
195        let file = File::open("./Cargo.toml")?;
196        io::BufReader::new(file).lines()
197    };
198    let mut in_package = false;
199    // Consumes the iterator, returns an (Optional) String
200    for line in lines.map_while(Result::ok) {
201        let line = line.trim();
202        if in_package {
203            if line.starts_with("name=") || line.starts_with("name") {
204                return Ok(line.split("=").last().unwrap().trim().replace("\"", ""));
205            }
206        } else if line.contains("[package]") {
207            in_package = true;
208        }
209    }
210    if in_package {
211        Err(Error::other("Cargo.toml do not have a [package]/name field"))
212    } else {
213        Err(Error::other("Cargo.toml do not have a [package] section"))
214    }
215}