current commit
261109.1 · sea: generate a compile cache for the VFS modules
'use strict';

// This tests that "useCodeCache" combined with "useVfs" embeds a compile
// cache entry for every module in the virtual file system into the
// preparation blob, and that the resulting executable runs.

const common = require('../common');

const {
  buildSEA,
  skipIfBuildSEAIsNotSupported,
  skipIfSigningIsNotSupported,
} = require('../common/sea');

skipIfBuildSEAIsNotSupported();
// buildSEA() signs the executable with signtool on Windows. Skip before the
// async main() below registers a mustCall, otherwise the test fails on the
// unmatched mustCall when it is skipped because signtool is not available.
skipIfSigningIsNotSupported();

const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { createWriteStream } = require('fs');
const { pipeline } = require('stream/promises');
const { zipFiles } = require('zlib');
const tmpdir = require('../common/tmpdir');
const fixtures = require('../common/fixtures');
const { spawnSyncAndAssert } = require('../common/child_process');

// Every entry is stored as an asset under this reserved prefix, see
// src/node_sea.cc.
const kCompileCachePrefix = 'node:sea:compile-cache/';

function countCompileCacheEntries(blobPath) {
  const blob = fs.readFileSync(blobPath).toString('latin1');
  return blob.split(kCompileCachePrefix).length - 1;
}

// Generates the preparation blob of a fixture with "useCodeCache" and returns
// the number of compile cache entries it contains.
async function generateBlob(fixtureDir, prepare) {
  tmpdir.refresh();
  fs.cpSync(fixtureDir, tmpdir.path, { recursive: true });
  await prepare?.();
  spawnSyncAndAssert(
    process.execPath,
    ['--experimental-sea-config', 'sea-config-code-cache.json'],
    {
      cwd: tmpdir.path,
      env: {
        ...process.env,
        NODE_DEBUG_NATIVE: 'SEA',
      },
    },
    {
      stderr: /Generated \d+ compile cache entries for \d+ modules/,
    });
  return countCompileCacheEntries(tmpdir.resolve('sea-prep.blob'));
}

async function main() {
  // CommonJS: sea.js, modules/math.js, modules/calculator.js and the two
  // packages under node_modules; config.json and greeting.txt are data.
  assert.strictEqual(await generateBlob(fixtures.path('sea', 'vfs')), 5);

  // ESM: main.mjs, modules/math.mjs, modules/calculator.cjs and the package.
  assert.strictEqual(await generateBlob(fixtures.path('sea', 'vfs-esm')), 4);

  // From a ZIP archive: sea.js, modules/math.js and the package.
  const zipFixture = (...args) => fixtures.path('sea', 'vfs-zip', ...args);
  assert.strictEqual(await generateBlob(zipFixture(), () => pipeline(
    zipFiles([
      [zipFixture('config.json'), 'config.json'],
      [zipFixture('greeting.txt'), 'data/greeting.txt'],
      [zipFixture('math.js'), 'modules/math.js'],
      [zipFixture('test-pkg-package.json'), 'node_modules/test-pkg/package.json'],
      [zipFixture('test-pkg-index.js'), 'node_modules/test-pkg/index.js'],
    ]),
    createWriteStream(tmpdir.resolve('assets.zip')),
  )), 3);

  // The build is not affected by the compile cache configuration of the
  // build process itself: the cache is generated although NODE_COMPILE_CACHE
  // points at an unusable location and the compile cache is disabled.
  tmpdir.refresh();
  fs.cpSync(fixtures.path('sea', 'vfs'), tmpdir.path, { recursive: true });
  spawnSyncAndAssert(
    process.execPath,
    ['--experimental-sea-config', 'sea-config-code-cache.json'],
    {
      cwd: tmpdir.path,
      env: {
        ...process.env,
        NODE_COMPILE_CACHE: path.join(tmpdir.path, 'config.json', 'nope'),
        NODE_DISABLE_COMPILE_CACHE: '1',
        NODE_DEBUG_NATIVE: undefined,
      },
    },
    {
      stderr: /Wrote single executable preparation blob/,
    });
  assert.strictEqual(
    countCompileCacheEntries(tmpdir.resolve('sea-prep.blob')), 5);

  // The executable runs with the cache embedded, and the reserved assets
  // are not visible as files of the virtual file system.
  tmpdir.refresh();
  const outputFile = buildSEA(fixtures.path('sea', 'vfs'), {
    configPath: 'sea-config-code-cache.json',
  });
  spawnSyncAndAssert(
    outputFile,
    {
      env: {
        ...process.env,
        NODE_DEBUG_NATIVE: undefined,
      },
    },
    {
      stdout(stdout) {
        assert.match(stdout, /All SEA VFS tests passed!/);
        assert.doesNotMatch(stdout, /node:sea:/);
      },
      stderr(stderr) {
        assert.doesNotMatch(stderr, /Code cache data rejected/);
        assert.doesNotMatch(stderr, /ExperimentalWarning: VirtualFileSystem/);
      },
    },
  );
}

main().then(common.mustCall());