diff options
author | Darshan Sen <raisinten@gmail.com> | 2023-05-04 20:57:54 +0530 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-05-04 15:27:54 +0000 |
commit | 2de10f5149c0fc25278bfb6c64f33e967ccd1741 (patch) | |
tree | 0f1dacf9e46e52ae038f725a9f4771030764d370 | |
parent | c868aad15a49d4ea20a06e47eb9cdafc0c218fb4 (diff) | |
download | node-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/CODEOWNERS | 1 | ||||
-rw-r--r-- | doc/api/single-executable-applications.md | 3 | ||||
-rw-r--r-- | lib/internal/main/embedding.js | 4 | ||||
-rw-r--r-- | src/json_parser.cc | 38 | ||||
-rw-r--r-- | src/json_parser.h | 3 | ||||
-rw-r--r-- | src/node_sea.cc | 91 | ||||
-rw-r--r-- | test/common/README.md | 16 | ||||
-rw-r--r-- | test/common/sea.js | 92 | ||||
-rw-r--r-- | test/fixtures/sea.js | 14 | ||||
-rw-r--r-- | test/parallel/test-single-executable-application.js | 124 | ||||
-rw-r--r-- | test/parallel/test-single-executable-blob-config-errors.js | 24 | ||||
-rw-r--r-- | test/parallel/test-single-executable-blob-config.js | 65 | ||||
-rw-r--r-- | test/sequential/test-single-executable-application-disable-experimental-sea-warning.js | 60 | ||||
-rw-r--r-- | test/sequential/test-single-executable-application.js | 59 |
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'); |