feat(freebsd): select the RTL unit list per target (Step 5)

EnsureRTLObjects hard-coded the Linux RTL adapter set: rtl.platform.layout.linux
in the base list, and for a --static link the Linux kernel leaf
(runtime.start.static.linux, runtime.syscall.linux, runtime.libc.linux,
runtime.libc2.linux, runtime.thread.static.linux). A --target freebsd-x86_64
program therefore always linked Linux/libc RTL code regardless of the target.

Extract the selection into a pure, unit-tested helper BuildRTLUnitList(AStatic,
AOS) that follows the target: the platform-layout adapter becomes
rtl.platform.layout.<os>, and the --static kernel leaf swaps to the freestanding
per-OS units (runtime.start.static.<os>, runtime.syscall.<os>,
runtime.libc.<os>, runtime.libc2.<os>, runtime.thread.static.<os>) plus the
OS-invariant runtime.cstub. EnsureRTLObjects now drives it off AOpts.Static and
AOpts.Target.OS. Linux is the default suffix, so the existing Linux link path is
byte-for-byte unchanged; FreeBSD selects its full adapter set with no Linux RTL
unit present.

Tests: TBackendDriverContractTests.TestRTLUnits_* cover the Linux-dynamic,
Linux-static, FreeBSD-static-swaps and FreeBSD-static-no-Linux-leaf cases.
Verified: --static Linux hello-world still runs (regression check); all four
fixpoints + QBE & native TestRunner green (4024 tests).
This commit is contained in:
Graeme Geldenhuys 2026-07-01 10:28:13 +01:00
parent a1ba257da7
commit d0032269d6
2 changed files with 179 additions and 19 deletions

View file

@ -247,6 +247,22 @@ type
function RunProcess(const AExe: string; AArgs: TStringList;
out AOutput: string): Integer;
{ Build the leaf-first RTL unit list for a target. The base list is shared;
the OS-specific bits are selected from AOS:
* the platform-layout adapter (rtl.platform.layout.<os>), and
* for a --static (freestanding, no-libc) link, the kernel leaf that
replaces libc the freestanding runtime.start.static.<os> in place of
the libc runtime.start, plus runtime.syscall.<os> / runtime.libc.<os> /
runtime.libc2.<os> / runtime.thread.static.<os> and the OS-invariant
runtime.cstub.
A dynamic (libc) link keeps the plain list with only the layout adapter
swapped. Exposed for unit testing; EnsureRTLObjects drives it off
AOpts.Static and AOpts.Target.OS. Caller owns and frees the result. }
function BuildRTLUnitList(AStatic: Boolean;
AOS: TTargetOS): TStringList;
{ Format one --help flag line with the shared 2-space indent and flag
column. Single source of truth for the column width so the common
usage block (Blaise.pas PrintUsage) and each driver's DescribeOptions
@ -433,6 +449,50 @@ const
'runtime.weak', 'runtime.float', 'runtime.thread', 'runtime.exc',
'rtl.platform.layout.linux', 'rtl.platform.posix');
{ Lower-case OS suffix used in the OS-specific RTL unit names
(rtl.platform.layout.<os>, runtime.syscall.<os>, ). }
function RTLOSSuffix(AOS: TTargetOS): string;
begin
case AOS of
osFreeBSD: Result := 'freebsd';
else
{ Linux is the default host leaf; other OSes gain their own suffix as
their adapter set lands. }
Result := 'linux';
end;
end;
function BuildRTLUnitList(AStatic: Boolean; AOS: TTargetOS): TStringList;
var
I: Integer;
OS: string;
begin
OS := RTLOSSuffix(AOS);
Result := TStringList.Create();
{ The base list, in leaf-first order. Two OS-specific substitutions:
the platform-layout adapter always follows the target, and for a --static
link the libc entry point (runtime.start) is replaced by the freestanding
per-OS start. A dynamic link keeps runtime.start. }
for I := 0 to High(RTL_UNITS) do
if SameText(RTL_UNITS[I], 'rtl.platform.layout.linux') then
Result.Add('rtl.platform.layout.' + OS)
else if AStatic and SameText(RTL_UNITS[I], 'runtime.start') then
Result.Add('runtime.start.static.' + OS)
else
Result.Add(RTL_UNITS[I]);
{ The freestanding kernel leaf that replaces libc under --static: the raw
syscall stubs, the OS-invariant freestanding C functions (runtime.cstub),
the libc-shim layers, and the static-TLS thread leaf. }
if AStatic then
begin
Result.Add('runtime.syscall.' + OS);
Result.Add('runtime.cstub');
Result.Add('runtime.libc.' + OS);
Result.Add('runtime.libc2.' + OS);
Result.Add('runtime.thread.static.' + OS);
end;
end;
function TBackendDriver.EnsureRTLObjects(AOpts: TBackendOpts;
AIncludeStartup: Boolean; AAlreadyProvided, AObjPaths: TStringList): string;
var
@ -492,25 +552,12 @@ begin
ForceDirectories(BuildDir);
BlaiseBin := ParamStr(0);
{ The unit list to build/link, in leaf-first order. For a --static link the
kernel leaf replaces libc: drop runtime.start (libc/__libc_start_main entry)
in favour of the freestanding runtime.start.static.<os>, and add the raw
syscall stubs (runtime.syscall.<os>) + the freestanding C functions
(runtime.cstub). A dynamic (libc) link uses the plain RTL_UNITS. }
Units := TStringList.Create();
for I := 0 to High(RTL_UNITS) do
if AOpts.Static and SameText(RTL_UNITS[I], 'runtime.start') then
Units.Add('runtime.start.static.linux')
else
Units.Add(RTL_UNITS[I]);
if AOpts.Static then
begin
Units.Add('runtime.syscall.linux');
Units.Add('runtime.cstub');
Units.Add('runtime.libc.linux');
Units.Add('runtime.libc2.linux');
Units.Add('runtime.thread.static.linux');
end;
{ The unit list to build/link, in leaf-first order. Selection is driven off
the target (AOpts.Target.OS) and link mode (AOpts.Static): the platform
layout adapter follows the target, and a --static link swaps in the
freestanding per-OS kernel leaf (start / syscall / libc shims / thread) in
place of libc. See BuildRTLUnitList. }
Units := BuildRTLUnitList(AOpts.Static, AOpts.Target.OS);
for I := 0 to Units.Count - 1 do
begin

View file

@ -22,12 +22,24 @@ interface
uses
SysUtils, Classes, blaise.testing,
blaise.codegen.driver,
blaise.codegen.target, { TTargetOS: osLinux / osFreeBSD }
blaise.codegen.qbe.driver, { registers the QBE driver }
blaise.codegen.native.driver; { registers the native driver }
type
TBackendDriverContractTests = class(TTestCase)
private
function ListContains(AList: TStringList; const AName: string): Boolean;
published
{ Per-target RTL unit-list selection (FreeBSD Step 5). BuildRTLUnitList is
the pure selection helper EnsureRTLObjects drives off AOpts.Static +
AOpts.Target.OS; these assert Linux vs FreeBSD swaps without invoking the
whole compile+link pipeline. }
procedure TestRTLUnits_LinuxDynamic_UsesLinuxLayout;
procedure TestRTLUnits_LinuxStatic_SwapsLinuxLeaf;
procedure TestRTLUnits_FreeBSDStatic_SwapsFreeBSDLeaf;
procedure TestRTLUnits_FreeBSDStatic_NoLinuxLeaf;
{ ClaimsEmitIR selection policy. }
procedure TestQBE_ClaimsEmitIR_True;
procedure TestNative_ClaimsEmitIR_False;
@ -54,6 +66,107 @@ type
implementation
function TBackendDriverContractTests.ListContains(AList: TStringList;
const AName: string): Boolean;
var
I: Integer;
begin
Result := False;
for I := 0 to AList.Count - 1 do
if SameText(AList.Strings[I], AName) then
Exit(True);
end;
procedure TBackendDriverContractTests.TestRTLUnits_LinuxDynamic_UsesLinuxLayout;
var
U: TStringList;
begin
{ A dynamic (libc) Linux link keeps the plain RTL list: the Linux layout
adapter is present and no freestanding kernel leaf is pulled in. }
U := BuildRTLUnitList(False, osLinux);
try
AssertTrue('linux layout present',
ListContains(U, 'rtl.platform.layout.linux'));
AssertFalse('no static start in dynamic mode',
ListContains(U, 'runtime.start.static.linux'));
AssertFalse('no syscall leaf in dynamic mode',
ListContains(U, 'runtime.syscall.linux'));
finally
U.Free();
end;
end;
procedure TBackendDriverContractTests.TestRTLUnits_LinuxStatic_SwapsLinuxLeaf;
var
U: TStringList;
begin
{ A --static Linux link swaps runtime.start for the freestanding
runtime.start.static.linux and adds the Linux syscall/cstub/libc leaf. }
U := BuildRTLUnitList(True, osLinux);
try
AssertTrue('freestanding start present',
ListContains(U, 'runtime.start.static.linux'));
AssertFalse('libc start dropped',
ListContains(U, 'runtime.start'));
AssertTrue('linux syscall leaf present',
ListContains(U, 'runtime.syscall.linux'));
AssertTrue('linux libc2 present',
ListContains(U, 'runtime.libc2.linux'));
AssertTrue('linux static thread leaf present',
ListContains(U, 'runtime.thread.static.linux'));
AssertTrue('linux layout present',
ListContains(U, 'rtl.platform.layout.linux'));
finally
U.Free();
end;
end;
procedure TBackendDriverContractTests.TestRTLUnits_FreeBSDStatic_SwapsFreeBSDLeaf;
var
U: TStringList;
begin
{ A --static FreeBSD link selects the FreeBSD adapter set: the FreeBSD
layout, freestanding start, syscall leaf, libc2 and static thread leaf. }
U := BuildRTLUnitList(True, osFreeBSD);
try
AssertTrue('freebsd layout present',
ListContains(U, 'rtl.platform.layout.freebsd'));
AssertTrue('freebsd freestanding start present',
ListContains(U, 'runtime.start.static.freebsd'));
AssertTrue('freebsd syscall leaf present',
ListContains(U, 'runtime.syscall.freebsd'));
AssertTrue('freebsd libc2 present',
ListContains(U, 'runtime.libc2.freebsd'));
AssertTrue('freebsd static thread leaf present',
ListContains(U, 'runtime.thread.static.freebsd'));
finally
U.Free();
end;
end;
procedure TBackendDriverContractTests.TestRTLUnits_FreeBSDStatic_NoLinuxLeaf;
var
U: TStringList;
begin
{ The FreeBSD list must contain NO Linux-specific RTL unit the whole point
of the per-target swap. }
U := BuildRTLUnitList(True, osFreeBSD);
try
AssertFalse('no linux layout',
ListContains(U, 'rtl.platform.layout.linux'));
AssertFalse('no linux start',
ListContains(U, 'runtime.start.static.linux'));
AssertFalse('no linux syscall leaf',
ListContains(U, 'runtime.syscall.linux'));
AssertFalse('no linux libc2',
ListContains(U, 'runtime.libc2.linux'));
AssertFalse('no linux static thread leaf',
ListContains(U, 'runtime.thread.static.linux'));
finally
U.Free();
end;
end;
procedure TBackendDriverContractTests.TestQBE_ClaimsEmitIR_True;
begin
AssertTrue('QBE must claim --emit-ir',