Rebuild shader crates if changed

This commit is contained in:
khyperia 2020-10-10 12:25:05 +02:00
parent 8c9583f2c6
commit 80398d8149
4 changed files with 211 additions and 39 deletions

8
Cargo.lock generated
View File

@ -910,6 +910,12 @@ dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "raw-string"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0501e134c6905fee1f10fed25b0a7e1261bf676cffac9543a7d0730dec01af2"
[[package]]
name = "raw-window-handle"
version = "0.3.3"
@ -1176,6 +1182,8 @@ dependencies = [
name = "spirv-builder"
version = "0.1.0"
dependencies = [
"memchr",
"raw-string",
"rustc_codegen_spirv",
"serde",
"serde_json",

View File

@ -7,6 +7,8 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
memchr = "2.3"
raw-string = "0.3.5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# See comment in lib.rs invoke_rustc for why this is here

View File

@ -0,0 +1,143 @@
//! Reading Makefile-style dependency files.
//! Taken with permission from https://github.com/m-ou-se/ninj/blob/master/lib/depfile/mod.rs
use raw_string::{RawStr, RawString};
use std::fs::File;
use std::io::{BufRead, BufReader, Error, ErrorKind, Read};
use std::mem::replace;
use std::path::Path;
/// Read a Makfile-style dependency file.
///
/// `f` is called for every target. The first argument is the target, the
/// second is the list of dependencies.
pub fn read_deps_file(
file_name: &Path,
f: impl FnMut(RawString, Vec<RawString>) -> Result<(), Error>,
) -> Result<(), Error> {
let file = File::open(file_name)
.map_err(|e| Error::new(e.kind(), format!("Unable to read {:?}: {}", file_name, e)))?;
read_deps_file_from(file, f)
}
/// Read a Makfile-style dependency file.
///
/// `f` is called for every target. The first argument is the target, the
/// second is the list of dependencies.
pub fn read_deps_file_from(
file: impl Read,
mut f: impl FnMut(RawString, Vec<RawString>) -> Result<(), Error>,
) -> Result<(), Error> {
let mut file = BufReader::new(file);
let mut state = State::default();
let mut line = RawString::new();
loop {
line.clear();
if file.read_until(b'\n', &mut line.as_mut_bytes())? == 0 {
break;
}
if line.last() == Some(b'\n') {
line.pop();
}
if cfg!(windows) && line.last() == Some(b'\r') {
line.pop();
}
let mut write_offset = 0;
let mut read_offset = 0;
loop {
match memchr::memchr2(b' ', b'\\', line[read_offset..].as_bytes())
.map(|i| i + read_offset)
{
Some(i) if line[i] == b'\\' && i + 1 == line.len() => {
// Backslash at the end of the line
state.add_part(&line[write_offset..i]);
state.finish_path()?;
break;
}
Some(i) if line[i] == b'\\' => {
// Backslash before character.
let c = line[i + 1];
match c {
b' ' | b'\\' | b'#' | b'*' | b'[' | b']' | b'|' => {
// Escaped character. Drop the '\'.
state.add_part(&line[write_offset..i]);
write_offset = i + 1;
}
_ => (), // Keep the '\'.
}
read_offset = i + 2;
}
Some(i) => {
// A space.
debug_assert_eq!(line[i], b' ');
state.add_part(&line[write_offset..i]);
state.finish_path()?;
write_offset = i + 1;
read_offset = i + 1;
}
None => {
// End of the line.
state.add_part(&line[write_offset..]);
state.finish_deps(&mut f)?;
break;
}
}
}
}
if state.target.is_none() {
Ok(())
} else {
Err(Error::new(ErrorKind::InvalidData, "Unexpected end of file"))
}
}
#[derive(Default)]
struct State {
/// The (incomplete) path we're currently reading.
path: RawString,
/// The target, once we've finished reading it.
target: Option<RawString>,
/// The rest of the paths we've finished reading.
deps: Vec<RawString>,
}
impl State {
fn add_part(&mut self, s: &RawStr) {
self.path.push_str(s);
}
fn finish_path(&mut self) -> Result<(), Error> {
if !self.path.is_empty() {
let mut path = replace(&mut self.path, RawString::new());
if self.target.is_none() && path.last() == Some(b':') {
path.pop();
self.target = Some(path);
} else if self.target.is_none() {
return Err(Error::new(
ErrorKind::InvalidData,
"Rule in dependency file has multiple outputs",
));
} else {
self.deps.push(path);
}
}
Ok(())
}
fn finish_deps(
&mut self,
f: &mut impl FnMut(RawString, Vec<RawString>) -> Result<(), Error>,
) -> Result<(), Error> {
self.finish_path()?;
if let Some(target) = self.target.take() {
f(target, replace(&mut self.deps, Vec::new()))?;
}
Ok(())
}
}

View File

@ -1,24 +1,22 @@
mod depfile;
use raw_string::{RawStr, RawString};
use serde::Deserialize;
use std::collections::HashMap;
use std::error::Error;
use std::fmt;
use std::path::Path;
use std::process::{Command, Output};
use std::process::{Command, Stdio};
#[derive(Debug)]
pub enum SpirvBuilderError {
BuildFailed(Output),
BuildFailed,
}
impl fmt::Display for SpirvBuilderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SpirvBuilderError::BuildFailed(output) => write!(
f,
"{}\nstdout:\n{}\nstderr:\n{}",
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
),
SpirvBuilderError::BuildFailed => f.write_str("Build failed"),
}
}
}
@ -33,34 +31,6 @@ pub fn build_spirv(path_to_crate: impl AsRef<Path>) -> Result<(), SpirvBuilderEr
Ok(())
}
#[derive(Deserialize)]
struct RustcOutput {
reason: String,
filenames: Option<Vec<String>>,
}
fn get_last_artifact(out: &str) -> String {
let out = serde_json::Deserializer::from_str(out).into_iter::<RustcOutput>();
let last = out
.map(|line| line.unwrap())
.filter(|line| line.reason == "compiler-artifact")
.last()
.expect("Did not find output file in rustc output");
let mut filenames = last.filenames.unwrap();
eprintln!("{:?}", filenames);
if cfg!(windows) {
filenames
.iter()
.find(|&v| v.ends_with(".dll") || v.ends_with(".spv"))
.unwrap()
.clone()
} else {
assert_eq!(filenames.len(), 1);
filenames.pop().unwrap()
}
}
fn invoke_rustc(path_to_crate: &Path) -> Result<String, SpirvBuilderError> {
// Okay, this is a little bonkers: in a normal world, we'd have the user clone
// rustc_codegen_spirv and pass in the path to it, and then we'd invoke cargo to build it, grab
@ -84,14 +54,63 @@ fn invoke_rustc(path_to_crate: &Path) -> Result<String, SpirvBuilderError> {
"spirv-unknown-unknown",
"--release",
])
.stderr(Stdio::inherit())
.current_dir(path_to_crate)
.env("RUSTFLAGS", rustflags)
.env("SPIRV_VAL", "1")
.output()
.expect("failed to execute cargo build");
if build.status.success() {
Ok(get_last_artifact(&String::from_utf8(build.stdout).unwrap()))
let stdout = String::from_utf8(build.stdout).unwrap();
let artifact = get_last_artifact(&stdout);
print_deps_of(&artifact);
Ok(artifact)
} else {
Err(SpirvBuilderError::BuildFailed(build))
Err(SpirvBuilderError::BuildFailed)
}
}
#[derive(Deserialize)]
struct RustcOutput {
reason: String,
filenames: Option<Vec<String>>,
}
fn get_last_artifact(out: &str) -> String {
let out = serde_json::Deserializer::from_str(out).into_iter::<RustcOutput>();
let last = out
.map(|line| line.unwrap())
.filter(|line| line.reason == "compiler-artifact")
.last()
.expect("Did not find output file in rustc output");
let mut filenames = last
.filenames
.unwrap()
.into_iter()
.filter(|v| v.ends_with(".spv"));
let filename = filenames.next().expect("Crate had no .spv artifacts");
assert_eq!(filenames.next(), None, "Crate had multiple .spv artifacts");
filename
}
fn print_deps_of(artifact: &str) {
let deps_file = Path::new(artifact).with_extension("d");
let mut deps_map = HashMap::new();
depfile::read_deps_file(&deps_file, |item, deps| {
deps_map.insert(item, deps);
Ok(())
})
.expect("Could not read dep file");
fn recurse(map: &HashMap<RawString, Vec<RawString>>, artifact: &RawStr) {
match map.get(artifact) {
Some(entries) => {
for entry in entries {
recurse(map, entry)
}
}
None => println!("cargo:rerun-if-changed={}", artifact),
}
}
recurse(&deps_map, artifact.into());
}