sea: add option to disable the experimental SEA warning

Refs: https://github.com/nodejs/single-executable/discussions/60
Signed-off-by: Darshan Sen <raisinten@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/47588
Fixes: https://github.com/nodejs/node/issues/47741
Reviewed-By: Michael Dawson <midawson@redhat.com>
Reviewed-By: Tierney Cyren <hello@bnb.im>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
This commit is contained in:
Darshan Sen authored and GitHub committed 2023-05-04 15:27:54 +00:00
1 parent c868aad15a
commit 2de10f5149
14 files changed
+443 -149

No files matched your search

+1
View File
@@ -150,6 +150,7 @@
/src/node_sea* @nodejs/single-executable
/test/fixtures/postject-copy @nodejs/single-executable
/test/parallel/test-single-executable-* @nodejs/single-executable
/test/sequential/test-single-executable-* @nodejs/single-executable
/tools/dep_updaters/update-postject.sh @nodejs/single-executable
# Permission Model
+2 -1
View File
@@ -164,7 +164,8 @@ The configuration currently reads the following top-level fields:
```json
{
"main": "/path/to/bundled/script.js",
"output": "/path/to/write/the/generated/blob.blob"
"output": "/path/to/write/the/generated/blob.blob",
"disableExperimentalSEAWarning": true // Default: false
}
```
+2 -2
View File
@@ -3,7 +3,7 @@ const {
prepareMainThreadExecution,
markBootstrapComplete,
} = require('internal/process/pre_execution');
const { isSea } = internalBinding('sea');
const { isExperimentalSeaWarningNeeded } = internalBinding('sea');
const { emitExperimentalWarning } = require('internal/util');
const { embedderRequire, embedderRunCjs } = require('internal/util/embedding');
const { getEmbedderEntryFunction } = internalBinding('mksnapshot');
@@ -11,7 +11,7 @@ const { getEmbedderEntryFunction } = internalBinding('mksnapshot');
prepareMainThreadExecution(false, true);
markBootstrapComplete();
if (isSea()) {
if (isExperimentalSeaWarningNeeded()) {
emitExperimentalWarning('Single executable application');
}
+33 -5
View File
@@ -58,8 +58,8 @@ bool JSONParser::Parse(const std::string& content) {
return true;
}
std::optional<std::string> JSONParser::GetTopLevelField(
const std::string& field) {
std::optional<std::string> JSONParser::GetTopLevelStringField(
std::string_view field) {
Isolate* isolate = isolate_.get();
Local<Context> context = context_.Get(isolate);
Local<Object> content_object = content_.Get(isolate);
@@ -67,9 +67,11 @@ std::optional<std::string> JSONParser::GetTopLevelField(
// It's not a real script, so don't print the source line.
errors::PrinterTryCatch bootstrapCatch(
isolate, errors::PrinterTryCatch::kDontPrintSourceLine);
if (!content_object
->Get(context, OneByteString(isolate, field.c_str(), field.length()))
.ToLocal(&value) ||
Local<Value> field_local;
if (!ToV8Value(context, field, isolate).ToLocal(&field_local)) {
return {};
}
if (!content_object->Get(context, field_local).ToLocal(&value) ||
!value->IsString()) {
return {};
}
@@ -77,4 +79,30 @@ std::optional<std::string> JSONParser::GetTopLevelField(
return utf8_value.ToString();
}
std::optional<bool> JSONParser::GetTopLevelBoolField(std::string_view field) {
Isolate* isolate = isolate_.get();
Local<Context> context = context_.Get(isolate);
Local<Object> content_object = content_.Get(isolate);
Local<Value> value;
bool has_field;
// It's not a real script, so don't print the source line.
errors::PrinterTryCatch bootstrapCatch(
isolate, errors::PrinterTryCatch::kDontPrintSourceLine);
Local<Value> field_local;
if (!ToV8Value(context, field, isolate).ToLocal(&field_local)) {
return {};
}
if (!content_object->Has(context, field_local).To(&has_field)) {
return {};
}
if (!has_field) {
return false;
}
if (!content_object->Get(context, field_local).ToLocal(&value) ||
!value->IsBoolean()) {
return {};
}
return value->BooleanValue(isolate);
}
} // namespace node
+2 -1
View File
@@ -18,7 +18,8 @@ class JSONParser {
JSONParser();
~JSONParser() {}
bool Parse(const std::string& content);
std::optional<std::string> GetTopLevelField(const std::string& field);
std::optional<std::string> GetTopLevelStringField(std::string_view field);
std::optional<bool> GetTopLevelBoolField(std::string_view field);
private:
// We might want a lighter-weight JSON parser for this use case. But for now
+79 -12
View File
@@ -33,15 +33,41 @@ using v8::Value;
namespace node {
namespace sea {
namespace {
// A special number that will appear at the beginning of the single executable
// preparation blobs ready to be injected into the binary. We use this to check
// that the data given to us are intended for building single executable
// applications.
static const uint32_t kMagic = 0x143da20;
const uint32_t kMagic = 0x143da20;
std::string_view FindSingleExecutableCode() {
enum class SeaFlags : uint32_t {
kDefault = 0,
kDisableExperimentalSeaWarning = 1 << 0,
};
SeaFlags operator|(SeaFlags x, SeaFlags y) {
return static_cast<SeaFlags>(static_cast<uint32_t>(x) |
static_cast<uint32_t>(y));
}
SeaFlags operator&(SeaFlags x, SeaFlags y) {
return static_cast<SeaFlags>(static_cast<uint32_t>(x) &
static_cast<uint32_t>(y));
}
SeaFlags operator|=(/* NOLINT (runtime/references) */ SeaFlags& x, SeaFlags y) {
return x = x | y;
}
struct SeaResource {
SeaFlags flags = SeaFlags::kDefault;
std::string_view code;
static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags);
};
SeaResource FindSingleExecutableResource() {
CHECK(IsSingleExecutable());
static const std::string_view sea_code = []() -> std::string_view {
static const SeaResource sea_resource = []() -> SeaResource {
size_t size;
#ifdef __APPLE__
postject_options options;
@@ -55,18 +81,40 @@ std::string_view FindSingleExecutableCode() {
#endif
uint32_t first_word = reinterpret_cast<const uint32_t*>(code)[0];
CHECK_EQ(first_word, kMagic);
SeaFlags flags{
reinterpret_cast<const SeaFlags*>(code + sizeof(first_word))[0]};
// TODO(joyeecheung): do more checks here e.g. matching the versions.
return {code + sizeof(first_word), size - sizeof(first_word)};
return {
flags,
{
code + SeaResource::kHeaderSize,
size - SeaResource::kHeaderSize,
},
};
}();
return sea_code;
return sea_resource;
}
} // namespace
std::string_view FindSingleExecutableCode() {
SeaResource sea_resource = FindSingleExecutableResource();
return sea_resource.code;
}
bool IsSingleExecutable() {
return postject_has_resource();
}
void IsSingleExecutable(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(IsSingleExecutable());
void IsExperimentalSeaWarningNeeded(const FunctionCallbackInfo<Value>& args) {
if (!IsSingleExecutable()) {
args.GetReturnValue().Set(false);
return;
}
SeaResource sea_resource = FindSingleExecutableResource();
args.GetReturnValue().Set(!static_cast<bool>(
sea_resource.flags & SeaFlags::kDisableExperimentalSeaWarning));
}
std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv) {
@@ -90,6 +138,7 @@ namespace {
struct SeaConfig {
std::string main_path;
std::string output_path;
SeaFlags flags = SeaFlags::kDefault;
};
std::optional<SeaConfig> ParseSingleExecutableConfig(
@@ -112,7 +161,8 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
return std::nullopt;
}
result.main_path = parser.GetTopLevelField("main").value_or(std::string());
result.main_path =
parser.GetTopLevelStringField("main").value_or(std::string());
if (result.main_path.empty()) {
FPrintF(stderr,
"\"main\" field of %s is not a non-empty string\n",
@@ -121,7 +171,7 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
}
result.output_path =
parser.GetTopLevelField("output").value_or(std::string());
parser.GetTopLevelStringField("output").value_or(std::string());
if (result.output_path.empty()) {
FPrintF(stderr,
"\"output\" field of %s is not a non-empty string\n",
@@ -129,6 +179,18 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
return std::nullopt;
}
std::optional<bool> disable_experimental_sea_warning =
parser.GetTopLevelBoolField("disableExperimentalSEAWarning");
if (!disable_experimental_sea_warning.has_value()) {
FPrintF(stderr,
"\"disableExperimentalSEAWarning\" field of %s is not a Boolean\n",
config_path);
return std::nullopt;
}
if (disable_experimental_sea_warning.value()) {
result.flags |= SeaFlags::kDisableExperimentalSeaWarning;
}
return result;
}
@@ -144,9 +206,11 @@ bool GenerateSingleExecutableBlob(const SeaConfig& config) {
std::vector<char> sink;
// TODO(joyeecheung): reuse the SnapshotSerializerDeserializer for this.
sink.reserve(sizeof(kMagic) + main_script.size());
sink.reserve(SeaResource::kHeaderSize + main_script.size());
const char* pos = reinterpret_cast<const char*>(&kMagic);
sink.insert(sink.end(), pos, pos + sizeof(kMagic));
pos = reinterpret_cast<const char*>(&(config.flags));
sink.insert(sink.end(), pos, pos + sizeof(SeaFlags));
sink.insert(
sink.end(), main_script.data(), main_script.data() + main_script.size());
@@ -181,11 +245,14 @@ void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
SetMethod(context, target, "isSea", IsSingleExecutable);
SetMethod(context,
target,
"isExperimentalSeaWarningNeeded",
IsExperimentalSeaWarningNeeded);
}
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(IsSingleExecutable);
registry->Register(IsExperimentalSeaWarningNeeded);
}
} // namespace sea
+16
View File
@@ -991,6 +991,22 @@ Validates the schema of a diagnostic report file whose path is specified in
Validates the schema of a diagnostic report whose content is specified in
`report`. If the report fails validation, an exception is thrown.
## SEA Module
The `sea` module provides helper functions for testing Single Executable
Application functionality.
### `skipIfSingleExecutableIsNotSupported()`
Skip the rest of the tests if single executable applications are not supported
in the current configuration.
### `injectAndCodeSign(targetExecutable, resource)`
Uses Postect to inject the contents of the file at the path `resource` into
the target executable file at the path `targetExecutable` and ultimately code
sign the final binary.
## tick Module
The `tick` module provides a helper function that can be used to call a callback
+92
View File
@@ -0,0 +1,92 @@
'use strict';
const common = require('../common');
const fixtures = require('../common/fixtures');
const { readFileSync } = require('fs');
const { execFileSync } = require('child_process');
function skipIfSingleExecutableIsNotSupported() {
if (!process.config.variables.single_executable_application)
common.skip('Single Executable Application support has been disabled.');
if (!['darwin', 'win32', 'linux'].includes(process.platform))
common.skip(`Unsupported platform ${process.platform}.`);
if (process.platform === 'linux' && process.config.variables.is_debug === 1)
common.skip('Running the resultant binary fails with `Couldn\'t read target executable"`.');
if (process.config.variables.node_shared)
common.skip('Running the resultant binary fails with ' +
'`/home/iojs/node-tmp/.tmp.2366/sea: error while loading shared libraries: ' +
'libnode.so.112: cannot open shared object file: No such file or directory`.');
if (process.config.variables.icu_gyp_path === 'tools/icu/icu-system.gyp')
common.skip('Running the resultant binary fails with ' +
'`/home/iojs/node-tmp/.tmp.2379/sea: error while loading shared libraries: ' +
'libicui18n.so.71: cannot open shared object file: No such file or directory`.');
if (!process.config.variables.node_use_openssl || process.config.variables.node_shared_openssl)
common.skip('Running the resultant binary fails with `Node.js is not compiled with OpenSSL crypto support`.');
if (process.config.variables.want_separate_host_toolset !== 0)
common.skip('Running the resultant binary fails with `Segmentation fault (core dumped)`.');
if (process.platform === 'linux') {
const osReleaseText = readFileSync('/etc/os-release', { encoding: 'utf-8' });
const isAlpine = /^NAME="Alpine Linux"/m.test(osReleaseText);
if (isAlpine) common.skip('Alpine Linux is not supported.');
if (process.arch === 's390x') {
common.skip('On s390x, postject fails with `memory access out of bounds`.');
}
if (process.arch === 'ppc64') {
common.skip('On ppc64, this test times out.');
}
}
}
function injectAndCodeSign(targetExecutable, resource) {
const postjectFile = fixtures.path('postject-copy', 'node_modules', 'postject', 'dist', 'cli.js');
execFileSync(process.execPath, [
postjectFile,
targetExecutable,
'NODE_SEA_BLOB',
resource,
'--sentinel-fuse', 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2',
...process.platform === 'darwin' ? [ '--macho-segment-name', 'NODE_SEA' ] : [],
]);
if (process.platform === 'darwin') {
execFileSync('codesign', [ '--sign', '-', targetExecutable ]);
execFileSync('codesign', [ '--verify', targetExecutable ]);
} else if (process.platform === 'win32') {
let signtoolFound = false;
try {
execFileSync('where', [ 'signtool' ]);
signtoolFound = true;
} catch (err) {
console.log(err.message);
}
if (signtoolFound) {
let certificatesFound = false;
try {
execFileSync('signtool', [ 'sign', '/fd', 'SHA256', targetExecutable ]);
certificatesFound = true;
} catch (err) {
if (!/SignTool Error: No certificates were found that met all the given criteria/.test(err)) {
throw err;
}
}
if (certificatesFound) {
execFileSync('signtool', 'verify', '/pa', 'SHA256', targetExecutable);
}
}
}
}
module.exports = {
skipIfSingleExecutableIsNotSupported,
injectAndCodeSign,
};
+8 -4
View File
@@ -3,11 +3,15 @@ const createdRequire = createRequire(__filename);
// Although, require('../common') works locally, that couldn't be used here
// because we set NODE_TEST_DIR=/Users/iojs/node-tmp on Jenkins CI.
const { expectWarning } = createdRequire(process.env.COMMON_DIRECTORY);
const { expectWarning, mustNotCall } = createdRequire(process.env.COMMON_DIRECTORY);
expectWarning('ExperimentalWarning',
'Single executable application is an experimental feature and ' +
'might change at any time');
if (createdRequire('./sea-config.json').disableExperimentalSEAWarning) {
process.on('warning', mustNotCall());
} else {
expectWarning('ExperimentalWarning',
'Single executable application is an experimental feature and ' +
'might change at any time');
}
// Should be possible to require core modules that optionally require the
// "node:" scheme.
@@ -1,124 +0,0 @@
'use strict';
const common = require('../common');
// This tests the creation of a single executable application.
const fixtures = require('../common/fixtures');
const tmpdir = require('../common/tmpdir');
const { copyFileSync, readFileSync, writeFileSync, existsSync } = require('fs');
const { execFileSync } = require('child_process');
const { join } = require('path');
const { strictEqual } = require('assert');
const assert = require('assert');
if (!process.config.variables.single_executable_application)
common.skip('Single Executable Application support has been disabled.');
if (!['darwin', 'win32', 'linux'].includes(process.platform))
common.skip(`Unsupported platform ${process.platform}.`);
if (process.platform === 'linux' && process.config.variables.is_debug === 1)
common.skip('Running the resultant binary fails with `Couldn\'t read target executable"`.');
if (process.config.variables.node_shared)
common.skip('Running the resultant binary fails with ' +
'`/home/iojs/node-tmp/.tmp.2366/sea: error while loading shared libraries: ' +
'libnode.so.112: cannot open shared object file: No such file or directory`.');
if (process.config.variables.icu_gyp_path === 'tools/icu/icu-system.gyp')
common.skip('Running the resultant binary fails with ' +
'`/home/iojs/node-tmp/.tmp.2379/sea: error while loading shared libraries: ' +
'libicui18n.so.71: cannot open shared object file: No such file or directory`.');
if (!process.config.variables.node_use_openssl || process.config.variables.node_shared_openssl)
common.skip('Running the resultant binary fails with `Node.js is not compiled with OpenSSL crypto support`.');
if (process.config.variables.want_separate_host_toolset !== 0)
common.skip('Running the resultant binary fails with `Segmentation fault (core dumped)`.');
if (process.platform === 'linux') {
const osReleaseText = readFileSync('/etc/os-release', { encoding: 'utf-8' });
const isAlpine = /^NAME="Alpine Linux"/m.test(osReleaseText);
if (isAlpine) common.skip('Alpine Linux is not supported.');
if (process.arch === 's390x') {
common.skip('On s390x, postject fails with `memory access out of bounds`.');
}
if (process.arch === 'ppc64') {
common.skip('On ppc64, this test times out.');
}
}
const inputFile = fixtures.path('sea.js');
const requirableFile = join(tmpdir.path, 'requirable.js');
const configFile = join(tmpdir.path, 'sea-config.json');
const seaPrepBlob = join(tmpdir.path, 'sea-prep.blob');
const outputFile = join(tmpdir.path, process.platform === 'win32' ? 'sea.exe' : 'sea');
tmpdir.refresh();
writeFileSync(requirableFile, `
module.exports = {
hello: 'world',
};
`);
writeFileSync(configFile, `
{
"main": "sea.js",
"output": "sea-prep.blob"
}
`);
// Copy input to working directory
copyFileSync(inputFile, join(tmpdir.path, 'sea.js'));
execFileSync(process.execPath, ['--experimental-sea-config', 'sea-config.json'], {
cwd: tmpdir.path
});
assert(existsSync(seaPrepBlob));
copyFileSync(process.execPath, outputFile);
const postjectFile = fixtures.path('postject-copy', 'node_modules', 'postject', 'dist', 'cli.js');
execFileSync(process.execPath, [
postjectFile,
outputFile,
'NODE_SEA_BLOB',
seaPrepBlob,
'--sentinel-fuse', 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2',
...process.platform === 'darwin' ? [ '--macho-segment-name', 'NODE_SEA' ] : [],
]);
if (process.platform === 'darwin') {
execFileSync('codesign', [ '--sign', '-', outputFile ]);
execFileSync('codesign', [ '--verify', outputFile ]);
} else if (process.platform === 'win32') {
let signtoolFound = false;
try {
execFileSync('where', [ 'signtool' ]);
signtoolFound = true;
} catch (err) {
console.log(err.message);
}
if (signtoolFound) {
let certificatesFound = false;
try {
execFileSync('signtool', [ 'sign', '/fd', 'SHA256', outputFile ]);
certificatesFound = true;
} catch (err) {
if (!/SignTool Error: No certificates were found that met all the given criteria/.test(err)) {
throw err;
}
}
if (certificatesFound) {
execFileSync('signtool', 'verify', '/pa', 'SHA256', outputFile);
}
}
}
const singleExecutableApplicationOutput = execFileSync(
outputFile,
[ '-a', '--b=c', 'd' ],
{ env: { COMMON_DIRECTORY: join(__dirname, '..', 'common') } });
strictEqual(singleExecutableApplicationOutput.toString(), 'Hello, world! 😊\n');
@@ -115,6 +115,30 @@ const { join } = require('path');
);
}
{
tmpdir.refresh();
const config = join(tmpdir.path, 'invalid-disableExperimentalSEAWarning.json');
writeFileSync(config, `
{
"main": "bundle.js",
"output": "sea-prep.blob",
"disableExperimentalSEAWarning": "💥"
}
`, 'utf8');
const child = spawnSync(
process.execPath,
['--experimental-sea-config', config], {
cwd: tmpdir.path,
});
const stderr = child.stderr.toString();
assert.strictEqual(child.status, 1);
assert(
stderr.includes(
`"disableExperimentalSEAWarning" field of ${config} is not a Boolean`
)
);
}
{
tmpdir.refresh();
const config = join(tmpdir.path, 'nonexistent-main-relative.json');
@@ -48,3 +48,68 @@ const { join } = require('path');
assert.strictEqual(child.status, 0);
assert(existsSync(output));
}
{
tmpdir.refresh();
const config = join(tmpdir.path, 'no-disableExperimentalSEAWarning.json');
const main = join(tmpdir.path, 'bundle.js');
const output = join(tmpdir.path, 'output.blob');
writeFileSync(main, 'console.log("hello")', 'utf-8');
const configJson = JSON.stringify({
main: 'bundle.js',
output: 'output.blob',
});
writeFileSync(config, configJson, 'utf8');
const child = spawnSync(
process.execPath,
['--experimental-sea-config', config], {
cwd: tmpdir.path,
});
assert.strictEqual(child.status, 0);
assert(existsSync(output));
}
{
tmpdir.refresh();
const config = join(tmpdir.path, 'true-disableExperimentalSEAWarning.json');
const main = join(tmpdir.path, 'bundle.js');
const output = join(tmpdir.path, 'output.blob');
writeFileSync(main, 'console.log("hello")', 'utf-8');
const configJson = JSON.stringify({
main: 'bundle.js',
output: 'output.blob',
disableExperimentalSEAWarning: true,
});
writeFileSync(config, configJson, 'utf8');
const child = spawnSync(
process.execPath,
['--experimental-sea-config', config], {
cwd: tmpdir.path,
});
assert.strictEqual(child.status, 0);
assert(existsSync(output));
}
{
tmpdir.refresh();
const config = join(tmpdir.path, 'false-disableExperimentalSEAWarning.json');
const main = join(tmpdir.path, 'bundle.js');
const output = join(tmpdir.path, 'output.blob');
writeFileSync(main, 'console.log("hello")', 'utf-8');
const configJson = JSON.stringify({
main: 'bundle.js',
output: 'output.blob',
disableExperimentalSEAWarning: false,
});
writeFileSync(config, configJson, 'utf8');
const child = spawnSync(
process.execPath,
['--experimental-sea-config', config], {
cwd: tmpdir.path,
});
assert.strictEqual(child.status, 0);
assert(existsSync(output));
}
@@ -0,0 +1,60 @@
'use strict';
require('../common');
const {
injectAndCodeSign,
skipIfSingleExecutableIsNotSupported,
} = require('../common/sea');
skipIfSingleExecutableIsNotSupported();
// This tests the creation of a single executable application which has the
// experimental SEA warning disabled.
const fixtures = require('../common/fixtures');
const tmpdir = require('../common/tmpdir');
const { copyFileSync, writeFileSync, existsSync } = require('fs');
const { execFileSync } = require('child_process');
const { join } = require('path');
const { strictEqual } = require('assert');
const assert = require('assert');
const inputFile = fixtures.path('sea.js');
const requirableFile = join(tmpdir.path, 'requirable.js');
const configFile = join(tmpdir.path, 'sea-config.json');
const seaPrepBlob = join(tmpdir.path, 'sea-prep.blob');
const outputFile = join(tmpdir.path, process.platform === 'win32' ? 'sea.exe' : 'sea');
tmpdir.refresh();
writeFileSync(requirableFile, `
module.exports = {
hello: 'world',
};
`);
writeFileSync(configFile, `
{
"main": "sea.js",
"output": "sea-prep.blob",
"disableExperimentalSEAWarning": true
}
`);
// Copy input to working directory
copyFileSync(inputFile, join(tmpdir.path, 'sea.js'));
execFileSync(process.execPath, ['--experimental-sea-config', 'sea-config.json'], {
cwd: tmpdir.path
});
assert(existsSync(seaPrepBlob));
copyFileSync(process.execPath, outputFile);
injectAndCodeSign(outputFile, seaPrepBlob);
const singleExecutableApplicationOutput = execFileSync(
outputFile,
[ '-a', '--b=c', 'd' ],
{ env: { COMMON_DIRECTORY: join(__dirname, '..', 'common') } });
strictEqual(singleExecutableApplicationOutput.toString(), 'Hello, world! 😊\n');
@@ -0,0 +1,59 @@
'use strict';
require('../common');
const {
injectAndCodeSign,
skipIfSingleExecutableIsNotSupported,
} = require('../common/sea');
skipIfSingleExecutableIsNotSupported();
// This tests the creation of a single executable application.
const fixtures = require('../common/fixtures');
const tmpdir = require('../common/tmpdir');
const { copyFileSync, writeFileSync, existsSync } = require('fs');
const { execFileSync } = require('child_process');
const { join } = require('path');
const { strictEqual } = require('assert');
const assert = require('assert');
const inputFile = fixtures.path('sea.js');
const requirableFile = join(tmpdir.path, 'requirable.js');
const configFile = join(tmpdir.path, 'sea-config.json');
const seaPrepBlob = join(tmpdir.path, 'sea-prep.blob');
const outputFile = join(tmpdir.path, process.platform === 'win32' ? 'sea.exe' : 'sea');
tmpdir.refresh();
writeFileSync(requirableFile, `
module.exports = {
hello: 'world',
};
`);
writeFileSync(configFile, `
{
"main": "sea.js",
"output": "sea-prep.blob",
"disableExperimentalSEAWarning": false
}
`);
// Copy input to working directory
copyFileSync(inputFile, join(tmpdir.path, 'sea.js'));
execFileSync(process.execPath, ['--experimental-sea-config', 'sea-config.json'], {
cwd: tmpdir.path
});
assert(existsSync(seaPrepBlob));
copyFileSync(process.execPath, outputFile);
injectAndCodeSign(outputFile, seaPrepBlob);
const singleExecutableApplicationOutput = execFileSync(
outputFile,
[ '-a', '--b=c', 'd' ],
{ env: { COMMON_DIRECTORY: join(__dirname, '..', 'common') } });
strictEqual(singleExecutableApplicationOutput.toString(), 'Hello, world! 😊\n');