diff --git a/compiler/src/main/pascal/Blaise.pas b/compiler/src/main/pascal/Blaise.pas index 63517f1..cb9a4c8 100644 --- a/compiler/src/main/pascal/Blaise.pas +++ b/compiler/src/main/pascal/Blaise.pas @@ -502,6 +502,7 @@ var any ExportUnitInterface bugs against real codebases. } I: Integer; + J: Integer; IR: string; IRFile: string; LinkErr: string; { Driver.LinkProgram result ('' on success) } @@ -1055,6 +1056,22 @@ begin if PrebuiltObjPaths.IndexOf(Loader.LinkOnlyObjects.Strings[I]) < 0 then PrebuiltObjPaths.Add(Loader.LinkOnlyObjects.Strings[I]); end; + { Capture link-library deps (-l) off the program AST and every used + unit's interface before Prog/Loader are freed below — the link step runs + after this finally block. Unioned into Opts.LinkLibs; the driver emits one + -l each, additive to the RTL's -lm/-lpthread. } + if Opts.LinkLibs = nil then Opts.LinkLibs := TStringList.Create(); + if (Prog <> nil) and (Prog.LinkLibs <> nil) then + for I := 0 to Prog.LinkLibs.Count - 1 do + if Opts.LinkLibs.IndexOf(TLinkLibDecl(Prog.LinkLibs.Items[I]).LibName) < 0 then + Opts.LinkLibs.Add(TLinkLibDecl(Prog.LinkLibs.Items[I]).LibName); + if Loader <> nil then + for I := 0 to Loader.PrebuiltIfaces.Count - 1 do + for J := 0 to TUnitInterface(Loader.PrebuiltIfaces.Items[I]).LinkLibs.Count - 1 do + if Opts.LinkLibs.IndexOf( + TUnitInterface(Loader.PrebuiltIfaces.Items[I]).LinkLibs.Strings[J]) < 0 then + Opts.LinkLibs.Add( + TUnitInterface(Loader.PrebuiltIfaces.Items[I]).LinkLibs.Strings[J]); Units.Free(); Loader.Free(); SearchPaths.Free(); diff --git a/compiler/src/main/pascal/blaise.codegen.driver.pas b/compiler/src/main/pascal/blaise.codegen.driver.pas index 36693c4..8734c71 100644 --- a/compiler/src/main/pascal/blaise.codegen.driver.pas +++ b/compiler/src/main/pascal/blaise.codegen.driver.pas @@ -98,6 +98,11 @@ type runtime.start.static.) replaces libc; the linker emits a non-PIE ET_EXEC with no PT_INTERP. docs/linux-syscall-migration.adoc } + LinkLibs: TStringList; { extra libraries to link (-l), unioned by + the frontend from the program's and its used + units' 'external ''lib''' declarations + (TUnitInterface.LinkLibs). nil = none. Owned; + ARC-released with the opts object. } end; TBackendDriver = class @@ -631,6 +636,12 @@ begin Args.Add(RTLObjs.Strings[I]); Args.Add('-lm'); { math functions (sqrt, sin, cos, etc.) } Args.Add('-lpthread'); { POSIX threads (blaise_thread unit) } + { Libraries declared via 'external ''lib''' in the program or any used unit + (unioned by the frontend into AOpts.LinkLibs). Emitted as -l — ld + expands to lib.so/.a. Additive to the always-needed -lm/-lpthread. } + if AOpts.LinkLibs <> nil then + for I := 0 to AOpts.LinkLibs.Count - 1 do + Args.Add('-l' + AOpts.LinkLibs.Strings[I]); ExitCode := RunProcess(Self.ToolPath('linker', AOpts.Target), Args, Msg); finally RTLObjs.Free(); diff --git a/compiler/src/main/pascal/blaise.codegen.native.driver.pas b/compiler/src/main/pascal/blaise.codegen.native.driver.pas index 2ffa773..9b9e898 100644 --- a/compiler/src/main/pascal/blaise.codegen.native.driver.pas +++ b/compiler/src/main/pascal/blaise.codegen.native.driver.pas @@ -197,6 +197,16 @@ var begin Result := ''; + { The internal linker links only Blaise ELF objects (+ the source-built RTL); + it has no concept of -l system libraries. A program that declares an + `external 'lib'` dependency therefore cannot be linked this way — fail loudly + rather than silently drop the library and emit a binary with unresolved + symbols. Use the external toolchain linker (the default) for such programs. } + if (AOpts.LinkLibs <> nil) and (AOpts.LinkLibs.Count > 0) then + Exit('internal linker cannot resolve external library ''' + + AOpts.LinkLibs.Strings[0] + + ''' (from an ''external ''''lib'''''' declaration); use --linker external'); + { 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 this the internal linker always produced a Linux-shaped ELF regardless of diff --git a/compiler/src/test/pascal/cp.test.cli.pas b/compiler/src/test/pascal/cp.test.cli.pas index d47979e..5f66447 100644 --- a/compiler/src/test/pascal/cp.test.cli.pas +++ b/compiler/src/test/pascal/cp.test.cli.pas @@ -75,6 +75,14 @@ type procedure TestEmitAsm_WithExplicitQbeBackend_Rejected; procedure TestEmitIr_WithoutBackend_StillWorks; procedure TestEmitIr_WithExplicitQbeBackend_StillWorks; + { ---- external 'lib' propagates a -l to the real link command ---- } + { A program declaring `external 'lib'` for a NON-EXISTENT library must fail + the link with the linker's "cannot find -l" — which only happens if + the driver actually emitted -l. The e2e harness can't test this (it + links with a hardcoded `cc ... -lm -lpthread` and never calls + LinkViaToolchain), so this drives the real compiler binary end to end. } + procedure TestExternalLib_MissingLib_FailsLink_QBE; + procedure TestExternalLib_MissingLib_FailsLink_Native; { ---- div/mod by zero raises a catchable EDivByZero (needs stdlib) ---- } procedure TestDivByZeroCaught_QBE; procedure TestDivByZeroCaught_Native; @@ -557,6 +565,52 @@ const ' end' + LineEnding + 'end.'; +const + { Declares an external from a library that does not exist. The function is + never called, but the parser collects the library into Prog.LinkLibs at + parse time, so the driver must still emit -lnosuchlib_blaise_xyz on the link + line — which the system linker then rejects. If the -l propagation + were broken (no flag emitted), the link would SUCCEED. } + SrcMissingExternalLib = + 'program P;' + LineEnding + + 'function f(X: Integer): Integer;' + + ' external ''nosuchlib_blaise_xyz'' name ''nope'';' + LineEnding + + 'begin' + LineEnding + + ' WriteLn(''unreached'')' + LineEnding + + 'end.'; + +procedure TCLIContractTests.TestExternalLib_MissingLib_FailsLink_QBE; +var SrcPath, BinPath, Out_: string; EC: Integer; +begin + if not CompilerAvailable() then begin Ignore(''); Exit; end; + SrcPath := WriteScratchSource(SrcMissingExternalLib); + BinPath := FScratch + 'cli_misslib_qbe_' + IntToStr(FCounter); + EC := RunCompiler(['--source', SrcPath, '--backend', 'qbe', + '--unit-path', FRTLPath, '--unit-path', FStdlibPath, + '--output', BinPath], Out_); + AssertTrue('link must fail (missing library)', EC <> 0); + AssertTrue('linker reports the -l it could not find', + Pos('nosuchlib_blaise_xyz', Out_) >= 0); +end; + +procedure TCLIContractTests.TestExternalLib_MissingLib_FailsLink_Native; +var SrcPath, BinPath, Out_: string; EC: Integer; +begin + if not CompilerAvailable() then begin Ignore(''); Exit; end; + SrcPath := WriteScratchSource(SrcMissingExternalLib); + BinPath := FScratch + 'cli_misslib_nat_' + IntToStr(FCounter); + { --linker external: the internal linker cannot resolve -l system + libraries (it errors out separately); the toolchain linker is what emits + and consumes -l. } + EC := RunCompiler(['--source', SrcPath, '--backend', 'native', + '--linker', 'external', + '--unit-path', FRTLPath, '--unit-path', FStdlibPath, + '--output', BinPath], Out_); + AssertTrue('link must fail (missing library)', EC <> 0); + AssertTrue('linker reports the -l it could not find', + Pos('nosuchlib_blaise_xyz', Out_) >= 0); +end; + procedure TCLIContractTests.TestDivByZeroCaught_QBE; var Out_: string; EC: Integer; begin