Skip to content

feat(cabal-install): add --watch to common commands (GNU/Linux-only) - #12485

Open
blackheaven wants to merge 2 commits into
haskell:masterfrom
blackheaven:feat/watch
Open

blackheaven wants to merge 2 commits into
haskell:masterfrom
blackheaven:feat/watch

Conversation

@blackheaven

@blackheaven blackheaven commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

Add a --watch options to auto-launch some commands

Manual QA notes

Tested on GNU/Linux (NixOS, GHC 9.10.3):

  • cabal build --watch: initial build runs; appending to a watched source
    file triggers File ... changed, re-running... and a rebuild; a failing
    rebuild is reported and the watcher keeps running; Ctrl-C stops it.
  • cabal sdist --watch: touching p.cabal re-generates the tarball.
  • cabal check --watch: a failing re-check (introduced warning) is reported
    and the watcher keeps watching (the loop catches ExitCode as well as
    synchronous exceptions; a Ctrl-C during a re-run still terminates).
  • v1-install --watch fails immediately with
    Error: [Cabal-7170] The '--watch' flag is not supported by the 'v1-install' command.
  • --watch is accepted (but ignored) by other nix-style commands such as
    freeze/path, mirroring the --dry-run precedent; it is excluded from
    the saved user config.
  • Non-Linux is untested locally: by construction the flag is accepted and the
    command dies immediately with
    Error: [Cabal-7169] The '--watch' flag is only supported on Linux.

@blackheaven
blackheaven force-pushed the feat/watch branch 11 times, most recently from 10885da to 99dd310 Compare October 24, 2026 17:58

@Bodigrim Bodigrim left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very interesting! Are build-type: Custom and build-type: Hooks supported?

manual: True

flag fswatch
description: Enable the --watch flag (adds the fsnotify dependency, Linux only)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to hide fsnotify behind the flag? It's not a particularly huge dependency from the look at it, and it's better not to introduce new flags if we can do without them.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have introduced the flag exists purely to keep the bootstrap plans (bootstrap/linux-*.json) unchanged. Bootstrap builds must be buildable from the pinned plan alone, and adding fsnotify unconditionally pulled in hinotify (Linux-only) plus ~10 transitive deps — which broke the Bootstrap macos-15 CI job.

I can investigate, but I do not have a Macintosh to test.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how if os(linux) build-depends: fsnotify could affect build plans for macOS. Updating pinned bootstrap plans is not a big deal.


if os(linux) && flag(fswatch)
build-depends: fsnotify >= 0.4.1 && < 0.5
cpp-options: -DHAVE_FSWATCH

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to pass an additional CPP option, you can check #ifdef MIN_VERSION_fsnotify.

-- | File extensions for which a @create@ event in an already-watched source
-- directory triggers a re-run (handles newly created module files).
watchCreateExtensions :: [String]
watchCreateExtensions =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list lacks Alex and Happy file extensions. Is it possible to come up with a more principled approach, which does not require to hardcode such a list?

-- (so that newly written modules trigger a rebuild).
watchEventRelevant :: [FilePath] -> WatchEventInfo -> Bool
watchEventRelevant names (path, isCreate) =
fileName `elem` names

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine names can get rather large, at which point linear search through a list of strings with repeated prefixes will become slow.

watchEventRelevant :: [FilePath] -> WatchEventInfo -> Bool
watchEventRelevant names (path, isCreate) =
fileName `elem` names
|| (isCreate && takeExtension path `elem` watchCreateExtensions)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will a newly created file, which was not there when the --watch started, be tracked for subsequent changes? Which part of the code enables this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Watchers are per-directory, not per-file: filesToDirWatches + groupDirWatches register one watchDir per parent directory of the watched files, and inotify reports events for the whole directory.
The first event of a new file
arrives as Added and passes watchEventRelevant via the create-extension rule (Added{} → True + extension in watchCreateExtensions) — that's the part that catches the creation itself.

@blackheaven

Copy link
Copy Markdown
Collaborator Author

Very interesting! Are build-type: Custom and build-type: Hooks supported?

Yes, at the same fidelity as cabal's own change detection. planWatchFiles records files via Distribution.Client.SourceFiles.needElaboratedConfiguredPackage, the same function the incremental rebuild monitor uses, which includes needSetup (monitors Setup.hs/Setup.lhs/SetupHooks.hs/SetupHooks.lhs), so Hooks setup scripts are watched. One inherited caveat, documented in SourceFiles itself: for build-type: Custom, a custom Setup.hs may have arbitrary extra dependencies that static analysis cannot see (the module comment says exactly this), so those extra files aren't watched, but a change to Setup.hs itself or any ordinary source file is.

- Rethrow failures from the watched command verbatim once the watcher is
  started instead of mislabeling them as WatchStartFailed
- Make the debounce drain actually block for the coalescing window
  (tryReadTChan never retried, so bursts caused double rebuilds)
- Collapse an unreachable nested try in watchLoop (ExitCode is a
  SomeException); merge the now-redundant unit test
- Watch the source directory itself in the MonitorFileGlob fallback
- Document that reruns reuse the startup-time elaborated plan and that
  install --watch effectively watches only per-package files
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants