1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @format
8 */
9
10'use strict';
11
12const fs = require('fs');
13const os = require('os');
14const path = require('path');
15const {execSync, spawnSync} = require('child_process');
16
17const SDKS_DIR = path.normalize(path.join(__dirname, '..', '..', 'sdks'));
18const HERMES_DIR = path.join(SDKS_DIR, 'hermes');
19const HERMES_TAG_FILE_PATH = path.join(SDKS_DIR, '.hermesversion');
20const HERMES_SOURCE_TARBALL_BASE_URL =
21  'https://github.com/facebook/hermes/tarball/';
22const HERMES_TARBALL_DOWNLOAD_DIR = path.join(SDKS_DIR, 'download');
23const MACOS_BIN_DIR = path.join(SDKS_DIR, 'hermesc', 'osx-bin');
24const MACOS_HERMESC_PATH = path.join(MACOS_BIN_DIR, 'hermesc');
25const MACOS_IMPORT_HERMESC_PATH = path.join(
26  MACOS_BIN_DIR,
27  'ImportHermesc.cmake',
28);
29
30/**
31 * Delegate execution to the supplied command.
32 *
33 * @param command Path to the command.
34 * @param args Array of arguments pass to the command.
35 * @param options child process options.
36 */
37function delegateSync(command, args, options) {
38  return spawnSync(command, args, {stdio: 'inherit', ...options});
39}
40
41function readHermesTag() {
42  if (fs.existsSync(HERMES_TAG_FILE_PATH)) {
43    const data = fs
44      .readFileSync(HERMES_TAG_FILE_PATH, {
45        encoding: 'utf8',
46        flag: 'r',
47      })
48      .trim();
49
50    if (data.length > 0) {
51      return data;
52    } else {
53      throw new Error('[Hermes] .hermesversion file is empty.');
54    }
55  }
56
57  return 'main';
58}
59
60function setHermesTag(hermesTag) {
61  if (readHermesTag() === hermesTag) {
62    // No need to update.
63    return;
64  }
65
66  if (!fs.existsSync(SDKS_DIR)) {
67    fs.mkdirSync(SDKS_DIR, {recursive: true});
68  }
69  fs.writeFileSync(HERMES_TAG_FILE_PATH, hermesTag.trim());
70  console.log('Hermes tag has been updated. Please commit your changes.');
71}
72
73function getHermesTagSHA(hermesTag) {
74  return execSync(
75    `git ls-remote https://github.com/facebook/hermes ${hermesTag} | cut -f 1`,
76  )
77    .toString()
78    .trim();
79}
80
81function getHermesTarballDownloadPath(hermesTag) {
82  const hermesTagSHA = getHermesTagSHA(hermesTag);
83  return path.join(HERMES_TARBALL_DOWNLOAD_DIR, `hermes-${hermesTagSHA}.tgz`);
84}
85
86function downloadHermesSourceTarball() {
87  const hermesTag = readHermesTag();
88  const hermesTagSHA = getHermesTagSHA(hermesTag);
89  const hermesTarballDownloadPath = getHermesTarballDownloadPath(hermesTag);
90  let hermesTarballUrl = HERMES_SOURCE_TARBALL_BASE_URL + hermesTag;
91
92  if (fs.existsSync(hermesTarballDownloadPath)) {
93    return;
94  }
95
96  if (!fs.existsSync(HERMES_TARBALL_DOWNLOAD_DIR)) {
97    fs.mkdirSync(HERMES_TARBALL_DOWNLOAD_DIR, {recursive: true});
98  }
99
100  console.info(
101    `[Hermes] Downloading Hermes source code for commit ${hermesTagSHA}`,
102  );
103  try {
104    delegateSync('curl', [hermesTarballUrl, '-Lo', hermesTarballDownloadPath]);
105  } catch (error) {
106    throw new Error(`[Hermes] Failed to download Hermes tarball. ${error}`);
107  }
108}
109
110function expandHermesSourceTarball() {
111  const hermesTag = readHermesTag();
112  const hermesTagSHA = getHermesTagSHA(hermesTag);
113  const hermesTarballDownloadPath = getHermesTarballDownloadPath(hermesTag);
114
115  if (!fs.existsSync(hermesTarballDownloadPath)) {
116    throw new Error('[Hermes] Could not locate Hermes tarball.');
117  }
118
119  if (!fs.existsSync(HERMES_DIR)) {
120    fs.mkdirSync(HERMES_DIR, {recursive: true});
121  }
122  console.info(`[Hermes] Expanding Hermes tarball for commit ${hermesTagSHA}`);
123  try {
124    delegateSync('tar', [
125      '-zxf',
126      hermesTarballDownloadPath,
127      '--strip-components=1',
128      '--directory',
129      HERMES_DIR,
130    ]);
131  } catch (error) {
132    throw new Error('[Hermes] Failed to expand Hermes tarball.');
133  }
134}
135
136function copyBuildScripts() {
137  if (!fs.existsSync(SDKS_DIR)) {
138    throw new Error(
139      '[Hermes] Failed to copy Hermes build scripts, no SDKs directory found.',
140    );
141  }
142
143  if (!fs.existsSync(HERMES_DIR)) {
144    fs.mkdirSync(path.join(HERMES_DIR, 'utils'), {recursive: true});
145  }
146
147  fs.copyFileSync(
148    path.join(SDKS_DIR, 'hermes-engine', 'utils', 'build-apple-framework.sh'),
149    path.join(HERMES_DIR, 'utils', 'build-apple-framework.sh'),
150  );
151  fs.copyFileSync(
152    path.join(SDKS_DIR, 'hermes-engine', 'utils', 'build-ios-framework.sh'),
153    path.join(HERMES_DIR, 'utils', 'build-ios-framework.sh'),
154  );
155  fs.copyFileSync(
156    path.join(SDKS_DIR, 'hermes-engine', 'utils', 'build-mac-framework.sh'),
157    path.join(HERMES_DIR, 'utils', 'build-mac-framework.sh'),
158  );
159}
160
161function copyPodSpec() {
162  if (!fs.existsSync(SDKS_DIR)) {
163    throw new Error(
164      '[Hermes] Failed to copy Hermes Podspec, no SDKs directory found.',
165    );
166  }
167
168  if (!fs.existsSync(HERMES_DIR)) {
169    fs.mkdirSync(HERMES_DIR, {recursive: true});
170  }
171  const podspec = 'hermes-engine.podspec';
172  fs.copyFileSync(
173    path.join(SDKS_DIR, 'hermes-engine', podspec),
174    path.join(HERMES_DIR, podspec),
175  );
176  const utils = 'hermes-utils.rb';
177  fs.copyFileSync(
178    path.join(SDKS_DIR, 'hermes-engine', utils),
179    path.join(HERMES_DIR, utils),
180  );
181}
182
183function isTestingAgainstLocalHermesTarball() {
184  return 'HERMES_ENGINE_TARBALL_PATH' in process.env;
185}
186
187function shouldBuildHermesFromSource(isInCI) {
188  return !isTestingAgainstLocalHermesTarball() && isInCI;
189}
190
191function shouldUsePrebuiltHermesC(platform) {
192  if (platform === 'macos') {
193    return fs.existsSync(MACOS_HERMESC_PATH);
194  }
195
196  return false;
197}
198
199function configureMakeForPrebuiltHermesC() {
200  const IMPORT_HERMESC_TEMPLATE = `add_executable(native-hermesc IMPORTED)
201set_target_properties(native-hermesc PROPERTIES
202  IMPORTED_LOCATION "${MACOS_HERMESC_PATH}"
203  )`;
204
205  try {
206    fs.mkdirSync(MACOS_BIN_DIR, {recursive: true});
207    fs.writeFileSync(MACOS_IMPORT_HERMESC_PATH, IMPORT_HERMESC_TEMPLATE);
208  } catch (error) {
209    console.warn(
210      `[Hermes] Re-compiling hermesc. Unable to configure make: ${error}`,
211    );
212  }
213}
214
215function getHermesPrebuiltArtifactsTarballName(buildType) {
216  if (!buildType) {
217    throw Error('Did not specify build type.');
218  }
219  return `hermes-ios-${buildType.toLowerCase()}.tar.gz`;
220}
221
222/**
223 * Creates a tarball with the contents of the supplied directory.
224 */
225function createTarballFromDirectory(directory, filename) {
226  const args = ['-C', directory, '-czvf', filename, '.'];
227  delegateSync('tar', args);
228}
229
230function createHermesPrebuiltArtifactsTarball(
231  hermesDir,
232  buildType,
233  tarballOutputDir,
234  excludeDebugSymbols,
235) {
236  validateHermesFrameworksExist(path.join(hermesDir, 'destroot'));
237
238  if (!fs.existsSync(tarballOutputDir)) {
239    fs.mkdirSync(tarballOutputDir, {recursive: true});
240  }
241
242  let tarballTempDir;
243  try {
244    tarballTempDir = fs.mkdtempSync(
245      path.join(os.tmpdir(), 'hermes-engine-destroot-'),
246    );
247
248    let args = ['-a'];
249    if (excludeDebugSymbols) {
250      args.push('--exclude=dSYMs/');
251      args.push('--exclude=*.dSYM/');
252    }
253    args.push('./destroot');
254    args.push(tarballTempDir);
255    delegateSync('rsync', args, {
256      cwd: hermesDir,
257    });
258    if (fs.existsSync(path.join(hermesDir, 'LICENSE'))) {
259      delegateSync('cp', ['LICENSE', tarballTempDir], {cwd: hermesDir});
260    }
261  } catch (error) {
262    throw new Error(`Failed to copy destroot to tempdir: ${error}`);
263  }
264
265  const tarballFilename = path.join(
266    tarballOutputDir,
267    getHermesPrebuiltArtifactsTarballName(buildType),
268  );
269
270  try {
271    createTarballFromDirectory(tarballTempDir, tarballFilename);
272  } catch (error) {
273    throw new Error(`[Hermes] Failed to create tarball: ${error}`);
274  }
275
276  if (!fs.existsSync(tarballFilename)) {
277    throw new Error(
278      `Tarball creation failed, could not locate tarball at ${tarballFilename}`,
279    );
280  }
281
282  return tarballFilename;
283}
284
285function validateHermesFrameworksExist(destrootDir) {
286  if (
287    !fs.existsSync(
288      path.join(destrootDir, 'Library/Frameworks/macosx/hermes.framework'),
289    )
290  ) {
291    throw new Error(
292      'Error: Hermes macOS Framework not found. Are you sure Hermes has been built?',
293    );
294  }
295  if (
296    !fs.existsSync(
297      path.join(destrootDir, 'Library/Frameworks/universal/hermes.xcframework'),
298    )
299  ) {
300    throw new Error(
301      'Error: Hermes iOS XCFramework not found. Are you sure Hermes has been built?',
302    );
303  }
304}
305
306module.exports = {
307  configureMakeForPrebuiltHermesC,
308  copyBuildScripts,
309  copyPodSpec,
310  createHermesPrebuiltArtifactsTarball,
311  createTarballFromDirectory,
312  downloadHermesSourceTarball,
313  expandHermesSourceTarball,
314  getHermesTagSHA,
315  getHermesTarballDownloadPath,
316  getHermesPrebuiltArtifactsTarballName,
317  readHermesTag,
318  setHermesTag,
319  shouldBuildHermesFromSource,
320  shouldUsePrebuiltHermesC,
321};
322