From 5c6200f39c31a6eb88a04e51ef8a1513b25ca0cf Mon Sep 17 00:00:00 2001 From: tuguzT Date: Wed, 1 Oct 2025 16:23:26 +0300 Subject: [PATCH 1/8] watch: Redesign `spirv-builder`'s watch API --- crates/spirv-builder/src/lib.rs | 13 +- crates/spirv-builder/src/watch.rs | 218 +++++++++++++++--------------- examples/runners/wgpu/src/lib.rs | 23 ++-- 3 files changed, 129 insertions(+), 125 deletions(-) diff --git a/crates/spirv-builder/src/lib.rs b/crates/spirv-builder/src/lib.rs index da3209f5c0..c408d04f9c 100644 --- a/crates/spirv-builder/src/lib.rs +++ b/crates/spirv-builder/src/lib.rs @@ -92,7 +92,7 @@ pub use rustc_codegen_spirv_types::Capability; pub use rustc_codegen_spirv_types::{CompileResult, ModuleResult}; #[cfg(feature = "watch")] -pub use self::watch::Watch; +pub use self::watch::{SpirvWatcher, SpirvWatcherError}; #[cfg(feature = "include-target-specs")] pub use rustc_codegen_spirv_target_specs::TARGET_SPEC_DIR_PATH; @@ -125,14 +125,15 @@ pub enum SpirvBuilderError { BuildFailed, #[error("multi-module build cannot be used with print_metadata = MetadataPrintout::Full")] MultiModuleWithPrintMetadata, - #[error("watching within build scripts will prevent build completion")] - WatchWithPrintMetadata, #[error("multi-module metadata file missing")] MetadataFileMissing(#[from] std::io::Error), #[error("unable to parse multi-module metadata file")] MetadataFileMalformed(#[from] serde_json::Error), #[error("cargo metadata error")] CargoMetadata(#[from] cargo_metadata::Error), + #[cfg(feature = "watch")] + #[error(transparent)] + WatchFailed(#[from] SpirvWatcherError), } const SPIRV_TARGET_PREFIX: &str = "spirv-unknown-"; @@ -518,6 +519,12 @@ impl Default for SpirvBuilder { } } +impl AsRef for SpirvBuilder { + fn as_ref(&self) -> &SpirvBuilder { + self + } +} + impl SpirvBuilder { pub fn new(path_to_crate: impl AsRef, target: impl Into) -> Self { Self { diff --git a/crates/spirv-builder/src/watch.rs b/crates/spirv-builder/src/watch.rs index 5689393210..cf0876d6c5 100644 --- a/crates/spirv-builder/src/watch.rs +++ b/crates/spirv-builder/src/watch.rs @@ -1,153 +1,149 @@ -use std::convert::Infallible; use std::path::{Path, PathBuf}; use std::sync::mpsc::Receiver; -use std::thread::JoinHandle; use std::{collections::HashSet, sync::mpsc::sync_channel}; -use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher as _}; +use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher}; use rustc_codegen_spirv_types::CompileResult; use crate::{SpirvBuilder, SpirvBuilderError, leaf_deps}; impl SpirvBuilder { - /// Watches the module for changes using [`notify`], rebuilding it upon changes. - /// - /// Calls `on_compilation_finishes` after each successful compilation. - /// The second `Option>` param allows you to return some `T` - /// on the first compile, which is then returned by this function - /// in pair with [`JoinHandle`] to the watching thread. - pub fn watch( - &self, - mut on_compilation_finishes: impl FnMut(CompileResult, Option>) - + Send - + 'static, - ) -> Result, SpirvBuilderError> { - let path_to_crate = self - .path_to_crate - .as_ref() - .ok_or(SpirvBuilderError::MissingCratePath)?; - if !matches!(self.print_metadata, crate::MetadataPrintout::None) { - return Err(SpirvBuilderError::WatchWithPrintMetadata); - } - - let metadata_result = crate::invoke_rustc(self); - // Load the dependencies of the thing - let metadata_file = if let Ok(path) = metadata_result { - path - } else { - // Fall back to watching from the crate root if the initial compilation fails - // This is likely to notice changes in the `target` dir, however, given that `cargo watch` doesn't seem to handle that, - let mut watcher = Watcher::new(); - watcher - .watcher - .watch(path_to_crate, RecursiveMode::Recursive) - .expect("Could watch crate root"); - loop { - watcher.recv(); - let metadata_file = crate::invoke_rustc(self); - if let Ok(f) = metadata_file { - break f; - } - } - }; - let metadata = self.parse_metadata_file(&metadata_file)?; - let mut first_compile = None; - on_compilation_finishes(metadata, Some(AcceptFirstCompile(&mut first_compile))); - - let builder = self.clone(); - let watch_thread = std::thread::spawn(move || { - let mut watcher = Watcher::new(); - watcher.watch_leaf_deps(&metadata_file); - - loop { - watcher.recv(); - let metadata_result = crate::invoke_rustc(&builder); - if let Ok(file) = metadata_result { - let metadata = builder - .parse_metadata_file(&file) - .expect("Metadata file is correct"); - watcher.watch_leaf_deps(&metadata_file); - on_compilation_finishes(metadata, None); - } - } - }); - - Ok(Watch { - first_compile, - watch_thread, - }) - } -} - -pub struct AcceptFirstCompile<'a, T>(&'a mut Option); - -impl<'a, T> AcceptFirstCompile<'a, T> { - pub fn new(write: &'a mut Option) -> Self { - Self(write) - } - - pub fn submit(self, t: T) { - *self.0 = Some(t); + /// Watches the module for changes, rebuilding it upon them. + pub fn watch(&self) -> Result, SpirvBuilderError> { + SpirvWatcher::new(self) } } -/// Result of [watching](SpirvBuilder::watch) a module for changes. -#[must_use] -#[non_exhaustive] -pub struct Watch { - /// Result of the first compile, if any. - pub first_compile: Option, - /// Join handle to the watching thread. - /// - /// You can drop it to detach the watching thread, - /// or [`join()`](JoinHandle::join) it to block the current thread until shutdown of the program. - pub watch_thread: JoinHandle, -} - -struct Watcher { +#[derive(Debug)] +pub struct SpirvWatcher { + builder: B, watcher: RecommendedWatcher, rx: Receiver<()>, + watch_path: PathBuf, watched_paths: HashSet, + first_result: bool, } -impl Watcher { - fn new() -> Self { +impl SpirvWatcher +where + B: AsRef, +{ + fn new(as_builder: B) -> Result { + let builder = as_builder.as_ref(); + let path_to_crate = builder + .path_to_crate + .as_ref() + .ok_or(SpirvBuilderError::MissingCratePath)?; + if !matches!(builder.print_metadata, crate::MetadataPrintout::None) { + return Err(SpirvWatcherError::WatchWithPrintMetadata.into()); + } + let (tx, rx) = sync_channel(0); let watcher = - notify::recommended_watcher(move |event: notify::Result| match event { - Ok(e) => match e.kind { - notify::EventKind::Access(_) => (), + notify::recommended_watcher(move |result: notify::Result| match result { + Ok(event) => match event.kind { notify::EventKind::Any | notify::EventKind::Create(_) | notify::EventKind::Modify(_) | notify::EventKind::Remove(_) | notify::EventKind::Other => { - let _ = tx.try_send(()); + if let Err(err) = tx.try_send(()) { + log::error!("send error: {err:?}"); + } } + notify::EventKind::Access(_) => {} }, - Err(e) => println!("notify error: {e:?}"), + Err(err) => log::error!("notify error: {err:?}"), }) - .expect("Could create watcher"); - Self { + .map_err(SpirvWatcherError::NotifyFailed)?; + + Ok(Self { + watch_path: path_to_crate.clone(), + builder: as_builder, watcher, rx, watched_paths: HashSet::new(), + first_result: false, + }) + } + + pub fn recv(&mut self) -> Result { + if !self.first_result { + return self.recv_first_result(); } + + self.rx.recv().expect("watcher should be alive"); + let builder = self.builder.as_ref(); + let metadata_file = crate::invoke_rustc(builder)?; + let result = builder.parse_metadata_file(&metadata_file)?; + + self.watch_leaf_deps(&self.watch_path.clone())?; + Ok(result) } - fn watch_leaf_deps(&mut self, metadata_file: &Path) { + fn recv_first_result(&mut self) -> Result { + let builder = self.builder.as_ref(); + let metadata_file = match crate::invoke_rustc(builder) { + Ok(path) => path, + Err(err) => { + log::error!("{err}"); + + self.watcher + .watch(&self.watch_path, RecursiveMode::Recursive) + .map_err(SpirvWatcherError::NotifyFailed)?; + let path = loop { + self.rx.recv().expect("watcher should be alive"); + match crate::invoke_rustc(builder) { + Ok(path) => break path, + Err(err) => log::error!("{err}"), + } + }; + self.watcher + .unwatch(&self.watch_path) + .map_err(SpirvWatcherError::NotifyFailed)?; + path + } + }; + let result = builder.parse_metadata_file(&metadata_file)?; + + self.watch_leaf_deps(&metadata_file)?; + self.watch_path = metadata_file; + self.first_result = true; + Ok(result) + } + + fn watch_leaf_deps(&mut self, metadata_file: &Path) -> Result<(), SpirvBuilderError> { leaf_deps(metadata_file, |it| { let path = it.to_path().unwrap(); - if self.watched_paths.insert(path.to_owned()) { - self.watcher + if self.watched_paths.insert(path.to_owned()) + && let Err(err) = self + .watcher .watch(it.to_path().unwrap(), RecursiveMode::NonRecursive) - .expect("Cargo dependencies are valid files"); + { + log::error!("files of cargo dependencies are not valid: {err}"); } }) - .expect("Could read dependencies file"); + .map_err(SpirvBuilderError::MetadataFileMissing) } +} - fn recv(&self) { - self.rx.recv().expect("Watcher still alive"); +impl SpirvWatcher<&SpirvBuilder> { + pub fn forget_lifetime(self) -> SpirvWatcher { + SpirvWatcher { + builder: self.builder.clone(), + watcher: self.watcher, + rx: self.rx, + watch_path: self.watch_path, + watched_paths: self.watched_paths, + first_result: self.first_result, + } } } + +#[derive(Debug, thiserror::Error)] +pub enum SpirvWatcherError { + #[error("watching within build scripts will prevent build completion")] + WatchWithPrintMetadata, + #[error("could not notify for changes: {0}")] + NotifyFailed(#[from] notify::Error), +} diff --git a/examples/runners/wgpu/src/lib.rs b/examples/runners/wgpu/src/lib.rs index da3c705598..ad368be3f2 100644 --- a/examples/runners/wgpu/src/lib.rs +++ b/examples/runners/wgpu/src/lib.rs @@ -177,18 +177,19 @@ fn maybe_watch( } if let Some(mut f) = on_watch { - builder - .watch(move |compile_result, accept| { + let mut watcher = builder.watch().unwrap(); + let first_compile = watcher.recv().unwrap(); + + let mut thread_watcher = watcher.forget_lifetime(); + std::thread::spawn(move || { + loop { + let compile_result = thread_watcher.recv().unwrap(); let modules = handle_compile_result(compile_result); - if let Some(accept) = accept { - accept.submit(modules); - } else { - f(modules); - } - }) - .expect("Configuration is correct for watching") - .first_compile - .unwrap() + f(modules); + } + }); + + handle_compile_result(first_compile) } else { handle_compile_result(builder.build().unwrap()) } From ca274154352b2fc9666b7635f95fd85685f80472 Mon Sep 17 00:00:00 2001 From: tuguzT Date: Wed, 1 Oct 2025 23:25:59 +0300 Subject: [PATCH 2/8] watch: Small refactoring --- crates/spirv-builder/src/watch.rs | 50 ++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/crates/spirv-builder/src/watch.rs b/crates/spirv-builder/src/watch.rs index cf0876d6c5..dbeb93ec1a 100644 --- a/crates/spirv-builder/src/watch.rs +++ b/crates/spirv-builder/src/watch.rs @@ -1,6 +1,8 @@ -use std::path::{Path, PathBuf}; -use std::sync::mpsc::Receiver; -use std::{collections::HashSet, sync::mpsc::sync_channel}; +use std::{ + collections::HashSet, + path::{Path, PathBuf}, + sync::mpsc::{Receiver, sync_channel}, +}; use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher}; use rustc_codegen_spirv_types::CompileResult; @@ -14,13 +16,16 @@ impl SpirvBuilder { } } +type WatchedPaths = HashSet; + +/// Watcher of a crate which rebuilds it on changes. #[derive(Debug)] pub struct SpirvWatcher { builder: B, watcher: RecommendedWatcher, rx: Receiver<()>, watch_path: PathBuf, - watched_paths: HashSet, + watched_paths: WatchedPaths, first_result: bool, } @@ -67,6 +72,10 @@ where }) } + /// Blocks the current thread until a change is detected + /// and the crate is rebuilt. + /// + /// Result of rebuilding of the crate is then returned to the caller. pub fn recv(&mut self) -> Result { if !self.first_result { return self.recv_first_result(); @@ -77,7 +86,7 @@ where let metadata_file = crate::invoke_rustc(builder)?; let result = builder.parse_metadata_file(&metadata_file)?; - self.watch_leaf_deps(&self.watch_path.clone())?; + Self::watch_leaf_deps(&self.watch_path, &mut self.watched_paths, &mut self.watcher)?; Ok(result) } @@ -88,8 +97,9 @@ where Err(err) => { log::error!("{err}"); + let watch_path = self.watch_path.as_ref(); self.watcher - .watch(&self.watch_path, RecursiveMode::Recursive) + .watch(watch_path, RecursiveMode::Recursive) .map_err(SpirvWatcherError::NotifyFailed)?; let path = loop { self.rx.recv().expect("watcher should be alive"); @@ -99,26 +109,28 @@ where } }; self.watcher - .unwatch(&self.watch_path) + .unwatch(watch_path) .map_err(SpirvWatcherError::NotifyFailed)?; path } }; let result = builder.parse_metadata_file(&metadata_file)?; - self.watch_leaf_deps(&metadata_file)?; + Self::watch_leaf_deps(&metadata_file, &mut self.watched_paths, &mut self.watcher)?; self.watch_path = metadata_file; self.first_result = true; Ok(result) } - fn watch_leaf_deps(&mut self, metadata_file: &Path) -> Result<(), SpirvBuilderError> { - leaf_deps(metadata_file, |it| { - let path = it.to_path().unwrap(); - if self.watched_paths.insert(path.to_owned()) - && let Err(err) = self - .watcher - .watch(it.to_path().unwrap(), RecursiveMode::NonRecursive) + fn watch_leaf_deps( + metadata_file: &Path, + watched_paths: &mut WatchedPaths, + watcher: &mut RecommendedWatcher, + ) -> Result<(), SpirvBuilderError> { + leaf_deps(metadata_file, |artifact| { + let path = artifact.to_path().unwrap(); + if watched_paths.insert(path.to_owned()) + && let Err(err) = watcher.watch(path, RecursiveMode::NonRecursive) { log::error!("files of cargo dependencies are not valid: {err}"); } @@ -127,10 +139,14 @@ where } } -impl SpirvWatcher<&SpirvBuilder> { +impl SpirvWatcher +where + B: AsRef, +{ + #[inline] pub fn forget_lifetime(self) -> SpirvWatcher { SpirvWatcher { - builder: self.builder.clone(), + builder: self.builder.as_ref().clone(), watcher: self.watcher, rx: self.rx, watch_path: self.watch_path, From 1c0270edf2f976c70b0afc282d97107c621fb4c4 Mon Sep 17 00:00:00 2001 From: firestar99 Date: Thu, 2 Oct 2025 10:00:02 +0200 Subject: [PATCH 3/8] watch: always consume `SpirvBuilder` --- crates/spirv-builder/src/lib.rs | 6 ---- crates/spirv-builder/src/watch.rs | 52 ++++++++++--------------------- examples/runners/wgpu/src/lib.rs | 7 ++--- 3 files changed, 19 insertions(+), 46 deletions(-) diff --git a/crates/spirv-builder/src/lib.rs b/crates/spirv-builder/src/lib.rs index c408d04f9c..de5e4d53d5 100644 --- a/crates/spirv-builder/src/lib.rs +++ b/crates/spirv-builder/src/lib.rs @@ -519,12 +519,6 @@ impl Default for SpirvBuilder { } } -impl AsRef for SpirvBuilder { - fn as_ref(&self) -> &SpirvBuilder { - self - } -} - impl SpirvBuilder { pub fn new(path_to_crate: impl AsRef, target: impl Into) -> Self { Self { diff --git a/crates/spirv-builder/src/watch.rs b/crates/spirv-builder/src/watch.rs index dbeb93ec1a..66afbffca7 100644 --- a/crates/spirv-builder/src/watch.rs +++ b/crates/spirv-builder/src/watch.rs @@ -11,7 +11,7 @@ use crate::{SpirvBuilder, SpirvBuilderError, leaf_deps}; impl SpirvBuilder { /// Watches the module for changes, rebuilding it upon them. - pub fn watch(&self) -> Result, SpirvBuilderError> { + pub fn watch(self) -> Result { SpirvWatcher::new(self) } } @@ -20,25 +20,24 @@ type WatchedPaths = HashSet; /// Watcher of a crate which rebuilds it on changes. #[derive(Debug)] -pub struct SpirvWatcher { - builder: B, +pub struct SpirvWatcher { + builder: SpirvBuilder, watcher: RecommendedWatcher, rx: Receiver<()>, + /// `first_result`: the path to the crate + /// `!first_result`: the path to our metadata file with entry point names and file paths watch_path: PathBuf, watched_paths: WatchedPaths, first_result: bool, } -impl SpirvWatcher -where - B: AsRef, -{ - fn new(as_builder: B) -> Result { - let builder = as_builder.as_ref(); +impl SpirvWatcher { + fn new(builder: SpirvBuilder) -> Result { let path_to_crate = builder .path_to_crate .as_ref() - .ok_or(SpirvBuilderError::MissingCratePath)?; + .ok_or(SpirvBuilderError::MissingCratePath)? + .clone(); if !matches!(builder.print_metadata, crate::MetadataPrintout::None) { return Err(SpirvWatcherError::WatchWithPrintMetadata.into()); } @@ -63,8 +62,8 @@ where .map_err(SpirvWatcherError::NotifyFailed)?; Ok(Self { - watch_path: path_to_crate.clone(), - builder: as_builder, + watch_path: path_to_crate, + builder, watcher, rx, watched_paths: HashSet::new(), @@ -82,17 +81,15 @@ where } self.rx.recv().expect("watcher should be alive"); - let builder = self.builder.as_ref(); - let metadata_file = crate::invoke_rustc(builder)?; - let result = builder.parse_metadata_file(&metadata_file)?; + let metadata_file = crate::invoke_rustc(&self.builder)?; + let result = self.builder.parse_metadata_file(&metadata_file)?; Self::watch_leaf_deps(&self.watch_path, &mut self.watched_paths, &mut self.watcher)?; Ok(result) } fn recv_first_result(&mut self) -> Result { - let builder = self.builder.as_ref(); - let metadata_file = match crate::invoke_rustc(builder) { + let metadata_file = match crate::invoke_rustc(&self.builder) { Ok(path) => path, Err(err) => { log::error!("{err}"); @@ -103,7 +100,7 @@ where .map_err(SpirvWatcherError::NotifyFailed)?; let path = loop { self.rx.recv().expect("watcher should be alive"); - match crate::invoke_rustc(builder) { + match crate::invoke_rustc(&self.builder) { Ok(path) => break path, Err(err) => log::error!("{err}"), } @@ -114,7 +111,7 @@ where path } }; - let result = builder.parse_metadata_file(&metadata_file)?; + let result = self.builder.parse_metadata_file(&metadata_file)?; Self::watch_leaf_deps(&metadata_file, &mut self.watched_paths, &mut self.watcher)?; self.watch_path = metadata_file; @@ -139,23 +136,6 @@ where } } -impl SpirvWatcher -where - B: AsRef, -{ - #[inline] - pub fn forget_lifetime(self) -> SpirvWatcher { - SpirvWatcher { - builder: self.builder.as_ref().clone(), - watcher: self.watcher, - rx: self.rx, - watch_path: self.watch_path, - watched_paths: self.watched_paths, - first_result: self.first_result, - } - } -} - #[derive(Debug, thiserror::Error)] pub enum SpirvWatcherError { #[error("watching within build scripts will prevent build completion")] diff --git a/examples/runners/wgpu/src/lib.rs b/examples/runners/wgpu/src/lib.rs index ad368be3f2..b67db69eda 100644 --- a/examples/runners/wgpu/src/lib.rs +++ b/examples/runners/wgpu/src/lib.rs @@ -180,16 +180,15 @@ fn maybe_watch( let mut watcher = builder.watch().unwrap(); let first_compile = watcher.recv().unwrap(); - let mut thread_watcher = watcher.forget_lifetime(); + let shader_modules = handle_compile_result(first_compile); std::thread::spawn(move || { loop { - let compile_result = thread_watcher.recv().unwrap(); + let compile_result = watcher.recv().unwrap(); let modules = handle_compile_result(compile_result); f(modules); } }); - - handle_compile_result(first_compile) + shader_modules } else { handle_compile_result(builder.build().unwrap()) } From 89017e5e6c767b55f0b86416922ae02c58603b6a Mon Sep 17 00:00:00 2001 From: firestar99 Date: Thu, 2 Oct 2025 10:05:24 +0200 Subject: [PATCH 4/8] watch: minor code cleanup --- crates/spirv-builder/src/watch.rs | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/crates/spirv-builder/src/watch.rs b/crates/spirv-builder/src/watch.rs index 66afbffca7..a5b0c9e176 100644 --- a/crates/spirv-builder/src/watch.rs +++ b/crates/spirv-builder/src/watch.rs @@ -1,14 +1,12 @@ +use crate::{SpirvBuilder, SpirvBuilderError, leaf_deps}; +use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher}; +use rustc_codegen_spirv_types::CompileResult; use std::{ collections::HashSet, - path::{Path, PathBuf}, + path::PathBuf, sync::mpsc::{Receiver, sync_channel}, }; -use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher}; -use rustc_codegen_spirv_types::CompileResult; - -use crate::{SpirvBuilder, SpirvBuilderError, leaf_deps}; - impl SpirvBuilder { /// Watches the module for changes, rebuilding it upon them. pub fn watch(self) -> Result { @@ -24,8 +22,8 @@ pub struct SpirvWatcher { builder: SpirvBuilder, watcher: RecommendedWatcher, rx: Receiver<()>, - /// `first_result`: the path to the crate - /// `!first_result`: the path to our metadata file with entry point names and file paths + /// `!first_result`: the path to the crate + /// `first_result`: the path to our metadata file with entry point names and file paths watch_path: PathBuf, watched_paths: WatchedPaths, first_result: bool, @@ -84,7 +82,7 @@ impl SpirvWatcher { let metadata_file = crate::invoke_rustc(&self.builder)?; let result = self.builder.parse_metadata_file(&metadata_file)?; - Self::watch_leaf_deps(&self.watch_path, &mut self.watched_paths, &mut self.watcher)?; + self.watch_leaf_deps()?; Ok(result) } @@ -113,21 +111,17 @@ impl SpirvWatcher { }; let result = self.builder.parse_metadata_file(&metadata_file)?; - Self::watch_leaf_deps(&metadata_file, &mut self.watched_paths, &mut self.watcher)?; self.watch_path = metadata_file; self.first_result = true; + self.watch_leaf_deps()?; Ok(result) } - fn watch_leaf_deps( - metadata_file: &Path, - watched_paths: &mut WatchedPaths, - watcher: &mut RecommendedWatcher, - ) -> Result<(), SpirvBuilderError> { - leaf_deps(metadata_file, |artifact| { + fn watch_leaf_deps(&mut self) -> Result<(), SpirvBuilderError> { + leaf_deps(&self.watch_path, |artifact| { let path = artifact.to_path().unwrap(); - if watched_paths.insert(path.to_owned()) - && let Err(err) = watcher.watch(path, RecursiveMode::NonRecursive) + if self.watched_paths.insert(path.to_owned()) + && let Err(err) = self.watcher.watch(path, RecursiveMode::NonRecursive) { log::error!("files of cargo dependencies are not valid: {err}"); } From cc293816a9c5b176b91bde2f6fb2342a4d9b8f35 Mon Sep 17 00:00:00 2001 From: firestar99 Date: Thu, 2 Oct 2025 10:31:11 +0200 Subject: [PATCH 5/8] spirv-builder: also search cwd for dylib --- crates/spirv-builder/src/lib.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/spirv-builder/src/lib.rs b/crates/spirv-builder/src/lib.rs index de5e4d53d5..d3082dd327 100644 --- a/crates/spirv-builder/src/lib.rs +++ b/crates/spirv-builder/src/lib.rs @@ -753,10 +753,14 @@ fn dylib_path_envvar() -> &'static str { } } fn dylib_path() -> Vec { - match env::var_os(dylib_path_envvar()) { + let mut dylibs = match env::var_os(dylib_path_envvar()) { Some(var) => env::split_paths(&var).collect(), None => Vec::new(), + }; + if let Ok(dir) = env::current_dir() { + dylibs.push(dir); } + dylibs } fn find_rustc_codegen_spirv() -> Result { @@ -766,7 +770,8 @@ fn find_rustc_codegen_spirv() -> Result { env::consts::DLL_PREFIX, env::consts::DLL_SUFFIX ); - for mut path in dylib_path() { + let dylib_paths = dylib_path(); + for mut path in dylib_paths { path.push(&filename); if path.is_file() { return Ok(path); From f1781893a978db79ba25971af23e6466ce0fd14f Mon Sep 17 00:00:00 2001 From: firestar99 Date: Thu, 2 Oct 2025 10:44:01 +0200 Subject: [PATCH 6/8] watch: handle errors and changes during compile gracefully --- crates/spirv-builder/src/watch.rs | 21 +++++++++++++-------- examples/runners/wgpu/src/lib.rs | 9 ++++++--- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/crates/spirv-builder/src/watch.rs b/crates/spirv-builder/src/watch.rs index a5b0c9e176..ed0aa0c22c 100644 --- a/crates/spirv-builder/src/watch.rs +++ b/crates/spirv-builder/src/watch.rs @@ -1,6 +1,7 @@ use crate::{SpirvBuilder, SpirvBuilderError, leaf_deps}; use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher}; use rustc_codegen_spirv_types::CompileResult; +use std::sync::mpsc::TrySendError; use std::{ collections::HashSet, path::PathBuf, @@ -40,7 +41,7 @@ impl SpirvWatcher { return Err(SpirvWatcherError::WatchWithPrintMetadata.into()); } - let (tx, rx) = sync_channel(0); + let (tx, rx) = sync_channel(1); let watcher = notify::recommended_watcher(move |result: notify::Result| match result { Ok(event) => match event.kind { @@ -48,11 +49,13 @@ impl SpirvWatcher { | notify::EventKind::Create(_) | notify::EventKind::Modify(_) | notify::EventKind::Remove(_) - | notify::EventKind::Other => { - if let Err(err) = tx.try_send(()) { - log::error!("send error: {err:?}"); - } - } + | notify::EventKind::Other => match tx.try_send(()) { + Ok(_) => (), + // disconnect is fine, SpirvWatcher is currently dropping + Err(TrySendError::Disconnected(_)) => (), + // full is fine, we just need to send a single event anyway + Err(TrySendError::Full(_)) => (), + }, notify::EventKind::Access(_) => {} }, Err(err) => log::error!("notify error: {err:?}"), @@ -78,7 +81,7 @@ impl SpirvWatcher { return self.recv_first_result(); } - self.rx.recv().expect("watcher should be alive"); + self.rx.recv().map_err(|_| SpirvWatcherError::WatcherDied)?; let metadata_file = crate::invoke_rustc(&self.builder)?; let result = self.builder.parse_metadata_file(&metadata_file)?; @@ -97,7 +100,7 @@ impl SpirvWatcher { .watch(watch_path, RecursiveMode::Recursive) .map_err(SpirvWatcherError::NotifyFailed)?; let path = loop { - self.rx.recv().expect("watcher should be alive"); + self.rx.recv().map_err(|_| SpirvWatcherError::WatcherDied)?; match crate::invoke_rustc(&self.builder) { Ok(path) => break path, Err(err) => log::error!("{err}"), @@ -136,4 +139,6 @@ pub enum SpirvWatcherError { WatchWithPrintMetadata, #[error("could not notify for changes: {0}")] NotifyFailed(#[from] notify::Error), + #[error("watcher died and closed channel")] + WatcherDied, } diff --git a/examples/runners/wgpu/src/lib.rs b/examples/runners/wgpu/src/lib.rs index b67db69eda..60730745da 100644 --- a/examples/runners/wgpu/src/lib.rs +++ b/examples/runners/wgpu/src/lib.rs @@ -183,9 +183,12 @@ fn maybe_watch( let shader_modules = handle_compile_result(first_compile); std::thread::spawn(move || { loop { - let compile_result = watcher.recv().unwrap(); - let modules = handle_compile_result(compile_result); - f(modules); + match watcher.recv() { + Ok(compile_result) => { + f(handle_compile_result(compile_result)); + } + Err(e) => println!("Shader compiling failed: {e}"), + } } }); shader_modules From e0b5e1c63b607b80f92812183298a2d760a0f403 Mon Sep 17 00:00:00 2001 From: firestar99 Date: Thu, 2 Oct 2025 11:51:26 +0200 Subject: [PATCH 7/8] watch: add `try_recv`, a non-blocking variant --- crates/spirv-builder/src/watch.rs | 129 +++++++++++++++++++----------- examples/runners/wgpu/src/lib.rs | 7 +- 2 files changed, 89 insertions(+), 47 deletions(-) diff --git a/crates/spirv-builder/src/watch.rs b/crates/spirv-builder/src/watch.rs index ed0aa0c22c..240917beb9 100644 --- a/crates/spirv-builder/src/watch.rs +++ b/crates/spirv-builder/src/watch.rs @@ -1,7 +1,8 @@ use crate::{SpirvBuilder, SpirvBuilderError, leaf_deps}; use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher}; use rustc_codegen_spirv_types::CompileResult; -use std::sync::mpsc::TrySendError; +use std::path::Path; +use std::sync::mpsc::{TryRecvError, TrySendError}; use std::{ collections::HashSet, path::PathBuf, @@ -17,17 +18,34 @@ impl SpirvBuilder { type WatchedPaths = HashSet; +#[derive(Copy, Clone, Debug)] +enum WatcherState { + /// upcoming compile is the first compile: + /// * always recompile regardless of file watches + /// * success: go to [`Self::Watching`] + /// * fail: go to [`Self::FirstFailed`] + First, + /// the first compile (and all consecutive ones) failed: + /// * only recompile when watcher notifies us + /// * the whole project dir is being watched, remove that watch + /// * success: go to [`Self::Watching`] + /// * fail: stay in [`Self::FirstFailed`] + FirstFailed, + /// at least one compile finished and has set up the proper file watches: + /// * only recompile when watcher notifies us + /// * always stays in [`Self::Watching`] + Watching, +} + /// Watcher of a crate which rebuilds it on changes. #[derive(Debug)] pub struct SpirvWatcher { builder: SpirvBuilder, watcher: RecommendedWatcher, rx: Receiver<()>, - /// `!first_result`: the path to the crate - /// `first_result`: the path to our metadata file with entry point names and file paths - watch_path: PathBuf, + path_to_crate: PathBuf, watched_paths: WatchedPaths, - first_result: bool, + state: WatcherState, } impl SpirvWatcher { @@ -63,65 +81,84 @@ impl SpirvWatcher { .map_err(SpirvWatcherError::NotifyFailed)?; Ok(Self { - watch_path: path_to_crate, + path_to_crate, builder, watcher, rx, watched_paths: HashSet::new(), - first_result: false, + state: WatcherState::First, }) } - /// Blocks the current thread until a change is detected - /// and the crate is rebuilt. + /// Blocks the current thread until a change is detected, rebuilds the crate and returns the [`CompileResult`] or + /// an [`SpirvBuilderError`]. Always builds once when called for the first time. /// - /// Result of rebuilding of the crate is then returned to the caller. + /// See [`Self::try_recv`] for a non-blocking variant. pub fn recv(&mut self) -> Result { - if !self.first_result { - return self.recv_first_result(); - } - - self.rx.recv().map_err(|_| SpirvWatcherError::WatcherDied)?; - let metadata_file = crate::invoke_rustc(&self.builder)?; - let result = self.builder.parse_metadata_file(&metadata_file)?; + self.recv_inner(|rx| rx.recv().map_err(|err| TryRecvError::from(err))) + .map(|result| result.unwrap()) + } - self.watch_leaf_deps()?; - Ok(result) + /// If a change is detected or this is the first invocation, builds the crate and returns the [`CompileResult`] + /// (wrapped in `Some`) or an [`SpirvBuilderError`]. If no change has been detected, returns `Ok(None)` without + /// blocking. + /// + /// See [`Self::recv`] for a blocking variant. + pub fn try_recv(&mut self) -> Result, SpirvBuilderError> { + self.recv_inner(Receiver::try_recv) } - fn recv_first_result(&mut self) -> Result { - let metadata_file = match crate::invoke_rustc(&self.builder) { - Ok(path) => path, - Err(err) => { - log::error!("{err}"); + #[inline] + fn recv_inner( + &mut self, + recv: impl FnOnce(&Receiver<()>) -> Result<(), TryRecvError>, + ) -> Result, SpirvBuilderError> { + let received = match self.state { + // always compile on first invocation + // file watches have yet to be setup, so recv channel is empty and must not be cleared + WatcherState::First => Ok(()), + WatcherState::FirstFailed | WatcherState::Watching => recv(&self.rx), + }; + match received { + Ok(_) => (), + Err(TryRecvError::Empty) => return Ok(None), + Err(TryRecvError::Disconnected) => return Err(SpirvWatcherError::WatcherDied.into()), + } - let watch_path = self.watch_path.as_ref(); - self.watcher - .watch(watch_path, RecursiveMode::Recursive) - .map_err(SpirvWatcherError::NotifyFailed)?; - let path = loop { - self.rx.recv().map_err(|_| SpirvWatcherError::WatcherDied)?; - match crate::invoke_rustc(&self.builder) { - Ok(path) => break path, - Err(err) => log::error!("{err}"), + let result = (|| { + let metadata_file = crate::invoke_rustc(&self.builder)?; + let result = self.builder.parse_metadata_file(&metadata_file)?; + self.watch_leaf_deps(&metadata_file)?; + Ok(result) + })(); + match result { + Ok(result) => { + if matches!(self.state, WatcherState::FirstFailed) { + self.watcher + .unwatch(&self.path_to_crate) + .map_err(SpirvWatcherError::NotifyFailed)?; + } + self.state = WatcherState::Watching; + Ok(Some(result)) + } + Err(err) => { + self.state = match self.state { + WatcherState::First => { + self.watcher + .watch(&self.path_to_crate, RecursiveMode::Recursive) + .map_err(SpirvWatcherError::NotifyFailed)?; + WatcherState::FirstFailed } + WatcherState::FirstFailed => WatcherState::FirstFailed, + WatcherState::Watching => WatcherState::Watching, }; - self.watcher - .unwatch(watch_path) - .map_err(SpirvWatcherError::NotifyFailed)?; - path + Err(err) } - }; - let result = self.builder.parse_metadata_file(&metadata_file)?; - - self.watch_path = metadata_file; - self.first_result = true; - self.watch_leaf_deps()?; - Ok(result) + } } - fn watch_leaf_deps(&mut self) -> Result<(), SpirvBuilderError> { - leaf_deps(&self.watch_path, |artifact| { + fn watch_leaf_deps(&mut self, watch_path: &Path) -> Result<(), SpirvBuilderError> { + leaf_deps(watch_path, |artifact| { let path = artifact.to_path().unwrap(); if self.watched_paths.insert(path.to_owned()) && let Err(err) = self.watcher.watch(path, RecursiveMode::NonRecursive) diff --git a/examples/runners/wgpu/src/lib.rs b/examples/runners/wgpu/src/lib.rs index 60730745da..e40c529042 100644 --- a/examples/runners/wgpu/src/lib.rs +++ b/examples/runners/wgpu/src/lib.rs @@ -178,7 +178,12 @@ fn maybe_watch( if let Some(mut f) = on_watch { let mut watcher = builder.watch().unwrap(); - let first_compile = watcher.recv().unwrap(); + let first_compile = loop { + match watcher.recv() { + Ok(e) => break e, + Err(e) => println!("Shader compiling failed: {e}"), + } + }; let shader_modules = handle_compile_result(first_compile); std::thread::spawn(move || { From 13a80b7ed9fbd5738746ef31ab8ddfee3f403551 Mon Sep 17 00:00:00 2001 From: firestar99 Date: Thu, 2 Oct 2025 11:54:28 +0200 Subject: [PATCH 8/8] watch: fix clippy --- crates/spirv-builder/src/watch.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/crates/spirv-builder/src/watch.rs b/crates/spirv-builder/src/watch.rs index 240917beb9..b13f2cf5b2 100644 --- a/crates/spirv-builder/src/watch.rs +++ b/crates/spirv-builder/src/watch.rs @@ -2,7 +2,7 @@ use crate::{SpirvBuilder, SpirvBuilderError, leaf_deps}; use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher}; use rustc_codegen_spirv_types::CompileResult; use std::path::Path; -use std::sync::mpsc::{TryRecvError, TrySendError}; +use std::sync::mpsc::TryRecvError; use std::{ collections::HashSet, path::PathBuf, @@ -67,14 +67,12 @@ impl SpirvWatcher { | notify::EventKind::Create(_) | notify::EventKind::Modify(_) | notify::EventKind::Remove(_) - | notify::EventKind::Other => match tx.try_send(()) { - Ok(_) => (), - // disconnect is fine, SpirvWatcher is currently dropping - Err(TrySendError::Disconnected(_)) => (), - // full is fine, we just need to send a single event anyway - Err(TrySendError::Full(_)) => (), - }, - notify::EventKind::Access(_) => {} + | notify::EventKind::Other => { + // `Err(Disconnected)` is fine, SpirvWatcher is currently dropping + // `Err(Full)` is fine, we just need to send a single event anyway + tx.try_send(()).ok(); + } + notify::EventKind::Access(_) => (), }, Err(err) => log::error!("notify error: {err:?}"), }) @@ -95,7 +93,7 @@ impl SpirvWatcher { /// /// See [`Self::try_recv`] for a non-blocking variant. pub fn recv(&mut self) -> Result { - self.recv_inner(|rx| rx.recv().map_err(|err| TryRecvError::from(err))) + self.recv_inner(|rx| rx.recv().map_err(TryRecvError::from)) .map(|result| result.unwrap()) }