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