• 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} 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