feat(freebsd): self-host on FreeBSD — runtime fixes + target-driven defines
Blaise now runs natively on FreeBSD and compiles+links working FreeBSD binaries
(fork/exec RTL sub-compiles, threaded incremental workers, internal assembler +
linker, freestanding runtime), and cross-compiles both directions (Linux<->
FreeBSD). Each of these was a real FreeBSD-only runtime bug exposed by running on
a real FreeBSD 14.3 kernel — invisible to host-side static checks:
* fork(2) child return: FreeBSD returns the child in %rax=parent-pid with
%edx=1 (child indicator); the raw leaf must zero %eax when %edx!=0, else the
forked child runs the parent branch and never execve's its target (exit 127).
* thread join teardown: the worker set its join word + umtx-WAKE while still on
its own stack, so pthread_join munmap'd the shared stack/CB region out from
under the still-live thread -> SIGSEGV. Fixed by passing the join word as
thr_exit(state): the kernel does the suword+umtx-wake AFTER the thread leaves
its stack, so the joiner only wakes once the stack is safe to free.
* argc stack pad word: FreeBSD 16-aligns the entry %rsp and inserts a zero pad
word below argc when the argv+envc pointer-count parity requires it, so argc
is at [rsp] or [rsp+8]. _BlaiseStartC now detects and skips the pad word;
previously an execve'd child with a different environment read the pad (0) as
argc and reported "--source is required".
Target-driven OS conditional compilation (self-hosting requires HostTarget to be
correct on the produced binary):
* The OS defines (LINUX/FREEBSD/WINDOWS/UNIX) now follow the resolved --target,
not the compiler's build host. SeedPredefines seeds the host OS as a default
(keeps direct-lexer callers/tests working) via compile-time IFDEF; the driver
injects the target's OS symbol and AddDefinesTo / the unit loader replace the
host-seeded OS symbols with it on every lexer (program + dep units).
* HostTarget() reads {$IFDEF FREEBSD} etc., so a blaise built with
--target freebsd bakes HostTarget=osFreeBSD — a FreeBSD-hosted blaise then
defaults (no --target) to producing FreeBSD binaries. Verified both
directions on the VM.
Usability:
* blaise.cfg now supports rtl-src=<dir> (relative resolved against the config
dir); a CLI --rtl-src overrides it. So a relocated binary can drop a
blaise.cfg with unit-path= + rtl-src= and need no path flags. Tests:
TConfigTests.TestParseRtlSrc_*.
* --help documents the previously-undocumented --rtl-src flag and the
blaise.cfg keys, and the --target line now shows the ACTUAL host default
(TargetName(HostTarget())) instead of a hardcoded linux-x86_64 — correct on
every host.
Verified: all four fixpoints green; full suite 4032 tests under QBE-built and
native-built runner; Linux self-host and Linux<->FreeBSD cross-compile confirmed;
FreeBSD self-host (blaise compiling+running a program) confirmed on FreeBSD 14.3.
This commit is contained in:
parent
db85aba08d
commit
0824ed8a6b
|
|
@ -93,12 +93,17 @@ begin
|
||||||
WriteLn(FormatFlagLine('--output <path>', 'Output binary path'));
|
WriteLn(FormatFlagLine('--output <path>', 'Output binary path'));
|
||||||
WriteLn(FormatFlagLine('--unit-path <dir>',
|
WriteLn(FormatFlagLine('--unit-path <dir>',
|
||||||
'Add directory to unit search path (repeatable)'));
|
'Add directory to unit search path (repeatable)'));
|
||||||
|
WriteLn(FormatFlagLine('--rtl-src <dir>',
|
||||||
|
'RTL source directory (default: beside the binary; or $BLAISE_RTL_SRC).'));
|
||||||
|
WriteLn(FormatFlagLine('',
|
||||||
|
'Needed when the binary is moved away from its source tree.'));
|
||||||
WriteLn(FormatFlagLine('--define <sym> | -d <sym>',
|
WriteLn(FormatFlagLine('--define <sym> | -d <sym>',
|
||||||
'Define a conditional-compilation symbol (repeatable)'));
|
'Define a conditional-compilation symbol (repeatable)'));
|
||||||
WriteLn(FormatFlagLine('--backend <id>', BackendUsageLine()));
|
WriteLn(FormatFlagLine('--backend <id>', BackendUsageLine()));
|
||||||
WriteLn(FormatFlagLine('--target <os>-<cpu>',
|
WriteLn(FormatFlagLine('--target <os>-<cpu>',
|
||||||
'linux-x86_64 (default), linux-i386, linux-arm64,'));
|
'Cross-compile target (default: ' + TargetName(HostTarget()) + ', the host).'));
|
||||||
WriteLn(FormatFlagLine('', 'freebsd-x86_64, windows-x86_64, macos-arm64'));
|
WriteLn(FormatFlagLine('', 'linux-x86_64, linux-i386, linux-arm64, freebsd-x86_64,'));
|
||||||
|
WriteLn(FormatFlagLine('', 'windows-x86_64, macos-arm64'));
|
||||||
WriteLn(FormatFlagLine('--emit-ir', 'Print QBE IR to stdout and exit'));
|
WriteLn(FormatFlagLine('--emit-ir', 'Print QBE IR to stdout and exit'));
|
||||||
WriteLn(FormatFlagLine('--emit-asm',
|
WriteLn(FormatFlagLine('--emit-asm',
|
||||||
'Print native assembly to stdout (requires --backend native)'));
|
'Print native assembly to stdout (requires --backend native)'));
|
||||||
|
|
@ -134,8 +139,10 @@ begin
|
||||||
'Emit OPDF debug info (.opdf.s companion file)'));
|
'Emit OPDF debug info (.opdf.s companion file)'));
|
||||||
WriteLn('');
|
WriteLn('');
|
||||||
WriteLn('Configuration:');
|
WriteLn('Configuration:');
|
||||||
WriteLn(' Unit search paths can also be set in blaise.cfg (one unit-path=<dir>');
|
WriteLn(' blaise.cfg (next to the binary, then ~/.blaise.cfg) can set, one');
|
||||||
WriteLn(' per line). Searched next to the binary, then ~/.blaise.cfg.');
|
WriteLn(' per line: unit-path=<dir> (repeatable) and rtl-src=<dir>. A relative');
|
||||||
|
WriteLn(' path is resolved against the config file''s directory; --rtl-src on');
|
||||||
|
WriteLn(' the command line overrides rtl-src= in the config.');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ Populate the two caller-constructed opts objects from the command line.
|
{ Populate the two caller-constructed opts objects from the command line.
|
||||||
|
|
@ -147,11 +154,29 @@ end;
|
||||||
bad flag; the caller owns and frees both objects regardless. }
|
bad flag; the caller owns and frees both objects regardless. }
|
||||||
{ Apply each -d/--define symbol in ADefines to ALexer's conditional-compilation
|
{ Apply each -d/--define symbol in ADefines to ALexer's conditional-compilation
|
||||||
table. No-op when ADefines is nil/empty. }
|
table. No-op when ADefines is nil/empty. }
|
||||||
|
{ True if ASym is one of the OS conditional-compilation symbols. }
|
||||||
|
function IsOSDefine(const ASym: string): Boolean;
|
||||||
|
var U: string;
|
||||||
|
begin
|
||||||
|
U := UpperCase(ASym);
|
||||||
|
Result := (U = 'LINUX') or (U = 'FREEBSD') or (U = 'WINDOWS')
|
||||||
|
or (U = 'DARWIN') or (U = 'UNIX');
|
||||||
|
end;
|
||||||
|
|
||||||
procedure AddDefinesTo(ALexer: TLexer; ADefines: TStringList);
|
procedure AddDefinesTo(ALexer: TLexer; ADefines: TStringList);
|
||||||
var
|
var
|
||||||
I: Integer;
|
I: Integer;
|
||||||
|
HasOS: Boolean;
|
||||||
begin
|
begin
|
||||||
if ADefines = nil then Exit;
|
if ADefines = nil then Exit;
|
||||||
|
{ If the caller supplies an OS symbol (a cross --target injects the target's),
|
||||||
|
it REPLACES the host OS symbols the lexer seeded in SeedPredefines: drop
|
||||||
|
those first so an IFDEF LINUX etc. reflects the target, not the host. }
|
||||||
|
HasOS := False;
|
||||||
|
for I := 0 to ADefines.Count - 1 do
|
||||||
|
if IsOSDefine(ADefines.Strings[I]) then HasOS := True;
|
||||||
|
if HasOS then
|
||||||
|
ALexer.ClearOSDefines();
|
||||||
for I := 0 to ADefines.Count - 1 do
|
for I := 0 to ADefines.Count - 1 do
|
||||||
ALexer.AddDefine(ADefines.Strings[I]);
|
ALexer.AddDefine(ADefines.Strings[I]);
|
||||||
end;
|
end;
|
||||||
|
|
@ -444,6 +469,7 @@ var
|
||||||
SourceFile, OutputFile: string;
|
SourceFile, OutputFile: string;
|
||||||
SearchPaths: TStringList;
|
SearchPaths: TStringList;
|
||||||
ConfigPaths: TStringList;
|
ConfigPaths: TStringList;
|
||||||
|
CfgRtlSrc: string;
|
||||||
EmitIR: Boolean;
|
EmitIR: Boolean;
|
||||||
EmitAsm: Boolean;
|
EmitAsm: Boolean;
|
||||||
DumpAST: Boolean;
|
DumpAST: Boolean;
|
||||||
|
|
@ -531,6 +557,23 @@ begin
|
||||||
Halt(1);
|
Halt(1);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
{ The OS conditional-compilation symbols follow the resolved --target (which
|
||||||
|
defaults to the host). Inject them into Front.Defines; AddDefinesTo then
|
||||||
|
replaces the lexer's host-seeded OS symbols with these on every lexer (top
|
||||||
|
program and, via the unit loader, each dependency unit). So an IFDEF
|
||||||
|
FREEBSD in compiled source reflects the TARGET, not this compiler's host,
|
||||||
|
which is what makes cross-compiling blaise itself for FreeBSD bake
|
||||||
|
HostTarget=FreeBSD into the result. }
|
||||||
|
case Opts.Target.OS of
|
||||||
|
osFreeBSD: Front.Defines.Add('FREEBSD');
|
||||||
|
osWindows: Front.Defines.Add('WINDOWS');
|
||||||
|
osMacOS: Front.Defines.Add('DARWIN');
|
||||||
|
else
|
||||||
|
Front.Defines.Add('LINUX');
|
||||||
|
end;
|
||||||
|
if Opts.Target.OS <> osWindows then
|
||||||
|
Front.Defines.Add('UNIX');
|
||||||
|
|
||||||
{ Seed the working locals from the opts objects. Keeping the locals lets
|
{ Seed the working locals from the opts objects. Keeping the locals lets
|
||||||
the (large) main body read them unchanged; the parser owns the objects.
|
the (large) main body read them unchanged; the parser owns the objects.
|
||||||
SearchPaths is the object's owned list (not copied) so config-path
|
SearchPaths is the object's owned list (not copied) so config-path
|
||||||
|
|
@ -556,7 +599,14 @@ begin
|
||||||
|
|
||||||
ConfigPaths := TStringList.Create();
|
ConfigPaths := TStringList.Create();
|
||||||
try
|
try
|
||||||
LoadConfigPaths(ConfigPaths);
|
{ blaise.cfg supplies extra unit-paths and, optionally, rtl-src. A CLI
|
||||||
|
--rtl-src (already in Opts.RTLSrcDir) takes precedence: only let the config
|
||||||
|
set it when the CLI did not. Seed CfgRtlSrc with the CLI value so the
|
||||||
|
config only overrides an EMPTY one. }
|
||||||
|
CfgRtlSrc := Opts.RTLSrcDir;
|
||||||
|
LoadConfigPaths(ConfigPaths, CfgRtlSrc);
|
||||||
|
if Opts.RTLSrcDir = '' then
|
||||||
|
Opts.RTLSrcDir := CfgRtlSrc;
|
||||||
for I := ConfigPaths.Count - 1 downto 0 do
|
for I := ConfigPaths.Count - 1 downto 0 do
|
||||||
SearchPaths.Insert(0, ConfigPaths.Strings[I]);
|
SearchPaths.Insert(0, ConfigPaths.Strings[I]);
|
||||||
finally
|
finally
|
||||||
|
|
|
||||||
|
|
@ -107,14 +107,22 @@ end;
|
||||||
|
|
||||||
function HostTarget: TTargetDesc;
|
function HostTarget: TTargetDesc;
|
||||||
begin
|
begin
|
||||||
{ A '.exe' suffix on our own path means we are running on Windows (incl.
|
{ The host OS is a COMPILE-TIME property of this binary: a blaise built with
|
||||||
under wine). Unix hosts carry no extension; we can't cheaply tell
|
`--target freebsd-x86_64` runs on FreeBSD, so its HostTarget must report
|
||||||
linux/macos/freebsd apart here, and only the Windows distinction matters
|
FreeBSD. The OS conditional symbols are target-driven (injected by the
|
||||||
for tool extensions today, so default the unix case to linux. }
|
driver from the resolved --target; see Blaise.pas and uLexer.SeedPredefines),
|
||||||
if LowerCase(ExtractFileExt(ParamStr(0))) = '.exe' then
|
the IFDEF FREEBSD below is true exactly when THIS compiler was built for
|
||||||
MakeTarget(osWindows, cpuX86_64, Result)
|
FreeBSD. This is what lets a FreeBSD-hosted blaise default (no --target) to
|
||||||
else
|
producing FreeBSD binaries — the basis of self-hosting on FreeBSD. }
|
||||||
MakeTarget(osLinux, cpuX86_64, Result);
|
{$IFDEF FREEBSD}
|
||||||
|
MakeTarget(osFreeBSD, cpuX86_64, Result);
|
||||||
|
{$ELSE}
|
||||||
|
{$IFDEF WINDOWS}
|
||||||
|
MakeTarget(osWindows, cpuX86_64, Result);
|
||||||
|
{$ELSE}
|
||||||
|
MakeTarget(osLinux, cpuX86_64, Result);
|
||||||
|
{$ENDIF}
|
||||||
|
{$ENDIF}
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function PtrSize(const ATarget: TTargetDesc): Integer;
|
function PtrSize(const ATarget: TTargetDesc): Integer;
|
||||||
|
|
|
||||||
|
|
@ -267,7 +267,18 @@ asm
|
||||||
jmp main
|
jmp main
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ The C-level entry: SP points at the kernel's initial stack (argc at [SP]). }
|
|
||||||
|
|
||||||
|
{ The C-level entry. The kernel hands us the initial process stack: argc, then
|
||||||
|
argv[], NULL, envp[], NULL, auxv. On FreeBSD the entry %rsp must be 16-byte
|
||||||
|
aligned, and the kernel achieves that by inserting a ZERO PAD WORD immediately
|
||||||
|
below argc when the argument-vector layout would otherwise leave argc at an
|
||||||
|
8-mod-16 address (this depends on the argc+envc pointer-count parity, so it
|
||||||
|
varies between the shell-launched top-level and an execve'd child with a
|
||||||
|
different environment). So argc is at [SP] usually, but at [SP+8] when a pad
|
||||||
|
word is present. Detect the pad: a real argc is a small non-negative count
|
||||||
|
and the slot after it (argv[0]) is a valid pointer; a pad word reads 0. If
|
||||||
|
[SP] is 0 and [SP+8] is a plausible argc, skip the pad word. }
|
||||||
procedure _BlaiseStartC(SP: Pointer);
|
procedure _BlaiseStartC(SP: Pointer);
|
||||||
var
|
var
|
||||||
Argc: Int64;
|
Argc: Int64;
|
||||||
|
|
@ -275,6 +286,13 @@ var
|
||||||
Ret: Integer;
|
Ret: Integer;
|
||||||
begin
|
begin
|
||||||
Argc := PInt64(SP)^;
|
Argc := PInt64(SP)^;
|
||||||
|
{ Skip a kernel alignment pad word below argc, if present. }
|
||||||
|
if Argc = 0 then
|
||||||
|
begin
|
||||||
|
if PInt64(Pointer(PChar(SP) + 8))^ > 0 then
|
||||||
|
SP := Pointer(PChar(SP) + 8);
|
||||||
|
Argc := PInt64(SP)^;
|
||||||
|
end;
|
||||||
Argv := Pointer(PChar(SP) + 8);
|
Argv := Pointer(PChar(SP) + 8);
|
||||||
{ envp = &argv[argc+1] }
|
{ envp = &argv[argc+1] }
|
||||||
Envp := Pointer(PChar(Argv) + (Argc + 1) * 8);
|
Envp := Pointer(PChar(Argv) + (Argc + 1) * 8);
|
||||||
|
|
|
||||||
|
|
@ -457,6 +457,14 @@ asm
|
||||||
ret
|
ret
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
{ FreeBSD fork(2) has a two-register return the libc wrapper normalises and a
|
||||||
|
raw syscall must replicate: the kernel returns the child pid in %rax for the
|
||||||
|
PARENT with %edx = 0, but for the CHILD it returns the PARENT's pid in %rax
|
||||||
|
with %edx = 1 as the child indicator. Without zeroing the child's %eax when
|
||||||
|
%edx != 0, the child sees a non-zero "pid" and every `if fork() = 0 then
|
||||||
|
(child path)` check runs the PARENT branch in the child — so a spawned
|
||||||
|
subprocess never execve's its target and the parent reaps exit 127. Mirror
|
||||||
|
libc: if %edx != 0, this is the child -> return 0. }
|
||||||
function fork: Integer;
|
function fork: Integer;
|
||||||
assembler; nostackframe;
|
assembler; nostackframe;
|
||||||
asm
|
asm
|
||||||
|
|
@ -464,7 +472,12 @@ asm
|
||||||
syscall
|
syscall
|
||||||
jae .Lok_fork
|
jae .Lok_fork
|
||||||
negq %rax
|
negq %rax
|
||||||
|
ret
|
||||||
.Lok_fork:
|
.Lok_fork:
|
||||||
|
testl %edx, %edx { %edx = 1 in the child, 0 in the parent }
|
||||||
|
jz .Lparent_fork
|
||||||
|
xorl %eax, %eax { child: return 0 }
|
||||||
|
.Lparent_fork:
|
||||||
ret
|
ret
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -209,10 +209,15 @@ begin
|
||||||
CB := PThreadCB(CBPtr);
|
CB := PThreadCB(CBPtr);
|
||||||
Entry := CB^.StartRoutine;
|
Entry := CB^.StartRoutine;
|
||||||
CallEntry(Entry, CB^.Arg);
|
CallEntry(Entry, CB^.Arg);
|
||||||
{ Signal completion to any joiner, then terminate this thread only. }
|
{ Terminate this thread AND signal the joiner in one kernel step: pass the
|
||||||
CB^.JoinWord := 1;
|
join word as thr_exit's `state`. The kernel does `suword(state, 1)` +
|
||||||
_umtx_op(@CB^.JoinWord, UMTX_OP_WAKE_PRIVATE, 1, nil, nil);
|
umtx-wake on it AFTER the thread has stopped executing on its stack — so the
|
||||||
thr_exit(nil);
|
joiner cannot free the stack while this thread is still running on it. (The
|
||||||
|
previous code set JoinWord + WAKEd here, on the stack, BEFORE thr_exit, which
|
||||||
|
let pthread_join munmap the shared stack/CB region out from under this still-
|
||||||
|
live thread → SIGSEGV during teardown. Never touch the stack after the wake;
|
||||||
|
let thr_exit's kernel-side wake be the last event on this word.) }
|
||||||
|
thr_exit(@CB^.JoinWord);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ Store an 8-byte value at Base + Offset (used to fill struct thr_param). }
|
{ Store an 8-byte value at Base + Offset (used to fill struct thr_param). }
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,22 @@ uses
|
||||||
|
|
||||||
function FindConfigFile: string;
|
function FindConfigFile: string;
|
||||||
|
|
||||||
|
{ Parse blaise.cfg lines. Recognised keys (KEY=VALUE, '#' comments, blank lines
|
||||||
|
ignored): `unit-path=<dir>` appends to APaths; `rtl-src=<dir>` sets ARtlSrc.
|
||||||
|
A relative VALUE is resolved against ABaseDir (the config file's directory).
|
||||||
|
ARtlSrc is only written when an rtl-src line is present (left unchanged
|
||||||
|
otherwise), so a caller can pre-seed it and let the config override only if
|
||||||
|
set. }
|
||||||
procedure ParseConfigLines(ALines: TStringList; const ABaseDir: string;
|
procedure ParseConfigLines(ALines: TStringList; const ABaseDir: string;
|
||||||
APaths: TStringList);
|
APaths: TStringList; var ARtlSrc: string); overload;
|
||||||
|
|
||||||
procedure LoadConfigPaths(APaths: TStringList);
|
{ Convenience overload for callers that only want unit-paths (ignores rtl-src). }
|
||||||
|
procedure ParseConfigLines(ALines: TStringList; const ABaseDir: string;
|
||||||
|
APaths: TStringList); overload;
|
||||||
|
|
||||||
|
{ Load the resolved blaise.cfg: appends unit-path entries to APaths and returns
|
||||||
|
any rtl-src via ARtlSrc (unchanged if the file has none / no file). }
|
||||||
|
procedure LoadConfigPaths(APaths: TStringList; var ARtlSrc: string);
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
|
|
||||||
|
|
@ -42,7 +54,7 @@ begin
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure ParseConfigLines(ALines: TStringList; const ABaseDir: string;
|
procedure ParseConfigLines(ALines: TStringList; const ABaseDir: string;
|
||||||
APaths: TStringList);
|
APaths: TStringList; var ARtlSrc: string);
|
||||||
var
|
var
|
||||||
I: Integer;
|
I: Integer;
|
||||||
Line: string;
|
Line: string;
|
||||||
|
|
@ -68,12 +80,27 @@ begin
|
||||||
Value := IncludeTrailingPathDelimiter(ABaseDir) + Value;
|
Value := IncludeTrailingPathDelimiter(ABaseDir) + Value;
|
||||||
APaths.Add(Value);
|
APaths.Add(Value);
|
||||||
end
|
end
|
||||||
|
else if SameText(Key, 'rtl-src') then
|
||||||
|
begin
|
||||||
|
if (Length(Value) > 0) and (Copy(Value, 0, 1) <> '/') then
|
||||||
|
Value := IncludeTrailingPathDelimiter(ABaseDir) + Value;
|
||||||
|
ARtlSrc := Value;
|
||||||
|
end
|
||||||
else
|
else
|
||||||
; { silently skip unrecognised keys }
|
; { silently skip unrecognised keys }
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure LoadConfigPaths(APaths: TStringList);
|
procedure ParseConfigLines(ALines: TStringList; const ABaseDir: string;
|
||||||
|
APaths: TStringList);
|
||||||
|
var
|
||||||
|
IgnoredRtlSrc: string;
|
||||||
|
begin
|
||||||
|
IgnoredRtlSrc := '';
|
||||||
|
ParseConfigLines(ALines, ABaseDir, APaths, IgnoredRtlSrc);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure LoadConfigPaths(APaths: TStringList; var ARtlSrc: string);
|
||||||
var
|
var
|
||||||
CfgFile: string;
|
CfgFile: string;
|
||||||
Lines: TStringList;
|
Lines: TStringList;
|
||||||
|
|
@ -86,7 +113,7 @@ begin
|
||||||
try
|
try
|
||||||
Lines.LoadFromFile(CfgFile);
|
Lines.LoadFromFile(CfgFile);
|
||||||
BaseDir := ExtractFilePath(CfgFile);
|
BaseDir := ExtractFilePath(CfgFile);
|
||||||
ParseConfigLines(Lines, BaseDir, APaths);
|
ParseConfigLines(Lines, BaseDir, APaths, ARtlSrc);
|
||||||
finally
|
finally
|
||||||
Lines.Free();
|
Lines.Free();
|
||||||
end;
|
end;
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,8 @@ type
|
||||||
{ Define a conditional-compilation symbol before lexing (e.g. from the
|
{ Define a conditional-compilation symbol before lexing (e.g. from the
|
||||||
-d command-line flag). Case-insensitive. }
|
-d command-line flag). Case-insensitive. }
|
||||||
procedure AddDefine(const ASym: string);
|
procedure AddDefine(const ASym: string);
|
||||||
|
{ Drop the host OS predefines so a cross-target's OS symbol replaces them. }
|
||||||
|
procedure ClearOSDefines;
|
||||||
function Next: TToken;
|
function Next: TToken;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
@ -187,15 +189,29 @@ end;
|
||||||
|
|
||||||
{ Seed the always-on predefined conditional symbols. BLAISE identifies this
|
{ Seed the always-on predefined conditional symbols. BLAISE identifies this
|
||||||
compiler (the headline use case: cross-compiler IFDEF BLAISE / ELSE / ENDIF);
|
compiler (the headline use case: cross-compiler IFDEF BLAISE / ELSE / ENDIF);
|
||||||
the CPU/OS symbols mirror the FPC family so portable source compiles
|
the CPU/OS symbols mirror the FPC family so portable source compiles unchanged.
|
||||||
unchanged. Targets beyond Linux/x86-64 will extend this set. }
|
The OS symbols follow the HOST this compiler was built for, via compile-time
|
||||||
|
conditional compilation, so a FreeBSD-built blaise seeds FREEBSD (matching
|
||||||
|
HostTarget), which is what makes it default to producing FreeBSD binaries.
|
||||||
|
A cross-target does not re-seed these; source that switches on the target OS
|
||||||
|
is rare and the RTL selects per-target units structurally (BuildRTLUnitList),
|
||||||
|
not via conditional compilation. }
|
||||||
procedure TLexer.SeedPredefines;
|
procedure TLexer.SeedPredefines;
|
||||||
begin
|
begin
|
||||||
Self.DefineSymbol('BLAISE');
|
Self.DefineSymbol('BLAISE');
|
||||||
Self.DefineSymbol('CPUX86_64');
|
Self.DefineSymbol('CPUX86_64');
|
||||||
Self.DefineSymbol('CPUAMD64'); { FPC's alias for the same target }
|
Self.DefineSymbol('CPUAMD64'); { FPC's alias for the same target }
|
||||||
|
{$IFDEF FREEBSD}
|
||||||
|
Self.DefineSymbol('FREEBSD');
|
||||||
|
Self.DefineSymbol('UNIX');
|
||||||
|
{$ELSE}
|
||||||
|
{$IFDEF WINDOWS}
|
||||||
|
Self.DefineSymbol('WINDOWS');
|
||||||
|
{$ELSE}
|
||||||
Self.DefineSymbol('LINUX');
|
Self.DefineSymbol('LINUX');
|
||||||
Self.DefineSymbol('UNIX');
|
Self.DefineSymbol('UNIX');
|
||||||
|
{$ENDIF}
|
||||||
|
{$ENDIF}
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TLexer.IsDefined(const ASym: string): Boolean;
|
function TLexer.IsDefined(const ASym: string): Boolean;
|
||||||
|
|
@ -218,6 +234,17 @@ begin
|
||||||
FDefines.Delete(Idx);
|
FDefines.Delete(Idx);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
{ Remove all OS predefines seeded for the host, so a cross --target's OS symbol
|
||||||
|
can take their place (the driver calls this before applying target defines). }
|
||||||
|
procedure TLexer.ClearOSDefines;
|
||||||
|
begin
|
||||||
|
Self.UndefSymbol('LINUX');
|
||||||
|
Self.UndefSymbol('FREEBSD');
|
||||||
|
Self.UndefSymbol('WINDOWS');
|
||||||
|
Self.UndefSymbol('DARWIN');
|
||||||
|
Self.UndefSymbol('UNIX');
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TLexer.AddDefine(const ASym: string);
|
procedure TLexer.AddDefine(const ASym: string);
|
||||||
begin
|
begin
|
||||||
Self.DefineSymbol(UpperCase(ASym));
|
Self.DefineSymbol(UpperCase(ASym));
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,15 @@ type
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
|
|
||||||
|
{ True if ASym is one of the OS conditional-compilation symbols (the target's
|
||||||
|
OS define, injected by the driver, replaces the lexer's host-seeded ones). }
|
||||||
|
function DefineIsOS(const ASym: string): Boolean;
|
||||||
|
begin
|
||||||
|
Result := SameText(ASym, 'LINUX') or SameText(ASym, 'FREEBSD') or
|
||||||
|
SameText(ASym, 'WINDOWS') or SameText(ASym, 'DARWIN') or
|
||||||
|
SameText(ASym, 'UNIX');
|
||||||
|
end;
|
||||||
|
|
||||||
function TUnitLoader.IsBuiltin(const AName: string): Boolean;
|
function TUnitLoader.IsBuiltin(const AName: string): Boolean;
|
||||||
begin
|
begin
|
||||||
Result :=
|
Result :=
|
||||||
|
|
@ -240,16 +249,27 @@ var
|
||||||
L: TLexer;
|
L: TLexer;
|
||||||
P: TParser;
|
P: TParser;
|
||||||
DI: Integer;
|
DI: Integer;
|
||||||
|
HasOS: Boolean;
|
||||||
begin
|
begin
|
||||||
SL := TStringList.Create();
|
SL := TStringList.Create();
|
||||||
try
|
try
|
||||||
SL.LoadFromFile(APath);
|
SL.LoadFromFile(APath);
|
||||||
L := TLexer.Create(SL.Text, APath);
|
L := TLexer.Create(SL.Text, APath);
|
||||||
{ Apply the same -d/--define symbols the main program got, so IFDEF
|
{ Apply the same -d/--define symbols the main program got, so IFDEF
|
||||||
directives resolve consistently across the program and all its units. }
|
directives resolve consistently across the program and all its units.
|
||||||
|
If the set carries an OS symbol (the target's, injected by the driver),
|
||||||
|
drop the lexer's host-seeded OS symbols first so the target wins here too
|
||||||
|
— mirrors AddDefinesTo in Blaise.pas. }
|
||||||
if FDefines <> nil then
|
if FDefines <> nil then
|
||||||
|
begin
|
||||||
|
HasOS := False;
|
||||||
|
for DI := 0 to FDefines.Count - 1 do
|
||||||
|
if DefineIsOS(FDefines.Strings[DI]) then HasOS := True;
|
||||||
|
if HasOS then
|
||||||
|
L.ClearOSDefines();
|
||||||
for DI := 0 to FDefines.Count - 1 do
|
for DI := 0 to FDefines.Count - 1 do
|
||||||
L.AddDefine(FDefines.Strings[DI]);
|
L.AddDefine(FDefines.Strings[DI]);
|
||||||
|
end;
|
||||||
try
|
try
|
||||||
P := TParser.Create(L);
|
P := TParser.Create(L);
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ type
|
||||||
procedure TestParseRelativePath_ResolvedAgainstBaseDir;
|
procedure TestParseRelativePath_ResolvedAgainstBaseDir;
|
||||||
procedure TestParseAbsolutePath_NotModified;
|
procedure TestParseAbsolutePath_NotModified;
|
||||||
procedure TestParseUnknownKey_IgnoredSilently;
|
procedure TestParseUnknownKey_IgnoredSilently;
|
||||||
|
procedure TestParseRtlSrc_Absolute_SetsValue;
|
||||||
|
procedure TestParseRtlSrc_Relative_ResolvedAgainstBaseDir;
|
||||||
|
procedure TestParseRtlSrc_Absent_LeavesSeedUnchanged;
|
||||||
procedure TestFindConfigFile_NextToBinary;
|
procedure TestFindConfigFile_NextToBinary;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
@ -227,6 +230,65 @@ begin
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TConfigTests.TestParseRtlSrc_Absolute_SetsValue;
|
||||||
|
var
|
||||||
|
Lines, Paths: TStringList;
|
||||||
|
Rtl: string;
|
||||||
|
begin
|
||||||
|
Lines := TStringList.Create();
|
||||||
|
Paths := TStringList.Create();
|
||||||
|
try
|
||||||
|
Rtl := '';
|
||||||
|
Lines.Add('rtl-src=/opt/blaise/rtl');
|
||||||
|
ParseConfigLines(Lines, '/base', Paths, Rtl);
|
||||||
|
AssertEquals('rtl-src set', '/opt/blaise/rtl', Rtl);
|
||||||
|
AssertEquals('rtl-src is not a unit-path', 0, Paths.Count);
|
||||||
|
finally
|
||||||
|
Lines.Free();
|
||||||
|
Paths.Free();
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TConfigTests.TestParseRtlSrc_Relative_ResolvedAgainstBaseDir;
|
||||||
|
var
|
||||||
|
Lines, Paths: TStringList;
|
||||||
|
Rtl: string;
|
||||||
|
begin
|
||||||
|
Lines := TStringList.Create();
|
||||||
|
Paths := TStringList.Create();
|
||||||
|
try
|
||||||
|
Rtl := '';
|
||||||
|
Lines.Add('rtl-src=src/main/pascal');
|
||||||
|
ParseConfigLines(Lines, '/base/', Paths, Rtl);
|
||||||
|
AssertEquals('relative rtl-src resolved against base dir',
|
||||||
|
'/base/src/main/pascal', Rtl);
|
||||||
|
finally
|
||||||
|
Lines.Free();
|
||||||
|
Paths.Free();
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TConfigTests.TestParseRtlSrc_Absent_LeavesSeedUnchanged;
|
||||||
|
var
|
||||||
|
Lines, Paths: TStringList;
|
||||||
|
Rtl: string;
|
||||||
|
begin
|
||||||
|
Lines := TStringList.Create();
|
||||||
|
Paths := TStringList.Create();
|
||||||
|
try
|
||||||
|
{ Seed a value (as the driver does with a CLI --rtl-src); a config with no
|
||||||
|
rtl-src line must leave it untouched. }
|
||||||
|
Rtl := '/cli/override';
|
||||||
|
Lines.Add('unit-path=/opt/blaise/runtime');
|
||||||
|
ParseConfigLines(Lines, '/base', Paths, Rtl);
|
||||||
|
AssertEquals('seed unchanged when config has no rtl-src',
|
||||||
|
'/cli/override', Rtl);
|
||||||
|
finally
|
||||||
|
Lines.Free();
|
||||||
|
Paths.Free();
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TConfigTests.TestFindConfigFile_NextToBinary;
|
procedure TConfigTests.TestFindConfigFile_NextToBinary;
|
||||||
var
|
var
|
||||||
CfgPath: string;
|
CfgPath: string;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue