From 1da26008189bb31916c4f7278df5bc55025dd4cf Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 8 Jun 2026 11:26:13 +0200 Subject: [PATCH 1/2] feat(developer): run project validation from IDE When option 'SkipMetadataFiles' is not set in the project (inverse of 'Build metadata files for Keyman Cloud' in Project Settings dialog), then will run the validation step and generate .keyboard_info / .model_info from the IDE. Also add corresponding log messages (and add formatting for some others that were missing). Fixes: #16068 Test-bot: skip --- .../docs/help/reference/kmc/cli/reference.md | 7 ++++ developer/src/kmc/src/commands/build.ts | 1 + .../buildClasses/BuildKeyboardInfo.ts | 1 + .../commands/buildClasses/BuildModelInfo.ts | 1 + .../src/commands/buildClasses/BuildProject.ts | 39 ++++++++++++------- .../src/messages/infrastructureMessages.ts | 21 ++++++++++ .../src/kmc/src/util/NodeCompilerCallbacks.ts | 4 ++ .../kmc/src/util/extendedCompilerOptions.ts | 7 ++++ .../Keyman.Developer.System.KmcWrapper.pas | 34 ++++++++++++++++ developer/src/tike/main/UfrmMessages.pas | 18 +++++++-- ...er.System.Project.kpsProjectFileAction.pas | 2 + 11 files changed, 119 insertions(+), 16 deletions(-) diff --git a/developer/docs/help/reference/kmc/cli/reference.md b/developer/docs/help/reference/kmc/cli/reference.md index 1d5ed9fc20..662d85a0a3 100644 --- a/developer/docs/help/reference/kmc/cli/reference.md +++ b/developer/docs/help/reference/kmc/cli/reference.md @@ -198,6 +198,13 @@ The following parameters are available: project (which can also be controlled at a project level with the `skipMetadataFiles` option). This option is only valid for compiling projects. +`--publish-only` + +: Skip building component files within a project, and only run the validation + step. Must be combined with `--for-publishing` in order to have an effect. + This option is only valid for compiling projects, and is mostly intended for + use by the IDE. Will fail if build artifacts for components are not present. + ### Examples ```shell diff --git a/developer/src/kmc/src/commands/build.ts b/developer/src/kmc/src/commands/build.ts index 20f1092a9f..7e21f99f9c 100644 --- a/developer/src/kmc/src/commands/build.ts +++ b/developer/src/kmc/src/commands/build.ts @@ -35,6 +35,7 @@ export function declareBuild(program: Command) { buildCommand.command('file [infile...]', {isDefault: true}) .description(`Compile one or more source files or projects ('file' subcommand is default).`) .option('--for-publishing', 'Verify that project meets @keymanapp repository requirements') + .option('--publish-only', 'Only run the for-publishing validation, skip all other build steps') .addHelpText('after', ` Supported file types: * folder: Keyman project in folder diff --git a/developer/src/kmc/src/commands/buildClasses/BuildKeyboardInfo.ts b/developer/src/kmc/src/commands/buildClasses/BuildKeyboardInfo.ts index 3109038a3a..d3d42f9ce6 100644 --- a/developer/src/kmc/src/commands/buildClasses/BuildKeyboardInfo.ts +++ b/developer/src/kmc/src/commands/buildClasses/BuildKeyboardInfo.ts @@ -49,6 +49,7 @@ export class BuildKeyboardInfo extends BuildActivity { sourcePath: calculateSourcePath(infile), lastCommitDate, forPublishing: !!options.forPublishing, + publishOnly: !!options.publishOnly, }; // Note: should we always ignore the passed-in output filename for .keyboard_info? const outputFilename = project.getOutputFilePath(KeymanFileTypes.Binary.KeyboardInfo); diff --git a/developer/src/kmc/src/commands/buildClasses/BuildModelInfo.ts b/developer/src/kmc/src/commands/buildClasses/BuildModelInfo.ts index f6a44ced5f..7f641a9b8e 100644 --- a/developer/src/kmc/src/commands/buildClasses/BuildModelInfo.ts +++ b/developer/src/kmc/src/commands/buildClasses/BuildModelInfo.ts @@ -75,6 +75,7 @@ export class BuildModelInfo extends BuildActivity { kpsFilename: project.resolveInputFilePath(kps), lastCommitDate, forPublishing: !!options.forPublishing, + publishOnly: !!options.publishOnly, }; // Note: should we always ignore the passed-in output filename for .model_info? diff --git a/developer/src/kmc/src/commands/buildClasses/BuildProject.ts b/developer/src/kmc/src/commands/buildClasses/BuildProject.ts index 82e1347aa5..76fdfe1d04 100644 --- a/developer/src/kmc/src/commands/buildClasses/BuildProject.ts +++ b/developer/src/kmc/src/commands/buildClasses/BuildProject.ts @@ -51,15 +51,17 @@ class ProjectBuilder { } } - // Go through the various file types and build them - for(const builder of buildActivities) { - if(builder.sourceExtension == KeymanFileTypes.Source.Project) { - // We don't support nested projects - continue; - } + if(!this.options.publishOnly) { + // Go through the various file types and build them + for(const builder of buildActivities) { + if(builder.sourceExtension == KeymanFileTypes.Source.Project) { + // We don't support nested projects + continue; + } - if(!await this.buildProjectTargets(builder)) { - return false; + if(!await this.buildProjectTargets(builder)) { + return false; + } } } @@ -113,7 +115,11 @@ class ProjectBuilder { const buildFilename = path.relative(process.cwd(), infile).replace(/\\/g, '/'); const callbacks = new CompilerFileCallbacks(buildFilename, options, this.callbacks); - callbacks.reportMessage(InfrastructureMessages.Info_BuildingFile({filename: infile, relativeFilename:buildFilename})); + if(activity.sourceExtension == KeymanFileTypes.Source.Project) { + callbacks.reportMessage(InfrastructureMessages.Info_ValidatingProject({filename: infile, relativeFilename:buildFilename})); + } else { + callbacks.reportMessage(InfrastructureMessages.Info_BuildingFile({filename: infile, relativeFilename:buildFilename})); + } fs.mkdirSync(path.dirname(outfile), {recursive:true}); @@ -123,12 +129,19 @@ class ProjectBuilder { // note: command line option here, if set, overrides project setting result = result && !callbacks.hasFailureMessage(this.options.compilerWarningsAsErrors ?? this.project.options.compilerWarningsAsErrors); - if(result) { - callbacks.reportMessage(InfrastructureMessages.Info_FileBuiltSuccessfully({filename: infile, relativeFilename:buildFilename})); + if(activity.sourceExtension == KeymanFileTypes.Source.Project) { + if(result) { + callbacks.reportMessage(InfrastructureMessages.Info_ProjectValidatedSuccessfully({filename: infile, relativeFilename:buildFilename})); + } else { + callbacks.reportMessage(InfrastructureMessages.Info_ProjectNotValidatedSuccessfully({filename: infile, relativeFilename:buildFilename})); + } } else { - callbacks.reportMessage(InfrastructureMessages.Info_FileNotBuiltSuccessfully({filename: infile, relativeFilename: buildFilename})); + if(result) { + callbacks.reportMessage(InfrastructureMessages.Info_FileBuiltSuccessfully({filename: infile, relativeFilename:buildFilename})); + } else { + callbacks.reportMessage(InfrastructureMessages.Info_FileNotBuiltSuccessfully({filename: infile, relativeFilename: buildFilename})); + } } - return result; } diff --git a/developer/src/kmc/src/messages/infrastructureMessages.ts b/developer/src/kmc/src/messages/infrastructureMessages.ts index d384aa7d86..338af89073 100644 --- a/developer/src/kmc/src/messages/infrastructureMessages.ts +++ b/developer/src/kmc/src/messages/infrastructureMessages.ts @@ -197,5 +197,26 @@ export class InfrastructureMessages { `Failed to generate new project '${def(o.id)}'.`, )}); + // For this message, we override the filename with the passed-in file. A bit of a hack but does the job + static INFO_ValidatingProject = SevInfo | 0x0029; + static Info_ValidatingProject = (o:{filename:string,relativeFilename:string}) => ({filename:o.filename, ...m( + this.INFO_ValidatingProject, + `Validating ${def(o.relativeFilename)}`, + )}); + + // For this message, we override the filename with the passed-in file. A bit of a hack but does the job + static INFO_ProjectValidatedSuccessfully = SevInfo | 0x002A; + static Info_ProjectValidatedSuccessfully = (o:{filename:string,relativeFilename:string}) => ({filename:o.filename, ...m( + this.INFO_ProjectValidatedSuccessfully, + `${def(o.relativeFilename)} validated successfully.`, + )}); + + // For this message, we override the filename with the passed-in file. A bit of a hack but does the job + static INFO_ProjectNotValidatedSuccessfully = SevInfo | 0x002B; + static Info_ProjectNotValidatedSuccessfully = (o:{filename:string,relativeFilename:string}) => ({filename:o.filename, ...m( + this.INFO_ProjectNotValidatedSuccessfully, + `${def(o.relativeFilename)} failed to validate.` + )}); + } diff --git a/developer/src/kmc/src/util/NodeCompilerCallbacks.ts b/developer/src/kmc/src/util/NodeCompilerCallbacks.ts index a92314aa5f..68209d95ce 100644 --- a/developer/src/kmc/src/util/NodeCompilerCallbacks.ts +++ b/developer/src/kmc/src/util/NodeCompilerCallbacks.ts @@ -242,22 +242,26 @@ export class NodeCompilerCallbacks implements CompilerCallbacks { * We treat a few certain infrastructure messages with special colours * @param event * @returns + * Keep in sync with: UfrmMessages.pas, TfrmMessages.Add */ messageSpecialColor(event: CompilerEvent) { switch(event.code) { case InfrastructureMessages.INFO_BuildingFile: case InfrastructureMessages.INFO_CopyingProject: case InfrastructureMessages.INFO_GeneratingProject: + case InfrastructureMessages.INFO_ValidatingProject: return color.whiteBright; case InfrastructureMessages.INFO_FileNotBuiltSuccessfully: case InfrastructureMessages.INFO_ProjectNotBuiltSuccessfully: case InfrastructureMessages.INFO_ProjectNotCopiedSuccessfully: case InfrastructureMessages.INFO_ProjectNotGeneratedSuccessfully: + case InfrastructureMessages.INFO_ProjectNotValidatedSuccessfully: return color.red; case InfrastructureMessages.INFO_FileBuiltSuccessfully: case InfrastructureMessages.INFO_ProjectBuiltSuccessfully: case InfrastructureMessages.INFO_ProjectCopiedSuccessfully: case InfrastructureMessages.INFO_ProjectGeneratedSuccessfully: + case InfrastructureMessages.INFO_ProjectValidatedSuccessfully: return color.green; } return null; diff --git a/developer/src/kmc/src/util/extendedCompilerOptions.ts b/developer/src/kmc/src/util/extendedCompilerOptions.ts index 53f68288f0..b94be88419 100644 --- a/developer/src/kmc/src/util/extendedCompilerOptions.ts +++ b/developer/src/kmc/src/util/extendedCompilerOptions.ts @@ -9,6 +9,12 @@ export interface ExtendedCompilerOptions extends CompilerOptions { * MIT */ forPublishing?: boolean; + /** + * Do not build components, just do the publish phase for the project. This is + * used mostly by Keyman Developer IDE, which will run the .keyboard_info / + * .package_info validation and compilation step if skipMetadataFiles is true. + */ + publishOnly?: boolean; /** * Overrides for message reporting */ @@ -198,6 +204,7 @@ export function commanderOptionsToCompilerOptions(options: any, callbacks: Compi warnDeprecatedCode: options.warnDeprecatedCode, // ExtendedOptions forPublishing: options.forPublishing, + publishOnly: options.publishOnly, messageOverrides: overrides, } } diff --git a/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas b/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas index 168ea8c77d..f64e961b15 100644 --- a/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas +++ b/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas @@ -17,6 +17,7 @@ type function Run(const parameters: TArray; const path, logFilename: string; Callback: TUtilExecuteCallbackEvent): Boolean; public function Compile(ProjectFile: TProjectFile; const infile, outfile: string; debug: Boolean; Callback: TUtilExecuteCallbackEvent = nil): Boolean; + function CompileForPublishing(ProjectFile: TProjectFile; const projectFilename: string; debug: Boolean; Callback: TUtilExecuteCallbackEvent = nil): Boolean; function Copy(const source, dest, cwd: string; relocateExternal: Boolean; Callback: TUtilExecuteCallbackEvent = nil): Boolean; end; @@ -39,6 +40,39 @@ uses { TKmcWrapper } +function TKmcWrapper.CompileForPublishing( + ProjectFile: TProjectFile; + const projectFilename: string; + debug: Boolean; + Callback: TUtilExecuteCallbackEvent +): Boolean; +var + cmdline: TArray; +begin + cmdline := [ + 'build', + '--log-format', 'tsv', + '--log-level', 'info', + '--for-publishing', + '--publish-only', + projectFilename + ]; + + if Assigned(FGlobalProject) and (FGlobalProject.Options.CompilerWarningsAsErrors) then + cmdline := cmdline + ['--compiler-warnings-as-errors'] + else + cmdline := cmdline + ['--no-compiler-warnings-as-errors']; + + if not FGlobalProject.Options.WarnDeprecatedCode then + cmdline := cmdline + ['--no-warn-deprecated-code']; + + if debug then + cmdline := cmdline + ['--debug']; + + Result := Run(cmdline, ExtractFileDir(projectFilename), projectFilename, Callback); +end; + + function TKmcWrapper.Compile( ProjectFile: TProjectFile; const infile: string; diff --git a/developer/src/tike/main/UfrmMessages.pas b/developer/src/tike/main/UfrmMessages.pas index 9a76254c96..924bec9acd 100644 --- a/developer/src/tike/main/UfrmMessages.pas +++ b/developer/src/tike/main/UfrmMessages.pas @@ -170,6 +170,12 @@ const // ABGR - text on white bg, Colors similar to Light+ VSCode color theme INFO_FileNotBuiltSuccessfully = NAMESPACE_Infrastructure or $0007; INFO_ProjectBuiltSuccessfully = NAMESPACE_Infrastructure or $000B; INFO_ProjectNotBuiltSuccessfully = NAMESPACE_Infrastructure or $000C; + INFO_ProjectCopiedSuccessfully = NAMESPACE_Infrastructure or $0024; + INFO_ProjectNotCopiedSuccessfully = NAMESPACE_Infrastructure or $0025; + INFO_ProjectGeneratedSuccessfully = NAMESPACE_Infrastructure or $0027; + INFO_ProjectNotGeneratedSuccessfully = NAMESPACE_Infrastructure or $0028; + INFO_ProjectValidatedSuccessfully = NAMESPACE_Infrastructure or $002A; + INFO_ProjectNotValidatedSuccessfully = NAMESPACE_Infrastructure or $002B; Segment_Filename = 0; Segment_Filename_Separator = 1; @@ -254,12 +260,18 @@ begin FColor := clBlack; FTextColor := Color_Text; - // Override formatting for 4 known messages + // Override formatting for known messages -- keep in sync with messageSpecialColor() in NodeCompilerCallbacks.ts if (MsgCode = INFO_FileBuiltSuccessfully) or - (MsgCode = INFO_ProjectBuiltSuccessfully) then + (MsgCode = INFO_ProjectBuiltSuccessfully) or + (MsgCode = INFO_ProjectCopiedSuccessfully) or + (MsgCode = INFO_ProjectGeneratedSuccessfully) or + (MsgCode = INFO_ProjectValidatedSuccessfully) then state := plsSuccess else if (MsgCode = INFO_FileNotBuiltSuccessfully) or - (MsgCode = INFO_ProjectNotBuiltSuccessfully) then + (MsgCode = INFO_ProjectNotBuiltSuccessfully) or + (MsgCode = INFO_ProjectNotCopiedSuccessfully) or + (MsgCode = INFO_ProjectNotGeneratedSuccessfully) or + (MsgCode = INFO_ProjectNotValidatedSuccessfully) then state := plsFailure; case state of diff --git a/developer/src/tike/project/Keyman.Developer.System.Project.kpsProjectFileAction.pas b/developer/src/tike/project/Keyman.Developer.System.Project.kpsProjectFileAction.pas index c7a7cafcc5..94cb28c151 100644 --- a/developer/src/tike/project/Keyman.Developer.System.Project.kpsProjectFileAction.pas +++ b/developer/src/tike/project/Keyman.Developer.System.Project.kpsProjectFileAction.pas @@ -29,6 +29,8 @@ begin w := TKmcWrapper.Create; try Result := w.Compile(Self, FileName, TargetFilename, False); + if not OwnerProject.Options.SkipMetadataFiles then + Result := Result and w.CompileForPublishing(Self, OwnerProject.FileName, False); // TODO(lowpri): FDebug flag finally w.Free; From a005e45d25c438d55d577ef3a22dbc22a7f4ff3b Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 9 Jun 2026 20:58:12 +0200 Subject: [PATCH 2/2] chore: Apply suggestions from code review Co-authored-by: rc-swag <58423624+rc-swag@users.noreply.github.com> --- .../src/tike/compile/Keyman.Developer.System.KmcWrapper.pas | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas b/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas index f64e961b15..6c33760ffc 100644 --- a/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas +++ b/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas @@ -63,7 +63,7 @@ begin else cmdline := cmdline + ['--no-compiler-warnings-as-errors']; - if not FGlobalProject.Options.WarnDeprecatedCode then + if Assigned(FGlobalProject) and (not FGlobalProject.Options.WarnDeprecatedCode) then cmdline := cmdline + ['--no-warn-deprecated-code']; if debug then