1/* 2 * Copyright (c) 2023 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import path from 'path'; 17import fs from 'fs'; 18import cluster from 'cluster'; 19import childProcess from 'child_process'; 20 21import { CommonMode } from '../common/common_mode'; 22import { 23 changeFileExtension, 24 genCachePath, 25 getEs2abcFileThreadNumber, 26 genTemporaryModuleCacheDirectoryForBundle, 27 isMasterOrPrimary, 28 isSpecifiedExt, 29 isDebug 30} from '../utils'; 31import { 32 ES2ABC, 33 EXTNAME_ABC, 34 EXTNAME_JS, 35 FILESINFO_TXT, 36 JSBUNDLE, 37 MAX_WORKER_NUMBER, 38 TEMP_JS, 39 TS2ABC, 40 red, 41 blue, 42 FAIL, 43 SUCCESS, 44 reset 45} from '../common/ark_define'; 46import { 47 mkDir, 48 toHashData, 49 toUnixPath, 50 unlinkSync, 51 validateFilePathLength 52} from '../../../utils'; 53import { 54 isEs2Abc, 55 isTs2Abc 56} from '../../../ark_utils'; 57import { 58 ArkTSErrorDescription, 59 ArkTSInternalErrorDescription, 60 ErrorCode 61} from '../error_code'; 62import { 63 LogData, 64 LogDataFactory 65} from '../logger'; 66 67interface File { 68 filePath: string; 69 cacheFilePath: string; 70 sourceFile: string; 71 size: number; 72} 73 74export class BundleMode extends CommonMode { 75 intermediateJsBundle: Map<string, File>; 76 filterIntermediateJsBundle: Array<File>; 77 hashJsonObject: Object; 78 filesInfoPath: string; 79 80 constructor(rollupObject: Object, rollupBundleFileSet: Object) { 81 super(rollupObject); 82 this.intermediateJsBundle = new Map<string, File>(); 83 this.filterIntermediateJsBundle = []; 84 this.hashJsonObject = {}; 85 this.filesInfoPath = ''; 86 this.prepareForCompilation(rollupObject, rollupBundleFileSet); 87 } 88 89 prepareForCompilation(rollupObject: Object, rollupBundleFileSet: Object): void { 90 this.collectBundleFileList(rollupBundleFileSet); 91 this.removeCacheInfo(rollupObject); 92 this.filterBundleFileListWithHashJson(); 93 } 94 95 collectBundleFileList(rollupBundleFileSet: Object): void { 96 Object.keys(rollupBundleFileSet).forEach((fileName) => { 97 // choose *.js 98 if (this.projectConfig.aceModuleBuild && isSpecifiedExt(fileName, EXTNAME_JS)) { 99 const tempFilePath: string = changeFileExtension(fileName, TEMP_JS); 100 const outputPath: string = path.resolve(this.projectConfig.aceModuleBuild, tempFilePath); 101 const cacheOutputPath: string = this.genCacheBundleFilePath(outputPath, tempFilePath); 102 let rollupBundleSourceCode: string = ''; 103 if (rollupBundleFileSet[fileName].type === 'asset') { 104 rollupBundleSourceCode = rollupBundleFileSet[fileName].source; 105 } else if (rollupBundleFileSet[fileName].type === 'chunk') { 106 rollupBundleSourceCode = rollupBundleFileSet[fileName].code; 107 } else { 108 const errInfo: LogData = LogDataFactory.newInstance( 109 ErrorCode.ETS2BUNDLE_INTERNAL_UNABLE_TO_RETRIEVE_SOURCE_CODE_FROM_SUMMARY, 110 ArkTSInternalErrorDescription, 111 `Failed to retrieve source code for ${fileName} from rollup file set.` 112 ); 113 this.logger.printErrorAndExit(errInfo); 114 } 115 fs.writeFileSync(cacheOutputPath, rollupBundleSourceCode, 'utf-8'); 116 if (!fs.existsSync(cacheOutputPath)) { 117 const errInfo: LogData = LogDataFactory.newInstance( 118 ErrorCode.ETS2BUNDLE_INTERNAL_UNABLE_TO_GENERATE_CACHE_SOURCE_FILE, 119 ArkTSInternalErrorDescription, 120 `Failed to generate cached source file: ${fileName}.` 121 ); 122 this.logger.printErrorAndExit(errInfo); 123 } 124 this.collectIntermediateJsBundle(outputPath, cacheOutputPath); 125 } 126 }); 127 } 128 129 filterBundleFileListWithHashJson() { 130 if (this.intermediateJsBundle.size === 0) { 131 return; 132 } 133 if (!fs.existsSync(this.hashJsonFilePath) || this.hashJsonFilePath.length === 0) { 134 this.intermediateJsBundle.forEach((value) => { 135 this.filterIntermediateJsBundle.push(value); 136 }); 137 return; 138 } 139 let updatedJsonObject: Object = {}; 140 let jsonObject: Object = {}; 141 let jsonFile: string = ''; 142 jsonFile = fs.readFileSync(this.hashJsonFilePath).toString(); 143 jsonObject = JSON.parse(jsonFile); 144 this.filterIntermediateJsBundle = []; 145 for (const value of this.intermediateJsBundle.values()) { 146 const cacheFilePath: string = value.cacheFilePath; 147 const cacheAbcFilePath: string = changeFileExtension(cacheFilePath, EXTNAME_ABC); 148 if (!fs.existsSync(cacheFilePath)) { 149 const errInfo: LogData = LogDataFactory.newInstance( 150 ErrorCode.ETS2BUNDLE_INTERNAL_UNABLE_TO_RETRIEVE_PACKAGE_CACHE_IN_INCREMENTAL_BUILD, 151 ArkTSInternalErrorDescription, 152 `Failed to get bundle cached abc from ${cacheFilePath} in incremental build.` 153 ); 154 this.logger.printErrorAndExit(errInfo); 155 } 156 if (fs.existsSync(cacheAbcFilePath)) { 157 const hashCacheFileContentData: string = toHashData(cacheFilePath); 158 const hashAbcContentData: string = toHashData(cacheAbcFilePath); 159 if (jsonObject[cacheFilePath] === hashCacheFileContentData && 160 jsonObject[cacheAbcFilePath] === hashAbcContentData) { 161 updatedJsonObject[cacheFilePath] = hashCacheFileContentData; 162 updatedJsonObject[cacheAbcFilePath] = hashAbcContentData; 163 continue; 164 } 165 } 166 this.filterIntermediateJsBundle.push(value); 167 } 168 169 this.hashJsonObject = updatedJsonObject; 170 } 171 172 executeArkCompiler() { 173 if (isEs2Abc(this.projectConfig)) { 174 this.filesInfoPath = this.generateFileInfoOfBundle(); 175 this.generateEs2AbcCmd(this.filesInfoPath); 176 this.executeEs2AbcCmd(); 177 } else if (isTs2Abc(this.projectConfig)) { 178 const splittedBundles: any[] = this.getSplittedBundles(); 179 this.invokeTs2AbcWorkersToGenAbc(splittedBundles); 180 } else { 181 const errInfo: LogData = LogDataFactory.newInstance( 182 ErrorCode.ETS2BUNDLE_INTERNAL_INVALID_COMPILE_MODE, 183 ArkTSInternalErrorDescription, 184 'Invalid compilation mode. ' + 185 `ProjectConfig.pandaMode should be either ${TS2ABC} or ${ES2ABC}.` 186 ); 187 this.logger.printErrorAndExit(errInfo); 188 } 189 } 190 191 afterCompilationProcess() { 192 this.writeHashJson(); 193 this.copyFileFromCachePathToOutputPath(); 194 this.cleanTempCacheFiles(); 195 } 196 197 private generateEs2AbcCmd(filesInfoPath: string) { 198 const fileThreads: number = getEs2abcFileThreadNumber(); 199 this.cmdArgs.push( 200 `"@${filesInfoPath}"`, 201 '--file-threads', 202 `"${fileThreads}"`, 203 `"--target-api-version=${this.projectConfig.compatibleSdkVersion}"`, 204 '--opt-try-catch-func=false' 205 ); 206 if (this.projectConfig.compatibleSdkReleaseType) { 207 this.cmdArgs.push(`"--target-api-sub-version=${this.projectConfig.compatibleSdkReleaseType}"`); 208 } 209 } 210 211 private generateFileInfoOfBundle(): string { 212 const filesInfoPath: string = genCachePath(FILESINFO_TXT, this.projectConfig, this.logger); 213 let filesInfo: string = ''; 214 this.filterIntermediateJsBundle.forEach((info) => { 215 const cacheFilePath: string = info.cacheFilePath; 216 const recordName: string = 'null_recordName'; 217 const moduleType: string = 'script'; 218 // In release mode, there are '.temp.js' and '.js' file in cache path, no js file in output path. 219 // In debug mode, '.temp.js' file is in cache path, and '.js' file is in output path. 220 // '.temp.js' file is the input of es2abc, and should be uesd as sourceFile here. However,in debug mode , 221 // using '.temp.js' file as sourceFile needs IDE to adapt, so use '.js' file in output path instead temporarily. 222 const sourceFile: string = (isDebug(this.projectConfig) ? info.sourceFile.replace(/(.*)_/, '$1') : 223 cacheFilePath).replace(toUnixPath(this.projectConfig.projectRootPath) + '/', ''); 224 const abcFilePath: string = changeFileExtension(cacheFilePath, EXTNAME_ABC); 225 filesInfo += `${cacheFilePath};${recordName};${moduleType};${sourceFile};${abcFilePath}\n`; 226 }); 227 fs.writeFileSync(filesInfoPath, filesInfo, 'utf-8'); 228 229 return filesInfoPath; 230 } 231 232 private executeEs2AbcCmd() { 233 // collect data error from subprocess 234 let logDataList: Object[] = []; 235 let errMsg: string = ''; 236 const genAbcCmd: string = this.cmdArgs.join(' '); 237 try { 238 const child = this.triggerAsync(() => { 239 return childProcess.exec(genAbcCmd, { windowsHide: true }); 240 }); 241 child.on('close', (code: number) => { 242 if (code === SUCCESS) { 243 this.afterCompilationProcess(); 244 this.triggerEndSignal(); 245 return; 246 } 247 for (const logData of logDataList) { 248 this.logger.printError(logData); 249 } 250 if (errMsg !== '') { 251 this.logger.error(`Error Message: ${errMsg}`); 252 } 253 const errInfo: LogData = LogDataFactory.newInstance( 254 ErrorCode.ETS2BUNDLE_EXTERNAL_ES2ABC_EXECUTION_FAILED, 255 ArkTSErrorDescription, 256 'Failed to execute es2abc.', 257 '', 258 ["Please refer to es2abc's error codes."] 259 ); 260 this.logger.printErrorAndExit(errInfo); 261 }); 262 263 child.on('error', (err: any) => { 264 const errInfo: LogData = LogDataFactory.newInstance( 265 ErrorCode.ETS2BUNDLE_INTERNAL_ES2ABC_SUBPROCESS_START_FAILED, 266 ArkTSInternalErrorDescription, 267 `Failed to initialize or launch the es2abc process. ${err.toString()}` 268 ); 269 this.logger.printErrorAndExit(errInfo); 270 }); 271 272 child.stderr.on('data', (data: any) => { 273 const logData = LogDataFactory.newInstanceFromEs2AbcError(data.toString()); 274 if (logData) { 275 logDataList.push(logData); 276 } else { 277 errMsg += data.toString(); 278 } 279 }); 280 } catch (e) { 281 const errInfo: LogData = LogDataFactory.newInstance( 282 ErrorCode.ETS2BUNDLE_INTERNAL_EXECUTE_ES2ABC_WITH_ASYNC_HANDLER_FAILED, 283 ArkTSInternalErrorDescription, 284 `Failed to execute es2abc with async handler. ${e.toString()}` 285 ); 286 this.logger.printErrorAndExit(errInfo); 287 } 288 } 289 290 private genCacheBundleFilePath(outputPath: string, tempFilePath: string): string { 291 let cacheOutputPath: string = ''; 292 if (this.projectConfig.cachePath) { 293 cacheOutputPath = path.join(genTemporaryModuleCacheDirectoryForBundle(this.projectConfig), tempFilePath); 294 } else { 295 cacheOutputPath = outputPath; 296 } 297 validateFilePathLength(cacheOutputPath, this.logger); 298 const parentDir: string = path.join(cacheOutputPath, '..'); 299 if (!(fs.existsSync(parentDir) && fs.statSync(parentDir).isDirectory())) { 300 mkDir(parentDir); 301 } 302 303 return cacheOutputPath; 304 } 305 306 private collectIntermediateJsBundle(filePath: string, cacheFilePath: string) { 307 const fileSize: number = fs.statSync(cacheFilePath).size; 308 let sourceFile: string = changeFileExtension(filePath, '_.js', TEMP_JS); 309 if (!this.arkConfig.isDebug && this.projectConfig.projectRootPath) { 310 sourceFile = sourceFile.replace(this.projectConfig.projectRootPath + path.sep, ''); 311 } 312 313 filePath = toUnixPath(filePath); 314 cacheFilePath = toUnixPath(cacheFilePath); 315 sourceFile = toUnixPath(sourceFile); 316 const bundleFile: File = { 317 filePath: filePath, 318 cacheFilePath: cacheFilePath, 319 sourceFile: sourceFile, 320 size: fileSize 321 }; 322 this.intermediateJsBundle.set(filePath, bundleFile); 323 } 324 325 private getSplittedBundles(): any[] { 326 const splittedBundles: any[] = this.splitJsBundlesBySize(this.filterIntermediateJsBundle, MAX_WORKER_NUMBER); 327 return splittedBundles; 328 } 329 330 private invokeTs2AbcWorkersToGenAbc(splittedBundles) { 331 if (isMasterOrPrimary()) { 332 this.setupCluster(cluster); 333 const workerNumber: number = splittedBundles.length < MAX_WORKER_NUMBER ? splittedBundles.length : MAX_WORKER_NUMBER; 334 for (let i = 0; i < workerNumber; ++i) { 335 const workerData: Object = { 336 inputs: JSON.stringify(splittedBundles[i]), 337 cmd: this.cmdArgs.join(' '), 338 mode: JSBUNDLE 339 }; 340 this.triggerAsync(() => { 341 const worker: Object = cluster.fork(workerData); 342 worker.on('message', (errorMsg) => { 343 this.logger.error(red, errorMsg.data.toString(), reset); 344 this.logger.throwArkTsCompilerError('ArkTS:ERROR Failed to execute ts2abc'); 345 }); 346 }); 347 } 348 349 let workerCount: number = 0; 350 cluster.on('exit', (worker, code, signal) => { 351 if (code === FAIL) { 352 this.logger.throwArkTsCompilerError('ArkTS:ERROR Failed to execute ts2abc, exit code non-zero'); 353 } 354 workerCount++; 355 if (workerCount === workerNumber) { 356 this.afterCompilationProcess(); 357 } 358 this.triggerEndSignal(); 359 }); 360 } 361 } 362 363 private getSmallestSizeGroup(groupSize: Map<number, number>): any { 364 const groupSizeArray: any = Array.from(groupSize); 365 groupSizeArray.sort(function(g1, g2) { 366 return g1[1] - g2[1]; // sort by size 367 }); 368 return groupSizeArray[0][0]; 369 } 370 371 private splitJsBundlesBySize(bundleArray: Array<File>, groupNumber: number): any { 372 const result: any = []; 373 if (bundleArray.length < groupNumber) { 374 for (const value of bundleArray) { 375 result.push([value]); 376 } 377 return result; 378 } 379 380 bundleArray.sort(function(f1: File, f2: File) { 381 return f2.size - f1.size; 382 }); 383 const groupFileSize: any = new Map(); 384 for (let i = 0; i < groupNumber; ++i) { 385 result.push([]); 386 groupFileSize.set(i, 0); 387 } 388 389 let index: number = 0; 390 while (index < bundleArray.length) { 391 const smallestGroup: any = this.getSmallestSizeGroup(groupFileSize); 392 result[smallestGroup].push(bundleArray[index]); 393 const sizeUpdate: any = groupFileSize.get(smallestGroup) + bundleArray[index].size; 394 groupFileSize.set(smallestGroup, sizeUpdate); 395 index++; 396 } 397 return result; 398 } 399 400 private writeHashJson() { 401 if (this.hashJsonFilePath.length === 0) { 402 return; 403 } 404 405 for (let i = 0; i < this.filterIntermediateJsBundle.length; ++i) { 406 const cacheFilePath: string = this.filterIntermediateJsBundle[i].cacheFilePath; 407 const cacheAbcFilePath: string = changeFileExtension(cacheFilePath, EXTNAME_ABC); 408 if (!fs.existsSync(cacheFilePath) || !fs.existsSync(cacheAbcFilePath)) { 409 const errInfo: LogData = LogDataFactory.newInstance( 410 ErrorCode.ETS2BUNDLE_INTERNAL_HASH_JSON_FILE_GENERATION_MISSING_PATHS, 411 ArkTSInternalErrorDescription, 412 `During hash JSON file generation, ${cacheFilePath} or ${cacheAbcFilePath} is not found.` 413 ); 414 this.logger.printErrorAndExit(errInfo); 415 } 416 const hashCacheFileContentData: string = toHashData(cacheFilePath); 417 const hashCacheAbcContentData: string = toHashData(cacheAbcFilePath); 418 this.hashJsonObject[cacheFilePath] = hashCacheFileContentData; 419 this.hashJsonObject[cacheAbcFilePath] = hashCacheAbcContentData; 420 } 421 422 fs.writeFileSync(this.hashJsonFilePath, JSON.stringify(this.hashJsonObject), 'utf-8'); 423 } 424 425 private copyFileFromCachePathToOutputPath() { 426 for (const value of this.intermediateJsBundle.values()) { 427 const abcFilePath: string = changeFileExtension(value.filePath, EXTNAME_ABC, TEMP_JS); 428 const cacheAbcFilePath: string = changeFileExtension(value.cacheFilePath, EXTNAME_ABC); 429 if (!fs.existsSync(cacheAbcFilePath)) { 430 const errInfo: LogData = LogDataFactory.newInstance( 431 ErrorCode.ETS2BUNDLE_INTERNAL_INCREMENTAL_BUILD_MISSING_CACHE_ABC_FILE_PATH, 432 ArkTSInternalErrorDescription, 433 `${cacheAbcFilePath} not found during incremental build.` 434 ); 435 this.logger.printErrorAndExit(errInfo); 436 } 437 const parent: string = path.join(abcFilePath, '..'); 438 if (!(fs.existsSync(parent) && fs.statSync(parent).isDirectory())) { 439 mkDir(parent); 440 } 441 // for preview mode, cache path and old abc file both exist, should copy abc file for updating 442 if (this.projectConfig.cachePath !== undefined) { 443 fs.copyFileSync(cacheAbcFilePath, abcFilePath); 444 } 445 } 446 } 447 448 private cleanTempCacheFiles() { 449 // in xts mode, as cache path is not provided, cache files are located in output path, clear them 450 if (this.projectConfig.cachePath !== undefined) { 451 return; 452 } 453 454 for (const value of this.intermediateJsBundle.values()) { 455 if (fs.existsSync(value.cacheFilePath)) { 456 fs.unlinkSync(value.cacheFilePath); 457 } 458 } 459 460 if (isEs2Abc(this.projectConfig) && fs.existsSync(this.filesInfoPath)) { 461 unlinkSync(this.filesInfoPath); 462 } 463 } 464 465 private removeCompilationCache(): void { 466 if (fs.existsSync(this.hashJsonFilePath)) { 467 fs.unlinkSync(this.hashJsonFilePath); 468 } 469 } 470} 471