Skip to content

Commit

Permalink
sea: add option to disable the experimental SEA warning
Browse files Browse the repository at this point in the history
Refs: nodejs/single-executable#60
Signed-off-by: Darshan Sen <raisinten@gmail.com>
  • Loading branch information
RaisinTen committed Apr 17, 2023
1 parent c94be41 commit c10ae9b
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 19 deletions.
3 changes: 2 additions & 1 deletion doc/api/single-executable-applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,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 / false // Default: false
}
```

Expand Down
4 changes: 2 additions & 2 deletions lib/internal/main/embedding.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ const {
prepareMainThreadExecution,
markBootstrapComplete,
} = require('internal/process/pre_execution');
const { isSea } = internalBinding('sea');
const { isExperimentalSeaWarningDisabled, isSea } = internalBinding('sea');
const { emitExperimentalWarning } = require('internal/util');
const { embedderRequire, embedderRunCjs } = require('internal/util/embedding');
const { getEmbedderEntryFunction } = internalBinding('mksnapshot');

prepareMainThreadExecution(false, true);
markBootstrapComplete();

if (isSea()) {
if (isSea() && !isExperimentalSeaWarningDisabled()) {
emitExperimentalWarning('Single executable application');
}

Expand Down
24 changes: 23 additions & 1 deletion src/json_parser.cc
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ bool JSONParser::Parse(const std::string& content) {
return true;
}

std::optional<std::string> JSONParser::GetTopLevelField(
std::optional<std::string> JSONParser::GetTopLevelStringField(
const std::string& field) {
Isolate* isolate = isolate_.get();
Local<Context> context = context_.Get(isolate);
Expand All @@ -77,4 +77,26 @@ std::optional<std::string> JSONParser::GetTopLevelField(
return utf8_value.ToString();
}

std::optional<bool> JSONParser::GetTopLevelBoolField(const std::string& field) {
Isolate* isolate = isolate_.get();
Local<Context> context = context_.Get(isolate);
Local<Object> content_object = content_.Get(isolate);
Local<Value> value;
// It's not a real script, so don't print the source line.
errors::PrinterTryCatch bootstrapCatch(
isolate, errors::PrinterTryCatch::kDontPrintSourceLine);
if (!content_object
->Has(context, OneByteString(isolate, field.c_str(), field.length()))
.FromMaybe(false)) {
return false;
}
if (!content_object
->Get(context, OneByteString(isolate, field.c_str(), field.length()))
.ToLocal(&value) ||
!value->IsBoolean()) {
return {};
}
return value->BooleanValue(isolate);
}

} // namespace node
3 changes: 2 additions & 1 deletion src/json_parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -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(const std::string& field);
std::optional<bool> GetTopLevelBoolField(const std::string& field);

private:
// We might want a lighter-weight JSON parser for this use case. But for now
Expand Down
65 changes: 57 additions & 8 deletions src/node_sea.cc
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,21 @@ 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() {
struct SeaResource {
std::string_view code;
bool disable_experimental_sea_warning;
};

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;
Expand All @@ -55,10 +61,22 @@ std::string_view FindSingleExecutableCode() {
#endif
uint32_t first_word = reinterpret_cast<const uint32_t*>(code)[0];
CHECK_EQ(first_word, kMagic);
bool disable_experimental_sea_warning =
reinterpret_cast<const bool*>(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 {
{code + sizeof(first_word) + sizeof(disable_experimental_sea_warning),
size - sizeof(first_word) - sizeof(disable_experimental_sea_warning)},
disable_experimental_sea_warning};
}();
return sea_code;
return sea_resource;
}

} // namespace

std::string_view FindSingleExecutableCode() {
SeaResource sea_resource = FindSingleExecutableResource();
return sea_resource.code;
}

bool IsSingleExecutable() {
Expand All @@ -69,6 +87,11 @@ void IsSingleExecutable(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(IsSingleExecutable());
}

void IsExperimentalSeaWarningDisabled(const FunctionCallbackInfo<Value>& args) {
SeaResource sea_resource = FindSingleExecutableResource();
args.GetReturnValue().Set(sea_resource.disable_experimental_sea_warning);
}

std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv) {
// Repeats argv[0] at position 1 on argv as a replacement for the missing
// entry point file path.
Expand All @@ -90,6 +113,7 @@ namespace {
struct SeaConfig {
std::string main_path;
std::string output_path;
bool disable_experimental_sea_warning;
};

std::optional<SeaConfig> ParseSingleExecutableConfig(
Expand All @@ -112,7 +136,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",
Expand All @@ -121,14 +146,25 @@ 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",
config_path);
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;
}
result.disable_experimental_sea_warning =
disable_experimental_sea_warning.value();

return result;
}

Expand All @@ -144,9 +180,17 @@ 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(sizeof(kMagic) +
sizeof(config.disable_experimental_sea_warning) +
main_script.size());
const char* pos = reinterpret_cast<const char*>(&kMagic);
sink.insert(sink.end(), pos, pos + sizeof(kMagic));
const char* disable_experimental_sea_warning =
reinterpret_cast<const char*>(&config.disable_experimental_sea_warning);
sink.insert(sink.end(),
disable_experimental_sea_warning,
disable_experimental_sea_warning +
sizeof(config.disable_experimental_sea_warning));
sink.insert(
sink.end(), main_script.data(), main_script.data() + main_script.size());

Expand Down Expand Up @@ -182,10 +226,15 @@ void Initialize(Local<Object> target,
Local<Context> context,
void* priv) {
SetMethod(context, target, "isSea", IsSingleExecutable);
SetMethod(context,
target,
"isExperimentalSeaWarningDisabled",
IsExperimentalSeaWarningDisabled);
}

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(IsSingleExecutable);
registry->Register(IsExperimentalSeaWarningDisabled);
}

} // namespace sea
Expand Down
14 changes: 9 additions & 5 deletions test/fixtures/sea.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

const { deepStrictEqual, strictEqual, throws } = require('assert');
const { dirname } = require('path');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
'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",
"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);
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');
3 changes: 2 additions & 1 deletion test/parallel/test-single-executable-application.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ module.exports = {
writeFileSync(configFile, `
{
"main": "sea.js",
"output": "sea-prep.blob"
"output": "sea-prep.blob",
"disableExperimentalSEAWarning": false
}
`);

Expand Down
Loading

0 comments on commit c10ae9b

Please sign in to comment.