From 35019609c27f775f2614843cf0ea742fd73c6c7f Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Tue, 6 Oct 2026 19:08:38 +0000 Subject: [PATCH 1/6] Fix watcher missing watch.lock removal during full rebuilds On macOS, notify's FSEvents backend stops and recreates its event stream on every watch/unwatch call, and events in between are lost. Full rebuilds unwatched and re-watched every path, so a watch.lock removal right after a source file removal (as in the rewatch watch tests) could be dropped, leaving the watcher running. Only unwatch/watch the paths that actually changed, and check that watch.lock still exists after a full rebuild. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Christoph Knittel --- CHANGELOG.md | 1 + rewatch/src/watcher.rs | 42 +++++++++++++++++++++++++++++++++++------- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 532b01f3f8f..59890b7c1f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ - Fix the formatter dropping a `@res.ternary` attribute written in source, e.g. `let b = @res.ternary x`, and print `@JSX` and `@res.ternary` with the same parens as other attributes, e.g. `(@JSX x) + 1`. https://github.com/rescript-lang/rescript/pull/8740 - Respect `@warning` attributes on the module in `module type of`, e.g. `module type of @warning("-3") DeprecatedModule`, which still reported the warning. https://github.com/rescript-lang/rescript/pull/8738 - Fix the formatter dropping or moving attributes and `await` on module expressions, such as `module M = @attr F(X)`, and printing invalid code for module constraints and functors in some positions, such as `module M: T = (X: S)`. https://github.com/rescript-lang/rescript/pull/8735 +- Fix `rescript watch` sometimes not exiting on macOS when `lib/watch.lock` is removed during a full rebuild, and stop re-registering unchanged watch paths on full rebuilds. https://github.com/rescript-lang/rescript/pull/PLACEHOLDER - Make rewatch compile independent modules after an unrelated failure and recompile blocked dependents when a changed interface survives a failed implementation, including across full watcher rebuilds. https://github.com/rescript-lang/rescript/pull/8667 - Fix `RescriptTools.binaryPath` failing with `ERR_PACKAGE_IMPORT_NOT_DEFINED` when used from a user project. https://github.com/rescript-lang/rescript/pull/8694 - GenType: type inline-record cases of untagged variants as objects instead of their single field's type. https://github.com/rescript-lang/rescript/pull/8693 diff --git a/rewatch/src/watcher.rs b/rewatch/src/watcher.rs index 2d6838bcfba..d4f5470e399 100644 --- a/rewatch/src/watcher.rs +++ b/rewatch/src/watcher.rs @@ -165,11 +165,27 @@ fn register_watches(watcher: &mut RecommendedWatcher, watch_paths: &[(PathBuf, R } } -/// Unregisters all watch paths from the given watcher. -fn unregister_watches(watcher: &mut RecommendedWatcher, watch_paths: &[(PathBuf, RecursiveMode)]) { - for (path, _) in watch_paths { - let _ = watcher.unwatch(path); +/// Brings the registered watches from `current` to `next`, touching only the paths that changed. +/// +/// Some backends restart their event stream on every `watch`/`unwatch` call (FSEvents on macOS +/// does), and events that happen during the restart are lost. Leaving unchanged paths alone +/// avoids those gaps entirely in the common case where a full rebuild keeps the same paths. +fn update_watches( + watcher: &mut RecommendedWatcher, + current: &[(PathBuf, RecursiveMode)], + next: &[(PathBuf, RecursiveMode)], +) { + for entry @ (path, _) in current { + if !next.contains(entry) { + let _ = watcher.unwatch(path); + } } + let added: Vec<_> = next + .iter() + .filter(|entry| !current.contains(entry)) + .cloned() + .collect(); + register_watches(watcher, &added); } fn carry_forward_compile_state(previous: &BuildCommandState, next: &mut BuildCommandState) { @@ -512,9 +528,9 @@ async fn async_watch( build_state = next_build_state; // Re-register watches based on the new build state - unregister_watches(watcher, ¤t_watch_paths); - current_watch_paths = compute_watch_paths(&build_state, path); - register_watches(watcher, ¤t_watch_paths); + let next_watch_paths = compute_watch_paths(&build_state, path); + update_watches(watcher, ¤t_watch_paths, &next_watch_paths); + current_watch_paths = next_watch_paths; let result = build::incremental_build_without_lock( &mut build_state, @@ -547,6 +563,18 @@ async fn async_watch( } } needs_compile_type = CompileType::None; + + // If watches were re-registered, the removal event of watch.lock may have been + // dropped while the backend restarted its event stream, so check the file itself. + if !path.join("lib").join(LockKind::Watch.file_name()).exists() { + cleanup_before_watch_exit( + path, + &build_state, + show_progress, + "\nExiting... (lockfile removed)", + ); + return Ok(()); + } } CompileType::None => { // We want to sleep for a little while so the CPU can schedule other work. That way we end From 09726524402be793ee595a096808d088875e5ef9 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Tue, 6 Oct 2026 19:09:15 +0000 Subject: [PATCH 2/6] Add PR link to CHANGELOG Co-Authored-By: Claude Opus 5.5 Signed-off-by: Christoph Knittel --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59890b7c1f0..5426a2438cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,7 @@ - Fix the formatter dropping a `@res.ternary` attribute written in source, e.g. `let b = @res.ternary x`, and print `@JSX` and `@res.ternary` with the same parens as other attributes, e.g. `(@JSX x) + 1`. https://github.com/rescript-lang/rescript/pull/8740 - Respect `@warning` attributes on the module in `module type of`, e.g. `module type of @warning("-3") DeprecatedModule`, which still reported the warning. https://github.com/rescript-lang/rescript/pull/8738 - Fix the formatter dropping or moving attributes and `await` on module expressions, such as `module M = @attr F(X)`, and printing invalid code for module constraints and functors in some positions, such as `module M: T = (X: S)`. https://github.com/rescript-lang/rescript/pull/8735 -- Fix `rescript watch` sometimes not exiting on macOS when `lib/watch.lock` is removed during a full rebuild, and stop re-registering unchanged watch paths on full rebuilds. https://github.com/rescript-lang/rescript/pull/PLACEHOLDER +- Fix `rescript watch` sometimes not exiting on macOS when `lib/watch.lock` is removed during a full rebuild, and stop re-registering unchanged watch paths on full rebuilds. https://github.com/rescript-lang/rescript/pull/8730 - Make rewatch compile independent modules after an unrelated failure and recompile blocked dependents when a changed interface survives a failed implementation, including across full watcher rebuilds. https://github.com/rescript-lang/rescript/pull/8667 - Fix `RescriptTools.binaryPath` failing with `ERR_PACKAGE_IMPORT_NOT_DEFINED` when used from a user project. https://github.com/rescript-lang/rescript/pull/8694 - GenType: type inline-record cases of untagged variants as objects instead of their single field's type. https://github.com/rescript-lang/rescript/pull/8693 From b1fb679d9b761dcba54b9e3d3bbf44ef8f2b055c Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 7 Oct 2026 06:49:13 +0000 Subject: [PATCH 3/6] Retry watch paths that failed to register Only record successfully registered watch paths, so that a path whose registration failed is retried on the next full rebuild instead of being treated as already watched. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Christoph Knittel --- rewatch/src/watcher.rs | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/rewatch/src/watcher.rs b/rewatch/src/watcher.rs index d4f5470e399..06297bc270a 100644 --- a/rewatch/src/watcher.rs +++ b/rewatch/src/watcher.rs @@ -150,8 +150,13 @@ fn compute_watch_paths(build_state: &BuildCommandState, root: &Path) -> Vec<(Pat watch_paths.into_iter().collect() } -/// Registers all watch paths with the given watcher. -fn register_watches(watcher: &mut RecommendedWatcher, watch_paths: &[(PathBuf, RecursiveMode)]) { +/// Registers the watch paths with the given watcher and returns the ones that were registered +/// successfully. Failed paths are left out so that the next full rebuild retries them. +fn register_watches( + watcher: &mut RecommendedWatcher, + watch_paths: &[(PathBuf, RecursiveMode)], +) -> WatchPaths { + let mut registered = Vec::with_capacity(watch_paths.len()); for (path, mode) in watch_paths { let mode_str = if *mode == RecursiveMode::Recursive { "recursive" @@ -159,10 +164,12 @@ fn register_watches(watcher: &mut RecommendedWatcher, watch_paths: &[(PathBuf, R "non-recursive" }; log::debug!(" watching ({mode_str}): {}", path.display()); - if let Err(e) = watcher.watch(path, *mode) { - log::error!("Could not watch {}: {}", path.display(), e); + match watcher.watch(path, *mode) { + Ok(()) => registered.push((path.clone(), *mode)), + Err(e) => log::error!("Could not watch {}: {}", path.display(), e), } } + registered } /// Brings the registered watches from `current` to `next`, touching only the paths that changed. @@ -170,22 +177,22 @@ fn register_watches(watcher: &mut RecommendedWatcher, watch_paths: &[(PathBuf, R /// Some backends restart their event stream on every `watch`/`unwatch` call (FSEvents on macOS /// does), and events that happen during the restart are lost. Leaving unchanged paths alone /// avoids those gaps entirely in the common case where a full rebuild keeps the same paths. +/// +/// Returns the watches that are registered afterwards, which excludes paths that failed to register. fn update_watches( watcher: &mut RecommendedWatcher, current: &[(PathBuf, RecursiveMode)], next: &[(PathBuf, RecursiveMode)], -) { +) -> WatchPaths { for entry @ (path, _) in current { if !next.contains(entry) { let _ = watcher.unwatch(path); } } - let added: Vec<_> = next - .iter() - .filter(|entry| !current.contains(entry)) - .cloned() - .collect(); - register_watches(watcher, &added); + let (mut registered, added): (WatchPaths, WatchPaths) = + next.iter().cloned().partition(|entry| current.contains(entry)); + registered.extend(register_watches(watcher, &added)); + registered } fn carry_forward_compile_state(previous: &BuildCommandState, next: &mut BuildCommandState) { @@ -529,8 +536,7 @@ async fn async_watch( // Re-register watches based on the new build state let next_watch_paths = compute_watch_paths(&build_state, path); - update_watches(watcher, ¤t_watch_paths, &next_watch_paths); - current_watch_paths = next_watch_paths; + current_watch_paths = update_watches(watcher, ¤t_watch_paths, &next_watch_paths); let result = build::incremental_build_without_lock( &mut build_state, @@ -633,8 +639,8 @@ pub fn start( .with_context(|| "Could not initialize build")?; // Compute and register targeted watches based on source folders. - let current_watch_paths = compute_watch_paths(&build_state, path); - register_watches(&mut watcher, ¤t_watch_paths); + let current_watch_paths = + register_watches(&mut watcher, &compute_watch_paths(&build_state, path)); let timing_total = Instant::now(); let initial_compile_result = build::incremental_build_without_lock( From 8e5cdc314fd24e5e6ab2da7b5708bdf8159542ab Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 7 Oct 2026 07:16:17 +0000 Subject: [PATCH 4/6] Re-register watches of source dirs replaced at the same path inotify ties a watch to the directory rather than its path, so a source directory that is deleted and recreated at the same path is no longer watched. Record each watched directory's identity (device and inode) and re-register watches whose directory changed on the next full rebuild. Platforms without stable file identities always re-register. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Christoph Knittel --- rewatch/src/watcher.rs | 97 +++++++++++++++---- rewatch/tests/suite.sh | 1 + .../watch/08-watch-replaced-source-dir.sh | 88 +++++++++++++++++ 3 files changed, 165 insertions(+), 21 deletions(-) create mode 100755 rewatch/tests/watch/08-watch-replaced-source-dir.sh diff --git a/rewatch/src/watcher.rs b/rewatch/src/watcher.rs index 06297bc270a..58e1342e475 100644 --- a/rewatch/src/watcher.rs +++ b/rewatch/src/watcher.rs @@ -28,7 +28,7 @@ enum CompileType { type WatchPaths = Vec<(PathBuf, RecursiveMode)>; type StartupBuildResult = ( BuildCommandState, - WatchPaths, + Vec, Option<(Instant, build::CompilationOutcome)>, ); @@ -150,12 +150,49 @@ fn compute_watch_paths(build_state: &BuildCommandState, root: &Path) -> Vec<(Pat watch_paths.into_iter().collect() } +/// Identity of a watched directory, used to notice when it was replaced at the same path. +type DirIdentity = (u64, u64); + +#[cfg(unix)] +fn dir_identity(path: &Path) -> Option { + use std::os::unix::fs::MetadataExt; + path.metadata() + .ok() + .map(|metadata| (metadata.dev(), metadata.ino())) +} + +// Stable file identities are not available on other platforms, so their watches are always +// re-registered on full rebuilds. +#[cfg(not(unix))] +fn dir_identity(_path: &Path) -> Option { + None +} + +/// A watch that was registered successfully. +struct RegisteredWatch { + path: PathBuf, + mode: RecursiveMode, + /// The identity of the directory when the watch was registered. Watches can be tied to the + /// directory rather than its path (inotify drops the watch when the directory is deleted), so + /// a watch is only still valid while the path refers to the same directory. + identity: Option, +} + +impl RegisteredWatch { + fn is_still_valid_for(&self, path: &Path, mode: RecursiveMode) -> bool { + self.path == path + && self.mode == mode + && self.identity.is_some() + && self.identity == dir_identity(path) + } +} + /// Registers the watch paths with the given watcher and returns the ones that were registered /// successfully. Failed paths are left out so that the next full rebuild retries them. fn register_watches( watcher: &mut RecommendedWatcher, watch_paths: &[(PathBuf, RecursiveMode)], -) -> WatchPaths { +) -> Vec { let mut registered = Vec::with_capacity(watch_paths.len()); for (path, mode) in watch_paths { let mode_str = if *mode == RecursiveMode::Recursive { @@ -164,8 +201,15 @@ fn register_watches( "non-recursive" }; log::debug!(" watching ({mode_str}): {}", path.display()); + // Read the identity before registering: if the directory is replaced in between, the + // recorded identity is stale and the next full rebuild registers the watch again. + let identity = dir_identity(path); match watcher.watch(path, *mode) { - Ok(()) => registered.push((path.clone(), *mode)), + Ok(()) => registered.push(RegisteredWatch { + path: path.clone(), + mode: *mode, + identity, + }), Err(e) => log::error!("Could not watch {}: {}", path.display(), e), } } @@ -175,24 +219,34 @@ fn register_watches( /// Brings the registered watches from `current` to `next`, touching only the paths that changed. /// /// Some backends restart their event stream on every `watch`/`unwatch` call (FSEvents on macOS -/// does), and events that happen during the restart are lost. Leaving unchanged paths alone +/// does), and events that happen during the restart are lost. Leaving unchanged watches alone /// avoids those gaps entirely in the common case where a full rebuild keeps the same paths. +/// Watches whose directory was replaced since registration are registered again. /// /// Returns the watches that are registered afterwards, which excludes paths that failed to register. fn update_watches( watcher: &mut RecommendedWatcher, - current: &[(PathBuf, RecursiveMode)], + current: Vec, next: &[(PathBuf, RecursiveMode)], -) -> WatchPaths { - for entry @ (path, _) in current { - if !next.contains(entry) { - let _ = watcher.unwatch(path); - } +) -> Vec { + let (mut kept, stale): (Vec<_>, Vec<_>) = current.into_iter().partition(|watch| { + next.iter() + .any(|(path, mode)| watch.is_still_valid_for(path, *mode)) + }); + for watch in stale { + let _ = watcher.unwatch(&watch.path); } - let (mut registered, added): (WatchPaths, WatchPaths) = - next.iter().cloned().partition(|entry| current.contains(entry)); - registered.extend(register_watches(watcher, &added)); - registered + let added: WatchPaths = next + .iter() + .filter(|(path, mode)| { + !kept + .iter() + .any(|watch| watch.path == *path && watch.mode == *mode) + }) + .cloned() + .collect(); + kept.extend(register_watches(watcher, &added)); + kept } fn carry_forward_compile_state(previous: &BuildCommandState, next: &mut BuildCommandState) { @@ -273,7 +327,7 @@ fn cleanup_before_watch_exit( struct AsyncWatchArgs<'a> { watcher: &'a mut RecommendedWatcher, - current_watch_paths: Vec<(PathBuf, RecursiveMode)>, + current_watches: Vec, initial_build_state: BuildCommandState, q: Arc>>, ctrlc_pressed: Arc, @@ -291,7 +345,7 @@ struct AsyncWatchArgs<'a> { async fn async_watch( AsyncWatchArgs { watcher, - mut current_watch_paths, + mut current_watches, initial_build_state, q, ctrlc_pressed, @@ -536,7 +590,8 @@ async fn async_watch( // Re-register watches based on the new build state let next_watch_paths = compute_watch_paths(&build_state, path); - current_watch_paths = update_watches(watcher, ¤t_watch_paths, &next_watch_paths); + current_watches = + update_watches(watcher, std::mem::take(&mut current_watches), &next_watch_paths); let result = build::incremental_build_without_lock( &mut build_state, @@ -623,7 +678,7 @@ pub fn start( // Initialization can clean previous build artifacts, so it has to be serialized // with the initial compile too. - let (build_state, current_watch_paths, initial_compile_result): StartupBuildResult = + let (build_state, current_watches, initial_compile_result): StartupBuildResult = build::with_build_lock(path, || { let mut build_state = build::initialize_build( None, @@ -639,7 +694,7 @@ pub fn start( .with_context(|| "Could not initialize build")?; // Compute and register targeted watches based on source folders. - let current_watch_paths = + let current_watches = register_watches(&mut watcher, &compute_watch_paths(&build_state, path)); let timing_total = Instant::now(); @@ -657,7 +712,7 @@ pub fn start( Ok::(( build_state, - current_watch_paths, + current_watches, initial_compile_result, )) })?; @@ -682,7 +737,7 @@ pub fn start( async_watch(AsyncWatchArgs { watcher: &mut watcher, - current_watch_paths, + current_watches, initial_build_state: build_state, q: consumer, ctrlc_pressed, diff --git a/rewatch/tests/suite.sh b/rewatch/tests/suite.sh index 3209eae41d8..5c8b64b819e 100755 --- a/rewatch/tests/suite.sh +++ b/rewatch/tests/suite.sh @@ -145,6 +145,7 @@ fi ./watch/05-watch-ignores-non-source.sh && ./watch/06-watch-missing-source-folder.sh && ./watch/07-changed-interface-after-failed-implementation.sh && +./watch/08-watch-replaced-source-dir.sh && # Lock tests ./lock/01-lock-when-watching.sh && diff --git a/rewatch/tests/watch/08-watch-replaced-source-dir.sh b/rewatch/tests/watch/08-watch-replaced-source-dir.sh new file mode 100755 index 00000000000..f423a3bfdf4 --- /dev/null +++ b/rewatch/tests/watch/08-watch-replaced-source-dir.sh @@ -0,0 +1,88 @@ +#!/bin/bash +cd $(dirname $0) +source "../utils.sh" +cd ../../testrepo + +bold "Test: Watcher keeps watching a source dir that was replaced at the same path" + +error_output=$(rewatch clean 2>&1) +if [ $? -eq 0 ]; +then + success "Repo Cleaned" +else + error "Error Cleaning Repo" + printf "%s\n" "$error_output" >&2 + exit 1 +fi + +rewatch_bg watch > rewatch.log 2>&1 & +success "Watcher Started" + +if ! wait_for_file "./src/Test.mjs" 20 || ! wait_for_pattern_count rewatch.log "Finished .*compilation" 1 30; then + error "Initial build did not complete" + cat rewatch.log + exit_watcher + exit 1 +fi +success "Initial build completed" + +cleanup() { + rm -rf ./src-replaced + git restore --worktree -- ./src/Test.res +} + +# Replace the source dir with a copy. Some backends (inotify) tie a watch to the +# directory rather than to its path, so the new directory is not watched yet. +mv ./src ./src-replaced +cp -Rp ./src-replaced ./src +rm -rf ./src-replaced + +# Trigger a full rebuild, which has to register a watch for the new directory. +# Any build counted below starts after the replacement, so it sees the new one. +completed_builds=$(grep -c "Finished .*compilation" rewatch.log 2>/dev/null || true) +touch rescript.json +if ! wait_for_pattern_count rewatch.log "Finished .*compilation" $((completed_builds + 1)) 30; then + error "Full rebuild after touching rescript.json did not complete" + cat rewatch.log + cleanup + exit_watcher + exit 1 +fi + +completed_builds=$(grep -c "Finished .*compilation" rewatch.log 2>/dev/null || true) +echo 'Console.log("replaced-dir-probe")' >> ./src/Test.res + +if wait_for_pattern_count ./src/Test.mjs "replaced-dir-probe" 1 20 \ + && wait_for_pattern_count rewatch.log "Finished .*compilation" $((completed_builds + 1)) 30; then + success "Change in replaced source dir was compiled" +else + error "Change in replaced source dir was not compiled" + cat rewatch.log + cleanup + exit_watcher + exit 1 +fi + +# Wait for the restored source to be compiled before stopping the watcher. +cleanup +timeout=$(platform_timeout 20) +while grep -q "replaced-dir-probe" ./src/Test.mjs && [ "$timeout" -gt 0 ]; do + sleep 1 + timeout=$((timeout - 1)) +done + +if ! exit_watcher; then + exit 1 +fi + +rm -f rewatch.log + +if git diff --exit-code . > /dev/null 2>&1 && [ -z "$(git ls-files --others --exclude-standard .)" ]; +then + success "No leftover changes" +else + error "Leftover changes detected" + git diff . + git ls-files --others --exclude-standard . + exit 1 +fi From e316a26bd9ba1a90f742f45444949dd6d95d14ac Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 7 Oct 2026 09:13:33 +0000 Subject: [PATCH 5/6] Replace replaced-source-dir watch test with a unit test The integration test only exercised the identity check on Linux while adding another timing-sensitive watch test on every platform. Test the identity check directly instead. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Christoph Knittel --- rewatch/src/watcher.rs | 22 +++++ rewatch/tests/suite.sh | 1 - .../watch/08-watch-replaced-source-dir.sh | 88 ------------------- 3 files changed, 22 insertions(+), 89 deletions(-) delete mode 100755 rewatch/tests/watch/08-watch-replaced-source-dir.sh diff --git a/rewatch/src/watcher.rs b/rewatch/src/watcher.rs index 58e1342e475..14a9196a7f5 100644 --- a/rewatch/src/watcher.rs +++ b/rewatch/src/watcher.rs @@ -977,4 +977,26 @@ mod tests { carry_forward_compile_state(&previous, &mut different_source); assert!(!different_source.get_module("ModuleA").unwrap().compile_dirty); } + + #[cfg(unix)] + #[test] + fn registered_watch_is_invalid_after_directory_is_replaced() { + let temp_dir = tempfile::TempDir::new().expect("temp dir should be created"); + let dir = temp_dir.path().join("src"); + std::fs::create_dir(&dir).expect("dir should be created"); + + let watch = RegisteredWatch { + path: dir.clone(), + mode: RecursiveMode::Recursive, + identity: dir_identity(&dir), + }; + assert!(watch.is_still_valid_for(&dir, RecursiveMode::Recursive)); + assert!(!watch.is_still_valid_for(&dir, RecursiveMode::NonRecursive)); + + // Move the old directory away instead of deleting it, so that its inode stays in use and + // cannot be reused for the new directory. + std::fs::rename(&dir, temp_dir.path().join("src-old")).expect("dir should be renamed"); + std::fs::create_dir(&dir).expect("dir should be recreated"); + assert!(!watch.is_still_valid_for(&dir, RecursiveMode::Recursive)); + } } diff --git a/rewatch/tests/suite.sh b/rewatch/tests/suite.sh index 5c8b64b819e..3209eae41d8 100755 --- a/rewatch/tests/suite.sh +++ b/rewatch/tests/suite.sh @@ -145,7 +145,6 @@ fi ./watch/05-watch-ignores-non-source.sh && ./watch/06-watch-missing-source-folder.sh && ./watch/07-changed-interface-after-failed-implementation.sh && -./watch/08-watch-replaced-source-dir.sh && # Lock tests ./lock/01-lock-when-watching.sh && diff --git a/rewatch/tests/watch/08-watch-replaced-source-dir.sh b/rewatch/tests/watch/08-watch-replaced-source-dir.sh deleted file mode 100755 index f423a3bfdf4..00000000000 --- a/rewatch/tests/watch/08-watch-replaced-source-dir.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/bin/bash -cd $(dirname $0) -source "../utils.sh" -cd ../../testrepo - -bold "Test: Watcher keeps watching a source dir that was replaced at the same path" - -error_output=$(rewatch clean 2>&1) -if [ $? -eq 0 ]; -then - success "Repo Cleaned" -else - error "Error Cleaning Repo" - printf "%s\n" "$error_output" >&2 - exit 1 -fi - -rewatch_bg watch > rewatch.log 2>&1 & -success "Watcher Started" - -if ! wait_for_file "./src/Test.mjs" 20 || ! wait_for_pattern_count rewatch.log "Finished .*compilation" 1 30; then - error "Initial build did not complete" - cat rewatch.log - exit_watcher - exit 1 -fi -success "Initial build completed" - -cleanup() { - rm -rf ./src-replaced - git restore --worktree -- ./src/Test.res -} - -# Replace the source dir with a copy. Some backends (inotify) tie a watch to the -# directory rather than to its path, so the new directory is not watched yet. -mv ./src ./src-replaced -cp -Rp ./src-replaced ./src -rm -rf ./src-replaced - -# Trigger a full rebuild, which has to register a watch for the new directory. -# Any build counted below starts after the replacement, so it sees the new one. -completed_builds=$(grep -c "Finished .*compilation" rewatch.log 2>/dev/null || true) -touch rescript.json -if ! wait_for_pattern_count rewatch.log "Finished .*compilation" $((completed_builds + 1)) 30; then - error "Full rebuild after touching rescript.json did not complete" - cat rewatch.log - cleanup - exit_watcher - exit 1 -fi - -completed_builds=$(grep -c "Finished .*compilation" rewatch.log 2>/dev/null || true) -echo 'Console.log("replaced-dir-probe")' >> ./src/Test.res - -if wait_for_pattern_count ./src/Test.mjs "replaced-dir-probe" 1 20 \ - && wait_for_pattern_count rewatch.log "Finished .*compilation" $((completed_builds + 1)) 30; then - success "Change in replaced source dir was compiled" -else - error "Change in replaced source dir was not compiled" - cat rewatch.log - cleanup - exit_watcher - exit 1 -fi - -# Wait for the restored source to be compiled before stopping the watcher. -cleanup -timeout=$(platform_timeout 20) -while grep -q "replaced-dir-probe" ./src/Test.mjs && [ "$timeout" -gt 0 ]; do - sleep 1 - timeout=$((timeout - 1)) -done - -if ! exit_watcher; then - exit 1 -fi - -rm -f rewatch.log - -if git diff --exit-code . > /dev/null 2>&1 && [ -z "$(git ls-files --others --exclude-standard .)" ]; -then - success "No leftover changes" -else - error "Leftover changes detected" - git diff . - git ls-files --others --exclude-standard . - exit 1 -fi From da2b230c2adf273f8aa46b0e65641193f0e3c86f Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Fri, 9 Oct 2026 06:53:09 +0000 Subject: [PATCH 6/6] Clarify watch.lock check comment Co-Authored-By: Claude Opus 5.5 Signed-off-by: Christoph Knittel --- rewatch/src/watcher.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rewatch/src/watcher.rs b/rewatch/src/watcher.rs index 14a9196a7f5..9984d6908b4 100644 --- a/rewatch/src/watcher.rs +++ b/rewatch/src/watcher.rs @@ -625,8 +625,8 @@ async fn async_watch( } needs_compile_type = CompileType::None; - // If watches were re-registered, the removal event of watch.lock may have been - // dropped while the backend restarted its event stream, so check the file itself. + // The removal event of watch.lock can be lost while watches are re-registered + // (some backends restart their event stream), so check the file itself. if !path.join("lib").join(LockKind::Watch.file_name()).exists() { cleanup_before_watch_exit( path,