feat(developer): kmc build command

This commit is contained in:
Marc Durdin 2023-02-23 13:16:01 +07:00
parent ca77d0728f
commit a30fdffd95
9 changed files with 255 additions and 164 deletions

View file

@ -42,7 +42,7 @@ foreach kbd : tests
)
configure_file(
command: kmc_cmd + ['@INPUT@', '--out-file', '@OUTPUT@'],
command: kmc_cmd + ['build', '@INPUT@', '--out-file', '@OUTPUT@'],
output: kbd + '.kmx',
input: kbd + '.xml'
)
@ -50,7 +50,7 @@ endforeach
foreach kbd : tests_with_testdata
configure_file(
command: kmc_cmd + ['@INPUT@', '--test-data', '@OUTPUT@'],
command: kmc_cmd + ['build-test-data', '@INPUT@', '@OUTPUT@'],
output: kbd + '-test.json',
input: kbd + '-test.xml'
)

View file

@ -34,7 +34,7 @@ kmc Usage
To compile an LDML keyboard from its `.xml` source, use `kmc`:
kmc my-keyboard.xml --outFile my-keyboard.kmx
kmc build my-keyboard.xml --outFile my-keyboard.kmx
To see more command line options by using the `--help` option:

View file

@ -45,8 +45,9 @@ if builder_start_action clean; then
else
# We need the schema file at runtime and bundled, so always copy it for all actions except `clean`
mkdir -p "$THIS_SCRIPT_PATH/build/src/"
cp "$KEYMAN_ROOT/resources/standards-data/ldml-keyboards/techpreview/ldml-keyboard.schema.json" "$THIS_SCRIPT_PATH/build/src/"
cp "$KEYMAN_ROOT/resources/standards-data/ldml-keyboards/techpreview/ldml-keyboardtest.schema.json" "$THIS_SCRIPT_PATH/build/src/"
cp "$KEYMAN_ROOT/resources/standards-data/ldml-keyboards/techpreview/ldml-keyboard.schema.json" "$THIS_SCRIPT_PATH/build/src/util/"
cp "$KEYMAN_ROOT/resources/standards-data/ldml-keyboards/techpreview/ldml-keyboardtest.schema.json" "$THIS_SCRIPT_PATH/build/src/util/"
cp "$KEYMAN_ROOT/common/schemas/kvks/kvks.schema.json" "$THIS_SCRIPT_PATH/build/src/util/"
fi

View file

@ -0,0 +1,81 @@
import * as path from 'path';
import * as fs from 'fs';
import * as kmc from '@keymanapp/kmc-keyboard';
import { KvkFileWriter, CompilerCallbacks } from '@keymanapp/common-types';
import { NodeCompilerCallbacks } from '../util/NodeCompilerCallbacks.js';
export function buildLdmlKeyboard(infile: string, options: any) {
// TODO-LDML: consider hardware vs touch -- touch-only layout will not have a .kvk
// Compile:
let [kmx,kvk,kmw] = buildLdmlKeyboardToMemory(infile, options);
// Output:
const fileBaseName = options.outFile ?? infile;
const outFileBase = path.basename(fileBaseName, path.extname(fileBaseName));
const outFileDir = path.dirname(fileBaseName);
if(kmx && kvk) {
const outFileKmx = path.join(outFileDir, outFileBase + '.kmx');
console.log(`Writing compiled keyboard to ${outFileKmx}`);
fs.writeFileSync(outFileKmx, kmx);
const outFileKvk = path.join(outFileDir, outFileBase + '.kvk');
console.log(`Writing compiled visual keyboard to ${outFileKvk}`);
fs.writeFileSync(outFileKvk, kvk);
} else {
console.error(`An error occurred compiling ${infile}`);
process.exit(1);
}
if(kmw) {
const outFileKmw = path.join(outFileDir, outFileBase + '.js');
console.log(`Writing compiled js keyboard to ${outFileKmw}`);
fs.writeFileSync(outFileKmw, kmw);
}
}
function buildLdmlKeyboardToMemory(inputFilename: string, options: any): [Uint8Array, Uint8Array, Uint8Array] {
let compilerOptions: kmc.CompilerOptions = {
debug: options.debug ?? false,
addCompilerVersion: options.compilerVersion ?? true
}
const c: CompilerCallbacks = new NodeCompilerCallbacks();
const k = new kmc.Compiler(c, options);
let source = k.load(inputFilename);
if (!source) {
return [null, null, null];
}
if (!k.validate(source)) {
return [null, null, null];
}
let kmx = k.compile(source);
if (!kmx) {
return [null, null, null];
}
// In order for the KMX file to be loaded by non-KMXPlus components, it is helpful
// to duplicate some of the metadata
kmc.KMXPlusMetadataCompiler.addKmxMetadata(kmx.kmxplus, kmx.keyboard, compilerOptions);
// Use the builder to generate the binary output file
const builder = new kmc.KMXBuilder(kmx, options.debug);
const kmx_binary = builder.compile();
const vkcompiler = new kmc.VisualKeyboardCompiler();
const vk = vkcompiler.compile(source);
const writer = new KvkFileWriter();
const kvk_binary = writer.write(vk);
// Note: we could have a step of generating source files here
// KvksFileWriter()...
// const tlcompiler = new kmc.TouchLayoutCompiler();
// const tl = tlcompiler.compile(source);
// const tlwriter = new TouchLayoutFileWriter();
const kmwcompiler = new kmc.KeymanWebCompiler(compilerOptions);
const kmw_string = kmwcompiler.compile(inputFilename, source);
const encoder = new TextEncoder();
const kmw_binary = encoder.encode(kmw_string);
return [kmx_binary, kvk_binary, kmw_binary];
}

View file

@ -0,0 +1,34 @@
import * as fs from 'fs';
import * as path from 'path';
import * as kmc from '@keymanapp/kmc-keyboard';
import { CompilerCallbacks, LDMLKeyboardTestDataXMLSourceFile } from '@keymanapp/common-types';
import { NodeCompilerCallbacks } from '../util/NodeCompilerCallbacks.js';
export function buildTestData(infile: string) {
let compilerOptions: kmc.CompilerOptions = {
debug: false,
addCompilerVersion: false
};
let testData = loadTestData(infile, compilerOptions);
if (!testData) {
return;
}
const fileBaseName = infile;
const outFileBase = path.basename(fileBaseName, path.extname(fileBaseName));
const outFileDir = path.dirname(fileBaseName);
const outFileJson = path.join(outFileDir, outFileBase + '.json');
console.log(`Writing JSON test data to ${outFileJson}`);
fs
.writeFileSync(outFileJson, JSON.stringify(testData, null, ' '));
}
function loadTestData(inputFilename: string, options: kmc.CompilerOptions): LDMLKeyboardTestDataXMLSourceFile {
const c: CompilerCallbacks = new NodeCompilerCallbacks();
const k = new kmc.Compiler(c, options);
let source = k.loadTestData(inputFilename);
if (!source) {
return null;
}
return source;
}

View file

@ -0,0 +1,53 @@
import { Command } from 'commander';
import { buildLdmlKeyboard } from '../activities/buildLdmlKeyboard.js';
export function declareBuild(program: Command) {
program
.command('build [infile...]')
.description('Build a source file into a final file')
.option('-d, --debug', 'Include debug information in output')
.option('-o, --out-file <filename>', 'where to save the resulting .kmx file')
.option('--no-compiler-version', 'Exclude compiler version metadata from output')
.action((infiles: string[], options: any) => {
if(!infiles.length) {
console.debug('Assuming infile == .');
build('.', options);
}
for(let infile of infiles) {
build(infile, options);
}
});
}
function build(infile: string, options: any) {
console.log(`Building ${infile}`);
if(infile.endsWith('.xml')) {
return buildLdmlKeyboard(infile, options);
}
/*
if(infile.endsWith('.kmn')) {
return buildKmnKeyboard(infile, options);
}
if(infile.endsWith('.kps')) {
return buildPackage(infile, options);
}
if(infile.endsWith('.model.ts')) {
return buildModel(infile, options);
}
if(infile.endsWith('.kpj')) {
return buildProject(infile, options);
}
if(fs.statSync(infile).isDirectory()) {
return buildProjectFolder(infile, options);
}
*/
console.error(`Unrecognised input file ${infile}, expecting .xml, .kmn, .kps, .model.ts, .kpj, or project folder`);
process.exit(2);
}

View file

@ -0,0 +1,14 @@
import { Command } from 'commander';
import { buildTestData } from '../activities/buildTestData.js';
export function declareBuildTestData(program: Command) {
program
.command('build-test-data <infile>')
.description('Convert keyboard test .xml to .json')
.option('-o, --out-file <filename>', 'where to save the resulting .kmx file')
.action(buildTestData);
}

View file

@ -3,172 +3,44 @@
* kmc - Keyman Next Generation Compiler
*/
import * as fs from 'fs';
import * as path from 'path';
import { Command } from 'commander';
import * as kmc from '@keymanapp/kmc-keyboard';
import KEYMAN_VERSION from "@keymanapp/keyman-version/keyman-version.mjs";
import { KvkFileWriter, CompilerCallbacks, CompilerEvent, LDMLKeyboardTestDataXMLSourceFile } from '@keymanapp/common-types';
let inputFilename: string;
import { declareBuild } from './commands/build.js';
import { declareBuildTestData } from './commands/buildTestData.js';
const program = new Command();
/* Arguments */
program
.description('Compiles Keyman LDML keyboards')
.version(KEYMAN_VERSION.VERSION_WITH_TAG)
.arguments('<infile>')
.action((infile:any) => inputFilename = infile)
.option('-d, --debug', 'Include debug information in output')
.option('--no-compiler-version', 'Exclude compiler version metadata from output')
.option('-o, --out-file <filename>', 'where to save the resulting .kmx file')
.option('-T, --test-data <filename>', 'Convert keyboard test .xml to .json');
.description('Keyman Developer Command Line Interface')
.version(KEYMAN_VERSION.VERSION_WITH_TAG);
declareBuild(program);
declareBuildTestData(program);
/*
program
.command('clean');
program
.command('copy');
program
.command('rename');
program
.command('generate');
program
.command('import');
program
.command('test');
program
.command('publish');
*/
program.parse(process.argv);
// Deal with input arguments:
if (!inputFilename) {
exitDueToUsageError('Must provide a LDML keyboard .xml source file.');
}
function exitDueToUsageError(message: string): never {
console.error(`${program._name}: ${message}`);
console.error();
program.outputHelp();
return process.exit(64); // TODO: SysExits.EX_USAGE
}
/**
* Concrete implementation for CLI use
*/
class NodeCompilerCallbacks implements CompilerCallbacks {
loadFile(baseFilename: string, filename: string | URL): Buffer {
// TODO: translate filename based on the baseFilename
try {
return fs.readFileSync(filename);
} catch(e) {
if (e.code === 'ENOENT') {
return null;
} else {
throw e;
}
}
}
reportMessage(event: CompilerEvent): void {
console.log(kmc.CompilerMessages.severityName(event.code) + ' ' + event.code.toString(16) + ': ' + event.message);
}
loadLdmlKeyboardSchema(): Buffer {
let schemaPath = new URL('ldml-keyboard.schema.json', import.meta.url);
return fs.readFileSync(schemaPath);
}
loadLdmlKeyboardTestSchema(): Buffer {
let schemaPath = new URL('ldml-keyboardtest.schema.json', import.meta.url);
return fs.readFileSync(schemaPath);
}
loadKvksJsonSchema(): Buffer {
let schemaPath = new URL('kvks.schema.json', import.meta.url);
return fs.readFileSync(schemaPath);
}
}
function compileKeyboard(inputFilename: string, options: kmc.CompilerOptions): [Uint8Array,Uint8Array,Uint8Array] {
const c : CompilerCallbacks = new NodeCompilerCallbacks();
const k = new kmc.Compiler(c, options);
let source = k.load(inputFilename);
if(!source) {
return [null, null, null];
}
if(!k.validate(source)) {
return [null, null, null];
}
let kmx = k.compile(source);
if(!kmx) {
return [null, null, null];
}
// In order for the KMX file to be loaded by non-KMXPlus components, it is helpful
// to duplicate some of the metadata
kmc.KMXPlusMetadataCompiler.addKmxMetadata(kmx.kmxplus, kmx.keyboard, options);
// Use the builder to generate the binary output file
const builder = new kmc.KMXBuilder(kmx, options.debug);
const kmx_binary = builder.compile();
const vkcompiler = new kmc.VisualKeyboardCompiler();
const vk = vkcompiler.compile(source);
const writer = new KvkFileWriter();
const kvk_binary = writer.write(vk);
// Note: we could have a step of generating source files here
// KvksFileWriter()...
// const tlcompiler = new kmc.TouchLayoutCompiler();
// const tl = tlcompiler.compile(source);
// const tlwriter = new TouchLayoutFileWriter();
const kmwcompiler = new kmc.KeymanWebCompiler(options);
const kmw_string = kmwcompiler.compile(inputFilename, source);
const encoder = new TextEncoder();
const kmw_binary = encoder.encode(kmw_string);
return [kmx_binary, kvk_binary, kmw_binary];
}
function loadTestData(inputFilename: string, options: kmc.CompilerOptions): LDMLKeyboardTestDataXMLSourceFile {
const c : CompilerCallbacks = new NodeCompilerCallbacks();
const k = new kmc.Compiler(c, options);
let source = k.loadTestData(inputFilename);
if(!source) {
return null;
}
return source;
}
let options: kmc.CompilerOptions = {
debug: program.debug ?? false,
addCompilerVersion: program.compilerVersion ?? true
}
if (program.testData) {
let testData = loadTestData(inputFilename, options);
if (testData) {
const fileBaseName = program.testData ?? inputFilename;
const outFileBase = path.basename(fileBaseName, path.extname(fileBaseName));
const outFileDir = path.dirname(fileBaseName);
const outFileJson = path.join(outFileDir, outFileBase + '.json');
console.log(`Writing JSON test data to ${outFileJson}`);
fs.writeFileSync(outFileJson, JSON.stringify(testData, null, ' '));
} else {
console.error(`An error occurred loading test data ${inputFilename}`);
process.exit(1);
}
} else {
// TODO-LDML: consider hardware vs touch -- touch-only layout will not have a .kvk
// Compile:
let [kmx,kvk,kmw] = compileKeyboard(inputFilename, options);
// Output:
const fileBaseName = program.outFile ?? inputFilename;
const outFileBase = path.basename(fileBaseName, path.extname(fileBaseName));
const outFileDir = path.dirname(fileBaseName);
if(kmx && kvk) {
const outFileKmx = path.join(outFileDir, outFileBase + '.kmx');
console.log(`Writing compiled keyboard to ${outFileKmx}`);
fs.writeFileSync(outFileKmx, kmx);
const outFileKvk = path.join(outFileDir, outFileBase + '.kvk');
console.log(`Writing compiled visual keyboard to ${outFileKvk}`);
fs.writeFileSync(outFileKvk, kvk);
} else {
console.error(`An error occurred compiling ${inputFilename}`);
process.exit(1);
}
if(kmw) {
const outFileKmw = path.join(outFileDir, outFileBase + '.js');
console.log(`Writing compiled js keyboard to ${outFileKmw}`);
fs.writeFileSync(outFileKmw, kmw);
}
}

View file

@ -0,0 +1,36 @@
import * as fs from 'fs';
import * as kmc from '@keymanapp/kmc-keyboard';
import { CompilerCallbacks, CompilerEvent } from '@keymanapp/common-types';
/**
* Concrete implementation for CLI use
*/
export class NodeCompilerCallbacks implements CompilerCallbacks {
loadFile(baseFilename: string, filename: string | URL): Buffer {
// TODO: translate filename based on the baseFilename
try {
return fs.readFileSync(filename);
} catch (e) {
if (e.code === 'ENOENT') {
return null;
} else {
throw e;
}
}
}
reportMessage(event: CompilerEvent): void {
console.log(kmc.CompilerMessages.severityName(event.code) + ' ' + event.code.toString(16) + ': ' + event.message);
}
loadLdmlKeyboardSchema(): Buffer {
let schemaPath = new URL('ldml-keyboard.schema.json', import.meta.url);
return fs.readFileSync(schemaPath);
}
loadLdmlKeyboardTestSchema(): Buffer {
let schemaPath = new URL('ldml-keyboardtest.schema.json', import.meta.url);
return fs.readFileSync(schemaPath);
}
loadKvksJsonSchema(): Buffer {
let schemaPath = new URL('kvks.schema.json', import.meta.url);
return fs.readFileSync(schemaPath);
}
}