• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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