@@ -224,23 +224,32 @@ TMakeRunner = class
224224 FTargetOs: string;
225225 FErrorCount: Integer;
226226 FUseColor: Boolean;
227+ // Selected via MAKE_RUN_BENCHMARK (defaults to False when unset). When true,
228+ // builds and runs console benchmark projects under BenchmarkTargetFolder
229+ // after the test suite completes.
230+ FRunBenchmark: Boolean;
227231 FGraph: TPackageGraph;
228232 function ParseBackendEnv : TBuildBackend;
229233 function ParsePackageScopeEnv : TPackageScope;
234+ function ParseBoolEnv (const AName: string; ADefault: Boolean): Boolean;
230235 function ParseDefinesEnv : TStringList;
231236 procedure InitEnvironment ;
232237 procedure UpdateSubmodules ;
233238 procedure InstallDependencies ;
234239 procedure BuildDiscoveredPackagesFpc ;
235240 function CollectProjectRequiredNames : TStringList;
241+ procedure CollectRequiredNamesFromDirectory (const ADirectory: string;
242+ AResult: TStringList);
236243 procedure BuildAllProjects ;
244+ procedure RunBenchmarkProjects ;
237245 procedure BuildGuiProject (const ALpiPath: string);
238246 function BuildProject (const ALpiPath: string): string;
239247 function BuildProjectWithLazbuild (const APath: string): string;
240248 function BuildProjectWithFpc (const APath: string): string;
241249 function ExtractBinaryFromBuildLog (const AOutput, AFallback: string): string;
242250 function IsGUIProject (const ALpiPath: string): Boolean;
243251 function IsTestProject (const ALpiPath: string): Boolean;
252+ function IsBenchmarkProject (const ALpiPath: string): Boolean;
244253 procedure RunTestProject (const APath: string);
245254 procedure RunSampleProject (const APath: string);
246255 procedure InitSslForDownloads ;
@@ -267,6 +276,7 @@ TMakeRunner = class
267276 AStreamToStderr: Boolean; out AOutput: string): Boolean; overload;
268277 function RepoRoot : string;
269278 function TargetDirectory : string;
279+ function BenchmarkDirectory : string;
270280 procedure ForEachLpkInDir (const ARoot: string; ACallback: TLpkPathProc);
271281 procedure RunBuiltBinary (const ABinaryPath: string;
272282 const AArgs: array of string; const AFailMessage: string);
@@ -295,7 +305,8 @@ TMakeRunner = class
295305// ---------------------------------------------------------------------------
296306
297307const
298- Target: string = ' HashLib.Tests' ;
308+ TestTargetFolder: string = ' HashLib.Tests' ;
309+ BenchmarkTargetFolder: string = ' HashLib.Benchmark' ;
299310
300311 CSI_Reset = #27 ' [0m' ;
301312 CSI_Red = #27 ' [31m' ;
@@ -510,7 +521,8 @@ class function TLazXml.CollectFileSourceDirs(const AContent, APkgDir, ATargetCpu
510521
511522class function TLazXml.ParseCompilerOptions (const AContent: string): TLazCompilerOptions;
512523var
513- Block: string;
524+ Block, AfterProjectOptions: string;
525+ P: Integer;
514526begin
515527 Result.CompilerMode := ' delphi' ;
516528 Result.OptLevel := ' 2' ;
@@ -520,7 +532,18 @@ class function TLazXml.ParseCompilerOptions(const AContent: string): TLazCompile
520532 Result.UnitPaths := ' ' ;
521533 Result.UnitOutputDirTemplate := ' lib\$(TargetCPU)-$(TargetOS)' ;
522534
523- Block := ExtractBlock(AContent, ' CompilerOptions' );
535+ // Lazarus stores the active compiler options in a root-level <CompilerOptions>
536+ // block (after </ProjectOptions>). BuildModes can contain additional
537+ // <CompilerOptions> sections; prefer the root block when present.
538+ Block := ' ' ;
539+ P := Pos(' </ProjectOptions>' , AContent);
540+ if P > 0 then
541+ begin
542+ AfterProjectOptions := Copy(AContent, P, Length(AContent) - P + 1 );
543+ Block := ExtractBlock(AfterProjectOptions, ' CompilerOptions' );
544+ end ;
545+ if Block = ' ' then
546+ Block := ExtractBlock(AContent, ' CompilerOptions' );
524547 if Block = ' ' then
525548 Exit;
526549
@@ -1330,6 +1353,7 @@ constructor TMakeRunner.Create;
13301353 FBackendResolved := False;
13311354 FPackageScope := TPackageScope.Required;
13321355 FErrorCount := 0 ;
1356+ FRunBenchmark := False;
13331357 // Honor the NO_COLOR convention (https://no-color.org): any value disables
13341358 // ANSI colors. GitHub Actions renders ANSI in its log viewer, so default on.
13351359 FUseColor := GetEnvironmentVariable(' NO_COLOR' ) = ' ' ;
@@ -1417,6 +1441,20 @@ function TMakeRunner.ParsePackageScopeEnv: TPackageScope;
14171441 raise Exception.CreateFmt(' unknown MAKE_PACKAGE_SCOPE: "%s"' , [Env]);
14181442end ;
14191443
1444+ function TMakeRunner.ParseBoolEnv (const AName: string; ADefault: Boolean): Boolean;
1445+ var
1446+ Env: string;
1447+ begin
1448+ Env := LowerCase(Trim(GetEnvironmentVariable(AName)));
1449+ if Env = ' ' then
1450+ Exit(ADefault);
1451+ if (Env = ' 1' ) or (Env = ' true' ) or (Env = ' yes' ) then
1452+ Exit(True);
1453+ if (Env = ' 0' ) or (Env = ' false' ) or (Env = ' no' ) then
1454+ Exit(False);
1455+ raise Exception.CreateFmt(' unknown %s: "%s"' , [AName, Env]);
1456+ end ;
1457+
14201458// Parse MAKE_DEFINES into a deduped list of conditional-define names. Accepts a
14211459// space/comma/semicolon-separated list; each entry must be a valid identifier
14221460// ([A-Za-z_][A-Za-z0-9_]*) so it is a safe -d argument in either backend.
@@ -1601,7 +1639,7 @@ function TMakeRunner.RepoRoot: string;
16011639 Candidate := Seeds[I];
16021640 while Candidate <> ' ' do
16031641 begin
1604- DemoDir := IncludeTrailingPathDelimiter(ConcatPaths([Candidate, Target ]));
1642+ DemoDir := IncludeTrailingPathDelimiter(ConcatPaths([Candidate, TestTargetFolder ]));
16051643 if DirectoryExists(DemoDir) then
16061644 Exit(ExcludeTrailingPathDelimiter(Candidate));
16071645 Parent := ExpandFileName(IncludeTrailingPathDelimiter(Candidate) + ' ..' );
@@ -1615,7 +1653,12 @@ function TMakeRunner.RepoRoot: string;
16151653
16161654function TMakeRunner.TargetDirectory : string;
16171655begin
1618- Result := IncludeTrailingPathDelimiter(ConcatPaths([RepoRoot, Target]));
1656+ Result := IncludeTrailingPathDelimiter(ConcatPaths([RepoRoot, TestTargetFolder]));
1657+ end ;
1658+
1659+ function TMakeRunner.BenchmarkDirectory : string;
1660+ begin
1661+ Result := IncludeTrailingPathDelimiter(ConcatPaths([RepoRoot, BenchmarkTargetFolder]));
16191662end ;
16201663
16211664procedure TMakeRunner.ForEachLpkInDir (const ARoot: string;
@@ -1690,6 +1733,12 @@ procedure TMakeRunner.InitEnvironment;
16901733 Log(CSI_Yellow, ' defines: ' + FDefines.DelimitedText)
16911734 else
16921735 Log(CSI_Yellow, ' defines: (none)' );
1736+
1737+ FRunBenchmark := ParseBoolEnv(' MAKE_RUN_BENCHMARK' , False);
1738+ if FRunBenchmark then
1739+ Log(CSI_Yellow, ' run benchmark: true' )
1740+ else
1741+ Log(CSI_Yellow, ' run benchmark: false' );
16931742end ;
16941743
16951744procedure TMakeRunner.UpdateSubmodules ;
@@ -1993,20 +2042,17 @@ procedure TMakeRunner.BuildDiscoveredPackagesFpc;
19932042 end ;
19942043end ;
19952044
1996- // Union of RequiredPackages across the buildable (non-GUI) projects under the
1997- // target directory. Drives the fpc backend's 'required' scope so it compiles
1998- // only the dependency closure those projects need.
1999- function TMakeRunner.CollectProjectRequiredNames : TStringList;
2045+ procedure TMakeRunner.CollectRequiredNamesFromDirectory (const ADirectory: string;
2046+ AResult: TStringList);
20002047var
20012048 List: TStringList;
20022049 Each: string;
20032050 Proj: TLpiProject;
20042051 I: Integer;
20052052begin
2006- Result := TStringList.Create;
2007- Result.Sorted := True;
2008- Result.Duplicates := dupIgnore;
2009- List := TProjectFiles.FindAll(TargetDirectory, ' *.lpi' );
2053+ if not DirectoryExists(ADirectory) then
2054+ Exit;
2055+ List := TProjectFiles.FindAll(ADirectory, ' *.lpi' );
20102056 try
20112057 for Each in List do
20122058 begin
@@ -2016,7 +2062,7 @@ function TMakeRunner.CollectProjectRequiredNames: TStringList;
20162062 try
20172063 if Proj.IsValid then
20182064 for I := 0 to Proj.RequiredPackageNames.Count - 1 do
2019- Result .Add(Proj.RequiredPackageNames[I]);
2065+ AResult .Add(Proj.RequiredPackageNames[I]);
20202066 finally
20212067 Proj.Free;
20222068 end ;
@@ -2026,6 +2072,19 @@ function TMakeRunner.CollectProjectRequiredNames: TStringList;
20262072 end ;
20272073end ;
20282074
2075+ // Union of RequiredPackages across the buildable (non-GUI) projects under the
2076+ // test and (when enabled) benchmark directories. Drives the fpc backend's
2077+ // 'required' scope so it compiles only the dependency closure those projects need.
2078+ function TMakeRunner.CollectProjectRequiredNames : TStringList;
2079+ begin
2080+ Result := TStringList.Create;
2081+ Result.Sorted := True;
2082+ Result.Duplicates := dupIgnore;
2083+ CollectRequiredNamesFromDirectory(TargetDirectory, Result);
2084+ if FRunBenchmark then
2085+ CollectRequiredNamesFromDirectory(BenchmarkDirectory, Result);
2086+ end ;
2087+
20292088function TMakeRunner.ExtractBinaryFromBuildLog (const AOutput,
20302089 AFallback: string): string;
20312090var
@@ -2190,6 +2249,26 @@ function TMakeRunner.IsTestProject(const ALpiPath: string): Boolean;
21902249 Result := Pos(' consoletestrunner' , Content) > 0 ;
21912250end ;
21922251
2252+ function TMakeRunner.IsBenchmarkProject (const ALpiPath: string): Boolean;
2253+ var
2254+ LprPath, Content, ProjectBaseName: string;
2255+ begin
2256+ Result := False;
2257+ if not SameText(ExtractFileExt(ALpiPath), ' .lpi' ) then
2258+ Exit;
2259+
2260+ ProjectBaseName := ChangeFileExt(ExtractFileName(ALpiPath), ' ' );
2261+ if Pos(' BenchmarkConsole' , ProjectBaseName) > 0 then
2262+ Exit(True);
2263+
2264+ LprPath := ChangeFileExt(ALpiPath, ' .lpr' );
2265+ if not FileExists(LprPath) then
2266+ Exit;
2267+
2268+ Content := TLazXml.ReadFile(LprPath);
2269+ Result := Pos(' TPerformanceBenchmark.Run' , Content) > 0 ;
2270+ end ;
2271+
21932272procedure TMakeRunner.RunTestProject (const APath: string);
21942273var
21952274 BinaryPath: string;
@@ -2271,13 +2350,73 @@ procedure TMakeRunner.BuildAllProjects;
22712350 end ;
22722351end ;
22732352
2353+ procedure TMakeRunner.RunBenchmarkProjects ;
2354+ var
2355+ List: TStringList;
2356+ Each, BinaryPath: string;
2357+ begin
2358+ if not FRunBenchmark then
2359+ begin
2360+ Log(CSI_Yellow, ' benchmarks: skipped (MAKE_RUN_BENCHMARK=false)' );
2361+ Exit;
2362+ end ;
2363+
2364+ if not DirectoryExists(BenchmarkDirectory) then
2365+ begin
2366+ IncError;
2367+ Log(CSI_Red, ' benchmark directory missing: ' + BenchmarkDirectory);
2368+ Exit;
2369+ end ;
2370+
2371+ Log(CSI_Cyan, ' using benchmark directory: ' + BenchmarkDirectory);
2372+ List := TProjectFiles.FindAll(BenchmarkDirectory, ' *.lpi' );
2373+ try
2374+ for Each in List do
2375+ begin
2376+ if IsGUIProject(Each) then
2377+ begin
2378+ if not LclSupported then
2379+ begin
2380+ Log(CSI_Yellow, ' skip GUI benchmark project ' + Each);
2381+ Continue;
2382+ end ;
2383+ BuildGuiProject(Each);
2384+ Continue;
2385+ end ;
2386+
2387+ if not IsBenchmarkProject(Each) then
2388+ begin
2389+ Log(CSI_Yellow, ' skip non-benchmark project ' + Each);
2390+ Continue;
2391+ end ;
2392+
2393+ BinaryPath := BuildProject(Each);
2394+ if BinaryPath = ' ' then
2395+ Continue;
2396+ try
2397+ Log(CSI_Yellow, ' run benchmark ' + BinaryPath);
2398+ RunBuiltBinary(BinaryPath, [], ' benchmark failed: ' + BinaryPath);
2399+ except
2400+ on E: Exception do
2401+ begin
2402+ IncError;
2403+ Log(CSI_Red, E.ClassName + ' : ' + E.Message);
2404+ end ;
2405+ end ;
2406+ end ;
2407+ finally
2408+ List.Free;
2409+ end ;
2410+ end ;
2411+
22742412function TMakeRunner.Execute : Integer;
22752413begin
22762414 InitEnvironment;
22772415 Log(CSI_Cyan, ' using target directory: ' + TargetDirectory);
22782416 UpdateSubmodules;
22792417 InstallDependencies;
22802418 BuildAllProjects;
2419+ RunBenchmarkProjects;
22812420 ReportSummary;
22822421 Result := FErrorCount;
22832422end ;
0 commit comments