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