chore(windows): Merge branch 'fix/windows/13767/error-updates-not-downloaded-after-3' into fix/windows/add-wuc-enum-result

This commit is contained in:
rc-swag 2025-06-12 14:58:20 +10:00
commit 0408262eed
80 changed files with 1932 additions and 475 deletions

9
.gitattributes vendored Normal file
View file

@ -0,0 +1,9 @@
# Always use LF for Dockerfile - otherwise HEREDOCs in Dockerfile
# will not work on Windows - we end up with a filename `foo\r` for
# the command
# ```Dockerfile
# RUN <<EOF cat > foo
# echo "Hello world"
# EOF
# ```
Dockerfile text eol=lf

View file

@ -31,7 +31,7 @@ runs:
path: artifacts/keyman-srcpkg
- name: Build
uses: sillsdev/gha-ubuntu-packaging@1ab4a5967afbadab82a480936b9e53d7190acdf2 # v2.0
uses: sillsdev/gha-ubuntu-packaging@556b268762be9bea0f39861a7392587211ef6a58 # v2.0.1
with:
dist: "${{ inputs.dist }}"
platform: "${{ inputs.arch }}"

View file

@ -1,5 +1,44 @@
# Keyman Version History
## 19.0.61 alpha 2025-06-10
* chore(developer): extend unit test timeout for getLastGitCommitDate (#14147)
* fix(common): Use zip.inc.sh for common test keyboards (#14145)
* maint(developer): add docker image to build developer node modules (#14058)
## 19.0.60 alpha 2025-06-06
* maint(developer): allow to build locally on Linux (#14059)
## 19.0.59 alpha 2025-06-05
* chore(deps): bump multer from 2.0.0 to 2.0.1 (#14127)
* maint(web): fix working directory for zipping artifacts for Web release (#14128)
* chore(linux): Update debian changelog (#14119)
* maint(web): improve TC build script for Keyman Web/Test (#14051)
* maint(developer): move TC configuration for Keyman Developer to scripts (#14052)
## 19.0.58 alpha 2025-06-04
* feat(developer): update strs compiler to collect context (#14064)
* feat(developer): line numbers for UsetCompiler, others (#14090)
* feat(developer): remaining line numbers for tran/bksp compilers (#14097)
* feat(developer): ldml: update errors for unparseable reorder (#14098)
* chore(developer): ldml: messages cleanup (#14100)
* maint(android): Create "archive" builder step to prep release artifacts (#14093)
* maint(web): move TC configuration for Web to script (#14049)
## 19.0.57 alpha 2025-06-03
* fix(windows): change from error to breadcrumb and log (#14069)
* maint(mac): make make-km-dmg.sh less quiet (#14105)
## 19.0.56 alpha 2025-06-02
* maint(ios): Use zip.inc.sh to prep release artifacts (#13919)
* maint(linux): update to new version of gha-ubuntu-packaging (#14094)
* chore(linux): add Python TC test runner (#14062)
## 19.0.55 alpha 2025-05-31
* maint(linux): `sudo` not required for removing temporary files (#14086)
@ -334,6 +373,13 @@
* refactor(windows): rename `TKeymanMutex.MutexOwned` to `TakeOwnership` and add `ReleaseOwnership` (#13168)
* chore: increment to alpha 19.0 (#13187)
## 18.0.238 stable 2025-06-04
* fix(developer): ldml: make sure .run() calls compile() for validation (#14071)
* maint(linux): fix typo of temporary dependency package (#14085)
* maint(linux): update to new version of gha-ubuntu-packaging (#14096)
* maint(mac): make make-km-dmg.sh less quiet (#14106)
## 18.0.237 stable 2025-05-30
* maint(android): Update FirstVoices launcher icon and red theme (#13917)

View file

@ -1 +1 @@
19.0.56
19.0.62

3
android/.gitignore vendored
View file

@ -57,6 +57,9 @@ KMAPro/**/assets/*.kmp
# Play Store release notes (copied from whatsnew.md)
KMAPro/kMAPro/src/main/play/release-notes/**/*.txt
# Release artifacts
upload/
# Eclipse files
.project
.settings/

View file

@ -15,6 +15,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh"
. "$KEYMAN_ROOT/resources/build/build-utils-ci.inc.sh"
. "$KEYMAN_ROOT/resources/build/zip.inc.sh"
################################ Main script ################################
@ -37,6 +38,7 @@ builder_describe \
build \
test \
"publish Publishes symbols to Sentry and the APKs to the Play Store." \
"archive Copy release artifacts to upload/ and rsync to downloads.keyman" \
"--ci+ Deprecated build option. Remove in 20.0" \
--upload-sentry+ \
":engine=KMEA Keyman Engine for Android" \
@ -70,4 +72,51 @@ function do_test_help() {
builder_run_action test:help do_test_help
builder_run_child_actions publish
builder_run_child_actions publish
# Copy release artifacts to upload/ and rsync to downloads.keyman.com
if builder_start_action archive; then
UPLOAD_PATH="$KEYMAN_ROOT/android/upload/${KEYMAN_VERSION}"
KEYMAN_ENGINE_ANDROID_ZIP="keyman-engine-android-${KEYMAN_VERSION}.zip"
KEYMAN_APK="keyman-${KEYMAN_VERSION}.apk"
FIRSTVOICES_APK="firstvoices-${KEYMAN_VERSION}.apk"
ZIP_FILE="${UPLOAD_PATH}/${KEYMAN_ENGINE_ANDROID_ZIP}"
ZIP_FLAGS=("-q" "-r") # quiet, recursive
mkdir -p "${UPLOAD_PATH}"
# Create Keyman Engine for Android archive
builder_echo "Copying Keyman Engine for Android into ${UPLOAD_PATH}..."
cd "${UPLOAD_PATH}"
cp "${KEYMAN_ROOT}/android/KMAPro/kMAPro/libs/keyman-engine.aar" ./
add_zip_files "${ZIP_FILE}" "${ZIP_FLAGS[@]}" "keyman-engine.aar"
rm -f "keyman-engine.aar"
builder_echo "Copying Keyman Engine for Android Sample projects into ${UPLOAD_PATH}..."
cp -rf "${KEYMAN_ROOT}/android/Samples" ./
add_zip_files "${ZIP_FILE}" "-x@../../zip-excludes" "${ZIP_FLAGS[@]}" "Samples"
rm -rf "Samples"
# Copy release APK
cp "${KEYMAN_ROOT}/android/KMAPro/kMAPro/build/outputs/apk/release/${KEYMAN_APK}" ./
# FirstVoices app
if [ "${RELEASE_OEM_FIRSTVOICES-false}" = true ]; then
cp "${KEYMAN_ROOT}/oem/firstvoices/android/app/build/outputs/apk/release/${FIRSTVOICES_APK}" ./
fi
#
# Write download info files
#
cd "${UPLOAD_PATH}"
write_download_info "Keyman Engine for Android" "${KEYMAN_ENGINE_ANDROID_ZIP}" "${KEYMAN_VERSION}" "${KEYMAN_TIER}" "android"
write_download_info "Keyman for Android" "${KEYMAN_APK}" "${KEYMAN_VERSION}" "${KEYMAN_TIER}" "android"
if [ "${RELEASE_OEM_FIRSTVOICES-false}" = true ]; then
write_download_info "FirstVoices Keyboards" "${FIRSTVOICES_APK}" "${KEYMAN_VERSION}" "${KEYMAN_TIER}" "android"
fi
builder_finish_action success archive
fi

1
android/zip-excludes Normal file
View file

@ -0,0 +1 @@
Samples/*/build.sh

View file

@ -5,6 +5,8 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
. "$KEYMAN_ROOT/resources/build/zip.inc.sh"
targets=()
# Build list of available targets from subfolders, if none specified
for d in */; do
@ -29,7 +31,7 @@ builder_parse "$@"
function zipsource() {
local target="$1"
pushd "$1" > /dev/null
7z a -r -x!build -x"!$target.kpj.user" "${target}_source.zip" .
add_zip_files "${target}_source.zip" -x@../zip-excludes -q -r . # -q quiet, -r recursive
popd > /dev/null
}

View file

@ -0,0 +1,3 @@
build/
*.kpj.user
*_source.zip

View file

@ -1,5 +1,5 @@
import { constants } from '@keymanapp/ldml-keyboard-constants';
import { DependencySections, StrsItem, UsetItem } from './kmx-plus.js';
import { DependencySections, StrsItem, StrsOptions, UsetItem } from './kmx-plus.js';
import { ElementParser, ElementSegment, ElementType } from '../../ldml-keyboard/pattern-parser.js';
import * as util from '../../util/util.js';
import MATCH_HEX_ESCAPE = util.MATCH_HEX_ESCAPE;
@ -33,7 +33,7 @@ export class ElementString extends Array<ElemElement> {
* @param source if a string array, does not get reinterpreted as UnicodeSet. This is used with vars, etc. Or pass `["str"]` for an explicit 1-element elem.
* If it is a string, will be interpreted per reorder element rules.
*/
static fromStrings(sections: DependencySections, source: string | string[], order?: string, tertiary?: string, tertiary_base?: string, prebase?: string) : ElementString {
static fromStrings(sections: DependencySections, options: StrsOptions, source: string | string[], order?: string, tertiary?: string, tertiary_base?: string, prebase?: string) : ElementString {
// the returned array
const array = new ElementString();
if(!source) {
@ -81,21 +81,21 @@ export class ElementString extends Array<ElemElement> {
// error. So we can just exit here.
return null; // UnicodeSet error
}
const uset = sections.usetparser.parseUnicodeSet(item.segment, needRanges);
const uset = sections.usetparser.parseUnicodeSet(item.segment, needRanges, options?.x);
if (!uset) {
return null; // UnicodeSet error already thrown
return null; // UnicodeSet error already added to callback
}
elem.uset = sections.uset.allocUset(uset, sections);
elem.value = sections.strs.allocString('', {singleOk: true}); // no string
elem.uset = sections.uset.allocUset(uset, sections, options?.x);
elem.value = sections.strs.allocString('', {...options, singleOk: true}); // no string
} else if (item.type === ElementType.codepoint || item.type === ElementType.escaped || item.type === ElementType.string) {
// some kind of a string
let str = item.segment;
if (item.type === ElementType.escaped && !MATCH_HEX_ESCAPE.test(str)) {
str = unescapeOneQuadString(str);
// TODO-LDML: any other escape forms here?
elem.value = sections.strs.allocString(str, { singleOk: true });
elem.value = sections.strs.allocString(str, { ...options, singleOk: true });
} else {
elem.value = sections.strs.allocString(str, { unescape: true, singleOk: true });
elem.value = sections.strs.allocString(str, { ...options, unescape: true, singleOk: true });
}
// Now did we end up with one char or no?
if (elem.value.isOneChar) {

View file

@ -41,14 +41,14 @@ export class Elem extends Section {
strings: ElementString[] = [];
constructor(sections: DependencySections) {
super();
this.strings.push(ElementString.fromStrings(sections, '')); // C7043: null element string
this.strings.push(ElementString.fromStrings(sections, {}, '')); // C7043: null element string
}
/**
* @param source if a string array, does not get reinterpreted as UnicodeSet. This is used with vars, etc. Or pass `["str"]` for an explicit 1-element elem.
* If it is a string, will be interpreted per reorder element ruls.
*/
allocElementString(sections: DependencySections, source: string | string[], order?: string, tertiary?: string, tertiary_base?: string, prebase?: string): ElementString {
const s = ElementString.fromStrings(sections, source, order, tertiary, tertiary_base, prebase);
allocElementString(sections: DependencySections, options: StrsOptions, source: string | string[], order?: string, tertiary?: string, tertiary_base?: string, prebase?: string): ElementString {
const s = ElementString.fromStrings(sections, options, source, order, tertiary, tertiary_base, prebase);
if (!s) return s;
let result = this.strings.find(item => item.isEqual(s));
if(result === undefined) {
@ -135,6 +135,18 @@ export class StrsItem {
isEqual(a: StrsItem): boolean {
return a.value === this.value && a.char === this.char;
}
private _context: any = undefined;
/** add any context from the options to this strsitem */
setContext(opts?: StrsOptions) {
// At present, there's only a single piece of context available
this._context = this._context || opts?.x;
}
get context() : any {
return this._context;
}
};
/**
@ -161,6 +173,8 @@ export interface StrsOptions {
nfd?: boolean;
/** string can be stored as a single CharStrsItem, not in strs table. */
singleOk?: boolean;
/** optional context */
x?: any;
};
export class Strs extends Section {
@ -192,6 +206,8 @@ export class Strs extends Section {
result = new StrsItem(s);
this.strings.push(result);
}
// give an option to set the context
result.setContext(opts);
return result;
}
@ -200,7 +216,7 @@ export class Strs extends Section {
s = s ?? '';
// type check everything else
if (typeof s !== 'string') {
throw new Error('alloc_string: s must be a string, undefined, or null.');
throw new Error(`Internal Error: processString: s must be a string, undefined, or null, not ${typeof s} ${s}`);
}
// substitute variables
if (opts?.stringVariables) {
@ -358,11 +374,13 @@ export class Vars extends Section {
export class VarsItem extends Section {
id: StrsItem;
value: StrsItem;
x?: any;
constructor(id: string, value: string, sections: DependencySections) {
constructor(id: string, value: string, sections: DependencySections, x?: any) {
super();
this.id = sections.strs.allocString(id);
this.value = sections.strs.allocString(value, {unescape: true});
this.x = x;
}
valid() : boolean {
@ -371,8 +389,8 @@ export class VarsItem extends Section {
};
export class UnicodeSetItem extends VarsItem {
constructor(id: string, value: string, sections: DependencySections, usetparser: UnicodeSetParser) {
super(id, value, sections);
constructor(id: string, value: string, sections: DependencySections, usetparser: UnicodeSetParser, x?: any) {
super(id, value, sections, x);
const needRanges = sections.usetparser.sizeUnicodeSet(value);
if (needRanges >= 0) {
this.unicodeSet = sections.usetparser.parseUnicodeSet(value, needRanges);
@ -385,9 +403,9 @@ export class UnicodeSetItem extends VarsItem {
};
export class SetVarItem extends VarsItem {
constructor(id: string, value: string[], sections: DependencySections) {
super(id, value.join(' '), sections);
this.items = sections.elem.allocElementString(sections, value);
constructor(id: string, value: string[], sections: DependencySections, x?: any) {
super(id, value.join(' '), sections, x);
this.items = sections.elem.allocElementString(sections, {x}, value);
}
items: ElementString; // element string array
valid() : boolean {
@ -396,8 +414,8 @@ export class SetVarItem extends VarsItem {
};
export class StringVarItem extends VarsItem {
constructor(id: string, value: string, sections: DependencySections) {
super(id, value, sections);
constructor(id: string, value: string, sections: DependencySections, x?: any) {
super(id, value, sections, x);
}
// no added fields
};
@ -435,7 +453,7 @@ export class Tran extends Section {
};
export class UsetItem {
constructor(public uset: UnicodeSet, public str: StrsItem) {
constructor(public uset: UnicodeSet, public str: StrsItem, public x?: any) {
}
compareTo(other: UsetItem) : number {
return this.str.compareTo(other.str);
@ -444,11 +462,11 @@ export class UsetItem {
export class Uset extends Section {
usets: UsetItem[] = [];
allocUset(set: UnicodeSet, sections: DependencySections) : UsetItem {
allocUset(set: UnicodeSet, sections: DependencySections, x?: any) : UsetItem {
// match the same pattern
let result = this.usets.find(s => set.pattern == s.uset.pattern);
if (result === undefined) {
result = new UsetItem(set, sections.strs.allocString(set.pattern));
result = new UsetItem(set, sections.strs.allocString(set.pattern), x);
this.usets.push(result);
}
return result;

View file

@ -8,13 +8,13 @@ export interface UnicodeSetParser {
* @param pattern string to parse such as `[a-z]`
* @param rangeCount number of ranges to allow for
*/
parseUnicodeSet(pattern: string, rangeCount: number) : UnicodeSet | null;
parseUnicodeSet(pattern: string, rangeCount: number, x?: any) : UnicodeSet | null;
/**
* Calculate the number of ranges in a UnicodeSet
* @param pattern string to parse such as `[a-z]`
* @returns number of ranges, or -1 (with callback-reported err) on err
*/
sizeUnicodeSet(pattern: string) : number;
sizeUnicodeSet(pattern: string, x?: any) : number;
}
/**

View file

@ -91,12 +91,12 @@ describe('Test of ElementString file', () => {
});
describe('Test of fromStrings()', () => {
it('returns an empty ElementString if source is null', () => {
const es = ElementString.fromStrings({}, null);
const es = ElementString.fromStrings({}, {}, null);
assert.deepEqual(es, new ElementString());
});
it('can create an ElementString from a string array', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌲"]);
const actual = ElementString.fromStrings(sections, {}, ["𐌰", "𐌱", "𐌲"]);
const expected = [
initElemElement(GOTHIC_A),
initElemElement(GOTHIC_B),
@ -106,7 +106,7 @@ describe('Test of ElementString file', () => {
});
it('can create an ElementString from a string', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(sections, "𐌰𐌱𐌲");
const actual = ElementString.fromStrings(sections, {}, "𐌰𐌱𐌲");
const expected = [
initElemElement(GOTHIC_A),
initElemElement(GOTHIC_B),
@ -118,6 +118,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
"1 2 3",
);
@ -132,6 +133,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
"1",
);
@ -146,6 +148,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
"1 2",
);
@ -160,6 +163,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
"1 A 3",
);
@ -174,6 +178,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
null,
"1 2 3",
@ -189,6 +194,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
null,
"1",
@ -204,6 +210,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
null,
"1 2",
@ -219,6 +226,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
null,
"1 A 3",
@ -234,6 +242,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
null,
null,
@ -250,6 +259,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
null,
null,
@ -266,6 +276,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
null,
null,
@ -282,6 +293,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
null,
null,
@ -299,6 +311,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
null,
null,
@ -316,6 +329,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
null,
null,
@ -333,7 +347,7 @@ describe('Test of ElementString file', () => {
ElementParser.segment = stubElementParserSegment_Uset;
sections.strs.allocString = stubStrsAllocString_Str;
sections.uset.allocUset = stubUsetAllocUset;
const actual = ElementString.fromStrings(sections, "[𐌰-𐍊]");
const actual = ElementString.fromStrings(sections, {}, "[𐌰-𐍊]");
const expected = [
initElemElement(
new StrsItem(''),
@ -348,17 +362,17 @@ describe('Test of ElementString file', () => {
it('returns null for an invalid unicode set size', () => {
ElementParser.segment = stubElementParserSegment_Uset;
sections.usetparser.sizeUnicodeSet = (pattern: string) : number => -1;
assert.isNull(ElementString.fromStrings(sections, "[𐌰-𐍊]"));
assert.isNull(ElementString.fromStrings(sections, {}, "[𐌰-𐍊]"));
});
it('returns null if it cannot parse the unicode set', () => {
ElementParser.segment = stubElementParserSegment_Uset;
sections.usetparser.parseUnicodeSet = (pattern: string, rangeCount: number) : UnicodeSet | null => null;
assert.isNull(ElementString.fromStrings(sections, "[𐌰-𐍊]"));
assert.isNull(ElementString.fromStrings(sections, {}, "[𐌰-𐍊]"));
});
it('can handle quad strings', () => {
sections.strs.allocString = stubStrsAllocString_Char;
ElementParser.segment = stubElementParserSegment_Escaped;
const actual = ElementString.fromStrings(sections, "\\ud800\\udf30");
const actual = ElementString.fromStrings(sections, {}, "\\ud800\\udf30");
const expected = [
initElemElement(HI_GOTHIC_A),
initElemElement(LO_GOTHIC_A),
@ -367,7 +381,7 @@ describe('Test of ElementString file', () => {
});
it('can handle ElemElement of string type', () => {
sections.strs.allocString = stubStrsAllocString_Str;
const actual = ElementString.fromStrings(sections, ["𐌰𐌱𐌲",]);
const actual = ElementString.fromStrings(sections, {}, ["𐌰𐌱𐌲",]);
const expected = [
initElemElement(
new StrsItem("𐌰𐌱𐌲"),

View file

@ -1,4 +1,6 @@
import { CompilerCallbacks } from "./compiler-callbacks.js";
import { ObjectWithMetadata } from "./symbol-utils.js";
import { KeymanXMLReader, XML_FILENAME_SYMBOL } from "./xml-utils.js";
/**
* Abstract interface for compiler error and warning messages
@ -206,6 +208,27 @@ export class CompilerError {
}
return null;
}
/**
* Get an offset from o and set event's offset field
* @param event a compiler event, such as from functions in this class
* @param x any object parsed from XML or with the XML_META_DATA_SYMBOL symbol copied over
* @returns modified event object
*/
public static setFromMetadata(event: CompilerEvent, x?: ObjectWithMetadata): CompilerEvent {
if(x) {
const metadata = KeymanXMLReader.getMetaData(x) || {};
const offset = metadata?.startIndex;
if (offset) {
event.offset = offset;
}
const filename = event.filename || metadata[XML_FILENAME_SYMBOL];
if (filename) {
event.filename = filename;
}
}
return event;
}
};
/** @deprecated use `CompilerError.severity` instead */
@ -423,6 +446,40 @@ export function dedentCompilerMessageDetail(event: CompilerEvent) {
return (event.detail ?? '').replace(/^[ ]+/gm, '');
}
/**
* Convenience function for constructing CompilerEvents with line numbers.
* Use it as below: (abbreviated as mx())
*
* ```js
* // Note: Indentation makes "InvalidScanCode" line up thrice
* static ERROR_InvalidScanCode = SevError | 0x0009;
* // Note:
* // 1. All parameters are passed in 'o', the context object is only used for context even if
* // it contains redundant info.
* // 2. No code execution within the arrow function other than the 'mx' call, string interpolation,
* // with `${def(o.property)}` as the max complexity of interpolation.
* static Error_InvalidScanCode = (o:{id: string, invalidCodeList: string}, x: ObjectWithMetadata) => mx(
* this.ERROR_InvalidScanCode, x,
* `Form '${def(o.id)}' has invalid/unknown scancodes '${def(o.codes)}'`,
* // Note: If detail is omitted, leave the trailing comma on the prior line to leave room for it
* `…additional markdown detail…`
* );
* ```
*
* @param code Unique numeric value of the event
* @param message A short description of the error presented to the user
* @param context Object to be used as a source for line number information
* @param detail Detailed Markdown-formatted description of the error
* including references to documentation, remediation options.
* @see CompilerMessageSpec
* @returns the event
*/
export function CompilerMessageObjectSpec(code: number, context: ObjectWithMetadata, message: string, detail?: string): CompilerEvent {
let evt = CompilerMessageSpec(code, message, detail); // constructs raw message
evt = CompilerError.setFromMetadata(evt, context); // updates with offset from context
return evt;
};
export const CompilerMessageDef = (param: any) => String(param ?? `<param>`);
export const CompilerMessageSpecWithException = (code: number, message: string, exceptionVar: any, detail?: string) : CompilerEvent => ({

View file

@ -48,7 +48,7 @@ export {
export { defaultCompilerOptions, CompilerBaseOptions, CompilerOptions, CompilerEvent, CompilerErrorNamespace,
CompilerErrorSeverity, CompilerCallbackOptions,
CompilerError, CompilerMessageSpec, CompilerMessageSpecWithException, compilerErrorSeverity, CompilerErrorMask, compilerErrorSeverityName,
CompilerError, CompilerMessageSpec, CompilerMessageObjectSpec, CompilerMessageSpecWithException, compilerErrorSeverity, CompilerErrorMask, compilerErrorSeverityName,
compilerErrorFormatCode, CompilerMessageDef,
compilerLogLevelToSeverity, CompilerLogLevel, compilerEventFormat, ALL_COMPILER_LOG_LEVELS,
ALL_COMPILER_LOG_FORMATS, CompilerLogFormat,

View file

@ -9,7 +9,8 @@ TODO: implement additional interfaces:
import { VisualKeyboard, KvkFileReader, LdmlKeyboardTypes, KeymanFileTypes, KvkFileWriter } from '@keymanapp/common-types';
import {
CompilerCallbacks, CompilerEvent, CompilerOptions, KeymanCompiler, KeymanCompilerArtifacts,
KeymanCompilerArtifactOptional, KeymanCompilerResult, KeymanCompilerArtifact, KvksFileReader
KeymanCompilerArtifactOptional, KeymanCompilerResult, KeymanCompilerArtifact, KvksFileReader,
CompilerError
} from '@keymanapp/developer-utils';
import * as Osk from './osk.js';
import loadWasmHost from '../import/kmcmplib/wasm-host.js';
@ -529,7 +530,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet
* @param rangeCount - number of ranges to allocate
* @returns UnicodeSet accessor object, or null on failure
*/
public parseUnicodeSet(pattern: string, rangeCount: number) : LdmlKeyboardTypes.UnicodeSet | null {
public parseUnicodeSet(pattern: string, rangeCount: number, x?: any) : LdmlKeyboardTypes.UnicodeSet | null {
if(!this.verifyInitialized()) {
/* c8 ignore next 2 */
// verifyInitialized will set a callback if needed
@ -562,7 +563,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet
// rc is negative: it's an error code.
this.wasmExports.free(buf);
// translate error code into callback
this.callbacks.reportMessage(getUnicodeSetError(rc));
this.callbacks.reportMessage(getUnicodeSetError(rc, x));
return null;
}
}
@ -570,7 +571,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet
/**
* @internal
*/
public sizeUnicodeSet(pattern: string) : number {
public sizeUnicodeSet(pattern: string, x?: any) : number {
if(!this.verifyInitialized()) {
/* c8 ignore next 2 */
return null;
@ -582,7 +583,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet
if (rc >= 0) {
return rc;
} else {
this.callbacks.reportMessage(getUnicodeSetError(rc));
this.callbacks.reportMessage(getUnicodeSetError(rc, x));
return -1;
}
}
@ -593,7 +594,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet
* @param rc parseUnicodeSet error code
* @returns the compiler event
*/
function getUnicodeSetError(rc: number) : CompilerEvent {
function getUnicodeSetError(rc: number, x?: any) : CompilerEvent {
// from kmcmplib.h
const KMCMP_ERROR_SYNTAX_ERR = -1;
const KMCMP_ERROR_HAS_STRINGS = -2;
@ -601,16 +602,16 @@ function getUnicodeSetError(rc: number) : CompilerEvent {
const KMCMP_FATAL_OUT_OF_RANGE = -4;
switch(rc) {
case KMCMP_ERROR_SYNTAX_ERR:
return KmnCompilerMessages.Error_UnicodeSetSyntaxError();
return KmnCompilerMessages.Error_UnicodeSetSyntaxError(x);
case KMCMP_ERROR_HAS_STRINGS:
return KmnCompilerMessages.Error_UnicodeSetHasStrings();
return KmnCompilerMessages.Error_UnicodeSetHasStrings(x);
case KMCMP_ERROR_UNSUPPORTED_PROPERTY:
return KmnCompilerMessages.Error_UnicodeSetHasProperties();
return KmnCompilerMessages.Error_UnicodeSetHasProperties(x);
case KMCMP_FATAL_OUT_OF_RANGE:
return KmnCompilerMessages.Fatal_UnicodeSetOutOfRange();
return KmnCompilerMessages.Fatal_UnicodeSetOutOfRange(x);
default:
/* c8 ignore next */
return KmnCompilerMessages.Fatal_UnexpectedException({e: `Unexpected UnicodeSet error code ${rc}`});
return CompilerError.setFromMetadata(KmnCompilerMessages.Fatal_UnexpectedException({e: `Unexpected UnicodeSet error code ${rc}`}), x);
}
}

View file

@ -1,6 +1,6 @@
import { KeyAddress } from "../kmw-compiler/validate-layout-file.js";
import { kmnfile } from "../kmw-compiler/compiler-globals.js";
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerEvent, CompilerMessageSpec as m, CompilerMessageDef as def, CompilerMessageSpecWithException, KeymanUrls } from "@keymanapp/developer-utils";
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerEvent, CompilerMessageSpec as m, CompilerMessageObjectSpec as mx, CompilerMessageDef as def, CompilerMessageSpecWithException, KeymanUrls, ObjectWithMetadata } from "@keymanapp/developer-utils";
const Namespace = CompilerErrorNamespace.KmnCompiler;
const SevInfo = CompilerErrorSeverity.Info | Namespace;
@ -12,6 +12,9 @@ const SevFatal = CompilerErrorSeverity.Fatal | Namespace;
// For messages from the KeymanWeb compiler, we need to construct our messages
// slightly differently. This could be refactored in the future, as it is not
// obvious which messages should use which function.
// Specifically, this could perhaps use CompilerFileCallbacks which provides a
// default filename.
// Then, there could be a CompilerMessageSpecWithLine
const mw = (code: number, message: string, o?: {filename?: string, line?: number}) : CompilerEvent => ({
...m(code, message),
filename: o?.filename ?? kmnfile,
@ -104,9 +107,8 @@ export class KmnCompilerMessages {
);
static FATAL_UnicodeSetOutOfRange = SevFatal | 0x904;
static Fatal_UnicodeSetOutOfRange = () => CompilerMessageSpecWithException(
this.FATAL_UnicodeSetOutOfRange,
null,
static Fatal_UnicodeSetOutOfRange = (x?: ObjectWithMetadata) => mx(
this.FATAL_UnicodeSetOutOfRange, x,
`UnicodeSet buffer was too small`,
`Raised when caller to UnicodeSet functions provides an invalid buffer. If
you experience this error, it should be reported to the Keyman team for
@ -116,8 +118,8 @@ export class KmnCompilerMessages {
// TODO: rename the following functions to Error_UsetHasStrings etc
static ERROR_UnicodeSetHasStrings = SevError | 0x905;
static Error_UnicodeSetHasStrings = () => m(
this.ERROR_UnicodeSetHasStrings,
static Error_UnicodeSetHasStrings = (x?: ObjectWithMetadata) => mx(
this.ERROR_UnicodeSetHasStrings, x,
`uset contains strings, not allowed`,
`The provided uset uses multi-character strings, (\`{}\` notation, e.g.
\`[żġħ{ie}{għ}]\`. ). Although full UnicodeSets support strings, LDML
@ -128,8 +130,8 @@ export class KmnCompilerMessages {
);
static ERROR_UnicodeSetHasProperties = SevError | 0x906;
static Error_UnicodeSetHasProperties = () => m(
this.ERROR_UnicodeSetHasProperties,
static Error_UnicodeSetHasProperties = (x?: ObjectWithMetadata) => mx(
this.ERROR_UnicodeSetHasProperties, x,
`uset contains properties, not allowed`,
`The provided uset uses property notation (\`\\p{…}\` or \`[:…:]\`). LDML
keyboards do not support Unicode properties in usets, because that would
@ -140,8 +142,8 @@ export class KmnCompilerMessages {
);
static ERROR_UnicodeSetSyntaxError = SevError | 0x907;
static Error_UnicodeSetSyntaxError = () => m(
this.ERROR_UnicodeSetSyntaxError,
static Error_UnicodeSetSyntaxError = (x?: ObjectWithMetadata) => mx(
this.ERROR_UnicodeSetSyntaxError, x,
`uset had a Syntax Error while parsing`,
`The provided uset has a syntax error and could not be parsed. Verify the
format of the uset against the specification.

View file

@ -12,9 +12,10 @@ import { SubstitutionUse, Substitutions } from "./substitution-tracker.js";
export class DispCompiler extends SectionCompiler {
static validateSubstitutions(keyboard: LDMLKeyboard.LKKeyboard, st : Substitutions): boolean {
keyboard.displays?.display?.forEach(({ display, output }) => {
st.addStringAndMarkerSubstitution(SubstitutionUse.match, output);
st.addStringSubstitution(SubstitutionUse.emit, display);
keyboard.displays?.display?.forEach((e) => {
const { display, output } = e;
st.addStringAndMarkerSubstitution(SubstitutionUse.match, output, e);
st.addStringSubstitution(SubstitutionUse.emit, display, e);
});
// no marker references in 'id'
return true;
@ -34,18 +35,18 @@ export class DispCompiler extends SectionCompiler {
for (const display of this.keyboard3.displays?.display) {
const { output, keyId } = display;
if ((output && keyId) || (!output && !keyId)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_DisplayNeedsToOrId({ output, keyId }, display));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_DisplayNeedsToOrId({ display: display.display }, display));
return false;
} else if (output) {
if (tos.has(output)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_DisplayIsRepeated({ output }, display));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_DisplayIsRepeated({ display: display.display }, display));
return false;
} else {
tos.add(output);
}
} else if (keyId) {
if (ids.has(keyId)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_DisplayIsRepeated({ keyId }, display));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_DisplayIsRepeated({ display: display.display }, display));
return false;
} else {
ids.add(keyId);
@ -61,7 +62,7 @@ export class DispCompiler extends SectionCompiler {
const result = new Disp();
// displayOptions
result.baseCharacter = sections.strs.allocString(this.keyboard3.displays?.displayOptions?.baseCharacter, {unescape: true});
result.baseCharacter = sections.strs.allocString(this.keyboard3.displays?.displayOptions?.baseCharacter, { unescape: true, x: this.keyboard3?.displays?.displayOptions });
// displays
result.disps = this.keyboard3.displays?.display.map(display => ({
@ -69,11 +70,13 @@ export class DispCompiler extends SectionCompiler {
stringVariables: true,
markers: true,
unescape: true,
x: display,
}, sections),
id: sections.strs.allocString(display.keyId), // not escaped, not substituted
id: sections.strs.allocString(display.keyId, { x: display }), // not escaped, not substituted
display: sections.strs.allocString(display.display, {
stringVariables: true,
unescape: true,
x: display,
}, sections),
})) || []; // TODO-LDML: need coverage for the []

View file

@ -1,7 +1,7 @@
import { SectionIdent, constants } from '@keymanapp/ldml-keyboard-constants';
import { SectionCompiler } from "./section-compiler.js";
import { util, KMXPlus, LdmlKeyboardTypes } from "@keymanapp/common-types";
import { CompilerCallbacks, LDMLKeyboard } from "@keymanapp/developer-utils";
import { CompilerCallbacks, LDMLKeyboard, ObjectWithMetadata } from "@keymanapp/developer-utils";
import { VarsCompiler } from './vars.js';
import { LdmlCompilerMessages } from './ldml-compiler-messages.js';
@ -33,13 +33,26 @@ export class StrsCompiler extends EmptyCompiler {
public postValidate(section?: KMXPlus.Section): boolean {
const strs = <KMXPlus.Strs>section;
/** attempt to find a context object for the string */
function findContextForString(s: string): ObjectWithMetadata {
// try exact match
for(const str of strs.strings) {
if (str.value == s) return str.context;
}
// try substring match
for(const str of strs.strings) {
if (str.value.includes(s)) return str.context;
}
return null;
}
if (strs) {
const badStringAnalyzer = new util.BadStringAnalyzer();
const CONTAINS_MARKER_REGEX = new RegExp(LdmlKeyboardTypes.MarkerParser.ANY_MARKER_MATCH);
for (let s of strs.allProcessedStrings.values()) {
// stop at the first denormalized string
if (!util.isNormalized(s)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Warn_StringDenorm({s}));
this.callbacks.reportMessage(LdmlCompilerMessages.Warn_StringDenorm({s}, findContextForString(s)));
}
// replace all \\uXXXX with the actual code point.
// this lets us analyze whether there are PUA, unassigned, etc.
@ -63,16 +76,19 @@ export class StrsCompiler extends EmptyCompiler {
const illegals = m.get(util.BadStringType.illegal);
if (puas) {
const [count, lowestCh] = [puas.size, Array.from(puas.values()).sort((a, b) => a - b)[0]];
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_PUACharacters({ count, lowestCh }))
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_PUACharacters({ count, lowestCh: util.describeCodepoint(lowestCh) },
findContextForString(String.fromCodePoint(lowestCh))));
}
if (unassigneds) {
const [count, lowestCh] = [unassigneds.size, Array.from(unassigneds.values()).sort((a, b) => a - b)[0]];
this.callbacks.reportMessage(LdmlCompilerMessages.Warn_UnassignedCharacters({ count, lowestCh }))
this.callbacks.reportMessage(LdmlCompilerMessages.Warn_UnassignedCharacters({ count, lowestCh: util.describeCodepoint(lowestCh) },
findContextForString(String.fromCodePoint(lowestCh))));
}
if (illegals) {
// do this last, because we will return false.
const [count, lowestCh] = [illegals.size, Array.from(illegals.values()).sort((a, b) => a - b)[0]];
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalCharacters({ count, lowestCh }))
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalCharacters({ count, lowestCh: util.describeCodepoint(lowestCh) },
findContextForString(String.fromCodePoint(lowestCh))));
return false;
}
}

View file

@ -37,7 +37,7 @@ export class KeysCompiler extends SectionCompiler {
for (const keyId of usedKeys.values()) {
const key = keyBag.get(keyId);
if (!key) continue; // key not found is handled elsewhere.
st.addStringAndMarkerSubstitution(SubstitutionUse.emit, key.output);
st.addStringAndMarkerSubstitution(SubstitutionUse.emit, key.output, key);
}
return true;
}
@ -324,15 +324,16 @@ export class KeysCompiler extends SectionCompiler {
// allocate the in-memory <flick id=…>
const flicks: KeysFlicks = new KeysFlicks(
sections.strs.allocString(flickId)
sections.strs.allocString(flickId, { x: flick })
);
// add data from each segment
for (const { keyId, directions } of flick.flickSegment) {
const keyIdStr = sections.strs.allocString(keyId);
for (const flickSegment of flick.flickSegment) {
const { keyId, directions } = flickSegment;
const keyIdStr = sections.strs.allocString(keyId, { x: flickSegment });
const directionsList: ListItem = sections.list.allocListFromSpaces(
directions,
{ },
{ x: flickSegment },
sections);
flicks.flicks.push({
directions: directionsList,
@ -369,20 +370,20 @@ export class KeysCompiler extends SectionCompiler {
if (!!gap) {
flags |= constants.keys_key_flags_gap;
}
const id = sections.strs.allocString(key.id);
const id = sections.strs.allocString(key.id, { x: key });
const longPress: ListItem = sections.list.allocListFromSpaces(
longPressKeyIds, {},
longPressKeyIds, { x: key },
sections);
const longPressDefault = sections.strs.allocString(longPressDefaultKeyId,
{},
{ x: key },
sections);
const multiTap: ListItem = sections.list.allocListFromSpaces(
multiTapKeyIds,
{},
{ x: key },
sections);
const keySwitch = sections.strs.allocString(layerId); // 'switch' is a reserved word
const keySwitch = sections.strs.allocString(layerId, { x: key }); // 'switch' is a reserved word
const toRaw = output;
@ -395,6 +396,7 @@ export class KeysCompiler extends SectionCompiler {
unescape: true,
singleOk: true,
nfd: true,
x: key,
},
sections);
if (!to.isOneChar) {
@ -461,7 +463,7 @@ export class KeysCompiler extends SectionCompiler {
const { modifiers } = layer;
if (!validModifier(modifiers)) {
this.callbacks.reportMessage(
LdmlCompilerMessages.Error_InvalidModifier({ modifiers, id: layer.id }, layer)
LdmlCompilerMessages.Error_InvalidModifier({ modifiers }, layer)
);
valid = false;
}
@ -482,7 +484,7 @@ export class KeysCompiler extends SectionCompiler {
LdmlCompilerMessages.Error_RowOnHardwareLayerHasTooManyKeys({
row: y + 1,
hardware: layers.formId,
modifiers,
modifiers: modifiers || 'none',
}, row)
);
valid = false;

View file

@ -52,7 +52,7 @@ export class LayrCompiler extends SectionCompiler {
const { modifiers } = layer;
totalLayerCount++;
if (!validModifier(modifiers)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidModifier({ modifiers, id: layer.id }, layer));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidModifier({ modifiers }, layer));
valid = false;
}
});
@ -69,13 +69,13 @@ export class LayrCompiler extends SectionCompiler {
const sect = new Layr();
sect.lists = this.keyboard3.layers.map((layers) => {
const hardware = sections.strs.allocString(layers.formId);
const hardware = sections.strs.allocString(layers.formId, {x:layers});
// Already validated in validate
const layerEntries = [];
for (const layer of layers.layer) {
const rows = layer.row.map((row) => {
const erow: LayrRow = {
keys: row.keys.trim().split(/[ \t]+/).map((id) => sections.strs.allocString(id)),
keys: row.keys.trim().split(/[ \t]+/).map((id) => sections.strs.allocString(id, { x: row })),
};
// include linenumber info for row
return SectionCompiler.copySymbols(erow, row);
@ -84,7 +84,7 @@ export class LayrCompiler extends SectionCompiler {
// push a layer entry for each modifier set
for (const mod of mods) {
layerEntries.push({
id: sections.strs.allocString(layer.id),
id: sections.strs.allocString(layer.id, {x:layer}),
mod,
rows,
});

View file

@ -1,5 +1,4 @@
import { util } from "@keymanapp/common-types";
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def, XML_FILENAME_SYMBOL, CompilerEvent, KeymanXMLReader, ObjectWithMetadata } from '@keymanapp/developer-utils';
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageObjectSpec as mx, CompilerMessageSpec as m, CompilerMessageDef as def, ObjectWithMetadata } from '@keymanapp/developer-utils';
// const SevInfo = CompilerErrorSeverity.Info | CompilerErrorNamespace.LdmlKeyboardCompiler;
const SevHint = CompilerErrorSeverity.Hint | CompilerErrorNamespace.LdmlKeyboardCompiler;
const SevWarn = CompilerErrorSeverity.Warn | CompilerErrorNamespace.LdmlKeyboardCompiler;
@ -9,42 +8,6 @@ const SevError = CompilerErrorSeverity.Error | CompilerErrorNamespace.LdmlKeyboa
// sub-numberspace for transform errors
const SevErrorTransform = SevError | 0xF00;
/**
* Convenience function for constructing CompilerEvents with line numbers.
* Use it as below: (abbreviated as mx())
*
* ```js
* // Note: Indentation makes "InvalidScanCode" line up thrice
* static ERROR_InvalidScanCode = SevError | 0x0009;
* // Note:
* // 1. All parameters are passed in 'o', the context object is only used for context even if
* // it contains redundant info.
* // 2. No code execution within the arrow function other than the 'mx' call, string interpolation,
* // with `${def(o.property)}` as the max complexity of interpolation.
* static Error_InvalidScanCode = (o:{id: string, invalidCodeList: string}, x: ObjectWithMetadata) => mx(
* this.ERROR_InvalidScanCode, x,
* `Form '${def(o.id)}' has invalid/unknown scancodes '${def(o.codes)}'`,
* // Note: If detail is omitted, leave the trailing comma on the prior line to leave room for it
* `…additional markdown detail…`
* );
* ```
*
* @param code Unique numeric value of the event
* @param message A short description of the error presented to the user
* @param context Object to be used as a source for line number information
* @param detail Detailed Markdown-formatted description of the error
* including references to documentation, remediation options.
* @see CompilerMessageSpec
* @returns
*/
function CompilerMessageObjectSpec(code: number, context: ObjectWithMetadata, message: string, detail?: string): CompilerEvent {
let evt = m(code, message, detail); // constructs raw message
evt = LdmlCompilerMessages.offset(evt, context); // updates with offset from context
return evt;
};
const mx = CompilerMessageObjectSpec;
/**
* @internal
*/
@ -70,7 +33,7 @@ export class LdmlCompilerMessages {
static ERROR_RowOnHardwareLayerHasTooManyKeys = SevError | 0x0004;
static Error_RowOnHardwareLayerHasTooManyKeys = (o: { row: number, hardware: string, modifiers: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_RowOnHardwareLayerHasTooManyKeys, x,
`Row #${def(o.row)} on 'hardware' ${def(o.hardware)} layer for modifier ${o.modifiers || 'none'} has too many keys`,
`Row #${def(o.row)} on 'hardware' ${def(o.hardware)} layer for modifier ${def(o.modifiers)} has too many keys`,
);
static ERROR_KeyNotFoundInKeyBag = SevError | 0x0005;
@ -85,6 +48,7 @@ export class LdmlCompilerMessages {
`After minimization, one or more locales is repeated and has been removed`,
);
// This is the only allowed use of m() vs mx() in this file, all the others take context.
static ERROR_InvalidFile = SevError | 0x0007;
static Error_InvalidFile = (o:{errorText: string}) =>
m(this.ERROR_InvalidFile, `The source file has an invalid structure: ${def(o.errorText)}`);
@ -108,15 +72,16 @@ export class LdmlCompilerMessages {
);
static ERROR_GestureKeyNotFoundInKeyBag = SevError | 0x000B;
static Error_GestureKeyNotFoundInKeyBag = (o:{keyId: string, parentKeyId: string, attribute: string}, x?: ObjectWithMetadata) =>
mx(
static Error_GestureKeyNotFoundInKeyBag = (o:{keyId: string, parentKeyId: string, attribute: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_GestureKeyNotFoundInKeyBag, x,
`Key '${def(o.keyId)}' not found in key bag, referenced from other '${def(o.parentKeyId)}' in ${def(o.attribute)}`,
);
static HINT_NoDisplayForMarker = SevHint | 0x000C;
static Hint_NoDisplayForMarker = (o: { id: string }) =>
m(this.HINT_NoDisplayForMarker, `Key element with id "${def(o.id)}" has only marker output, but there is no matching display element by output or keyId. Keycap may be blank.`);
static Hint_NoDisplayForMarker = (o: { id: string }, x?: ObjectWithMetadata) => mx(
this.HINT_NoDisplayForMarker, x,
`Key element with id "${def(o.id)}" has only marker output, but there is no matching display element by output or keyId. Keycap may be blank.`,
);
static ERROR_InvalidVersion = SevError | 0x000D;
static Error_InvalidVersion = (o: { version: string; }, x?: ObjectWithMetadata) => mx(
@ -139,26 +104,15 @@ export class LdmlCompilerMessages {
);
static HINT_NoDisplayForSwitch = SevHint | 0x000F;
static Hint_NoDisplayForSwitch = (o: { id: string }) =>
m(this.HINT_NoDisplayForSwitch, `Key element with id "${def(o.id)}" is a layer switch key, but there is no matching display element by keyId. Keycap may be blank.`);
/** annotate the to= or id= entry */
private static outputOrKeyId(o:{output?: string, keyId?: string}) {
if (o.output && o.keyId) {
return `output='${o.output}' keyId='${o.keyId}'`;
} else if(o.keyId) {
return `keyId='${o.keyId}'`;
} else if (o.output) {
return `output='${o.output}'`;
} else {
return '';
}
}
static Hint_NoDisplayForSwitch = (o: { id: string }, x?: ObjectWithMetadata) => mx(
this.HINT_NoDisplayForSwitch, x,
`Key element with id "${def(o.id)}" is a layer switch key, but there is no matching display element by keyId. Keycap may be blank.`,
);
static ERROR_DisplayIsRepeated = SevError | 0x0010;
static Error_DisplayIsRepeated = (o:{output?: string, keyId?: string}, x?: ObjectWithMetadata) => mx(
static Error_DisplayIsRepeated = (o:{display?: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_DisplayIsRepeated, x,
`display ${LdmlCompilerMessages.outputOrKeyId(o)} has more than one display entry.`,
`display display='${def(o.display)}' refers to the same keyId or output as another entry.`,
);
static ERROR_KeyMissingToGapOrSwitch = SevError | 0x0011;
@ -179,18 +133,10 @@ export class LdmlCompilerMessages {
`layers has invalid value formId=${def(o.formId)}`,
);
private static layerIdOrEmpty(layer : string) {
if (layer) {
return ` on layer id=${def(layer)}`;
} else {
return '';
}
}
static ERROR_InvalidModifier = SevError | 0x0014;
static Error_InvalidModifier = (o:{modifiers: string, id: string}, x?: ObjectWithMetadata) => mx(
static Error_InvalidModifier = (o:{modifiers: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_InvalidModifier, x,
`layer has invalid modifiers='${def(o.modifiers)}'` + LdmlCompilerMessages.layerIdOrEmpty(o.id),
`layer has invalid modifiers='${def(o.modifiers)}'`,
);
static ERROR_MissingFlicks = SevError | 0x0015;
@ -237,12 +183,16 @@ export class LdmlCompilerMessages {
);
static ERROR_MissingSetVariable = SevError | 0x001D;
static Error_MissingSetVariable = (o:{id: string}) =>
m(this.ERROR_MissingSetVariable, `Reference to undefined set variable: \$[${def(o.id)}]`);
static Error_MissingSetVariable = (o:{id: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_MissingSetVariable, x,
`Reference to undefined set variable: \$[${def(o.id)}]`,
);
static ERROR_MissingUnicodeSetVariable = SevError | 0x001E;
static Error_MissingUnicodeSetVariable = (o:{id: string}) =>
m(this.ERROR_MissingUnicodeSetVariable, `Reference to undefined UnicodeSet variable: \$[${def(o.id)}]`);
static Error_MissingUnicodeSetVariable = (o:{id: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_MissingUnicodeSetVariable, x,
`Reference to undefined UnicodeSet variable: \$[${def(o.id)}]`,
);
static ERROR_NeedSpacesBetweenSetVariables = SevError | 0x001F;
static Error_NeedSpacesBetweenSetVariables = (o:{item: string}, x?: ObjectWithMetadata) => mx(
@ -257,79 +207,88 @@ export class LdmlCompilerMessages {
);
static ERROR_MissingMarkers = SevError | 0x0021;
static Error_MissingMarkers = (o: { ids: string[] }) =>
m(this.ERROR_MissingMarkers, `Markers used for matching but not defined: ${def(o.ids?.join(','))}`);
static Error_MissingMarkers = (o: { ids: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_MissingMarkers, x,
`Markers used for matching but not defined: ${def(o.ids)}`
);
static ERROR_DisplayNeedsToOrId = SevError | 0x0022;
static Error_DisplayNeedsToOrId = (o:{output?: string, keyId?: string}, x?: ObjectWithMetadata) => mx(
static Error_DisplayNeedsToOrId = (o:{display?: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_DisplayNeedsToOrId, x,
`display ${LdmlCompilerMessages.outputOrKeyId(o)} needs output= or keyId=, but not both`,
`display display='${def(o.display)}' needs output= or keyId=, but not both`,
);
static HINT_PUACharacters = SevHint | 0x0023;
static Hint_PUACharacters = (o: { count: number, lowestCh: number }) =>
m(this.HINT_PUACharacters, `File contains ${def(o.count)} PUA character(s), including ${util.describeCodepoint(o.lowestCh)}`);
static Hint_PUACharacters = (o: { count: number, lowestCh: string }, x?: ObjectWithMetadata) => mx(
this.HINT_PUACharacters, x,
`File contains ${def(o.count)} PUA character(s), including ${def(o.lowestCh)}`,
);
static WARN_UnassignedCharacters = SevWarn | 0x0024;
static Warn_UnassignedCharacters = (o: { count: number, lowestCh: number }) =>
m(this.WARN_UnassignedCharacters, `File contains ${def(o.count)} unassigned character(s), including ${util.describeCodepoint(o.lowestCh)}`);
static Warn_UnassignedCharacters = (o: { count: number, lowestCh: string }, x?: ObjectWithMetadata) => mx(
this.WARN_UnassignedCharacters, x,
`File contains ${def(o.count)} unassigned character(s), including ${def(o.lowestCh)}`,
);
static ERROR_IllegalCharacters = SevError | 0x0025;
static Error_IllegalCharacters = (o: { count: number, lowestCh: number }) =>
m(this.ERROR_IllegalCharacters, `File contains ${def(o.count)} illegal character(s), including ${util.describeCodepoint(o.lowestCh)}`);
static Error_IllegalCharacters = (o: { count: number, lowestCh: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_IllegalCharacters, x,
`File contains ${def(o.count)} illegal character(s), including ${def(o.lowestCh) }`,
);
static HINT_CharClassImplicitDenorm = SevHint | 0x0026;
static Hint_CharClassImplicitDenorm = (o: { lowestCh: number }, x?: ObjectWithMetadata) => mx(
this.HINT_CharClassImplicitDenorm, x,
`File has character classes which span non-NFD character(s), including ${util.describeCodepoint(o.lowestCh)}. These will not match any text.`,
`File has character classes which span non-NFD character(s), including ${def(o.lowestCh)}. These will not match any text.`,
);
static WARN_CharClassExplicitDenorm = SevWarn | 0x0027;
static Warn_CharClassExplicitDenorm = (o: { lowestCh: number }, x?: ObjectWithMetadata) => mx(
this.WARN_CharClassExplicitDenorm, x,
`File has character classes which include non-NFD characters(s), including ${util.describeCodepoint(o.lowestCh)}. These will not match any text.`,
`File has character classes which include non-NFD characters(s), including ${def(o.lowestCh)}. These will not match any text.`,
);
static ERROR_UnparseableReorderSet = SevError | 0x0028;
static Error_UnparseableReorderSet = (o: { from: string, set: string }) =>
m(this.ERROR_UnparseableReorderSet, `Illegal UnicodeSet "${def(o.set)}" in reorder "${def(o.from)}`);
// Available: 0x0028
static ERROR_InvalidVariableIdentifier = SevError | 0x0029;
static Error_InvalidVariableIdentifier = (o: { id: string }) => m(
this.ERROR_InvalidVariableIdentifier,
static Error_InvalidVariableIdentifier = (o: { id: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_InvalidVariableIdentifier, x,
`Invalid variable identifier "${def(o.id)}". Identifiers must be between 1 and 32 characters, and can use A-Z, a-z, 0-9, and _.`,
);
static ERROR_InvalidMarkerIdentifier = SevError | 0x002A;
static Error_InvalidMarkerIdentifier = (o: { id: string }) => m(
this.ERROR_InvalidMarkerIdentifier,
static Error_InvalidMarkerIdentifier = (o: { id: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_InvalidMarkerIdentifier, x,
`Invalid marker identifier "\m{${def(o.id)}}". Identifiers must be between 1 and 32 characters, and can use A-Z, a-z, 0-9, and _.`,
);
static WARN_StringDenorm = SevWarn | 0x002B;
static Warn_StringDenorm = (o: { s: string }) =>
m(this.WARN_StringDenorm, `File contains string "${def(o.s)}" that is neither NFC nor NFD.`);
static Warn_StringDenorm = (o: { s: string }, x?: ObjectWithMetadata) => mx(
this.WARN_StringDenorm, x,
`File contains string "${def(o.s)}" that is neither NFC nor NFD.`,
);
static ERROR_DuplicateLayerWidth = SevError | 0x002C;
static Error_DuplicateLayerWidth = (o: { minDeviceWidth: number }, x?: ObjectWithMetadata) => mx(
this.ERROR_DuplicateLayerWidth, x,
`Two or more layers have minDeviceWidth=${def(o?.minDeviceWidth)}`,
`Two or more layers have minDeviceWidth=${def(o.minDeviceWidth)}`,
`Touch layers must have distinct widths.`
);
static ERROR_InvalidLayerWidth = SevError | 0x002D;
static Error_InvalidLayerWidth = (o: { minDeviceWidth: number }, x?: ObjectWithMetadata) => mx(
this.ERROR_InvalidLayerWidth, x,
`Invalid Layers minDeviceWidth=${def(o?.minDeviceWidth)}`,
`Invalid Layers minDeviceWidth=${def(o.minDeviceWidth)}`,
`Width must be between 1-999 (millimeters), inclusive.` // sync with layr_max_minDeviceWidth / layr_max_maxDeviceWidth (from spec)
);
// Available: 0x02E-0x2F
static ERROR_InvalidQuadEscape = SevError | 0x0030;
static Error_InvalidQuadEscape = (o: { cp: number }, x?: ObjectWithMetadata) => mx(
static Error_InvalidQuadEscape = (o: { cp: string, recommended: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_InvalidQuadEscape, x,
`Invalid escape "\\u${util.hexQuad(o?.cp || 0)}". Hint: Use "\\u{${def(o?.cp?.toString(16))}}"`,
`Invalid escape "${def(o.cp)}"`,
`**Hint**: Use "${def(o.recommended)}"`,
);
//
@ -337,16 +296,18 @@ export class LdmlCompilerMessages {
// This is a bit of a catch-all and represents messages bubbling up from the underlying regex engine
static ERROR_UnparseableTransformFrom = SevErrorTransform | 0x00;
static Error_UnparseableTransformFrom = (o: { from: string, message: string }) =>
m(this.ERROR_UnparseableTransformFrom, `Invalid transform from="${def(o.from)}": "${def(o.message)}"`);
static Error_UnparseableTransformFrom = (o: { from: string, message: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_UnparseableTransformFrom, x,
`Invalid transform from="${def(o.from)}": "${def(o.message)}"`,
);
//------------------------------------------------------------------------------|
// max length of detail message lines (checked by verifyCompilerMessagesObject) |
//------------------------------------------------------------------------------|
static ERROR_IllegalTransformDollarsign = SevErrorTransform | 0x01;
static Error_IllegalTransformDollarsign = (o: { from: string }) => m(
this.ERROR_IllegalTransformDollarsign,
static Error_IllegalTransformDollarsign = (o: { from: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_IllegalTransformDollarsign, x,
`Invalid transform from="${def(o.from)}": Unescaped dollar-sign ($) is not valid transform syntax.`, `
**Hint**: Use \`\\$\` to match a literal dollar-sign. If this precedes a
variable name, the variable name may not be valid (A-Z, a-z, 0-9, _, 32
@ -360,51 +321,30 @@ export class LdmlCompilerMessages {
);
static ERROR_IllegalTransformPlus = SevErrorTransform | 0x03;
static Error_IllegalTransformPlus = (o: { from: string }) => m(
this.ERROR_IllegalTransformPlus,
static Error_IllegalTransformPlus = (o: { from: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_IllegalTransformPlus, x,
`Invalid transform from="${def(o.from)}": Unescaped plus (+) is not valid transform syntax.`, `
**Hint**: Use \`\\+\` to match a literal plus.
`);
static ERROR_IllegalTransformAsterisk = SevErrorTransform | 0x04;
static Error_IllegalTransformAsterisk = (o: { from: string }) =>m(
this.ERROR_IllegalTransformAsterisk,
static Error_IllegalTransformAsterisk = (o: { from: string }, x?: ObjectWithMetadata) =>mx(
this.ERROR_IllegalTransformAsterisk, x,
`Invalid transform from="${def(o.from)}": Unescaped asterisk (*) is not valid transform syntax.`, `
**Hint**: Use \`\\*\` to match a literal asterisk.
`);
static ERROR_IllegalTransformToUset = SevErrorTransform | 0x05;
static Error_IllegalTransformToUset = (o: { to: string }) => m(
this.ERROR_IllegalTransformToUset,
static Error_IllegalTransformToUset = (o: { to: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_IllegalTransformToUset, x,
`Invalid transform to="${def(o.to)}": Set variable (\\$[…]) cannot be used in 'to=' unless part of a map.`, `
**Hint**: If a map was meant, must use the form
\`<transform from="($[fromSet])" to="$[1:toSet]"/>\`.
`);
static ERROR_UnparseableTransformTo = SevErrorTransform | 0x06;
static Error_UnparseableTransformTo = (o: {to: string, message: string}) => m(
this.ERROR_UnparseableTransformTo,
static Error_UnparseableTransformTo = (o: {to: string, message: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_UnparseableTransformTo, x,
`Invalid transform to="${def(o.to)}": "${def(o.message)}"`,
);
/**
* Get an offset from o and set e's offset field
* @param event a compiler event, such as from functions in this class
* @param x any object parsed from XML or with the XML_META_DATA_SYMBOL symbol copied over
* @returns modified event object
*/
static offset(event: CompilerEvent, x?: any): CompilerEvent {
if(x) {
const metadata = KeymanXMLReader.getMetaData(x) || {};
const offset = metadata?.startIndex;
if (offset) {
event.offset = offset;
}
const filename = event.filename || metadata[XML_FILENAME_SYMBOL];
if (filename) {
event.filename = filename;
}
}
return event;
}
}

View file

@ -35,13 +35,13 @@ export class LinterKeycaps extends Linter {
const disp = this.findDisp(id.value, to.value);
if (!disp) {
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_NoDisplayForSwitch({ id: id.value }));
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_NoDisplayForSwitch({ id: id.value }, key));
}
} else if (to.value !== '' && nonMarkerOutput === '') {
// has output, but only markers
const disp = this.findDisp(id.value, to.value);
if (!disp) {
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_NoDisplayForMarker({ id: id.value }));
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_NoDisplayForMarker({ id: id.value }, key));
}
}
}

View file

@ -71,7 +71,7 @@ export class LocaCompiler extends SectionCompiler {
// yet include `getCanonicalLocales` but node 16 does include it so we can
// safely use it. Also well supported in modern browsers.
const canonicalLocales = (Intl as any).getCanonicalLocales(locales) as string[];
result.locales = canonicalLocales.map(locale => sections.strs.allocString(locale));
result.locales = canonicalLocales.map(locale => sections.strs.allocString(locale, {x: this.contextForLocale(locale)}));
if(result.locales.length < locales.length) {
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_OneOrMoreRepeatedLocales(this.keyboard3?.locales));

View file

@ -53,12 +53,18 @@ export class MetaCompiler extends SectionCompiler {
public compile(sections: DependencySections): Meta {
const result = new Meta();
result.author = sections.strs.allocString(this.keyboard3.info?.author);
result.conform = sections.strs.allocString(this.keyboard3.conformsTo);
result.layout = sections.strs.allocString(this.keyboard3.info?.layout);
result.name = sections.strs.allocString(this.keyboard3.info?.name);
result.indicator = sections.strs.allocString(this.keyboard3.info?.indicator);
result.version = sections.strs.allocString(this.keyboard3.version?.number ?? "0.0.0");
result.author = sections.strs.allocString(this.keyboard3.info?.author,
{x: this.keyboard3.info});
result.conform = sections.strs.allocString(this.keyboard3.conformsTo,
{x: this.keyboard3});
result.layout = sections.strs.allocString(this.keyboard3.info?.layout,
{x: this.keyboard3.info});
result.name = sections.strs.allocString(this.keyboard3.info?.name,
{x: this.keyboard3.info});
result.indicator = sections.strs.allocString(this.keyboard3.info?.indicator,
{x: this.keyboard3.info});
result.version = sections.strs.allocString(this.keyboard3.version?.number ?? "0.0.0",
{x: this.keyboard3.version});
result.settings =
(this.keyboard3.settings?.normalization == "disabled" ? KeyboardSettings.normalizationDisabled : 0);
return result;

View file

@ -1,4 +1,5 @@
import { LdmlKeyboardTypes } from "@keymanapp/common-types";
import { ObjectWithMetadata } from "@keymanapp/developer-utils";
/**
* Verb for SubstitutionTracker.add()
@ -14,7 +15,7 @@ export enum SubstitutionUse {
variable,
}
type SubstitutionSet = Set<string>;
type SubstitutionSet = Map<string, ObjectWithMetadata>;
/** Tracks usage of markers */
export class SubstitutionTracker {
@ -28,10 +29,10 @@ export class SubstitutionTracker {
all: SubstitutionSet;
constructor() {
this.emitted = new Set<string>();
this.consumed = new Set<string>();
this.matched = new Set<string>();
this.all = new Set<string>();
this.emitted = new Map<string, ObjectWithMetadata>();
this.consumed = new Map<string, ObjectWithMetadata>();
this.matched = new Map<string, ObjectWithMetadata>();
this.all = new Map<string, ObjectWithMetadata>();
}
/**
@ -39,32 +40,33 @@ export class SubstitutionTracker {
* @param verb what kind of use we are adding
* @param markers list of substitutions to add
*/
add(verb: SubstitutionUse, markers: string[]) {
add(verb: SubstitutionUse, markers: string[], x?: ObjectWithMetadata) {
if (!markers.length) {
return; // skip if empty
}
x = x || {}; // need at least an empty object
if (verb == SubstitutionUse.emit) {
markers.forEach((m) => {
this.emitted.add(m);
this.all.add(m);
this.emitted.set(m, x);
this.all.set(m,x);
});
} else if (verb == SubstitutionUse.consume) {
markers.forEach((m) => {
this.consumed.add(m);
this.all.add(m);
this.consumed.set(m,x);
this.all.set(m,x);
});
} else if (verb == SubstitutionUse.match) {
markers.forEach((m) => {
this.matched.add(m);
this.all.add(m);
this.matched.set(m,x);
this.all.set(m,x);
});
} else if (verb == SubstitutionUse.variable) {
markers.forEach((m) => {
// we don't know, so add it to all three
this.matched.add(m);
this.emitted.add(m);
this.consumed.add(m);
this.all.add(m);
this.matched.set(m,x);
this.emitted.set(m,x);
this.consumed.set(m,x);
this.all.set(m,x);
});
/* c8 skip next 3 */
} else {
@ -75,27 +77,27 @@ export class SubstitutionTracker {
/** rollup of several substitution types */
export class Substitutions {
addSetAndStringSubtitution(verb: SubstitutionUse, str?: string) {
this.set.add(verb, LdmlKeyboardTypes.VariableParser.allSetReferences(str));
this.addStringAndMarkerSubstitution(verb, str);
addSetAndStringSubtitution(verb: SubstitutionUse, str?: string, x?: ObjectWithMetadata) {
this.set.add(verb, LdmlKeyboardTypes.VariableParser.allSetReferences(str), x);
this.addStringAndMarkerSubstitution(verb, str, x);
}
/** add a string that can have string var substitutions or markers */
addStringAndMarkerSubstitution(verb: SubstitutionUse, str?: string) {
this.addMarkers(verb, str);
this.addStringSubstitution(verb, str);
addStringAndMarkerSubstitution(verb: SubstitutionUse, str?: string, x?: ObjectWithMetadata) {
this.addMarkers(verb, str, x);
this.addStringSubstitution(verb, str, x);
}
addStringSubstitution(verb: SubstitutionUse, str?: string) {
this.string.add(verb, LdmlKeyboardTypes.VariableParser.allStringReferences(str));
addStringSubstitution(verb: SubstitutionUse, str?: string, x?: ObjectWithMetadata) {
this.string.add(verb, LdmlKeyboardTypes.VariableParser.allStringReferences(str), x);
}
/** add a string that's just markers */
addMarkers(verb: SubstitutionUse, str?: string) {
this.markers.add(verb, LdmlKeyboardTypes.MarkerParser.allReferences(str));
LdmlKeyboardTypes.MarkerParser.allBrokenReferences(str).forEach(m => this.badMarkers.add(m));
addMarkers(verb: SubstitutionUse, str?: string, x?: ObjectWithMetadata) {
this.markers.add(verb, LdmlKeyboardTypes.MarkerParser.allReferences(str), x);
LdmlKeyboardTypes.MarkerParser.allBrokenReferences(str).forEach(m => this.badMarkers.set(m, x));
}
// all valid markers
markers: SubstitutionTracker;
// all invalid markers
badMarkers: Set<string>;
badMarkers: SubstitutionSet;
// all valid set ids
set: SubstitutionTracker;
// all valid string ids
@ -108,6 +110,6 @@ export class Substitutions {
this.set = new SubstitutionTracker();
this.string = new SubstitutionTracker();
this.uset = new SubstitutionTracker();
this.badMarkers = new Set<string>();
this.badMarkers = new Map<string, ObjectWithMetadata>();
}
}

View file

@ -1,6 +1,6 @@
import { constants, SectionIdent } from "@keymanapp/ldml-keyboard-constants";
import { KMXPlus, LdmlKeyboardTypes, util } from '@keymanapp/common-types';
import { CompilerCallbacks, LDMLKeyboard } from "@keymanapp/developer-utils";
import { CompilerCallbacks, LDMLKeyboard, ObjectWithMetadata } from "@keymanapp/developer-utils";
import { SectionCompiler } from "./section-compiler.js";
import Bksp = KMXPlus.Bksp;
@ -18,6 +18,7 @@ import { verifyValidAndUnique } from "../util/util.js";
import { LdmlCompilerMessages } from "./ldml-compiler-messages.js";
import { Substitutions, SubstitutionUse } from "./substitution-tracker.js";
import { transform_from_parse, transform_to_parse } from "../util/abnf/abnf.js";
import { StrsOptions } from "../../../../../common/web/types/src/kmx/kmx-plus/kmx-plus.js";
type TransformCompilerType = 'simple' | 'backspace';
@ -26,22 +27,24 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
static validateSubstitutions(keyboard: LDMLKeyboard.LKKeyboard, st: Substitutions): boolean {
keyboard?.transforms?.forEach(transforms =>
transforms.transformGroup.forEach(transformGroup => {
transformGroup.transform?.forEach(({ to, from }) => {
st.addSetAndStringSubtitution(SubstitutionUse.consume, from);
st.addSetAndStringSubtitution(SubstitutionUse.emit, to);
transformGroup.transform?.forEach((transform) => {
const { to, from } = transform;
st.addSetAndStringSubtitution(SubstitutionUse.consume, from, transform);
st.addSetAndStringSubtitution(SubstitutionUse.emit, to, transform);
const mapFrom = LdmlKeyboardTypes.VariableParser.CAPTURE_SET_REFERENCE.exec(from);
const mapTo = LdmlKeyboardTypes.VariableParser.MAPPED_SET_REFERENCE.exec(to || '');
if (mapFrom) {
// add the 'from' as a match
st.set.add(SubstitutionUse.consume, [mapFrom[1]]);
st.set.add(SubstitutionUse.consume, [mapFrom[1]], transform);
}
if (mapTo) {
// add the 'from' as a match
st.set.add(SubstitutionUse.emit, [mapTo[1]]);
st.set.add(SubstitutionUse.emit, [mapTo[1]], transform);
}
});
transformGroup.reorder?.forEach(({ before }) => {
st.addStringSubstitution(SubstitutionUse.consume, before);
transformGroup.reorder?.forEach((reorder) => {
const { before } = reorder;
st.addStringSubstitution(SubstitutionUse.consume, before, reorder);
});
}));
return true;
@ -138,6 +141,8 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
}
private compileTransform(sections: DependencySections, transform: LKTransform) : TranTransform {
// we have lots of strings to allocate, that will all have these options
const stropts : StrsOptions = { x: transform };
const result = new TranTransform();
// setup for serializing
result._from = transform.from;
@ -146,7 +151,7 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
// check for incorrect \uXXXX escapes. Do this before substituting markers or sets.
// We run this first because it's more helpful than the ABNF.
cookedFrom = this.checkEscapes(cookedFrom); // check for \uXXXX escapes before normalizing
cookedFrom = this.checkEscapes(cookedFrom, transform); // check for \uXXXX escapes before normalizing
if (cookedFrom === '') {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_TransformFromMatchesNothing({ from: cookedFrom }, transform));
@ -157,11 +162,11 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
const mapFrom = LdmlKeyboardTypes.VariableParser.CAPTURE_SET_REFERENCE.exec(cookedFrom);
const mapTo = LdmlKeyboardTypes.VariableParser.MAPPED_SET_REFERENCE.exec(transform.to || '');
if (mapFrom && mapTo) { // TODO-LDML: error cases
result.mapFrom = sections.strs.allocString(mapFrom[1]); // var name
result.mapTo = sections.strs.allocString(mapTo[1]); // var name
result.mapFrom = sections.strs.allocString(mapFrom[1], stropts); // var name
result.mapTo = sections.strs.allocString(mapTo[1], stropts); // var name
} else {
result.mapFrom = sections.strs.allocString('');
result.mapTo = sections.strs.allocString('');
result.mapFrom = sections.strs.allocString('', stropts);
result.mapTo = sections.strs.allocString('', stropts);
// validate 'to' here
if (!this.isValidTo(transform.to || '')) {
@ -199,7 +204,7 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
transform_from_parse(transform.from);
}
} catch (e) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_UnparseableTransformFrom({ from: cookedFrom, message: e.toString() }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_UnparseableTransformFrom({ from: cookedFrom, message: e.toString() }, transform));
return null;
}
@ -207,13 +212,14 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
try {
transform_to_parse(transform.to || '');
} catch (e) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_UnparseableTransformTo({ to: transform.to || '', message: e.toString() }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_UnparseableTransformTo({ to: transform.to || '', message: e.toString() }, transform));
return null;
}
// cookedFrom is cooked above, since there's some special treatment
result.from = sections.strs.allocString(cookedFrom, {
unescape: false,
x: transform,
}, sections);
// 'to' is handled via allocString
result.to = sections.strs.allocString(transform.to, {
@ -221,6 +227,7 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
markers: true,
unescape: true,
nfd: true,
x: transform,
}, sections);
return result;
}
@ -230,9 +237,9 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
* We have already checked that it's not a mapTo,
* so there should not be any illegal substitutions.
*/
private isValidTo(to: string) : boolean {
private isValidTo(to: string, x?: ObjectWithMetadata) : boolean {
if (/(?<!\\)(?:\\\\)*\$\[/.test(to)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformToUset({ to }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformToUset({ to }, x));
return false;
}
return true;
@ -244,18 +251,18 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
* @param from the original from - for error reporting
* @returns true if OK
*/
private isValidRegex(cookedFrom: string, from: string) : boolean {
private isValidRegex(cookedFrom: string, from: string, x?: ObjectWithMetadata) : boolean {
// check for any unescaped dollar sign here
if (/(?<!\\)(?:\\\\)*\$/.test(cookedFrom)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformDollarsign({ from }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformDollarsign({ from }, x));
return false;
}
if (/(?<!\\)(?:\\\\)*\*/.test(cookedFrom)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformAsterisk({ from }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformAsterisk({ from }, x));
return false;
}
if (/(?<!\\)(?:\\\\)*\+/.test(cookedFrom)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformPlus({ from }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformPlus({ from }, x));
return false;
}
// Verify that the regex is syntactically valid
@ -265,14 +272,14 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
// does it match an empty string?
if (rg.test('')) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_TransformFromMatchesNothing({ from }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_TransformFromMatchesNothing({ from }, x));
return false;
}
} catch (e) {
// We're exposing the internal regex error message here.
// In the future, CLDR plans to expose the EBNF for the transform,
// at which point we would have more precise validation prior to getting to this point.
this.callbacks.reportMessage(LdmlCompilerMessages.Error_UnparseableTransformFrom({ from, message: e.message }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_UnparseableTransformFrom({ from, message: e.message }, x));
return false;
}
return true;
@ -294,14 +301,14 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
result._before = reorder.before;
result._from = reorder.from;
result._order = reorder.order;
if (reorder.from && this.checkEscapes(reorder.from) === null) {
if (reorder.from && this.checkEscapes(reorder.from, reorder) === null) {
return null; // error'ed
}
if (reorder.before && this.checkEscapes(reorder.before) === null) {
if (reorder.before && this.checkEscapes(reorder.before, reorder) === null) {
return null; // error'ed
}
result.elements = sections.elem.allocElementString(sections, reorder.from, reorder.order, reorder.tertiary, reorder.tertiaryBase, reorder.preBase);
result.before = sections.elem.allocElementString(sections, reorder.before);
result.elements = sections.elem.allocElementString(sections, {x:reorder}, reorder.from, reorder.order, reorder.tertiary, reorder.tertiaryBase, reorder.preBase);
result.before = sections.elem.allocElementString(sections, {x:reorder}, reorder.before);
if (!result.elements || !result.before) {
return null; // already error'ed
} else {
@ -337,16 +344,21 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
* @param cookedFrom the original string
* @returns the original string, or null if an error was reported
*/
private checkEscapes(cookedFrom: string, transform?: LKTransform): string | null {
private checkEscapes(cookedFrom: string, transform?: ObjectWithMetadata): string | null {
if (!cookedFrom) return cookedFrom;
// should not follow marker prefix, nor marker prefix with range
const anyQuad = /(?<!\\uffff\\u0008(?:\[[0-9a-fA-F\\u-]*)?)\\u([0-9a-fA-F]{4})/g;
for (const [, sub] of cookedFrom.matchAll(anyQuad)) {
for (const [matched, sub] of cookedFrom.matchAll(anyQuad)) {
const s = util.unescapeOne(sub);
if (s !== '\uffff' && s !== '\u0008') { // markers
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidQuadEscape({ cp: s.codePointAt(0) }, transform));
const codepoint: number = s.codePointAt(0);
const cp: string = matched; // the original match from the file
const recommended: string = `\\u{${codepoint.toString(16)}}`;
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidQuadEscape({
cp, recommended
}, transform));
return null; // exit on the first error
}
}

View file

@ -1,6 +1,6 @@
import { SectionIdent, constants } from "@keymanapp/ldml-keyboard-constants";
import { KMXPlus, LdmlKeyboardTypes } from '@keymanapp/common-types';
import { LDMLKeyboard, CompilerCallbacks } from '@keymanapp/developer-utils';
import { LDMLKeyboard, CompilerCallbacks, ObjectWithMetadata } from '@keymanapp/developer-utils';
import { SectionCompiler } from "./section-compiler.js";
import Vars = KMXPlus.Vars;
import StringVarItem = KMXPlus.StringVarItem;
@ -40,9 +40,9 @@ export class VarsCompiler extends SectionCompiler {
return valid;
}
private validateIdentifier(id: string) {
private validateIdentifier(id: string, x?: ObjectWithMetadata) {
if(!id.match(LdmlKeyboardTypes.VariableParser.ID)) { // From <string> DTD
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidVariableIdentifier({id}));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidVariableIdentifier({id}, x));
return false;
}
return true;
@ -77,7 +77,7 @@ export class VarsCompiler extends SectionCompiler {
// Strings
for (const e of variables.string) {
const { id, value } = e;
if(!this.validateIdentifier(id)) {
if(!this.validateIdentifier(id, e)) {
valid = false;
continue;
}
@ -90,9 +90,9 @@ export class VarsCompiler extends SectionCompiler {
allStrings.add(ref); // avoids multiple reports of same missing variable
}
}
st.string.add(SubstitutionUse.variable, stringrefs);
st.string.add(SubstitutionUse.variable, stringrefs, e);
allStrings.add(id);
st.addMarkers(SubstitutionUse.variable, value);
st.addMarkers(SubstitutionUse.variable, value, e);
}
// Sets
for (const e of variables.set) {
@ -105,7 +105,7 @@ export class VarsCompiler extends SectionCompiler {
allSets.add(id);
// check for illegal references, here.
const stringrefs = LdmlKeyboardTypes.VariableParser.allStringReferences(value);
st.string.add(SubstitutionUse.variable, stringrefs);
st.string.add(SubstitutionUse.variable, stringrefs, e);
// Now split into spaces.
const items: string[] = LdmlKeyboardTypes.VariableParser.setSplitter(value);
@ -116,7 +116,7 @@ export class VarsCompiler extends SectionCompiler {
valid = false;
this.callbacks.reportMessage(LdmlCompilerMessages.Error_NeedSpacesBetweenSetVariables({ item }, e));
} else {
st.set.add(SubstitutionUse.variable, setrefs); // the reference to a 'map'
st.set.add(SubstitutionUse.variable, setrefs, e); // the reference to a 'map'
}
// TODO-LDML: Are there other illegal cases here? what about "x$[set]"?
}
@ -131,7 +131,7 @@ export class VarsCompiler extends SectionCompiler {
addId(id, e);
allUnicodeSets.add(id);
const stringrefs = LdmlKeyboardTypes.VariableParser.allStringReferences(value);
st.string.add(SubstitutionUse.variable, stringrefs);
st.string.add(SubstitutionUse.variable, stringrefs, e);
const setrefs = LdmlKeyboardTypes.VariableParser.allSetReferences(value);
for (const id2 of setrefs) {
if (!allUnicodeSets.has(id2)) {
@ -140,7 +140,7 @@ export class VarsCompiler extends SectionCompiler {
// $[set] in a UnicodeSet must be another UnicodeSet.
this.callbacks.reportMessage(LdmlCompilerMessages.Error_CantReferenceSetFromUnicodeSet({ id: id2 }, e));
} else {
st.uset.add(SubstitutionUse.variable, [id2]);
st.uset.add(SubstitutionUse.variable, [id2], e);
}
}
}
@ -157,24 +157,24 @@ export class VarsCompiler extends SectionCompiler {
}
// check for any missing vars
for (const id of st.set.all) {
for (const [id,x] of st.set.all.entries()) {
// note: we check the uset list also. we don't know until later which was
// intended. collisions are handled separately.
if (!allSets.has(id) && !allUnicodeSets.has(id)) {
valid = false;
this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingSetVariable({ id }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingSetVariable({ id }, x));
}
}
for (const id of st.string.all) {
for (const [id,x] of st.string.all.entries()) {
if (!allStrings.has(id)) {
valid = false;
this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingStringVariable({ id }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingStringVariable({ id }, x));
}
}
for (const id of st.uset.all) {
for (const [id,x] of st.uset.all.entries()) {
if (!allUnicodeSets.has(id)) {
valid = false;
this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingUnicodeSetVariable({ id }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingUnicodeSetVariable({ id }, x));
}
}
@ -198,13 +198,13 @@ export class VarsCompiler extends SectionCompiler {
// see if there are any matched-but-not-emitted markers
const matchedNotEmitted : Set<string> = new Set<string>();
const mt = st.markers;
for (const m of mt.matched.values()) {
for (const m of mt.matched.keys()) {
if (m === LdmlKeyboardTypes.MarkerParser.ANY_MARKER_ID) continue; // match-all marker
if (!mt.emitted.has(m)) {
matchedNotEmitted.add(m);
}
}
for (const m of mt.consumed.values()) {
for (const m of mt.consumed.keys()) {
if (m === LdmlKeyboardTypes.MarkerParser.ANY_MARKER_ID) continue; // match-all marker
if (!mt.emitted.has(m)) {
matchedNotEmitted.add(m);
@ -213,7 +213,9 @@ export class VarsCompiler extends SectionCompiler {
// report once
if (matchedNotEmitted.size > 0) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingMarkers({ ids: Array.from(matchedNotEmitted.values()).sort() }));
const ids = Array.from(matchedNotEmitted.values()).sort();
const x = mt.all.get(ids[0]); // get the FIRST object for context
this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingMarkers({ ids: ids.join(',')}, x));
valid = false;
}
@ -222,19 +224,24 @@ export class VarsCompiler extends SectionCompiler {
if (!!st.badMarkers.size) {
valid = false;
st.badMarkers.forEach(id =>
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidMarkerIdentifier({ id })));
for (const [id, x] of st.badMarkers.entries()) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidMarkerIdentifier({ id }, x));
}
}
return valid;
}
validateSubstitutions(keyboard: LDMLKeyboard.LKKeyboard, st : Substitutions) : boolean {
keyboard?.variables?.string?.forEach(({value}) =>
st.markers.add(SubstitutionUse.variable, LdmlKeyboardTypes.MarkerParser.allReferences(value)));
keyboard?.variables?.string?.forEach((string) => {
const {value} = string;
st.markers.add(SubstitutionUse.variable, LdmlKeyboardTypes.MarkerParser.allReferences(value), string);
});
// get markers mentioned in a set
keyboard?.variables?.set?.forEach(({ value }) =>
LdmlKeyboardTypes.VariableParser.setSplitter(value).forEach(v => st.markers.add(SubstitutionUse.match, LdmlKeyboardTypes.MarkerParser.allReferences(v))));
keyboard?.variables?.set?.forEach((set) => {
const { value } = set;
LdmlKeyboardTypes.VariableParser.setSplitter(value).forEach(v => st.markers.add(SubstitutionUse.match, LdmlKeyboardTypes.MarkerParser.allReferences(v), set));
});
return true;
}
@ -260,8 +267,16 @@ export class VarsCompiler extends SectionCompiler {
const mt = st.markers;
// collect all markers, excluding the match-all
const allMarkers : string[] = Array.from(mt.all).filter(m => m !== LdmlKeyboardTypes.MarkerParser.ANY_MARKER_ID).sort();
result.markers = sections.list.allocList(allMarkers, {}, sections);
const allMarkers : string[] = Array.from(mt.all.keys()).filter(m => m !== LdmlKeyboardTypes.MarkerParser.ANY_MARKER_ID).sort();
result.markers = sections.list.allocList(allMarkers, {
// pass the first object in the listr
x: mt.all.get(allMarkers[0]||'')
// however, this is the string with the marker *id*. So, from the StrsCompiler
// point of view, the value of this context would be if the marker ID had, say,
// invalid Unicode in it for ERROR_IllegalCharacters - but, that wouldn't be a
// valid Marker ID either.
// So it's not likely that this context will be used, but it's available.
}, sections);
// sets need to be added late, because they can refer to markers
variables?.set?.forEach((e) =>

View file

@ -1,12 +1,13 @@
import 'mocha';
import {assert} from 'chai';
import hextobin from '@keymanapp/hextobin';
import {compileKeyboard, compilerTestCallbacks, compilerTestOptions, makePathToFixture} from './helpers/index.js';
import {compileKeyboard, compilerTestCallbacks, compilerTestOptions, makePathToFixture, scrubContextFromMessages} from './helpers/index.js';
import { compareXml } from './helpers/compareXml.js';
import { LdmlKeyboardCompiler } from '../src/compiler/compiler.js';
import { kmxToXml } from '../src/util/serialize.js';
import { writeFileSync } from 'node:fs';
import { LdmlCompilerMessages } from '../src/main.js';
import { util } from '@keymanapp/common-types';
/** Overall compiler tests */
describe('compiler-tests', function() {
@ -53,11 +54,11 @@ describe('compiler-tests', function() {
const runOutput = await k.run(inputFilename, "invalid-illegal.kmx"); // need the exact name passed to build-fixtures
assert.isNull(runOutput, "Expect invalid-illegal to fail to run()");
assert.sameDeepMembers(compilerTestCallbacks.messages, [
assert.sameDeepMembers( scrubContextFromMessages(compilerTestCallbacks.messages), [
// copied from strs.tests.ts
// validation messages
LdmlCompilerMessages.Error_IllegalCharacters({ count: 5, lowestCh: 0xFDD0 }),
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }),
LdmlCompilerMessages.Error_IllegalCharacters({ count: 5, lowestCh: util.describeCodepoint(0xFDD0) }),
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: util.describeCodepoint(0xE010) }),
]);
});

View file

@ -67,25 +67,25 @@ describe('disp', function () {
const disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-dupto.xml', compilerTestCallbacks) as Disp;
assert.isNull(disp);
assert.equal(compilerTestCallbacks.messages.length, 1);
assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayIsRepeated({ output: 'e' }, withOffset(330)));
assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayIsRepeated({ display: '(e)' }, withOffset(330)));
});
it('should reject duplicate ids', async function() {
const disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-dupid.xml', compilerTestCallbacks) as Disp;1
assert.isNull(disp);
assert.equal(compilerTestCallbacks.messages.length, 1);
assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayIsRepeated({ keyId: 'e' }, withOffset(329)));
assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayIsRepeated({ display: '(e)' }, withOffset(329)));
});
it('should reject if neither to nor id', async function() {
const disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-none.xml', compilerTestCallbacks) as Disp;
assert.isNull(disp);
assert.equal(compilerTestCallbacks.messages.length, 1);
assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayNeedsToOrId({}, withOffset(182)));
assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayNeedsToOrId({ display: '(f)'}, withOffset(182)));
});
it('should reject if both to and id', async function() {
const disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-both.xml', compilerTestCallbacks) as Disp;
assert.isNull(disp);
assert.equal(compilerTestCallbacks.messages.length, 1);
assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayNeedsToOrId({ output: 'e', keyId: 'e' }, withOffset(182)));
assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayNeedsToOrId({ display: '(e)' }, withOffset(182)));
});
testCompilationCases(DispCompiler, [
{

View file

@ -144,7 +144,7 @@ export async function compileKeyboard(inputFilename: string, options: LdmlCompil
const valid = await k.validate(source);
if (validateMessages) {
assert.sameDeepMembers(compilerTestCallbacks.messages, validateMessages, "validation messages mismatch");
assert.sameDeepMembers(scrubContextFromMessages(compilerTestCallbacks.messages), validateMessages, "validation messages mismatch");
assert.notEqual(valid, expectFailValidate, 'validation failure');
} else {
checkMessages();
@ -155,7 +155,7 @@ export async function compileKeyboard(inputFilename: string, options: LdmlCompil
const kmx = await k.compile(source);
if (compileMessages) {
assert.sameDeepMembers(compilerTestCallbacks.messages, compileMessages, "compiler messages mismatch");
assert.sameDeepMembers(scrubContextFromMessages(compilerTestCallbacks.messages), compileMessages, "compiler messages mismatch");
} else {
checkMessages();
}
@ -259,6 +259,26 @@ export interface CompilationCase {
retainOffsetInMessages?: boolean;
}
/**
* Scrub 'context' from messages. to simplify unit tests
* @param messages input array of messages
* @returns copy of messages
*/
export function scrubContextFromMessages(messages: CompilerEvent[]): CompilerEvent[] {
return messages.map(m => {
const scrubbed = Object.assign({}, m);
// Turn this on once all messages have offsets, see messages.tests.ts
// if (!scrubbed.offset) {
// throw Error(`Error, no offset detected in message ${CompilerError.formatEvent(m)}`);
// }
delete scrubbed.offset;
delete scrubbed.line;
delete scrubbed.filename;
delete scrubbed.column;
return scrubbed;
});
}
/**
* Run a bunch of cases
* @param cases cases to run
@ -283,15 +303,7 @@ export function testCompilationCases(compiler: SectionCompilerNew, cases : Compi
let messagesToCheck = callbacks.messages;
// scrub offsets from messages to reduce churn in the test casws
if (!testcase.retainOffsetInMessages && callbacks.messages) {
messagesToCheck = callbacks.messages.map(m => {
const scrubbed = Object.assign({}, m);
// Turn this on once all messages have offsets, see messages.tests.ts
// if (!scrubbed.offset) {
// throw Error(`Error, no offset detected in message ${CompilerError.formatEvent(m)}`);
// }
delete scrubbed.offset;
return scrubbed;
});
messagesToCheck = scrubContextFromMessages(callbacks.messages);
}
const testcaseErrors = matchCompilerEventsOrBoolean(messagesToCheck, testcase.errors);
const testcaseWarnings = matchCompilerEvents(messagesToCheck, testcase.warnings);

View file

@ -312,7 +312,7 @@ describe('keys.kmap', function () {
{
subpath: 'sections/keys/invalid-bad-modifier.xml',
errors: [
LdmlCompilerMessages.Error_InvalidModifier({id:'base',modifiers:'altR-shift'}),
LdmlCompilerMessages.Error_InvalidModifier({ modifiers: 'altR-shift' }),
]
},
{

View file

@ -96,8 +96,7 @@ describe('layr', function () {
subpath: 'sections/keys/invalid-bad-modifier.xml',
errors: [
LdmlCompilerMessages.Error_InvalidModifier({
id: 'base',
modifiers: 'altR-shift'
modifiers: 'altR-shift',
}),
],
},
@ -139,7 +138,7 @@ describe('layr', function () {
{
subpath: 'sections/layr/error-bogus-modifiers.xml',
errors: [
LdmlCompilerMessages.Error_InvalidModifier({ id: '', modifiers: 'caps bogus'}),
LdmlCompilerMessages.Error_InvalidModifier({ modifiers: 'caps bogus' }),
]
},
{

View file

@ -15,18 +15,20 @@ describe('LdmlCompilerMessages', function () {
/** all fns */
let total = 0;
/** does not take line numbers */
let noLines = 0;
const noLines = new Set<string>();
/** takes line numbers */
let lines = 0;
const fakeOffsetNumber = 1234;
const fakeOffsetObject = withOffset(fakeOffsetNumber);
for(const key of keys) {
if(typeof m[key] == 'function') {
// exclude this one, does not need line numbers
if (key == 'Error_InvalidFile') continue;
if (typeof m[key] == 'function') {
total++;
const f = m[key] as Function;
// console.log(`${f.name}: ${f.length}`);
if (f.length === 0) { // Error_foo()
noLines++;
noLines.add(key);
continue;
}
// now try to call it
@ -42,16 +44,30 @@ describe('LdmlCompilerMessages', function () {
expect(resp.offset).to.equal(fakeOffsetNumber, `Offset number round trip for error ${f.name} did not work, check the message function`);
} else {
// did not get a column number back
noLines++;
noLines.add(key);
}
}
}
expect(lines).to.not.be.equal(0, `None of ${total} messages had offset reporting.`);
if (noLines > 0) {
// Once this goes to zero, make it an error if it goes up!
// Oh, and while you're here, once this is zero, uncomment the code in testCompilationCases
// that asserts that all messages are actually generated with an offset.
console.warn(`TODO-LDML (#10622) ${noLines}/${total} ${Number((noLines/total)*1000).toFixed(0)}‰ of message(s) did not have detectable offset (line number) reporting.`);
expect(Array.from(noLines.values())).to.deep.equal([], `${noLines.size}/${total} ${Number((noLines.size/total)*100).toFixed(0)}% of message(s) did not have detectable offset (line number) reporting`);
});
it('Should only have simple ${def(o...)} interpolation', () => {
const m = LdmlCompilerMessages as Record<string,any>;
const keys = Object.keys(LdmlCompilerMessages);
const complexInterpolation : string[] = [];
for(const key of keys) {
// exclude this one, does not need line numbers
if (key == 'Error_InvalidFile') continue;
if (typeof m[key] == 'function') {
const f = m[key] as Function;
const s = f.toString();
// scan for interpolation
// Interpolation.. A non-escaped ${ that's NOT followed by `def(o.` triggers this error.
if (/.*`.*(?<!\\)(?:\\\\)*\$\{(?!def\(o\.)/.test(s)) {
complexInterpolation.push(key);
}
}
}
expect(complexInterpolation).to.deep.equal([], 'Complex interpolation found, please only use the ${def(o... form')
});
});

View file

@ -2,6 +2,7 @@ import 'mocha';
import { assert } from 'chai';
import { compileKeyboard, compilerTestCallbacks, compilerTestOptions, makePathToFixture } from './helpers/index.js';
import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js';
import { util } from '@keymanapp/common-types';
/** strs tests */
@ -17,8 +18,8 @@ describe('strs', function () {
const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false },
[
// validation messages
LdmlCompilerMessages.Error_IllegalCharacters({ count: 5, lowestCh: 0xFDD0 }),
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }),
LdmlCompilerMessages.Error_IllegalCharacters({ count: 5, lowestCh: util.describeCodepoint(0xFDD0) }),
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: util.describeCodepoint(0xE010) }),
],
true, // validation should fail
[
@ -32,12 +33,12 @@ describe('strs', function () {
const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false },
[
// validation messages
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }),
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: util.describeCodepoint(0xE010) }),
],
false, // validation should pass
[
// same messages
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }),
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: util.describeCodepoint(0xE010) }),
]);
assert.isNotNull(kmx);
});
@ -47,14 +48,14 @@ describe('strs', function () {
const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false },
[
// validation messages
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }),
LdmlCompilerMessages.Warn_UnassignedCharacters({ count: 1, lowestCh: 0x0CFFFD }),
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: util.describeCodepoint(0xE010) }),
LdmlCompilerMessages.Warn_UnassignedCharacters({ count: 1, lowestCh: util.describeCodepoint(0x0CFFFD) }),
],
false, // validation should pass
[
// same messages
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }),
LdmlCompilerMessages.Warn_UnassignedCharacters({ count: 1, lowestCh: 0x0CFFFD }),
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: util.describeCodepoint(0xE010) }),
LdmlCompilerMessages.Warn_UnassignedCharacters({ count: 1, lowestCh: util.describeCodepoint(0x0CFFFD) }),
]);
assert.isNotNull(kmx);
});
@ -63,7 +64,8 @@ describe('strs', function () {
const inputFilename = makePathToFixture('sections/tran/fail-bad-tran-2.xml');
const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false },
[
LdmlCompilerMessages.Error_InvalidQuadEscape({ cp: 295 }),
// \uXXXX is not allowed in this context
LdmlCompilerMessages.Error_InvalidQuadEscape({ cp: "\\u0127", recommended: "\\u{127}" }),
],
true, // validation should fail
[

View file

@ -273,13 +273,14 @@ describe('tran', function () {
{
subpath: 'sections/tran/fail-bad-reorder-2.xml',
errors: [
LdmlCompilerMessages.Error_InvalidQuadEscape({ cp: 0x1a6b }),
// 'cp' has exact casing from original
LdmlCompilerMessages.Error_InvalidQuadEscape({ cp: "\\u1A6B", recommended: "\\u{1a6b}" }),
],
},
{
subpath: 'sections/tran/fail-bad-reorder-3.xml',
errors: [
LdmlCompilerMessages.Error_InvalidQuadEscape({ cp: 0x1a60 }),
LdmlCompilerMessages.Error_InvalidQuadEscape({ cp: "\\u1A60", recommended: "\\u{1a60}" }),
],
},
// error due to bad regex
@ -295,7 +296,7 @@ describe('tran', function () {
// also used in test-compiler-e2e.ts
subpath: `sections/tran/fail-bad-tran-2.xml`,
errors: [
LdmlCompilerMessages.Error_InvalidQuadEscape({ cp: 295 }),
LdmlCompilerMessages.Error_InvalidQuadEscape({ cp: "\\u0127", recommended: "\\u{127}" }),
],
},
{
@ -466,7 +467,7 @@ describe('bksp', function () {
'doesnt_exist_1',
'doesnt_exist_2',
'doesnt_exist_3',
]
].join(',')
}),
],
},

View file

@ -238,7 +238,7 @@ describe('vars', function () {
'doesnt_exist_1',
'doesnt_exist_2',
'doesnt_exist_3',
]
].join(',')
}),
],
},

View file

@ -5,17 +5,20 @@ import { expectedGitDateFormat, getLastGitCommitDate } from '../src/util/getLast
describe('getLastGitCommitDate', function () {
it('should return a valid date for a folder in this repo', async function() {
this.timeout(5000); // getLastGitCommitDate depends on git which can sometimes take longer
const path = makePathToFixture('.');
const date = getLastGitCommitDate(path);
assert.match(date, expectedGitDateFormat);
});
it('should return null for a folder outside the repo', async function() {
this.timeout(5000); // getLastGitCommitDate depends on git which can sometimes take longer
const date = getLastGitCommitDate('/');
assert.isNull(date);
});
it('should return a valid date for a specific file in the repo', async function() {
this.timeout(5000); // getLastGitCommitDate depends on git which can sometimes take longer
const path = makePathToFixture('get-last-git-commit-date/README.md');
const date = getLastGitCommitDate(path);
// The expected date was manually extracted using the following command, with msec appended:

View file

@ -13,7 +13,7 @@
"@sentry/node": "^7.57.0",
"chalk": "^4.1.2",
"express": "^4.20.0",
"multer": "^2.0.0",
"multer": "^2.0.1",
"ngrok": "^5.0.0-beta.2",
"open": "^8.4.0",
"restructure": "^3.0.1",

View file

@ -6,6 +6,7 @@ On Linux, you can build the following projects:
- [Keyman Core](#keyman-core) (aka core)
- [Keyman for Linux](#keyman-for-linux)
- [Keyman Developer (node modules only)](#keyman-developer-node-modules-only)
- [Keyman Web](#keyman-web)
- [Keyman for Android](#keyman-for-android)
<!-- TODO: document how to build for Common/Web on Linux.
@ -15,7 +16,7 @@ On Linux, you can build the following projects:
The following projects **cannot** be built on Linux:
- Keyman for Windows
- Keyman Developer
- Keyman Developer (Windows components - IDE, setup, Server - has Windows components currently)
- Keyman for macOS
- Keyman for iOS
@ -47,7 +48,7 @@ They are most easily installed with the `mk-build-deps` tool:
```bash
sudo apt update
sudo apt install devscripts equivs
sudo apt install devscripts equivs jq
sudo mk-build-deps --install linux/debian/control
```
@ -126,6 +127,15 @@ Keyman for Linux can be built with the `linux/build.sh` script.
- [Building Keyman for Linux](../../linux/README.md)
## Keyman Developer (node modules only)
All dependencies are already installed if you followed the instructions
under [Prerequisites](#prerequisites).
### Building Keyman Developer
The node modules for Keyman Developer can be built with the `developer/build.sh` script.
## Keyman Web
Most dependencies are already installed if you followed the instructions under

View file

@ -1,3 +1,9 @@
keyman (18.0.238-1) unstable; urgency=medium
* New upstream release.
-- Eberhard Beilharz <eb1@sil.org> Wed, 04 Jun 2025 19:14:08 +0200
keyman (18.0.236-1) unstable; urgency=medium
* New upstream release.

View file

@ -15,12 +15,14 @@ sudo apt-get upgrade
sudo apt-get install keyman onboard-keyman
```
An older version of Keyman for Linux packages is also available in any current Debian at:
A possibly older version of Keyman for Linux packages is also available in
any current Debian at:
[https://packages.debian.org/keyman](https://packages.debian.org/keyman)
**Note** in order to fully work you'll need a patched version of ibus
which is available in the above launchpad repo. Patched versions of ibus
will have the string `sil` in the package version number.
**Note** Versions before Keyman 18 required a patched version of ibus in order
to fully work. Patched versions of ibus were available in the above mentioned
launchpad repository and had the string `sil` in the package version number.
Keyman 18+ no longer requires a patched version of ibus.
## Q. How do I install a Keyman keyboard?

View file

@ -1,5 +1,5 @@
#!/bin/bash
PYTHONPATH=.:${PYTHONPATH}
export PYTHONPATH=.:${PYTHONPATH}
XDG_CONFIG_HOME=$(mktemp --directory)
export XDG_CONFIG_HOME
@ -13,6 +13,8 @@ if [[ "$1" == "--coverage" ]]; then
fi
if [[ -n "${TEAMCITY_GIT_PATH}" ]]; then
PYTHONPATH=$(dirname "$0")/../tools:${PYTHONPATH}
if ! pip3 list --format=columns | grep -q teamcity-messages; then
if [[ -n "${TEAMCITY_PLATFORM}" ]] || [[ -n "${DOCKER_RUNNING}" ]]; then
# Ubuntu 24.04+ prevents mixing pip and system packages and wants us
@ -29,7 +31,9 @@ if [[ -n "${TEAMCITY_GIT_PATH}" ]]; then
# shellcheck disable=SC2086
pip3 install --user ${PIP_ARGS:-} teamcity-messages
fi
test_module=teamcity.unittestpy
test_module=teamcity_pytestrunner.unittestpy
echo "##teamcity[testStarted name='|[keyman-config|] Running unit tests']"
echo "##teamcity[flowStarted flowId='unit_tests']"
else
test_module=unittest
extra_opts=-v
@ -38,4 +42,8 @@ fi
# shellcheck disable=SC2086
python3 ${coverage:-} -m "${test_module:-}" discover ${extra_opts:-} -s tests/ -p "*_tests.py"
if [[ -n "${TEAMCITY_GIT_PATH}" ]]; then
echo "##teamcity[flowFinished flowId='unit_tests']"
echo "##teamcity[testFinished name='|[keyman-config|] Finished running unit tests']"
fi
rm -rf "${XDG_CONFIG_HOME}"

View file

@ -0,0 +1,12 @@
# Teamcity Python Test Runner
This implements an improved version of JetBrains' python unittest runner for
Teamcity. It makes the output of the tests indented and collapsible under
the test block by specifying a parent flow.
Requires the `teamcity-messages` package.
## References
- <https://www.jetbrains.com/help/teamcity/service-messages.html#Message+FlowId>
- <https://github.com/JetBrains/teamcity-messages>

View file

@ -0,0 +1,61 @@
import sys
from teamcity.messages import TeamcityServiceMessages
from teamcity.unittestpy import TeamcityTestResult, TeamcityTestRunner
from unittest import main
class KeymanTeamcityServiceMessages(TeamcityServiceMessages):
def __init__(self, parent_flow='unit_tests'):
self.parent_flow = parent_flow
super().__init__()
def testStarted(self, testName, captureStandardOutput=None, flowId=None, metainfo=None):
self.message('testStarted', name=testName, metainfo=metainfo)
self.message('flowStarted', flowId=flowId, parent=self.parent_flow)
def testFinished(self, testName, testDuration=None, flowId=None):
self.message('flowFinished', flowId=flowId, parent=self.parent_flow)
if testDuration is not None:
duration_ms = testDuration.days * 86400000 + \
testDuration.seconds * 1000 + \
int(testDuration.microseconds / 1000)
self.message('testFinished', name=testName, duration=str(duration_ms))
else:
self.message('testFinished', name=testName)
def testIgnored(self, testName, message='', flowId=None):
self.message('flowFinished', flowId=flowId, parent=self.parent_flow)
self.message('testIgnored', name=testName, message=message)
def testFailed(self, testName, message='', details='', flowId=None, comparison_failure=None):
self.message('flowFinished', flowId=flowId, parent=self.parent_flow)
if not comparison_failure:
self.message('testFailed', name=testName, message=message, details=details)
else:
diff_message = u"\n{0} != {1}\n".format(comparison_failure.actual, comparison_failure.expected)
self.message(
'testFailed',
name=testName,
message=str(message) + diff_message,
details=details,
type="comparisonFailure",
actual=comparison_failure.actual,
expected=comparison_failure.expected,
)
class KeymanTestResult(TeamcityTestResult):
def __init__(self, stream=sys.stdout, descriptions=None, verbosity=None):
super().__init__(stream, descriptions, verbosity)
self.messages = KeymanTeamcityServiceMessages()
class KeymanTestRunner(TeamcityTestRunner):
resultclass = KeymanTestResult
def run(self, test):
return super().run(test)
if __name__ == '__main__':
main(module=None, testRunner=KeymanTestRunner())

View file

@ -69,16 +69,12 @@ PREPRELEASE=false
UPDATE_VERSION_IN_PLIST=true
DO_CODESIGN=true
CODESIGNING_SUPPRESSION="CODE_SIGN_IDENTITY=\"\" CODE_SIGNING_REQUIRED=NO"
QUIET=true
QUIET=false
if builder_verbose; then
BUILD_OPTIONS=""
QUIET_FLAG=
QUIET=false
else
BUILD_OPTIONS="-quiet"
QUIET_FLAG="-quiet"
fi
BUILD_ACTIONS="build"
@ -290,7 +286,7 @@ do_publish() {
builder_heading "Preparing files for release deployment..."
./setup/build.sh
"$KM4MIM_BASE_PATH/make-km-dmg.sh" $QUIET_FLAG
"$KM4MIM_BASE_PATH/make-km-dmg.sh"
# Create download info
"$KM4MIM_BASE_PATH/write-download_info.sh"

48
package-lock.json generated
View file

@ -1153,7 +1153,7 @@
"@sentry/node": "^7.57.0",
"chalk": "^4.1.2",
"express": "^4.20.0",
"multer": "^2.0.0",
"multer": "^2.0.1",
"ngrok": "^5.0.0-beta.2",
"open": "^8.4.0",
"restructure": "^3.0.1",
@ -5748,20 +5748,34 @@
"license": "MIT"
},
"node_modules/concat-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"engines": [
"node >= 0.8"
"node >= 6.0"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^2.2.2",
"readable-stream": "^3.0.2",
"typedarray": "^0.0.6"
}
},
"node_modules/concat-stream/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"license": "MIT",
@ -10479,18 +10493,18 @@
"license": "MIT"
},
"node_modules/multer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.0.0.tgz",
"integrity": "sha512-bS8rPZurbAuHGAnApbM9d4h1wSoYqrOqkE+6a64KLMK9yWU7gJXBDDVklKQ3TPi9DRb85cRs6yXaC0+cjxRtRg==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.0.1.tgz",
"integrity": "sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.0.0",
"concat-stream": "^1.5.2",
"mkdirp": "^0.5.4",
"busboy": "^1.6.0",
"concat-stream": "^2.0.0",
"mkdirp": "^0.5.6",
"object-assign": "^4.1.1",
"type-is": "^1.6.4",
"xtend": "^4.0.0"
"type-is": "^1.6.18",
"xtend": "^4.0.2"
},
"engines": {
"node": ">= 10.16.0"
@ -12738,9 +12752,9 @@
}
},
"node_modules/tar-fs": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz",
"integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz",
"integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==",
"dev": true,
"license": "MIT",
"dependencies": {

View file

@ -166,9 +166,10 @@ while IFS= read -r line; do
# Which platform are we watching?
eval watch='$'watch_$platform
# Add common patterns to the watch list
watch="^(${platform}|(oem/[^/]+/${platform})|resources/((?!teamcity)|teamcity/(${platform}|includes))|$watch)"
# Since bash doesn't support negative look-aheads we use grep to test
if echo "${line}" | grep --quiet --perl-regexp "${watch}"; then
watch="^(${platform}|(oem/[^/]+/${platform})|resources/((?!teamcity)|teamcity/(${platform}|includes))|${watch})"
# Since bash doesn't support negative look-aheads we use perl to test
# (grep has a --perl-regexp option, but not the version on macOS)
if perl -e 'exit($ARGV[0] =~ /$ARGV[1]/ ? 0 : 1)' "${line}" "${watch}"; then
build_platforms+=($platform)
fi
fi

View file

@ -35,6 +35,12 @@ function add_zip_files() {
SEVENZ_FLAGS+=($1)
shift
;;
-x@*)
# Filename for a file containing list of files to exclude from the archive - Identical flag to zip and 7z
ZIP_FLAGS+=($1)
SEVENZ_FLAGS+=($1)
shift
;;
# Zip flags that have a corresponding 7z flag
-q)
@ -50,7 +56,7 @@ function add_zip_files() {
# -1 indicates low compression (fastest)
# -9 indicates ultra compression (slowest)
ZIP_FLAGS+=($1)
if [[ $1 =~ -([0-9]) ]] then
if [[ $1 =~ -([0-9]) ]]; then
SEVENZ_FLAGS+=("-mx${BASH_REMATCH[1]}")
fi
shift;

View file

@ -181,28 +181,30 @@ function builder_heading() {
builder_echo() {
local color=white message= mark= block= action= do_output=true
local color=white message= mark= block= action= do_output=true test=
local echo_target=echo
if [[ $# -gt 1 ]]; then
if [[ $1 =~ ^(white|grey|green|success|blue|heading|yellow|warning|red|error|purple|brightwhite|teal|debug|setmark)$ ]]; then
color="$1"
shift
elif [[ $1 == "start" ]]; then
elif [[ $1 == "start" ]] || [[ $1 == "startTest" ]]; then
# builder_echo start block message
action="$1"
test="$1"
block="$2"
shift 2
action="start"
color="heading"
if ! builder_is_running_on_teamcity && builder_is_child_build; then
do_output=${_builder_debug_internal:-false}
fi
elif [[ $1 == "end" ]]; then
elif [[ $1 == "end" ]] || [[ $1 == "endTest" ]]; then
# builder_echo end block status message
action="$1"
test="$1"
block="$2"
color="$3"
shift 3
action="end"
if [[ "${color}" != "error" ]] && ! builder_is_running_on_teamcity && builder_is_child_build; then
do_output=${_builder_debug_internal:-false}
fi
@ -211,7 +213,11 @@ builder_echo() {
message="$*"
if [[ "${action}" == "start" ]] && builder_is_running_on_teamcity; then
$echo_target -e "##teamcity[blockOpened name='|[${THIS_SCRIPT_IDENTIFIER}|] ${block}']"
if [[ "${test}" == "startTest" ]]; then
$echo_target -e "##teamcity[testSuiteStarted name='|[${THIS_SCRIPT_IDENTIFIER}|] ${block}']"
else
$echo_target -e "##teamcity[blockOpened name='|[${THIS_SCRIPT_IDENTIFIER}|] ${block}']"
fi
fi
if ${do_output}; then
@ -241,7 +247,11 @@ builder_echo() {
fi
if [[ "${action}" == "end" ]] && builder_is_running_on_teamcity; then
$echo_target -e "##teamcity[blockClosed name='|[${THIS_SCRIPT_IDENTIFIER}|] ${block}']"
if [[ "${test}" == "endTest" ]]; then
$echo_target -e "##teamcity[testSuiteFinished name='|[${THIS_SCRIPT_IDENTIFIER}|] ${block}']"
else
$echo_target -e "##teamcity[blockClosed name='|[${THIS_SCRIPT_IDENTIFIER}|] ${block}']"
fi
fi
}

View file

@ -22,11 +22,11 @@ resources/docker-images/build.sh
```
By default this will create 64-bit images for building
Keyman Core, Keyman for Android, Keyman for Linux and
Keyman for Web. These images are based on the Ubuntu 24.04
with Node 20 and Emscripten 3.1.58 (for the exact versions,
see [`minimum-versions.inc.sh`](../build/minimum-versions.inc.sh))
and are named e.g. `keyman-core-ci:default`.
Keyman Core, Keyman for Android, Keyman for Linux, Keyman for Web
and (cross-platform parts of) Keyman Developer. These images are based
on Ubuntu 24.04 with Node 20 and Emscripten 3.1.58 (for the exact versions,
see [`minimum-versions.inc.sh`](../build/minimum-versions.inc.sh)).
The images are named e.g. `keymanapp/keyman-core-ci:default`.
The versions can be changed, e.g.
@ -34,7 +34,8 @@ The versions can be changed, e.g.
resources/docker-images/build.sh --distro-version jammy --node 20
```
This will create an image named e.g. `keyman-core-ci:jammy-node20`.
This will create an image named e.g.
`keymanapp/keyman-core-ci:ubuntu-jammy-java21-node20.16.0-emsdk3.1.58`.
Once the image is built, it may be used to build parts of Keyman.
@ -44,7 +45,7 @@ It is possible to build locally with these images:
```shell
# build 'Keyman Core' in docker
resources/docker-images/run.sh core -- core/build.sh --debug build
resources/docker-images/run.sh :core -- core/build.sh --debug build
```
Note: For Core and Linux we put the generated binaries in a
@ -61,5 +62,5 @@ To run the tests locally, use the `run.sh` script:
```shell
# Run common/web tests
resources/docker-images/run.sh web -- common/web/build.sh --debug test
resources/docker-images/run.sh :web -- common/web/build.sh --debug test
```

View file

@ -1,3 +1,4 @@
# syntax=docker/dockerfile:1.4
# Keyman is copyright (C) SIL Global. MIT License.
ARG BASE_VERSION=default

View file

@ -1,3 +1,4 @@
# syntax=docker/dockerfile:1.4
# Keyman is copyright (C) SIL Global. MIT License.
ARG DISTRO=ubuntu

View file

@ -17,6 +17,7 @@ builder_describe \
":android" \
":base" \
":core" \
":developer" \
":linux" \
":web" \
"--distro=DISTRO The distribution to use for the base image (debian or ubuntu, default: ubuntu)" \
@ -45,6 +46,7 @@ build_action() {
# shellcheck disable=SC2164
cd "${platform}"
export DOCKER_BUILDKIT=1
# shellcheck disable=SC2248,SC2086
docker build ${OPTION_NO_CACHE:-} -t "keymanapp/keyman-${platform}-ci:${build_version}" "${build_args[@]}" .
# If the user didn't specify particular versions we will additionaly create an image
@ -63,18 +65,20 @@ test_action() {
builder_echo debug "Testing image for ${platform}"
./run.sh --distro "${DISTRO}" --distro-version "${DISTRO_VERSION}" \
"${platform}" -- ./build.sh configure,build,test:"${platform}"
":${platform}" -- ./build.sh configure,build,test:"${platform}"
}
check_buildx_available
check_for_default_values
convert_parameters_to_args
if builder_has_action build; then
build_action base
builder_run_action build:android build_action android
builder_run_action build:core build_action core
builder_run_action build:linux build_action linux
builder_run_action build:web build_action web
builder_run_action build:android build_action android
builder_run_action build:core build_action core
builder_run_action build:linux build_action linux
builder_run_action build:web build_action web
builder_run_action build:developer build_action developer
fi
builder_run_action test:core test_action core
@ -82,3 +86,4 @@ builder_run_action test:linux test_action linux
builder_run_action test:web test_action web
# Android uses artifacts from web, so it has to come after web
builder_run_action test:android test_action android
builder_run_action test:developer test_action developer

View file

@ -1,3 +1,4 @@
# syntax=docker/dockerfile:1.4
# Keyman is copyright (C) SIL Global. MIT License.
#
# ARGS used in this file:

View file

@ -0,0 +1,55 @@
# syntax=docker/dockerfile:1.4
# Keyman is copyright (C) SIL Global. MIT License.
#
# ARGS used in this file:
# - ARG BASE_VERSION=default
# - ARG REQUIRED_EMSCRIPTEN_VERSION=unset
ARG BASE_VERSION=default
FROM keymanapp/keyman-base-ci:${BASE_VERSION}
LABEL org.opencontainers.image.authors="SIL Global."
LABEL org.opencontainers.image.url="https://github.com/keymanapp/keyman.git"
LABEL org.opencontainers.image.title="Keyman Developer Build Image"
USER root
RUN apt-get update && \
apt-get install -qy devscripts jq meson
# Pre-install emscripten
USER build
ARG REQUIRED_EMSCRIPTEN_VERSION=unset
RUN echo "Installing emscripten version ${REQUIRED_EMSCRIPTEN_VERSION}" && \
export EMSDK_KEEP_DOWNLOADS=1 && \
cd /home/build/ && \
git clone https://github.com/emscripten-core/emsdk.git && \
cd emsdk && \
./emsdk install ${REQUIRED_EMSCRIPTEN_VERSION} && \
./emsdk activate ${REQUIRED_EMSCRIPTEN_VERSION}
USER root
RUN echo "export EMSCRIPTEN_BASE=/home/build/emsdk/upstream/emscripten" >> /usr/bin/bashwrapper && \
echo "export KEYMAN_USE_EMSDK=1" >> /usr/bin/bashwrapper
# Finish bashwrapper script and adjust permissions
RUN <<EOF cat >> /usr/bin/bashwrapper
if [[ "\$@" =~ test ]] && [ -f /usr/bin/run-tests.sh ]; then
/usr/bin/run-tests.sh "\${@:-bash}"
else
"\${@:-bash}"
fi
EOF
# now, switch to build user
USER build
# Pre-install node
RUN export NVM_DIR="/home/build/.nvm" && \
. /home/build/.nvm/nvm.sh && \
cd /home/build/emsdk/upstream/emscripten && \
npm install
VOLUME /home/build/build
WORKDIR /home/build/build
ENTRYPOINT [ "/usr/bin/bashwrapper" ]

View file

@ -59,3 +59,15 @@ check_for_default_values() {
is_default_values() {
${is_default_values}
}
# Check that `docker buildx` is available
check_buildx_available() {
if ! docker --help | grep -q buildx; then
if [[ "${OSTYPE:-}" == "linux-gnu" ]]; then
builder_echo error "Docker buildx is not available. Please install Docker buildx to use this script. e.g. 'sudo apt install docker-buildx'"
else
builder_echo error "Docker buildx is not available. Please install Docker buildx to use this script."
fi
exit 1
fi
}

View file

@ -1,3 +1,4 @@
# syntax=docker/dockerfile:1.4
# Keyman is copyright (C) SIL Global. MIT License.
ARG BASE_VERSION=default

View file

@ -14,10 +14,12 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
builder_describe \
"Run build.sh script inside of a docker image. Pass the build script and parameters after --." \
"android" \
"core" \
"linux" \
"web" \
":android" \
":core" \
":developer" \
":linux" \
":web" \
"run+ Run command in docker image" \
"--distro=DISTRO The distribution (debian or ubuntu, default: ubuntu)" \
"--distro-version=DISTRO_VERSION The Ubuntu/Debian version (default: ${KEYMAN_DEFAULT_VERSION_UBUNTU_CONTAINER})"
@ -48,6 +50,13 @@ run_core() {
"${builder_extra_params[@]}"
}
run_developer() {
docker run -it --rm -v "${KEYMAN_ROOT}":/home/build/build \
-v "${KEYMAN_ROOT}/core/build/docker-core/${build_dir}":/home/build/build/core/build \
"keymanapp/keyman-developer-ci:${image_version}" \
"${builder_extra_params[@]}"
}
run_linux() {
mkdir -p "${KEYMAN_ROOT}/linux/build/docker-linux/${build_dir}"
mkdir -p "${KEYMAN_ROOT}/linux/keyman-system-service/build/docker-linux/${build_dir}"
@ -69,7 +78,8 @@ run_web() {
mkdir -p "${KEYMAN_ROOT}/core/build/docker-core/${build_dir}"
builder_run_action android run_android
builder_run_action core run_core
builder_run_action linux run_linux
builder_run_action web run_web
builder_run_action run:android run_android
builder_run_action run:core run_core
builder_run_action run:developer run_developer
builder_run_action run:linux run_linux
builder_run_action run:web run_web

View file

@ -1,3 +1,4 @@
# syntax=docker/dockerfile:1.4
# Keyman is copyright (C) SIL Global. MIT License.
#
# ARGS used in this file:

View file

@ -20,19 +20,15 @@ docker run -v $(pwd):/Develop -it ubuntu:24.04 /bin/bash
Inside of the container, run
```bash
apt update && apt install sudo
apt update && apt install -y sudo
export DOCKER_RUNNING=1
echo "ubuntu ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
su ubuntu
```
and then run the build, e.g.
```bash
cd /Develop
resources/teamcity/linux/keyman-linux-test.sh configure,build,test
```
NOTE: by default this will run the build as `root` in the container,
so you will end up with files owned by `root` in your tree.
Either create a user with the same user id as your local user in the
container, or use one of the images created by `resources/docker-images`
(though you won't be able to test if all the dependencies get installed
if using the docker-images containers).

View file

@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Keyman is copyright (C) SIL Global. MIT License.
developer_install_dependencies_on_linux_action() {
if ! is_ubuntu; then
return 0
fi
builder_echo start "install dependencies" "Installing dependencies"
linux_check_and_install_packages devscripts jq meson
install_nvm
install_emscripten
builder_echo end "install dependencies" success "Finished installing dependencies"
}

View file

@ -0,0 +1,60 @@
# Keyman is copyright (C) SIL Global. MIT License.
#
# This script should be the identical for nightly/beta/stable for a given platform
#
$ErrorActionPreference = "Stop"
$RSYNC_PATH = $env:RSYNC_PATH
$RSYNC_HOME = $env:RSYNC_HOME
$USERPROFILE = $env:USERPROFILE
New-Item -Force -ItemType Directory symbols\000admin
cd symbols\000admin
#
# Download the symbol server index files with rsync from downloads.keyman.com
# (rsync requires that we are in the symbols folder to get folders in
# sync correctly; it is possible to resolve this but easier to just cd.)
#
$rsync_args = @(
'-vrzltp', # verbose, recurse, zip, copy symlinks, preserve times, permissions
'--chmod=Dug=rwx,Do=rx,Fug=rw,Fo=r', # map Windows security to host security
'--stats', # show statistics for log
'--rsync-path="$RSYNC_PATH"', # path on remote server
"--rsh=$RSYNC_HOME\ssh -i $USERPROFILE\.ssh\id_rsa -o UserKnownHostsFile=$USERPROFILE\.ssh\known_hosts", # use ssh
"root@sysops.downloads.keyman.com:/var/www/virtual/downloads.keyman.com/htdocs/windows/symbols/000admin/lastid.txt", # target server + path
"." # download the whole symbols 000Admin folder
)
& $RSYNC_HOME\rsync.exe $rsync_args
if ($LASTEXITCODE -ne 0) { throw "Exit code is $LASTEXITCODE" }
$rsync_args = @(
'-vrzltp', # verbose, recurse, zip, copy symlinks, preserve times, permissions
'--chmod=Dug=rwx,Do=rx,Fug=rw,Fo=r', # map Windows security to host security
'--stats', # show statistics for log
'--rsync-path="$RSYNC_PATH"', # path on remote server
"--rsh=$RSYNC_HOME\ssh -i $USERPROFILE\.ssh\id_rsa -o UserKnownHostsFile=$USERPROFILE\.ssh\known_hosts", # use ssh
"root@sysops.downloads.keyman.com:/var/www/virtual/downloads.keyman.com/htdocs/windows/symbols/000admin/history.txt", # target server + path
"." # download the whole symbols 000Admin folder
)
& $RSYNC_HOME\rsync.exe $rsync_args
if ($LASTEXITCODE -ne 0) { throw "Exit code is $LASTEXITCODE" }
$rsync_args = @(
'-vrzltp', # verbose, recurse, zip, copy symlinks, preserve times, permissions
'--chmod=Dug=rwx,Do=rx,Fug=rw,Fo=r', # map Windows security to host security
'--stats', # show statistics for log
'--rsync-path="$RSYNC_PATH"', # path on remote server
"--rsh=$RSYNC_HOME\ssh -i $USERPROFILE\.ssh\id_rsa -o UserKnownHostsFile=$USERPROFILE\.ssh\known_hosts", # use ssh
"root@sysops.downloads.keyman.com:/var/www/virtual/downloads.keyman.com/htdocs/windows/symbols/000admin/server.txt", # target server + path
"." # download the whole symbols 000Admin folder
)
& $RSYNC_HOME\rsync.exe $rsync_args
if ($LASTEXITCODE -ne 0) { throw "Exit code is $LASTEXITCODE" }
# EOF

View file

@ -0,0 +1,136 @@
#!/usr/bin/env bash
# Keyman is copyright (C) SIL Global. MIT License.
#
# TC build script to build release of Keyman Developer
# shellcheck disable=SC2164
# shellcheck disable=SC1091
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
# shellcheck disable=SC2154
. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-helpers.inc.sh"
################################ Main script ################################
builder_describe \
"Build Keyman Developer on Windows" \
"all run all actions" \
"build build Keyman Developer and test keyboards" \
"publish publish release of Keyman Developer" \
"--rsync-path=RSYNC_PATH rsync path on remote server" \
"--rsync-user=RSYNC_USER rsync user on remote server" \
"--rsync-host=RSYNC_HOST rsync host on remote server" \
"--rsync-root=RSYNC_ROOT rsync root on remote server" \
"--help.keyman.com=HELP_KEYMAN_COM path to help.keyman.com repository"
builder_parse "$@"
# shellcheck disable=SC2154
cd "${KEYMAN_ROOT}/developer/src"
function _build_developer() {
builder_echo start "build developer" "Building Keyman Developer"
./build.sh configure build test api publish --npm-publish
builder_echo end "build developer" success "Finished building Keyman Developer"
}
function _build_testkeyboards() {
builder_echo start "build testkeyboards" "Building test keyboards"
"${KEYMAN_ROOT}/common/test/keyboards/build.sh"
builder_echo end "build testkeyboards" success "Finished building test keyboards"
}
function _publish_sentry() {
builder_echo start "publish sentry" "Publishing debug information files to Sentry"
"${KEYMAN_ROOT}/developer/src/tools/sentry-upload-difs.sh"
builder_echo end "publish sentry" success "Finished publishing debug information files to Sentry"
}
function _download_symbol_server_index() {
# Download symbol server index from symbol server
builder_echo start "download symbol server index" "Downloading symbol server index"
cd "${KEYMAN_ROOT}/.."
# shellcheck disable=SC2154
powershell -NonInteractive -ExecutionPolicy Bypass -File "${THIS_SCRIPT_PATH}/download-symbol-server-index.ps1"
cd "${KEYMAN_ROOT}/developer/src"
builder_echo end "download symbol server index" success "Finished downloading symbol server index"
}
function _publish_new_symbols() {
# Publish new symbols to symbol server
builder_echo start "publish new symbols" "Publishing new symbols to symbol server"
cd "${KEYMAN_ROOT}/../symbols"
# shellcheck disable=SC2154
powershell -NonInteractive -ExecutionPolicy Bypass -File "${THIS_SCRIPT_PATH}/publish-new-symbols.ps1"
cd "${KEYMAN_ROOT}/developer/src"
builder_echo end "publish new symbols" success "Finished publishing new symbols to symbol server"
}
function _publish_to_downloads_keyman_com() {
# Publish to downloads.keyman.com
builder_echo start "publish to downloads.keyman.com" "Publishing release to downloads.keyman.com"
cd "${KEYMAN_ROOT}/developer"
# shellcheck disable=SC2154
powershell -NonInteractive -ExecutionPolicy Bypass -File "${THIS_SCRIPT_PATH}/publish-to-downloads-keyman-com.ps1"
cd "${KEYMAN_ROOT}/developer/src"
builder_echo end "publish to downloads.keyman.com" success "Finished publishing release to downloads.keyman.com"
}
function _publish_api_documentation() {
# Upload new Keyman Developer API documentation to help.keyman.com
builder_echo start "publish api documentation" "Uploading new Keyman Developer API documentation to help.keyman.com"
export HELP_KEYMAN_COM="${HELP_KEYMAN_COM:-${KEYMAN_ROOT}/../help.keyman.com}"
"${KEYMAN_ROOT}/resources/build/help-keyman-com.sh" developer
builder_echo end "publish api documentation" success "Finished uploading new Keyman Developer API documentation to help.keyman.com"
}
function build_developer_action() {
_build_developer
_build_testkeyboards
}
function publish_action() {
if ! is_windows; then
# requires Powershell, so currently only supported on Windows
builder_echo error "This script is intended to be run on Windows only."
return 1
fi
export RSYNC_PATH
export RSYNC_USER
export RSYNC_HOST
export RSYNC_ROOT
_publish_sentry
_download_symbol_server_index
_publish_new_symbols
_publish_to_downloads_keyman_com
_publish_api_documentation
}
if builder_has_action all; then
build_developer_action
publish_action
else
builder_run_action build build_developer_action
builder_run_action publish publish_action
fi

View file

@ -0,0 +1,60 @@
#!/usr/bin/env bash
# Keyman is copyright (C) SIL Global. MIT License.
#
# TC build script for Keyman Developer on Linux and macOS
# shellcheck disable=SC2164
# shellcheck disable=SC1091
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
# shellcheck disable=SC2154
. "${KEYMAN_ROOT}/resources/teamcity/developer/developer-actions.inc.sh"
. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-actions.inc.sh"
. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-helpers.inc.sh"
. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-linux.inc.sh"
################################ Main script ################################
builder_describe \
"Build Keyman Developer on Linux and macOS" \
"all run all actions" \
"configure install dependencies" \
"build build"
builder_parse "$@"
cd "${KEYMAN_ROOT}/developer/src"
function build_developer_action() {
builder_echo start "build developer" "Building Keyman Developer"
"${KEYMAN_ROOT}/developer/src/build.sh" configure build test
builder_echo end "build developer" success "Finished building Keyman Developer"
}
if is_windows; then
builder_echo error "This script is intended to be run on Linux or macOS only."
exit 1
fi
if builder_has_action all; then
developer_install_dependencies_on_linux_action
set_variables_for_nvm
set_variables_for_emscripten
build_developer_action
else
builder_run_action configure developer_install_dependencies_on_linux_action
set_variables_for_nvm
set_variables_for_emscripten
builder_run_action build build_developer_action
fi

View file

@ -0,0 +1,71 @@
#!/usr/bin/env bash
# Keyman is copyright (C) SIL Global. MIT License.
#
# TC build script for Keyman Developer on Windows
# shellcheck disable=SC2164
# shellcheck disable=SC1091
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
# shellcheck disable=SC2154
. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-helpers.inc.sh"
################################ Main script ################################
builder_describe \
"Build Keyman Developer on Windows" \
"all run all actions" \
"build build Keyman Developer and test keyboards" \
"publish publish debug information files to sentry"
builder_parse "$@"
# shellcheck disable=SC2154
cd "${KEYMAN_ROOT}/developer/src"
function build_developer_action() {
_build_developer
_build_testkeyboards
}
function _build_developer() {
builder_echo start "build developer" "Building Keyman Developer"
./build.sh configure build test api publish --dry-run
builder_echo end "build developer" success "Finished building Keyman Developer"
}
function _build_testkeyboards() {
builder_echo start "build testkeyboards" "Building test keyboards"
"${KEYMAN_ROOT}/common/test/keyboards/build.sh" --zip-source --index
builder_echo end "build testkeyboards" success "Finished building test keyboards"
}
function publish_sentry_action() {
builder_echo start "publish sentry" "Publishing debug information files to Sentry"
"${KEYMAN_ROOT}/developer/src/tools/sentry-upload-difs.sh"
builder_echo end "publish sentry" success "Finished publishing debug information files to Sentry"
}
if ! is_windows; then
builder_echo error "This script is intended to be run on Windows only."
exit 1
fi
if builder_has_action all; then
build_developer_action
publish_sentry_action
else
builder_run_action build build_developer_action
builder_run_action publish publish_sentry_action
fi

View file

@ -0,0 +1,39 @@
# Keyman is copyright (C) SIL Global. MIT License.
#
# This script should be the identical for nightly/beta/stable for a given platform
#
$ErrorActionPreference = "Stop"
$RSYNC_HOME = $env:RSYNC_HOME
$RSYNC_PATH = $env:RSYNC_PATH
$RSYNC_USER = $env:RSYNC_USER
$RSYNC_HOST = $env:RSYNC_HOST
$RSYNC_ROOT = $env:RSYNC_ROOT
$USERPROFILE = $env:USERPROFILE
# Rename 000Admin to 000admin
ren 000Admin 000admin_
ren 000admin_ 000admin
#
# Upload with rsync to downloads.keyman.com
# (rsync requires that we are in the symbols folder to get folders in
# sync correctly; it is possible to resolve this but easier to just cd.)
#
$rsync_args = @(
'-vrzltp', # verbose, recurse, zip, copy symlinks, preserve times, permissions
'--chmod=Dug=rwx,Do=rx,Fug=rw,Fo=r', # map Windows security to host security
'--stats', # show statistics for log
'--rsync-path="$RSYNC_PATH"', # path on remote server
"--rsh=$RSYNC_HOME\ssh -i $USERPROFILE\.ssh\id_rsa -o UserKnownHostsFile=$USERPROFILE\.ssh\known_hosts", # use ssh
".", # upload the whole symbols folder
"$RSYNC_USER@$RSYNC_HOST:$RSYNC_ROOT/windows/symbols/" # target server + path
)
& $RSYNC_HOME\rsync.exe $rsync_args
if ($LASTEXITCODE -ne 0) { throw "Exit code is $LASTEXITCODE" }
# EOF

View file

@ -0,0 +1,223 @@
# Keyman is copyright (C) SIL Global. MIT License.
$ErrorActionPreference = "Stop"
$tier = Get-Content ..\TIER.md
$build_number = Get-Content ..\VERSION.md
$build_counter = $build_number -replace "^\d+\.\d+\.(\d+)$", '$1'
$msi_major_version = $build_number -replace "^(\d+)\.(\d+)\.(\d+)$", '$1$2'
if ( $tier -eq "alpha" -or $tier -eq "beta") {
$version_tag = $tier
$version_with_tag = $build_number # we may go with $build_number + "-" + $version_tag in the future one day
} else {
$version_tag = ""
$version_with_tag = $build_number
}
$upload_path = "upload\$build_number"
$keyboards_path = "$upload_path\keyboards"
# Keyman Developer installers
$kmcomp_zip = "kmcomp-$version_with_tag.zip"
$keyman_developer_exe = "keymandeveloper-$version_with_tag.exe"
$setup_exe = "setup.exe"
# Debug files
$debug_zip = "debug-$build_number.zip"
$7Z_HOME = $env:7Z_HOME
$RSYNC_HOME = $env:RSYNC_HOME
$RSYNC_PATH = $env:RSYNC_PATH
$RSYNC_USER = $env:RSYNC_USER
$RSYNC_HOST = $env:RSYNC_HOST
$RSYNC_ROOT = $env:RSYNC_ROOT
$USERPROFILE = $env:USERPROFILE
###########################################################################################
## Developer upload
###########################################################################################
#
# Preparation
#
if((Test-Path $upload_path) -ne 0) {
Remove-Item -Path $upload_path -Recurse
}
mkdir $upload_path
#
# Build Keyman Compiler WINE archive
#
if((Test-Path ..\release\$kmcomp_zip) -ne 0) {
copy ..\release\$kmcomp_zip ..\$upload_path\$kmcomp_zip
} else {
cd bin
if((Test-Path ..\..\common\schemas\keyboard_info\keyboard_info.source.json) -ne 0) {
# Keyman versions through -16.0
copy ..\..\common\schemas\keyboard_info\keyboard_info.source.json .
copy ..\..\common\schemas\keyboard_info\keyboard_info.distribution.json .
& "$7Z_HOME\7z.exe" a -bd -bb0 ..\$upload_path\$kmcomp_zip kmcomp.exe kmcmpdll.dll kmcomp.x64.exe kmcmpdll.x64.dll kmconvert.exe keyboard_info.source.json keyboard_info.distribution.json xml\layoutbuilder\*.keyman-touch-layout projects\
# Add Keyman Developer Server to the archive (15.0 late alpha - after 171)
if((Test-Path ..\src\server\build) -ne 0) {
copy ..\src\server\build\ server\ -Recurse
& "$7Z_HOME\7z.exe" a -bd -bb0 ..\$upload_path\$kmcomp_zip server\
}
} else {
# Keyman versions 17.0+; note, use npm install for most modules
copy ..\..\common\schemas\keyboard_info\keyboard_info.schema.json .
& "$7Z_HOME\7z.exe" a -bd -bb0 ..\$upload_path\$kmcomp_zip kmconvert.exe keyboard_info.schema.json xml\layoutbuilder\*.keyman-touch-layout projects\ server\
}
cd ..
}
#
# Copy source files
#
copy release\$build_number\$keyman_developer_exe $upload_path\$keyman_developer_exe
#
# Construct keyman_developer.download_info
#
$hash = get-filehash $upload_path\$keyman_developer_exe -Algorithm MD5
$keyman_developer_download_info = @"
{
"name": "Keyman Developer",
"version": "$version_with_tag",
"date": "$([DateTime]::Now.ToString("yyyy-MM-dd"))",
"platform": "win",
"stability": "$tier",
"file": "keymandeveloper-$version_with_tag.exe",
"md5": "$($hash.Hash)",
"type": "exe",
"build": "$build_counter"
}
"@
[System.IO.File]::WriteAllLines("$pwd\$upload_path\$keyman_developer_exe.download_info", $keyman_developer_download_info)
#
# Construct kmcomp.download_info
#
$hash = get-filehash $upload_path\$kmcomp_zip -Algorithm MD5
$kmcomp_download_info = @"
{
"name": "Keyman Developer Command-Line Compiler",
"version": "$version_with_tag",
"date": "$([DateTime]::Now.ToString("yyyy-MM-dd"))",
"platform": "win",
"stability": "$tier",
"file": "kmcomp-$version_with_tag.zip",
"md5": "$($hash.Hash)",
"type": "zip",
"build": "$build_counter"
}
"@
[System.IO.File]::WriteAllLines("$pwd\$upload_path\$kmcomp_zip.download_info", $kmcomp_download_info)
#
# Copy common/test/keyboards/*/build/*.kmp to keyboards/
#
if((Test-Path ..\common\test\keyboards) -ne 0) {
mkdir $keyboards_path
Copy-Item ..\common\test\keyboards\*\build\*.kmp $keyboards_path\
}
#
# Upload with rsync to downloads.keyman.com
# (rsync requires that we are in the upload folder to get folders in
# sync correctly; it is possible to resolve this but easier to just cd.)
#
$rsync_args = @(
'-vrzltp', # verbose, recurse, zip, copy symlinks, preserve times, permissions
'--chmod=Dug=rwx,Do=rx,Fug=rw,Fo=r', # map Windows security to host security
'--stats', # show statistics for log
'--rsync-path="$RSYNC_PATH"', # path on remote server
"--rsh=$RSYNC_HOME\ssh -i $USERPROFILE\.ssh\id_rsa -o UserKnownHostsFile=$USERPROFILE\.ssh\known_hosts", # use ssh
"$build_number", # upload the whole build folder
"$RSYNC_USER@$RSYNC_HOST:$RSYNC_ROOT/developer/$tier/" # target server + path
)
# Write-Output "rsync parameters:" $rsync_args
cd upload
& $RSYNC_HOME\rsync.exe $rsync_args
if( $LASTEXITCODE -ne 0 ) {
Write-Output "rsync failed."
exit 99
}
cd ..
#################
# DEBUG.ZIP
#################
if((Test-Path release\$build_number\$debug_zip) -ne 0) {
#
# Copy source files
#
copy release\$build_number\$debug_zip $upload_path\$debug_zip
#
# Construct debug-<version>.download_info
#
$hash = get-filehash $upload_path\$debug_zip -Algorithm MD5
$debug_zip_download_info = @"
{
"name": "Keyman Developer debug files",
"version": "$version_with_tag",
"date": "$([DateTime]::Now.ToString("yyyy-MM-dd"))",
"platform": "win",
"stability": "$tier",
"file": "debug-$build_number.zip",
"md5": "$($hash.Hash)",
"type": "zip",
"build": "$build_counter"
}
"@
[System.IO.File]::WriteAllLines("$pwd\$upload_path\$debug_zip.download_info", $debug_zip_download_info)
}
#
# Upload with rsync to downloads.keyman.com
# (rsync requires that we are in the upload folder to get folders in
# sync correctly; it is possible to resolve this but easier to just cd.)
#
$rsync_args = @(
'-vrzltp', # verbose, recurse, zip, copy symlinks, preserve times, permissions
'--chmod=Dug=rwx,Do=rx,Fug=rw,Fo=r', # map Windows security to host security
'--stats', # show statistics for log
'--rsync-path="$RSYNC_PATH"', # path on remote server
"--rsh=$RSYNC_HOME\ssh -i $USERPROFILE\.ssh\id_rsa -o UserKnownHostsFile=$USERPROFILE\.ssh\known_hosts", # use ssh
"$build_number", # upload the whole build folder
"$RSYNC_USER@$RSYNC_HOST:$RSYNC_ROOT/developer/$tier/" # target server + path
)
# Write-Output "rsync parameters:" $rsync_args
cd upload
& $RSYNC_HOME\rsync.exe $rsync_args
if( $LASTEXITCODE -ne 0 ) {
Write-Output "rsync failed."
exit 99
}
cd ..
# EOF

View file

@ -1,4 +1,5 @@
#!/usr/bin/env bash
# Keyman is copyright (C) SIL Global. MIT License.
# Run the clean action in the `linux` directory.`
linux_clean_action() {
@ -25,7 +26,7 @@ linux_additional_test_dependencies_action() {
fi
# shellcheck disable=SC2086
check_and_install_packages ${TOINSTALL}
linux_check_and_install_packages ${TOINSTALL}
if ! is_os_version_or_higher 24.04; then
builder_heading "Installing python3-coverage from pip"
@ -47,7 +48,8 @@ linux_build_action() {
# Run unit tests for Keyman for Linux.
linux_unit_tests_action() {
builder_echo start unit_tests "Running unit tests"
builder_echo startTest unit_tests "Running unit tests"
rm -f /tmp/ibus-engine-keyman.log
rm -f /tmp/ibus-daemon.log
# symlink might point to wrong location, so delete it - will be re-created during tests
@ -56,6 +58,5 @@ linux_unit_tests_action() {
export NO_AT_BRIDGE=1
# shellcheck disable=SC2068
"${KEYMAN_ROOT}/linux/build.sh" test $@
builder_echo end unit_tests success "Finished running unit tests"
builder_echo endTest unit_tests success "Finished running unit tests"
}

View file

@ -0,0 +1,68 @@
#!/usr/bin/env bash
# Keyman is copyright (C) SIL Global. MIT License.
# Returns 0 if we're running on Ubuntu.
is_ubuntu() {
if [[ "${OSTYPE:-}" == "linux-gnu" ]]; then
return 0
else
return 1
fi
}
# Returns 0 if we're running on Windows, i.e. if the environment variable
# `OSTYPE` is set to "msys" or "cygwin".
is_windows() {
if [[ "${OSTYPE:-}" == "msys" ]] || [[ "${OSTYPE:-}" == "cygwin" ]]; then
return 0
else
return 1
fi
}
# Returns 0 if we're running on macOS.
is_macos() {
if [[ "${OSTYPE:-}" == "darwin" ]]; then
return 0
else
return 1
fi
}
install_nvm() {
if ! is_ubuntu; then
# on Windows and macOS build agents are configured manually
return 0
fi
linux_install_nvm
}
# Set the environment variables required to use node/nvm and set the
# `KEYMAN_USE_NVM` variable so that the build can automatically install
# the required node version.
set_variables_for_nvm() {
if [[ -f "${HOME}/.nvm/nvm.sh" ]] && [[ -d "${HOME}/.keyman/node" ]]; then
# nvm.sh uses some variables that might not be initialized, so we
# disable the "unbound variable" check temporarily
set -u
export NVM_DIR="${HOME}/.nvm"
# shellcheck disable=SC1091
. "${NVM_DIR}/nvm.sh"
set +u
export KEYMAN_USE_NVM=1
PATH=${HOME}/.keyman/node:${PATH}
fi
}
install_emscripten() {
if ! is_ubuntu; then
# on Windows and macOS build agents are configured manually
return 0
fi
linux_install_emscripten
}
set_variables_for_emscripten() {
export EMSCRIPTEN_BASE="${EMSCRIPTEN_BASE:-${HOME}/emsdk/upstream/emscripten}"
export KEYMAN_USE_EMSDK=1
}

View file

@ -1,4 +1,5 @@
#!/usr/bin/env bash
# Keyman is copyright (C) SIL Global. MIT License.
# Returns 0 if the OS version is greater than or equal to the specified version.
# Parameter:
@ -23,7 +24,12 @@ is_package_installed() {
# Check if the specified packages are installed and install them if not.
# Parameters:
# $* - List of package names to check and install (e.g., "lcov jq")
check_and_install_packages() {
linux_check_and_install_packages() {
if ! is_ubuntu; then
return 0
fi
builder_echo start "check and install packages" "Checking and installing packages"
local PACKAGES=$*
local TOINSTALL=""
@ -39,21 +45,7 @@ check_and_install_packages() {
# shellcheck disable=SC2086
sudo DEBIAN_FRONTEND="noninteractive" apt-get install -qy ${TOINSTALL}
fi
}
# Set the environment variables required to use node/nvm and set the
# `KEYMAN_USE_NVM` variable so that the build can automatically install
# the required node version.
set_variables_for_nvm() {
# nvm.sh uses some variables that might not be initialized, so we
# disable the "unbound variable" check temporarily
set -u
export NVM_DIR="${HOME}/.nvm"
# shellcheck disable=SC1091
. "${NVM_DIR}/nvm.sh"
set +u
export KEYMAN_USE_NVM=1
PATH=${HOME}/.keyman/node:${PATH}
builder_echo end "check and install packages" success "Finished checking and installing packages"
}
# Install nvm if it is not already installed and install the latest LTS
@ -74,12 +66,60 @@ linux_install_nvm() {
builder_echo end install_nvm success "Finished checking and installing nvm"
}
linux_install_emscripten() {
builder_echo start "install emscripten" "Checking and installing emscripten"
if { [[ ! -z "${EMSCRIPTEN_BASE:-}" ]] && [[ -f "${EMSCRIPTEN_BASE}/emcc" ]] ; } ||
[[ -f "${HOME}/emsdk/upstream/emscripten/emcc" ]]; then
builder_echo "Emscripten is already installed at ${EMSCRIPTEN_BASE:-${HOME}/emsdk/upstream/emscripten}"
else
# shellcheck disable=SC2154
. "${KEYMAN_ROOT}/resources/build/minimum-versions.inc.sh"
builder_echo "Installing emscripten version ${KEYMAN_MIN_VERSION_EMSCRIPTEN}"
export EMSDK_KEEP_DOWNLOADS=1
# shellcheck disable=SC2164
cd "${HOME}"
git clone https://github.com/emscripten-core/emsdk.git
# shellcheck disable=SC2164
cd emsdk
./emsdk install "${KEYMAN_MIN_VERSION_EMSCRIPTEN}"
./emsdk activate "${KEYMAN_MIN_VERSION_EMSCRIPTEN}"
fi
set_variables_for_emscripten
builder_echo end "install emscripten" success "Finished checking and installing emscripten"
}
# Install additional dependencies required for running integration tests.
linux_install_dependencies_for_tests() {
builder_echo start install_dependencies_for_tests "Installing dependencies for tests"
check_and_install_packages xvfb xserver-xephyr metacity mutter dbus-x11 weston xwayland
linux_check_and_install_packages xvfb xserver-xephyr metacity mutter dbus-x11 weston xwayland
builder_echo end install_dependencies_for_tests success "Finished installing dependencies for tests"
}
linux_start_xvfb() {
# On Linux start Xvfb etc
local PID_FILE=/tmp/keymanweb-pids
builder_echo "Starting Xvfb..."
Xvfb -screen 0 1024x768x24 :33 &> /dev/null &
echo "kill -9 $! || true" > "${PID_FILE}"
sleep 1
builder_echo "Starting Xephyr..."
DISPLAY=:33 Xephyr :32 -screen 1024x768 &> /dev/null &
echo "kill -9 $! || true" >> "${PID_FILE}"
sleep 1
builder_echo "Starting metacity"
metacity --display=:32 &> /dev/null &
echo "kill -9 $! || true" >> "${PID_FILE}"
export DISPLAY=:32
}
linux_stop_xvfb() {
# On Linux stop Xvfb etc
local PID_FILE=/tmp/keymanweb-pids
if [[ -f "${PID_FILE}" ]]; then
bash "${PID_FILE}"
rm -f "${PID_FILE}"
fi
}

View file

@ -11,6 +11,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
# shellcheck disable=SC2154
. "${KEYMAN_ROOT}/resources/shellHelperFunctions.sh"
. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-actions.inc.sh"
. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-helpers.inc.sh"
. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-linux.inc.sh"
################################ Main script ################################

View file

@ -12,6 +12,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
# shellcheck disable=SC2154
. "${KEYMAN_ROOT}/resources/shellHelperFunctions.sh"
. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-actions.inc.sh"
. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-helpers.inc.sh"
. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-linux.inc.sh"
################################ Main script ################################

View file

@ -12,6 +12,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
# shellcheck disable=SC2154
. "${KEYMAN_ROOT}/resources/shellHelperFunctions.sh"
. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-actions.inc.sh"
. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-helpers.inc.sh"
. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-linux.inc.sh"
################################ Main script ################################

View file

@ -0,0 +1,102 @@
#!/usr/bin/env bash
# Keyman is copyright (C) SIL Global. MIT License.
#
# TC build script to build release of KeymanWeb.
# shellcheck disable=SC2164
# shellcheck disable=SC1091
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
# shellcheck disable=SC2154
. "${KEYMAN_ROOT}/resources/shellHelperFunctions.sh"
. "${KEYMAN_ROOT}/resources/teamcity/web/web-actions.inc.sh"
. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-helpers.inc.sh"
. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-linux.inc.sh"
################################ Main script ################################
builder_describe \
"Run tests for native KeymanWeb" \
"all run all actions" \
"configure install dependencies" \
"build build Web + embedded" \
"publish publish release" \
"--s.keyman.com=S_KEYMAN_COM path to s.keyman.com repository" \
"--help.keyman.com=HELP_KEYMAN_COM path to help.keyman.com repository"
builder_parse "$@"
cd "${KEYMAN_ROOT}/web"
function _push_release_to_skeymancom() {
# Push release to s.keyman.com/kmw/engine (do this before updating
# downloads.keyman.com so we can ensure files are available)
builder_echo start publish "Publishing release to s.keyman.com"
cd "${S_KEYMAN_COM:=${KEYMAN_ROOT}/../s.keyman.com}"
git pull https://github.com/keymanapp/s.keyman.com.git master
cd "${KEYMAN_ROOT}/web"
"${KEYMAN_ROOT}/web/ci.sh" prepare:s.keyman.com --s.keyman.com "${S_KEYMAN_COM}"
builder_echo end publish success "Finished publishing release to s.keyman.com"
}
function _zip_and_upload_artifacts() {
builder_echo start "zip and upload artifacts" "Zipping and uploading artifacts"
# shellcheck disable=SC2154
powershell -NonInteractive -ExecutionPolicy Bypass -File "${THIS_SCRIPT_PATH}/zip-and-upload-artifacts.ps1"
builder_echo end "zip and upload artifacts" success "Finished zipping and uploading artifacts"
}
function _upload_help() {
builder_echo start "upload help" "Uploading new Keyman for Web help to help.keyman.com"
export HELP_KEYMAN_COM="${HELP_KEYMAN_COM:-${KEYMAN_ROOT}/../help.keyman.com}"
cd "${KEYMAN_ROOT}/resources/build"
"${KEYMAN_ROOT}/resources/build/help-keyman-com.sh" web
cd "${KEYMAN_ROOT}/web"
builder_echo end "upload help" success "Finished uploading new Keyman for Web help to help.keyman.com"
}
function publish_web_action() {
builder_echo start publish "Publishing KeymanWeb release"
# TODO: refactor to allow to run on Linux/macOS as well
if ! is_windows; then
builder_echo end publish error "Publishing KeymanWeb is only supported on Windows"
return 1
fi
# Push release to s.keyman.com/kmw/engine (do this before updating
# downloads.keyman.com so we can ensure files are available)
_push_release_to_skeymancom
_zip_and_upload_artifacts
_upload_help
builder_echo end publish success "Finished publishing KeymanWeb release"
}
if builder_has_action all; then
web_install_dependencies_on_linux_action
set_variables_for_nvm
web_build_action
publish_web_action
else
builder_run_action configure web_install_dependencies_on_linux_action
set_variables_for_nvm
builder_run_action build web_build_action
builder_run_action publish publish_web_action
fi

View file

@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Keyman is copyright (C) SIL Global. MIT License.
#
# TC build script for Keyman Web/Test
# shellcheck disable=SC2164
# shellcheck disable=SC1091
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
# shellcheck disable=SC2154
. "${KEYMAN_ROOT}/resources/shellHelperFunctions.sh"
. "${KEYMAN_ROOT}/resources/teamcity/web/web-actions.inc.sh"
. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-helpers.inc.sh"
. "${KEYMAN_ROOT}/resources/teamcity/includes/tc-linux.inc.sh"
################################ Main script ################################
builder_describe \
"Run tests for native KeymanWeb" \
"all run all actions" \
"configure install dependencies" \
"build build Web + embedded" \
"test run native KeymanWeb tests and check build size"
builder_parse "$@"
cd "${KEYMAN_ROOT}/web"
function check_build_size_action() {
builder_echo start "check build size" "Check build size"
"${KEYMAN_ROOT}/web/ci.sh" validate-size
builder_echo end "check build size" success "Finished checking build size"
}
if builder_has_action all; then
web_install_dependencies_on_linux_action
set_variables_for_nvm
web_build_action
web_test_action
check_build_size_action
else
builder_run_action configure web_install_dependencies_on_linux_action
set_variables_for_nvm
builder_run_action build web_build_action
builder_run_action test web_test_action
builder_run_action test check_build_size_action
fi

View file

@ -0,0 +1,59 @@
#!/usr/bin/env bash
# Keyman is copyright (C) SIL Global. MIT License.
web_install_dependencies_on_linux_action() {
if ! is_ubuntu; then
return 0
fi
builder_echo start "install dependencies" "Install dependencies"
# shellcheck disable=SC2086
linux_check_and_install_packages devscripts jq
linux_install_nvm
_install_playwright_dependencies
builder_echo end "install dependencies" success "Finished installing dependencies"
}
_install_playwright_dependencies() {
if ! is_ubuntu || ! is_os_version_or_higher 24.04; then
return 0
fi
# shellcheck disable=SC2086
linux_check_and_install_packages ibevent-2.1-7t64 libxslt1.1 libwoff1 \
libvpx9 libgstreamer-plugins-bad1.0-0 libwebpdemux2 libharfbuzz-icu0 \
libenchant-2-2 libsecret-1-0 libhyphen0 libmanette-0.2-0 libflite1 \
gstreamer1.0-libav libnss3 libnspr4 libatk1.0-0t64 libatk-bridge2.0-0t64 \
libcups2t64 libatspi2.0-0t64 libxcomposite1 libxdamage1 libxrandr2 \
libxcursor1 libgtk-3-0t64 libgles2
}
web_build_action() {
builder_echo start web_build "Building web"
# shellcheck disable=SC2154
"${KEYMAN_ROOT}/web/ci.sh" build
builder_echo end web_build success "Finished building web"
}
web_test_action() {
builder_echo start web_test "Running tests for native KeymanWeb"
if is_ubuntu; then
linux_start_xvfb
trap "linux_stop_xvfb" ERR
fi
"${KEYMAN_ROOT}/web/ci.sh" test
if is_ubuntu; then
linux_stop_xvfb
trap ERR
fi
"${KEYMAN_ROOT}/web/build.sh" coverage
builder_echo end web_test success "Finished running tests for native KeymanWeb"
}

View file

@ -0,0 +1,75 @@
# Keyman is copyright (C) SIL Global. MIT License.
#
# This script should be the identical for nightly/beta/stable for a given platform
#
# Working directory should be ${KEYMAN_ROOT}/web
#
$ErrorActionPreference = "Stop";
$tier = Get-Content ..\TIER.md
$build_number = Get-Content ..\VERSION.md
$build_counter = $build_number -replace "^\d+\.\d+\.(\d+)$", '$1'
$upload_path = "build\upload\$build_number"
$zip = "$upload_path\keymanweb-$build_number.zip"
$7Z_HOME = $env:7Z_HOME
$RSYNC_HOME = $env:RSYNC_HOME
$USERPROFILE = $env:USERPROFILE
# Since shell-scripting doesn't like number-initial variables, we convert it to a friendlier name.
$env:SEVEN_Z_HOME=$7Z_HOME
# PowerShell fun: `--` parameters will break a PowerShell command when not escaped in some form.
# We can build the command in string form first and then execute the string, fortunately.
$cmd = '"C:\Program Files\Git\bin\bash.exe" --init-file "c:\Program Files\Git\etc\profile" -l ./ci.sh prepare:downloads.keyman.com'
cmd /c $cmd
$hash = get-filehash $zip -Algorithm MD5
#
# Construct .build_info
#
$download_info = @"
{
"name": "KeymanWeb",
"version": "$build_number",
"date": "$([DateTime]::Now.ToString("yyyy-MM-dd"))",
"platform": "web",
"stability": "$tier",
"file": "keymanweb-$build_number.zip",
"md5": "$($hash.Hash)",
"type": "zip",
"build": "$build_counter"
}
"@
# WriteAllLines is needed to avoid BOM
[System.IO.File]::WriteAllLines("$pwd\$upload_path\keymanweb-$build_number.zip.download_info", $download_info)
# $download_info | Out-File $upload_path\keymanweb-$build_number.zip.download_info -Encoding utf8
#
# Upload with rsync to downloads.keyman.com
# (rsync requires that we are in the upload folder to get folders in
# sync correctly; it is possible to resolve this but easier to just cd.)
#
$rsync_args = @(
'-vrzltp', # verbose, recurse, zip, copy symlinks, preserve times, permissions
'--chmod=Dug=rwx,Do=rx,Fug=rw,Fo=r', # map Windows security to host security
'--stats', # show statistics for log
'--rsync-path="%downloads_rsync_path%"', # path on remote server
"--rsh=$RSYNC_HOME\ssh -i $USERPROFILE\.ssh\id_rsa -o UserKnownHostsFile=$USERPROFILE\.ssh\known_hosts", # use ssh
"$build_number", # upload the whole build folder
"%downloads_rsync_user%@%downloads_rsync_host%:%downloads_rsync_root%/web/$tier/" # target server + path
)
# Write-Output "rsync parameters:" $rsync_args
cd "$upload_path\.."
& $RSYNC_HOME\rsync.exe $rsync_args
if ($LASTEXITCODE -ne 0) { throw "Exit code is $LASTEXITCODE" }
cd ..
# EOF

View file

@ -12,8 +12,8 @@ type Options = {
};
export class KeymanSentryManager {
keymanPlatform: string;
_enabled: boolean = true;
private keymanPlatform: string;
private _enabled: boolean = true;
static STANDARD_ALIASABLE_FILES = {
'keymanweb.js': 'keymanweb.js',
@ -34,7 +34,7 @@ export class KeymanSentryManager {
}
// If we've recognized one of our source files,
aliasFilename(filename: string): string|null {
private aliasFilename(filename: string): string|null {
if(!this.mayAlias(filename)) {
return null;
}
@ -49,14 +49,14 @@ export class KeymanSentryManager {
}
}
mayAlias(filename: string): boolean {
private mayAlias(filename: string): boolean {
return !!KeymanSentryManager.STANDARD_ALIASABLE_FILES[filename];
}
// Filters all expected but unnecessary path prefixes, affixes, and suffixes reported by Sentry from our products.
// This allows us to mask all different sorts of installations with a single uploaded path.
// Modifies original object.
pathFilter(event: any) {
private pathFilter(event: any) {
// Get the underlying JS error.
let exception = event.exception;
if(!exception) {
@ -90,7 +90,7 @@ export class KeymanSentryManager {
}
// Attaches some useful debugging information to the specified object, pass-by-reference style.
attachEventMetadata(event: any) {
private attachEventMetadata(event: any) {
// Ensure that the 'extra' object exists. (May not exist for synthetic/custom Errors.)
event.extra = event.extra || {};
//@ts-ignore
@ -100,7 +100,7 @@ export class KeymanSentryManager {
// Sanitizes the event object (in-place) to remove sensitive information
// from the breadcrumbs and url (for embedded KeymanWeb)
sanitizeEvent(event: any) {
private sanitizeEvent(event: any) {
if (event && event.breadcrumbs) {
event.breadcrumbs.forEach((b: any) => {
if (b.category == 'navigation') {
@ -123,7 +123,7 @@ export class KeymanSentryManager {
* Also will sanitize the Sentry event.
* @param event A Sentry-generated event
*/
prepareEvent(event: any): boolean {
private prepareEvent(event: any): boolean {
this.pathFilter(event);
this.attachEventMetadata(event);
this.sanitizeEvent(event);
@ -148,7 +148,7 @@ export class KeymanSentryManager {
* if we want to prevent the event from being sent to the server.
* @param event
*/
prepareEventDebugWrapper(event: any) {
public prepareEventDebugWrapper(event: any) {
if(DEBUG) {
try {
if(this.prepareEvent(event)) {
@ -178,7 +178,7 @@ export class KeymanSentryManager {
*
* https://stackoverflow.com/a/53214615/1836776
*/
initConsole() {
private initConsole() {
// creating function declarations for better stacktraces (otherwise they'd be anonymous function expressions)
let oldConsoleError = console.error;
let _this = this;
@ -223,7 +223,7 @@ export class KeymanSentryManager {
}
}
init() {
public init() {
// Do the actual Sentry initialization.
//@ts-ignore
Sentry.init({
@ -238,11 +238,11 @@ export class KeymanSentryManager {
this.initConsole();
}
get enabled(): boolean {
public get enabled(): boolean {
return this._enabled;
}
set enabled(value: boolean) {
public set enabled(value: boolean) {
this._enabled = value;
}
}

View file

@ -782,6 +782,7 @@ begin
// Return to Idle state
bucStateContext.RemoveCachedFiles;
ChangeState(IdleState);
Exit;
end;
DownloadResult := DownloadUpdatesBackground;
@ -920,7 +921,7 @@ begin
CheckForUpdates.Free;
end;
{ Response OK and go back to update available so files can be downloaded }
// TODO: This acually needs to check if the updates available are newer then the already downloaded updates
// TODO: This actually needs to check if the updates available are newer then the already downloaded updates
if Result = wucUpdateAvailable then
begin
@ -980,10 +981,11 @@ begin
executeResult := WaitForElevatedConfiguration(0, '-ikp');
if (executeResult <> 0) then
begin
TKeymanSentryClient.Client.MessageEvent
(Sentry.Client.SENTRY_LEVEL_ERROR,
'Executing kmshell process to install keyboard packages failed:"' +
IntToStr(Ord(executeResult)) + '"');
TKeymanSentryClient.Breadcrumb('error',
'Executing kmshell process to install keyboard packages failed"' +
IntToStr(Ord(executeResult)) + '"', 'update');
KL.Log('InstallingState.LaunchInstallPackageProcess failed executing kmshell ' +
'process to install keyboard packages: "' + IntToStr(Ord(executeResult)) + '"');
ChangeState(IdleState);
end;
end
@ -1035,9 +1037,10 @@ begin
if not FResult then
begin
bucStateContext.RemoveCachedFiles;
TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR,
'Executing kmshell process to install failed:"' +
IntToStr(Ord(FResult)) + '"');
TKeymanSentryClient.Breadcrumb('error',
'InstallingState.DoInstallKeyman: failed executing kmshell or file not found', 'update');
KL.Log('InstallingState.DoInstallKeyman failed executing kmshell ' +
'or the update was not found in the cache');
ChangeState(IdleState);
end;