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