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