fix(driver): write per-unit .o/.bif to CWD, not root, for a bare --output

In default incremental mode each used unit is compiled to its own .o/.bif
by a worker thread.  The worker's output directory was derived as
IncludeTrailingPathDelimiter(ExtractFilePath(OutputFile)).  When --output is
a bare filename with no directory part (e.g. `--output mcd`), ExtractFilePath
returns '' and IncludeTrailingPathDelimiter('') yields '/', so the worker
tried to write `/<unit>.o.bif.tmp` at the filesystem root and aborted with
`Worker exception: Cannot open file for writing: /<unit>.o.bif.tmp`.

Resolve the artefact directory once before the worker loop, treating an empty
directory (bare or omitted --output, no --unit-cache) as the current
directory instead of root.  Priority is unchanged: --unit-cache, then the
--output directory, then CWD.

Add a CLI regression test that compiles a program using a separate unit with
a bare --output name and asserts no root-path worker failure and a clean
exit.
This commit is contained in:
Graeme Geldenhuys 2026-06-25 14:39:44 +01:00
parent d7460cb069
commit ed262aa238
2 changed files with 72 additions and 9 deletions

View file

@ -498,6 +498,7 @@ var
IRFile: string;
LinkErr: string; { Driver.LinkProgram result ('' on success) }
UnitOPath: string; { per-dep .o output path in incremental mode }
UnitODir: string; { directory the per-dep .o/.bif files are written to }
Workers: TObjectList; { TCompileWorker threads for parallel incremental }
Worker: TCompileWorker;
@ -825,17 +826,24 @@ begin
WorkerDriver := GetDriver(bkQBE);
Workers := TObjectList.Create(True);
try
{ Resolve the directory the per-unit .o/.bif artefacts go in.
Priority: an explicit --unit-cache, else the --output file's own
directory. When neither yields a directory (e.g. --output is a
bare filename with no path component, or omitted entirely) the
artefacts are written to the current directory never to the
filesystem root. ExtractFilePath returns '' for a path with no
directory part; IncludeTrailingPathDelimiter('') would turn that
into '/', anchoring the unit objects at root, so only prepend a
delimiter when the directory is non-empty. }
if UnitCacheDir <> '' then
UnitODir := IncludeTrailingPathDelimiter(UnitCacheDir)
else if ExtractFilePath(OutputFile) <> '' then
UnitODir := IncludeTrailingPathDelimiter(ExtractFilePath(OutputFile))
else
UnitODir := '';
for I := 0 to Units.Count - 1 do
begin
UnitOPath := UnitCacheDir;
if UnitOPath <> '' then
UnitOPath := IncludeTrailingPathDelimiter(UnitOPath) +
LowerCase(TUnit(Units.Items[I]).Name) + '.o'
else if OutputFile <> '' then
UnitOPath := IncludeTrailingPathDelimiter(ExtractFilePath(OutputFile)) +
LowerCase(TUnit(Units.Items[I]).Name) + '.o'
else
UnitOPath := LowerCase(TUnit(Units.Items[I]).Name) + '.o';
UnitOPath := UnitODir + LowerCase(TUnit(Units.Items[I]).Name) + '.o';
Worker := TCompileWorker.Create(True);
Worker.WorkUnit := TUnit(Units.Items[I]);

View file

@ -81,6 +81,9 @@ type
procedure TestDivByZeroCaught_Native;
procedure TestModByZeroCaught_QBE;
procedure TestModByZeroCaught_Native;
{ ---- a bare --output (no directory part) must not anchor per-unit
.o/.bif artefacts at the filesystem root ---- }
procedure TestBareOutput_UnitArtefacts_NotWrittenToRoot;
end;
implementation
@ -559,6 +562,58 @@ begin
AssertTrue('mod by zero caught', Pos('mod caught', Out_) >= 0);
end;
procedure TCLIContractTests.TestBareOutput_UnitArtefacts_NotWrittenToRoot;
var
UnitPath, ProgPath, Out_: string;
EC: Integer;
begin
if not CompilerAvailable() then begin Ignore('<toolchain-missing>'); Exit; end;
{ Regression: incremental compilation (default) compiles each used unit to
its own .o/.bif via a worker. The worker's output directory was derived
from --output via IncludeTrailingPathDelimiter(ExtractFilePath(OutputFile)).
For a bare --output filename (no directory part) ExtractFilePath returns ''
and IncludeTrailingPathDelimiter('') yields '/', so the worker tried to
write '/<unit>.o.bif.tmp' at the filesystem root and failed with
'Cannot open file for writing: /<unit>.o.bif.tmp'. The artefacts must land
in the current directory instead. }
UnitPath := FScratch + 'clidemo_unit.pas';
WriteFile(UnitPath,
'unit clidemo_unit;' + LineEnding +
'interface' + LineEnding +
'function Answer: Integer;' + LineEnding +
'implementation' + LineEnding +
'function Answer: Integer;' + LineEnding +
'begin' + LineEnding +
' Result := 42' + LineEnding +
'end;' + LineEnding +
'end.');
ProgPath := WriteScratchSource(
'program cli_bareout;' + LineEnding +
'uses clidemo_unit;' + LineEnding +
'begin' + LineEnding +
' WriteLn(Answer())' + LineEnding +
'end.');
{ Bare --output name (no '/') is the trigger; the unit is found via the
scratch --unit-path. }
EC := RunCompiler([
'--source', ProgPath,
'--unit-path', FScratch,
'--unit-path', FRTLPath,
'--unit-path', FStdlibPath,
'--output', 'cli_bareout_bin'
], Out_);
{ The bare output name resolves relative to the compiler's CWD (the test
runner's working directory): the program binary and the per-unit .o land
there. Remove them so the working tree stays clean. }
if FileExists('cli_bareout_bin') then DeleteFile('cli_bareout_bin');
if FileExists('clidemo_unit.o') then DeleteFile('clidemo_unit.o');
AssertTrue('no root-path worker failure: ' + Out_,
Pos('Cannot open file for writing: /', Out_) < 0);
AssertTrue('no worker exception: ' + Out_,
Pos('Worker exception', Out_) < 0);
AssertEquals('bare --output compile exits 0: ' + Out_, 0, EC);
end;
{ ---- Registration ---- }
initialization