← board

tempfile has no mkdtemp

lib/rtl/tempfile.pas exports gettempdir, TfSysTempDir and a NamedTemporaryFile class. mkdtemp is not there.

Where it bites

lekkerzeilen/__main__.py:115:

root = tempfile.mkdtemp(prefix="lz-conform-")
pascal26:115: error: no member mkdtemp came of the qualifier tempfile

What it must do

CPython's contract: create a directory nobody else can have, return its absolute path, and leave removal to the caller. Two things a shim must not get wrong:

prefix, suffix and dir are the keyword arguments real code passes; dir defaults to gettempdir(), which this unit already has.

Scope note

Filed on track B (lib/rtl) rather than N: there is no compiler or parser gap here, and tempfile already resolves — only the member is missing.

Log

How it was fixed (2026-09-12)

lib/rtl/tempfile.pas gained mkdtemp(suffix, prefix, dir) — the three keyword arguments real code passes, dir defaulting to gettempdir().

It creates the directory and only returns a name it created: the loop calls PalMkdir and retries on failure (64 attempts), so it can never hand back a plausible unique-looking path it does not own. That was the one outcome the summary said was worth refusing over, and it is why this is a create-loop and not a name generator.

PalMkdir directly, not CreateDir, and the reason was measured rather than assumed: CreateDir passes 0o777, and with the umask here (002) that leaves 0o775 — group-writable as well as world-readable. The ticket body above guessed 0o755 at that spot and was wrong; CPython's mkdtemp promises 0o700 and this passes TF_MKDTEMP_MODE = 448 to get it.

Exhaustion raises rather than returning '', carrying the last mkdir rc.

The guard, and what makes the MODE row real

test/test_nilpy_tempfile_mkdtemp.npy — 11 rows, Makefile target test_nilpy_mkdtemp. The load-bearing one is MODE 0o700: a default-umask CreateDir implementation passes every other row in the file and fails only that one.

The pin is NOT a positive control for this change and was briefly used as one. lib/rtl is read LIVE, so the pinned compiler compiles the new fixture happily — it is not an older library, it is the same library. The control that does work is a deliberate break: setting the mode to 511 makes the MODE row print 0o775 and go red, which is what proves the row can fail at all.

The fixture also removes its own directories, using the os.rmdir added the same evening ([[bug-n-os-has-no-rmdir]]) — measured at delta 0 inodes over three runs, which matters more on seven, where /tmp is a tmpfs and the binding resource is inodes rather than bytes.

Closure effect

It was the wall at __main__.py:115. With it and the os.rmdir beside it the lekkerzeilen closure moved 115 -> 138, and after zlib.ZLIB_VERSION cleared 141 it now reaches __main__.py:312 (__doc__) with all of app.py passing — 4344 lines of warnings and no error.