feat(codegen): native linker builds the RTL from source, drops blaise_rtl.a
RTL-unification Stage 2 (docs/rtl-unification-plan.adoc): the native internal linker no longer links a pre-built blaise_rtl.a. Instead it compiles the implicit RTL units (runtime.* + rtl.platform.*) from source into an object cache beside the compiler (compiler/target/rtl/*.o) and links those, recompiling a unit only when its cached .o is missing or older than the source. - EnsureRTLObjects (native driver) drives the per-unit compile via self-invoke (blaise --no-incremental --assembler internal --source U.pas), the proven per-unit path; LinkViaInternalLinker adds the cached .o via AddOwnedObject instead of AddArchive. - uToolchain.RTLSourceDir resolves the RTL source robustly: binary-relative (compiler/target -> ../src/main/pascal), then CWD-relative (handles a binary copied to /tmp but invoked from the project root, as the fixpoints do). - New --rtl-src DIR flag (TBackendOpts.RTLSrcDir) + $BLAISE_RTL_SRC for a relocated/release binary; the driver fails loudly when the RTL source is unreachable rather than emitting a broken binary. Performance: warm cache adds ~0ms (RTL skipped); a cold full RTL recompile is ~0.4s. Scope: this changes the NATIVE internal-linker path only — the QBE backend and cc-link paths still use blaise_rtl.a, so the archive remains a build artifact (the fixpoint scripts and QBE-based tests still build it). TestLink_MissingRTL_FailsLoudly rewritten for the new contract (--rtl-src at a nonexistent dir must fail naming the RTL source). All four fixpoints OK; full suite 3829 green; cold-bootstrap from the existing release works.
This commit is contained in:
parent
ac9e2fa0b1
commit
d85ff170cf
|
|
@ -231,6 +231,11 @@ begin
|
|||
Inc(I);
|
||||
AFront.UnitCacheDir := ParamStr(I);
|
||||
end
|
||||
else if (Arg = '--rtl-src') and (I < ParamCount()) then
|
||||
begin
|
||||
Inc(I);
|
||||
AOpts.RTLSrcDir := ParamStr(I);
|
||||
end
|
||||
else if Arg = '--emit-ir' then
|
||||
AFront.EmitIR := True
|
||||
else if Arg = '--emit-asm' then
|
||||
|
|
|
|||
|
|
@ -88,6 +88,10 @@ type
|
|||
UseInternalLinker: Boolean; { --linker internal (native backend) }
|
||||
LinkerExplicit: Boolean; { True when user passed --linker }
|
||||
LinkerChoiceBad: Boolean; { --linker value not 'internal' or 'external' }
|
||||
RTLSrcDir: string; { --rtl-src DIR: explicit RTL source directory.
|
||||
Overrides the binary/CWD-relative lookup, for
|
||||
a relocated/release binary whose RTL source
|
||||
lives elsewhere (empty = use default lookup) }
|
||||
end;
|
||||
|
||||
TBackendDriver = class
|
||||
|
|
|
|||
|
|
@ -69,6 +69,13 @@ type
|
|||
private
|
||||
function LinkViaInternalLinker(const AObjFile, AOutputFile: string;
|
||||
AOpts: TBackendOpts; AExtraObjects: TStringList): string;
|
||||
{ Compile the implicit RTL units to a per-compiler object cache and return
|
||||
their .o paths in AObjPaths (link order). Each unit is (re)compiled only
|
||||
when its cached .o is missing or older than the source. Replaces the
|
||||
pre-built blaise_rtl.a: the RTL is now built from source by the compiler
|
||||
itself (docs/rtl-unification-plan.adoc). '' on success, else an error. }
|
||||
function EnsureRTLObjects(AOpts: TBackendOpts;
|
||||
AObjPaths: TStringList): string;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
|
@ -182,19 +189,90 @@ begin
|
|||
Result := 'cc -c error (exit ' + IntToStr(ExitCode) + '): ' + Msg;
|
||||
end;
|
||||
|
||||
{ The implicit RTL units, in archive/link order. The compiler emits calls to
|
||||
their symbols (_SetArgs, _BlaiseGetMem, _start, ARC helpers, …) in every
|
||||
program, so every program links them. Names are the dotted-flat unit names;
|
||||
the source files are <name>.pas in the RTL source directory. }
|
||||
const
|
||||
RTL_UNITS: array[0..13] of string = (
|
||||
'runtime.start', 'runtime.atomic', 'runtime.setjmp', 'runtime.utf8',
|
||||
'runtime.mem', 'runtime.str', 'runtime.set', 'runtime.arc',
|
||||
'runtime.weak', 'runtime.float', 'runtime.thread', 'runtime.exc',
|
||||
'rtl.platform.layout.linux', 'rtl.platform.posix');
|
||||
|
||||
function TNativeBackendDriver.EnsureRTLObjects(AOpts: TBackendOpts;
|
||||
AObjPaths: TStringList): string;
|
||||
var
|
||||
SrcDir, CacheDir, BlaiseBin: string;
|
||||
SrcFile, ObjFile: string;
|
||||
I, ExitCode: Integer;
|
||||
Args: TStringList;
|
||||
Msg: string;
|
||||
begin
|
||||
Result := '';
|
||||
{ RTL source lives in the compiler's own source tree. Resolution order:
|
||||
1. --rtl-src DIR (AOpts.RTLSrcDir) — explicit, for a relocated binary;
|
||||
2. $BLAISE_RTL_SRC;
|
||||
3. binary/CWD-relative default (RTLSourceDir). }
|
||||
if AOpts.RTLSrcDir <> '' then
|
||||
SrcDir := ExpandFileName(AOpts.RTLSrcDir)
|
||||
else
|
||||
begin
|
||||
SrcDir := GetEnvironmentVariable('BLAISE_RTL_SRC');
|
||||
if SrcDir = '' then
|
||||
SrcDir := ExpandFileName(RTLSourceDir());
|
||||
end;
|
||||
if not DirectoryExists(SrcDir) then
|
||||
Exit('internal linker: RTL source directory not found (' + SrcDir +
|
||||
'); pass --rtl-src DIR or set $BLAISE_RTL_SRC to compiler/src/main/pascal');
|
||||
|
||||
{ Object cache lives beside the compiler binary so repeated program builds
|
||||
reuse it (compiler/target/rtl/). }
|
||||
CacheDir := IncludeTrailingPathDelimiter(CompilerBinDir()) + 'rtl';
|
||||
ForceDirectories(CacheDir);
|
||||
BlaiseBin := ParamStr(0);
|
||||
|
||||
for I := 0 to High(RTL_UNITS) do
|
||||
begin
|
||||
SrcFile := IncludeTrailingPathDelimiter(SrcDir) + RTL_UNITS[I] + '.pas';
|
||||
ObjFile := IncludeTrailingPathDelimiter(CacheDir) + RTL_UNITS[I] + '.o';
|
||||
if not FileExists(SrcFile) then
|
||||
Exit('internal linker: RTL unit source missing: ' + SrcFile);
|
||||
|
||||
{ Recompile only when the cached object is missing or stale. }
|
||||
if (not FileExists(ObjFile)) or (FileAge(ObjFile) < FileAge(SrcFile)) then
|
||||
begin
|
||||
Args := TStringList.Create();
|
||||
try
|
||||
Args.Add('--no-incremental');
|
||||
Args.Add('--assembler'); Args.Add('internal');
|
||||
Args.Add('--source'); Args.Add(SrcFile);
|
||||
Args.Add('--unit-path'); Args.Add(SrcDir);
|
||||
Args.Add('--output'); Args.Add(ObjFile);
|
||||
ExitCode := RunProcess(BlaiseBin, Args, Msg);
|
||||
finally
|
||||
Args.Free();
|
||||
end;
|
||||
if ExitCode <> 0 then
|
||||
Exit('internal linker: failed to build RTL unit ' + RTL_UNITS[I] +
|
||||
' (exit ' + IntToStr(ExitCode) + '): ' + Msg);
|
||||
end;
|
||||
AObjPaths.Add(ObjFile);
|
||||
end;
|
||||
end;
|
||||
|
||||
function TNativeBackendDriver.LinkViaInternalLinker(
|
||||
const AObjFile, AOutputFile: string;
|
||||
AOpts: TBackendOpts; AExtraObjects: TStringList): string;
|
||||
var
|
||||
Lk: TLinker;
|
||||
Obj: TElfObjectFile;
|
||||
TC: TToolchain;
|
||||
Toolkit: TTargetToolkit;
|
||||
LinkTarget: TLinkTarget;
|
||||
RTLObjs: TStringList;
|
||||
I: Integer;
|
||||
begin
|
||||
Result := '';
|
||||
TC := ResolveToolchain(AOpts.Target);
|
||||
|
||||
{ Build the linker with the resolved target's TLinkTarget so the emitted ELF
|
||||
carries the right EI_OSABI / machine / load base for AOpts.Target — without
|
||||
|
|
@ -206,49 +284,53 @@ begin
|
|||
Exit('internal linker: no toolkit registered for target ' +
|
||||
TargetName(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');
|
||||
|
||||
{ TLinker.Create(ATarget) borrows the target — we own and free it here. }
|
||||
LinkTarget := Toolkit.MakeLinkTarget();
|
||||
Lk := TLinker.Create(LinkTarget);
|
||||
{ Build the implicit RTL objects from source (cached beside the compiler).
|
||||
Every Blaise program emits calls to RTL symbols (_SetArgs, _BlaiseGetMem,
|
||||
_start, ARC/string/exception helpers, …) as undefined externals; these
|
||||
objects supply them. Replaces the pre-built blaise_rtl.a — the RTL is now
|
||||
compiled by the compiler itself. }
|
||||
RTLObjs := TStringList.Create();
|
||||
try
|
||||
Result := Self.EnsureRTLObjects(AOpts, RTLObjs);
|
||||
if Result <> '' then Exit;
|
||||
|
||||
{ TLinker.Create(ATarget) borrows the target — we own and free it here. }
|
||||
LinkTarget := Toolkit.MakeLinkTarget();
|
||||
Lk := TLinker.Create(LinkTarget);
|
||||
try
|
||||
Lk.SetDynamic(True);
|
||||
try
|
||||
Lk.SetDynamic(True);
|
||||
|
||||
Obj := ReadElfObjectFile(AObjFile);
|
||||
Lk.AddOwnedObject(Obj);
|
||||
Obj := ReadElfObjectFile(AObjFile);
|
||||
Lk.AddOwnedObject(Obj);
|
||||
|
||||
if AExtraObjects <> nil then
|
||||
for I := 0 to AExtraObjects.Count - 1 do
|
||||
if AExtraObjects <> nil then
|
||||
for I := 0 to AExtraObjects.Count - 1 do
|
||||
begin
|
||||
Obj := ReadElfObjectFile(AExtraObjects.Strings[I]);
|
||||
Lk.AddOwnedObject(Obj);
|
||||
end;
|
||||
|
||||
{ The RTL objects — including _start (runtime.start) and the implicit
|
||||
runtime symbols — built from source above. }
|
||||
for I := 0 to RTLObjs.Count - 1 do
|
||||
begin
|
||||
Obj := ReadElfObjectFile(AExtraObjects.Strings[I]);
|
||||
Obj := ReadElfObjectFile(RTLObjs.Strings[I]);
|
||||
Lk.AddOwnedObject(Obj);
|
||||
end;
|
||||
|
||||
{ _start now lives in blaise_rtl.a (blaise_start_x86_64.s) — no system
|
||||
Scrt1.o / crtbegin / crtend / crti / crtn needed. AddArchive adds all
|
||||
members unconditionally, so the entry symbol is always present. }
|
||||
Lk.AddArchive(TC.RTLPath);
|
||||
|
||||
Lk.Link('_start', AOutputFile);
|
||||
except
|
||||
on E: Exception do
|
||||
Result := 'internal linker error [' + Exception(E).ClassName + ']: ' +
|
||||
Exception(E).Message;
|
||||
Lk.Link('_start', AOutputFile);
|
||||
except
|
||||
on E: Exception do
|
||||
Result := 'internal linker error [' + Exception(E).ClassName + ']: ' +
|
||||
Exception(E).Message;
|
||||
end;
|
||||
finally
|
||||
Lk.Free();
|
||||
LinkTarget.Free();
|
||||
end;
|
||||
finally
|
||||
Lk.Free();
|
||||
LinkTarget.Free();
|
||||
RTLObjs.Free();
|
||||
end;
|
||||
end;
|
||||
|
||||
|
|
|
|||
|
|
@ -89,6 +89,14 @@ function ResolveAssembler: TTool;
|
|||
function ResolveLinker(const ATarget: TTargetDesc): TTool;
|
||||
function FindRTLArchive(const ATarget: TTargetDesc): string;
|
||||
|
||||
{ Directory containing the running compiler binary (ParamStr(0)). }
|
||||
function CompilerBinDir: string;
|
||||
|
||||
{ RTL source directory (compiler/src/main/pascal), relative to the binary.
|
||||
The driver builds the RTL objects from here when linking; overridable via
|
||||
$BLAISE_RTL_SRC. }
|
||||
function RTLSourceDir: string;
|
||||
|
||||
{ Walks $PATH for the first file named ABaseName that exists. Returns the
|
||||
absolute path on hit, '' on miss. On Windows hosts also tries '.exe'. }
|
||||
function WhichInPath(const ABaseName: string): string;
|
||||
|
|
@ -319,6 +327,42 @@ begin
|
|||
Result := ExtractFilePath(ParamStr(0));
|
||||
end;
|
||||
|
||||
function RTLDirHasUnits(const ADir: string): Boolean;
|
||||
begin
|
||||
Result := (ADir <> '') and
|
||||
FileExists(IncludeTrailingPathDelimiter(ADir) + 'runtime.arc.pas');
|
||||
end;
|
||||
|
||||
function RTLSourceDir: string;
|
||||
var
|
||||
Cand: string;
|
||||
begin
|
||||
{ The RTL units (runtime.*, rtl.platform.*) live in the compiler's own source
|
||||
tree. Resolve the directory robustly across the ways the compiler is run:
|
||||
|
||||
1. binary-relative: <bindir>/../src/main/pascal — the in-tree
|
||||
compiler/target/blaise.
|
||||
2. CWD-relative: compiler/src/main/pascal then src/main/pascal — the
|
||||
binary may be copied elsewhere (e.g. /tmp during a fixpoint) but
|
||||
invoked from the project root by the build.
|
||||
|
||||
The driver consults $BLAISE_RTL_SRC before calling this, for a fully
|
||||
relocated binary (release) that ships its RTL source elsewhere. }
|
||||
Cand := IncludeTrailingPathDelimiter(CompilerBinDir()) + '../src/main/pascal';
|
||||
if RTLDirHasUnits(ExpandFileName(Cand)) then Exit(ExpandFileName(Cand));
|
||||
|
||||
Cand := 'compiler/src/main/pascal';
|
||||
if RTLDirHasUnits(ExpandFileName(Cand)) then Exit(ExpandFileName(Cand));
|
||||
|
||||
Cand := 'src/main/pascal';
|
||||
if RTLDirHasUnits(ExpandFileName(Cand)) then Exit(ExpandFileName(Cand));
|
||||
|
||||
{ Fall back to the binary-relative path so callers get a meaningful "not
|
||||
found" message naming a concrete location. }
|
||||
Result := ExpandFileName(IncludeTrailingPathDelimiter(CompilerBinDir())
|
||||
+ '../src/main/pascal');
|
||||
end;
|
||||
|
||||
function FindRTLArchive(const ATarget: TTargetDesc): string;
|
||||
var
|
||||
BinDir: string;
|
||||
|
|
|
|||
|
|
@ -1459,17 +1459,7 @@ begin
|
|||
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;
|
||||
IsoCompiler := FCompiler;
|
||||
|
||||
SrcFile := IsoDir + 'prog.pas';
|
||||
OutFile := IsoDir + 'prog';
|
||||
|
|
@ -1479,21 +1469,23 @@ begin
|
|||
' 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. }
|
||||
{ The RTL is now compiled from SOURCE by the compiler at link time (no prebuilt
|
||||
blaise_rtl.a). Point --rtl-src at a nonexistent directory so the RTL source
|
||||
is unreachable: the driver must fail loudly rather than emit a broken binary. }
|
||||
Rc := Self.RunProc(IsoCompiler, [
|
||||
'--source', SrcFile,
|
||||
'--unit-path', FRTLPath,
|
||||
'--unit-path', FStdlibPath,
|
||||
'--output', OutFile,
|
||||
'--backend', 'native',
|
||||
'--assembler', 'internal',
|
||||
'--linker', 'internal'
|
||||
'--linker', 'internal',
|
||||
'--rtl-src', IsoDir + 'no_such_rtl_src'
|
||||
], 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);
|
||||
AssertTrue('compiler must FAIL when the RTL source is unreachable (got rc=0)',
|
||||
Rc <> 0);
|
||||
AssertTrue('error must name the missing RTL source: ' + CompOut,
|
||||
Pos('RTL source', CompOut) >= 0);
|
||||
AssertFalse('no output binary should be produced on RTL-missing failure',
|
||||
FileExists(OutFile));
|
||||
end;
|
||||
|
|
|
|||
Loading…
Reference in a new issue