diff --git a/.github/workflows/ci/native-build.sh b/.github/workflows/ci/native-build.sh index 73e0f53..cf8f641 100644 --- a/.github/workflows/ci/native-build.sh +++ b/.github/workflows/ci/native-build.sh @@ -6,4 +6,5 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/shared/common.sh" ci_init_paths ci_openssl_hack +ci_install_lcl_gui_deps ci_build_standard diff --git a/.github/workflows/ci/shared/common.sh b/.github/workflows/ci/shared/common.sh index 4d3d5b4..f0ed6d6 100644 --- a/.github/workflows/ci/shared/common.sh +++ b/.github/workflows/ci/shared/common.sh @@ -166,6 +166,19 @@ ci_run_make() { "$build_dir/make" } +# The lazbuild backend builds LCL/GUI projects as a compile-check (the fpc +# backend skips them). On Linux those link against the GTK2 widgetset, whose dev +# libraries are not preinstalled on the GitHub runners, so the GUI link fails. +# Install them here. No-op on the fpc backend and off Linux (macOS/Windows ship +# their native widgetset). GUI projects are built but never run. +ci_install_lcl_gui_deps() { + [ "${MAKE_BUILD_BACKEND:-}" = "lazbuild" ] || return 0 + [ "$(uname -s)" = "Linux" ] || return 0 + echo "installing GTK2 dev libraries for LCL GUI project build (lazbuild/Linux)" >&2 + sudo apt-get update + sudo apt-get install -y libgtk2.0-dev +} + ci_build_standard() { ci_install_toolchain ci_export_toolchain_path diff --git a/.github/workflows/make.pas b/.github/workflows/make.pas index 1d09a2a..f7f16be 100644 --- a/.github/workflows/make.pas +++ b/.github/workflows/make.pas @@ -91,11 +91,10 @@ TLazXml = class const AStubFileName, AUnitOutDir: string); class procedure AppendProjectBuildArgs(AArgs: TStrings; const AMainSource, AUnitOutDir, ATargetBinary: string); - // Conditional defines, formatted per backend: fpc takes -dNAME directly on - // the argv (must precede the source); lazbuild forwards them to the compiler - // via repeatable --opt=-dNAME (its own -d means --skip-dependencies). + // Conditional defines for the fpc backend: -dNAME on the argv (must precede + // the source). The lazbuild backend instead injects defines via fpc.cfg (see + // TMakeRunner.InjectDefinesIntoFpcConfig) so recursive package builds see them. class procedure AppendFpcDefineArgs(const ADefines: TStrings; AArgs: TStrings); - class procedure AppendLazbuildDefineArgs(const ADefines: TStrings; AArgs: TStrings); private class function IsAbsolutePath(const S: string): Boolean; class function ExpandMacros(const S, AProjDir, AUnitOutDir, APkgOutDir, @@ -228,7 +227,16 @@ TMakeRunner = class // builds and runs console benchmark projects under BenchmarkTargetFolder // after the test suite completes. FRunBenchmark: Boolean; + // Selected via MAKE_LAZBUILD_GUI (defaults to True). Only meaningful under + // the lazbuild backend - the fpc backend never builds GUI projects (it has + // no widgetset). When False, LCL/GUI projects are skipped even under + // lazbuild, letting a run opt out of the per-platform widgetset toolchain + // the GUI link needs. + FBuildGuiProjects: Boolean; FGraph: TPackageGraph; + // Non-empty while a MAKE_DEFINES block is appended to this fpc.cfg (lazbuild + // backend only); RestoreFpcConfig strips it back out. + FInjectedFpcCfgPath: string; function ParseBackendEnv: TBuildBackend; function ParsePackageScopeEnv: TPackageScope; function ParseBoolEnv(const AName: string; ADefault: Boolean): Boolean; @@ -243,6 +251,7 @@ TMakeRunner = class procedure BuildAllProjects; procedure RunBenchmarkProjects; procedure BuildGuiProject(const ALpiPath: string); + function TryHandleGuiProject(const ALpiPath, ASkipLabel: string): Boolean; function BuildProject(const ALpiPath: string): string; function BuildProjectWithLazbuild(const APath: string): string; function BuildProjectWithFpc(const APath: string): string; @@ -284,6 +293,9 @@ TMakeRunner = class function RunFpcInfoProbeWithRetry(const AInfoFlag: string; out AValue: string): Boolean; procedure PrepareProjectBuild(Proj: TLpiProject); + function LocateFpcConfig: string; + procedure InjectDefinesIntoFpcConfig; + procedure RestoreFpcConfig; public constructor Create; destructor Destroy; override; @@ -317,6 +329,12 @@ TMakeRunner = class OPMBaseUrl = 'https://packages.lazarus-ide.org/'; GitHubArchiveBaseUrl = 'https://github.com/'; + // Delimiters for the MAKE_DEFINES block appended to fpc.cfg under the lazbuild + // backend (see InjectDefinesIntoFpcConfig). '#'-prefixed lines are fpc.cfg + // comments, so a leftover block is inert. + FpcCfgMarkerBegin = '#--- make.pas MAKE_DEFINES begin (auto-generated) ---'; + FpcCfgMarkerEnd = '#--- make.pas MAKE_DEFINES end (auto-generated) ---'; + Dependencies: array of TDependency = ( // Examples: // (Kind: TDependencyKind.OPM; Name: 'SimpleBaseLib'; Ref: ''), @@ -685,17 +703,6 @@ class procedure TLazXml.AppendFpcDefineArgs(const ADefines: TStrings; AArgs.Add('-d' + ADefines[I]); end; -class procedure TLazXml.AppendLazbuildDefineArgs(const ADefines: TStrings; - AArgs: TStrings); -var - I: Integer; -begin - if not Assigned(ADefines) then - Exit; - for I := 0 to ADefines.Count - 1 do - AArgs.Add('--opt=-d' + ADefines[I]); -end; - // --------------------------------------------------------------------------- // TProjectFiles // --------------------------------------------------------------------------- @@ -1354,6 +1361,7 @@ constructor TMakeRunner.Create; FPackageScope := TPackageScope.Required; FErrorCount := 0; FRunBenchmark := False; + FBuildGuiProjects := True; // Honor the NO_COLOR convention (https://no-color.org): any value disables // ANSI colors. GitHub Actions renders ANSI in its log viewer, so default on. FUseColor := GetEnvironmentVariable('NO_COLOR') = ''; @@ -1626,6 +1634,119 @@ function TMakeRunner.RunFpcInfoProbeWithRetry(const AInfoFlag: string; Result := False; end; +// Return the path of the main fpc.cfg the toolchain reads, by parsing the +// config trace `fpc -va` prints (it echoes each config file as it reads it). +// `fpc -va` exits non-zero here because no source is given, but RunCommandEx +// still captures the trace. Returns '' if no config path can be found. +function TMakeRunner.LocateFpcConfig: string; +var + Output, Line, Low, Cand: string; + Lines: TStringList; + I, P: Integer; +begin + Result := ''; + RunCommandEx('fpc', ['-va'], '', False, Output); + Lines := TStringList.Create; + try + Lines.Text := Output; + for I := 0 to Lines.Count - 1 do + begin + Line := Lines[I]; + Low := LowerCase(Line); + // Only trace lines that name a config file; skip option-echo lines. + if (Pos('config file', Low) = 0) and (Pos('options from file', Low) = 0) then + Continue; + P := Pos('file ', Low); + if P = 0 then + Continue; + Cand := Trim(Copy(Line, P + Length('file '), MaxInt)); + if (Length(Cand) >= 4) and + (LowerCase(Copy(Cand, Length(Cand) - 3, 4)) = '.cfg') then + Exit(Cand); // first config read = the main fpc.cfg + end; + finally + Lines.Free; + end; +end; + +// lazbuild applies `--opt` compiler options to the top-level project only; its +// `--recursive` rebuild of required packages uses each package's own options, +// so MAKE_DEFINES never reaches the dependency packages that way (the fpc +// backend avoids this by compiling each package with -d directly). To match it, +// append the defines to fpc.cfg so every fpc invocation lazbuild spawns - the +// project and every recursive package build - inherits them. No-op on the fpc +// backend (which already injects per compile) and when no defines are set. +procedure TMakeRunner.InjectDefinesIntoFpcConfig; +var + Cfg: string; + SL: TStringList; + I: Integer; +begin + FInjectedFpcCfgPath := ''; + if (not UsesLazbuild) or (FDefines.Count = 0) then + Exit; + + Cfg := LocateFpcConfig; + if (Cfg = '') or (not FileExists(Cfg)) then + raise Exception.Create( + 'lazbuild backend with MAKE_DEFINES set, but fpc.cfg could not be located ' + + 'to propagate the defines to package builds. Refusing to continue, since ' + + 'the defines would silently not reach the dependency packages. ' + + 'Use MAKE_BUILD_BACKEND=fpc or make fpc.cfg discoverable via `fpc -va`.'); + + SL := TStringList.Create; + try + SL.LoadFromFile(Cfg); + SL.Add(FpcCfgMarkerBegin); + for I := 0 to FDefines.Count - 1 do + SL.Add('-d' + FDefines[I]); + SL.Add(FpcCfgMarkerEnd); + SL.SaveToFile(Cfg); + finally + SL.Free; + end; + FInjectedFpcCfgPath := Cfg; + Log(CSI_Yellow, Format('injected %d define(s) into %s (lazbuild package propagation)', + [FDefines.Count, Cfg])); +end; + +// Strip the block InjectDefinesIntoFpcConfig appended, restoring fpc.cfg. +procedure TMakeRunner.RestoreFpcConfig; +var + SL, Kept: TStringList; + I: Integer; + InBlock: Boolean; +begin + if (FInjectedFpcCfgPath = '') or (not FileExists(FInjectedFpcCfgPath)) then + Exit; + SL := TStringList.Create; + Kept := TStringList.Create; + try + SL.LoadFromFile(FInjectedFpcCfgPath); + InBlock := False; + for I := 0 to SL.Count - 1 do + begin + if SL[I] = FpcCfgMarkerBegin then + begin + InBlock := True; + Continue; + end; + if SL[I] = FpcCfgMarkerEnd then + begin + InBlock := False; + Continue; + end; + if not InBlock then + Kept.Add(SL[I]); + end; + Kept.SaveToFile(FInjectedFpcCfgPath); + finally + Kept.Free; + SL.Free; + end; + FInjectedFpcCfgPath := ''; +end; + function TMakeRunner.RepoRoot: string; var Seeds: array[0..1] of string; @@ -1739,6 +1860,12 @@ procedure TMakeRunner.InitEnvironment; Log(CSI_Yellow, 'run benchmark: true') else Log(CSI_Yellow, 'run benchmark: false'); + + FBuildGuiProjects := ParseBoolEnv('MAKE_LAZBUILD_GUI', True); + if FBuildGuiProjects then + Log(CSI_Yellow, 'build GUI projects (lazbuild): true') + else + Log(CSI_Yellow, 'build GUI projects (lazbuild): false'); end; procedure TMakeRunner.UpdateSubmodules; @@ -1934,9 +2061,11 @@ procedure TMakeRunner.RegisterAllPackagesLazbuild(const ASearchDir: string); ForEachLpkInDir(ASearchDir, @RegisterPackageLazbuild); end; -// Assemble a lazbuild argv: base flags, then the configured defines as -// --opt=-dNAME, then the target path. Centralizes define injection so project -// and package builds stay consistent. +// Assemble a lazbuild argv: base flags, then the target path. MAKE_DEFINES are +// NOT passed here as --opt: lazbuild would apply them to the top-level target +// only, not to the packages it rebuilds via --recursive. They are injected into +// fpc.cfg instead (see InjectDefinesIntoFpcConfig) so every spawned fpc compile +// - project and recursive package builds - picks them up. function TMakeRunner.LazbuildArgs(const ABaseFlags: array of string; const APath: string): TStringList; var @@ -1945,7 +2074,6 @@ function TMakeRunner.LazbuildArgs(const ABaseFlags: array of string; Result := TStringList.Create; for Flag in ABaseFlags do Result.Add(Flag); - TLazXml.AppendLazbuildDefineArgs(FDefines, Result); Result.Add(APath); end; @@ -2318,6 +2446,23 @@ procedure TMakeRunner.BuildGuiProject(const ALpiPath: string); Log(CSI_Green, 'built GUI project (not run) ' + BinaryPath); end; +// Handle a GUI (LCL) project: build it (compile-check only - GUI apps are never +// run, they need a display) when the lazbuild backend is active AND GUI building +// is enabled (MAKE_LAZBUILD_GUI); otherwise skip it - the fpc backend has no +// widgetset, and MAKE_LAZBUILD_GUI=false opts lazbuild out too. Returns True if +// ALpiPath was a GUI project, so the caller stops its normal processing for it. +// ASkipLabel is the log prefix used when skipping (e.g. 'skip GUI project '). +function TMakeRunner.TryHandleGuiProject(const ALpiPath, ASkipLabel: string): Boolean; +begin + Result := IsGUIProject(ALpiPath); + if not Result then + Exit; + if LclSupported and FBuildGuiProjects then + BuildGuiProject(ALpiPath) + else + Log(CSI_Yellow, ASkipLabel + ALpiPath); +end; + procedure TMakeRunner.BuildAllProjects; var List: TStringList; @@ -2327,18 +2472,8 @@ procedure TMakeRunner.BuildAllProjects; try for Each in List do begin - if IsGUIProject(Each) then - begin - if not LclSupported then - begin - Log(CSI_Yellow, 'skip GUI project ' + Each); - Continue; - end; - // lazbuild backend: the full LCL is present, so compile the GUI project - // to verify it builds. It is not run because GUI apps need a display. - BuildGuiProject(Each); + if TryHandleGuiProject(Each, 'skip GUI project ') then Continue; - end; if IsTestProject(Each) then RunTestProject(Each) @@ -2373,16 +2508,8 @@ procedure TMakeRunner.RunBenchmarkProjects; try for Each in List do begin - if IsGUIProject(Each) then - begin - if not LclSupported then - begin - Log(CSI_Yellow, 'skip GUI benchmark project ' + Each); - Continue; - end; - BuildGuiProject(Each); + if TryHandleGuiProject(Each, 'skip GUI benchmark project ') then Continue; - end; if not IsBenchmarkProject(Each) then begin @@ -2414,9 +2541,14 @@ function TMakeRunner.Execute: Integer; InitEnvironment; Log(CSI_Cyan, 'using target directory: ' + TargetDirectory); UpdateSubmodules; - InstallDependencies; - BuildAllProjects; - RunBenchmarkProjects; + InjectDefinesIntoFpcConfig; + try + InstallDependencies; + BuildAllProjects; + RunBenchmarkProjects; + finally + RestoreFpcConfig; + end; ReportSummary; Result := FErrorCount; end; diff --git a/.github/workflows/make.yml b/.github/workflows/make.yml index fb98381..96232f2 100644 --- a/.github/workflows/make.yml +++ b/.github/workflows/make.yml @@ -38,8 +38,9 @@ env: # a package that fails on the target even if unused); 'required' only compiles # the projects' dependency closure. Override per job below, like MAKE_BUILD_BACKEND. MAKE_PACKAGE_SCOPE: required - # Space/comma/semicolon-separated conditional defines passed to every compile in - # both backends (fpc: -dNAME, lazbuild: --opt=-dNAME). Override per job as needed. + # Space/comma/semicolon-separated conditional defines passed to every compile. + # fpc backend: -dNAME per compile. lazbuild backend: injected into fpc.cfg so + # the project and every recursive package rebuild inherit them. Override per job. # Examples: # single: MAKE_DEFINES: WITH_GTK2_IM # multiple: MAKE_DEFINES: 'WITH_GTK2_IM DEBUG_LOG USE_CTHREADS' @@ -47,6 +48,11 @@ env: # When true, make.pas builds and runs console benchmarks after the test suite. # Override per job below, like MAKE_BUILD_BACKEND. MAKE_RUN_BENCHMARK: false + # When true, make.pas builds (never runs) LCL/GUI projects. Only meaningful under + # the lazbuild backend - the fpc backend has no widgetset and always skips them. + # Set false to opt out of the per-platform GUI widgetset toolchain (GTK2/X11 on + # Unix, cocoa on macOS). Override per job below, like MAKE_BUILD_BACKEND. + MAKE_LAZBUILD_GUI: false # Set by the workflow_dispatch "debug" toggle; '1' enables `set -x` tracing in # install-fpc-lazarus.sh, '0' otherwise. Forwarded into the sandboxed jobs below. CI_DEBUG: ${{ github.event.inputs.debug == 'true' && '1' || '0' }} @@ -239,6 +245,7 @@ jobs: MAKE_BUILD_BACKEND: ${{ env.MAKE_BUILD_BACKEND }} MAKE_PACKAGE_SCOPE: ${{ env.MAKE_PACKAGE_SCOPE }} MAKE_RUN_BENCHMARK: ${{ env.MAKE_RUN_BENCHMARK }} + MAKE_LAZBUILD_GUI: ${{ env.MAKE_LAZBUILD_GUI }} CI_DEBUG: ${{ env.CI_DEBUG }} run: bash .github/workflows/ci/arm32-run.sh @@ -265,6 +272,7 @@ jobs: MAKE_BUILD_BACKEND: ${{ env.MAKE_BUILD_BACKEND }} MAKE_PACKAGE_SCOPE: ${{ env.MAKE_PACKAGE_SCOPE }} MAKE_RUN_BENCHMARK: ${{ env.MAKE_RUN_BENCHMARK }} + MAKE_LAZBUILD_GUI: ${{ env.MAKE_LAZBUILD_GUI }} run: bash .github/workflows/ci/ppc64-be-build.sh freebsd: @@ -286,7 +294,7 @@ jobs: FPC_TARBALL_TARGET: ${{ fromJSON(needs.setup.outputs.target_map)['freebsd'].fpc_tarball_target }} FREEBSD_INSTALL_MODE: interim with: - envs: FPC_VERSION FPC_TARGET FPC_TARBALL_TARGET LAZARUS_BRANCH LAZARUS_REPO MAKE_BUILD_BACKEND MAKE_PACKAGE_SCOPE MAKE_RUN_BENCHMARK FREEBSD_INSTALL_MODE CI_DEBUG + envs: FPC_VERSION FPC_TARGET FPC_TARBALL_TARGET LAZARUS_BRANCH LAZARUS_REPO MAKE_BUILD_BACKEND MAKE_PACKAGE_SCOPE MAKE_RUN_BENCHMARK MAKE_LAZBUILD_GUI FREEBSD_INSTALL_MODE CI_DEBUG release: "15.0" usesh: true prepare: sh .github/workflows/ci/vm-freebsd-prepare.sh @@ -310,7 +318,7 @@ jobs: FPC_TARGET: ${{ fromJSON(needs.setup.outputs.target_map)['netbsd'].fpc_target }} FPC_TARBALL_TARGET: ${{ fromJSON(needs.setup.outputs.target_map)['netbsd'].fpc_tarball_target }} with: - envs: FPC_VERSION LAZARUS_BRANCH LAZARUS_REPO MAKE_BUILD_BACKEND MAKE_PACKAGE_SCOPE MAKE_RUN_BENCHMARK FPC_TARGET FPC_TARBALL_TARGET CI_DEBUG + envs: FPC_VERSION LAZARUS_BRANCH LAZARUS_REPO MAKE_BUILD_BACKEND MAKE_PACKAGE_SCOPE MAKE_RUN_BENCHMARK MAKE_LAZBUILD_GUI FPC_TARGET FPC_TARBALL_TARGET CI_DEBUG prepare: sh .github/workflows/ci/vm-netbsd-prepare.sh run: bash .github/workflows/ci/vm-run-shared.sh @@ -333,7 +341,7 @@ jobs: FPC_TARBALL_TARGET: ${{ fromJSON(needs.setup.outputs.target_map)['dragonflybsd'].fpc_tarball_target }} LD_LIBRARY_PATH_EXTRA: /usr/local/lib with: - envs: FPC_VERSION LAZARUS_BRANCH LAZARUS_REPO MAKE_BUILD_BACKEND MAKE_PACKAGE_SCOPE MAKE_RUN_BENCHMARK FPC_TARGET FPC_TARBALL_TARGET LD_LIBRARY_PATH_EXTRA CI_DEBUG + envs: FPC_VERSION LAZARUS_BRANCH LAZARUS_REPO MAKE_BUILD_BACKEND MAKE_PACKAGE_SCOPE MAKE_RUN_BENCHMARK MAKE_LAZBUILD_GUI FPC_TARGET FPC_TARBALL_TARGET LD_LIBRARY_PATH_EXTRA CI_DEBUG usesh: true prepare: sh .github/workflows/ci/vm-dragonfly-prepare.sh run: bash .github/workflows/ci/vm-run-shared.sh @@ -356,7 +364,7 @@ jobs: FPC_TARGET: ${{ fromJSON(needs.setup.outputs.target_map)['solaris'].fpc_target }} FPC_TARBALL_TARGET: ${{ fromJSON(needs.setup.outputs.target_map)['solaris'].fpc_tarball_target }} with: - envs: FPC_VERSION LAZARUS_BRANCH LAZARUS_REPO MAKE_BUILD_BACKEND MAKE_PACKAGE_SCOPE MAKE_RUN_BENCHMARK FPC_TARGET FPC_TARBALL_TARGET CI_DEBUG + envs: FPC_VERSION LAZARUS_BRANCH LAZARUS_REPO MAKE_BUILD_BACKEND MAKE_PACKAGE_SCOPE MAKE_RUN_BENCHMARK MAKE_LAZBUILD_GUI FPC_TARGET FPC_TARBALL_TARGET CI_DEBUG release: "11.4-gcc" usesh: true prepare: sh .github/workflows/ci/vm-solaris-prepare.sh