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
17 changes: 16 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,14 @@ clean: ## Remove object files, keep the binary
# default set above does not, so a plain `make fclean` used to reach libft with
# SAFE unset -- landing in its SAFE!=1 branch, running the LTO capability probe
# and printing its warning just to delete files.
#
# The final `rm -rf build` cannot finish on NFS (the 42 machines' sgoinfre)
# while a shell started from build/bin is still running: NFS keeps a deleted
# file that is still open as build/bin/.nfsXXXX until its last user exits, and
# refuses to remove it ("Device or resource busy"). That made `make re` stop
# here with nothing rebuilt. Such a file is not ours to remove and does not
# block the rebuild, so fclean succeeds when only those remain; anything else
# left behind is retried and fails the target as before.
fclean: clean ## Remove objects, the binary and libft's build trees
@printf " \033[1;33m⚠\033[0m \033[1;37mRemoving binary\033[0m" >&2
@rm -f $(BIN_DIR)/$(BAPTIZE_SHELL)
Expand All @@ -496,7 +504,14 @@ fclean: clean ## Remove objects, the binary and libft's build trees
@$(MAKE) -C vendor/libft fclean SAFE=$(SAFE) BUILD_DIR=build-$(SAFE_TAG)
@rm -rf vendor/libft/build-ft vendor/libft/build-libc vendor/libft/build
@printf "\r\033[K \033[1;32m✓\033[0m \033[37mlibft cleaned\033[0m\n" >&2
@rm -rf build
@if ! rm -rf build 2>/dev/null; then \
if find build ! -type d ! -name '.nfs*' | grep -q .; then \
rm -rf build; \
else \
printf " \033[1;33m⚠\033[0m \033[37mkept %s: still open in a running process (NFS)\033[0m\n" \
"$$(find build -name '.nfs*' | xargs)" >&2; \
fi; \
fi
@printf "\n" >&2

# Run fclean THEN all, strictly in order. As plain prerequisites
Expand Down
101 changes: 101 additions & 0 deletions tests/fclean_nfs_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""`make fclean` must not fail on a file NFS keeps for a running process.

On NFS, deleting a file that a process still has open leaves it behind as
.nfsXXXX until that process exits, and removing it fails with "Device or
resource busy". With a shell from build/bin still running, `make re` used to
stop in fclean on exactly that file, with nothing rebuilt.

This runs the Makefile's own cleanup recipe (cut out of the Makefile, so the
test cannot drift from it) in a scratch directory next to the repo:
- only a held-open .nfs file left: fclean succeeds and says what it kept;
- a real leftover (an unwritable directory): fclean still fails.
Where the repo is not on NFS no .nfs file ever appears; that is a SKIP, not a
pass. argv[1] (the shell, as for every tests/*.py) is not used.
"""
import os
import shutil
import subprocess
import sys
import tempfile
import time

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FAILS = []


def check(name, ok, detail=""):
print(("ok " if ok else "FAIL ") + name)
if not ok:
FAILS.append(name)
print(" " + detail)


def recipe():
"""The cleanup block of the fclean target, from `@if ! rm -rf build` to
its closing `fi`."""
out, inside = [], False
with open(os.path.join(ROOT, "Makefile")) as f:
for line in f:
if line.startswith("\t@if ! rm -rf build"):
inside = True
if inside:
out.append(line)
if line == "\tfi\n":
return "".join(out)
return None


def fclean(d):
return subprocess.run(["make", "-s", "-C", d, "fclean"],
capture_output=True, text=True, timeout=30)


def main():
body = recipe()
check("recipe/found-in-Makefile", body is not None)
if body is None:
return 1
d = tempfile.mkdtemp(prefix=".fclean-nfs-", dir=ROOT)
holder = None
try:
with open(os.path.join(d, "Makefile"), "w") as f:
f.write("fclean:\n" + body)
os.makedirs(os.path.join(d, "build", "bin"))
held = os.path.join(d, "build", "bin", "held")
with open(held, "w") as f:
f.write("x")
holder = subprocess.Popen(["sh", "-c", "exec 3<\"$1\"; exec sleep 30",
"sh", held])
time.sleep(0.3)
os.unlink(held)
if not any(n.startswith(".nfs")
for n in os.listdir(os.path.join(d, "build", "bin"))):
print("SKIP not on NFS: no .nfs file is left for an open file")
return 0
r = fclean(d)
check("nfs-only/fclean-succeeds", r.returncode == 0, r.stderr)
check("nfs-only/says-what-it-kept", "kept build/bin/.nfs" in r.stderr,
r.stderr)
locked = os.path.join(d, "build", "locked")
os.makedirs(locked)
open(os.path.join(locked, "f"), "w").close()
os.chmod(locked, 0o555)
r = fclean(d)
os.chmod(locked, 0o755)
check("real-leftover/fclean-still-fails", r.returncode != 0, r.stderr)
finally:
if holder:
holder.kill()
holder.wait()
# NFS drops the .nfs file only once the holder is gone; retry.
for _ in range(20):
shutil.rmtree(d, ignore_errors=True)
if not os.path.exists(d):
break
time.sleep(0.25)
print("%d failed" % len(FAILS))
return 1 if FAILS else 0


sys.exit(main())
Loading