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