From 80398d8149b276d51258db8e613a60cbce1084e4 Mon Sep 17 00:00:00 2001 From: khyperia Date: Sat, 10 Oct 2020 12:25:05 +0200 Subject: [PATCH] Rebuild shader crates if changed --- Cargo.lock | 8 ++ spirv-builder/Cargo.toml | 2 + spirv-builder/src/depfile.rs | 143 +++++++++++++++++++++++++++++++++++ spirv-builder/src/lib.rs | 97 ++++++++++++++---------- 4 files changed, 211 insertions(+), 39 deletions(-) create mode 100644 spirv-builder/src/depfile.rs diff --git a/Cargo.lock b/Cargo.lock index 2f7027ec90..26e72efb25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/spirv-builder/Cargo.toml b/spirv-builder/Cargo.toml index ba99327e6d..23c341da89 100644 --- a/spirv-builder/Cargo.toml +++ b/spirv-builder/Cargo.toml @@ -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 diff --git a/spirv-builder/src/depfile.rs b/spirv-builder/src/depfile.rs new file mode 100644 index 0000000000..be0acecbc1 --- /dev/null +++ b/spirv-builder/src/depfile.rs @@ -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) -> 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) -> 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, + /// The rest of the paths we've finished reading. + deps: Vec, +} + +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) -> Result<(), Error>, + ) -> Result<(), Error> { + self.finish_path()?; + if let Some(target) = self.target.take() { + f(target, replace(&mut self.deps, Vec::new()))?; + } + Ok(()) + } +} diff --git a/spirv-builder/src/lib.rs b/spirv-builder/src/lib.rs index 9d36713ecd..f4276bbb64 100644 --- a/spirv-builder/src/lib.rs +++ b/spirv-builder/src/lib.rs @@ -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) -> Result<(), SpirvBuilderEr Ok(()) } -#[derive(Deserialize)] -struct RustcOutput { - reason: String, - filenames: Option>, -} - -fn get_last_artifact(out: &str) -> String { - let out = serde_json::Deserializer::from_str(out).into_iter::(); - 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 { // 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 { "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>, +} + +fn get_last_artifact(out: &str) -> String { + let out = serde_json::Deserializer::from_str(out).into_iter::(); + 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>, 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()); +}