summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDarshan Sen <raisinten@gmail.com>2023-05-04 20:57:54 +0530
committerGitHub <noreply@github.com>2023-05-04 15:27:54 +0000
commit2de10f5149c0fc25278bfb6c64f33e967ccd1741 (patch)
tree0f1dacf9e46e52ae038f725a9f4771030764d370
parentc868aad15a49d4ea20a06e47eb9cdafc0c218fb4 (diff)
downloadnode-new-2de10f5149c0fc25278bfb6c64f33e967ccd1741.tar.gz
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>
-rw-r--r--.github/CODEOWNERS1
-rw-r--r--doc/api/single-executable-applications.md3
-rw-r--r--lib/internal/main/embedding.js4
-rw-r--r--src/json_parser.cc38
-rw-r--r--src/json_parser.h3
-rw-r--r--src/node_sea.cc91
-rw-r--r--test/common/README.md16
-rw-r--r--test/common/sea.js92
-rw-r--r--test/fixtures/sea.js14
-rw-r--r--test/parallel/test-single-executable-application.js124
-rw-r--r--test/parallel/test-single-executable-blob-config-errors.js24
-rw-r--r--test/parallel/test-single-executable-blob-config.js65
-rw-r--r--test/sequential/test-single-executable-application-disable-experimental-sea-warning.js60
-rw-r--r--test/sequential/test-single-executable-application.js59
14 files changed, 444 insertions, 150 deletions
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 49d850eea5..ebedf0077f 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -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
diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md
index 46c3c8e87a..fe8ff1b954 100644
--- a/doc/api/single-executable-applications.md
+++ b/doc/api/single-executable-applications.md
@@ -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
}
```
diff --git a/lib/internal/main/embedding.js b/lib/internal/main/embedding.js
index aa3f06cca1..63676385b1 100644
--- a/lib/internal/main/embedding.js
+++ b/lib/internal/main/embedding.js
@@ -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');
}
diff --git a/src/json_parser.cc b/src/json_parser.cc
index 4778ea2960..a9973c0990 100644
--- a/src/json_parser.cc
+++ b/src/json_parser.cc
@@ -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
diff --git a/src/json_parser.h b/src/json_parser.h
index 41fe779298..555f539acf 100644
--- a/src/json_parser.h
+++ b/src/json_parser.h
@@ -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
diff --git a/src/node_sea.cc b/src/node_sea.cc
index 5936dc7de9..796123eae4 100644
--- a/src/node_sea.cc
+++ b/src/node_sea.cc
@@ -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
diff --git a/test/common/README.md b/test/common/README.md
index 8e89e473f2..8cf0168b4f 100644
--- a/test/common/README.md
+++ b/test/common/README.md
@@ -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
diff --git a/test/common/sea.js b/test/common/sea.js
new file mode 100644
index 0000000000..b2df249cb9
--- /dev/null
+++ b/test/common/sea.js
@@ -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,
+};
diff --git a/test/fixtures/sea.js b/test/fixtures/sea.js
index 2cd82c709e..4e1f37ce5d 100644
--- a/test/fixtures/sea.js
+++ b/test/fixtures/sea.js
@@ -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);
-
-expectWarning('ExperimentalWarning',
- 'Single executable application is an experimental feature and ' +
- 'might change at any time');
+const { expectWarning, mustNotCall } = createdRequire(process.env.COMMON_DIRECTORY);
+
+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.
diff --git a/test/parallel/test-single-executable-application.js b/test/parallel/test-single-executable-application.js
deleted file mode 100644
index 823f02bbf4..0000000000
--- a/test/parallel/test-single-executable-application.js
+++ /dev/null
@@ -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');
diff --git a/test/parallel/test-single-executable-blob-config-errors.js b/test/parallel/test-single-executable-blob-config-errors.js
index 9c4013f7dc..fd4a413339 100644
--- a/test/parallel/test-single-executable-blob-config-errors.js
+++ b/test/parallel/test-single-executable-blob-config-errors.js
@@ -117,6 +117,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');
writeFileSync(config, '{"main": "bundle.js", "output": "sea.blob"}', 'utf8');
const child = spawnSync(
diff --git a/test/parallel/test-single-executable-blob-config.js b/test/parallel/test-single-executable-blob-config.js
index 919026e97a..c96bc73520 100644
--- a/test/parallel/test-single-executable-blob-config.js
+++ b/test/parallel/test-single-executable-blob-config.js
@@ -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));
+}
diff --git a/test/sequential/test-single-executable-application-disable-experimental-sea-warning.js b/test/sequential/test-single-executable-application-disable-experimental-sea-warning.js
new file mode 100644
index 0000000000..a20dce8398
--- /dev/null
+++ b/test/sequential/test-single-executable-application-disable-experimental-sea-warning.js
@@ -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');
diff --git a/test/sequential/test-single-executable-application.js b/test/sequential/test-single-executable-application.js
new file mode 100644
index 0000000000..99d0c0d6e3
--- /dev/null
+++ b/test/sequential/test-single-executable-application.js
@@ -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');