Skip to content

Commit 92c9975

Browse files
Gusclaude
andcommitted
Skip unnecessary source database refresh for unchanged files
When redo-ifchange encounters a source file from within a .do file, it previously called initializeSourceDatabase unconditionally — even if the file hadn't changed. That function deletes and recreates the entire database directory, which opens a corruption window: if the process is killed (e.g. Ctrl+C triggering SIGKILL via the process group handler) between the delete and the markSource write, the database is left without a source marker. This causes permanent "No rule to build" errors in projects with a catch-all default.do (like Adamant). Now we compare the current file stamp against the cached stamp first. If they match, we skip the refresh entirely — the database is already in the correct state. This eliminates the corruption window for the vast majority of source files on incremental builds (only files that actually changed need the refresh). Includes tests verifying: - Unchanged sources skip DB refresh (inode stability check) - Changed sources still trigger refresh and dependent rebuilds - New sources get properly initialized Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent 396e2d7 commit 92c9975

5 files changed

Lines changed: 120 additions & 2 deletions

File tree

src/Build.hs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,24 @@ redoIfChange = buildTargets redoIfChange'
120120
runFromDo <- isRunFromDoFile
121121
case (source, runFromDo) of
122122
(True, False) -> targetSourceWarning target
123-
(True, True) -> do initializeSourceDatabase key target
124-
return ExitSuccess
123+
-- Source file encountered from within a .do file. We need to ensure
124+
-- the database is marked as source with a current stamp. However,
125+
-- if the stamp hasn't changed since last time, we can skip the
126+
-- expensive initializeSourceDatabase call (which deletes and recreates
127+
-- the entire database directory). This is important because that
128+
-- delete-recreate cycle opens a corruption window: if the process is
129+
-- killed (e.g. Ctrl+C -> SIGKILL) between the delete and the
130+
-- markSource write, the database is left without a source marker,
131+
-- causing permanent "No rule to build" errors for the file.
132+
-- By skipping unchanged sources, we eliminate this window for the
133+
-- vast majority of source files on incremental builds.
134+
(True, True) -> do
135+
currentStamp <- safeStampTarget target
136+
cachedStamp <- getStamp key
137+
if currentStamp == cachedStamp
138+
then return ExitSuccess
139+
else do initializeSourceDatabase key target
140+
return ExitSuccess
125141
(False, _) -> do
126142
currentStamp <- safeStampTarget target
127143
modified <- isTargetModified key currentStamp

test/375-default-do-source/all.do

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
redo clean
2+
sh run_tests.sh
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rm -rf build src *.log
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Catch-all default.do that mimics a typical project structure:
2+
# NOTE: no shebang — redo adds "sh -e" automatically, so errors propagate.
3+
# - Handles targets in build/ directory by depending on corresponding source
4+
# - Handles "clean" and "all" redo targets
5+
# - Errors on anything else (source files should never reach here)
6+
7+
case "$1" in
8+
build/*)
9+
# Build targets: depend on the corresponding source file
10+
BASENAME=$(basename "$1")
11+
redo-ifchange "src/$BASENAME"
12+
echo "built from: $(cat "src/$BASENAME")" > "$3"
13+
;;
14+
clean)
15+
rm -rf build src *.log
16+
;;
17+
all)
18+
# Run the test script
19+
sh run_tests.sh
20+
;;
21+
*)
22+
echo "default.do: No rule to build '$1'." >&2
23+
exit 1
24+
;;
25+
esac
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/bin/sh
2+
# Test for source database refresh optimization.
3+
#
4+
# This test directory has a catch-all default.do that handles build/*
5+
# targets and errors on everything else. The test verifies that
6+
# initializeSourceDatabase is skipped for unchanged source files
7+
# (the DB directory is not deleted and recreated unnecessarily),
8+
# while still refreshing when source files actually change.
9+
10+
set -e
11+
12+
##############################################################################
13+
# Helpers
14+
##############################################################################
15+
get_db_dir() {
16+
DB_KEY=$(printf '%s' "$1" | md5sum | awk '{print toupper($1)}')
17+
echo "$HOME/.redo/database/$(echo $DB_KEY | cut -c1-3)/$(echo $DB_KEY | cut -c4-9)/$(echo $DB_KEY | cut -c10-21)/$(echo $DB_KEY | cut -c22-)"
18+
}
19+
get_stamp_dir() {
20+
DB_KEY=$(printf '%s' "$1" | md5sum | awk '{print toupper($1)}')
21+
echo "$HOME/.redo/stamps/$(echo $DB_KEY | cut -c1-3)/$(echo $DB_KEY | cut -c4-9)/$(echo $DB_KEY | cut -c10-21)/$(echo $DB_KEY | cut -c22-)"
22+
}
23+
24+
##############################################################################
25+
# Setup
26+
##############################################################################
27+
mkdir -p src build
28+
SRC_PATH="$(cd src && pwd)/data.txt"
29+
BUILD_PATH="$(cd build && pwd)/data.txt"
30+
rm -rf "$(get_db_dir "$SRC_PATH")" "$(get_stamp_dir "$SRC_PATH")"
31+
rm -rf "$(get_db_dir "$BUILD_PATH")" "$(get_stamp_dir "$BUILD_PATH")"
32+
../flush-cache
33+
34+
echo "content v1" > src/data.txt
35+
36+
##############################################################################
37+
# Initial build — creates source DB with stamp
38+
##############################################################################
39+
redo-ifchange build/data.txt
40+
test "$(cat build/data.txt)" = "built from: content v1" || exit 1
41+
42+
SRC_DB=$(get_db_dir "$SRC_PATH")
43+
test -d "$SRC_DB/y" || exit 2
44+
45+
# Record the DB directory inode to detect if it gets recreated
46+
INODE_BEFORE=$(stat -c %i "$SRC_DB" 2>/dev/null || stat -f %i "$SRC_DB" 2>/dev/null)
47+
48+
##############################################################################
49+
# Rebuild with no changes — DB should NOT be recreated
50+
##############################################################################
51+
../flush-cache
52+
redo-ifchange build/data.txt
53+
54+
test -d "$SRC_DB/y" || exit 3
55+
56+
INODE_AFTER=$(stat -c %i "$SRC_DB" 2>/dev/null || stat -f %i "$SRC_DB" 2>/dev/null)
57+
if [ "$INODE_BEFORE" != "$INODE_AFTER" ]; then
58+
echo "FAIL: source DB was recreated on unchanged rebuild (inode $INODE_BEFORE -> $INODE_AFTER)" >&2
59+
exit 4
60+
fi
61+
62+
##############################################################################
63+
# Modify source — DB should refresh and target should rebuild
64+
##############################################################################
65+
../sleep
66+
echo "content v2" > src/data.txt
67+
../flush-cache
68+
69+
redo-ifchange build/data.txt
70+
test "$(cat build/data.txt)" = "built from: content v2" || exit 5
71+
test -d "$SRC_DB/y" || exit 6
72+
73+
echo "PASS: source DB skip optimization" >&2
74+
rm -rf src build

0 commit comments

Comments
 (0)