diff --git a/.github/workflows/make.pas b/.github/workflows/make.pas
index f6612af..d949007 100644
--- a/.github/workflows/make.pas
+++ b/.github/workflows/make.pas
@@ -17,182 +17,329 @@
Target: string = '.';
Dependencies: array of string = ();
-type
- Output = record
- Code: boolean;
- Output: ansistring;
- end;
+ // ANSI color codes
+ CSI_Reset = #27'[0m';
+ CSI_Red = #27'[31m';
+ CSI_Green = #27'[32m';
+ CSI_Yellow = #27'[33m';
+ CSI_Cyan = #27'[36m';
+
+ // Package path filter — skip platform-incompatible and template packages
+ PackageExcludePattern =
+ {$IFDEF MSWINDOWS}
+ '(cocoa|x11|_template)'
+ {$ELSE}
+ '(cocoa|gdi|_template)'
+ {$ENDIF}
+ ;
+
+ OPMBaseUrl = 'https://packages.lazarus-ide.org/';
+
+var
+ ErrorCount: Integer = 0;
+
+// ---------------------------------------------------------------------------
+// Logging helpers
+// ---------------------------------------------------------------------------
+
+procedure Log(const AColor, AMessage: string);
+begin
+ WriteLn(stderr, AColor, AMessage, CSI_Reset);
+end;
+
+procedure LogInline(const AColor, AMessage: string);
+begin
+ Write(stderr, AColor, AMessage, CSI_Reset);
+end;
- function CheckModules: Output;
- begin
- if FileExists('.gitmodules') then
- if RunCommand('git', ['submodule', 'update', '--init', '--recursive',
- '--force', '--remote'], Result.Output) then
- Writeln(stderr, #27'[33m', Result.Output, #27'[0m');
+// ---------------------------------------------------------------------------
+// Git submodules
+// ---------------------------------------------------------------------------
+
+procedure UpdateSubmodules;
+var
+ CommandOutput: ansistring;
+begin
+ if not FileExists('.gitmodules') then
+ Exit;
+ if RunCommand('git', ['submodule', 'update', '--init', '--recursive',
+ '--force', '--remote'], CommandOutput) then
+ Log(CSI_Yellow, Trim(CommandOutput));
+end;
+
+// ---------------------------------------------------------------------------
+// Package registration
+// ---------------------------------------------------------------------------
+
+procedure RegisterPackage(const APath: string);
+var
+ Filter: TRegExpr;
+ CommandOutput: ansistring;
+begin
+ Filter := TRegExpr.Create(PackageExcludePattern);
+ try
+ if Filter.Exec(APath) then
+ Exit;
+ if RunCommand('lazbuild', ['--add-package-link', APath], CommandOutput) then
+ Log(CSI_Yellow, 'added ' + APath);
+ finally
+ Filter.Free;
end;
+end;
+
+// ---------------------------------------------------------------------------
+// Extract linked binary path from lazbuild output
+// ---------------------------------------------------------------------------
- function AddPackage(Path: string): Output;
- begin
- with TRegExpr.Create do
+function ExtractLinkedBinary(const ABuildOutput: string): string;
+var
+ Line: string;
+ Parts: TStringArray;
+begin
+ Result := '';
+ for Line in SplitString(ABuildOutput, LineEnding) do
+ if ContainsStr(Line, 'Linking') then
begin
- Expression :=
- {$IFDEF MSWINDOWS}
- '(cocoa|x11|_template)'
- {$ELSE}
- '(cocoa|gdi|_template)'
- {$ENDIF}
- ;
- if not Exec(Path) and RunCommand('lazbuild', ['--add-package-link', Path],
- Result.Output) then
- Writeln(stderr, #27'[33m', 'added ', Path, #27'[0m');
- Free;
+ Parts := SplitString(Line, ' ');
+ if Length(Parts) >= 3 then
+ Result := Parts[2];
+ Exit;
end;
- end;
+end;
- function BuildProject(Path: string): Output;
- var
- Line: string;
- begin
- Write(stderr, #27'[33m', 'build from ', Path, #27'[0m');
- try
- Result.Code := RunCommand('lazbuild', ['--build-all', '--recursive',
- '--no-write-project', Path], Result.Output);
- if Result.Code then
- for Line in SplitString(Result.Output, LineEnding) do
- begin
- if ContainsStr(Line, 'Linking') then
- begin
- Result.Output := SplitString(Line, ' ')[2];
- Writeln(stderr, #27'[32m', ' to ', Result.Output, #27'[0m');
- break;
- end;
- end
- else
- begin
- ExitCode += 1;
- for Line in SplitString(Result.Output, LineEnding) do
- with TRegExpr.Create do
- begin
- Expression := '(Fatal|Error):';
- if Exec(Line) then
- begin
- WriteLn(stderr);
- Writeln(stderr, #27'[31m', Line, #27'[0m');
- end;
- Free;
- end;
- end;
- except
- on E: Exception do
- WriteLn(stderr, 'Error: ' + E.ClassName + #13#10 + E.Message);
- end;
- end;
+// ---------------------------------------------------------------------------
+// Report build errors from lazbuild output
+// ---------------------------------------------------------------------------
- function RunTest(Path: string): Output;
- var
- Temp: string;
- begin
- Result := BuildProject(Path);
- Temp:= Result.Output;
- if Result.Code then
- try
- if not RunCommand(Temp, ['--all', '--format=plain', '--progress'], Result.Output) then
- ExitCode += 1;
- WriteLn(stderr, Result.Output);
- except
- on E: Exception do
- WriteLn(stderr, 'Error: ' + E.ClassName + #13#10 + E.Message);
- end;
+procedure ReportBuildErrors(const ABuildOutput: string);
+var
+ Line: string;
+ ErrorFilter: TRegExpr;
+begin
+ ErrorFilter := TRegExpr.Create('(Fatal|Error):');
+ try
+ for Line in SplitString(ABuildOutput, LineEnding) do
+ if ErrorFilter.Exec(Line) then
+ Log(CSI_Red, Line);
+ finally
+ ErrorFilter.Free;
end;
+end;
+
+// ---------------------------------------------------------------------------
+// Build a single .lpi project
+// Returns the path to the linked binary on success, empty string on failure
+// ---------------------------------------------------------------------------
- function AddOPM(Each: string): string;
- var
- TempFile, Url: string;
- Zip: TStream;
- begin
- Result :=
- {$IFDEF MSWINDOWS}
- GetEnvironmentVariable('APPDATA') + '\.lazarus\onlinepackagemanager\packages\'
- {$ELSE}
- GetEnvironmentVariable('HOME') + '/.lazarus/onlinepackagemanager/packages/'
- {$ENDIF}
- + Each;
- TempFile := GetTempFileName;
- Url := 'https://packages.lazarus-ide.org/' + Each + '.zip';
- if not DirectoryExists(Result) then
+function BuildProject(const APath: string): string;
+var
+ BuildOutput: string;
+ Success: Boolean;
+begin
+ Result := '';
+ LogInline(CSI_Yellow, 'build from ' + APath);
+ try
+ Success := RunCommand('lazbuild', ['--build-all', '--recursive',
+ '--no-write-project', APath], BuildOutput);
+ if Success then
+ begin
+ Result := ExtractLinkedBinary(BuildOutput);
+ if Result <> '' then
+ Log(CSI_Green, ' -> ' + Result)
+ else
+ WriteLn(stderr);
+ end
+ else
+ begin
+ WriteLn(stderr);
+ Inc(ErrorCount);
+ ReportBuildErrors(BuildOutput);
+ end;
+ except
+ on E: Exception do
begin
- Zip := TFileStream.Create(TempFile, fmCreate or fmOpenWrite);
- with TFPHttpClient.Create(nil) do
- begin
- try
- AddHeader('User-Agent', 'Mozilla/5.0 (compatible; fpweb)');
- AllowRedirect := True;
- Get(Url, Zip);
- WriteLn(stderr, 'Download from ', Url, ' to ', TempFile);
- finally
- Free;
- end;
- end;
- Zip.Free;
- CreateDir(Result);
- with TUnZipper.Create do
- begin
- try
- FileName := TempFile;
- OutputPath := Result;
- Examine;
- UnZipAllFiles;
- WriteLn(stderr, 'Unzip from ', TempFile, ' to ', Result);
- finally
- Free;
- end;
- end;
- DeleteFile(TempFile);
+ WriteLn(stderr);
+ Inc(ErrorCount);
+ Log(CSI_Red, E.ClassName + ': ' + E.Message);
end;
end;
+end;
- function Main: Output;
- var
- Each, Item: string;
- List: TStringList;
- begin
- CheckModules;
- InitSSLInterface;
- for Each in Dependencies do
+// ---------------------------------------------------------------------------
+// Build and run a test project
+// ---------------------------------------------------------------------------
+
+procedure RunTestProject(const APath: string);
+var
+ BinaryPath, TestOutput: string;
+begin
+ BinaryPath := BuildProject(APath);
+ if BinaryPath = '' then
+ Exit;
+ try
+ if RunCommand(BinaryPath, ['--all', '--format=plain', '--progress'],
+ TestOutput) then
+ WriteLn(stderr, TestOutput)
+ else
begin
- List := FindAllFiles(AddOPM(Each), '*.lpk', True);
- try
- for Item in List do
- AddPackage(Item);
- finally
- List.Free;
- end;
+ Inc(ErrorCount);
+ WriteLn(stderr, TestOutput);
end;
- List := FindAllFiles(GetCurrentDir, '*.lpk', True);
- try
- for Each in List do
- AddPackage(Each);
- finally
- List.Free;
+ except
+ on E: Exception do
+ begin
+ Inc(ErrorCount);
+ Log(CSI_Red, E.ClassName + ': ' + E.Message);
end;
- List := FindAllFiles(Target, '*.lpi', True);
+ end;
+end;
+
+// ---------------------------------------------------------------------------
+// OPM dependency installation
+// ---------------------------------------------------------------------------
+
+function GetOPMPackagesDir: string;
+begin
+ Result :=
+ {$IFDEF MSWINDOWS}
+ GetEnvironmentVariable('APPDATA') + '\.lazarus\onlinepackagemanager\packages\'
+ {$ELSE}
+ GetEnvironmentVariable('HOME') + '/.lazarus/onlinepackagemanager/packages/'
+ {$ENDIF}
+ ;
+end;
+
+procedure DownloadAndExtract(const AUrl, ADestDir: string);
+var
+ TempFile: string;
+ Stream: TFileStream;
+ Client: TFPHttpClient;
+ Unzipper: TUnZipper;
+begin
+ TempFile := GetTempFileName;
+ Stream := TFileStream.Create(TempFile, fmCreate or fmOpenWrite);
+ try
+ Client := TFPHttpClient.Create(nil);
try
- for Each in List do
- if ContainsStr(ReadFileToString(ReplaceStr(Each, '.lpi', '.lpr')),
- 'consoletestrunner') then
- RunTest(Each)
- else
- BuildProject(Each);
+ Client.AddHeader('User-Agent', 'Mozilla/5.0 (compatible; fpweb)');
+ Client.AllowRedirect := True;
+ Client.Get(AUrl, Stream);
+ Log(CSI_Cyan, 'downloaded ' + AUrl);
finally
- List.Free;
+ Client.Free;
end;
- WriteLn(stderr);
- if ExitCode <> 0 then
- WriteLn(stderr, #27'[31m', 'Errors: ', ExitCode, #27'[0m')
- else
- WriteLn(stderr, #27'[32m', 'Errors: ', ExitCode, #27'[0m');
+ finally
+ Stream.Free;
end;
+ CreateDir(ADestDir);
+ Unzipper := TUnZipper.Create;
+ try
+ Unzipper.FileName := TempFile;
+ Unzipper.OutputPath := ADestDir;
+ Unzipper.Examine;
+ Unzipper.UnZipAllFiles;
+ Log(CSI_Cyan, 'extracted to ' + ADestDir);
+ finally
+ Unzipper.Free;
+ DeleteFile(TempFile);
+ end;
+end;
+
+function InstallOPMPackage(const APackageName: string): string;
+begin
+ Result := GetOPMPackagesDir + APackageName;
+ if DirectoryExists(Result) then
+ Exit;
+ DownloadAndExtract(OPMBaseUrl + APackageName + '.zip', Result);
+end;
+
+// ---------------------------------------------------------------------------
+// Determine whether an .lpi project is a test runner
+// ---------------------------------------------------------------------------
+
+function IsTestProject(const ALpiPath: string): Boolean;
+var
+ LprPath, Content: string;
+begin
+ Result := False;
+ LprPath := ChangeFileExt(ALpiPath, '.lpr');
+ if not FileExists(LprPath) then
+ Exit;
+ Content := ReadFileToString(LprPath);
+ Result := ContainsStr(Content, 'consoletestrunner');
+end;
+
+// ---------------------------------------------------------------------------
+// Register all .lpk packages found under a directory
+// ---------------------------------------------------------------------------
+
+procedure RegisterAllPackages(const ASearchDir: string);
+var
+ List: TStringList;
+ Each: string;
+begin
+ List := FindAllFiles(ASearchDir, '*.lpk', True);
+ try
+ for Each in List do
+ RegisterPackage(Each);
+ finally
+ List.Free;
+ end;
+end;
+
+// ---------------------------------------------------------------------------
+// Build (and optionally test) all .lpi projects found under Target
+// ---------------------------------------------------------------------------
+
+procedure BuildAllProjects;
+var
+ List: TStringList;
+ Each: string;
+begin
+ List := FindAllFiles(Target, '*.lpi', True);
+ try
+ for Each in List do
+ if IsTestProject(Each) then
+ RunTestProject(Each)
+ else
+ BuildProject(Each);
+ finally
+ List.Free;
+ end;
+end;
+
+// ---------------------------------------------------------------------------
+// Entry point
+// ---------------------------------------------------------------------------
+
+procedure Main;
+var
+ Each: string;
+begin
+ UpdateSubmodules;
+ InitSSLInterface;
+
+ // Install and register OPM dependencies
+ for Each in Dependencies do
+ RegisterAllPackages(InstallOPMPackage(Each));
+
+ // Register all local packages
+ RegisterAllPackages(GetCurrentDir);
+
+ // Build and test
+ BuildAllProjects;
+
+ // Summary
+ WriteLn(stderr);
+ if ErrorCount > 0 then
+ Log(CSI_Red, 'Errors: ' + IntToStr(ErrorCount))
+ else
+ Log(CSI_Green, 'Errors: 0');
+
+ ExitCode := ErrorCount;
+end;
+
begin
Main;
-end.
+end.
\ No newline at end of file
diff --git a/.github/workflows/make.yml b/.github/workflows/make.yml
index a02cde4..caa6810 100644
--- a/.github/workflows/make.yml
+++ b/.github/workflows/make.yml
@@ -23,6 +23,7 @@ jobs:
matrix:
os:
- ubuntu-latest
+ - ubuntu-24.04-arm
- windows-latest
steps:
@@ -31,8 +32,16 @@ jobs:
with:
submodules: true
- - name: Build on Linux
- if: runner.os == 'Linux'
+ - name: Build on Linux (x86_64)
+ if: runner.os == 'Linux' && runner.arch == 'X64'
+ shell: bash
+ run: |
+ set -xeuo pipefail
+ sudo bash -c 'apt-get update; apt-get install -y lazarus' >/dev/null
+ instantfpc -Fu/usr/lib/lazarus/*/components/lazutils .github/workflows/make.pas
+
+ - name: Build on Linux (AArch64)
+ if: runner.os == 'Linux' && runner.arch == 'ARM64'
shell: bash
run: |
set -xeuo pipefail
@@ -63,4 +72,4 @@ jobs:
Get-Command instantfpc
Write-Host "Building make.pas..."
- instantfpc '-FuC:\Lazarus\components\lazutils' .github/workflows/make.pas
+ instantfpc '-FuC:\Lazarus\components\lazutils' .github/workflows/make.pas
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 46ab4dd..38f0c15 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,7 +50,7 @@
*.ocx
# Delphi autogenerated files (duplicated info)
-*.cfg
+#*.cfg
*.hpp
*Resource.rc
diff --git a/BuildConfig/FPC/SimpleBaseLibFPCMessages.cfg b/BuildConfig/FPC/SimpleBaseLibFPCMessages.cfg
new file mode 100644
index 0000000..6673319
--- /dev/null
+++ b/BuildConfig/FPC/SimpleBaseLibFPCMessages.cfg
@@ -0,0 +1,34 @@
+# Customize messages displayed by FPC.
+#
+# FPC emits some messages that are unfortunately useless and unavoidable
+# in user code.
+# So we hide them, by using proper -vmXXX options.
+
+# do not show Warning: APPTYPE is not supported by the target OS
+-vm2045
+
+# Warning: An inherited method is hidden by ... (generics.defaults)
+-vm3057
+
+# Warning: Constructor should be public
+-vm3018
+
+# Warning: function result variable of a managed type does not seem to be initialized (generics.defaults)
+-vm5093
+
+# Note: Call to subroutine marked as inline is not inlined (generics.collections)
+-vm6058
+
+#IFDEF VER3_2
+# Warning: Constructing a class with abstract method (generics; FPC 3.2 RTL)
+-vm4046
+
+# Note: Private type never used (generics.collections; FPC 3.2 RTL)
+-vm5071
+#ENDIF
+
+# Hint: Found abstract method (generics)
+-vm5062
+
+# Hint: Parameter "AFrom"/"ATo" not used (generics.dictionariesh.inc)
+-vm5024
diff --git a/SimpleBaseLib.Tests/FreePascal.Tests/SimpleBaseLib.Tests.lpi b/SimpleBaseLib.Tests/FreePascal.Tests/SimpleBaseLib.Tests.lpi
index e44ab67..a908a26 100644
--- a/SimpleBaseLib.Tests/FreePascal.Tests/SimpleBaseLib.Tests.lpi
+++ b/SimpleBaseLib.Tests/FreePascal.Tests/SimpleBaseLib.Tests.lpi
@@ -189,11 +189,6 @@
-
-
-
-
-
diff --git a/SimpleBaseLib.Tests/FreePascal.Tests/SimpleBaseLibConsole.lpr b/SimpleBaseLib.Tests/FreePascal.Tests/SimpleBaseLibConsole.lpr
index cdf97f2..500906d 100644
--- a/SimpleBaseLib.Tests/FreePascal.Tests/SimpleBaseLibConsole.lpr
+++ b/SimpleBaseLib.Tests/FreePascal.Tests/SimpleBaseLibConsole.lpr
@@ -3,6 +3,7 @@
{$mode objfpc}{$H+}
uses
+ {$IFDEF UNIX}cwstring,{$ENDIF}
consoletestrunner, SimpleBaseLibTestBase, Base36Tests, Base10Tests,
Base16StreamRegressionTests, Base16Tests, Base2StreamRegressionTests,
Base2Tests, Base32Tests, Base32StreamRegressionTests, Base32AlphabetTests,
diff --git a/SimpleBaseLib.Tests/src/Multibase/MultibaseTests.pas b/SimpleBaseLib.Tests/src/Multibase/MultibaseTests.pas
index bb2644a..b98d4cd 100644
--- a/SimpleBaseLib.Tests/src/Multibase/MultibaseTests.pas
+++ b/SimpleBaseLib.Tests/src/Multibase/MultibaseTests.pas
@@ -286,6 +286,7 @@ procedure TTestMultibase.Test_TryDecode_DecodesCorrectly;
LI, LBytesWritten: Int32;
LBytes: TSimpleBaseLibByteArray;
begin
+ LBytes := nil;
SetLength(LBytes, 1024);
for LI := Low(Encoded) to High(Encoded) do
begin