fix(linker,bootstrap): error on missing RTL; carry RTL with correct name

The internal linker silently emitted a runtime-less binary when blaise_rtl.a
could not be found beside the compiler (RTLPath empty): every RTL symbol became
an undefined dynamic import, so the binary linked but died at runtime with
`undefined symbol: _SetArgs`.

scripts/rolling-bootstrap.sh hit exactly this: it carried each step's binary
forward as _carry_blaise with its RTL as _carry_rtl.a, but FindRTLArchive looks
for `blaise_rtl.a` beside the binary (CompilerBinDir) — a name it never used —
so the next step linked the compiler with no runtime archive and SMOKE_FAILED.

- Carry the binary + RTL into a dedicated _carry/ dir as blaise + blaise_rtl.a,
  so FindRTLArchive resolves it. Rolling bootstrap reaches HEAD again.
- Make an empty RTLPath a hard error on both link paths (internal and external
  cc) instead of producing a broken binary.
- Regression test TInternalLinkerE2ETests.TestLink_MissingRTL_FailsLoudly:
  runs a symlinked compiler from an RTL-free dir and asserts a non-zero exit, a
  message naming blaise_rtl.a, and no output binary.

The four fixpoint scripts missed this because fixpoint-native.sh links the .s
via gcc, never exercising the internal-linker + FindRTL path on a carried
binary layout.
This commit is contained in:
Graeme Geldenhuys 2026-06-24 01:12:07 +01:00
parent 6c38672be3
commit 48569f36d1
4 changed files with 100 additions and 8 deletions

View file

@ -343,6 +343,15 @@ var
begin
Result := '';
TC := ResolveToolchain(AOpts.Target);
{ Every Blaise program needs blaise_rtl.a; an empty RTLPath means it was not
found beside the compiler binary or via $BLAISE_RTL. Linking without it
yields a flood of undefined-reference errors from the external linker (or,
on the internal linker, a silently broken binary) — fail with a clear
message instead. }
if TC.RTLPath = '' then
Exit('runtime archive blaise_rtl.a not found '
+ '(looked beside the compiler binary and in $BLAISE_RTL); '
+ 'cannot link a Blaise program without the RTL');
Args := TStringList.Create();
try
Args.Add('-o');
@ -359,8 +368,7 @@ begin
if AExtraObjects <> nil then
for I := 0 to AExtraObjects.Count - 1 do
Args.Add(AExtraObjects.Strings[I]);
if TC.RTLPath <> '' then
Args.Add(TC.RTLPath);
Args.Add(TC.RTLPath);
Args.Add('-lm'); { math functions (sqrt, sin, cos, etc.) }
Args.Add('-lpthread'); { POSIX threads (blaise_thread unit) }
ExitCode := RunProcess(Self.ToolPath('linker', AOpts.Target), Args, Msg);

View file

@ -197,6 +197,18 @@ begin
TC := ResolveToolchain(AOpts.Target);
{ Every Blaise program links blaise_rtl.a — the program entry emits
_SetArgs and the runtime supplies ARC, strings, exceptions, the platform
layer, etc. An empty RTLPath means FindRTLArchive could not locate
blaise_rtl.a beside the compiler binary (or via BLAISE_RTL). Linking
anyway is never correct: every RTL symbol becomes an undefined dynamic
import that silently links but dies at run time (e.g. `undefined symbol:
_SetArgs`). Fail loudly instead of emitting a broken executable. }
if TC.RTLPath = '' then
Exit('internal linker: runtime archive blaise_rtl.a not found '
+ '(looked beside the compiler binary and in $BLAISE_RTL); '
+ 'cannot link a Blaise program without the RTL');
Lk := TLinker.Create();
try
try
@ -215,8 +227,7 @@ begin
Lk.AddOwnedObject(Obj);
end;
if TC.RTLPath <> '' then
Lk.AddArchive(TC.RTLPath);
Lk.AddArchive(TC.RTLPath);
Lk.AddCrtObject(CrtEndS);
Lk.AddCrtObject(Crtn);

View file

@ -117,6 +117,7 @@ type
procedure TestRun_StringOps;
procedure TestRun_ExceptionHandling;
procedure TestRun_ClassAndVirtual;
procedure TestLink_MissingRTL_FailsLoudly;
end;
implementation
@ -1145,6 +1146,12 @@ end;
{ ---- TInternalLinkerE2ETests ---- }
{ symlink(2): point the isolated-dir compiler name at the real binary so
CompilerBinDir() resolves to the isolated dir (no blaise_rtl.a beside it),
without copying ~3 MB. Returns 0 on success. }
function _test_symlink(ATarget, ALinkPath: PChar): Integer;
external name 'symlink';
var
GIntLinkSkipNoted: Boolean = False;
@ -1387,6 +1394,66 @@ begin
AssertEquals('stdout', 'child' + LineEnding, Out_);
end;
{ Regression: when blaise_rtl.a cannot be found beside the compiler binary
(and BLAISE_RTL is unset), the compiler must REFUSE to link rather than
silently emit a binary with every RTL symbol left as an undefined dynamic
import — that broken-but-runnable binary dies at run time with
`undefined symbol: _SetArgs`. A mismatched carried-RTL name in
rolling-bootstrap surfaced exactly this. We reproduce by running a COPY of
the compiler from a scratch dir that has no blaise_rtl.a beside it. }
procedure TInternalLinkerE2ETests.TestLink_MissingRTL_FailsLoudly;
var
IsoDir, IsoCompiler, SrcFile, OutFile, CompOut: string;
Rc: Integer;
begin
if not Self.CompilerAvailable() then
begin
Ignore('<toolchain-missing>');
Exit;
end;
FCounter := FCounter + 1;
IsoDir := FScratch + 'no_rtl_' + IntToStr(FCounter) + '/';
ForceDirectories(IsoDir);
IsoCompiler := IsoDir + 'blaise';
{ Symlink the real compiler into the isolated dir — argv[0]'s dirname is the
isolated dir, which has NO blaise_rtl.a beside it, so FindRTLArchive
returns ''. (A symlink avoids copying the multi-MB binary and the
byte-exactness pitfalls of a string round-trip.) }
if FileExists(IsoCompiler) then DeleteFile(IsoCompiler);
if _test_symlink(PChar(FCompiler), PChar(IsoCompiler)) <> 0 then
begin
Ignore('<symlink-unavailable>');
Exit;
end;
SrcFile := IsoDir + 'prog.pas';
OutFile := IsoDir + 'prog';
WriteFile(SrcFile,
'program test_no_rtl;' + LineEnding +
'begin' + LineEnding +
' WriteLn(6 * 7)' + LineEnding +
'end.');
{ BLAISE_RTL is normally unset in the test environment; the isolated dir has
no blaise_rtl.a, so FindRTLArchive returns '' and the guard must fire. }
Rc := Self.RunProc(IsoCompiler, [
'--source', SrcFile,
'--unit-path', FRTLPath,
'--unit-path', FStdlibPath,
'--output', OutFile,
'--backend', 'native',
'--assembler', 'internal',
'--linker', 'internal'
], CompOut);
AssertTrue('compiler must FAIL when RTL is unreachable (got rc=0)', Rc <> 0);
AssertTrue('error must name the missing RTL archive: ' + CompOut,
Pos('blaise_rtl.a', CompOut) >= 0);
AssertFalse('no output binary should be produced on RTL-missing failure',
FileExists(OutFile));
end;
initialization
RegisterTest(TElfReaderTests);
RegisterTest(TSectionMergerTests);

View file

@ -264,10 +264,16 @@ for sha in "${COMMITS[@]}"; do
fi
# Carry this step's artifacts forward as the compiler for the next commit.
cp "$WT/_boot/blaise" "$WT/_carry_blaise"
cp "$WT/_boot/blaise_rtl.a" "$WT/_carry_rtl.a"
CUR_BIN="$WT/_carry_blaise"
CUR_RTL="$WT/_carry_rtl.a"
# FindRTLArchive (uToolchain.pas) looks for `blaise_rtl.a` BESIDE the compiler
# binary, so the carried RTL must sit next to the carried binary under that
# exact name — keep both in a dedicated _carry/ dir. (A mismatched name left
# RTLPath empty, so the next step linked the compiler with NO runtime archive
# and silently produced a binary missing _SetArgs et al. — SMOKE_FAILED.)
mkdir -p "$WT/_carry"
cp "$WT/_boot/blaise" "$WT/_carry/blaise"
cp "$WT/_boot/blaise_rtl.a" "$WT/_carry/blaise_rtl.a"
CUR_BIN="$WT/_carry/blaise"
CUR_RTL="$WT/_carry/blaise_rtl.a"
done
echo