diff --git a/AGENTS.md b/AGENTS.md
index 424b38c..309d45b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -19,7 +19,9 @@ R CMD INSTALL .
```r
# Run tests (in the R console)
devtools::test()
-# or without devtools:
+# or without devtools (package must be installed and attached;
+# NOT_CRAN=true also runs the tests that skip on aarch64):
+library(watcher)
testthat::test_dir("tests/testthat")
```
@@ -69,6 +71,9 @@ The package uses GitHub Actions workflows in `.github/workflows/`:
**Callback Mechanism:**
- File events trigger `process_events()` callback in C
- Events are bundled by path and passed to R via the 'later' package's `execLaterNative2`
+- Paths are deduplicated per callback (first occurrence kept) and copied in a
+ single allocation; combined with the patched-in event bubbling (see Build
+ System), paths reported by a single callback are always unique
- R callbacks execute when R is idle or when `later::run_now()` is called
- If no callback is provided, events are printed to stdout
@@ -100,10 +105,15 @@ hand-maintained config header, not host probing.
- `src/link.cpp`: empty `.cpp` that forces R to link with the C++ linker.
- `tools/update_libfswatch.sh`: re-vendors libfswatch and regenerates the config
header, object list, and Makevars; calls `tools/patch_libfswatch.sh`.
-- `tools/patch_libfswatch.sh`: idempotent source patches — a null-format guard
- in `string_utils`, self-guards on the fsevents/inotify/fanotify monitors, and
- neutering of libfswatch's stdout/stderr/cerr logging (for the R CMD check
- "compiled code" policy).
+- `tools/patch_libfswatch.sh`: idempotent source patches — (1) empty
+ `string_utils.cpp` to an include-only TU: after the logging neutering its
+ only live references are two regex-error exception messages in `monitor.cpp`,
+ unreachable in watcher (no text filters are ever added), so those are
+ replaced with a static message; also fixes a GCC 16 `-Warray-bounds` false
+ positive. (2) Self-guards on the fsevents/inotify/fanotify monitors. (3)
+ Neutering of libfswatch's stdout/stderr/cerr logging (for the R CMD check
+ "compiled code" policy). (4) `bubble_events = true` by default (the C API
+ does not expose `set_bubble_events()`).
**Key dependencies:** an R C/C++ toolchain plus `make`; pthread; the
'later' R package. A system libfswatch is used automatically when present.
diff --git a/src/fswatch/libfswatch/src/libfswatch/c++/monitor.cpp b/src/fswatch/libfswatch/src/libfswatch/c++/monitor.cpp
index 28b75b0..7e51c35 100644
--- a/src/fswatch/libfswatch/src/libfswatch/c++/monitor.cpp
+++ b/src/fswatch/libfswatch/src/libfswatch/c++/monitor.cpp
@@ -127,9 +127,7 @@ namespace fsw
catch (const std::regex_error& error)
{
throw libfsw_exception(
- string_utils::string_from_format(
- _("An error occurred during the compilation of %s"),
- filter.text.c_str()),
+ _("An error occurred during the compilation of a filter regex"),
FSW_ERR_INVALID_REGEX);
}
}
@@ -148,9 +146,7 @@ namespace fsw
catch (const std::regex_error& error)
{
throw libfsw_exception(
- string_utils::string_from_format(
- _("An error occurred during the compilation of %s"),
- filter.text.c_str()),
+ _("An error occurred during the compilation of a filter regex"),
FSW_ERR_INVALID_REGEX);
}
}
diff --git a/src/fswatch/libfswatch/src/libfswatch/c++/string/string_utils.cpp b/src/fswatch/libfswatch/src/libfswatch/c++/string/string_utils.cpp
index 343267a..64d11d5 100644
--- a/src/fswatch/libfswatch/src/libfswatch/c++/string/string_utils.cpp
+++ b/src/fswatch/libfswatch/src/libfswatch/c++/string/string_utils.cpp
@@ -13,54 +13,8 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see .
*/
-#include
-#include
-#include
+/* watcher: bodies removed as dead code. Logging is neutered, and the only
+ * other callers (regex-error paths in monitor.cpp) are unreachable in watcher.
+ * Also sidesteps a GCC 16 -Warray-bounds false positive.
+ * See tools/patch_libfswatch.sh. */
#include "string_utils.hpp"
-
-using namespace std;
-
-namespace fsw
-{
- namespace string_utils
- {
- string vstring_from_format(const char *format, va_list args)
- {
- if (!format) return string();
- size_t current_buffer_size = 0;
- int required_chars = 512;
-
- vector buffer;
-
- do
- {
- current_buffer_size += required_chars;
- buffer.resize(current_buffer_size);
- required_chars = vsnprintf(&buffer[0], current_buffer_size, format, args);
-
- // If an encoding error occurs, break and write an empty string into the
- // buffer.
- if (required_chars < 0)
- {
- buffer.resize(1);
- break;
- }
- }
- while ((size_t) required_chars > current_buffer_size);
-
- return string(&buffer[0]);
- }
-
- string string_from_format(const char *format, ...)
- {
- va_list args;
- va_start(args, format);
-
- string ret = vstring_from_format(format, args);
-
- va_end(args);
-
- return ret;
- }
- }
-}
\ No newline at end of file
diff --git a/tools/patch_libfswatch.sh b/tools/patch_libfswatch.sh
index 71324ea..0b46dc0 100755
--- a/tools/patch_libfswatch.sh
+++ b/tools/patch_libfswatch.sh
@@ -10,8 +10,14 @@
#
# Every patch is IDEMPOTENT: running this on already-patched sources is a no-op.
# The patches are:
-# 1. string_utils.cpp - guard against a null format string (GCC
-# -Wformat-truncation in stricter CRAN/rhub builds).
+# 1. string_utils.cpp body removal - after patch 3's logging neutering,
+# string_from_format()/vstring_from_format() are referenced only by two
+# regex-error exception messages in monitor.cpp, which watcher can never
+# reach (it adds only event-flag filters, never text filters). Replace
+# those call sites with a static message (1a) and empty string_utils.cpp
+# to an include-only TU (1b). Also resolves a GCC 16 -Warray-bounds false
+# positive against the upstream vector::resize implementation, which
+# R CMD check reports as a significant warning.
# 2. Monitor self-guards - wrap fsevents/inotify/fanotify monitor bodies in
# their platform #ifdef so they compile to empty objects on the wrong OS
# (kqueue/fen/windows monitors already self-guard upstream).
@@ -59,19 +65,47 @@ add_guard() {
echo -e "${GREEN} ✓ self-guard $macro applied${NC}"
}
-# --- Patch 1: string_utils.cpp null-format guard -----------------------------
-STRING_UTILS="$LIBFSW_SRC/c++/string/string_utils.cpp"
-if [ -f "$STRING_UTILS" ]; then
- if grep -q "if (!format) return" "$STRING_UTILS"; then
- echo -e "${GREEN} ✓ string_utils null-format guard already present${NC}"
- else
- sed -i.bak 's|^ size_t current_buffer_size = 0;$| if (!format) return string();\
- size_t current_buffer_size = 0;|' "$STRING_UTILS"
- rm -f "$STRING_UTILS.bak"
- echo -e "${GREEN} ✓ string_utils null-format guard applied${NC}"
+# --- Patch 1a: static regex-error messages in monitor.cpp --------------------
+# watcher never adds text filters (only event-flag filters), so the regex-error
+# exceptions in add_filter()/add_prune_filter() are unreachable. Replace their
+# string_from_format() messages with a static string so string_utils.cpp can be
+# emptied (patch 1b).
+MON_CPP="$LIBFSW_SRC/c++/monitor.cpp"
+if grep -q 'compilation of a filter regex' "$MON_CPP" 2>/dev/null; then
+ echo -e "${GREEN} ✓ monitor.cpp regex-error messages already static${NC}"
+else
+ perl -0777 -pi -e 's/ string_utils::string_from_format\(\n _\("An error occurred during the compilation of %s"\),\n filter\.text\.c_str\(\)\),/ _("An error occurred during the compilation of a filter regex"),/g' "$MON_CPP"
+ if grep -q 'filter\.text\.c_str()' "$MON_CPP"; then
+ echo "Error: failed to patch string_from_format call sites in $MON_CPP" >&2
+ exit 1
fi
+ echo -e "${GREEN} ✓ monitor.cpp regex-error messages made static${NC}"
+fi
+
+# --- Patch 1b: empty string_utils.cpp bodies (dead code) ---------------------
+# After patch 3's logging neutering and patch 1a, nothing references
+# string_from_format()/vstring_from_format(): empty the TU. The object stays in
+# the build, compiling empty like the self-guarded monitors. This also resolves
+# the GCC 16 -Warray-bounds false positive against the upstream
+# vector::resize implementation, which R CMD check reports as a
+# significant warning.
+STRING_UTILS="$LIBFSW_SRC/c++/string/string_utils.cpp"
+if grep -q 'watcher: bodies removed' "$STRING_UTILS" 2>/dev/null; then
+ echo -e "${GREEN} ✓ string_utils.cpp bodies already removed${NC}"
else
- echo -e "${YELLOW} ⚠ string_utils.cpp not found, skipping${NC}"
+ # Preserve the upstream licence header (everything before the first #include).
+ HEADER=$(awk '/^#include/{exit} {print}' "$STRING_UTILS")
+ {
+ printf '%s\n' "$HEADER"
+ cat <<'EOF'
+/* watcher: bodies removed as dead code. Logging is neutered, and the only
+ * other callers (regex-error paths in monitor.cpp) are unreachable in watcher.
+ * Also sidesteps a GCC 16 -Warray-bounds false positive.
+ * See tools/patch_libfswatch.sh. */
+#include "string_utils.hpp"
+EOF
+ } > "$STRING_UTILS.tmp" && mv "$STRING_UTILS.tmp" "$STRING_UTILS"
+ echo -e "${GREEN} ✓ string_utils.cpp bodies removed${NC}"
fi
# --- Patch 2: self-guard the three non-self-guarding monitors ----------------