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 reset 44} from '../common/ark_define'; 45import { 46 mkDir, 47 toHashData, 48 toUnixPath, 49 unlinkSync, 50 validateFilePathLength 51} from '../../../utils'; 52import { 53 isEs2Abc, 54 isTs2Abc 55} from '../../../ark_utils'; 56 57interface File { 58 filePath: string; 59 cacheFilePath: string; 60 sourceFile: string; 61 size: number; 62} 63 64export class BundleMode extends CommonMode { 65 intermediateJsBundle: Map<string, File>; 66 filterIntermediateJsBundle: Array<File>; 67 hashJsonObject: Object; 68 filesInfoPath: string; 69 70 constructor(rollupObject: Object, rollupBundleFileSet: Object) { 71 super(rollupObject); 72 this.intermediateJsBundle = new Map<string, File>(); 73 this.filterIntermediateJsBundle = []; 74 this.hashJsonObject = {}; 75 this.filesInfoPath = ''; 76 this.prepareForCompilation(rollupObject, rollupBundleFileSet); 77 } 78 79 prepareForCompilation(rollupObject: Object, rollupBundleFileSet: Object): void { 80 this.collectBundleFileList(rollupBundleFileSet); 81 this.removeCacheInfo(rollupObject); 82 this.filterBundleFileListWithHashJson(); 83 } 84 85 collectBundleFileList(rollupBundleFileSet: Object): void { 86 Object.keys(rollupBundleFileSet).forEach((fileName) => { 87 // choose *.js 88 if (this.projectConfig.aceModuleBuild && isSpecifiedExt(fileName, EXTNAME_JS)) { 89 const tempFilePath: string = changeFileExtension(fileName, TEMP_JS); 90 const outputPath: string = path.resolve(this.projectConfig.aceModuleBuild, tempFilePath); 91 const cacheOutputPath: string = this.genCacheBundleFilePath(outputPath, tempFilePath); 92 let rollupBundleSourceCode: string = ''; 93 if (rollupBundleFileSet[fileName].type === 'asset') { 94 rollupBundleSourceCode = rollupBundleFileSet[fileName].source; 95 } else if (rollupBundleFileSet[fileName].type === 'chunk') { 96 rollupBundleSourceCode = rollupBundleFileSet[fileName].code; 97 } else { 98 this.throwArkTsCompilerError('ArkTS:INTERNAL ERROR: Failed to retrieve source code ' + 99 `for ${fileName} from rollup file set.`); 100 } 101 fs.writeFileSync(cacheOutputPath, rollupBundleSourceCode, 'utf-8'); 102 if (!fs.existsSync(cacheOutputPath)) { 103 this.throwArkTsCompilerError(`ArkTS:INTERNAL ERROR: Failed to generate cached source file: ${fileName}`); 104 } 105 this.collectIntermediateJsBundle(outputPath, cacheOutputPath); 106 } 107 }); 108 } 109 110 filterBundleFileListWithHashJson() { 111 if (this.intermediateJsBundle.size === 0) { 112 return; 113 } 114 if (!fs.existsSync(this.hashJsonFilePath) || this.hashJsonFilePath.length === 0) { 115 this.intermediateJsBundle.forEach((value) => { 116 this.filterIntermediateJsBundle.push(value); 117 }); 118 return; 119 } 120 let updatedJsonObject: Object = {}; 121 let jsonObject: Object = {}; 122 let jsonFile: string = ''; 123 jsonFile = fs.readFileSync(this.hashJsonFilePath).toString(); 124 jsonObject = JSON.parse(jsonFile); 125 this.filterIntermediateJsBundle = []; 126 for (const value of this.intermediateJsBundle.values()) { 127 const cacheFilePath: string = value.cacheFilePath; 128 const cacheAbcFilePath: string = changeFileExtension(cacheFilePath, EXTNAME_ABC); 129 if (!fs.existsSync(cacheFilePath)) { 130 this.throwArkTsCompilerError( 131 `ArkTS:INTERNAL ERROR: Failed to get bundle cached abc from ${cacheFilePath} in incremental build.` + 132 'Please try to rebuild the project.'); 133 } 134 if (fs.existsSync(cacheAbcFilePath)) { 135 const hashCacheFileContentData: string = toHashData(cacheFilePath); 136 const hashAbcContentData: string = toHashData(cacheAbcFilePath); 137 if (jsonObject[cacheFilePath] === hashCacheFileContentData && 138 jsonObject[cacheAbcFilePath] === hashAbcContentData) { 139 updatedJsonObject[cacheFilePath] = hashCacheFileContentData; 140 updatedJsonObject[cacheAbcFilePath] = hashAbcContentData; 141 continue; 142 } 143 } 144 this.filterIntermediateJsBundle.push(value); 145 } 146 147 this.hashJsonObject = updatedJsonObject; 148 } 149 150 executeArkCompiler() { 151 if (isEs2Abc(this.projectConfig)) { 152 this.filesInfoPath = this.generateFileInfoOfBundle(); 153 this.generateEs2AbcCmd(this.filesInfoPath); 154 this.executeEs2AbcCmd(); 155 } else if (isTs2Abc(this.projectConfig)) { 156 const splittedBundles: any[] = this.getSplittedBundles(); 157 this.invokeTs2AbcWorkersToGenAbc(splittedBundles); 158 } else { 159 this.throwArkTsCompilerError('ArkTS:INTERNAL ERROR: Invalid compilation mode.'); 160 } 161 } 162 163 afterCompilationProcess() { 164 this.writeHashJson(); 165 this.copyFileFromCachePathToOutputPath(); 166 this.cleanTempCacheFiles(); 167 } 168 169 private generateEs2AbcCmd(filesInfoPath: string) { 170 const fileThreads: number = getEs2abcFileThreadNumber(); 171 this.cmdArgs.push( 172 `"@${filesInfoPath}"`, 173 '--file-threads', 174 `"${fileThreads}"`, 175 `"--target-api-version=${this.projectConfig.compatibleSdkVersion}"`, 176 '--opt-try-catch-func=false' 177 ); 178 if (this.projectConfig.compatibleSdkReleaseType) { 179 this.cmdArgs.push(`"--target-api-sub-version=${this.projectConfig.compatibleSdkReleaseType}"`); 180 } 181 } 182 183 private generateFileInfoOfBundle(): string { 184 const filesInfoPath: string = genCachePath(FILESINFO_TXT, this.projectConfig, this.logger); 185 let filesInfo: string = ''; 186 this.filterIntermediateJsBundle.forEach((info) => { 187 const cacheFilePath: string = info.cacheFilePath; 188 const recordName: string = 'null_recordName'; 189 const moduleType: string = 'script'; 190 // In release mode, there are '.temp.js' and '.js' file in cache path, no js file in output path. 191 // In debug mode, '.temp.js' file is in cache path, and '.js' file is in output path. 192 // '.temp.js' file is the input of es2abc, and should be uesd as sourceFile here. However,in debug mode , 193 // using '.temp.js' file as sourceFile needs IDE to adapt, so use '.js' file in output path instead temporarily. 194 const sourceFile: string = (isDebug(this.projectConfig) ? info.sourceFile.replace(/(.*)_/, '$1') : 195 cacheFilePath).replace(toUnixPath(this.projectConfig.projectRootPath) + '/', ''); 196 const abcFilePath: string = changeFileExtension(cacheFilePath, EXTNAME_ABC); 197 filesInfo += `${cacheFilePath};${recordName};${moduleType};${sourceFile};${abcFilePath}\n`; 198 }); 199 fs.writeFileSync(filesInfoPath, filesInfo, 'utf-8'); 200 201 return filesInfoPath; 202 } 203 204 private executeEs2AbcCmd() { 205 // collect data error from subprocess 206 let errMsg: string = ''; 207 const genAbcCmd: string = this.cmdArgs.join(' '); 208 try { 209 const child = this.triggerAsync(() => { 210 return childProcess.exec(genAbcCmd, { windowsHide: true }); 211 }); 212 child.on('close', (code: number) => { 213 if (code === FAIL) { 214 this.throwArkTsCompilerError('ArkTS:ERROR Failed to execute es2abc.'); 215 } 216 this.afterCompilationProcess(); 217 this.triggerEndSignal(); 218 }); 219 220 child.on('error', (err: any) => { 221 this.throwArkTsCompilerError(err.toString()); 222 }); 223 224 child.stderr.on('data', (data: any) => { 225 errMsg += data.toString(); 226 }); 227 228 child.stderr.on('end', () => { 229 if (errMsg !== undefined && errMsg.length > 0) { 230 this.logger.error(red, errMsg, reset); 231 } 232 }); 233 } catch (e) { 234 this.throwArkTsCompilerError('ArkTS:ERROR failed to execute es2abc with async handler: ' + e.toString()); 235 } 236 } 237 238 private genCacheBundleFilePath(outputPath: string, tempFilePath: string): string { 239 let cacheOutputPath: string = ''; 240 if (this.projectConfig.cachePath) { 241 cacheOutputPath = path.join(genTemporaryModuleCacheDirectoryForBundle(this.projectConfig), tempFilePath); 242 } else { 243 cacheOutputPath = outputPath; 244 } 245 validateFilePathLength(cacheOutputPath, this.logger); 246 const parentDir: string = path.join(cacheOutputPath, '..'); 247 if (!(fs.existsSync(parentDir) && fs.statSync(parentDir).isDirectory())) { 248 mkDir(parentDir); 249 } 250 251 return cacheOutputPath; 252 } 253 254 private collectIntermediateJsBundle(filePath: string, cacheFilePath: string) { 255 const fileSize: number = fs.statSync(cacheFilePath).size; 256 let sourceFile: string = changeFileExtension(filePath, '_.js', TEMP_JS); 257 if (!this.arkConfig.isDebug && this.projectConfig.projectRootPath) { 258 sourceFile = sourceFile.replace(this.projectConfig.projectRootPath + path.sep, ''); 259 } 260 261 filePath = toUnixPath(filePath); 262 cacheFilePath = toUnixPath(cacheFilePath); 263 sourceFile = toUnixPath(sourceFile); 264 const bundleFile: File = { 265 filePath: filePath, 266 cacheFilePath: cacheFilePath, 267 sourceFile: sourceFile, 268 size: fileSize 269 }; 270 this.intermediateJsBundle.set(filePath, bundleFile); 271 } 272 273 private getSplittedBundles(): any[] { 274 const splittedBundles: any[] = this.splitJsBundlesBySize(this.filterIntermediateJsBundle, MAX_WORKER_NUMBER); 275 return splittedBundles; 276 } 277 278 private invokeTs2AbcWorkersToGenAbc(splittedBundles) { 279 if (isMasterOrPrimary()) { 280 this.setupCluster(cluster); 281 const workerNumber: number = splittedBundles.length < MAX_WORKER_NUMBER ? splittedBundles.length : MAX_WORKER_NUMBER; 282 for (let i = 0; i < workerNumber; ++i) { 283 const workerData: Object = { 284 inputs: JSON.stringify(splittedBundles[i]), 285 cmd: this.cmdArgs.join(' '), 286 mode: JSBUNDLE 287 }; 288 this.triggerAsync(() => { 289 const worker: Object = cluster.fork(workerData); 290 worker.on('message', (errorMsg) => { 291 this.logger.error(red, errorMsg.data.toString(), reset); 292 this.throwArkTsCompilerError('ArkTS:ERROR Failed to execute ts2abc'); 293 }); 294 }); 295 } 296 297 let workerCount: number = 0; 298 cluster.on('exit', (worker, code, signal) => { 299 if (code === FAIL) { 300 this.throwArkTsCompilerError('ArkTS:ERROR Failed to execute ts2abc, exit code non-zero'); 301 } 302 workerCount++; 303 if (workerCount === workerNumber) { 304 this.afterCompilationProcess(); 305 } 306 this.triggerEndSignal(); 307 }); 308 } 309 } 310 311 private getSmallestSizeGroup(groupSize: Map<number, number>): any { 312 const groupSizeArray: any = Array.from(groupSize); 313 groupSizeArray.sort(function(g1, g2) { 314 return g1[1] - g2[1]; // sort by size 315 }); 316 return groupSizeArray[0][0]; 317 } 318 319 private splitJsBundlesBySize(bundleArray: Array<File>, groupNumber: number): any { 320 const result: any = []; 321 if (bundleArray.length < groupNumber) { 322 for (const value of bundleArray) { 323 result.push([value]); 324 } 325 return result; 326 } 327 328 bundleArray.sort(function(f1: File, f2: File) { 329 return f2.size - f1.size; 330 }); 331 const groupFileSize: any = new Map(); 332 for (let i = 0; i < groupNumber; ++i) { 333 result.push([]); 334 groupFileSize.set(i, 0); 335 } 336 337 let index: number = 0; 338 while (index < bundleArray.length) { 339 const smallestGroup: any = this.getSmallestSizeGroup(groupFileSize); 340 result[smallestGroup].push(bundleArray[index]); 341 const sizeUpdate: any = groupFileSize.get(smallestGroup) + bundleArray[index].size; 342 groupFileSize.set(smallestGroup, sizeUpdate); 343 index++; 344 } 345 return result; 346 } 347 348 private writeHashJson() { 349 if (this.hashJsonFilePath.length === 0) { 350 return; 351 } 352 353 for (let i = 0; i < this.filterIntermediateJsBundle.length; ++i) { 354 const cacheFilePath: string = this.filterIntermediateJsBundle[i].cacheFilePath; 355 const cacheAbcFilePath: string = changeFileExtension(cacheFilePath, EXTNAME_ABC); 356 if (!fs.existsSync(cacheFilePath) || !fs.existsSync(cacheAbcFilePath)) { 357 this.throwArkTsCompilerError('ArkTS:INTERNAL ERROR: During hash JSON file generation, ' + 358 `${cacheFilePath} or ${cacheAbcFilePath} is not found.`); 359 } 360 const hashCacheFileContentData: string = toHashData(cacheFilePath); 361 const hashCacheAbcContentData: string = toHashData(cacheAbcFilePath); 362 this.hashJsonObject[cacheFilePath] = hashCacheFileContentData; 363 this.hashJsonObject[cacheAbcFilePath] = hashCacheAbcContentData; 364 } 365 366 fs.writeFileSync(this.hashJsonFilePath, JSON.stringify(this.hashJsonObject), 'utf-8'); 367 } 368 369 private copyFileFromCachePathToOutputPath() { 370 for (const value of this.intermediateJsBundle.values()) { 371 const abcFilePath: string = changeFileExtension(value.filePath, EXTNAME_ABC, TEMP_JS); 372 const cacheAbcFilePath: string = changeFileExtension(value.cacheFilePath, EXTNAME_ABC); 373 if (!fs.existsSync(cacheAbcFilePath)) { 374 this.throwArkTsCompilerError(`ArkTS:INTERNAL ERROR: ${cacheAbcFilePath} not found during incremental build. ` + 375 'Please try to rebuild the project'); 376 } 377 const parent: string = path.join(abcFilePath, '..'); 378 if (!(fs.existsSync(parent) && fs.statSync(parent).isDirectory())) { 379 mkDir(parent); 380 } 381 // for preview mode, cache path and old abc file both exist, should copy abc file for updating 382 if (this.projectConfig.cachePath !== undefined) { 383 fs.copyFileSync(cacheAbcFilePath, abcFilePath); 384 } 385 } 386 } 387 388 private cleanTempCacheFiles() { 389 // in xts mode, as cache path is not provided, cache files are located in output path, clear them 390 if (this.projectConfig.cachePath !== undefined) { 391 return; 392 } 393 394 for (const value of this.intermediateJsBundle.values()) { 395 if (fs.existsSync(value.cacheFilePath)) { 396 fs.unlinkSync(value.cacheFilePath); 397 } 398 } 399 400 if (isEs2Abc(this.projectConfig) && fs.existsSync(this.filesInfoPath)) { 401 unlinkSync(this.filesInfoPath); 402 } 403 } 404 405 private removeCompilationCache(): void { 406 if (fs.existsSync(this.hashJsonFilePath)) { 407 fs.unlinkSync(this.hashJsonFilePath); 408 } 409 } 410} 411