Merge pull request #16076 from keymanapp/feat/developer/16068-run-project-validation-from-tike

feat(developer): run project validation from IDE
This commit is contained in:
Marc Durdin 2026-06-15 22:17:10 +10:00 committed by GitHub
commit f52f383d93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 119 additions and 16 deletions

View file

@ -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

View file

@ -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

View file

@ -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);

View file

@ -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?

View file

@ -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;
}

View file

@ -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.`
)});
}

View file

@ -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;

View file

@ -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,
}
}

View file

@ -17,6 +17,7 @@ type
function Run(const parameters: TArray<string>; 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<string>;
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 Assigned(FGlobalProject) and (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;

View file

@ -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

View file

@ -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;