From d9d6f2208585402a02007b8e03365dae91962cf0 Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Wed, 29 Jul 2026 12:07:43 +0000 Subject: [PATCH 1/2] Support FSEvents on macOS 10.5 and 10.6 The FSEvents stream API dates back to 10.5, but fsevents.cpp used a few later additions unconditionally, so HAVE_FSEVENTS was gated on a 10.7 deployment target and older systems fell back to the kqueue watcher (which needs an open fd per file). Lower the HAVE_FSEVENTS floor to 10.5 and gate each later feature on the version that actually introduced it, so nothing is dropped on SDKs that have it: - 10.7 file-level events: define the missing kFSEventStreamCreateFlagFileEvents / kFSEventStreamEventFlagItem* constants when building against a pre-10.7 SDK (the bit values are ABI-stable and older fseventsd never emits them), and clamp hasFileWatching_ via a runtime kern.osrelease check when the deployment target predates 10.7. On older systems the watcher degrades to the existing directory-level rescan mode (dirfsevents). - 10.9 FSEventStreamSetExclusionPaths: compile the exclusions block only when the SDK has the declaration, and check the weak-linked symbol at runtime when the deployment target predates 10.9. Exclusions are an optimization, so skipping them is safe. Builds targeting 10.7+ compile to exactly the previous behavior. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015ECmTd2wkJqqrArR8VHCDm --- watchman/watcher/fsevents.cpp | 106 ++++++++++++++++++++++++++++++++-- watchman/watchman_system.h | 5 +- 2 files changed, 106 insertions(+), 5 deletions(-) diff --git a/watchman/watcher/fsevents.cpp b/watchman/watcher/fsevents.cpp index 6f9b1ee..05f51b8 100644 --- a/watchman/watcher/fsevents.cpp +++ b/watchman/watcher/fsevents.cpp @@ -30,10 +30,102 @@ #include "watchman/telemetry/LogEvent.h" #include "watchman/telemetry/WatchmanStructuredLogger.h" +#include + +#if __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ < 1070 +#include +#include +#endif + +// Support building against older SDKs (10.5/10.6), where only part of the +// current FSEvents API exists. Each feature is gated on the version that +// introduced it, independently of the others: +// +// - 10.7 added file-level events: kFSEventStreamCreateFlagFileEvents and +// the kFSEventStreamEventFlagItem* family. The bit values are ABI-stable +// and older versions of fseventsd simply never emit them, so defining the +// missing constants is safe; whether the flag may be used is a separate +// runtime question (see osSupportsFileEvents below). +// - 10.9 added FSEventStreamSetExclusionPaths. That is a function, not a +// constant, so it cannot be called at all when building against an older +// SDK. +#ifndef MAC_OS_X_VERSION_10_7 +#define kFSEventStreamCreateFlagFileEvents 0x00000010 +#define kFSEventStreamEventFlagItemCreated 0x00000100 +#define kFSEventStreamEventFlagItemRemoved 0x00000200 +#define kFSEventStreamEventFlagItemInodeMetaMod 0x00000400 +#define kFSEventStreamEventFlagItemRenamed 0x00000800 +#define kFSEventStreamEventFlagItemModified 0x00001000 +#define kFSEventStreamEventFlagItemFinderInfoMod 0x00002000 +#define kFSEventStreamEventFlagItemChangeOwner 0x00004000 +#define kFSEventStreamEventFlagItemXattrMod 0x00008000 +#define kFSEventStreamEventFlagItemIsFile 0x00010000 +#define kFSEventStreamEventFlagItemIsDir 0x00020000 +#define kFSEventStreamEventFlagItemIsSymlink 0x00040000 +#endif + +#if defined(MAC_OS_X_VERSION_10_9) && \ + MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_9 +#define HAVE_FSEVENTS_EXCLUSIONS 1 +#else +#define HAVE_FSEVENTS_EXCLUSIONS 0 +#endif + namespace watchman { namespace { +/** + * Returns true if the OS we are running on supports file-level FSEvents + * (kFSEventStreamCreateFlagFileEvents), which was introduced in 10.7. + * On older systems the watcher degrades to directory-level events. + */ +bool osSupportsFileEvents() { +#if __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ >= 1070 + return true; +#else + // The deployment target predates 10.7, so consult the running kernel: + // kern.osrelease is the Darwin version, whose major number 11 + // corresponds to OS X 10.7. + static const bool supported = [] { + char osrelease[64]; + size_t len = sizeof(osrelease); + if (sysctlbyname("kern.osrelease", osrelease, &len, nullptr, 0) != 0) { + return false; + } + if (atoi(osrelease) >= 11) { + return true; + } + logf( + DBG, + "this system (Darwin {}) predates 10.7: FSEvents will use " + "directory-level events\n", + osrelease); + return false; + }(); + return supported; +#endif +} + +#if HAVE_FSEVENTS_EXCLUSIONS +/** + * Returns true if FSEventStreamSetExclusionPaths (10.9+) is available on + * the running system. + */ +bool exclusionsAvailableAtRuntime() { +#if __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ >= 1090 + return true; +#else + // Weak-linked because the deployment target predates 10.9. + return &FSEventStreamSetExclusionPaths != nullptr; +#endif +} +#endif + +} // namespace + +namespace { + // The FSEventStreamSetExclusionPaths API has a limit of 8 items. // If that limit is exceeded, it will fail. constexpr inline size_t kMaxExclusions = 8; @@ -431,7 +523,9 @@ std::unique_ptr FSEventsWatcher::fse_stream_make( FSEventStreamScheduleWithRunLoop( fse_stream->stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); - if (root->config.getBool("_use_fsevents_exclusions", true)) { +#if HAVE_FSEVENTS_EXCLUSIONS + if (exclusionsAvailableAtRuntime() && + root->config.getBool("_use_fsevents_exclusions", true)) { auto& dirs_vec = root->ignore.getIgnoredDirs(); size_t nitems = std::min(dirs_vec.size(), kMaxExclusions); @@ -482,6 +576,7 @@ std::unique_ptr FSEventsWatcher::fse_stream_make( } } } +#endif // HAVE_FSEVENTS_EXCLUSIONS return fse_stream; } @@ -547,10 +642,13 @@ FSEventsWatcher::FSEventsWatcher( const Configuration& config, std::optional dir) : Watcher( - hasFileWatching ? "fsevents" : "dirfsevents", - hasFileWatching ? WATCHER_HAS_PER_FILE_NOTIFICATIONS : 0), + (hasFileWatching && osSupportsFileEvents()) ? "fsevents" + : "dirfsevents", + (hasFileWatching && osSupportsFileEvents()) + ? WATCHER_HAS_PER_FILE_NOTIFICATIONS + : 0), attemptResyncOnDrop_{config.getBool("fsevents_try_resync", false)}, - hasFileWatching_{hasFileWatching}, + hasFileWatching_{hasFileWatching && osSupportsFileEvents()}, enableStreamFlush_{config.getBool("fsevents_enable_stream_flush", true)}, subdir{std::move(dir)} { // TODO: Add ring buffer logging for events in the shared kqueue+fsevents diff --git a/watchman/watchman_system.h b/watchman/watchman_system.h index cd851a9..b05355b 100644 --- a/watchman/watchman_system.h +++ b/watchman/watchman_system.h @@ -149,7 +149,10 @@ char* realpath(const char* filename, char* target); #ifdef HAVE_CORESERVICES_CORESERVICES_H #include // @manual -#if __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ < 1070 +// The FSEvents stream API is available from 10.5. Later additions +// (file-level events on 10.7, exclusion paths on 10.9) are handled with +// per-feature gates in watcher/fsevents.cpp. +#if __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ < 1050 #define HAVE_FSEVENTS 0 #else #define HAVE_FSEVENTS 1 -- 2.43.0