Browse Source

Even more error handling improvements

Use map_err everywhere to add context / file paths when possible. Also
add some basic compile_fail doctests for missing / bad syntax shader
sources.
pull/22/head
Ian Chamberlain 1 year ago
parent
commit
c19876af66
No known key found for this signature in database
GPG Key ID: AE5484D09405AA60
  1. 98
      citro3d-macros/src/lib.rs
  2. 11
      citro3d-macros/tests/bad-shader.pica
  3. 4
      citro3d-macros/tests/integration.rs

98
citro3d-macros/src/lib.rs

@ -25,9 +25,25 @@ use quote::quote;
/// ///
/// # Example /// # Example
/// ///
/// ```no_run /// ```
/// use citro3d_macros::include_shader;
///
/// static SHADER_BYTES: &[u8] = include_shader!("../tests/integration.pica");
/// ```
///
/// # Errors
///
/// The macro will fail to compile if the `.pica` file cannot be found, or contains
/// `picasso` syntax errors.
///
/// ```compile_fail
/// # use citro3d_macros::include_shader;
/// static _ERROR: &[u8] = include_shader!("../tests/nonexistent.pica");
/// ```
///
/// ```compile_fail
/// # use citro3d_macros::include_shader; /// # use citro3d_macros::include_shader;
/// static SHADER_BYTES: &[u8] = include_shader!("assets/vshader.pica"); /// static _ERROR: &[u8] = include_shader!("../tests/bad-shader.pica");
/// ``` /// ```
#[proc_macro] #[proc_macro]
pub fn include_shader(input: proc_macro::TokenStream) -> proc_macro::TokenStream { pub fn include_shader(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
@ -47,7 +63,9 @@ fn include_shader_impl(input: TokenStream) -> Result<TokenStream, Box<dyn Error>
return Err(format!("expected exactly one input token, got {}", tokens.len()).into()); return Err(format!("expected exactly one input token, got {}", tokens.len()).into());
} }
let string_lit = match StringLit::try_from(&tokens[0]) { let shader_source_filename = &tokens[0];
let string_lit = match StringLit::try_from(shader_source_filename) {
Ok(lit) => lit, Ok(lit) => lit,
Err(err) => return Ok(err.to_compile_error()), Err(err) => return Ok(err.to_compile_error()),
}; };
@ -56,52 +74,70 @@ fn include_shader_impl(input: TokenStream) -> Result<TokenStream, Box<dyn Error>
// https://users.rust-lang.org/t/which-directory-does-a-proc-macro-run-from/71917 // https://users.rust-lang.org/t/which-directory-does-a-proc-macro-run-from/71917
// //
// But the span's `source_file()` seems to always be relative to the cwd. // But the span's `source_file()` seems to always be relative to the cwd.
let cwd = env::current_dir().expect("unable to determine working directory"); let cwd = env::current_dir()
let span = tokens[0].span(); .map_err(|err| format!("unable to determine current directory: {err}"))?;
let invoking_source_file = span.source_file().path();
let invoking_source_file = shader_source_filename.span().source_file().path();
let Some(invoking_source_dir) = invoking_source_file.parent() else { let Some(invoking_source_dir) = invoking_source_file.parent() else {
return Err("unable to find parent directory of invoking source file".into()); return Ok(quote! {
compile_error!(
concat!(
"unable to find parent directory of current source file \"",
file!(),
"\""
)
)
}
.into());
}; };
// By joining these three pieces, we arrive at approximately the same behavior as `include_bytes!` // By joining these three pieces, we arrive at approximately the same behavior as `include_bytes!`
let shader_source_file = cwd.join(invoking_source_dir).join(string_lit.value()); let shader_source_file = cwd
.join(invoking_source_dir)
.join(string_lit.value())
// This might be overkill, but it ensures we get a unique path if different
// shaders with the same relative path are used within one program
.canonicalize()
.map_err(|err| format!("unable to resolve absolute path of shader source: {err}"))?;
let shader_out_file: PathBuf = shader_source_file.with_extension("shbin"); let shader_out_file: PathBuf = shader_source_file.with_extension("shbin");
let out_dir = PathBuf::from(env!("OUT_DIR")); let out_dir = PathBuf::from(env!("OUT_DIR"));
// This might be overkill, but it ensures we get a unique path if different
// shaders with the same relative path are used within one program
let relative_out_path = shader_out_file.canonicalize()?;
let out_path = out_dir.join( let out_path = out_dir.join(shader_out_file.components().skip(1).collect::<PathBuf>());
relative_out_path // UNWRAP: we already canonicalized the source path, so it should have a parent.
.strip_prefix("/") let out_parent = out_path.parent().unwrap();
.unwrap_or(&shader_out_file),
);
let parent_dir = out_path.parent().ok_or("invalid input filename")?; DirBuilder::new()
DirBuilder::new().recursive(true).create(parent_dir)?; .recursive(true)
.create(out_parent)
.map_err(|err| format!("unable to create output directory {out_parent:?}: {err}"))?;
let devkitpro = PathBuf::from(env!("DEVKITPRO")); let devkitpro = PathBuf::from(env!("DEVKITPRO"));
let picasso = devkitpro.join("tools/bin/picasso");
let output = process::Command::new(devkitpro.join("tools/bin/picasso")) let output = process::Command::new(&picasso)
.arg("--out") .arg("--out")
.args([&out_path, &shader_source_file]) .args([&out_path, &shader_source_file])
.output()?; .output()
.map_err(|err| format!("unable to run {picasso:?}: {err}"))?;
match output.status.code() { let error_code = match output.status.code() {
Some(0) => {} Some(0) => None,
code => { code => Some(code.map_or_else(|| String::from("<unknown>"), |c| c.to_string())),
let code = code.map_or_else(|| String::from("unknown"), |c| c.to_string()); };
return Err(format!( if let Some(code) = error_code {
"failed to compile shader: exit status from `picasso` was {code}: {}", return Err(format!(
String::from_utf8_lossy(&output.stderr), "failed to compile shader: `picasso` exited with status {code}: {}",
) String::from_utf8_lossy(&output.stderr),
.into()); )
} .into());
} }
let bytes = std::fs::read(out_path)?; let bytes = std::fs::read(&out_path)
.map_err(|err| format!("unable to read output file {out_path:?}: {err}"))?;
let source_file_path = shader_source_file.to_string_lossy(); let source_file_path = shader_source_file.to_string_lossy();
let result = quote! { let result = quote! {

11
citro3d-macros/tests/bad-shader.pica

@ -0,0 +1,11 @@
; Vertex shader that won't compile
.out outpos position
.out outclr color
.proc main
mov outpos, 1
mov outclr, 0
end
.end

4
citro3d-macros/tests/integration.rs

@ -2,14 +2,14 @@ use citro3d_macros::include_shader;
#[test] #[test]
fn includes_shader_static() { fn includes_shader_static() {
static SHADER_BYTES: &[u8] = include_shader!("test.pica"); static SHADER_BYTES: &[u8] = include_shader!("integration.pica");
assert_eq!(SHADER_BYTES.len() % 4, 0); assert_eq!(SHADER_BYTES.len() % 4, 0);
} }
#[test] #[test]
fn includes_shader_const() { fn includes_shader_const() {
const SHADER_BYTES: &[u8] = include_shader!("test.pica"); const SHADER_BYTES: &[u8] = include_shader!("integration.pica");
assert_eq!(SHADER_BYTES.len() % 4, 0); assert_eq!(SHADER_BYTES.len() % 4, 0);
} }

Loading…
Cancel
Save