diff --git a/HISTORY.md b/HISTORY.md index 882fe4ce96..f5c3aca81c 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,21 @@ # 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) +* 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) +* 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..414f9e906a 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.274 \ No newline at end of file +19.0.277 \ No newline at end of file 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/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/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; diff --git a/developer/src/kmc-kmn/test/compiler.tests.ts b/developer/src/kmc-kmn/test/compiler.tests.ts index db5ea60b06..ce27552585 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,20 @@ 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); + + // 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/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..9566ba5243 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 store 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) { @@ -2079,7 +2102,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/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-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 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 = "<>"; diff --git a/developer/src/server/src/environment.ts b/developer/src/server/src/environment.ts index f3e0566024..8c5a26881d 100644 --- a/developer/src/server/src/environment.ts +++ b/developer/src/server/src/environment.ts @@ -1,5 +1,20 @@ -import { extractVersionData } from './version-data.js'; -// TODO: environment should be just KEYMAN_VERSION +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Environmental variables and paths + */ +import path from 'node:path'; -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/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 acccf23402..fc07968b5a 100644 --- a/developer/src/server/src/routes.ts +++ b/developer/src/server/src/routes.ts @@ -1,6 +1,12 @@ +/* + * 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'; +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'; @@ -11,22 +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(); }); @@ -51,7 +55,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; @@ -93,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] || '' - }; -}; - 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': 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 diff --git a/docs/build/macos.md b/docs/build/macos.md index b9f64c3856..92203ddb6d 100644 --- a/docs/build/macos.md +++ b/docs/build/macos.md @@ -28,6 +28,8 @@ The following projects **cannot** be built on macOS: ## System Requirements * Minimum macOS version: macOS Catalina 10.15 or Big Sur 11.0 +(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 @@ -39,8 +41,9 @@ 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. +* 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` @@ -105,7 +108,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 @@ -141,7 +144,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 ``` @@ -204,7 +208,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 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/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 diff --git a/resources/build/ci/sentry-control.inc.sh b/resources/build/ci/sentry-control.inc.sh index 9331152eed..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,15 +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 + builder_warn "Skipping Sentry upload: SENTRY_ORG and/or SENTRY_PROJECT are unset." + return + fi + + builder_echo "Uploading ${SOURCEMAP_DIR} to Sentry..." + + 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 acf904fe0f..fb803aae89 100755 --- a/resources/teamcity/ios/ios-actions.inc.sh +++ b/resources/teamcity/ios/ios-actions.inc.sh @@ -3,13 +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" +} 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 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 } diff --git a/web/src/engine/src/main/contextManagerBase.ts b/web/src/engine/src/main/contextManagerBase.ts index 1f3e7f95dc..e9ddd9a18a 100644 --- a/web/src/engine/src/main/contextManagerBase.ts +++ b/web/src/engine/src/main/contextManagerBase.ts @@ -327,7 +327,8 @@ export abstract class ContextManagerBase if(!requestedStub) { if(keyboardId) { - throw new Error("No matching stub has been registered."); + 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 { keyboard: Promise.resolve(null), 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'); } diff --git a/windows/src/desktop/insthelp/build.sh b/windows/src/desktop/insthelp/build.sh index d57a4b935f..5abbe74dd6 100755 --- a/windows/src/desktop/insthelp/build.sh +++ b/windows/src/desktop/insthelp/build.sh @@ -26,8 +26,7 @@ builder_describe_outputs \ 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" @@ -38,8 +37,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 + diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index 6e67a9ac59..b1b0345556 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -69,6 +69,7 @@ extern "C" __declspec(dllexport) BOOL WINAPI TIPActivateKeyboard(GUID *profile) if(!_td) return FALSE; RefreshThreadKeyboardsIfRequired(); + RefreshToggleState(); if(profile != NULL) { for(int i = 0; i < _td->nKeyboards; i++) { diff --git a/windows/src/engine/keyman32/capsstate.cpp b/windows/src/engine/keyman32/capsstate.cpp index 4f9b466e9e..bc3fc36e5b 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; } + +/** + * 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(); + + 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", previousShiftState, 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 7767bf394b..cef36c382a 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(); } } @@ -413,20 +413,19 @@ ProcessWMKeymanControl(WPARAM wParam, LPARAM lParam) { #endif } -/* - GetCapsAndNumlockState: +/** + * 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(); - 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(); - - 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; @@ -443,18 +442,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;