Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/graph/dag.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,18 @@ auto collect_affected_commands(Graph const& graph, Vec<StringId> const& changed_
}
}

// InjectImplicitDeps siblings (dep-scan commands) have no graph outputs,
// so the cascade above can't reach them. They must run whenever their
// parent compile runs — otherwise newly-introduced transitive includes
// are never re-discovered. Walk all commands and attach any whose
// parent_command was just marked affected.
for (auto cmd_id : nodes_of_type(graph, NodeType::Command)) {
auto parent = get_parent_command(graph, cmd_id);
if (parent != INVALID_NODE_ID && affected.contains(parent)) {
affected.set(cmd_id, 1);
}
}

return affected;
}

Expand Down
1 change: 1 addition & 0 deletions test/e2e/fixtures/header_dep_transitive/src/Tupfile
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
include_rules
: main.c |> gcc $(CFLAGS) -c %f -o %o |> %B.o
: main.o |> gcc %f -o %o |> program
55 changes: 27 additions & 28 deletions test/e2e/fixtures/header_dep_transitive/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,45 +9,44 @@
# 3. newhdr.h is modified. pup reports "Nothing to do." because its
# stale dep set says the source only depends on old.h (unchanged).
#
# Expected: step 3 rebuilds, .o reflects the new constant.
# Observed: step 3 is a no-op; .o is stale.
# Expected: step 3 rebuilds, the linked program reflects the new constant.
# Observed: step 3 is a no-op; program is stale.
#
# Test exits 0 on success (bug fixed), non-zero on failure (bug present).
#
# Oracle: link main.c into a tiny program whose main() returns the constant.
# Running it and reading $? is portable across Linux GNU-binutils and macOS
# LLVM toolchains. Constants stay < 256 so the exit code carries them
# unmodified (POSIX masks exit codes to 8 bits).

set -u

PUP="${PUP:-$(command -v pup || echo /home/mural/bin/pup)}"
BUILD_DIR="build/x86"
OBJ="${BUILD_DIR}/src/main.o"
PROG="${BUILD_DIR}/src/program"

die() {
echo "FAIL: $*" >&2
exit 1
}

# Extract the immediate-mode operand of the first mov $0xXX instruction.
# Returns the hex literal as it appears in objdump output (e.g. "0x1").
constant_in_obj() {
objdump -d "$1" 2>/dev/null \
| grep -oE 'mov +\$0x[0-9a-f]+' \
| head -1 \
| grep -oE '0x[0-9a-f]+'
}

# Decode a hex literal (with 0x prefix) to a decimal integer for printing.
hex_to_dec() {
printf '%d' "$1" 2>/dev/null
# Run the program and report its exit code (= the constant baked into main.o).
constant_in_prog() {
"$1"
echo $?
}

# ---------- Step 1: initial build ----------
echo "=== Step 1: initial build (source includes old.h only) ==="
"$PUP" configure -B "$BUILD_DIR" >/dev/null || die "configure failed"
"$PUP" -B "$BUILD_DIR" >/dev/null || die "initial build failed"
[[ -f "$OBJ" ]] || die "expected $OBJ after initial build"
[[ -x "$PROG" ]] || die "expected $PROG after initial build"

K1="$(constant_in_obj "$OBJ")"
echo " observed constant in $OBJ: $K1 ($(hex_to_dec "$K1"))"
[[ "$K1" == "0x1" ]] || die "expected 0x1 (ANSWER=1) after step 1, got $K1"
K1="$("$PROG"; echo $?)"
echo " $PROG exits with: $K1"
[[ "$K1" == "1" ]] || die "expected exit 1 (ANSWER=1) after step 1, got $K1"

# ---------- Step 2: make old.h transitively include newhdr.h ----------
echo
Expand All @@ -60,10 +59,10 @@ EOF

"$PUP" -B "$BUILD_DIR" >/dev/null || die "rebuild after old.h edit failed"

K2="$(constant_in_obj "$OBJ")"
echo " observed constant in $OBJ: $K2 ($(hex_to_dec "$K2"))"
[[ "$K2" == "0x64" ]] \
|| die "expected 0x64 (EXTRA=100) after step 2, got $K2 — old.h change not picked up"
K2="$("$PROG"; echo $?)"
echo " $PROG exits with: $K2"
[[ "$K2" == "100" ]] \
|| die "expected exit 100 (EXTRA=100) after step 2, got $K2 — old.h change not picked up"

# ---------- Forensic between step 2 and 3: did pup record newhdr.h? ----------
# After step 2 the .d file from gcc lists both old.h AND newhdr.h. If pup's
Expand All @@ -87,19 +86,19 @@ fi

# ---------- Step 3: modify newhdr.h (the transitive header) ----------
echo
echo "=== Step 3: modify newhdr.h (EXTRA 100 -> 7777) ==="
echo "=== Step 3: modify newhdr.h (EXTRA 100 -> 77) ==="
cat > include/newhdr.h << 'EOF'
#pragma once
#define EXTRA 7777
#define EXTRA 77
EOF

OUT="$("$PUP" -B "$BUILD_DIR" 2>&1)"
EC=$?
echo " pup output: $OUT"
[[ $EC -eq 0 ]] || die "rebuild returned non-zero: $EC"

K3="$(constant_in_obj "$OBJ")"
echo " observed constant in $OBJ: $K3 ($(hex_to_dec "$K3"))"
K3="$("$PROG"; echo $?)"
echo " $PROG exits with: $K3"

if [[ "$OUT" == *"Nothing to do"* ]]; then
echo
Expand All @@ -109,11 +108,11 @@ if [[ "$OUT" == *"Nothing to do"* ]]; then
echo " never recorded — confirmed by 'pup show index' above."
fi

if [[ "$K3" != "0x1e61" ]]; then
if [[ "$K3" != "77" ]]; then
echo
echo "Diagnostic:"
echo " expected $OBJ to contain 0x1e61 (EXTRA=7777)"
echo " actually contains $K3 ($(hex_to_dec "$K3"))"
echo " expected $PROG to exit 77 (EXTRA=77)"
echo " actually exits $K3"
echo " this means main.c was NOT recompiled after newhdr.h changed"
die "transitive header change was not detected"
fi
Expand Down
16 changes: 6 additions & 10 deletions test/unit/test_e2e.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2105,13 +2105,11 @@ SCENARIO("Pupignore test via shell fixture", "[e2e][shell]")
}
}

// Reproducer: when a source's already-tracked header is edited to transitively
// include a new header, pup recompiles the source but fails to record the new
// transitive header. Subsequent edits to that new header are then invisible to
// change detection, and pup reports "Nothing to do" while the .o is stale.
// See test/e2e/fixtures/header_dep_transitive/ and the parent project's
// notes/pup-header-detection-bug.md.
SCENARIO("Transitive implicit-dep header tracking", "[e2e][shell][incremental][!shouldfail]")
// Regression guard: when a source's already-tracked header is edited to
// transitively include a new header, pup must record the new transitive
// header and rebuild on subsequent edits to it. Fixed by binding dep-scan
// command dirty-status to its parent compile (collect_affected_commands).
SCENARIO("Transitive implicit-dep header tracking", "[e2e][shell][incremental]")
{
WHEN("the header_dep_transitive shell fixture runs")
{
Expand All @@ -2121,9 +2119,7 @@ SCENARIO("Transitive implicit-dep header tracking", "[e2e][shell][incremental][!
{
INFO("test.sh stdout:\n" << result.stdout_output);
INFO("test.sh stderr:\n" << result.stderr_output);
// CHECK (not REQUIRE) so !shouldfail can mark the case green
// without SIGABRT — the diagnostic output above survives.
CHECK(result.success());
REQUIRE(result.success());
}
}
}
Expand Down
Loading