diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 49d850eea51a7a..ebedf0077f5627 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 46c3c8e87afe95..fe8ff1b9545783 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 aa3f06cca10f99..63676385b14e8c 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 4778ea2960361a..a9973c099087e5 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 JSONParser::GetTopLevelField( - const std::string& field) { +std::optional JSONParser::GetTopLevelStringField( + std::string_view field) { Isolate* isolate = isolate_.get(); Local context = context_.Get(isolate); Local content_object = content_.Get(isolate); @@ -67,9 +67,11 @@ std::optional 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 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 JSONParser::GetTopLevelField( return utf8_value.ToString(); } +std::optional JSONParser::GetTopLevelBoolField(std::string_view field) { + Isolate* isolate = isolate_.get(); + Local context = context_.Get(isolate); + Local content_object = content_.Get(isolate); + Local 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 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 41fe77929882c9..555f539acf3076 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 GetTopLevelField(const std::string& field); + std::optional GetTopLevelStringField(std::string_view field); + std::optional 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 5936dc7de9a0d2..796123eae47bd7 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(static_cast(x) | + static_cast(y)); +} + +SeaFlags operator&(SeaFlags x, SeaFlags y) { + return static_cast(static_cast(x) & + static_cast(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(code)[0]; CHECK_EQ(first_word, kMagic); + SeaFlags flags{ + reinterpret_cast(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& args) { - args.GetReturnValue().Set(IsSingleExecutable()); +void IsExperimentalSeaWarningNeeded(const FunctionCallbackInfo& args) { + if (!IsSingleExecutable()) { + args.GetReturnValue().Set(false); + return; + } + + SeaResource sea_resource = FindSingleExecutableResource(); + args.GetReturnValue().Set(!static_cast( + sea_resource.flags & SeaFlags::kDisableExperimentalSeaWarning)); } std::tuple 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 ParseSingleExecutableConfig( @@ -112,7 +161,8 @@ std::optional 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 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 ParseSingleExecutableConfig( return std::nullopt; } + std::optional 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 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(&kMagic); sink.insert(sink.end(), pos, pos + sizeof(kMagic)); + pos = reinterpret_cast(&(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 target, Local unused, Local 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 8e89e473f2aed5..8cf0168b4fc0a7 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 00000000000000..b2df249cb9fcc3 --- /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 2cd82c709ea157..4e1f37ce5d8fad 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 823f02bbf4cdc9..00000000000000 --- 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 9c4013f7dc6f66..fd4a4133399ab6 100644 --- a/test/parallel/test-single-executable-blob-config-errors.js +++ b/test/parallel/test-single-executable-blob-config-errors.js @@ -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'); diff --git a/test/parallel/test-single-executable-blob-config.js b/test/parallel/test-single-executable-blob-config.js index 919026e97aeae7..c96bc735204ddb 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 00000000000000..a20dce83988228 --- /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 00000000000000..99d0c0d6e352dc --- /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');