* Copyright (c) 2025-2026 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as path from 'path';
import * as fs from 'fs';
import { initKoalaModules } from '../init/init_koala_modules';
import {
BuildConfig,
PluginsConfig,
CompileJobInfo,
FileInfo,
DeclgenV1JobInfo,
CompileJobType,
AliasConfig,
ArkTS,
KoalaModule,
JobContentType,
BUILD_MODE
} from '../types';
import {
Logger,
LogDataFactory
} from '../logger';
import {
buildDeclgenOutputPath,
changeFileExtension,
createFileIfNotExists,
ensurePathExists,
validatePathLength,
} from './utils';
import {
DECL_ETS_SUFFIX,
DECL_TS_SUFFIX,
STATIC_RECORD_FILE,
STATIC_RECORD_FILE_CONTENT,
ENABLE_DECLARATION_BARRIER,
MERGED_INTERMEDIATE_FILE
} from '../pre_define';
import {
PluginDriver,
PluginHook
} from '../plugins/plugins_driver';
import { KitImportTransformer } from '../plugins/KitImportTransformer';
import { ErrorCode, DriverError } from '../util/error';
import {
BS_PERF_FILE_NAME,
RecordEvent,
StatisticsRecorder,
} from '../util/statsRecorder'
enum Ets2pandaEvent {
CREATE_INSTANCE = 'Create instance',
PARSE = 'Parse stage',
PLUGIN_PARSE = 'Parse plugins stage',
DECLGEN = 'Declgen stage',
CHECK = 'Checker stage',
PLUGIN_CHECK = 'Checker plugins stage',
EMIT = 'Emit binary stage',
DESTROY_INSTANCE = 'Destroy instance'
}
function formEvent(event: Ets2pandaEvent): string {
return '[Ets2panda] ' + event;
}
export class Ets2panda {
private static instance?: Ets2panda;
private readonly logger: Logger = Logger.getInstance();
private readonly plugins: PluginsConfig;
private readonly buildSdkPath: string;
private readonly aliasConfig: Record<string, Record<string, AliasConfig>>;
private readonly declgenV2OutDir: string;
private readonly cacheDir: string;
private readonly pluginDriver: PluginDriver = PluginDriver.getInstance();
private readonly recordType?: 'ON' | 'OFF';
private readonly projectRootPath: string;
private readonly debugBuild: boolean = false;
private readonly dumpPerf: boolean = false;
private readonly declFileNameCachePath?: string;
private readonly koalaModule: KoalaModule;
private constructor(buildConfig: BuildConfig) {
this.koalaModule = initKoalaModules(buildConfig);
this.plugins = buildConfig.plugins;
this.buildSdkPath = buildConfig.buildSdkPath;
this.aliasConfig = buildConfig.aliasConfig;
this.cacheDir = buildConfig.cachePath;
this.recordType = buildConfig.recordType;
this.declgenV2OutDir = buildConfig.declgenV2OutPath;
this.pluginDriver.initPlugins(buildConfig);
this.projectRootPath = buildConfig.projectRootPath;
this.debugBuild = (buildConfig.buildMode === BUILD_MODE.DEBUG);
this.dumpPerf = buildConfig.dumpPerf ?? false;
this.declFileNameCachePath = buildConfig.declFileNameCacheConfig?.nameCachePath;
}
public static getInstance(buildConfig?: BuildConfig): Ets2panda {
if (!Ets2panda.instance) {
if (!buildConfig) {
throw new Error('buildConfig is required for the first Ets2panda instantiation.');
}
Ets2panda.instance = new Ets2panda(buildConfig);
}
return Ets2panda.instance;
}
public static destroyInstance(): void {
Ets2panda.instance = undefined;
}
public initalize(): void {
if (this.koalaModule === undefined) {
throw new Error('KoalaModule is not initialized');
}
const arkts: ArkTS = this.koalaModule.arkts;
arkts.memInitialize();
}
public finalize(): void {
if (this.koalaModule === undefined) {
throw new Error('KoalaModule is not initialized');
}
const arkts: ArkTS = this.koalaModule.arkts;
arkts.memFinalize();
}
private transformImportStatementsWithAliasConfig(): void {
if (this.plugins === undefined) {
return;
}
const { arkts, arktsGlobal } = this.koalaModule;
let ast = arkts.EtsScript.fromContext();
if (this.aliasConfig && Object.keys(this.aliasConfig).length > 0) {
this.logger.printDebug('Transforming import statements with alias config');
let transformAst = new KitImportTransformer(
arkts,
arktsGlobal.compilerContext.program,
this.buildSdkPath,
this.aliasConfig
).transform(ast);
this.pluginDriver.getPluginContext().setArkTSAst(transformAst);
} else {
this.pluginDriver.getPluginContext().setArkTSAst(ast);
}
}
private formCompileCliCmd(
job: CompileJobInfo,
incremental: boolean = false
): string[] {
const ets2pandaCmd: string[] = [
'_',
'--extension',
'ets',
'--arktsconfig',
job.arktsConfig
]
ets2pandaCmd.push('--simultaneous');
if ((job.contentType === JobContentType.CLUSTER) && incremental) {
ets2pandaCmd.push('--incremental');
}
if ((job.jobType & CompileJobType.DECL) !== 0) {
ets2pandaCmd.push('--emit-declaration');
if (this.declFileNameCachePath) {
ets2pandaCmd.push(`--generate-decl:nameCachePath=${this.declFileNameCachePath}`);
}
}
if (job.contentType === JobContentType.FILE) {
ets2pandaCmd.push('--output')
ets2pandaCmd.push((job.content as FileInfo).output)
} else if (job.contentType === JobContentType.CLUSTER && !incremental) {
ets2pandaCmd.push('--output')
ets2pandaCmd.push(path.resolve(this.cacheDir, MERGED_INTERMEDIATE_FILE))
}
if (this.debugBuild) {
ets2pandaCmd.push('--debug-info');
ets2pandaCmd.push('--opt-level=0');
}
ets2pandaCmd.push('--ets-warnings:diagnostic-format=build-system');
if (job.contentType === JobContentType.FILE) {
ets2pandaCmd.push((job.content as FileInfo).input)
}
if (this.dumpPerf) {
ets2pandaCmd.push('--dump-perf-metrics');
}
return ets2pandaCmd
}
private formDeclgenCliCmd(
job: DeclgenV1JobInfo,
): string[] {
const ets2pandaCmd: string[] = [
'_',
'--extension',
'ets',
'--arktsconfig',
job.arktsConfig
]
ets2pandaCmd.push('--simultaneous')
ets2pandaCmd.push('--ets-warnings:base-path=' + this.projectRootPath);
if (job.contentType === JobContentType.FILE) {
ets2pandaCmd.push((job.content as FileInfo).input)
}
if (this.declFileNameCachePath) {
ets2pandaCmd.push(`--generate-decl:nameCachePath=${this.declFileNameCachePath}`);
}
return ets2pandaCmd
}
public compile(
jobId: string,
job: CompileJobInfo,
incremental: boolean = false,
declGenCb?: () => void,
compAbcCb?: () => void
): void {
let statsRecorder = new StatisticsRecorder(
path.resolve(this.cacheDir, BS_PERF_FILE_NAME),
this.recordType,
`Compile. Job id: ${jobId.slice(0, 5)}`
);
this.logger.printDebug(`Ets2panda.compile Job: ${jobId}`)
const ets2pandaCmd: string[] = this.formCompileCliCmd(job, incremental);
this.logger.printDebug('ets2pandaCmd: ' + ets2pandaCmd.join(' '));
let inputs: string[];
if (job.contentType === JobContentType.CLUSTER) {
inputs = (job.content as FileInfo[]).map((fi: FileInfo) => fi.input);
} else {
inputs = [(job.content as FileInfo).input];
}
let { arkts, arktsGlobal } = this.koalaModule;
if (!ENABLE_DECLARATION_BARRIER) {
declGenCb?.();
}
try {
statsRecorder.record(formEvent(Ets2pandaEvent.CREATE_INSTANCE));
arktsGlobal.config = arkts.Config.create(ets2pandaCmd).peer;
arktsGlobal.compilerContext = arkts.Context.createContextSimultaneousMode(inputs);
this.pluginDriver.getPluginContext().setArkTSProgram(arktsGlobal.compilerContext.program);
this.logger.printInfo('[Ets2panda] Created instance');
statsRecorder.record(formEvent(Ets2pandaEvent.PARSE));
arkts.proceedToState(arkts.Es2pandaContextState.ES2PANDA_STATE_PARSED, arktsGlobal.compilerContext.peer);
this.logger.printInfo('[Ets2panda] Parsed');
if (job.contentType === JobContentType.CLUSTER) {
(job.content as FileInfo[]).forEach((fi: FileInfo) => {
fi.output = arkts.formOutputPathForFile(fi.input);
validatePathLength(fi.output, 'Output file path');
})
} else {
const fi: FileInfo = job.content as FileInfo;
fi.output = arkts.formOutputPathForFile(fi.input);
validatePathLength(fi.output, 'Output file path');
}
statsRecorder.record(formEvent(Ets2pandaEvent.PLUGIN_PARSE));
this.transformImportStatementsWithAliasConfig()
this.pluginDriver.runPluginHook(PluginHook.PARSED);
this.logger.printInfo('[Ets2panda] Parser plugins finished');
statsRecorder.record(formEvent(Ets2pandaEvent.CHECK));
arkts.proceedToState(arkts.Es2pandaContextState.ES2PANDA_STATE_CHECKED, arktsGlobal.compilerContext.peer);
this.logger.printInfo('[Ets2panda] Checked');
if (job.jobType & CompileJobType.DECL) {
statsRecorder.record(formEvent(Ets2pandaEvent.DECLGEN));
const contents = job.contentType === JobContentType.CLUSTER
? job.content as FileInfo[]
: [job.content as FileInfo];
for (const fi of contents) {
const relative: string = changeFileExtension(
path.relative(job.moduleRoot, fi.input),
DECL_ETS_SUFFIX
)
const declEtsOutputPath: string = path.resolve(
this.declgenV2OutDir,
relative
)
ensurePathExists(declEtsOutputPath);
validatePathLength(declEtsOutputPath, 'Declaration output path');
arkts.generateStaticDeclarationsFromContext(declEtsOutputPath);
this.logger.printInfo(`[Ets2panda] Generated 1.2 decl file for ${fi.input}`)
}
if (ENABLE_DECLARATION_BARRIER) {
declGenCb?.();
}
}
if (job.jobType & CompileJobType.ABC) {
statsRecorder.record(formEvent(Ets2pandaEvent.PLUGIN_CHECK));
let ast = arkts.EtsScript.fromContext();
this.pluginDriver.getPluginContext().setArkTSAst(ast);
this.pluginDriver.runPluginHook(PluginHook.CHECKED);
this.logger.printInfo('[Ets2panda] Checker plugins finished');
statsRecorder.record(formEvent(Ets2pandaEvent.EMIT));
arkts.proceedToState(
arkts.Es2pandaContextState.ES2PANDA_STATE_ASM_GENERATED,
arktsGlobal.compilerContext.peer
);
arktsGlobal.es2panda._FreeCompilerPartMemory(arktsGlobal.compilerContext.peer);
arkts.proceedToState(
arkts.Es2pandaContextState.ES2PANDA_STATE_BIN_GENERATED,
arktsGlobal.compilerContext.peer
);
compAbcCb?.();
this.logger.printInfo(`[Ets2panda] Compiled abc file for cycle ${jobId}`)
}
} catch (error) {
if (error instanceof DriverError) {
throw error;
}
if (error instanceof Error) {
throw new DriverError(
LogDataFactory.newInstance(
ErrorCode.BUILDSYSTEM_COMPILE_ABC_FAIL,
'Compile abc files failed.',
error.message
)
);
}
} finally {
statsRecorder.record(formEvent(Ets2pandaEvent.DESTROY_INSTANCE));
this.pluginDriver.runPluginHook(PluginHook.CLEAN);
if (arktsGlobal.compilerContext) {
arktsGlobal.es2panda._DestroyContext(arktsGlobal.compilerContext.peer);
}
arkts.destroyConfig(arktsGlobal.config);
statsRecorder.record(RecordEvent.END);
statsRecorder.writeSumSingle();
}
}
public declgenV1(
jobInfo: DeclgenV1JobInfo,
skipDeclCheck: boolean,
genDeclAnnotations: boolean
): void {
const contentFiles: FileInfo[] = jobInfo.contentType === JobContentType.FILE
? [jobInfo.content as FileInfo]
: jobInfo.content as FileInfo[];
const inputFiles: string[] = contentFiles.map(fi => fi.input);
const outputDeclEtsPaths: string[] = [];
const outputEtsPaths: string[] = [];
for (const file of inputFiles) {
const { declEtsOutputPath, glueCodeOutputPath } = buildDeclgenOutputPath(
file, jobInfo.fileToModuleMap[file], this.cacheDir
);
outputDeclEtsPaths.push(declEtsOutputPath);
outputEtsPaths.push(glueCodeOutputPath);
validatePathLength(declEtsOutputPath, 'Declaration file path');
validatePathLength(glueCodeOutputPath, 'Bridge code file path');
}
const firstFileModule = jobInfo.fileToModuleMap[inputFiles[0]];
const staticRecordPath = path.join(
firstFileModule.declgenV1OutPath!,
STATIC_RECORD_FILE
)
validatePathLength(staticRecordPath, 'Static record file path');
const declEtsOutputDir = path.dirname(outputDeclEtsPaths[0]);
const staticRecordRelativePath = changeFileExtension(
path.relative(declEtsOutputDir, staticRecordPath).replace(/\\/g, '\/'),
'',
DECL_TS_SUFFIX
);
createFileIfNotExists(staticRecordPath, STATIC_RECORD_FILE_CONTENT);
let ets2pandaCmd = this.formDeclgenCliCmd(jobInfo)
this.logger.printDebug(`ets2panda cmd: ${ets2pandaCmd.join(' ')}`)
let { arkts, arktsGlobal } = this.koalaModule;
let declgen: any = null;
try {
arktsGlobal.config = arkts.Config.create(ets2pandaCmd).peer;
arktsGlobal.compilerContext = arkts.Context.createContextSimultaneousMode(
inputFiles
);
this.pluginDriver.getPluginContext().setArkTSProgram(arktsGlobal.compilerContext.program);
arkts.proceedToState(arkts.Es2pandaContextState.ES2PANDA_STATE_PARSED, arktsGlobal.compilerContext.peer, skipDeclCheck);
declgen = arkts.createTsDeclgen(
inputFiles,
outputDeclEtsPaths,
outputEtsPaths,
false,
false,
staticRecordRelativePath,
genDeclAnnotations
);
let ast = arkts.EtsScript.fromContext();
this.pluginDriver.getPluginContext().setArkTSAst(ast);
this.pluginDriver.runPluginHook(PluginHook.PARSED);
arkts.generateTsDeclarationsAfterParsed(declgen);
arkts.proceedToState(arkts.Es2pandaContextState.ES2PANDA_STATE_CHECKED, arktsGlobal.compilerContext.peer, skipDeclCheck);
ast = arkts.EtsScript.fromContext();
this.pluginDriver.getPluginContext().setArkTSAst(ast);
this.pluginDriver.runPluginHook(PluginHook.CHECKED);
arkts.generateTsDeclarationsAfterCheck(declgen);
arkts.writeTsDeclarations(declgen);
this.logger.printInfo(`[Ets2panda] Generated 1.0 declaration file for ${inputFiles[0]}`)
} catch (error) {
if (error instanceof Error) {
throw new DriverError(
LogDataFactory.newInstance(
ErrorCode.BUILDSYSTEM_DECLGEN_FAIL,
'Failed to generate 1.0 declaration file.',
error.message,
inputFiles[0]
)
);
}
} finally {
this.pluginDriver.runPluginHook(PluginHook.CLEAN);
if (declgen) {
arkts.destroyTsDeclgen(declgen);
}
if (arktsGlobal.compilerContext) {
arktsGlobal.es2panda._DestroyContext(arktsGlobal.compilerContext.peer);
}
arkts.destroyConfig(arktsGlobal.config);
}
}
public extractDeclarationsFromAbcFile(abcFile: string, cacheDir: string) {
let { arkts } = this.koalaModule;
arkts.ExtractDeclarationsFromAbcFile(abcFile, cacheDir);
}
}