From f730e6fc4fc92606fe57b051280c621a6406ecd1 Mon Sep 17 00:00:00 2001 From: sabine_schmitt Date: Fri, 21 Aug 2026 08:47:07 +0200 Subject: [PATCH 01/30] chore(mac): update setup document - add comment on use of bash 4 and XCode 26.2 /XCode 26.3 --- docs/build/macos.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/build/macos.md b/docs/build/macos.md index 99d6d9997e..76a6577ea1 100644 --- a/docs/build/macos.md +++ b/docs/build/macos.md @@ -28,6 +28,7 @@ The following projects **cannot** be built on macOS: ## System Requirements * Minimum macOS version: macOS Catalina 10.15 or Big Sur 11.0 +(make sure bash 4.0 is installed and used as modern macOS defaults to Zsh as its primary system shell) **Note:** to make a fully M1-compatible release build of Keyman for macOS (for the setup Applescript), Big Sur 11.0 is required, as osacompile on earlier @@ -41,6 +42,8 @@ Many dependencies are only required for specific projects. * XCode (iOS, macOS) 12.4 or later is needed only for Keyman for macOS and Keyman for iOS. + (Be sure to install XCode 26.2 or 26.3 maximum as they use clang 17.0 . Higher versions of XCode use Clang 21.0 which may not work) + * Install from App Store * Accept the Xcode license: `sudo xcodebuild -license accept` From 11a920711328163186c1d5959decdcfb9ee2414a Mon Sep 17 00:00:00 2001 From: SabineSIL <86713187+SabineSIL@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:03:11 +0200 Subject: [PATCH 02/30] Apply suggestions from code review Co-authored-by: Marc Durdin --- docs/build/macos.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/build/macos.md b/docs/build/macos.md index 6622645229..c37b1d6bd9 100644 --- a/docs/build/macos.md +++ b/docs/build/macos.md @@ -40,9 +40,8 @@ Rosetta 2 installed. Many dependencies are only required for specific projects. -* XCode (iOS, macOS) 12.4 or later is needed only for Keyman for macOS and Keyman - for iOS. - (Be sure to install XCode 26.2 or 26.3 maximum as they use clang 17.0 . Higher versions of XCode use Clang 21.0 which may not work) +* XCode (iOS, macOS) 26.2 or 26.3 (not higher at this time, see #16416) + is needed only for Keyman for macOS and Keyman for iOS (). * Install from App Store * Accept the Xcode license: `sudo xcodebuild -license accept` @@ -108,7 +107,7 @@ PATH="$HOMEBREW_PREFIX/opt/coreutils/libexec/gnubin:$PATH" ## KeymanWeb Dependencies -* node.js, emscripten 3.1.46 or later +* node.js 20.0, emscripten 3.1.64 ### node.js @@ -144,7 +143,8 @@ If you are updating an existing install of Emscripten: 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 ``` @@ -207,7 +207,9 @@ components such as emulator images and SDK updates. ```shell brew install node emscripten -``` +* node.js 20.0, emscripten 3.1.64 + +See section KeymanWeb Dependencies for installation ## Optional Tools From 5d073fa44f4905eac9f6b9d4d84d7134f12af0b8 Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Fri, 21 Aug 2026 23:21:34 +0300 Subject: [PATCH 03/30] fix(windows): resync caps lock state when Keyman keyboard is activated The Caps Lock and Num Lock flags in Globals::ShiftState() are updated only as key events pass through the engine, so they go stale when the toggle is changed while a non-Keyman layout is active. The existing resync in GetCapsAndNumlockState() runs on window focus change, which a keyboard switch does not trigger, so the core processor was told Caps Lock was off until the user toggled it twice. Extract the toggle resync as RefreshToggleState() and call it from TIPActivateKeyboard(), which is the profile activation path for both Win+Space and the Windows language selector. Fixes #16422 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011GdFpFi5eqJP6C1YjuQytY --- windows/src/engine/keyman32/appint/aiTIP.cpp | 3 +++ windows/src/engine/keyman32/capsstate.cpp | 18 ++++++++++++++++++ windows/src/engine/keyman32/capsstate.h | 1 + .../src/engine/keyman32/kmhook_getmessage.cpp | 6 +----- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index a2baada55b..8401ccb8ee 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -67,6 +67,9 @@ extern "C" __declspec(dllexport) BOOL WINAPI TIPIsKeymanRunning() { extern "C" __declspec(dllexport) BOOL WINAPI TIPActivateKeyboard(GUID *profile) { // I3581 PKEYMAN64THREADDATA _td = ThreadGlobals(); if(!_td) return FALSE; + + RefreshToggleState(); // #16422 - a keyboard switch does not change window focus + if(profile != NULL) { for(int i = 0; i < _td->nKeyboards; i++) { for(int j = 0; j < _td->lpKeyboards[i].nProfiles; j++) { diff --git a/windows/src/engine/keyman32/capsstate.cpp b/windows/src/engine/keyman32/capsstate.cpp index 4f9b466e9e..4a6631f5de 100644 --- a/windows/src/engine/keyman32/capsstate.cpp +++ b/windows/src/engine/keyman32/capsstate.cpp @@ -29,3 +29,21 @@ BOOL IsCapsLockOn(void) { return GetKeyState(VK_CAPITAL) & 1; } + +/* + RefreshToggleState: + + Resyncs the caps and numlock state, because it may have been changed while + Keyman was not aware of it +*/ +void RefreshToggleState(void) { + DWORD n = Globals::get_ShiftState(); + + if (GetKeyState(VK_CAPITAL) & 1) *Globals::ShiftState() |= CAPITALFLAG; + else *Globals::ShiftState() &= ~CAPITALFLAG; + + if (GetKeyState(VK_NUMLOCK) & 1) *Globals::ShiftState() |= NUMLOCKFLAG; + else *Globals::ShiftState() &= ~NUMLOCKFLAG; + + SendDebugMessageFormat("Enter: %x Exit: %x", n, Globals::get_ShiftState()); +} diff --git a/windows/src/engine/keyman32/capsstate.h b/windows/src/engine/keyman32/capsstate.h index d28ba9c0db..0be449db30 100644 --- a/windows/src/engine/keyman32/capsstate.h +++ b/windows/src/engine/keyman32/capsstate.h @@ -20,5 +20,6 @@ #define __CAPSSTATE_H BOOL IsCapsLockOn(void); +void RefreshToggleState(void); #endif diff --git a/windows/src/engine/keyman32/kmhook_getmessage.cpp b/windows/src/engine/keyman32/kmhook_getmessage.cpp index 4c11dc31b5..4efd487946 100644 --- a/windows/src/engine/keyman32/kmhook_getmessage.cpp +++ b/windows/src/engine/keyman32/kmhook_getmessage.cpp @@ -418,11 +418,7 @@ ProcessWMKeymanControl(WPARAM wParam, LPARAM lParam) { void GetCapsAndNumlockState() { // I4793 DWORD n = Globals::get_ShiftState(); - if(GetKeyState(VK_NUMLOCK) & 1) *Globals::ShiftState() |= NUMLOCKFLAG; - else *Globals::ShiftState() &= ~NUMLOCKFLAG; - - if(GetKeyState(VK_CAPITAL) & 1) *Globals::ShiftState() |= CAPITALFLAG; - else *Globals::ShiftState() &= ~CAPITALFLAG; + RefreshToggleState(); if(GetKeyState(VK_SHIFT) < 0) *Globals::ShiftState() |= K_SHIFTFLAG; else *Globals::ShiftState() &= ~K_SHIFTFLAG; From 19680dca467f5aa205106e0d75e6dcf0aadfea39 Mon Sep 17 00:00:00 2001 From: SabineSIL <86713187+SabineSIL@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:04:32 +0200 Subject: [PATCH 04/30] chore(mac): add an info on macOS version Clarify system requirements for macOS, specifying Xcode versions and bash installation. --- docs/build/macos.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/build/macos.md b/docs/build/macos.md index c37b1d6bd9..92203ddb6d 100644 --- a/docs/build/macos.md +++ b/docs/build/macos.md @@ -28,7 +28,8 @@ The following projects **cannot** be built on macOS: ## System Requirements * Minimum macOS version: macOS Catalina 10.15 or Big Sur 11.0 -(make sure bash 4.0 is installed and used as modern macOS defaults to Zsh as its primary system shell) +(For using Xcode 26.2 or 26.3 macOS Sequoia 15.6 or later is required. +Make sure bash 4.0 is installed and used as modern macOS defaults to Zsh as its primary system shell) **Note:** to make a fully M1-compatible release build of Keyman for macOS (for the setup Applescript), Big Sur 11.0 is required, as osacompile on earlier From f586e94cf5ae4052ce97d3c3ebe20bb7d95b4f88 Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Sat, 22 Aug 2026 10:25:22 +0300 Subject: [PATCH 05/30] Update windows/src/engine/keyman32/appint/aiTIP.cpp Co-authored-by: Marc Durdin --- windows/src/engine/keyman32/appint/aiTIP.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index 8401ccb8ee..be657e4a12 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -68,7 +68,7 @@ extern "C" __declspec(dllexport) BOOL WINAPI TIPActivateKeyboard(GUID *profile) PKEYMAN64THREADDATA _td = ThreadGlobals(); if(!_td) return FALSE; - RefreshToggleState(); // #16422 - a keyboard switch does not change window focus + RefreshToggleState(); if(profile != NULL) { for(int i = 0; i < _td->nKeyboards; i++) { From 13326b409132e4c226a927244920d40a305e221c Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Sat, 22 Aug 2026 10:25:46 +0300 Subject: [PATCH 06/30] Update windows/src/engine/keyman32/capsstate.cpp Co-authored-by: Marc Durdin --- windows/src/engine/keyman32/capsstate.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/windows/src/engine/keyman32/capsstate.cpp b/windows/src/engine/keyman32/capsstate.cpp index 4a6631f5de..7d1e1eb096 100644 --- a/windows/src/engine/keyman32/capsstate.cpp +++ b/windows/src/engine/keyman32/capsstate.cpp @@ -30,12 +30,10 @@ BOOL IsCapsLockOn(void) { return GetKeyState(VK_CAPITAL) & 1; } -/* - RefreshToggleState: - - Resyncs the caps and numlock state, because it may have been changed while - Keyman was not aware of it -*/ +/** + * Resync the Caps Lock and Num Lock state, because it may have been + * changed while Keyman was not aware of it + */ void RefreshToggleState(void) { DWORD n = Globals::get_ShiftState(); From dcbfcf810f6253f3986af913610116c77ac4638e Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Sat, 22 Aug 2026 10:26:23 +0300 Subject: [PATCH 07/30] Update windows/src/engine/keyman32/capsstate.cpp Co-authored-by: Marc Durdin --- windows/src/engine/keyman32/capsstate.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/src/engine/keyman32/capsstate.cpp b/windows/src/engine/keyman32/capsstate.cpp index 7d1e1eb096..b87ad92ff5 100644 --- a/windows/src/engine/keyman32/capsstate.cpp +++ b/windows/src/engine/keyman32/capsstate.cpp @@ -35,7 +35,7 @@ BOOL IsCapsLockOn(void) { * changed while Keyman was not aware of it */ void RefreshToggleState(void) { - DWORD n = Globals::get_ShiftState(); + DWORD previousShiftState = Globals::get_ShiftState(); if (GetKeyState(VK_CAPITAL) & 1) *Globals::ShiftState() |= CAPITALFLAG; else *Globals::ShiftState() &= ~CAPITALFLAG; From fb91d2fc7526cd9eaab1675155863e77e1255616 Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Sat, 22 Aug 2026 10:26:53 +0300 Subject: [PATCH 08/30] Update windows/src/engine/keyman32/capsstate.cpp Co-authored-by: Marc Durdin --- windows/src/engine/keyman32/capsstate.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/src/engine/keyman32/capsstate.cpp b/windows/src/engine/keyman32/capsstate.cpp index b87ad92ff5..85d58900b7 100644 --- a/windows/src/engine/keyman32/capsstate.cpp +++ b/windows/src/engine/keyman32/capsstate.cpp @@ -43,5 +43,5 @@ void RefreshToggleState(void) { if (GetKeyState(VK_NUMLOCK) & 1) *Globals::ShiftState() |= NUMLOCKFLAG; else *Globals::ShiftState() &= ~NUMLOCKFLAG; - SendDebugMessageFormat("Enter: %x Exit: %x", n, Globals::get_ShiftState()); + SendDebugMessageFormat("Enter: %x Exit: %x", previousShiftState, Globals::get_ShiftState()); } From f07e05fb159b9c807c00875cf30f841b5b9f6c33 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Fri, 21 Aug 2026 10:24:02 +0200 Subject: [PATCH 09/30] chore(web): skip guide-examples.tests.ts until race is fixed The guide-examples.tests.ts have a race so that they occasionally fail (#16167). This change disables the tests until we have a fix in place. Related-to: #16167 Build-bot: skip build:web Test-bot: skip --- web/src/test/auto/e2e/guide-examples.tests.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web/src/test/auto/e2e/guide-examples.tests.ts b/web/src/test/auto/e2e/guide-examples.tests.ts index 19f2fb4bf8..13deb44472 100644 --- a/web/src/test/auto/e2e/guide-examples.tests.ts +++ b/web/src/test/auto/e2e/guide-examples.tests.ts @@ -9,7 +9,7 @@ async function setTimeoutAndLoadPage(page: Page, url: string): Promise { await loadPage(page, url); } -test.describe('First example from the guide', function () { +test.describe.skip('First example from the guide', function () { const beforeEach = async (page: Page) => { await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__first-example.html'); } @@ -41,7 +41,7 @@ test.describe('First example from the guide', function () { }); }); -test.describe('Auto-control example from the guide', function () { +test.describe.skip('Auto-control example from the guide', function () { const beforeEach = async (page: Page) => { await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__auto-control.html'); } @@ -69,7 +69,7 @@ test.describe('Auto-control example from the guide', function () { }); }); -test.describe('Control-by-control example from the guide', function () { +test.describe.skip('Control-by-control example from the guide', function () { const beforeEach = async (page: Page) => { await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__control-by-control.html'); } @@ -119,7 +119,7 @@ test.describe('Control-by-control example from the guide', function () { }); }); -test.describe('Full manual control example from the guide', function () { +test.describe.skip('Full manual control example from the guide', function () { const beforeEach = async (page: Page) => { await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__full-manual-control.html'); } @@ -203,7 +203,7 @@ test.describe('Full manual control example from the guide', function () { }); }); -test.describe('Manual control example from the guide', function () { +test.describe.skip('Manual control example from the guide', function () { const beforeEach = async (page: Page) => { await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__manual-control.html'); } From 446559c2210ffa9df5431285f09f87ed60ef795a Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Mon, 24 Aug 2026 09:19:47 -0400 Subject: [PATCH 10/30] Update windows/src/engine/keyman32/capsstate.cpp Co-authored-by: rc-swag <58423624+rc-swag@users.noreply.github.com> --- windows/src/engine/keyman32/capsstate.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/windows/src/engine/keyman32/capsstate.cpp b/windows/src/engine/keyman32/capsstate.cpp index 85d58900b7..bc3fc36e5b 100644 --- a/windows/src/engine/keyman32/capsstate.cpp +++ b/windows/src/engine/keyman32/capsstate.cpp @@ -31,8 +31,10 @@ BOOL IsCapsLockOn(void) { } /** - * Resync the Caps Lock and Num Lock state, because it may have been - * changed while Keyman was not aware of it + * Resync the Caps Lock and Num Lock state cache. + * + * Use when state may be stale after focus changes or when the toggle keys + * changed while this Keyman engine instance was not processing key events. */ void RefreshToggleState(void) { DWORD previousShiftState = Globals::get_ShiftState(); From c54d6405bc5e05da24e7d99c237a9145b9fcec5f Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Mon, 24 Aug 2026 13:01:39 -0500 Subject: [PATCH 11/30] auto: increment master version to 19.0.275 Test-bot: skip Build-bot: skip --- HISTORY.md | 5 +++++ VERSION.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 882fe4ce96..b80e08f694 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,10 @@ # Keyman Version History +## 19.0.274 alpha 2026-08-24 + +* maint(windows): move to vs2026 arm runner for GHA (#16418) +* chore(web): skip guide-examples.tests.ts until race is fixed (#16427) + ## 19.0.273 alpha 2026-08-22 * feat(linux): use display language for keyboard search (#16393) diff --git a/VERSION.md b/VERSION.md index e1f2426df6..32062d3f5c 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.274 \ No newline at end of file +19.0.275 \ No newline at end of file From afd4d7dcc9b44a2a6c5a567ac5e3a0c79b39e17a Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 25 Aug 2026 07:51:04 +0200 Subject: [PATCH 12/30] docs(mac): refresh build/setup docs and remove APPSTORECONNECT_PROVIDER Env var `APPSTORECONNECT_PROVIDER` is unused, so remove it from docs and requirements. Refresh the build and setup docs to reflect modern XCode requirements more accurately and clarify use of `--quick` option for `install`. Test-bot: skip --- mac/README.md | 119 +++++++++++++++++++++++++++++++------------------- mac/build.sh | 7 ++- 2 files changed, 76 insertions(+), 50 deletions(-) diff --git a/mac/README.md b/mac/README.md index f15997e827..b4b500dfc3 100644 --- a/mac/README.md +++ b/mac/README.md @@ -9,94 +9,121 @@ Keyman for macOS can be built from a command line (preferred) or Xcode. ### Setting up your code signing and notarization environment. -With macOS 10.14 and later, Keyman must be notarized in order to be permitted to interact with -keyboard input. You have two options for local builds: +With macOS 10.14 and later, Keyman must be notarized in order to be permitted to +interact with keyboard input. You have two options for local builds: 1. You can disable security checks for the system with the command: - `sudo spctl --master-disable` + `sudo spctl --master-disable` - This has obvious security implications and the risk is up to you. However, builds are - much, much, faster than with the alternative option below, and for extensive local - debugging is far less painful. + This has obvious security implications and the risk is up to you. However, + builds are much, much, faster than with the alternative option below, and for + extensive local debugging is far less painful. -2. Or, you must sign and notarize every build. See below. (Use `-deploy local`) +2. Or, you must sign and notarize every build. This requires an Apple Developer + Program subscription or access to Developer ID code signing certificates -- a + self-signed certificate will not work. ### Signing and notarizing builds -Keyman must be signed then notarized by Apple, even for local test builds. This requires additional -configuration for your build environment. +Unless security checks are disabled, Keyman must be signed then notarized by +Apple, even for local test builds. This requires additional configuration for +your build environment. -1. First, open XCode, Settings, Accounts, and select Manage Certificates for the identity - you wish to use for signing. Click **+** and select **Apple Development**. A - certificate will then be generated and listed in your Keychain. +1. First, open XCode, Settings, Apple Accounts, ``, and select Manage + Certificates for the identity you wish to use for signing. Find the + appropriate Development ID certificate. 2. Find the SHA-1 hash. To find the certificate in terminal: - `security find-certificate -Z -c "" -a` + `security find-certificate -Z -c "" -a` - (If you have more than one, you may need to use Keychain Access to differentiate). - Copy the SHA-1 hash from this command's output. + (If you have more than one, you may need to use Keychain Access to + differentiate). Copy the SHA-1 hash from this command's output. - Take note also of the Development Team ID, found in parentheses at the end of the - `labl` blob line. + Take note also of the Development Team ID, found in parentheses at the end of + the `labl` blob line. -2. Determine the Apple ID details in order to run a build. You may wish to create an - App-Specific Password at https://appleid.apple.com/ and use this. Your Shortname will - be either your 10-digit Team Identifier or a shortname that can be extracted with - the following command: +2. Determine the Apple ID details in order to run a build. You will need to + create an App-Specific Password at https://appleid.apple.com/. - `/Applications/Xcode.app/Contents/Developer/usr/bin/iTMSTransporter -m provider -u '' -p '' -account_type itunes_connect -v off` + Note: Use your Apple ID for `` and the app-specific password you + generated above for ``. You'll use these in the next step. - Note: Use your Apple ID for `` and the app-specific password you generated above - for ``. You'll use these in the next step as well. +3. Add the following environment variables to `mac/localenv.sh` (or to + `~/.bashrc`), replacing with the values you collected in the previous steps: -3. Add the following environment variables to mac/localenv.sh (or to your .bashrc file), replacing with the - values you collected in the previous steps: - - export CERTIFICATE_ID= - export APPSTORECONNECT_PROVIDER= - export APPSTORECONNECT_USERNAME= - export APPSTORECONNECT_PASSWORD= - export DEVELOPMENT_TEAM= + ```sh + export CERTIFICATE_ID= + export APPSTORECONNECT_USERNAME= + export APPSTORECONNECT_PASSWORD= + export DEVELOPMENT_TEAM= + ``` ### Compiling from Command Line To build Keyman for macOS, do the following: + 1. Open a Terminal window. 2. Build using `mac/build.sh clean configure build` -Note: If Carthage prompts you to allow it access to your github credentials, it's fine to click Deny. +Note: If Carthage prompts you to allow it access to your github credentials, +it's fine to click Deny. ### Running Keyman -1. Deploy Keyman locally using `mac/build.sh install`. - * This will notarize the app, signing with your local credentials if not already signed, and copy **keyman/mac/Keyman4MacIM/build/Debug/Keyman.app** to **~/Library/Input Methods** -2. If running for the first time, follow the installation instructions at [Install Keyman for macOS]. +1. Deploy Keyman locally: -You can also use `mac/build.sh build install` to do a single-step build, notarize, and deploy (see above for faster options). + * Without notarization (requires `spctl --master-disable`), recommended: + + `mac/build.sh install --quick` + + This will copy **keyman/mac/Keyman4MacIM/build/Debug/Keyman.app** + to **~/Library/Input Methods** + + * With notarization (requires appropriate certificates / Apple Developer + subscription): + + `mac/build.sh install` + + This will notarize the app, signing with your local credentials if not + already signed, and copy **keyman/mac/Keyman4MacIM/build/Debug/Keyman.app** + to **~/Library/Input Methods** + +2. If running for the first time, follow the installation instructions at + [Install Keyman for macOS]. + +You can also use `mac/build.sh build install [--quick]` to do a single-step +build, notarize, and deploy (see above for faster options). ### Compiling from Xcode -To build using Xcode, you will need to build KeymanEngine4Mac first and then build Keyman4MacIM. The very _first_ time after getting the source code (and any time the Podfile is edited; e.g., to install new pods), you need to go to **keyman/mac/Keyman4MacIM** and run "pod install" in Terminal. +To build using Xcode, you will need to build KeymanEngine4Mac first and then +build Keyman4MacIM. The very _first_ time after getting the source code (and any +time the Podfile is edited; e.g., to install new pods), you need to go to +**keyman/mac/Keyman4MacIM** and run `pod install` in Terminal. 1. Launch Xcode 2. Open **keyman/mac/KeymanEngine4Mac/KeymanEngine4Mac.xcodeproj** 3. Build the project: Product > Build (or Cmd-B) 4. Open **keyman/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj** -5. If you do not have signing credentials from the core development team, disable code signing in Xcode. - 1. Open the Project Navigator: View > Navigators > Show Project Navigator (Cmd-1) +5. If you do not have signing credentials from the core development team, + disable code signing in Xcode. + 1. Open the Project Navigator: View > Navigators > Show Project Navigator + (Cmd-1) 2. Select Keyman4MacIM and click Build Settings - 3. In the Signing section, change Code Signing Identity to Don't Code Sign. This will modify - **Keyman4MacIM.xcodeproj**. Do not commit the change. -6. Build the project. Refer to [Running Keyman](#running-keyman) on how to install the app. + 3. In the Signing section, change Code Signing Identity to Don't Code Sign. + This will modify **Keyman4MacIM.xcodeproj**. Do not commit the change. +6. Build the project. Refer to [Running Keyman](#running-keyman) on how to + install the app. ### Testing -The Keyman4Mac project builds a test-bed app that can be used to test keyboards without installing the input method. -It can also be used as reference for the usage of Keyman Engine. +The Keyman4Mac project builds a test-bed app that can be used to test keyboards +without installing the input method. It can also be used as reference for the +usage of Keyman Engine. -Keyman4Mac tests are run using `./build.sh -test -no-codesign`. +Keyman4Mac tests are run using `./build.sh test`. ### A note about Homebrew and xcodebuild diff --git a/mac/build.sh b/mac/build.sh index 66064b4300..dd1e60ac87 100755 --- a/mac/build.sh +++ b/mac/build.sh @@ -87,9 +87,8 @@ UPLOAD_SENTRY=false # Import local environment variables for build # # /mac/localenv.sh can be used to define CERTIFICATE_ID, -# APPSTORECONNECT_PROVIDER, APPSTORECONNECT_USERNAME, -# APPSTORECONNECT_PASSWORD, DEVELOPMENT_TEAM variables; -# see /mac/README.md for details. +# APPSTORECONNECT_USERNAME, APPSTORECONNECT_PASSWORD, DEVELOPMENT_TEAM +# variables; see /mac/README.md for details. # if [[ -f "$THIS_SCRIPT_PATH/localenv.sh" ]]; then . "$THIS_SCRIPT_PATH/localenv.sh" @@ -223,7 +222,7 @@ do_notarize() { builder_die "Notarization and signed executable is required for deployment, even locally. Specify CERTIFICATE_ID environment variable for custom certificate." fi - if [ -z "${APPSTORECONNECT_PROVIDER+x}" ] || [ -z "${APPSTORECONNECT_USERNAME+x}" ] || [ -z "${APPSTORECONNECT_PASSWORD+x}" ]; then + if [ -z "${APPSTORECONNECT_USERNAME+x}" ] || [ -z "${APPSTORECONNECT_PASSWORD+x}" ]; then builder_die "Appstoreconnect Apple ID credentials must be configured in environment. See README.md for details." fi From 27b60a4fcc0c4e61bc324c59a0d5733a75e2d2e3 Mon Sep 17 00:00:00 2001 From: Joshua Horton Date: Tue, 25 Aug 2026 13:22:04 -0500 Subject: [PATCH 13/30] change(web): enhance missing-stubs error Relates-to: #15425 Build-bot: skip build:web Test-bot: skip --- web/src/engine/src/main/contextManagerBase.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/src/engine/src/main/contextManagerBase.ts b/web/src/engine/src/main/contextManagerBase.ts index 1f3e7f95dc..9718736096 100644 --- a/web/src/engine/src/main/contextManagerBase.ts +++ b/web/src/engine/src/main/contextManagerBase.ts @@ -327,7 +327,10 @@ export abstract class ContextManagerBase if(!requestedStub) { if(keyboardId) { - throw new Error("No matching stub has been registered."); + const availableStubList = this.keyboardCache.getStubList().map((stub) => { + return `${stub.KI}@${stub.KLC}` + }); + throw new Error(`No matching stub has been registered for keyboard ${keyboardId}. Available stubs: ${JSON.stringify(availableStubList)}`); } else { return { keyboard: Promise.resolve(null), From 3597a555e7270ec211305cd2db9dcea1bcd8a333 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Tue, 25 Aug 2026 12:16:01 +1000 Subject: [PATCH 14/30] fix(windows): insthelp trustinfo and add manifest Change the manifest trustinfo to asInvoker from requireAdminstrator. This was causing the preuninstaller to fail as it couldn't elevate. This also adds the manifest back into the build as it was previously removed. fixes: 15187 --- windows/src/desktop/insthelp/build.sh | 4 ++-- windows/src/desktop/insthelp/insthelp.dpr | 2 +- windows/src/desktop/insthelp/manifest.in | 18 ++++++++---------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/windows/src/desktop/insthelp/build.sh b/windows/src/desktop/insthelp/build.sh index d57a4b935f..a227162896 100755 --- a/windows/src/desktop/insthelp/build.sh +++ b/windows/src/desktop/insthelp/build.sh @@ -27,7 +27,7 @@ function do_build() { create-windows-output-folders build_version.res # TODO: why no manifest? - # build_manifest.res + build_manifest.res delphi_msbuild insthelp.dproj "//p:Platform=Win32" sentrytool_delphiprep "$WIN32_TARGET" insthelp.dpr tds2dbg "$WIN32_TARGET" @@ -39,7 +39,7 @@ function do_build() { function do_publish() { # test that (a) linked manifest exists and correct # TODO: no manifest included? - # wrap-mt -nologo -inputresource:"$WINDOWS_PROGRAM_APP/insthelp.exe" -validate_manifest + wrap-mt -nologo -inputresource:"$WINDOWS_PROGRAM_APP/insthelp.exe" -validate_manifest wrap-signcode //d "Keyman for Windows Install Helper" "$WINDOWS_PROGRAM_APP/insthelp.exe" wrap-symstore "$WINDOWS_PROGRAM_APP/insthelp.exe" //t keyman-windows diff --git a/windows/src/desktop/insthelp/insthelp.dpr b/windows/src/desktop/insthelp/insthelp.dpr index ab0e47ff7d..ad5c5eb70a 100644 --- a/windows/src/desktop/insthelp/insthelp.dpr +++ b/windows/src/desktop/insthelp/insthelp.dpr @@ -11,7 +11,7 @@ uses UserMessages in '..\..\..\..\common\windows\delphi\general\UserMessages.pas'; {$R version.res} -{-R manifest.res} +{$R manifest.res} begin CoInitializeEx(nil, COINIT_APARTMENTTHREADED); diff --git a/windows/src/desktop/insthelp/manifest.in b/windows/src/desktop/insthelp/manifest.in index d78e04053e..676ffc8db1 100644 --- a/windows/src/desktop/insthelp/manifest.in +++ b/windows/src/desktop/insthelp/manifest.in @@ -6,15 +6,13 @@ version="$VersionWin" processorArchitecture="x86"/> Keyman Installation Assistant - - - - - - - + + + + + + + @@ -27,4 +25,4 @@ - \ No newline at end of file + From 39a76520568eab1825c54866c6630af2bce05be4 Mon Sep 17 00:00:00 2001 From: Joshua Horton Date: Wed, 26 Aug 2026 20:47:57 +0700 Subject: [PATCH 15/30] change(web): apply MD suggestion from code review Co-authored-by: Marc Durdin --- web/src/engine/src/main/contextManagerBase.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/web/src/engine/src/main/contextManagerBase.ts b/web/src/engine/src/main/contextManagerBase.ts index 9718736096..e9ddd9a18a 100644 --- a/web/src/engine/src/main/contextManagerBase.ts +++ b/web/src/engine/src/main/contextManagerBase.ts @@ -327,9 +327,7 @@ export abstract class ContextManagerBase if(!requestedStub) { if(keyboardId) { - const availableStubList = this.keyboardCache.getStubList().map((stub) => { - return `${stub.KI}@${stub.KLC}` - }); + const availableStubList = this.keyboardCache.getStubList().map(stub => `${stub.KI}@${stub.KLC}`); throw new Error(`No matching stub has been registered for keyboard ${keyboardId}. Available stubs: ${JSON.stringify(availableStubList)}`); } else { return { From 16a1a6e7885b11d2db6b51204427f75502a001e7 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Thu, 27 Aug 2026 07:46:17 +1000 Subject: [PATCH 16/30] fix(windows): remove old TODO comments --- windows/src/desktop/insthelp/build.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/windows/src/desktop/insthelp/build.sh b/windows/src/desktop/insthelp/build.sh index a227162896..5abbe74dd6 100755 --- a/windows/src/desktop/insthelp/build.sh +++ b/windows/src/desktop/insthelp/build.sh @@ -26,7 +26,6 @@ builder_describe_outputs \ function do_build() { create-windows-output-folders build_version.res - # TODO: why no manifest? build_manifest.res delphi_msbuild insthelp.dproj "//p:Platform=Win32" sentrytool_delphiprep "$WIN32_TARGET" insthelp.dpr @@ -38,7 +37,6 @@ function do_build() { function do_publish() { # test that (a) linked manifest exists and correct - # TODO: no manifest included? wrap-mt -nologo -inputresource:"$WINDOWS_PROGRAM_APP/insthelp.exe" -validate_manifest wrap-signcode //d "Keyman for Windows Install Helper" "$WINDOWS_PROGRAM_APP/insthelp.exe" From 33e64f1eddc36c370b256c97822e6a825be35bc8 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 27 Aug 2026 08:20:21 +0200 Subject: [PATCH 17/30] refactor(windows): rename `GetCapsAndNumlockState()` to `RefreshModifierState()` Fixes: #16434 Test-bot: skip --- .../src/engine/keyman32/kmhook_getmessage.cpp | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/windows/src/engine/keyman32/kmhook_getmessage.cpp b/windows/src/engine/keyman32/kmhook_getmessage.cpp index 4efd487946..f000c2661e 100644 --- a/windows/src/engine/keyman32/kmhook_getmessage.cpp +++ b/windows/src/engine/keyman32/kmhook_getmessage.cpp @@ -68,7 +68,7 @@ void ProcessWMKeymanControlInternal(HWND hwnd, WPARAM wParam, LPARAM lParam); void ProcessWMKeymanControl(WPARAM wParam, LPARAM lParam); void ProcessWMKeyman(HWND hwnd, WPARAM wParam, LPARAM lParam); -void GetCapsAndNumlockState(); +void RefreshModifierState(); /* BOOL SysTabCtrl(HWND hwnd) @@ -354,7 +354,7 @@ void ProcessWMKeyman(HWND hwnd, WPARAM wParam, LPARAM lParam) if(IsFocusedThread()) { if(_td->app) _td->app->ResetQueue(); - GetCapsAndNumlockState(); + RefreshModifierState(); UpdateActiveWindows(); } } @@ -409,14 +409,17 @@ ProcessWMKeymanControl(WPARAM wParam, LPARAM lParam) { } } -/* - GetCapsAndNumlockState: - - Updates the global caps and numlock state when a window is focused, because it - may have been reset while Keyman was not aware of it -*/ -void GetCapsAndNumlockState() { // I4793 - DWORD n = Globals::get_ShiftState(); +/** + * Refresh Keyman's internal keyboard modifier and toggle state according + * to the Windows current keyboard state (not async state). The state may + * have been reset while Keyman was not aware of it. This is used only when + * focus changes. Note that the global values are per-architecture, so + * these may track separately. + * + * @see ProcessModifierChange() + */ +void RefreshModifierState() { // I4793 + DWORD previousShiftState = Globals::get_ShiftState(); RefreshToggleState(); @@ -435,18 +438,19 @@ void GetCapsAndNumlockState() { // I4793 if(GetKeyState(VK_RMENU) < 0) *Globals::ShiftState() |= RALTFLAG; else *Globals::ShiftState() &= ~RALTFLAG; - SendDebugMessageFormat("Enter: %x Exit: %x", n, Globals::get_ShiftState()); + SendDebugMessageFormat("Enter: %x Exit: %x", previousShiftState, Globals::get_ShiftState()); } -/* - Update the Keyman shift state based on the key event. This has to be - done from both the GetMessage hook and the TSF methods, because we don't - consistently receive modifier events in some applications (e.g. ALT in Firefox) - via TSF, and we don't receive the notifications via the GetMessage hook when - in UWP apps (#4369). - - TODO: test whether we still need the GetMessage hook for reading modifier state. -*/ +/** + * Update the Keyman shift state based on the key event. This has to be done + * from both the GetMessage hook and the TSF methods, because we don't + * consistently receive modifier events in some applications (e.g. ALT in + * Firefox) via TSF, and we don't receive the notifications via the GetMessage + * hook when in UWP apps (#4369). + * + * TODO: test whether we still need the GetMessage hook for reading modifier + * state. + */ void ProcessModifierChange(UINT key, BOOL isUp, BOOL isExtended) { // I4793 UINT flag = 0; From babb473682210900578678a3014f2f0301b373e8 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 27 Aug 2026 11:05:06 +0200 Subject: [PATCH 18/30] fix(developer): handle leading whitespace correctly in `u16tok()` A string with leading delimiters passed into `u16tok()` would not skip those delimiters. This impacted `&targets` store and other locations. Note that this tightens input parameter checks for delimiters and context as well. Fixes: #13721 Test-bot: skip --- common/cpp/km_u16.cpp | 24 +++++++++- developer/src/kmc-kmn/test/compiler.tests.ts | 36 ++++++++++++-- .../keyboards/targets-with-whitespace.kmn | 11 +++++ developer/src/kmc-kmn/test/helpers/index.ts | 21 ++++++-- developer/src/kmcmplib/src/Compiler.cpp | 31 ++++++++++-- developer/src/kmcmplib/tests/api.tests.cpp | 48 ++++++++++++++----- .../src/kmcmplib/tests/gtest-km_u16.tests.cpp | 34 +++++++++++++ 7 files changed, 180 insertions(+), 25 deletions(-) create mode 100644 developer/src/kmc-kmn/test/fixtures/keyboards/targets-with-whitespace.kmn diff --git a/common/cpp/km_u16.cpp b/common/cpp/km_u16.cpp index 78077202fc..c378946156 100644 --- a/common/cpp/km_u16.cpp +++ b/common/cpp/km_u16.cpp @@ -326,10 +326,20 @@ int u16ncmp(const KMX_WCHAR* p, const KMX_WCHAR* q, size_t count) { * @return Pointer to the first token in p */ KMX_WCHAR* u16tok(KMX_WCHAR* p, const KMX_WCHAR ch, KMX_WCHAR** ctx) { + if(ch == 0 || !ctx) { + return NULL; + } + if (!p) { p = *ctx; - if (!p) + if (!p) { return NULL; + } + } + + // skip initial delimiter + while (*p == ch) { + p++; } KMX_WCHAR* q = p; @@ -356,10 +366,20 @@ KMX_WCHAR* u16tok(KMX_WCHAR* p, const KMX_WCHAR ch, KMX_WCHAR** ctx) { * @return Pointer to the first token in p */ KMX_WCHAR* u16tok(KMX_WCHAR* p, const KMX_WCHAR* delimiters, KMX_WCHAR** ctx) { + if(!ctx || !delimiters || !(*delimiters)) { + return NULL; + } + if (!p) { p = *ctx; - if (!p) + if (!p) { return NULL; + } + } + + // skip initial delimiters + while (*p && u16chr(delimiters, *p)) { + p++; } KMX_WCHAR* q = p; diff --git a/developer/src/kmc-kmn/test/compiler.tests.ts b/developer/src/kmc-kmn/test/compiler.tests.ts index db5ea60b06..c4bc020929 100644 --- a/developer/src/kmc-kmn/test/compiler.tests.ts +++ b/developer/src/kmc-kmn/test/compiler.tests.ts @@ -1,10 +1,15 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import fs from 'node:fs'; import 'mocha'; import { assert } from 'chai'; -import { KmnCompiler } from '../src/main.js'; -import { dirname } from 'path'; -import { fileURLToPath } from 'url'; -import fs from 'fs'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; +import { KmxFileReader } from '@keymanapp/common-types'; +import { compileTestKeyboard } from './helpers/index.js'; +import { KmnCompiler } from '../src/main.js'; const __dirname = dirname(fileURLToPath(import.meta.url)).replace(/\\/g, '/'); const keyboardsDir = __dirname + '/../../../../../common/test/keyboards/'; @@ -13,6 +18,16 @@ const baselineDir = keyboardsDir + 'baseline/'; describe('Compiler class', function() { const callbacks = new TestCompilerCallbacks(this); + this.beforeEach(function() { + callbacks.clear(); + }); + + this.afterEach(function() { + if(this.currentTest?.isFailed()) { + callbacks.printMessages(); + } + }); + it('should throw on failure', async function() { const compiler = new KmnCompiler(); const callbacks : any = null; // ERROR @@ -129,4 +144,17 @@ describe('Compiler class', function() { assert.deepEqual(kvkData, kvkFixtureData); }); + it('should trim all whitespace for `&targets` store', async function() { + const result = await compileTestKeyboard(callbacks, ['keyboards', 'targets-with-whitespace.kmn']); + assert.isNotNull(result); + + // Verifies that targets has 'any' as it contains both JS and KMX targets + assert.isNotNull(result.artifacts.js); + assert.isNotNull(result.artifacts.kmx); + + const reader = new KmxFileReader(); + const keyboard = reader.read(result.artifacts.kmx.data); + assert.equal(keyboard.targets, 'any'); + }); + }); diff --git a/developer/src/kmc-kmn/test/fixtures/keyboards/targets-with-whitespace.kmn b/developer/src/kmc-kmn/test/fixtures/keyboards/targets-with-whitespace.kmn new file mode 100644 index 0000000000..4afe77ecd5 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/keyboards/targets-with-whitespace.kmn @@ -0,0 +1,11 @@ +store(&NAME) 'targets_with_whitespace' +store(&VERSION) '10.0' + +c this used to generate KM0207B: At least one compile target must be specified +store(&TARGETS) ' any' + +begin unicode > use(main) + +group(main) using keys + ++ 'x' > 'y' diff --git a/developer/src/kmc-kmn/test/helpers/index.ts b/developer/src/kmc-kmn/test/helpers/index.ts index 8250dfd101..ae0ad21268 100644 --- a/developer/src/kmc-kmn/test/helpers/index.ts +++ b/developer/src/kmc-kmn/test/helpers/index.ts @@ -1,8 +1,13 @@ -/** +/* + * Keyman is copyright (C) SIL Global. MIT License. + * * Helpers and utilities for the Mocha tests. */ -import * as path from 'path'; -import { fileURLToPath } from 'url'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { assert } from 'chai'; +import { CompilerCallbacks } from '@keymanapp/developer-utils'; +import { KmnCompiler } from '../../src/compiler/compiler.js'; /** * Builds a path to the fixture with the given path components. @@ -15,3 +20,13 @@ import { fileURLToPath } from 'url'; export function makePathToFixture(...components: string[]): string { return fileURLToPath(new URL(path.join('..', '..', '..', 'test', 'fixtures', ...components), import.meta.url)); } + +export async function compileTestKeyboard(callbacks: CompilerCallbacks, fixture: string[]) { + const compiler = new KmnCompiler(); + assert(await compiler.init(callbacks, {saveDebug: true, shouldAddCompilerVersion: false})); + assert(compiler.verifyInitialized()); + + const kmnPath = makePathToFixture(...fixture); + + return await compiler.run(kmnPath, null); +} \ No newline at end of file diff --git a/developer/src/kmcmplib/src/Compiler.cpp b/developer/src/kmcmplib/src/Compiler.cpp index d9f3c5c9cb..e73b2b9a45 100644 --- a/developer/src/kmcmplib/src/Compiler.cpp +++ b/developer/src/kmcmplib/src/Compiler.cpp @@ -138,7 +138,7 @@ KMX_BOOL IsSameToken(PKMX_WCHAR *p, KMX_WCHAR const * token); KMX_DWORD GetRHS(PFILE_KEYBOARD fk, PKMX_WCHAR p, PKMX_WCHAR buf, int bufsize, int offset, int IsUnicode); PKMX_WCHAR GetDelimitedString(PKMX_WCHAR *p, KMX_WCHAR const * Delimiters, KMX_WORD Flags); KMX_DWORD GetXString(PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX_WCHAR const * token, PKMX_WCHAR output, int max, int offset, PKMX_WCHAR *newp, int isVKey, int isUnicode); -KMX_BOOL GetCompileTargetsFromTargetsStore(const KMX_WCHAR* store, int &targets); +KMX_BOOL GetCompileTargetsFromTargetsStore(KMX_WCHAR *store, int &targets); int GetGroupNum(PFILE_KEYBOARD fk, PKMX_WCHAR p); @@ -1331,7 +1331,16 @@ KMX_BOOL ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE s return TRUE; } -KMX_BOOL GetCompileTargetsFromTargetsStore(const KMX_WCHAR* store, int &targets) { +/** + * Extract the compile targets from the &targets store, and rewrite the + * &targets dpString value to remove unnecessary whitespace. Does not + * reallocate sp->dpString, but overwrites its value with a string the + * same length or shorter. + * @param sp store value to rewrite + * @param targets (output) + * @return FALSE if no targets found or invalid targets found + */ +KMX_BOOL GetCompileTargetsFromTargetsStore(KMX_WCHAR *store, int &targets) { // Compile to .kmx const std::vector KMXKeymanTargets{ u"windows", u"macosx", u"linux", u"desktop" @@ -1347,10 +1356,17 @@ KMX_BOOL GetCompileTargetsFromTargetsStore(const KMX_WCHAR* store, int &targets) targets = 0; auto p = new KMX_WCHAR[u16len(store)+1]; - u16cpy(p, store); + auto q = p; + *p = 0; KMX_WCHAR* ctx; - auto token = u16tok(p, u" ", &ctx); + auto token = u16tok(store, u" ", &ctx); while(token) { + if(q > p) { + // Insert a delimiter between tokens, if + // more than one token + *q++ = ' '; + *q = 0; + } bool found = false; if(*token) { if(AnyTarget == token) { @@ -1378,9 +1394,16 @@ KMX_BOOL GetCompileTargetsFromTargetsStore(const KMX_WCHAR* store, int &targets) targets = 0; return FALSE; } + + // Append the token to the output, we know it is long enough + // because the buffer is the same length as the input + u16cpy(q, token); + q = const_cast(u16chr(q, 0)); + *q = 0; } token = u16tok(nullptr, u" ", &ctx); } + u16cpy(store, p); delete[] p; if(targets == 0) { diff --git a/developer/src/kmcmplib/tests/api.tests.cpp b/developer/src/kmcmplib/tests/api.tests.cpp index ea44e3fade..445c69928b 100644 --- a/developer/src/kmcmplib/tests/api.tests.cpp +++ b/developer/src/kmcmplib/tests/api.tests.cpp @@ -71,66 +71,90 @@ void test_kmcmp_CompileKeyboard(char *kmn_file) { unlink(kmn_file); } -extern KMX_BOOL GetCompileTargetsFromTargetsStore(const KMX_WCHAR* store, int &targets); +extern KMX_BOOL GetCompileTargetsFromTargetsStore(KMX_WCHAR* buf, int &targets); + +KMX_BOOL do_GetCompileTargetsFromTargetsStore(const KMX_WCHAR* store, int &targets, const KMX_WCHAR* expected = nullptr) { + KMX_WCHAR* buf; + buf = new KMX_WCHAR[u16len(store)+1]; + u16cpy(buf, store); + KMX_BOOL result = GetCompileTargetsFromTargetsStore(buf, targets); + + if(expected) { + test_assert(u16cmp(expected, buf) == 0); + } + + delete[] buf; + return result; +} void test_GetCompileTargetsFromTargetsStore() { int targets = 0; setup(); - test_assert(GetCompileTargetsFromTargetsStore(u"any", targets)); + test_assert(do_GetCompileTargetsFromTargetsStore(u"any", targets, u"any")); test_assert(error_vec.size() == 0); test_assert(targets == (COMPILETARGETS_KMX | COMPILETARGETS_JS)); setup(); - test_assert(GetCompileTargetsFromTargetsStore(u"windows", targets)); + test_assert(do_GetCompileTargetsFromTargetsStore(u"windows", targets, u"windows")); test_assert(error_vec.size() == 0); test_assert(targets == COMPILETARGETS_KMX); setup(); - test_assert(GetCompileTargetsFromTargetsStore(u"desktop", targets)); + test_assert(do_GetCompileTargetsFromTargetsStore(u"desktop", targets, u"desktop")); test_assert(error_vec.size() == 0); test_assert(targets == COMPILETARGETS_KMX); setup(); - test_assert(GetCompileTargetsFromTargetsStore(u"mobile", targets)); + test_assert(do_GetCompileTargetsFromTargetsStore(u"mobile", targets, u"mobile")); test_assert(error_vec.size() == 0); test_assert(targets == COMPILETARGETS_JS); setup(); - test_assert(GetCompileTargetsFromTargetsStore(u"web", targets)); + test_assert(do_GetCompileTargetsFromTargetsStore(u"web", targets, u"web")); test_assert(error_vec.size() == 0); test_assert(targets == COMPILETARGETS_JS); setup(); - test_assert(GetCompileTargetsFromTargetsStore(u"desktop mobile", targets)); + test_assert(do_GetCompileTargetsFromTargetsStore(u"desktop mobile", targets, u"desktop mobile")); test_assert(error_vec.size() == 0); test_assert(targets == (COMPILETARGETS_KMX | COMPILETARGETS_JS)); setup(); - test_assert(GetCompileTargetsFromTargetsStore(u"desktop tablet", targets)); + test_assert(do_GetCompileTargetsFromTargetsStore(u"desktop tablet", targets, u"desktop tablet")); test_assert(error_vec.size() == 0); test_assert(targets == (COMPILETARGETS_KMX | COMPILETARGETS_JS)); setup(); - test_assert(!GetCompileTargetsFromTargetsStore(u"foo bar baz", targets)); + test_assert(do_GetCompileTargetsFromTargetsStore(u" desktop tablet", targets, u"desktop tablet")); + test_assert(error_vec.size() == 0); + test_assert(targets == (COMPILETARGETS_KMX | COMPILETARGETS_JS)); + + setup(); + test_assert(do_GetCompileTargetsFromTargetsStore(u" windows androidphone ", targets, u"windows androidphone")); + test_assert(error_vec.size() == 0); + test_assert(targets == (COMPILETARGETS_KMX | COMPILETARGETS_JS)); + + setup(); + test_assert(!do_GetCompileTargetsFromTargetsStore(u"foo bar baz", targets)); test_assert(error_vec.size() == 1); test_assert(error_vec[0] == KmnCompilerMessages::ERROR_InvalidTarget); test_assert(targets == 0); setup(); - test_assert(!GetCompileTargetsFromTargetsStore(u"windows chromeos", targets)); + test_assert(!do_GetCompileTargetsFromTargetsStore(u"windows chromeos", targets)); test_assert(error_vec.size() == 1); test_assert(error_vec[0] == KmnCompilerMessages::ERROR_InvalidTarget); test_assert(targets == 0); setup(); - test_assert(!GetCompileTargetsFromTargetsStore(u" ", targets)); + test_assert(!do_GetCompileTargetsFromTargetsStore(u" ", targets)); test_assert(error_vec.size() == 1); test_assert(error_vec[0] == KmnCompilerMessages::ERROR_NoTargetsSpecified); test_assert(targets == 0); setup(); - test_assert(!GetCompileTargetsFromTargetsStore(u"", targets)); + test_assert(!do_GetCompileTargetsFromTargetsStore(u"", targets)); test_assert(error_vec.size() == 1); test_assert(error_vec[0] == KmnCompilerMessages::ERROR_NoTargetsSpecified); test_assert(targets == 0); diff --git a/developer/src/kmcmplib/tests/gtest-km_u16.tests.cpp b/developer/src/kmcmplib/tests/gtest-km_u16.tests.cpp index 75d74607cd..db40baf12d 100644 --- a/developer/src/kmcmplib/tests/gtest-km_u16.tests.cpp +++ b/developer/src/kmcmplib/tests/gtest-km_u16.tests.cpp @@ -32,6 +32,11 @@ TEST(km_u16_Test, u16tok_char_delim) { KMX_WCHAR str[LINESIZE]; KMX_WCHAR *ctx = nullptr; + // invalid parameters + EXPECT_EQ(nullptr, u16tok(str, ' ', nullptr)); + EXPECT_EQ(nullptr, u16tok(str, (KMX_WCHAR)0, &ctx)); + EXPECT_EQ(nullptr, u16tok(nullptr, ' ', nullptr)); + // sequence of tokens u16cpy(str, u"test a space and two"); ctx = nullptr; @@ -55,6 +60,13 @@ TEST(km_u16_Test, u16tok_char_delim) { EXPECT_TRUE(!u16cmp(u"b", u16tok(nullptr, ' ', &ctx))); EXPECT_EQ(nullptr, u16tok(nullptr, ' ', &ctx)); + // delimiters at start + u16cpy(str, u" a b"); + ctx = nullptr; + EXPECT_TRUE(!u16cmp(u"a", u16tok(str, ' ', &ctx))); + EXPECT_TRUE(!u16cmp(u"b", u16tok(nullptr, ' ', &ctx))); + EXPECT_EQ(nullptr, u16tok(nullptr, ' ', &ctx)); + // no string, no context ctx = nullptr; EXPECT_EQ(nullptr, u16tok(nullptr, ' ', &ctx)); @@ -73,6 +85,12 @@ TEST(km_u16_Test, u16tok_str_delim) { KMX_WCHAR str[LINESIZE]; KMX_WCHAR *ctx = nullptr; + // invalid parameters + EXPECT_EQ(nullptr, u16tok(str, u" ", nullptr)); + EXPECT_EQ(nullptr, u16tok(str, nullptr, &ctx)); + EXPECT_EQ(nullptr, u16tok(str, u"", &ctx)); + EXPECT_EQ(nullptr, u16tok(nullptr, u" ", nullptr)); + // sequence of tokens u16cpy(str, u"test a space and two"); ctx = nullptr; @@ -96,6 +114,13 @@ TEST(km_u16_Test, u16tok_str_delim) { EXPECT_TRUE(!u16cmp(u"b", u16tok(nullptr, u" ", &ctx))); EXPECT_EQ(nullptr, u16tok(nullptr, u" ", &ctx)); + // delimiters at start + u16cpy(str, u" a b"); + ctx = nullptr; + EXPECT_TRUE(!u16cmp(u"a", u16tok(str, u" ", &ctx))); + EXPECT_TRUE(!u16cmp(u"b", u16tok(nullptr, u" ", &ctx))); + EXPECT_EQ(nullptr, u16tok(nullptr, ' ', &ctx)); + // no string, no context ctx = nullptr; EXPECT_EQ(nullptr, u16tok(nullptr, u"", &ctx)); @@ -130,6 +155,9 @@ TEST(km_u16_Test, u16tok_str_compare_to_strtok) { // Compare behaviour of strtok: char str[LINESIZE]; + // Note: strtok behavior is undefined with invalid parameters so we don't do a reference test + // of invalid parameters + // sequence of tokens strcpy(str, "test a space and two"); EXPECT_TRUE(!strcmp("test", strtok(str, " "))); @@ -150,6 +178,12 @@ TEST(km_u16_Test, u16tok_str_compare_to_strtok) { EXPECT_TRUE(!strcmp("b", strtok(nullptr, " "))); EXPECT_EQ(nullptr, strtok(nullptr, " ")); + // delimiters at start + strcpy(str, " a b"); + EXPECT_TRUE(!strcmp("a", strtok(str, " "))); + EXPECT_TRUE(!strcmp("b", strtok(nullptr, " "))); + EXPECT_EQ(nullptr, strtok(nullptr, " ")); + // multiple delimiters strcpy(str, "abcghi"); const char *delim = "<>"; From 07e62e9fe28b420ae854613de8b46136e5f3e6e6 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Thu, 27 Aug 2026 18:57:18 +0200 Subject: [PATCH 19/30] chore(android): upload webview source map during android and ios build Previously we uploaded the webview source map during a web build. This can lead to problems because that is not the actual artifact used for Android and iOS. This change therefore moves uploading the webview source map to Sentry to the android and ios builds. Build-bot: skip build:web,android,ios Test-bot: skip --- android/KMEA/build.sh | 15 ++++++++++++--- ios/build.sh | 2 ++ ios/engine/build.sh | 17 +++++++++++++---- resources/build/ci/sentry-control.inc.sh | 14 ++++++++++++++ web/ci.sh | 21 +++------------------ 5 files changed, 44 insertions(+), 25 deletions(-) diff --git a/android/KMEA/build.sh b/android/KMEA/build.sh index 85a1894672..66471c0bbb 100755 --- a/android/KMEA/build.sh +++ b/android/KMEA/build.sh @@ -9,6 +9,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "${THIS_SCRIPT%/*}/../../resources/build/builder-full.inc.sh" ## END STANDARD BUILD SCRIPT INCLUDE +. "${KEYMAN_ROOT}/resources/build/ci/sentry-control.inc.sh" # ################################ Main script ################################ @@ -19,6 +20,7 @@ builder_describe "Builds Keyman Engine for Android." \ "configure" \ "build" \ "test Runs lint and unit tests." \ + "publish-symbols Publishes symbols to Sentry." \ ":engine Builds Engine" builder_parse "$@" @@ -79,6 +81,13 @@ do_test() { ./gradlew $GRADLE_DAEMON $TEST_FLAGS } -builder_run_action clean:engine rm -rf build app/build -builder_run_action build:engine do_build -builder_run_action test:engine do_test +do_publish_symbols() { + if builder_is_ci_build && builder_is_ci_build_level_release; then + sentry_upload_web "${KEYMAN_ROOT}/web/build/app/webview/release/" + fi +} + +builder_run_action clean:engine rm -rf build app/build +builder_run_action build:engine do_build +builder_run_action test:engine do_test +builder_run_action publish-symbols do_publish_symbols diff --git a/ios/build.sh b/ios/build.sh index 81b477932c..e59f10a0dd 100755 --- a/ios/build.sh +++ b/ios/build.sh @@ -32,6 +32,7 @@ builder_describe "Builds Keyman Engine and the Keyman app for use on iOS devices "configure" \ "build" \ "test" \ + "publish-symbols Publishes symbols to Sentry" \ ":engine Builds KeymanEngine.xcframework, usable by our main app and by third-party apps" \ ":app=keyman Builds the Keyman app for iOS platforms" \ ":help Online documentation" \ @@ -50,3 +51,4 @@ function do_test_help() { } builder_run_action test:help do_test_help +builder_run_child_actions publish-symbols diff --git a/ios/engine/build.sh b/ios/engine/build.sh index f341e13983..2fa7add082 100755 --- a/ios/engine/build.sh +++ b/ios/engine/build.sh @@ -9,6 +9,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "$KEYMAN_ROOT/resources/build/utils.inc.sh" . "$KEYMAN_ROOT/resources/build/mac/mac.inc.sh" . "$KEYMAN_ROOT/resources/build/build-download-resources.sh" +. "${KEYMAN_ROOT}/resources/build/ci/sentry-control.inc.sh" builder_describe "Builds Keyman Engine for use on iOS devices - iPhone and iPad." \ "@/web/src/app/webview build" \ @@ -16,7 +17,8 @@ builder_describe "Builds Keyman Engine for use on iOS devices - iPhone and iPad. "clean" \ "configure" \ "build" \ - "--sim-artifact Also outputs a simulator-friendly test artifact corresponding to the build" + "publish-symbols Publishes symbols to Sentry." \ + "--sim-artifact Also outputs a simulator-friendly test artifact corresponding to the build" builder_parse "$@" @@ -138,6 +140,13 @@ function build_engine() { fi } -builder_run_action clean do_clean -builder_run_action configure do_configure -builder_run_action build build_engine +do_publish_symbols() { + if builder_is_ci_build && builder_is_ci_build_level_release; then + sentry_upload_web "${KEYMAN_ROOT}/web/build/app/webview/release/" + fi +} + +builder_run_action clean do_clean +builder_run_action configure do_configure +builder_run_action build build_engine +builder_run_action publish-symbols do_publish_symbols diff --git a/resources/build/ci/sentry-control.inc.sh b/resources/build/ci/sentry-control.inc.sh index 9331152eed..a01a0f730d 100644 --- a/resources/build/ci/sentry-control.inc.sh +++ b/resources/build/ci/sentry-control.inc.sh @@ -40,3 +40,17 @@ function makeSentryRelease() { fi } +function sentry_upload_web () { + if ! isSentryConfigured; then + echo "Skipping Sentry upload: SENTRY_ORG and/or SENTRY_PROJECT are unset." + return + fi + + echo "Uploading $1 to Sentry..." + + # --strip-common-prefix does not take an argument, unlike --strip-prefix. It auto-detects + # the most common prefix instead. + sentry-cli releases files "${KEYMAN_VERSION_GIT_TAG}" upload-sourcemaps --strip-common-prefix "$1" \ + --rewrite --ext js --ext map --ext ts + echo "Upload successful." +} diff --git a/web/ci.sh b/web/ci.sh index f9c4e4263d..30a9200cd7 100755 --- a/web/ci.sh +++ b/web/ci.sh @@ -15,6 +15,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "${KEYMAN_ROOT}/resources/build/utils.inc.sh" . "${KEYMAN_ROOT}/resources/build/zip.inc.sh" . "${KEYMAN_ROOT}/resources/build/ci/pull-requests.inc.sh" +. "${KEYMAN_ROOT}/resources/build/ci/sentry-control.inc.sh" # This script runs from its own folder cd "${THIS_SCRIPT_PATH}" @@ -42,21 +43,6 @@ builder_parse "$@" KEYMAN_TIER=$(cat ../TIER.md) BUILD_NUMBER=$(cat ../VERSION.md) -function web_sentry_upload () { - if [[ -z "${SENTRY_ORG:-}" ]] || [[ -z "${SENTRY_PROJECT:-}" ]]; then - echo "Skipping Sentry upload: SENTRY_ORG and/or SENTRY_PROJECT are unset." - return - fi - - echo "Uploading $1 to Sentry..." - - # --strip-common-prefix does not take an argument, unlike --strip-prefix. It auto-detects - # the most common prefix instead. - sentry-cli releases files "${KEYMAN_VERSION_GIT_TAG}" upload-sourcemaps --strip-common-prefix "$1" \ - --rewrite --ext js --ext map --ext ts - echo "Upload successful." -} - function build_action() { # Build step: since CI builds start (and should start) from scratch, run the following # three actions: @@ -73,11 +59,10 @@ function build_action() { for sourcemap in "${KEYMAN_ROOT}/common/web/sentry-manager/build/lib/"*.map; do node "${KEYMAN_ROOT}/web/build/tools/building/sourcemap-root/index.js" null "${sourcemap}" --clean done - web_sentry_upload "${KEYMAN_ROOT}/common/web/sentry-manager/build/lib/" + sentry_upload_web "${KEYMAN_ROOT}/common/web/sentry-manager/build/lib/" # And, of course, the main build-products too - web_sentry_upload "${KEYMAN_ROOT}/web/build/app/webview/release/" - web_sentry_upload "${KEYMAN_ROOT}/web/build/publish/release/" + sentry_upload_web "${KEYMAN_ROOT}/web/build/publish/release/" fi } From ecbe7d857bde55062d278be18dfde21756224bb0 Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Thu, 27 Aug 2026 13:01:56 -0500 Subject: [PATCH 20/30] auto: increment master version to 19.0.276 Test-bot: skip Build-bot: skip --- HISTORY.md | 6 ++++++ VERSION.md | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index b80e08f694..3d9fea7e02 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,11 @@ # Keyman Version History +## 19.0.275 alpha 2026-08-27 + +* docs(mac): refresh build/setup docs and remove APPSTORECONNECT_PROVIDER (#16432) +* chore(mac): update setup document (#16415) +* change(web): enhance missing-stubs error (#16437) + ## 19.0.274 alpha 2026-08-24 * maint(windows): move to vs2026 arm runner for GHA (#16418) diff --git a/VERSION.md b/VERSION.md index 32062d3f5c..c2aea38f65 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.275 \ No newline at end of file +19.0.276 \ No newline at end of file From 20e651fd99eaf08e2b40c2ae6de60324436ff72c Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Fri, 28 Aug 2026 15:36:37 +0200 Subject: [PATCH 21/30] chore(web): add publishing symbols to TC ios release build Build-bot: skip release:web,android,ios --- resources/teamcity/ios/ios-actions.inc.sh | 8 ++++++++ resources/teamcity/ios/keyman-ios-release.sh | 1 + 2 files changed, 9 insertions(+) diff --git a/resources/teamcity/ios/ios-actions.inc.sh b/resources/teamcity/ios/ios-actions.inc.sh index acf904fe0f..765e3542ab 100755 --- a/resources/teamcity/ios/ios-actions.inc.sh +++ b/resources/teamcity/ios/ios-actions.inc.sh @@ -13,3 +13,11 @@ ios_capture_build_artifacts() { "${KEYMAN_ROOT}/ios/tools/prepRelease.sh" builder_echo end "prep-release" success "Finished capturing build artifacts" } + +ios_publish_symbols() { + builder_echo start "publish to Sentry" "Publishing source map to Sentry" + + builder_launch /ios/build.sh "publish-symbols" + + builder_echo end "publish to Sentry" success "Finished publishing source map to Sentry" +} diff --git a/resources/teamcity/ios/keyman-ios-release.sh b/resources/teamcity/ios/keyman-ios-release.sh index 8942a9f135..15d09e5ef7 100755 --- a/resources/teamcity/ios/keyman-ios-release.sh +++ b/resources/teamcity/ios/keyman-ios-release.sh @@ -243,6 +243,7 @@ function do_build() { function do_publish() { _publish_to_downloads_keyman_com + ios_publish_symbols tc_upload_help "Keyman for iOS" ios if [[ "${KEYMAN_TIER}" == "stable" ]]; then From 2f60544835fbe596ce66984692d3af11bb7b2e4a Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Fri, 28 Aug 2026 17:31:06 +0200 Subject: [PATCH 22/30] docs(developer): tweak comments Co-authored-by: Eberhard Beilharz Co-authored-by: Sabine Schmitt --- developer/src/kmc-kmn/test/compiler.tests.ts | 5 ++++- developer/src/kmcmplib/src/Compiler.cpp | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/developer/src/kmc-kmn/test/compiler.tests.ts b/developer/src/kmc-kmn/test/compiler.tests.ts index c4bc020929..ce27552585 100644 --- a/developer/src/kmc-kmn/test/compiler.tests.ts +++ b/developer/src/kmc-kmn/test/compiler.tests.ts @@ -148,10 +148,13 @@ describe('Compiler class', function() { const result = await compileTestKeyboard(callbacks, ['keyboards', 'targets-with-whitespace.kmn']); assert.isNotNull(result); - // Verifies that targets has 'any' as it contains both JS and KMX targets + // Verify implictly that `&targets` store was interpreted correctly as 'any' + // because the compiler generated both JS and KMX targets (#13721) assert.isNotNull(result.artifacts.js); assert.isNotNull(result.artifacts.kmx); + // Then verify directly that the `&targets` store was trimmed by looking at + // the final value in the kmx data const reader = new KmxFileReader(); const keyboard = reader.read(result.artifacts.kmx.data); assert.equal(keyboard.targets, 'any'); diff --git a/developer/src/kmcmplib/src/Compiler.cpp b/developer/src/kmcmplib/src/Compiler.cpp index e73b2b9a45..8204cc2e2e 100644 --- a/developer/src/kmcmplib/src/Compiler.cpp +++ b/developer/src/kmcmplib/src/Compiler.cpp @@ -1336,7 +1336,7 @@ KMX_BOOL ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE s * &targets dpString value to remove unnecessary whitespace. Does not * reallocate sp->dpString, but overwrites its value with a string the * same length or shorter. - * @param sp store value to rewrite + * @param store store value to rewrite * @param targets (output) * @return FALSE if no targets found or invalid targets found */ From 932deccff4b555e772873660d68da8e984837b03 Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Fri, 28 Aug 2026 13:01:38 -0500 Subject: [PATCH 23/30] auto: increment master version to 19.0.277 Test-bot: skip Build-bot: skip --- HISTORY.md | 5 +++++ VERSION.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 3d9fea7e02..f5c3aca81c 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,10 @@ # Keyman Version History +## 19.0.276 alpha 2026-08-28 + +* fix(windows): add manifest to insthelper and set trustinfo to asInvoker (#16430) +* fix(windows): resync caps lock state when Keyman keyboard is activated (#16423) + ## 19.0.275 alpha 2026-08-27 * docs(mac): refresh build/setup docs and remove APPSTORECONNECT_PROVIDER (#16432) diff --git a/VERSION.md b/VERSION.md index c2aea38f65..414f9e906a 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.276 \ No newline at end of file +19.0.277 \ No newline at end of file From d7b40844389ef27297a5ce73e97f98e147518b8f Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 31 Aug 2026 07:55:16 +0200 Subject: [PATCH 24/30] fix(developer): improve scroll wheel behavior in Character Map The Character Map scroll will now scroll from current scroll position when the scroll wheel is used, one row at a time, rather than moving the currently selected cell. This behavior is more natural and allows mixed use of the scrollbar and the scrollwheel. Fixes: #14159 Test-bot: skip --- .../delphi/charmap/UfrmCharacterMapNew.pas | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/common/windows/delphi/charmap/UfrmCharacterMapNew.pas b/common/windows/delphi/charmap/UfrmCharacterMapNew.pas index 2e807ede94..a99212761f 100644 --- a/common/windows/delphi/charmap/UfrmCharacterMapNew.pas +++ b/common/windows/delphi/charmap/UfrmCharacterMapNew.pas @@ -896,22 +896,36 @@ end; procedure TfrmCharacterMapNew.gridMouseWheelDown(Sender: TObject; Shift: TShiftState; MousePos: TPoint; var Handled: Boolean); begin + Handled := True; if ssCtrl in Shift then begin - Handled := True; + // Zoom if tbSize.Position < tbSize.Max then tbSize.Position := tbSize.Position + tbSize.PageSize; + end + else + begin + // Scroll + if grid.TopRow < grid.RowCount - grid.VisibleRowCount then + grid.TopRow := grid.TopRow + 1; end; end; procedure TfrmCharacterMapNew.gridMouseWheelUp(Sender: TObject; Shift: TShiftState; MousePos: TPoint; var Handled: Boolean); begin + Handled := True; if ssCtrl in Shift then begin - Handled := True; + // Zoom if tbSize.Position > tbSize.Min then tbSize.Position := tbSize.Position - tbSize.PageSize; + end + else + begin + // Scroll + if grid.TopRow > 0 then + grid.TopRow := grid.TopRow - 1; end; end; From f65299cb361e7e600eae99e6ee1e241ee97f6263 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 31 Aug 2026 09:28:34 +0200 Subject: [PATCH 25/30] fix(developer): use path not dependent on pwd for starting Server Fixes: #12165 Test-bot: skip --- developer/src/server/src/environment.ts | 20 ++++++++++++++++++++ developer/src/server/src/routes.ts | 8 +++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/developer/src/server/src/environment.ts b/developer/src/server/src/environment.ts index f3e0566024..9b9659ba4a 100644 --- a/developer/src/server/src/environment.ts +++ b/developer/src/server/src/environment.ts @@ -1,5 +1,25 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Environmental variables and paths + */ +import path from 'node:path'; import { extractVersionData } from './version-data.js'; // TODO: environment should be just KEYMAN_VERSION import KEYMAN_VERSION from "@keymanapp/keyman-version"; export const environment = extractVersionData(KEYMAN_VERSION.VERSION_WITH_TAG); + +/** + * @returns base path for the running server -- where index.js is stored + */ +export function serverBasePath() { + return path.join(import.meta.dirname, '../../build/src'); +} + +/** + * @returns path where the site public files can be found -- index.html + */ +export function serverSitePath() { + return path.join(serverBasePath(), 'site'); +} diff --git a/developer/src/server/src/routes.ts b/developer/src/server/src/routes.ts index acccf23402..4bb4386522 100644 --- a/developer/src/server/src/routes.ts +++ b/developer/src/server/src/routes.ts @@ -1,3 +1,8 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Setup URL routes for Keyman Developer Server + */ import * as express from 'express'; import * as ws from 'ws'; import * as multer from 'multer'; @@ -16,6 +21,7 @@ import { standardPaths } from './standardPaths.js'; import chalk from 'chalk'; import { shutdown } from './shutdown.js'; import { getOption } from './options.js'; +import { serverSitePath } from './environment.js'; export default function setupRoutes(app: express.Express, upload: multer.Multer, wsServer: ws.WebSocketServer, environment: Environment ) { @@ -51,7 +57,7 @@ export default function setupRoutes(app: express.Express, upload: multer.Multer, /* All routes */ - app.use('/', express.static('build/src/site')); + app.use('/', express.static(serverSitePath())); app.post('/upload', localhostOnly, upload.single('file'), (req, res, next) => { const name = req.file.originalname; From 9f7beafc57a9b7f4e3a8bedfd70bc05333d2c378 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 31 Aug 2026 09:52:30 +0200 Subject: [PATCH 26/30] chore(developer): remove duplicated versioning information from Server Use `KEYMAN_VERSION` instead of parsing version information. Test-bot: skip --- developer/src/server/src/environment.ts | 5 -- .../server/src/handlers/inc/packages-json.ts | 14 +++-- developer/src/server/src/index.ts | 11 ++-- developer/src/server/src/routes.ts | 10 ++-- developer/src/server/src/version-data.ts | 52 ------------------- 5 files changed, 22 insertions(+), 70 deletions(-) delete mode 100644 developer/src/server/src/version-data.ts diff --git a/developer/src/server/src/environment.ts b/developer/src/server/src/environment.ts index 9b9659ba4a..8c5a26881d 100644 --- a/developer/src/server/src/environment.ts +++ b/developer/src/server/src/environment.ts @@ -4,11 +4,6 @@ * Environmental variables and paths */ import path from 'node:path'; -import { extractVersionData } from './version-data.js'; -// TODO: environment should be just KEYMAN_VERSION - -import KEYMAN_VERSION from "@keymanapp/keyman-version"; -export const environment = extractVersionData(KEYMAN_VERSION.VERSION_WITH_TAG); /** * @returns base path for the running server -- where index.js is stored diff --git a/developer/src/server/src/handlers/inc/packages-json.ts b/developer/src/server/src/handlers/inc/packages-json.ts index 377e4b9856..12dfd1ba57 100644 --- a/developer/src/server/src/handlers/inc/packages-json.ts +++ b/developer/src/server/src/handlers/inc/packages-json.ts @@ -1,15 +1,21 @@ -import { KeymanUrls } from '@keymanapp/developer-utils'; +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * API: return list of packages under testing and urls for installing in mobile + * apps + */ import * as express from 'express'; +import { KeymanUrls } from '@keymanapp/developer-utils'; +import KEYMAN_VERSION from '@keymanapp/keyman-version'; import { data } from "../../data.js"; -import { environment } from '../../environment.js'; export default function handleIncPackagesJson (req: express.Request, res: express.Response) { const packages = Object.keys(data.packages).map(id => { return { id: id, filename: id+'.kmp', name: data.packages[id].name} }); res.send({ packages: packages, urls: { - installLinkAndroid: KeymanUrls.KeymanDeveloper_KeymanForAndroidDownload(environment.versionRelease), - installLinkIos: KeymanUrls.KeymanDeveloper_KeymanForIosDownload(environment.versionRelease), + installLinkAndroid: KeymanUrls.KeymanDeveloper_KeymanForAndroidDownload(KEYMAN_VERSION.VERSION_RELEASE), + installLinkIos: KeymanUrls.KeymanDeveloper_KeymanForIosDownload(KEYMAN_VERSION.VERSION_RELEASE), } }); } diff --git a/developer/src/server/src/index.ts b/developer/src/server/src/index.ts index e248b19625..68b2cf7a9e 100644 --- a/developer/src/server/src/index.ts +++ b/developer/src/server/src/index.ts @@ -1,12 +1,17 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Keyman Developer Server main program + */ import * as fs from 'node:fs'; import * as path from 'node:path'; import chalk from 'chalk'; import express from 'express'; import multer from 'multer'; import * as ws from 'ws'; +import KEYMAN_VERSION from '@keymanapp/keyman-version'; import { KeymanSentry } from './KeymanSentry.js'; import { standardPaths } from './standardPaths.js'; -import { environment } from './environment.js'; import setupRoutes from './routes.js'; import { shutdown } from './shutdown.js'; import { initTray } from './tray.js'; @@ -23,7 +28,7 @@ const options = { // set of options if an error occurs. await loadOptions(); -console.log(`Starting Keyman Developer Server ${environment.versionWithTag}, listening on port ${getOption('web host port')}.`); +console.log(`Starting Keyman Developer Server ${KEYMAN_VERSION.VERSION_WITH_TAG}, listening on port ${getOption('web host port')}.`); KeymanSentry.init(); try { @@ -74,7 +79,7 @@ export async function run() { /* Setup routes */ - setupRoutes(app, upload, wsServer, environment); + setupRoutes(app, upload, wsServer); /* Start the web server */ diff --git a/developer/src/server/src/routes.ts b/developer/src/server/src/routes.ts index 4bb4386522..fc07968b5a 100644 --- a/developer/src/server/src/routes.ts +++ b/developer/src/server/src/routes.ts @@ -6,6 +6,7 @@ import * as express from 'express'; import * as ws from 'ws'; import * as multer from 'multer'; +import KEYMAN_VERSION from '@keymanapp/keyman-version'; import handleIncKeyboardsJs from './handlers/inc/keyboards-js.js'; import { data, DebugFont, DebugKeyboard, DebugModel, DebugObject, DebugPackage, isValidId } from './data.js'; import apiGet from './handlers/api/debugobject/get.js'; @@ -16,23 +17,20 @@ import apiUnregister from './handlers/api/debugobject/unregister.js'; import handleIncPackagesJson from './handlers/inc/packages-json.js'; import apiPackageRegister from './handlers/api/package/register.js'; import handleIncKeyboardsCss from './handlers/inc/keyboards-css.js'; -import { Environment } from './version-data.js'; import { standardPaths } from './standardPaths.js'; import chalk from 'chalk'; import { shutdown } from './shutdown.js'; import { getOption } from './options.js'; import { serverSitePath } from './environment.js'; -export default function setupRoutes(app: express.Express, upload: multer.Multer, wsServer: ws.WebSocketServer, environment: Environment ) { +export default function setupRoutes(app: express.Express, upload: multer.Multer, wsServer: ws.WebSocketServer ) { /* Middleware - JSON and logging */ app.use(express.json()); // for parsing application/json app.use(function (req, _res, next) { - // if(environment.environment == 'local') { - console.log(req.method + ' ' + req.path); - // } + console.log(req.method + ' ' + req.path); next(); }); @@ -99,7 +97,7 @@ export default function setupRoutes(app: express.Express, upload: multer.Multer, app.get('/inc/packages.json', handleIncPackagesJson); app.get('/api-public/version', (req,res,next)=>{ - res.json({version: environment.versionWithTag, isApiAvailable: isLocalhost(req)}); + res.json({version: KEYMAN_VERSION.VERSION_WITH_TAG, isApiAvailable: isLocalhost(req)}); next(); }); diff --git a/developer/src/server/src/version-data.ts b/developer/src/server/src/version-data.ts deleted file mode 100644 index a082a65170..0000000000 --- a/developer/src/server/src/version-data.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - Version information from builder-basic.inc.sh: - - # KEYMAN_VERSION: Full current build version, e.g. "14.0.1" - # [KEYMAN_VERSION_WIN: Full current build version for Windows, e.g. "14.0.1.0"] - # KEYMAN_VERSION_RELEASE: Current release version, e.g. "14.0" - # KEYMAN_VERSION_MAJOR: Major version, e.g. "14" - # KEYMAN_VERSION_MINOR: Minor version, e.g. "0" - # KEYMAN_VERSION_PATCH: Patch version, e.g. "1" - # KEYMAN_TIER: Current tier, one of "alpha", "beta" or "stable" - # KEYMAN_VERSION_TAG: Tier + Pull Request + Location of build [-alpha|-beta][-test[-1234]][-local] - # KEYMAN_VERSION_WITH_TAG: e.g. "14.0.1-alpha-test-1234" or "14.0.5-beta-local" or "14.0.1-alpha-test" - # [KEYMAN_ROOT: fully resolved root path of Keyman repository] - # KEYMAN_VERSION_ENVIRONMENT: One of: local, test, alpha, beta, stable - # KEYMAN_VERSION_GIT_TAG: Git tag for the release, "release@$KEYMAN_VERSION_WITH_TAG", e.g. "release@14.0.1-alpha-test-1234" -*/ - -export interface Environment { - version: string; - versionRelease: string; - versionMajor: string; - versionMinor: string; - versionPatch: string; - tier: string; - versionTag: string; - versionWithTag: string; - versionEnvironment: string; - versionGitTag: string; - // Pull Request Data - pr: string; -} - -export function extractVersionData(version: string): Environment { - const versionData = /^(\d+)\.(\d+)\.(\d+)((?:-(alpha|beta))?(?:-(test|local))?(?:-(\d+))?)$/.exec(version); - if(!versionData) return null; - - return { - version: `${versionData[1]}.${versionData[2]}.${versionData[3]}`, - versionRelease: `${versionData[1]}.${versionData[2]}`, - versionMajor: versionData[1], - versionMinor: versionData[2], - versionPatch: versionData[3], - tier: versionData[5] || 'stable', - versionTag: versionData[4], - versionWithTag: version, - versionEnvironment: versionData[6] || versionData[5] || 'stable', - versionGitTag: 'release@'+version, - - pr: versionData[7] || '' - }; -}; - From 4c2ba9f8e08cb1193aa03ef0045b01a16d2fd7db Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 31 Aug 2026 10:07:00 +0200 Subject: [PATCH 27/30] fix(developer): handle comment without trailing whitespace Fixes: #12070 Test-bot: skip --- developer/src/kmcmplib/src/Compiler.cpp | 2 +- developer/src/kmcmplib/tests/gtest-compiler.tests.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/developer/src/kmcmplib/src/Compiler.cpp b/developer/src/kmcmplib/src/Compiler.cpp index d9f3c5c9cb..e0f4b273ca 100644 --- a/developer/src/kmcmplib/src/Compiler.cpp +++ b/developer/src/kmcmplib/src/Compiler.cpp @@ -2079,7 +2079,7 @@ int LineTokenType(PKMX_WCHAR *str) switch (towupper(*p)) { case 'C': - if (iswspace(*(p + 1))) return T_COMMENT; + if (iswspace(*(p + 1)) || *(p + 1) == 0) return T_COMMENT; break; case 0: return T_BLANK; diff --git a/developer/src/kmcmplib/tests/gtest-compiler.tests.cpp b/developer/src/kmcmplib/tests/gtest-compiler.tests.cpp index 7e65bfdd55..3862eca44b 100644 --- a/developer/src/kmcmplib/tests/gtest-compiler.tests.cpp +++ b/developer/src/kmcmplib/tests/gtest-compiler.tests.cpp @@ -663,7 +663,7 @@ TEST_F(CompilerTest, LineTokenType_test) { // comment without following space ... potential bug, but ReadLine() currently ensures following space u16cpy(str, u"c"); p = str; - EXPECT_EQ(T_UNKNOWN, LineTokenType(&p)); + EXPECT_EQ(T_COMMENT, LineTokenType(&p)); EXPECT_EQ(0, p - str); // T_KEYTOKEY From efedd63fa4f4a89e063c316995214edb9afa6b79 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 31 Aug 2026 10:25:10 +0200 Subject: [PATCH 28/30] chore(developer): remove version-data tests and add environment tests Server lacks tests at present. Removing version-data tests as no longer relevant meant that there were no tests! So, added environment tests which relate to changes in #16476. --- .../src/server/test/environment.tests.ts | 20 +++++++ .../src/server/test/version-data.tests.ts | 55 ------------------- 2 files changed, 20 insertions(+), 55 deletions(-) create mode 100644 developer/src/server/test/environment.tests.ts delete mode 100644 developer/src/server/test/version-data.tests.ts diff --git a/developer/src/server/test/environment.tests.ts b/developer/src/server/test/environment.tests.ts new file mode 100644 index 0000000000..352cdc3d32 --- /dev/null +++ b/developer/src/server/test/environment.tests.ts @@ -0,0 +1,20 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import {assert} from 'chai'; +import 'mocha'; +import { serverBasePath, serverSitePath } from '../src/environment.js'; + +describe('serverBasePath', function() { + it('should find index.js in the base path', function() { + assert.isTrue(fs.existsSync(path.join(serverBasePath(), 'index.js'))); + }); +}); + +describe('serverSitePath', function() { + it('should find index.html in the base path', function() { + assert.isTrue(fs.existsSync(path.join(serverSitePath(), 'index.html'))); + }); +}); diff --git a/developer/src/server/test/version-data.tests.ts b/developer/src/server/test/version-data.tests.ts deleted file mode 100644 index 74762b7d7f..0000000000 --- a/developer/src/server/test/version-data.tests.ts +++ /dev/null @@ -1,55 +0,0 @@ -import {assert} from 'chai'; -import 'mocha'; -import { extractVersionData } from '../src/version-data.js'; - -describe('extractVersionData', function() { - it('should parse version strings', function() { - let v = extractVersionData('14.0.283'); - assert.equal(v.version, '14.0.283'); - assert.equal(v.versionRelease, '14.0'); - assert.equal(v.versionMajor, '14'); - assert.equal(v.versionMinor, '0'); - assert.equal(v.versionPatch, '283'); - assert.equal(v.tier, 'stable'); - assert.equal(v.versionTag, ''); - assert.equal(v.versionWithTag, '14.0.283'); - assert.equal(v.versionEnvironment, 'stable'); - assert.equal(v.pr, ''); - - v = extractVersionData('14.0.283-beta'); - assert.equal(v.version, '14.0.283'); - assert.equal(v.versionRelease, '14.0'); - assert.equal(v.versionMajor, '14'); - assert.equal(v.versionMinor, '0'); - assert.equal(v.versionPatch, '283'); - assert.equal(v.tier, 'beta'); - assert.equal(v.versionTag, '-beta'); - assert.equal(v.versionWithTag, '14.0.283-beta'); - assert.equal(v.versionEnvironment, 'beta'); - assert.equal(v.pr, ''); - - v = extractVersionData('14.0.283-alpha-local'); - assert.equal(v.version, '14.0.283'); - assert.equal(v.versionRelease, '14.0'); - assert.equal(v.versionMajor, '14'); - assert.equal(v.versionMinor, '0'); - assert.equal(v.versionPatch, '283'); - assert.equal(v.tier, 'alpha'); - assert.equal(v.versionTag, '-alpha-local'); - assert.equal(v.versionWithTag, '14.0.283-alpha-local'); - assert.equal(v.versionEnvironment, 'local'); - assert.equal(v.pr, ''); - - v = extractVersionData('14.0.283-alpha-test-1234'); - assert.equal(v.version, '14.0.283'); - assert.equal(v.versionRelease, '14.0'); - assert.equal(v.versionMajor, '14'); - assert.equal(v.versionMinor, '0'); - assert.equal(v.versionPatch, '283'); - assert.equal(v.tier, 'alpha'); - assert.equal(v.versionTag, '-alpha-test-1234'); - assert.equal(v.versionWithTag, '14.0.283-alpha-test-1234'); - assert.equal(v.versionEnvironment, 'test'); - assert.equal(v.pr, '1234'); - }); -}); \ No newline at end of file From 0f3e84bcc727481fda9fab30d8584a264c9e8f9f Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 31 Aug 2026 14:56:12 +0200 Subject: [PATCH 29/30] fix(windows): call 'Show Console' twice to make the console actually show on first use The underlying issue may be in the node-hide-console-window component. This fix is adequate in my view, for a low-use code path. Fixes: #9694 Test-bot: skip --- developer/src/server/src/win32-tray.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/developer/src/server/src/win32-tray.ts b/developer/src/server/src/win32-tray.ts index a71771f3de..a248f9b669 100644 --- a/developer/src/server/src/win32-tray.ts +++ b/developer/src/server/src/win32-tray.ts @@ -71,6 +71,11 @@ export class Win32Tray { open(ngrokAddress); break; case 'item-id-show-console': + // #9694 - the first time, this has to be called twice for the console + // window to appear. This could probably be fixed in the source + // node-hide-console-window component, but this works well enough and + // has little in the way of side-effects. + WindowsConsole.showConsole(); WindowsConsole.showConsole(); break; case 'item-id-hide-console': From 7a0fa55a75c49a2c6265a3a6cc48bc18ce7f2d0b Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Mon, 31 Aug 2026 19:02:05 +0200 Subject: [PATCH 30/30] chore(web): address code review comments Co-authored-by: Marc Durdin --- resources/build/ci/sentry-control.inc.sh | 30 ++++++++++++++--------- resources/teamcity/ios/ios-actions.inc.sh | 7 ++---- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/resources/build/ci/sentry-control.inc.sh b/resources/build/ci/sentry-control.inc.sh index a01a0f730d..aae674f8c9 100644 --- a/resources/build/ci/sentry-control.inc.sh +++ b/resources/build/ci/sentry-control.inc.sh @@ -12,7 +12,7 @@ function isSentryConfigured() { if [ -z "${SENTRY_AUTH_TOKEN-}" ] || [ -z "${SENTRY_ORG-}" ] || [ -z "${SENTRY_URL-}" ]; then - echo "WARNING: Sentry environment variables SENTRY_AUTH_TOKEN, SENTRY_ORG and SENTRY_URL must be configured." + builder_warn "WARNING: Sentry environment variables SENTRY_AUTH_TOKEN, SENTRY_ORG and SENTRY_URL must be configured." return 1 fi return 0 @@ -20,7 +20,7 @@ function isSentryConfigured() { function isSentryCliAvailable() { which sentry-cli > /dev/null && return 0 - echo "WARNING: sentry-cli could not be found. Skipping all sentry integration." + builder_warn "WARNING: sentry-cli could not be found. Skipping all sentry integration." return 1 } @@ -28,29 +28,35 @@ function makeSentryRelease() { if isSentryConfigured; then if isSentryCliAvailable; then # This version tag matches the repository version tag release@x.y.z - echo "Making a Sentry release for tag $KEYMAN_VERSION_GIT_TAG" + builder_echo "Making a Sentry release for tag $KEYMAN_VERSION_GIT_TAG" sentry-cli releases new -p keyman-android -p keyman-developer -p keyman-ios -p keyman-linux -p keyman-mac -p keyman-web -p keyman-windows $KEYMAN_VERSION_GIT_TAG - echo "Setting commits for release tag $KEYMAN_VERSION_GIT_TAG" + builder_echo "Setting commits for release tag $KEYMAN_VERSION_GIT_TAG" sentry-cli releases set-commits --auto $KEYMAN_VERSION_GIT_TAG - echo "Finalizing release tag $KEYMAN_VERSION_GIT_TAG" + builder_echo "Finalizing release tag $KEYMAN_VERSION_GIT_TAG" sentry-cli releases finalize "$KEYMAN_VERSION_GIT_TAG" fi fi } +# +# Upload sourcemaps to Sentry +# +# Parameters: +# 1: Directory containing sourcemaps to upload +# function sentry_upload_web () { + local SOURCEMAP_DIR="$1" if ! isSentryConfigured; then - echo "Skipping Sentry upload: SENTRY_ORG and/or SENTRY_PROJECT are unset." + builder_warn "Skipping Sentry upload: SENTRY_ORG and/or SENTRY_PROJECT are unset." return fi - echo "Uploading $1 to Sentry..." + builder_echo "Uploading ${SOURCEMAP_DIR} to Sentry..." - # --strip-common-prefix does not take an argument, unlike --strip-prefix. It auto-detects - # the most common prefix instead. - sentry-cli releases files "${KEYMAN_VERSION_GIT_TAG}" upload-sourcemaps --strip-common-prefix "$1" \ - --rewrite --ext js --ext map --ext ts - echo "Upload successful." + sentry-cli releases files "${KEYMAN_VERSION_GIT_TAG}" upload-sourcemaps --strip-common-prefix \ + "${SOURCEMAP_DIR}" --rewrite --ext js --ext map --ext ts + + builder_echo "Upload successful." } diff --git a/resources/teamcity/ios/ios-actions.inc.sh b/resources/teamcity/ios/ios-actions.inc.sh index 765e3542ab..fb803aae89 100755 --- a/resources/teamcity/ios/ios-actions.inc.sh +++ b/resources/teamcity/ios/ios-actions.inc.sh @@ -3,21 +3,18 @@ ios_build() { builder_echo start "build" "Building KeymanEngine + Keyman for iOS" - # shellcheck disable=SC2154 - "${KEYMAN_ROOT}/ios/ci.sh" build + builder_launch /ios/ci.sh build builder_echo end "build" success "Finished building KeymanEngine + Keyman for iOS" } ios_capture_build_artifacts() { builder_echo start "prep-release" "Using prep-release script to capture build artifacts" - "${KEYMAN_ROOT}/ios/tools/prepRelease.sh" + builder_launch /ios/tools/prepRelease.sh builder_echo end "prep-release" success "Finished capturing build artifacts" } ios_publish_symbols() { builder_echo start "publish to Sentry" "Publishing source map to Sentry" - builder_launch /ios/build.sh "publish-symbols" - builder_echo end "publish to Sentry" success "Finished publishing source map to Sentry" }