diff --git a/.gitignore b/.gitignore
index 7b872195880..dd7d41d3709 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
diff --git a/common/windows/delphi/components/FixedTrackbar.pas b/common/windows/delphi/components/FixedTrackbar.pas
index 50cb444a55f..096d6340415 100644
--- a/common/windows/delphi/components/FixedTrackbar.pas
+++ b/common/windows/delphi/components/FixedTrackbar.pas
@@ -64,16 +64,16 @@ procedure TTntFixedDrawGrid.WMEraseBkgnd(var Message: TMessage);
Tested on VER320 (10.2)
Tested on VER330 (10.3) - 29 Oct 2019 - mcdurdin
+ TODO: Not yet fully verified against Vcl.Grids.pas in VER350 (11) or VER360 (12)
}
-{$IFNDEF VER340}
-{$MESSAGE WARN 'Not yet checked against Delphi 10.4'}
-{$IFNDEF VER330}
-{$IFNDEF VER320}
+{$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'}
+{$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}
+{$IFEND}
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/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;
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..9af0ac60e96 100644
--- a/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas
+++ b/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas
@@ -44,10 +44,15 @@ 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
-{$IFNDEF VER330}
- ERROR! Check if this is still needed with Delphi update
-{$ENDIF}
+ // We may need to check this with updated versions of Delphi.
+ // 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)}
+ // Verified against Delphi 10.3 (VER330)
+{$ELSE}
+ {$MESSAGE ERROR 'Check if Indy URL UTF-8 handling is still needed with Delphi update'}
+{$IFEND}
SetLength(s, p.Length);
for i := 1 to p.Length do
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/jedi/jvcl/jvcl/run/JvComponent.pas b/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas
index 1877ee48efe..9c0cd1e9719 100644
--- a/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas
+++ b/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas
@@ -123,8 +123,10 @@ constructor TJvForm.Create(AOwner: TComponent);
finally
Exclude(FFormState, fsCreating);
end;
+ {$IFDEF HAS_PROPERTY_OLDCREATEORDER}
if OldCreateOrder then
DoCreate;
+ {$ENDIF HAS_PROPERTY_OLDCREATEORDER}
end;
finally
GlobalNameSpace.EndWrite;
diff --git a/developer/src/ext/mbcolor/mxs.inc b/developer/src/ext/mbcolor/mxs.inc
index 41b82ad96e5..358ee4b84b3 100644
--- a/developer/src/ext/mbcolor/mxs.inc
+++ b/developer/src/ext/mbcolor/mxs.inc
@@ -7,6 +7,26 @@
{$define DELPHI_10_UP}
{$endif}
+ // 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}
+ {$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/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 edc756340ff..0452d2fa078 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
```
@@ -257,6 +257,17 @@ 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.
+
+ * 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.
@@ -313,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
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/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/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
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 @@
-
+
-
+
-
+
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);