Skip to content

Commit c10ae9b

Browse files
committed
sea: add option to disable the experimental SEA warning
Refs: nodejs/single-executable#60 Signed-off-by: Darshan Sen <raisinten@gmail.com>
1 parent c94be41 commit c10ae9b

10 files changed

+311
-19
lines changed

doc/api/single-executable-applications.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ The configuration currently reads the following top-level fields:
128128
```json
129129
{
130130
"main": "/path/to/bundled/script.js",
131-
"output": "/path/to/write/the/generated/blob.blob"
131+
"output": "/path/to/write/the/generated/blob.blob",
132+
"disableExperimentalSEAWarning": true / false // Default: false
132133
}
133134
```
134135

lib/internal/main/embedding.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ const {
33
prepareMainThreadExecution,
44
markBootstrapComplete,
55
} = require('internal/process/pre_execution');
6-
const { isSea } = internalBinding('sea');
6+
const { isExperimentalSeaWarningDisabled, isSea } = internalBinding('sea');
77
const { emitExperimentalWarning } = require('internal/util');
88
const { embedderRequire, embedderRunCjs } = require('internal/util/embedding');
99
const { getEmbedderEntryFunction } = internalBinding('mksnapshot');
1010

1111
prepareMainThreadExecution(false, true);
1212
markBootstrapComplete();
1313

14-
if (isSea()) {
14+
if (isSea() && !isExperimentalSeaWarningDisabled()) {
1515
emitExperimentalWarning('Single executable application');
1616
}
1717

src/json_parser.cc

+23-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ bool JSONParser::Parse(const std::string& content) {
5858
return true;
5959
}
6060

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

80+
std::optional<bool> JSONParser::GetTopLevelBoolField(const std::string& field) {
81+
Isolate* isolate = isolate_.get();
82+
Local<Context> context = context_.Get(isolate);
83+
Local<Object> content_object = content_.Get(isolate);
84+
Local<Value> value;
85+
// It's not a real script, so don't print the source line.
86+
errors::PrinterTryCatch bootstrapCatch(
87+
isolate, errors::PrinterTryCatch::kDontPrintSourceLine);
88+
if (!content_object
89+
->Has(context, OneByteString(isolate, field.c_str(), field.length()))
90+
.FromMaybe(false)) {
91+
return false;
92+
}
93+
if (!content_object
94+
->Get(context, OneByteString(isolate, field.c_str(), field.length()))
95+
.ToLocal(&value) ||
96+
!value->IsBoolean()) {
97+
return {};
98+
}
99+
return value->BooleanValue(isolate);
100+
}
101+
80102
} // namespace node

src/json_parser.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ class JSONParser {
1818
JSONParser();
1919
~JSONParser() {}
2020
bool Parse(const std::string& content);
21-
std::optional<std::string> GetTopLevelField(const std::string& field);
21+
std::optional<std::string> GetTopLevelStringField(const std::string& field);
22+
std::optional<bool> GetTopLevelBoolField(const std::string& field);
2223

2324
private:
2425
// We might want a lighter-weight JSON parser for this use case. But for now

src/node_sea.cc

+57-8
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,21 @@ using v8::Value;
3333
namespace node {
3434
namespace sea {
3535

36+
namespace {
3637
// A special number that will appear at the beginning of the single executable
3738
// preparation blobs ready to be injected into the binary. We use this to check
3839
// that the data given to us are intended for building single executable
3940
// applications.
40-
static const uint32_t kMagic = 0x143da20;
41+
const uint32_t kMagic = 0x143da20;
4142

42-
std::string_view FindSingleExecutableCode() {
43+
struct SeaResource {
44+
std::string_view code;
45+
bool disable_experimental_sea_warning;
46+
};
47+
48+
SeaResource FindSingleExecutableResource() {
4349
CHECK(IsSingleExecutable());
44-
static const std::string_view sea_code = []() -> std::string_view {
50+
static const SeaResource sea_resource = []() -> SeaResource {
4551
size_t size;
4652
#ifdef __APPLE__
4753
postject_options options;
@@ -55,10 +61,22 @@ std::string_view FindSingleExecutableCode() {
5561
#endif
5662
uint32_t first_word = reinterpret_cast<const uint32_t*>(code)[0];
5763
CHECK_EQ(first_word, kMagic);
64+
bool disable_experimental_sea_warning =
65+
reinterpret_cast<const bool*>(code + sizeof(first_word))[0];
5866
// TODO(joyeecheung): do more checks here e.g. matching the versions.
59-
return {code + sizeof(first_word), size - sizeof(first_word)};
67+
return {
68+
{code + sizeof(first_word) + sizeof(disable_experimental_sea_warning),
69+
size - sizeof(first_word) - sizeof(disable_experimental_sea_warning)},
70+
disable_experimental_sea_warning};
6071
}();
61-
return sea_code;
72+
return sea_resource;
73+
}
74+
75+
} // namespace
76+
77+
std::string_view FindSingleExecutableCode() {
78+
SeaResource sea_resource = FindSingleExecutableResource();
79+
return sea_resource.code;
6280
}
6381

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

90+
void IsExperimentalSeaWarningDisabled(const FunctionCallbackInfo<Value>& args) {
91+
SeaResource sea_resource = FindSingleExecutableResource();
92+
args.GetReturnValue().Set(sea_resource.disable_experimental_sea_warning);
93+
}
94+
7295
std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv) {
7396
// Repeats argv[0] at position 1 on argv as a replacement for the missing
7497
// entry point file path.
@@ -90,6 +113,7 @@ namespace {
90113
struct SeaConfig {
91114
std::string main_path;
92115
std::string output_path;
116+
bool disable_experimental_sea_warning;
93117
};
94118

95119
std::optional<SeaConfig> ParseSingleExecutableConfig(
@@ -112,7 +136,8 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
112136
return std::nullopt;
113137
}
114138

115-
result.main_path = parser.GetTopLevelField("main").value_or(std::string());
139+
result.main_path =
140+
parser.GetTopLevelStringField("main").value_or(std::string());
116141
if (result.main_path.empty()) {
117142
FPrintF(stderr,
118143
"\"main\" field of %s is not a non-empty string\n",
@@ -121,14 +146,25 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
121146
}
122147

123148
result.output_path =
124-
parser.GetTopLevelField("output").value_or(std::string());
149+
parser.GetTopLevelStringField("output").value_or(std::string());
125150
if (result.output_path.empty()) {
126151
FPrintF(stderr,
127152
"\"output\" field of %s is not a non-empty string\n",
128153
config_path);
129154
return std::nullopt;
130155
}
131156

157+
std::optional<bool> disable_experimental_sea_warning =
158+
parser.GetTopLevelBoolField("disableExperimentalSEAWarning");
159+
if (!disable_experimental_sea_warning.has_value()) {
160+
FPrintF(stderr,
161+
"\"disableExperimentalSEAWarning\" field of %s is not a Boolean\n",
162+
config_path);
163+
return std::nullopt;
164+
}
165+
result.disable_experimental_sea_warning =
166+
disable_experimental_sea_warning.value();
167+
132168
return result;
133169
}
134170

@@ -144,9 +180,17 @@ bool GenerateSingleExecutableBlob(const SeaConfig& config) {
144180

145181
std::vector<char> sink;
146182
// TODO(joyeecheung): reuse the SnapshotSerializerDeserializer for this.
147-
sink.reserve(sizeof(kMagic) + main_script.size());
183+
sink.reserve(sizeof(kMagic) +
184+
sizeof(config.disable_experimental_sea_warning) +
185+
main_script.size());
148186
const char* pos = reinterpret_cast<const char*>(&kMagic);
149187
sink.insert(sink.end(), pos, pos + sizeof(kMagic));
188+
const char* disable_experimental_sea_warning =
189+
reinterpret_cast<const char*>(&config.disable_experimental_sea_warning);
190+
sink.insert(sink.end(),
191+
disable_experimental_sea_warning,
192+
disable_experimental_sea_warning +
193+
sizeof(config.disable_experimental_sea_warning));
150194
sink.insert(
151195
sink.end(), main_script.data(), main_script.data() + main_script.size());
152196

@@ -182,10 +226,15 @@ void Initialize(Local<Object> target,
182226
Local<Context> context,
183227
void* priv) {
184228
SetMethod(context, target, "isSea", IsSingleExecutable);
229+
SetMethod(context,
230+
target,
231+
"isExperimentalSeaWarningDisabled",
232+
IsExperimentalSeaWarningDisabled);
185233
}
186234

187235
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
188236
registry->Register(IsSingleExecutable);
237+
registry->Register(IsExperimentalSeaWarningDisabled);
189238
}
190239

191240
} // namespace sea

test/fixtures/sea.js

+9-5
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@ const createdRequire = createRequire(__filename);
33

44
// Although, require('../common') works locally, that couldn't be used here
55
// because we set NODE_TEST_DIR=/Users/iojs/node-tmp on Jenkins CI.
6-
const { expectWarning } = createdRequire(process.env.COMMON_DIRECTORY);
7-
8-
expectWarning('ExperimentalWarning',
9-
'Single executable application is an experimental feature and ' +
10-
'might change at any time');
6+
const { expectWarning, mustNotCall } = createdRequire(process.env.COMMON_DIRECTORY);
7+
8+
if (createdRequire('./sea-config.json').disableExperimentalSEAWarning) {
9+
process.on('warning', mustNotCall());
10+
} else {
11+
expectWarning('ExperimentalWarning',
12+
'Single executable application is an experimental feature and ' +
13+
'might change at any time');
14+
}
1115

1216
const { deepStrictEqual, strictEqual, throws } = require('assert');
1317
const { dirname } = require('path');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
'use strict';
2+
const common = require('../common');
3+
4+
// This tests the creation of a single executable application.
5+
6+
const fixtures = require('../common/fixtures');
7+
const tmpdir = require('../common/tmpdir');
8+
const { copyFileSync, readFileSync, writeFileSync, existsSync } = require('fs');
9+
const { execFileSync } = require('child_process');
10+
const { join } = require('path');
11+
const { strictEqual } = require('assert');
12+
const assert = require('assert');
13+
14+
if (!process.config.variables.single_executable_application)
15+
common.skip('Single Executable Application support has been disabled.');
16+
17+
if (!['darwin', 'win32', 'linux'].includes(process.platform))
18+
common.skip(`Unsupported platform ${process.platform}.`);
19+
20+
if (process.platform === 'linux' && process.config.variables.is_debug === 1)
21+
common.skip('Running the resultant binary fails with `Couldn\'t read target executable"`.');
22+
23+
if (process.config.variables.node_shared)
24+
common.skip('Running the resultant binary fails with ' +
25+
'`/home/iojs/node-tmp/.tmp.2366/sea: error while loading shared libraries: ' +
26+
'libnode.so.112: cannot open shared object file: No such file or directory`.');
27+
28+
if (process.config.variables.icu_gyp_path === 'tools/icu/icu-system.gyp')
29+
common.skip('Running the resultant binary fails with ' +
30+
'`/home/iojs/node-tmp/.tmp.2379/sea: error while loading shared libraries: ' +
31+
'libicui18n.so.71: cannot open shared object file: No such file or directory`.');
32+
33+
if (!process.config.variables.node_use_openssl || process.config.variables.node_shared_openssl)
34+
common.skip('Running the resultant binary fails with `Node.js is not compiled with OpenSSL crypto support`.');
35+
36+
if (process.config.variables.want_separate_host_toolset !== 0)
37+
common.skip('Running the resultant binary fails with `Segmentation fault (core dumped)`.');
38+
39+
if (process.platform === 'linux') {
40+
const osReleaseText = readFileSync('/etc/os-release', { encoding: 'utf-8' });
41+
const isAlpine = /^NAME="Alpine Linux"/m.test(osReleaseText);
42+
if (isAlpine) common.skip('Alpine Linux is not supported.');
43+
44+
if (process.arch === 's390x') {
45+
common.skip('On s390x, postject fails with `memory access out of bounds`.');
46+
}
47+
48+
if (process.arch === 'ppc64') {
49+
common.skip('On ppc64, this test times out.');
50+
}
51+
}
52+
53+
const inputFile = fixtures.path('sea.js');
54+
const requirableFile = join(tmpdir.path, 'requirable.js');
55+
const configFile = join(tmpdir.path, 'sea-config.json');
56+
const seaPrepBlob = join(tmpdir.path, 'sea-prep.blob');
57+
const outputFile = join(tmpdir.path, process.platform === 'win32' ? 'sea.exe' : 'sea');
58+
59+
tmpdir.refresh();
60+
61+
writeFileSync(requirableFile, `
62+
module.exports = {
63+
hello: 'world',
64+
};
65+
`);
66+
67+
writeFileSync(configFile, `
68+
{
69+
"main": "sea.js",
70+
"output": "sea-prep.blob",
71+
"disableExperimentalSEAWarning": true
72+
}
73+
`);
74+
75+
// Copy input to working directory
76+
copyFileSync(inputFile, join(tmpdir.path, 'sea.js'));
77+
execFileSync(process.execPath, ['--experimental-sea-config', 'sea-config.json'], {
78+
cwd: tmpdir.path
79+
});
80+
81+
assert(existsSync(seaPrepBlob));
82+
83+
copyFileSync(process.execPath, outputFile);
84+
const postjectFile = fixtures.path('postject-copy', 'node_modules', 'postject', 'dist', 'cli.js');
85+
execFileSync(process.execPath, [
86+
postjectFile,
87+
outputFile,
88+
'NODE_SEA_BLOB',
89+
seaPrepBlob,
90+
'--sentinel-fuse', 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2',
91+
...process.platform === 'darwin' ? [ '--macho-segment-name', 'NODE_SEA' ] : [],
92+
]);
93+
94+
if (process.platform === 'darwin') {
95+
execFileSync('codesign', [ '--sign', '-', outputFile ]);
96+
execFileSync('codesign', [ '--verify', outputFile ]);
97+
} else if (process.platform === 'win32') {
98+
let signtoolFound = false;
99+
try {
100+
execFileSync('where', [ 'signtool' ]);
101+
signtoolFound = true;
102+
} catch (err) {
103+
console.log(err.message);
104+
}
105+
if (signtoolFound) {
106+
let certificatesFound = false;
107+
try {
108+
execFileSync('signtool', [ 'sign', '/fd', 'SHA256', outputFile ]);
109+
certificatesFound = true;
110+
} catch (err) {
111+
if (!/SignTool Error: No certificates were found that met all the given criteria/.test(err)) {
112+
throw err;
113+
}
114+
}
115+
if (certificatesFound) {
116+
execFileSync('signtool', 'verify', '/pa', 'SHA256', outputFile);
117+
}
118+
}
119+
}
120+
121+
const singleExecutableApplicationOutput = execFileSync(
122+
outputFile,
123+
[ '-a', '--b=c', 'd' ],
124+
{ env: { COMMON_DIRECTORY: join(__dirname, '..', 'common') } });
125+
strictEqual(singleExecutableApplicationOutput.toString(), 'Hello, world! 😊\n');

test/parallel/test-single-executable-application.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ module.exports = {
6767
writeFileSync(configFile, `
6868
{
6969
"main": "sea.js",
70-
"output": "sea-prep.blob"
70+
"output": "sea-prep.blob",
71+
"disableExperimentalSEAWarning": false
7172
}
7273
`);
7374

0 commit comments

Comments
 (0)