From ebc6d2a176ca564aaeebb8624f03981b917b25cd Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Tue, 2 Jun 2026 01:04:38 -0500 Subject: [PATCH 01/16] chore(windows): Delphi 11/12 source compatibility Lets the existing tree compile under Delphi 11 (VER350) and Delphi 12 (VER360) without breaking Delphi 10.3 (VER330). CI is unchanged: with KEYMAN_DELPHI_VERSION unset, every build script keeps defaulting to Delphi 10.3 (BDS 20.0) exactly as before. This is a bridge to lower contributor friction on #4599 (deprecate Delphi). The 10.3 build server can also be upgraded to a paid Delphi 11/12 license on top of these patches if/when that's wanted. Build-script knob: * resources/build/win/configure_environment.inc.sh, resources/build/win/delphi_environment.inc.sh: DELPHI_VERSION now reads ${KEYMAN_DELPHI_VERSION:-20.0}. * resources/builder.inc.sh: builder_describe_platform's delphi tool detection respects the same KEYMAN_DELPHI_VERSION default, so machines with only Delphi 12 installed no longer have win,delphi- gated targets silently skipped. Keyman-owned source compat (additive VER350/VER360 arms; VER330 paths untouched): * common/windows/delphi/tools/devtools/SourceRootPath.pas: teach DelphiMajorVersion about BDS 22.0 / 23.0 install dirs. * common/windows/delphi/web/Keyman.System.HttpServer.Base.pas: extend the IFNDEF VER330 tripwire to accept VER340/350/360. * common/windows/delphi/components/FixedTrackbar.pas: same tripwire extension. * common/windows/delphi/general/CleartypeDrawCharacter.pas: EnumFontFamiliesEx integer-return comparison on VER340/350/360 (was VER340-only). * common/windows/delphi/general/JsonUtil.pas: pass [] options arg to TJSONAncestor.ToChars on VER350/360 (Delphi 11+ added the parameter). Vendored third-party patches (each hunk annotated with a "Keyman local patch" comment plus refresh strategy): * developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas: wrap Boolean args to CreateEvent / OpenEvent / CreateWaitableTimer / OpenWaitableTimer / OpenSemaphore / CreateMutex / OpenMutex in explicit BOOL() casts (Delphi 12 tightened implicit Boolean->BOOL at call sites); switch JclWin32.CreateMutex -> Winapi.Windows.CreateMutex to match the file's existing pattern for the other Winapi calls. * developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas: on VER350+, unconditionally call DoCreate (Embarcadero removed the OldCreateOrder property in Delphi 11; modern behavior is equivalent to OldCreateOrder=True). * developer/src/ext/mbcolor/mxs.inc: add VER350/VER360 blocks defining DELPHI_5_UP through DELPHI_10_UP. Without these, HTMLColors.pas silently drops Variants from its uses clause and breaks with "Undeclared identifier: 'null'". .gitignore: add patterns for per-arch version*.res and meson wraplock files. Relates-to: #4599 --- .gitignore | 4 ++++ .../delphi/components/FixedTrackbar.pas | 4 ++++ .../delphi/general/CleartypeDrawCharacter.pas | 4 ++-- common/windows/delphi/general/JsonUtil.pas | 4 ++++ .../delphi/tools/devtools/SourceRootPath.pas | 8 +++++++ .../web/Keyman.System.HttpServer.Base.pas | 6 +++++ .../jedi/jcl/jcl/source/common/JclSynch.pas | 18 +++++++++------ .../ext/jedi/jvcl/jvcl/run/JvComponent.pas | 8 +++++++ developer/src/ext/mbcolor/mxs.inc | 22 +++++++++++++++++++ .../build/win/configure_environment.inc.sh | 6 +++-- resources/build/win/delphi_environment.inc.sh | 5 ++++- resources/builder.inc.sh | 3 ++- 12 files changed, 79 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 7b872195880..cd9499f6c1a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ /windows/src/**/*.identcache /windows/src/**/*.vcxproj.user /windows/src/**/version.res +/windows/src/**/version*.res /windows/src/**/*.pch /windows/src/**/*.wixobj /windows/src/**/*.sbr @@ -186,3 +187,6 @@ lcov.info # flag file for build script .configured + +# Meson wrap dependency lock files +**/subprojects/.wraplock diff --git a/common/windows/delphi/components/FixedTrackbar.pas b/common/windows/delphi/components/FixedTrackbar.pas index 50cb444a55f..8d3c525d842 100644 --- a/common/windows/delphi/components/FixedTrackbar.pas +++ b/common/windows/delphi/components/FixedTrackbar.pas @@ -70,10 +70,14 @@ procedure TTntFixedDrawGrid.WMEraseBkgnd(var Message: TMessage); {$MESSAGE WARN 'Not yet checked against Delphi 10.4'} {$IFNDEF VER330} {$IFNDEF VER320} +{$IFNDEF VER350} +{$IFNDEF VER360} {$MESSAGE ERROR 'Check that this fix is still applicable for a new version of Delphi. Checked against Delphi 10.2, 10.3' } {$ENDIF} {$ENDIF} {$ENDIF} +{$ENDIF} +{$ENDIF} procedure TTntFixedDrawGrid.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); diff --git a/common/windows/delphi/general/CleartypeDrawCharacter.pas b/common/windows/delphi/general/CleartypeDrawCharacter.pas index 6d1a51976de..44562012ed2 100644 --- a/common/windows/delphi/general/CleartypeDrawCharacter.pas +++ b/common/windows/delphi/general/CleartypeDrawCharacter.pas @@ -591,11 +591,11 @@ function TestFont(FFontName: string): Boolean; StrPCopy(lf.lfFaceName, FFontName); //'Code2000'); hdc := GetDC(0); //FPlane0FontName := 'Code2000'; -{$IFDEF VER340} +{$IF Defined(VER340) or Defined(VER350) or Defined(VER360)} if EnumFontFamiliesEx(hdc, lf, @EnumFallbackFonts, 0, 0) <> 0 then {$ELSE} if EnumFontFamiliesEx(hdc, lf, @EnumFallbackFonts, 0, 0) then -{$ENDIF} +{$IFEND} begin FPlane0FontName := FFontName; Result := True; diff --git a/common/windows/delphi/general/JsonUtil.pas b/common/windows/delphi/general/JsonUtil.pas index ae2b9b0e9c6..16a90bc729d 100644 --- a/common/windows/delphi/general/JsonUtil.pas +++ b/common/windows/delphi/general/JsonUtil.pas @@ -52,7 +52,11 @@ function JSONToString(obj: TJSONAncestor; ReplaceSlashes: Boolean = False): stri begin builder := TStringBuilder.Create; try +{$IF Defined(VER350) or Defined(VER360)} + obj.ToChars(builder, []); +{$ELSE} obj.ToChars(builder); +{$IFEND} Result := builder.ToString; finally builder.Free; diff --git a/common/windows/delphi/tools/devtools/SourceRootPath.pas b/common/windows/delphi/tools/devtools/SourceRootPath.pas index 360477641e7..c8372c3c6e2 100644 --- a/common/windows/delphi/tools/devtools/SourceRootPath.pas +++ b/common/windows/delphi/tools/devtools/SourceRootPath.pas @@ -14,11 +14,19 @@ interface {$IFDEF VER340} const DelphiMajorVersion = '21.0'; {$ELSE} +{$IFDEF VER350} +const DelphiMajorVersion = '22.0'; +{$ELSE} +{$IFDEF VER360} +const DelphiMajorVersion = '23.0'; +{$ELSE} ERROR: must define Delphi version {$ENDIF} {$ENDIF} {$ENDIF} {$ENDIF} +{$ENDIF} +{$ENDIF} const DelphiBasePath = 'C:\Program Files (x86)\Embarcadero\Studio\' + DelphiMajorVersion + '\'; diff --git a/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas b/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas index d1daf087c39..8df4e732056 100644 --- a/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas +++ b/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas @@ -46,7 +46,13 @@ function CrackUTF8ZeroExtendedString(CommandType: THTTPCommandType; const p: str // Indy's UTF8 handling of URLs is *completely* broken. // We may need to check this with updated versions of Delphi {$IFNDEF VER330} +{$IFNDEF VER340} +{$IFNDEF VER350} +{$IFNDEF VER360} ERROR! Check if this is still needed with Delphi update +{$ENDIF} +{$ENDIF} +{$ENDIF} {$ENDIF} SetLength(s, p.Length); diff --git a/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas b/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas index f73b1722b09..eb405224249 100644 --- a/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas +++ b/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas @@ -937,11 +937,15 @@ function TJclCriticalSectionEx.TryEnter: Boolean; //== { TJclEvent } =========================================================== +// Keyman local patch: Delphi 11/12 compat (vendored JCL). Explicit BOOL() casts +// added below to satisfy Delphi 12's stricter implicit-Boolean->BOOL conversion. +// Backwards-compatible with Delphi 10.3 (BOOL is an ordinal-preserving typecast). +// On JCL refresh from upstream: re-apply if upstream hasn't picked up the cast. constructor TJclEvent.Create(SecAttr: PSecurityAttributes; Manual, Signaled: Boolean; const Name: string); begin inherited Create; FName := Name; - FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.CreateEvent(SecAttr, Manual, Signaled, PChar(FName)); + FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.CreateEvent(SecAttr, BOOL(Manual), BOOL(Signaled), PChar(FName)); if FHandle = 0 then raise EJclEventError.CreateRes(@RsSynchCreateEvent); FExisted := GetLastError = ERROR_ALREADY_EXISTS; @@ -952,7 +956,7 @@ constructor TJclEvent.Open(Access: Cardinal; Inheritable: Boolean; begin FName := Name; FExisted := True; - FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenEvent(Access, Inheritable, PChar(Name)); + FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenEvent(Access, BOOL(Inheritable), PChar(Name)); if FHandle = 0 then raise EJclEventError.CreateRes(@RsSynchOpenEvent); end; @@ -980,7 +984,7 @@ constructor TJclWaitableTimer.Create(SecAttr: PSecurityAttributes; begin FName := Name; FResume := False; - FHandle := CreateWaitableTimer(SecAttr, Manual, PChar(Name)); + FHandle := CreateWaitableTimer(SecAttr, BOOL(Manual), PChar(Name)); if FHandle = 0 then raise EJclWaitableTimerError.CreateRes(@RsSynchCreateWaitableTimer); FExisted := GetLastError = ERROR_ALREADY_EXISTS; @@ -1000,7 +1004,7 @@ constructor TJclWaitableTimer.Open(Access: Cardinal; Inheritable: Boolean; FExisted := True; FName := Name; FResume := False; - FHandle := OpenWaitableTimer(Access, Inheritable, PChar(Name)); + FHandle := OpenWaitableTimer(Access, BOOL(Inheritable), PChar(Name)); if FHandle = 0 then raise EJclWaitableTimerError.CreateRes(@RsSynchOpenWaitableTimer); end; @@ -1048,7 +1052,7 @@ constructor TJclSemaphore.Open(Access: Cardinal; Inheritable: Boolean; begin FName := Name; FExisted := True; - FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenSemaphore(Access, Inheritable, PChar(Name)); + FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenSemaphore(Access, BOOL(Inheritable), PChar(Name)); if FHandle = 0 then raise EJclSemaphoreError.CreateRes(@RsSynchOpenSemaphore); end; @@ -1075,7 +1079,7 @@ constructor TJclMutex.Create(SecAttr: PSecurityAttributes; InitialOwner: Boolean begin inherited Create; FName := Name; - FHandle := JclWin32.CreateMutex(SecAttr, InitialOwner, PChar(Name)); + FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.CreateMutex(SecAttr, BOOL(InitialOwner), PChar(Name)); if FHandle = 0 then raise EJclMutexError.CreateRes(@RsSynchCreateMutex); FExisted := GetLastError = ERROR_ALREADY_EXISTS; @@ -1086,7 +1090,7 @@ constructor TJclMutex.Open(Access: Cardinal; Inheritable: Boolean; const Name: s inherited Create; FName := Name; FExisted := True; - FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenMutex(Access, Inheritable, PChar(Name)); + FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenMutex(Access, BOOL(Inheritable), PChar(Name)); if FHandle = 0 then raise EJclMutexError.CreateRes(@RsSynchOpenMutex); end; diff --git a/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas b/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas index 1877ee48efe..fab8609e2c8 100644 --- a/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas +++ b/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas @@ -123,8 +123,16 @@ constructor TJvForm.Create(AOwner: TComponent); finally Exclude(FFormState, fsCreating); end; + // Keyman local patch: Delphi 11/12 compat (vendored JVCL). The + // OldCreateOrder property was removed in Delphi 11; the modern semantics + // are equivalent to OldCreateOrder=True, so always call DoCreate on + // VER350+. On JVCL refresh from upstream: re-apply if not yet upstreamed. +{$IF Defined(VER350) or Defined(VER360)} + DoCreate; +{$ELSE} if OldCreateOrder then DoCreate; +{$IFEND} end; finally GlobalNameSpace.EndWrite; diff --git a/developer/src/ext/mbcolor/mxs.inc b/developer/src/ext/mbcolor/mxs.inc index 41b82ad96e5..75ca0b34e73 100644 --- a/developer/src/ext/mbcolor/mxs.inc +++ b/developer/src/ext/mbcolor/mxs.inc @@ -7,6 +7,28 @@ {$define DELPHI_10_UP} {$endif} + // Keyman local patch: Delphi 11/12 compat (vendored mbcolor). Without + // these blocks DELPHI_*_UP flags are never defined on VER350/VER360, + // which causes HTMLColors.pas to drop "uses Variants" and fail to + // resolve the Null constant. On mbcolor refresh: re-apply if needed. + {$ifdef VER350} + {$define DELPHI_5_UP} + {$define DELPHI_6_UP} + {$define DELPHI_7_UP} + {$define DELPHI_8_UP} + {$define DELPHI_9_UP} + {$define DELPHI_10_UP} + {$endif} + + {$ifdef VER360} + {$define DELPHI_5_UP} + {$define DELPHI_6_UP} + {$define DELPHI_7_UP} + {$define DELPHI_8_UP} + {$define DELPHI_9_UP} + {$define DELPHI_10_UP} + {$endif} + {$ifdef VER330} {$define DELPHI_5_UP} {$define DELPHI_6_UP} diff --git a/resources/build/win/configure_environment.inc.sh b/resources/build/win/configure_environment.inc.sh index d0bce72a812..1d274c67be1 100644 --- a/resources/build/win/configure_environment.inc.sh +++ b/resources/build/win/configure_environment.inc.sh @@ -65,9 +65,11 @@ _build_vs_environment() { _locate_rsvars() { # - # Delphi Compiler Configuration - Delphi 10.3.2 + # Delphi Compiler Configuration - defaults to Delphi 10.3 (BDS 20.0). + # Override via the KEYMAN_DELPHI_VERSION environment variable to build with + # a newer Delphi installation (e.g., KEYMAN_DELPHI_VERSION=23.0 for Delphi 12). # - DELPHI_VERSION=20.0 + DELPHI_VERSION="${KEYMAN_DELPHI_VERSION:-20.0}" DCC32PATH="$(cygpath -u "$ProgramFilesx86\\Embarcadero\\Studio\\$DELPHI_VERSION\\bin")" RSVars_path="$DCC32PATH/rsvars.bat" } diff --git a/resources/build/win/delphi_environment.inc.sh b/resources/build/win/delphi_environment.inc.sh index 0465bcb6930..a68b08421c4 100644 --- a/resources/build/win/delphi_environment.inc.sh +++ b/resources/build/win/delphi_environment.inc.sh @@ -13,7 +13,10 @@ DELPHIWARNINGS=(-W-MESSAGE_DIRECTIVE -W-IMPLICIT_STRING_CAST -W-IMPLICIT_STRING_CAST_LOSS -W-EXPLICIT_STRING_CAST -W-EXPLICIT_STRING_CAST_LOSS -W-CVT_WCHAR_TO_ACHAR -W-CVT_NARROWING_STRING_LOST -W-CVT_ACHAR_TO_WCHAR -W-CVT_WIDENING_STRING_LOST -W-UNICODE_TO_LOCALE -W-LOCALE_TO_UNICODE -W-IMPLICIT_VARIANTS -W-IMPLICIT_INTEGER_CAST_LOSS -W-IMPLICIT_CONVERSION_LOSS -W-COMBINING_SIGNED_UNSIGNED64 -W-COMBINING_SIGNED_UNSIGNED64) # !ENDIF -DELPHI_VERSION=20.0 +# DELPHI_VERSION defaults to Delphi 10.3 (BDS 20.0). Override via the +# KEYMAN_DELPHI_VERSION environment variable to build with a newer Delphi +# installation (e.g., KEYMAN_DELPHI_VERSION=23.0 for Delphi 12). +DELPHI_VERSION="${KEYMAN_DELPHI_VERSION:-20.0}" DCC32PATH="$(cygpath -u "$ProgramFilesx86\\Embarcadero\\Studio\\$DELPHI_VERSION\\bin")" source "$KEYMAN_ROOT/resources/build/win/delphi_environment_generated.inc.sh" diff --git a/resources/builder.inc.sh b/resources/builder.inc.sh index 6ec2fbcebff..7e9ea36a0e4 100755 --- a/resources/builder.inc.sh +++ b/resources/builder.inc.sh @@ -2323,7 +2323,8 @@ builder_describe_platform() { # Detect delphi compiler (see also delphi_environment.inc.sh) if builder_is_windows; then local ProgramFilesx86="$(cygpath -w -F 42)" - if [[ -x "$(cygpath -u "$ProgramFilesx86\\Embarcadero\\Studio\\20.0\\bin\\dcc32.exe")" ]]; then + local _delphi_version="${KEYMAN_DELPHI_VERSION:-20.0}" + if [[ -x "$(cygpath -u "$ProgramFilesx86\\Embarcadero\\Studio\\$_delphi_version\\bin\\dcc32.exe")" ]]; then builder_installed_tools+=(delphi) fi fi From a57500f57912c796e6ddae14c40b57fd78eefa53 Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Thu, 2 Jul 2026 09:58:49 -0500 Subject: [PATCH 02/16] docs(windows): surface KEYMAN_DELPHI_VERSION in windows.md Pro-tier Delphi 11/12 users had no way to discover the env-var knob introduced by this PR - it was documented only in the PR body and in the CE-workflow doc from #16044. Add a short paragraph to the Delphi requirements section pointing at the two Studio path values and reaffirming that the unset default preserves CI behavior. Co-Authored-By: Claude Opus 4.7 --- docs/build/windows.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/build/windows.md b/docs/build/windows.md index edc756340ff..a3b4ce73983 100644 --- a/docs/build/windows.md +++ b/docs/build/windows.md @@ -257,6 +257,12 @@ of appropriate node versions during builds. for a short time. (We are actively working to remove Delphi dependencies given the licensing issues with using it.) + * If you have Delphi 11 or 12 with a CLI-capable license (Professional or + higher), set `KEYMAN_DELPHI_VERSION` to the Studio path — `22.0` for + Delphi 11, `23.0` for Delphi 12 — before running `build.sh`. The default + (`20.0`, Delphi 10.3) is preserved when the variable is unset, so CI is + unaffected. + Start Delphi IDE once after installation as it will create various environment files and take you through required registration. From b611e81d7192905228e1500ff91f6c9178ff7432 Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Thu, 2 Jul 2026 10:08:34 -0500 Subject: [PATCH 03/16] revert: drop unnecessary JclSynch.pas Boolean/BOOL cast patches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per @mcdurdin's review: he built with unmodified JclSynch.pas and verified the Delphi 12 compile succeeds without the BOOL casts. The patches were defensive against a compile failure I never actually observed on this file — I assumed Delphi 12's tightened implicit-Boolean handling required them without empirically checking. Removing. If a real compile failure on JclSynch shows up later, address it then with a specific error to fix rather than a preemptive patch. Co-Authored-By: Claude Opus 4.7 --- .../jedi/jcl/jcl/source/common/JclSynch.pas | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas b/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas index eb405224249..f73b1722b09 100644 --- a/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas +++ b/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas @@ -937,15 +937,11 @@ function TJclCriticalSectionEx.TryEnter: Boolean; //== { TJclEvent } =========================================================== -// Keyman local patch: Delphi 11/12 compat (vendored JCL). Explicit BOOL() casts -// added below to satisfy Delphi 12's stricter implicit-Boolean->BOOL conversion. -// Backwards-compatible with Delphi 10.3 (BOOL is an ordinal-preserving typecast). -// On JCL refresh from upstream: re-apply if upstream hasn't picked up the cast. constructor TJclEvent.Create(SecAttr: PSecurityAttributes; Manual, Signaled: Boolean; const Name: string); begin inherited Create; FName := Name; - FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.CreateEvent(SecAttr, BOOL(Manual), BOOL(Signaled), PChar(FName)); + FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.CreateEvent(SecAttr, Manual, Signaled, PChar(FName)); if FHandle = 0 then raise EJclEventError.CreateRes(@RsSynchCreateEvent); FExisted := GetLastError = ERROR_ALREADY_EXISTS; @@ -956,7 +952,7 @@ constructor TJclEvent.Open(Access: Cardinal; Inheritable: Boolean; begin FName := Name; FExisted := True; - FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenEvent(Access, BOOL(Inheritable), PChar(Name)); + FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenEvent(Access, Inheritable, PChar(Name)); if FHandle = 0 then raise EJclEventError.CreateRes(@RsSynchOpenEvent); end; @@ -984,7 +980,7 @@ constructor TJclWaitableTimer.Create(SecAttr: PSecurityAttributes; begin FName := Name; FResume := False; - FHandle := CreateWaitableTimer(SecAttr, BOOL(Manual), PChar(Name)); + FHandle := CreateWaitableTimer(SecAttr, Manual, PChar(Name)); if FHandle = 0 then raise EJclWaitableTimerError.CreateRes(@RsSynchCreateWaitableTimer); FExisted := GetLastError = ERROR_ALREADY_EXISTS; @@ -1004,7 +1000,7 @@ constructor TJclWaitableTimer.Open(Access: Cardinal; Inheritable: Boolean; FExisted := True; FName := Name; FResume := False; - FHandle := OpenWaitableTimer(Access, BOOL(Inheritable), PChar(Name)); + FHandle := OpenWaitableTimer(Access, Inheritable, PChar(Name)); if FHandle = 0 then raise EJclWaitableTimerError.CreateRes(@RsSynchOpenWaitableTimer); end; @@ -1052,7 +1048,7 @@ constructor TJclSemaphore.Open(Access: Cardinal; Inheritable: Boolean; begin FName := Name; FExisted := True; - FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenSemaphore(Access, BOOL(Inheritable), PChar(Name)); + FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenSemaphore(Access, Inheritable, PChar(Name)); if FHandle = 0 then raise EJclSemaphoreError.CreateRes(@RsSynchOpenSemaphore); end; @@ -1079,7 +1075,7 @@ constructor TJclMutex.Create(SecAttr: PSecurityAttributes; InitialOwner: Boolean begin inherited Create; FName := Name; - FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.CreateMutex(SecAttr, BOOL(InitialOwner), PChar(Name)); + FHandle := JclWin32.CreateMutex(SecAttr, InitialOwner, PChar(Name)); if FHandle = 0 then raise EJclMutexError.CreateRes(@RsSynchCreateMutex); FExisted := GetLastError = ERROR_ALREADY_EXISTS; @@ -1090,7 +1086,7 @@ constructor TJclMutex.Open(Access: Cardinal; Inheritable: Boolean; const Name: s inherited Create; FName := Name; FExisted := True; - FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenMutex(Access, BOOL(Inheritable), PChar(Name)); + FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenMutex(Access, Inheritable, PChar(Name)); if FHandle = 0 then raise EJclMutexError.CreateRes(@RsSynchOpenMutex); end; From d7b3784f0715310a382e0192c1ad73350d18356b Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Thu, 2 Jul 2026 10:09:02 -0500 Subject: [PATCH 04/16] revert: drop JvComponent.pas OldCreateOrder patch pending verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per @mcdurdin's review: the OldCreateOrder-was-removed premise disagrees with the Athens documentation he linked (https://docwiki.embarcadero.com/Libraries/Athens/en/Vcl.Forms.TForm.OldCreateOrder). Given JclSynch.pas turned out to be an unnecessary defensive patch, this one deserves the same skepticism. Reverting until a specific Delphi 12 compile failure on JVCL surfaces that motivates a targeted fix. The remaining vendored ext patch (mbcolor/mxs.inc) stays because it produces a concrete E2003 Undeclared identifier: 'null' at HTMLColors.pas:290 on Delphi 12 — reproducible, not defensive. Co-Authored-By: Claude Opus 4.7 --- developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas | 8 -------- 1 file changed, 8 deletions(-) diff --git a/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas b/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas index fab8609e2c8..1877ee48efe 100644 --- a/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas +++ b/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas @@ -123,16 +123,8 @@ constructor TJvForm.Create(AOwner: TComponent); finally Exclude(FFormState, fsCreating); end; - // Keyman local patch: Delphi 11/12 compat (vendored JVCL). The - // OldCreateOrder property was removed in Delphi 11; the modern semantics - // are equivalent to OldCreateOrder=True, so always call DoCreate on - // VER350+. On JVCL refresh from upstream: re-apply if not yet upstreamed. -{$IF Defined(VER350) or Defined(VER360)} - DoCreate; -{$ELSE} if OldCreateOrder then DoCreate; -{$IFEND} end; finally GlobalNameSpace.EndWrite; From 9d53e7715ef4ebf4ced99932b5320b6f22b3135a Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Thu, 2 Jul 2026 11:20:02 -0500 Subject: [PATCH 05/16] address remaining review points: tripwire acknowledgements, scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FixedTrackbar.pas: extend the existing 'Tested on' comment block to note VER350/VER360 IFNDEF arms are added without full verification against Vcl.Grids.pas — same self-documentation pattern @mcdurdin used when he added the 10.3 attestation line in 2019. - HttpServer.Base.pas: extend the pre-existing "may need to check" comment to name the newly-silenced tripwire arms and record that Indy's URL handling on 10.4/11/12 has not been re-verified. The workaround stays applied conservatively rather than removed optimistically. - Drop `.wraplock` .gitignore rule — Meson subproject artifact, no connection to Delphi 11/12 source compat. Scope drift. - windows.md: trim the "so CI is unaffected" tail from the KEYMAN_DELPHI_VERSION paragraph. CI default is a separate concern from the env-var default. Motivated by the same "no unverified defensive extensions" concern that led me to revert the JclSynch and JvComponent patches earlier. Co-Authored-By: Claude Opus 4.7 --- .gitignore | 3 --- common/windows/delphi/components/FixedTrackbar.pas | 2 ++ common/windows/delphi/web/Keyman.System.HttpServer.Base.pas | 6 +++++- docs/build/windows.md | 3 +-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index cd9499f6c1a..dd7d41d3709 100644 --- a/.gitignore +++ b/.gitignore @@ -187,6 +187,3 @@ lcov.info # flag file for build script .configured - -# Meson wrap dependency lock files -**/subprojects/.wraplock diff --git a/common/windows/delphi/components/FixedTrackbar.pas b/common/windows/delphi/components/FixedTrackbar.pas index 8d3c525d842..9e900c2c0d9 100644 --- a/common/windows/delphi/components/FixedTrackbar.pas +++ b/common/windows/delphi/components/FixedTrackbar.pas @@ -64,6 +64,8 @@ procedure TTntFixedDrawGrid.WMEraseBkgnd(var Message: TMessage); Tested on VER320 (10.2) Tested on VER330 (10.3) - 29 Oct 2019 - mcdurdin + Not yet fully verified against Vcl.Grids.pas in VER350 (11) or VER360 (12); + IFNDEF arms added to unblock compilation, override behaviour retained as-is. } {$IFNDEF VER340} diff --git a/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas b/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas index 8df4e732056..bec398bcbca 100644 --- a/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas +++ b/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas @@ -44,7 +44,11 @@ function CrackUTF8ZeroExtendedString(CommandType: THTTPCommandType; const p: str end; // Indy's UTF8 handling of URLs is *completely* broken. - // We may need to check this with updated versions of Delphi + // We may need to check this with updated versions of Delphi. + // VER340/VER350/VER360 added to the IFNDEF chain to unblock + // Delphi 10.4/11/12 compilation; whether Indy's URL handling was + // fixed in those versions has not been re-verified — the + // workaround stays applied conservatively. {$IFNDEF VER330} {$IFNDEF VER340} {$IFNDEF VER350} diff --git a/docs/build/windows.md b/docs/build/windows.md index a3b4ce73983..f11e6ac1e33 100644 --- a/docs/build/windows.md +++ b/docs/build/windows.md @@ -260,8 +260,7 @@ of appropriate node versions during builds. * If you have Delphi 11 or 12 with a CLI-capable license (Professional or higher), set `KEYMAN_DELPHI_VERSION` to the Studio path — `22.0` for Delphi 11, `23.0` for Delphi 12 — before running `build.sh`. The default - (`20.0`, Delphi 10.3) is preserved when the variable is unset, so CI is - unaffected. + (`20.0`, Delphi 10.3) is preserved when the variable is unset. Start Delphi IDE once after installation as it will create various environment files and take you through required registration. From 736afd9b56e6d87544f959560f870453b92c85f4 Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Thu, 2 Jul 2026 23:47:34 -0500 Subject: [PATCH 06/16] docs(windows): sync emsdk install version with the pinned minimum resources/build/minimum-versions.inc.sh pins KEYMAN_MIN_VERSION_EMSCRIPTEN at 3.1.64, but the windows.md walkthrough still told contributors to `emsdk install 3.1.58`. That version now fails to compile core/src/wasm.cpp because a recent Core commit (9909d7d901 "expose km_core_state_options_update to WASM") uses stricter emscripten pointer-binding APIs. Bumping the docs to match the pin. Discovered while running the CE build walkthrough on this branch. Co-Authored-By: Claude Opus 4.7 --- docs/build/windows.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/build/windows.md b/docs/build/windows.md index f11e6ac1e33..9e482a894cc 100644 --- a/docs/build/windows.md +++ b/docs/build/windows.md @@ -184,8 +184,8 @@ In bash, run the following commands: cd /c/Projects/keyman git clone https://github.com/emscripten-core/emsdk cd emsdk -emsdk install 3.1.58 -emsdk activate 3.1.58 +emsdk install 3.1.64 +emsdk activate 3.1.64 cd upstream/emscripten npm install ``` @@ -195,8 +195,8 @@ If you are updating an existing install of Emscripten: ```bash cd emsdk git pull -emsdk install 3.1.58 -emsdk activate 3.1.58 +emsdk install 3.1.64 +emsdk activate 3.1.64 cd upstream/emscripten npm install ``` From 9ef5278f19ac80b03f061e11505c65c41d4f257f Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Fri, 3 Jul 2026 14:25:19 -0500 Subject: [PATCH 07/16] fix(devtools): tolerate Delphi 12 EnvOptions.proj Null attrs and Win64x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two Delphi 12 dev-env issues in devtools' -ai / TIncludePaths.Reset when processing %AppData%\Embarcadero\BDS\23.0\EnvOptions.proj: 1. Delphi 12 emits an empty at the top of the file with no Condition attribute. The pre-existing VarIsNull guard didn't short-circuit reliably under Delphi 12's variant handling, so a Null Attributes['Condition'] hit Pos() and raised EVariantTypeCastError. Replace with VarToStrDef(..., '') for a defensive Variant->string coercion that handles Null and Empty. 2. The Win32/Win64 match was Pos('Win32', ...) — a substring match that also matches 'Win64x' (a Delphi 12 target now present in the file). Building against the Win64x PropertyGroup then hit an empty and again produced Null. Match with the surrounding literal single quotes ('''Win32''' / '''Win64''') so 'Win64x' no longer matches, and additionally wrap the NodeValue reads in VarToStrDef so empty child elements degrade cleanly. Applied to both AddPathToProjectXML (the -ai/-ip path) and Reset (the -ri path) — same code shape in both. Found while running the CE build workflow against Delphi 12 CE — the tripwire fired at cef4delphi's `devtools -ai` step and blocked the engine build entirely until this fix landed. Co-Authored-By: Claude Opus 4.7 --- .../delphi/tools/devtools/DevIncludePaths.pas | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/common/windows/delphi/tools/devtools/DevIncludePaths.pas b/common/windows/delphi/tools/devtools/DevIncludePaths.pas index 509045d4d17..d6df62621df 100644 --- a/common/windows/delphi/tools/devtools/DevIncludePaths.pas +++ b/common/windows/delphi/tools/devtools/DevIncludePaths.pas @@ -144,6 +144,7 @@ class function TIncludePaths.AddPathToProjectXML(const ProjectXMLFileName, Path: doc: IXMLDocument; sn, node: IXMLNode; IncludePath: string; + Condition: string; I: Integer; begin if not FileExists(ProjectXMLFileName) then @@ -159,16 +160,20 @@ class function TIncludePaths.AddPathToProjectXML(const ProjectXMLFileName, Path: for I := 0 to node.ChildNodes.Count - 1 do begin sn := node.ChildNodes[I]; + // Delphi 12 EnvOptions.proj emits an empty without a + // Condition attribute; convert defensively via VarToStrDef so a Null or + // Empty variant returns '' instead of raising EVariantTypeCastError in Pos(). + Condition := VarToStrDef(sn.Attributes['Condition'], ''); if (sn.NodeName = 'PropertyGroup') and - not VarIsNull(sn.Attributes['Condition']) and - ((Pos('Win32', sn.Attributes['Condition']) > 0) or - (Pos('Win64', sn.Attributes['Condition']) > 0)) then + ((Pos('''Win32''', Condition) > 0) or (Pos('''Win64''', Condition) > 0)) then begin - IncludePath := sn.ChildNodes['DelphiBrowsingPath'].NodeValue; + // Guard against empty child nodes (e.g. ) whose + // NodeValue is Null on Delphi 12 and can't coerce to a string directly. + IncludePath := VarToStrDef(sn.ChildNodes['DelphiBrowsingPath'].NodeValue, ''); if AddPathToIncludePath(IncludePath, Path) then sn.ChildNodes['DelphiBrowsingPath'].nodeValue := IncludePath; - IncludePath := sn.ChildNodes['DelphiLibraryPath'].NodeValue; + IncludePath := VarToStrDef(sn.ChildNodes['DelphiLibraryPath'].NodeValue, ''); if AddPathToIncludePath(IncludePath, Path) then sn.ChildNodes['DelphiLibraryPath'].nodeValue := IncludePath; end; @@ -256,6 +261,7 @@ class function TIncludePaths.Reset: Boolean; doc: IXMLDocument; node: IXMLNode; ProjectFileName: string; + Condition: string; I: Integer; sn: IXMLNode; begin @@ -296,10 +302,11 @@ class function TIncludePaths.Reset: Boolean; for I := 0 to node.ChildNodes.Count - 1 do begin sn := node.ChildNodes[I]; + // See AddPathToProjectXML: guard against Delphi 12's empty + // where Attributes['Condition'] returns a Null variant. + Condition := VarToStrDef(sn.Attributes['Condition'], ''); if (sn.NodeName = 'PropertyGroup') and - not VarIsNull(sn.Attributes['Condition']) and - ((Pos('Win32', sn.Attributes['Condition']) > 0) or - (Pos('Win64', sn.Attributes['Condition']) > 0)) then + ((Pos('''Win32''', Condition) > 0) or (Pos('''Win64''', Condition) > 0)) then begin sn.ChildNodes['DelphiBrowsingPath'].NodeValue := SDefault_DelphiBrowsingPath; sn.ChildNodes['DelphiLibraryPath'].NodeValue := SDefault_DelphiSearchPath; From d69d9b7fe3737edc2d6329b98a9b751fb64c0fb8 Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Fri, 3 Jul 2026 14:25:20 -0500 Subject: [PATCH 08/16] fix(global): let CustomisationStorage compile under Delphi 12 dcc64 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TCustFileList.AddCustFile used a nested expression the Delphi 12 Win64 compiler rejects with E2010 'Incompatible types: TCustFile and TObject': Result := Items[inherited Add(FObjectClass.Create)]; dcc32 (Win32) accepted the same source, so the file has been building fine for CI's default 10.3 flow. On Delphi 12 dcc64, the inline chain loses the widening from TCustFile (returned by FObjectClass.Create) to TObject (Add's parameter). Splitting the object creation into a local TObject variable lets the widening happen explicitly and both compilers accept the code without any IFDEF. Found while building the Win64 half of kmcomapi.dproj as part of the CE workflow walkthrough — the Win32 half compiled cleanly. Co-Authored-By: Claude Opus 4.7 --- windows/src/global/delphi/cust/CustomisationStorage.pas | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/windows/src/global/delphi/cust/CustomisationStorage.pas b/windows/src/global/delphi/cust/CustomisationStorage.pas index db4e5c018fa..1af7f2a8a5e 100644 --- a/windows/src/global/delphi/cust/CustomisationStorage.pas +++ b/windows/src/global/delphi/cust/CustomisationStorage.pas @@ -157,8 +157,13 @@ function TCustomisationStorage.GetFileOfType(FileType: TCustFileType; StartIndex { TCustFileList } function TCustFileList.AddCustFile: TCustFile; +var + NewObj: TObject; begin - Result := Items[inherited Add(FObjectClass.Create)]; + // Delphi 12 dcc64 rejects the inline form with E2010 'TCustFile' and 'TObject'; + // splitting via a local TObject makes the widening explicit. + NewObj := FObjectClass.Create; + Result := Items[inherited Add(NewObj)]; end; constructor TCustFileList.Create(AObjectClass: TCustFileClass); From 4d090204864fae54b9f2a159176c89a47717ac19 Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Fri, 3 Jul 2026 15:04:12 -0500 Subject: [PATCH 09/16] =?UTF-8?q?Reapply=20JclSynch.pas=20BOOL=20casts=20?= =?UTF-8?q?=E2=80=94=20needed=20by=20TIKE=20compile=20chain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts the earlier revert (dcef4c29cf). @mcdurdin's original empirical test — building without the JclSynch patches to prove they were unnecessary — was correct for the projects he compiled, but it did not include TIKE. TIKE pulls in JCL through JVCL's dependency graph and instantiates TJclMutex.Create (JclSynch.pas:1078). On Delphi 12, that hits: [dcc32 Error] JclSynch.pas(1078): E2010 Incompatible types: 'Cardinal' and 'Boolean' [dcc32 Fatal Error] JclSysUtils.pas(2685): F2063 Could not compile used unit 'JclSynch.pas' The BOOL() casts on CreateEvent/OpenEvent/CreateWaitableTimer/ OpenWaitableTimer/OpenSemaphore/CreateMutex/OpenMutex are what let TIKE compile. Marc's smaller test set was Delphi-12-compatible without them; the fuller Developer tree isn't. Discovered by running the CE build workflow through `developer/src/tike/build.sh` on Delphi 12 CE — TIKE fails at the JclSynch link stage without this patch. Note the CreateMutex site also switches JclWin32.CreateMutex to Winapi.Windows.CreateMutex directly, because JclWin32's own CreateMutex is a re-declaration Delphi 12 resolves ambiguously. Co-Authored-By: Claude Opus 4.7 --- .../jedi/jcl/jcl/source/common/JclSynch.pas | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas b/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas index f73b1722b09..eb405224249 100644 --- a/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas +++ b/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas @@ -937,11 +937,15 @@ function TJclCriticalSectionEx.TryEnter: Boolean; //== { TJclEvent } =========================================================== +// Keyman local patch: Delphi 11/12 compat (vendored JCL). Explicit BOOL() casts +// added below to satisfy Delphi 12's stricter implicit-Boolean->BOOL conversion. +// Backwards-compatible with Delphi 10.3 (BOOL is an ordinal-preserving typecast). +// On JCL refresh from upstream: re-apply if upstream hasn't picked up the cast. constructor TJclEvent.Create(SecAttr: PSecurityAttributes; Manual, Signaled: Boolean; const Name: string); begin inherited Create; FName := Name; - FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.CreateEvent(SecAttr, Manual, Signaled, PChar(FName)); + FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.CreateEvent(SecAttr, BOOL(Manual), BOOL(Signaled), PChar(FName)); if FHandle = 0 then raise EJclEventError.CreateRes(@RsSynchCreateEvent); FExisted := GetLastError = ERROR_ALREADY_EXISTS; @@ -952,7 +956,7 @@ constructor TJclEvent.Open(Access: Cardinal; Inheritable: Boolean; begin FName := Name; FExisted := True; - FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenEvent(Access, Inheritable, PChar(Name)); + FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenEvent(Access, BOOL(Inheritable), PChar(Name)); if FHandle = 0 then raise EJclEventError.CreateRes(@RsSynchOpenEvent); end; @@ -980,7 +984,7 @@ constructor TJclWaitableTimer.Create(SecAttr: PSecurityAttributes; begin FName := Name; FResume := False; - FHandle := CreateWaitableTimer(SecAttr, Manual, PChar(Name)); + FHandle := CreateWaitableTimer(SecAttr, BOOL(Manual), PChar(Name)); if FHandle = 0 then raise EJclWaitableTimerError.CreateRes(@RsSynchCreateWaitableTimer); FExisted := GetLastError = ERROR_ALREADY_EXISTS; @@ -1000,7 +1004,7 @@ constructor TJclWaitableTimer.Open(Access: Cardinal; Inheritable: Boolean; FExisted := True; FName := Name; FResume := False; - FHandle := OpenWaitableTimer(Access, Inheritable, PChar(Name)); + FHandle := OpenWaitableTimer(Access, BOOL(Inheritable), PChar(Name)); if FHandle = 0 then raise EJclWaitableTimerError.CreateRes(@RsSynchOpenWaitableTimer); end; @@ -1048,7 +1052,7 @@ constructor TJclSemaphore.Open(Access: Cardinal; Inheritable: Boolean; begin FName := Name; FExisted := True; - FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenSemaphore(Access, Inheritable, PChar(Name)); + FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenSemaphore(Access, BOOL(Inheritable), PChar(Name)); if FHandle = 0 then raise EJclSemaphoreError.CreateRes(@RsSynchOpenSemaphore); end; @@ -1075,7 +1079,7 @@ constructor TJclMutex.Create(SecAttr: PSecurityAttributes; InitialOwner: Boolean begin inherited Create; FName := Name; - FHandle := JclWin32.CreateMutex(SecAttr, InitialOwner, PChar(Name)); + FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.CreateMutex(SecAttr, BOOL(InitialOwner), PChar(Name)); if FHandle = 0 then raise EJclMutexError.CreateRes(@RsSynchCreateMutex); FExisted := GetLastError = ERROR_ALREADY_EXISTS; @@ -1086,7 +1090,7 @@ constructor TJclMutex.Open(Access: Cardinal; Inheritable: Boolean; const Name: s inherited Create; FName := Name; FExisted := True; - FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenMutex(Access, Inheritable, PChar(Name)); + FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenMutex(Access, BOOL(Inheritable), PChar(Name)); if FHandle = 0 then raise EJclMutexError.CreateRes(@RsSynchOpenMutex); end; From fc6978675ab9f0f9e9500bb889792e3bfb3c4892 Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Fri, 3 Jul 2026 15:11:53 -0500 Subject: [PATCH 10/16] =?UTF-8?q?Reapply=20JvComponent.pas=20OldCreateOrde?= =?UTF-8?q?r=20patch=20=E2=80=94=20needed=20by=20TIKE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts the earlier revert (d7b3784f07). @mcdurdin's original concern was that the Athens docs seemed to say OldCreateOrder was still present, so the patch's premise was wrong. Empirical Delphi 12 CE contradicts that: [dcc32 Error] JvComponent.pas(126): E2003 Undeclared identifier: 'OldCreateOrder' [dcc32 Fatal Error] JvDockControlForm.pas(2442): F2063 Could not compile used unit 'JvComponent.pas' Line 126 is `if OldCreateOrder then DoCreate;`. Under Delphi 12, `TCustomForm.OldCreateOrder` genuinely no longer exists. The {$IF Defined(VER350) or Defined(VER360)} arm unconditionally calls DoCreate on 11/12 (equivalent to OldCreateOrder=True, which the docs describe as the modern default) and keeps the runtime check on older Delphis. Discovered by running the CE build workflow through `developer/src/tike/build.sh` on Delphi 12 CE — TIKE pulls in JvDockControlForm which uses JvComponent, and TIKE fails without this patch. The docs Marc linked may still be reachable, but the compiler is the source of truth here — VER350+ has removed OldCreateOrder from TCustomForm's published surface. Co-Authored-By: Claude Opus 4.7 --- developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas b/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas index 1877ee48efe..fab8609e2c8 100644 --- a/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas +++ b/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas @@ -123,8 +123,16 @@ constructor TJvForm.Create(AOwner: TComponent); finally Exclude(FFormState, fsCreating); end; + // Keyman local patch: Delphi 11/12 compat (vendored JVCL). The + // OldCreateOrder property was removed in Delphi 11; the modern semantics + // are equivalent to OldCreateOrder=True, so always call DoCreate on + // VER350+. On JVCL refresh from upstream: re-apply if not yet upstreamed. +{$IF Defined(VER350) or Defined(VER360)} + DoCreate; +{$ELSE} if OldCreateOrder then DoCreate; +{$IFEND} end; finally GlobalNameSpace.EndWrite; From 7ebe6117fcf86c13fbdd3e098e20f92d288a3488 Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Thu, 9 Jul 2026 17:58:55 -0500 Subject: [PATCH 11/16] Using the upstream guard for JvComponent.pas Missing this guard means that tike fails to build on a fresh install without the OLDCREATEORDER flag. --- developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas b/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas index fab8609e2c8..9c0cd1e9719 100644 --- a/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas +++ b/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas @@ -123,16 +123,10 @@ constructor TJvForm.Create(AOwner: TComponent); finally Exclude(FFormState, fsCreating); end; - // Keyman local patch: Delphi 11/12 compat (vendored JVCL). The - // OldCreateOrder property was removed in Delphi 11; the modern semantics - // are equivalent to OldCreateOrder=True, so always call DoCreate on - // VER350+. On JVCL refresh from upstream: re-apply if not yet upstreamed. -{$IF Defined(VER350) or Defined(VER360)} - DoCreate; -{$ELSE} + {$IFDEF HAS_PROPERTY_OLDCREATEORDER} if OldCreateOrder then DoCreate; -{$IFEND} + {$ENDIF HAS_PROPERTY_OLDCREATEORDER} end; finally GlobalNameSpace.EndWrite; From 1c32b233ba7fd0c68cfffa62788d4c3e2717f81e Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Thu, 9 Jul 2026 21:39:45 -0500 Subject: [PATCH 12/16] fix(windows): align vendored JCL with upstream, apply review suggestions Revert the JclSynch.pas BOOL() cast patch to match project-jedi/jcl upstream verbatim at all 7 call sites (no casts; restore JclWin32.CreateMutex). TIKE builds without it once the JvComponent.pas OldCreateOrder guard (already the upstream guard) is in place. Apply mcdurdin's inline review suggestions on FixedTrackbar.pas and Keyman.System.HttpServer.Base.pas: reorder version guards newest-outermost, consolidate messages, and convert the raw ERROR! line to a {$MESSAGE ERROR} directive. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../delphi/components/FixedTrackbar.pas | 9 ++++----- .../web/Keyman.System.HttpServer.Base.pas | 3 ++- .../jedi/jcl/jcl/source/common/JclSynch.pas | 18 +++++++----------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/common/windows/delphi/components/FixedTrackbar.pas b/common/windows/delphi/components/FixedTrackbar.pas index 9e900c2c0d9..9696062a62e 100644 --- a/common/windows/delphi/components/FixedTrackbar.pas +++ b/common/windows/delphi/components/FixedTrackbar.pas @@ -64,16 +64,15 @@ procedure TTntFixedDrawGrid.WMEraseBkgnd(var Message: TMessage); Tested on VER320 (10.2) Tested on VER330 (10.3) - 29 Oct 2019 - mcdurdin - Not yet fully verified against Vcl.Grids.pas in VER350 (11) or VER360 (12); - IFNDEF arms added to unblock compilation, override behaviour retained as-is. + TODO: Not yet fully verified against Vcl.Grids.pas in VER350 (11) or VER360 (12) } +{$IFNDEF VER360} +{$IFNDEF VER350} {$IFNDEF VER340} -{$MESSAGE WARN 'Not yet checked against Delphi 10.4'} +{$MESSAGE WARN 'TODO: Trackbar scrolling on bottom cell not yet checked against Delphi 10.4, 11.0 or 12.0'} {$IFNDEF VER330} {$IFNDEF VER320} -{$IFNDEF VER350} -{$IFNDEF VER360} {$MESSAGE ERROR 'Check that this fix is still applicable for a new version of Delphi. Checked against Delphi 10.2, 10.3' } {$ENDIF} {$ENDIF} diff --git a/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas b/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas index bec398bcbca..4e28593a7c4 100644 --- a/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas +++ b/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas @@ -51,9 +51,10 @@ function CrackUTF8ZeroExtendedString(CommandType: THTTPCommandType; const p: str // workaround stays applied conservatively. {$IFNDEF VER330} {$IFNDEF VER340} + {$MESSAGE WARN 'TODO: Check if Indy URL UTF-8 handling is still needed with Delphi 10.4/11.0/12.0'} {$IFNDEF VER350} {$IFNDEF VER360} - ERROR! Check if this is still needed with Delphi update + {$MESSAGE ERROR 'Check if Indy URL UTF-8 handling is still needed with Delphi update'} {$ENDIF} {$ENDIF} {$ENDIF} diff --git a/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas b/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas index eb405224249..f73b1722b09 100644 --- a/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas +++ b/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas @@ -937,15 +937,11 @@ function TJclCriticalSectionEx.TryEnter: Boolean; //== { TJclEvent } =========================================================== -// Keyman local patch: Delphi 11/12 compat (vendored JCL). Explicit BOOL() casts -// added below to satisfy Delphi 12's stricter implicit-Boolean->BOOL conversion. -// Backwards-compatible with Delphi 10.3 (BOOL is an ordinal-preserving typecast). -// On JCL refresh from upstream: re-apply if upstream hasn't picked up the cast. constructor TJclEvent.Create(SecAttr: PSecurityAttributes; Manual, Signaled: Boolean; const Name: string); begin inherited Create; FName := Name; - FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.CreateEvent(SecAttr, BOOL(Manual), BOOL(Signaled), PChar(FName)); + FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.CreateEvent(SecAttr, Manual, Signaled, PChar(FName)); if FHandle = 0 then raise EJclEventError.CreateRes(@RsSynchCreateEvent); FExisted := GetLastError = ERROR_ALREADY_EXISTS; @@ -956,7 +952,7 @@ constructor TJclEvent.Open(Access: Cardinal; Inheritable: Boolean; begin FName := Name; FExisted := True; - FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenEvent(Access, BOOL(Inheritable), PChar(Name)); + FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenEvent(Access, Inheritable, PChar(Name)); if FHandle = 0 then raise EJclEventError.CreateRes(@RsSynchOpenEvent); end; @@ -984,7 +980,7 @@ constructor TJclWaitableTimer.Create(SecAttr: PSecurityAttributes; begin FName := Name; FResume := False; - FHandle := CreateWaitableTimer(SecAttr, BOOL(Manual), PChar(Name)); + FHandle := CreateWaitableTimer(SecAttr, Manual, PChar(Name)); if FHandle = 0 then raise EJclWaitableTimerError.CreateRes(@RsSynchCreateWaitableTimer); FExisted := GetLastError = ERROR_ALREADY_EXISTS; @@ -1004,7 +1000,7 @@ constructor TJclWaitableTimer.Open(Access: Cardinal; Inheritable: Boolean; FExisted := True; FName := Name; FResume := False; - FHandle := OpenWaitableTimer(Access, BOOL(Inheritable), PChar(Name)); + FHandle := OpenWaitableTimer(Access, Inheritable, PChar(Name)); if FHandle = 0 then raise EJclWaitableTimerError.CreateRes(@RsSynchOpenWaitableTimer); end; @@ -1052,7 +1048,7 @@ constructor TJclSemaphore.Open(Access: Cardinal; Inheritable: Boolean; begin FName := Name; FExisted := True; - FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenSemaphore(Access, BOOL(Inheritable), PChar(Name)); + FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenSemaphore(Access, Inheritable, PChar(Name)); if FHandle = 0 then raise EJclSemaphoreError.CreateRes(@RsSynchOpenSemaphore); end; @@ -1079,7 +1075,7 @@ constructor TJclMutex.Create(SecAttr: PSecurityAttributes; InitialOwner: Boolean begin inherited Create; FName := Name; - FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.CreateMutex(SecAttr, BOOL(InitialOwner), PChar(Name)); + FHandle := JclWin32.CreateMutex(SecAttr, InitialOwner, PChar(Name)); if FHandle = 0 then raise EJclMutexError.CreateRes(@RsSynchCreateMutex); FExisted := GetLastError = ERROR_ALREADY_EXISTS; @@ -1090,7 +1086,7 @@ constructor TJclMutex.Open(Access: Cardinal; Inheritable: Boolean; const Name: s inherited Create; FName := Name; FExisted := True; - FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenMutex(Access, BOOL(Inheritable), PChar(Name)); + FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenMutex(Access, Inheritable, PChar(Name)); if FHandle = 0 then raise EJclMutexError.CreateRes(@RsSynchOpenMutex); end; From e363227eb056d1a0ce333dec57ae4155979cc245 Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Thu, 9 Jul 2026 22:13:38 -0500 Subject: [PATCH 13/16] style(windows): use {$IF Defined()} guards instead of nested {$IFNDEF} Collapse the nested {$IFNDEF} version walls in FixedTrackbar.pas and Keyman.System.HttpServer.Base.pas into single {$IF Defined(VERxxx) or ...} / {$ELSEIF} / {$ELSE} blocks, matching the style already used in CleartypeDrawCharacter.pas and JsonUtil.pas. Expressed positively: WARN on the unblocked-but-unverified Delphi 10.4/11/12 (VER340/VER350/VER360), silent on the tested baselines, and ERROR on any unrecognised version. No change to the runtime workarounds. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../delphi/components/FixedTrackbar.pas | 15 +++++-------- .../web/Keyman.System.HttpServer.Base.pas | 21 ++++++++----------- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/common/windows/delphi/components/FixedTrackbar.pas b/common/windows/delphi/components/FixedTrackbar.pas index 9696062a62e..096d6340415 100644 --- a/common/windows/delphi/components/FixedTrackbar.pas +++ b/common/windows/delphi/components/FixedTrackbar.pas @@ -67,18 +67,13 @@ procedure TTntFixedDrawGrid.WMEraseBkgnd(var Message: TMessage); TODO: Not yet fully verified against Vcl.Grids.pas in VER350 (11) or VER360 (12) } -{$IFNDEF VER360} -{$IFNDEF VER350} -{$IFNDEF VER340} +{$IF Defined(VER340) or Defined(VER350) or Defined(VER360)} {$MESSAGE WARN 'TODO: Trackbar scrolling on bottom cell not yet checked against Delphi 10.4, 11.0 or 12.0'} -{$IFNDEF VER330} -{$IFNDEF VER320} +{$ELSEIF Defined(VER320) or Defined(VER330)} +// Tested on Delphi 10.2 (VER320) and 10.3 (VER330) +{$ELSE} {$MESSAGE ERROR 'Check that this fix is still applicable for a new version of Delphi. Checked against Delphi 10.2, 10.3' } -{$ENDIF} -{$ENDIF} -{$ENDIF} -{$ENDIF} -{$ENDIF} +{$IFEND} procedure TTntFixedDrawGrid.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); diff --git a/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas b/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas index 4e28593a7c4..fbdb7a0e65b 100644 --- a/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas +++ b/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas @@ -45,20 +45,17 @@ function CrackUTF8ZeroExtendedString(CommandType: THTTPCommandType; const p: str // Indy's UTF8 handling of URLs is *completely* broken. // We may need to check this with updated versions of Delphi. - // VER340/VER350/VER360 added to the IFNDEF chain to unblock - // Delphi 10.4/11/12 compilation; whether Indy's URL handling was - // fixed in those versions has not been re-verified — the - // workaround stays applied conservatively. -{$IFNDEF VER330} -{$IFNDEF VER340} + // VER340/VER350/VER360 (Delphi 10.4/11/12) are treated as unblocked- + // but-unverified: whether Indy's URL handling was fixed in those + // versions has not been re-verified — the workaround stays applied + // conservatively. +{$IF Defined(VER340) or Defined(VER350) or Defined(VER360)} {$MESSAGE WARN 'TODO: Check if Indy URL UTF-8 handling is still needed with Delphi 10.4/11.0/12.0'} -{$IFNDEF VER350} -{$IFNDEF VER360} +{$ELSEIF Defined(VER330)} + // Verified against Delphi 10.3 (VER330) +{$ELSE} {$MESSAGE ERROR 'Check if Indy URL UTF-8 handling is still needed with Delphi update'} -{$ENDIF} -{$ENDIF} -{$ENDIF} -{$ENDIF} +{$IFEND} SetLength(s, p.Length); for i := 1 to p.Length do From 90792cfa9c84d29be4095282c9fb12d285b382f7 Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Thu, 9 Jul 2026 22:59:02 -0500 Subject: [PATCH 14/16] fix(windows): route JclSynch CreateMutex via RTL for Delphi 12 source build TJclMutex.Create called JclWin32.CreateMutex, whose `external kernel32` redeclaration takes a BOOL param (legacy WinNT4 workaround). Delphi 12's dcc32 rejects the implicit Boolean->BOOL there (E2010), which blocks a from-source build of the vendored JCL. Route through the RTL like the sibling OpenMutex, with an explicit BOOL() cast. Only the CreateMutex site is touched; the other calls already go through the RTL. Builds that link a precompiled JCL .dcu never hit this. Also compact the local-patch comments in mxs.inc and HttpServer.Base.pas. Co-Authored-By: Claude Opus 4.8 (1M context) --- common/windows/delphi/web/Keyman.System.HttpServer.Base.pas | 5 +---- developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas | 5 ++++- developer/src/ext/mbcolor/mxs.inc | 6 ++---- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas b/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas index fbdb7a0e65b..9af0ac60e96 100644 --- a/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas +++ b/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas @@ -45,10 +45,7 @@ function CrackUTF8ZeroExtendedString(CommandType: THTTPCommandType; const p: str // Indy's UTF8 handling of URLs is *completely* broken. // We may need to check this with updated versions of Delphi. - // VER340/VER350/VER360 (Delphi 10.4/11/12) are treated as unblocked- - // but-unverified: whether Indy's URL handling was fixed in those - // versions has not been re-verified — the workaround stays applied - // conservatively. + // VER340/VER350/VER360 (10.4/11/12): unblocked but not re-verified; workaround kept. {$IF Defined(VER340) or Defined(VER350) or Defined(VER360)} {$MESSAGE WARN 'TODO: Check if Indy URL UTF-8 handling is still needed with Delphi 10.4/11.0/12.0'} {$ELSEIF Defined(VER330)} diff --git a/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas b/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas index f73b1722b09..c014318f09e 100644 --- a/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas +++ b/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas @@ -1075,7 +1075,10 @@ constructor TJclMutex.Create(SecAttr: PSecurityAttributes; InitialOwner: Boolean begin inherited Create; FName := Name; - FHandle := JclWin32.CreateMutex(SecAttr, InitialOwner, PChar(Name)); + // Keyman patch (D12, source-compiled JCL): JclWin32.CreateMutex is an external + // decl with a BOOL param; D12 rejects Boolean->BOOL there (E2010). Route via the + // RTL like OpenMutex below. Only bites when JCL is built from source. + FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.CreateMutex(SecAttr, BOOL(InitialOwner), PChar(Name)); if FHandle = 0 then raise EJclMutexError.CreateRes(@RsSynchCreateMutex); FExisted := GetLastError = ERROR_ALREADY_EXISTS; diff --git a/developer/src/ext/mbcolor/mxs.inc b/developer/src/ext/mbcolor/mxs.inc index 75ca0b34e73..358ee4b84b3 100644 --- a/developer/src/ext/mbcolor/mxs.inc +++ b/developer/src/ext/mbcolor/mxs.inc @@ -7,10 +7,8 @@ {$define DELPHI_10_UP} {$endif} - // Keyman local patch: Delphi 11/12 compat (vendored mbcolor). Without - // these blocks DELPHI_*_UP flags are never defined on VER350/VER360, - // which causes HTMLColors.pas to drop "uses Variants" and fail to - // resolve the Null constant. On mbcolor refresh: re-apply if needed. + // Keyman patch (D11/12, vendored mbcolor): define DELPHI_*_UP on VER350/VER360 + // too, else HTMLColors.pas drops "uses Variants" and Null won't resolve. {$ifdef VER350} {$define DELPHI_5_UP} {$define DELPHI_6_UP} From 556af89437d3937ca2b73323b92ae13320e119ab Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Thu, 9 Jul 2026 23:27:40 -0500 Subject: [PATCH 15/16] feat(windows): Delphi CE interactive build workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delphi Community Edition blocks CLI compilation, so when KEYMAN_DELPHI_CE=1 delphi_msbuild pauses and prompts the developer to build the project in the IDE, then continues. To support the "open the .groupproj, Build All once, then Enter through each prompt" flow, prompts auto-skip when the expected Debug output is already newer than its source. Also fix a stale path in engine.groupproj: insthelper was referenced as inst\insthelper\insthelper.dproj, but the project lives at insthelper\; the bad path broke IDE Build All for the engine group. (The automated build is unaffected — it builds insthelper via insthelper/build.sh, not the groupproj.) Co-Authored-By: Claude Opus 4.8 (1M context) --- resources/build/win/environment.inc.sh | 47 ++++++++++++++++++++++++++ windows/src/engine/engine.groupproj | 8 ++--- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/resources/build/win/environment.inc.sh b/resources/build/win/environment.inc.sh index 9408c6660ef..488046b19da 100644 --- a/resources/build/win/environment.inc.sh +++ b/resources/build/win/environment.inc.sh @@ -99,7 +99,54 @@ tds2dbg() { builder_if_release_build_level "$TDS2DBG" "$@" } +# Check whether the expected Delphi output for a project is already fresh — +# useful when a developer has pre-built everything via a .groupproj Build All +# in the IDE. Compares bin//Debug/.{exe,dll,bpl} mtime against +# the .dpr or .dpk source. Returns 0 (up-to-date, skip prompt) if any match. +_delphi_ce_output_fresh() { + local project="$1" platform="$2" + local base="${project%.dproj}" + local source + if [[ -f "${base}.dpr" ]]; then source="${base}.dpr" + elif [[ -f "${base}.dpk" ]]; then source="${base}.dpk" + else return 1 # can't judge freshness without a source file + fi + local candidate ext + for ext in exe dll bpl; do + candidate="bin/${platform}/Debug/${base}.${ext}" + if [[ -f "$candidate" && "$candidate" -nt "$source" ]]; then + return 0 + fi + done + return 1 +} + delphi_msbuild() { + # When KEYMAN_DELPHI_CE=1 the Delphi Community Edition blocks CLI + # compilation. Pause and prompt the developer to build in the IDE. + if [[ "${KEYMAN_DELPHI_CE:-}" == "1" ]]; then + local project="${1:-}" + local platform="Win32" + local arg + for arg in "$@"; do + [[ "$arg" == *Platform=Win64* ]] && platform="Win64" + done + + # Skip prompt if the expected Debug output is already fresh — supports the + # "open .groupproj + Build All once, then Enter through each script prompt" + # workflow. Subsequent prompts become no-ops as long as outputs are newer + # than their sources. + if _delphi_ce_output_fresh "$project" "$platform"; then + builder_echo "Delphi CE: $project ($platform) — output is up-to-date, skipping prompt." + return 0 + fi + + builder_echo warning "Delphi CE: CLI compilation is not available." + builder_echo warning "Please build ${project} (Platform: ${platform}, Config: Debug) in the Delphi IDE now, then press Enter to continue (or Ctrl-C to abort)." + builder_echo warning "Tip: opening the parent .groupproj and running Build All pre-builds all sibling projects at once; subsequent CE prompts in this script will auto-skip when their outputs are fresh." + read -r _ + return 0 + fi run_in_delphi_env msbuild.exe "$@" "$DELPHI_MSBUILD_FLAG_DEBUG" } diff --git a/windows/src/engine/engine.groupproj b/windows/src/engine/engine.groupproj index bb2548b6103..79830a476ed 100644 --- a/windows/src/engine/engine.groupproj +++ b/windows/src/engine/engine.groupproj @@ -15,7 +15,7 @@ - + @@ -63,13 +63,13 @@ - + - + - + From f6a2c6357bb332f24dda1d66602f3a151f8f1027 Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Thu, 9 Jul 2026 23:27:51 -0500 Subject: [PATCH 16/16] docs(windows): add Delphi CE build guide Document the Community Edition interactive build workflow (KEYMAN_DELPHI_CE, the IDE Build All + auto-skip flow, Config Manager Debug/Win32 requirement) in windows-delphi-ce.md, and link it from windows.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/build/windows-delphi-ce.md | 412 ++++++++++++++++++++++++++++++++ docs/build/windows.md | 31 +++ 2 files changed, 443 insertions(+) create mode 100644 docs/build/windows-delphi-ce.md diff --git a/docs/build/windows-delphi-ce.md b/docs/build/windows-delphi-ce.md new file mode 100644 index 00000000000..0636503b59a --- /dev/null +++ b/docs/build/windows-delphi-ce.md @@ -0,0 +1,412 @@ +# Build Keyman on Windows with Delphi Community Edition + +Delphi CE (11 and 12) blocks CLI `dcc32`, so `build.sh` cannot drive +Delphi builds end-to-end. Setting `KEYMAN_DELPHI_CE=1` switches +`delphi_msbuild` to interactive mode: at each Delphi step the script +pauses and prompts for an IDE build. The surrounding pre/post-build +work (`rc.exe`, manifest generation, codegen, binary copies) still +runs automatically. + +The body is written against Delphi 12 Athens CE (currently the only +free tier). Delphi 11 CE works the same way; only the `Studio\22.0\` +path differs. + +This doc is a delta on top of [windows.md](windows.md) — read that +first for repository layout, base dependencies, and the standard +build. + +## 1. Prerequisites (delta from windows.md) + +* **Delphi 12 Athens Community Edition** from + https://www.embarcadero.com/products/delphi/starter/free-download. + Installs to `C:\Program Files (x86)\Embarcadero\Studio\23.0\`. During + install, select **DUnit Unit Testing Frameworks** (Keyman test + projects require it). Launch the IDE once after install so the + per-user `BDS\23.0` registry hive gets populated. +* **Keyman 19 (official release)** from https://keyman.com/windows is + strongly recommended — it populates the Keyman install-path registry + key that kmshell reads via `TKeymanPaths.KeymanDesktopInstallPath()`, + which simplifies the debugging setup and gives you a working system + to overlay dev binaries onto. It's possible to debug individual + components without an official install, but each component then + needs its support files located manually. +* **Test signing certificates** — one-time setup. Several `build.sh` + scripts run `signtool.exe` against test certificates at + `common/windows/delphi/tools/certificates/`. If the `.pfx` files + don't exist, the `wrap-signcode` step fails with + `SignTool Error: File not found: ...keymantest-sha1.pfx`. Generate + them once via: + + ```bash + ./common/windows/delphi/tools/certificates/build.sh certificates + ``` + + This runs `makecert` + `pvk2pfx` from the Windows SDK and installs + two Keyman test-CA root certificates into your current-user cert + store (via `certutil -user -addstore Root`). To clean up later: + `certutil -user -delstore Root "Keyman Test CA"` and the SHA1 variant. + +## 2. Environment variables + +Set both: + +```bat +SETX KEYMAN_DELPHI_VERSION 23.0 +SETX KEYMAN_DELPHI_CE 1 +``` + +* `KEYMAN_DELPHI_VERSION` tells builder which version of Delphi to target + (defaults to `20.0`). Without it, Delphi targets are silently skipped + if Delphi 10.3 is not installed. +* `KEYMAN_DELPHI_CE=1` makes `delphi_msbuild` prompt at each Delphi step: + + ``` + Delphi CE: CLI compilation is not available. + Please build in the Delphi IDE now, then press + Enter to continue (or Ctrl-C to abort). + ``` + +`SETX` is persistent but does not affect the current shell — open a +fresh shell to pick the values up. + +> [!CAUTION] +> **Verify these variables are set in every terminal you build from.** +> If `KEYMAN_DELPHI_CE` is unset, `delphi_msbuild` falls through to +> `msbuild.exe` which — on Delphi CE — reports `Build succeeded` with +> a small `Time Elapsed` line but produces **no output**. Downstream +> `cp` / `mv` / `sentrytool_delphiprep` / `mt.exe` steps then fail +> with confusing "file not found" errors on paths where Delphi never +> actually wrote anything. If you see a suspiciously fast Delphi +> build followed by a missing-file error, check this first: +> +> ```bash +> echo "KEYMAN_DELPHI_CE='$KEYMAN_DELPHI_CE' KEYMAN_DELPHI_VERSION='$KEYMAN_DELPHI_VERSION'" +> ``` +> +> Both must be set. If either is empty, `export` them in the current +> shell before rerunning any `build.sh`. + +## 3. Delphi IDE Library Search Paths + +`build.sh` passes `-U/-I/-R` flags to `dcc32` that the IDE never sees. +Without them, opening any Keyman `.dproj` fails with `F1026 File not +found: jvcl.inc` / `jedi.inc` / `jcl.inc`. + +Register them with `devtools -ai`, which writes the IDE **Search** and +**Browsing** paths for both Win32 and Win64 (registry + `EnvOptions.proj`) +in one shot — no manual **Tools → Options** step needed. `devtools` has no +JCL/JVCL dependencies, so it builds before the paths exist: build it first +(one of the CE prompts under `windows/src/global/delphi/build.sh build`), +**close Delphi** (it caches library paths at startup), then register each +path (`-ai` takes one absolute path per call): + +```bash +DEVTOOLS="$KEYMAN_ROOT/common/windows/delphi/tools/devtools/bin/Win32/Debug/devtools.exe" +register() { "$DEVTOOLS" -ai "$(cygpath -w "$1")"; } + +# The six CLI include paths, read from their single source of truth: +eval "$(grep '^DELPHIINCLUDES=' "$KEYMAN_ROOT/resources/build/win/delphi_flags.inc.sh")" +IFS=';'; for p in $DELPHIINCLUDES; do register "$p"; done; unset IFS + +# The IDE-only JCL/JVCL/jedi paths (no build-side equivalent): +for p in \ + developer/src/ext/jedi/jcl/jcl/source/common \ + developer/src/ext/jedi/jcl/jcl/source/prototypes \ + developer/src/ext/jedi/jcl/jcl/source/vcl \ + developer/src/ext/jedi/jcl/jcl/source/windows \ + developer/src/ext/jedi/jcl/jcl/source/include \ + developer/src/ext/jedi/jvcl/jvcl/design \ + developer/src/ext/jedi/jvcl/jvcl/run \ + developer/src/ext/jedi/jvcl/jvcl/common \ + developer/src/ext/jedi/jvcl/jvcl/resources \ + developer/src/ext/jedi/jedi \ + developer/src/ext/jedi ; do + register "$KEYMAN_ROOT/$p" +done +``` + +The first block reads the six CLI include paths straight from `DELPHIINCLUDES` +in [`delphi_flags.inc.sh`](../../resources/build/win/delphi_flags.inc.sh), so +they can't drift from the build. The eleven JCL / JVCL / jedi paths are +IDE-only with no build-side equivalent, so they stay listed explicitly. +`devtools` targets the `BDS\` hive matching the Delphi it was built +with (`23.0` under Delphi 12, per `SourceRootPath.pas`), so build it under the +same Delphi you'll open the projects in. + +## 4. Building the Delphi projects under CE + +The implicit cross-project **build-order dependencies** (codegen `.pas` +files, embedded `.res`, COM registration) are general to all Windows +builds, not CE-specific, so they now live in +[windows.md § Delphi build-order dependencies](windows.md#delphi-build-order-dependencies). +They bite harder under CE because you build each project by hand: accept +the IDE prompts in the order `build.sh` issues them and let none get +skipped. + +Run `./build.sh build` in each child directory (or at the repo root +to fan out) with `KEYMAN_DELPHI_CE=1` set — the script drives the +order and prompts you at each Delphi step. + +**Install (a separate step).** Once built, overlay the dev binaries and +register `kmcomapi.dll` from an **elevated Git Bash**: + +```bash +windows/src/engine/build.sh install +windows/src/desktop/build.sh install +``` + +### Faster iteration: script + IDE Build All (warm-state only) + +`delphi_msbuild` under `KEYMAN_DELPHI_CE=1` **auto-skips its prompt** +when the expected output at `bin//Debug/.{exe,dll,bpl}` +is newer than the corresponding `.dpr` / `.dpk` source. On subsequent +builds — where the previous build's `.res` files, codegen `.pas` +files, and vendored `.bpl`s are all still on disk — this enables: + +1. From a terminal, kick off the shell build script for a subsystem + (e.g. `./windows/src/engine/build.sh build`). The first CE prompt + fires and blocks. +2. Alt-Tab to Delphi 12 CE. Open the relevant project group: + * `windows/src/engine/engine.groupproj` — engine children (keyman, + kmcomapi, tsysinfo, tsysinfox64). Post-PR #16044 `insthelper` + is not in the group and needs to be opened on its own. + * `windows/src/desktop/desktop.groupproj` — desktop children + (kmshell, kmbrowserhost, kmconfig, insthelp, setup). + * `developer/src/developer.groupproj` — TIKE, kmconvert, + developer/setup. +3. Check Delphi's Config = **Debug** and Platform = **Win32** (or + Win64 for the 64-bit half of kmcomapi and for tsysinfox64), then + **Build All**. Every project in the group compiles once. +4. Alt-Tab back to the terminal, press Enter to release the first + prompt. +5. Each subsequent `delphi_msbuild` call detects its output is fresh + and skips the prompt automatically. Post-build steps + (`sentrytool_delphiprep`, `tds2dbg`, staging `cp`) run against the + pre-built binaries. + +**This flow only works when all Delphi inputs already exist on disk.** +Delphi Build All can't fabricate the inputs the shell scripts +generate: + +* `MessageIdentifierConsts.pas` (from `devtools -buildmessageconstants` + in `windows/src/global/delphi/build.sh`) +* `Keyman.Setup.System.Locale.*.pas` (from `devtools -buildsetupstrings` + in `windows/src/desktop/setup/build.sh`) +* `Keyman.System.Standards.BCP47*.pas` (from `build_standards_data/build.sh`) +* `keyman_components.bpl`, `common_components.bpl`, `CEF4Delphi.bpl` + (from their respective `build.sh` scripts) +* `tsysinfo_x64.res` (from tsysinfo/build.sh's tsysinfox64 → copy → + rc.exe chain) +* `kbd_noicon.res` + `kmcomapi.tlb` (from kmcomapi/build.sh do_build) +* Per-project `version.res` / `manifest.res` (from each `build.sh`'s + pre-build) + +**Fresh clone or after `git clean -fdx`**: you must walk the CE +prompts one-by-one at least once. Each project's `build.sh` fires the +pre-build steps that generate the inputs above; then the CE prompt +fires; you Build in Delphi; script proceeds. **On the next build** +(codegen outputs still present) you can switch to the Build All flow. + +If a project's output is missing or older than the source (you +edited a `.dpr`, or Delphi built to Release instead of Debug), the +CE prompt still fires for that project — no risk of silent +stale-output failures. + +**Caveat on engine.groupproj even for warm builds**: the group +currently orders `tsysinfo` before `tsysinfox64`, so a bare Build All +on the engine group fails on tsysinfo (missing `tsysinfo_x64.res`) +unless `windows/src/engine/tsysinfo/build.sh` was run at least once +first (which produces the `.res`). +[#16192](https://github.com/keymanapp/keyman/issues/16192) proposes +a reorder + pre-build event that would remove this caveat. + +If you prefer per-project prompts (e.g. debugging a single project +in isolation, or a truly fresh clone), skip step 3 above — the +script will prompt for each `.dproj` in dep order and you can Build +them one at a time. + +### Verify Configuration + Platform before every IDE build + +Delphi's active Configuration (top-of-IDE dropdown) and Platform +persist across projects — if you switched to Release or Win64 while +inspecting an earlier `.dproj`, Delphi opens the next one with those +same settings. The `.dproj`'s `DCC_ExeOutput` interpolates +`$(Platform)` and `$(Config)`, so a Release/Win32 build lands at +`bin/Win32/Release/.exe` — not the `bin/Win32/Debug/` +location the `build.sh` post-build step expects. + +Symptom: Delphi's Messages pane reports `Build succeeded`, but the +`build.sh` fails with +`EFOpenError: Cannot open file "...\bin\Win32\Debug\.exe". +The system cannot find the path specified.` + +Before Building each project via the CE prompt, confirm the top-of-IDE +dropdown reads **Debug** and **Win32** (or **Win64** for tsysinfox64 +and the Win64 half of kmcomapi). + +### Multi-platform Delphi packages (kmcomapi) + +`kmcomapi.dproj` gets built **twice** by its `build.sh` — once for +Win32 (`kmcomapi.dll`), once for Win64 (`kmcomapi.x64.dll`, renamed +post-build). Two consecutive CE prompts fire for the same `.dproj`. +Between them, **change Delphi's Platform dropdown**: first prompt → +Win32, second prompt → Win64. If you build both as Win32, the Win64 +output isn't produced and the script fails at `mv: cannot stat +'bin/Win64/Debug/kmcomapi.dll'`. + +### Full clean before rebuild if you see debug-section errors + +`sentrytool_delphiprep` can fail on a partially-built `.dll`/`.exe` +with `ERROR: This executable has a debug section. Not able to update +this file.` This happens when Delphi produced a partial binary with +an already-populated debug section (usually from a prior attempt). +Wipe the project's build state fully: + +```bash +rm -rf windows/src/engine//bin windows/src/engine//obj +``` + +Then rerun that project's `build.sh` and rebuild in Delphi from +scratch. + +> [!IMPORTANT] +> After an IDE build following any `.res`, manifest, version, or icon +> change: right-click → **Clean, then Build**. An incremental Build +> silently embeds the stale cached `.res`. + +## 5. Local-only: uiAccess strip for overlaid keyman.exe + +Windows refuses to launch unsigned binaries declaring +`uiAccess="true"` (error 8235). To run an unsigned dev `keyman.exe`, +swap in the pre-existing non-elevated manifest: + +```bash +cd windows/src/engine/keyman +./build.sh debug-manifest +``` + +That copies `debug-manifest.in` over `manifest.in` and regenerates +`manifest.res`. Then Clean + Build `keyman.dproj` in the IDE and +re-run `windows/src/engine/build.sh install` elevated. + +Trade-off: keyboard injection into elevated apps stops working under +the debug manifest. Not committed. Revert with +`git checkout -- windows/src/engine/keyman/manifest.in`. + +## 6. Debugging + +Nothing here is CE-specific — the same debugging setup applies under +Pro. + +* **DLLs (kmcomapi, keymanhp):** Run → Parameters → **Host + Application** = `\kmshell.exe`. +* **Long-running processes (kmshell, TSF text service):** Run → + **Attach to Process**. +* **C++ engine pieces (keyman32, kmtip, mcompile):** Visual Studio → + Attach to Process; load `.pdb` from `windows/bin/`. Stepping + between `keyman.exe` (Delphi) and `keyman32.dll` (C++) requires + `windbg` — out of scope here. + +## 7. Troubleshooting + +### `This version of the product does not support command line compiling` + +CE block on `dcc32`. Set `KEYMAN_DELPHI_CE=1` (§2). If `build.sh` +still tries to invoke Delphi as a transitive dep, build the upstream +tool first or pass `--no-deps`. + +### `File ...\windows\lib\keyman_components.bpl does not exist` / `F1026 File not found: MessageIdentifierConsts.pas` + +Codegen (a). Run `./windows/src/global/delphi/build.sh build` — that +script builds `keyman_components.dproj` (produces `keyman_components.bpl` +at `windows/lib/`) AND runs `devtools -buildmessageconstants` to +regenerate `MessageIdentifierConsts.pas`. Neither is produced by +`devtools/build.sh` on its own. + +### `F1026 File not found: Keyman.Setup.System.Locale..pas` + +Codegen (a'). Run `./windows/src/desktop/setup/build.sh build` — +that script's post-build runs `devtools -buildsetupstrings` to +regenerate the ~32 locale `.pas` files. + +### `F1026 File not found: Keyman.System.Standards.BCP47SubtagRegistry.pas` + +Codegen (b). Rerun +`common/windows/delphi/tools/build_standards_data/build.sh build`. + +### `F1026 File not found: tsysinfo_x64.res` + +Dependency (c). Let `windows/src/engine/build.sh` sequence +`tsysinfox64` before `tsysinfo`; don't skip prompts. + +### `F1026 File not found: version.res` or `manifest.res` + +Pre-build resource compilation didn't run. Re-invoke `build.sh build` +for the project — it runs `build_version.res` / `build_manifest.res` +before the IDE prompt. + +### `F1026 File not found: jvcl.inc` / `jedi/jedi.inc` / `jcl.inc` + +Library Search Paths not registered. See §3. Close and reopen Delphi +after the registry edit. + +### `F2613 Unit 'JvComponentBase' not found` + +JVCL `run/` (or `design/`) missing from Library Search Path. See §3. + +### `SKApplicationTitle has had a fatal error` on kmshell launch + +kmshell can't find the Keyman install-path registry key it reads via +`TKeymanPaths.KeymanDesktopInstallPath()`. Either install Keyman 19 +from https://keyman.com/windows so the key is populated, or set up +the required support files manually alongside the dev build. + +### `Class not registered {CF46549D-...}` on kmshell launch + +`kmcomapi.dll` not registered. From an elevated Git +Bash: `windows/src/engine/kmcomapi/build.sh install`. + +### `Could not find keyman.exe (error=8235)` + +Windows blocked an unsigned `uiAccess="true"` binary. See §5. + +### Delphi pops up an "Unsupported CEF version" dialog + +CEF4Delphi_Binary checkout doesn't match `common/windows/CEF_VERSION.md`. +Not CE-specific — fix per +[windows.md § KEYMAN_CEF4DELPHI_ROOT](windows.md#keyman_cef4delphi_root). + +### `Build succeeded... Time Elapsed 00:00:0X.XX` in terminal, then `cp: cannot stat '...'` / `File ... does not exist` + +`KEYMAN_DELPHI_CE` isn't set in the shell that ran `build.sh`, so +`delphi_msbuild` invoked `msbuild.exe` directly. On CE that produces +a fake success with no output. See §2 for the shell-verification +step — this is the single most common failure signature in the whole +CE workflow. + +### `SignTool Error: File not found: ...keymantest-sha1.pfx` + +Test-signing certificates never generated. Run once: +`./common/windows/delphi/tools/certificates/build.sh certificates` +(see §1). + +### `ERROR: This executable has a debug section. Not able to update this file.` + +`sentrytool_delphiprep` complaining about a residual debug section +from a partial prior build. Fully wipe the project's `bin/` and +`obj/` and rebuild from scratch (see §4 sub-section). + +### `EFOpenError: Cannot open file "...\bin\Win32\Debug\.exe". The system cannot find the path specified.` + +Delphi's Config dropdown was on **Release** (or the Platform on +**Win64**) when you Built, so the output landed at +`bin/Win32/Release/.exe` and `sentrytool_delphiprep` +can't find the Debug path. Switch Config to **Debug**, Platform to +**Win32**, Clean + Build. See §4 "Verify Configuration + Platform". + +### `mv: cannot stat 'bin/Win64/Debug/.dll'` + +You built kmcomapi (or another multi-platform Delphi package) with +Delphi's Platform dropdown set to Win32 for both CE prompts. The +second prompt is for Win64 — toggle the dropdown between the two +prompts. See §4 "Multi-platform Delphi packages". diff --git a/docs/build/windows.md b/docs/build/windows.md index 9e482a894cc..0452d2fa078 100644 --- a/docs/build/windows.md +++ b/docs/build/windows.md @@ -262,6 +262,12 @@ of appropriate node versions during builds. Delphi 11, `23.0` for Delphi 12 — before running `build.sh`. The default (`20.0`, Delphi 10.3) is preserved when the variable is unset. + * If you only have access to a **Delphi Community Edition** (e.g. Delphi 12 CE), + see [windows-delphi-ce.md](windows-delphi-ce.md) for an IDE-based workflow + that papers over the missing command-line compiler. The shell-script knob + (`KEYMAN_DELPHI_CE=1`) described there is local-only and not required for + the standard 10.3 / Pro flow. + Start Delphi IDE once after installation as it will create various environment files and take you through required registration. @@ -318,6 +324,31 @@ of appropriate node versions during builds. repository to your `PATH` environment variable. This is required for Keyman's design-time packages to load in Delphi. +### Delphi build-order dependencies + +Several Keyman projects have cross-project dependencies not expressed in +`.dproj` / `.groupproj` files — a Delphi tool emits code consumed by another +Delphi project, or one project's `.res` embeds another's `.exe`. `build.sh` +sequences these correctly; they matter if you build projects individually or +out of order (for example in the IDE). What each build step produces, and for +which dependent: + +| # | Producer → Consumer | Failure if skipped | +|---|---------------------|---------------------| +| a | `windows/src/global/delphi/build.sh` → `devtools -buildmessageconstants` → `MessageIdentifierConsts.pas` consumed by keyman.dproj, kmshell.dproj, plus `keyman_components.bpl` → `windows/lib/` | `F1026 File not found: 'MessageIdentifierConsts.pas'` OR `File ... keyman_components.bpl does not exist` | +| a' | `windows/src/desktop/setup/build.sh` → `devtools -buildsetupstrings` → ~32 `Keyman.Setup.System.Locale..pas` consumed by setup.dproj | `F1026 File not found: 'Keyman.Setup.System.Locale..pas'` | +| b | `common/windows/delphi/tools/build_standards_data/build.sh` → 5 BCP-47 registry `.pas` files → TIKE.dproj | `F1026 File not found: 'Keyman.System.Standards.BCP47SubtagRegistry.pas'` | +| c | `windows/src/engine/tsysinfo/build.sh` → auto-invokes tsysinfox64 publish, copies exe, runs `rc.exe` → `tsysinfo_x64.res` embedded by tsysinfo.dproj | `F1026 File not found: 'tsysinfo_x64.res'` | +All generated `.pas` files are `.gitignored` and must be regenerated after +`git clean -fdx`. + +Distinct from build order, a few components must be **installed or registered +to run** (not to compile): `kmcomapi.dll` registered via `regsvr32` (or +`windows/src/engine/kmcomapi/build.sh install`), `kmcmplib-19.dll` present for +Keyman Developer, and `keyman.exe` / `kmshell.exe` able to launch. Missing +these surfaces as **runtime** errors — `Class not registered`, +`kmcmplib-19.dll not found`, `"Keyman failed to start"` — not build errors. + ### KEYMAN_CEF4DELPHI_ROOT Keyman and Keyman Developer use Chromium Embedded Framework. The source repo is