Skip to content

Commit c4596b9

Browse files
RaisinTentargos
authored andcommitted
sea: add option to disable the experimental SEA warning
Refs: nodejs/single-executable#60 Signed-off-by: Darshan Sen <raisinten@gmail.com> PR-URL: #47588 Fixes: #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>
1 parent 1a7fc18 commit c4596b9

14 files changed

+444
-150
lines changed

.github/CODEOWNERS

+1
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@
150150
/src/node_sea* @nodejs/single-executable
151151
/test/fixtures/postject-copy @nodejs/single-executable
152152
/test/parallel/test-single-executable-* @nodejs/single-executable
153+
/test/sequential/test-single-executable-* @nodejs/single-executable
153154
/tools/dep_updaters/update-postject.sh @nodejs/single-executable
154155

155156
# Permission Model

doc/api/single-executable-applications.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,8 @@ The configuration currently reads the following top-level fields:
164164
```json
165165
{
166166
"main": "/path/to/bundled/script.js",
167-
"output": "/path/to/write/the/generated/blob.blob"
167+
"output": "/path/to/write/the/generated/blob.blob",
168+
"disableExperimentalSEAWarning": true // Default: false
168169
}
169170
```
170171

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 { isExperimentalSeaWarningNeeded } = 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 (isExperimentalSeaWarningNeeded()) {
1515
emitExperimentalWarning('Single executable application');
1616
}
1717

src/json_parser.cc

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

61-
std::optional<std::string> JSONParser::GetTopLevelField(
62-
const std::string& field) {
61+
std::optional<std::string> JSONParser::GetTopLevelStringField(
62+
std::string_view field) {
6363
Isolate* isolate = isolate_.get();
6464
Local<Context> context = context_.Get(isolate);
6565
Local<Object> content_object = content_.Get(isolate);
6666
Local<Value> value;
6767
// It's not a real script, so don't print the source line.
6868
errors::PrinterTryCatch bootstrapCatch(
6969
isolate, errors::PrinterTryCatch::kDontPrintSourceLine);
70-
if (!content_object
71-
->Get(context, OneByteString(isolate, field.c_str(), field.length()))
72-
.ToLocal(&value) ||
70+
Local<Value> field_local;
71+
if (!ToV8Value(context, field, isolate).ToLocal(&field_local)) {
72+
return {};
73+
}
74+
if (!content_object->Get(context, field_local).ToLocal(&value) ||
7375
!value->IsString()) {
7476
return {};
7577
}
7678
Utf8Value utf8_value(isolate, value);
7779
return utf8_value.ToString();
7880
}
7981

82+
std::optional<bool> JSONParser::GetTopLevelBoolField(std::string_view field) {
83+
Isolate* isolate = isolate_.get();
84+
Local<Context> context = context_.Get(isolate);
85+
Local<Object> content_object = content_.Get(isolate);
86+
Local<Value> value;
87+
bool has_field;
88+
// It's not a real script, so don't print the source line.
89+
errors::PrinterTryCatch bootstrapCatch(
90+
isolate, errors::PrinterTryCatch::kDontPrintSourceLine);
91+
Local<Value> field_local;
92+
if (!ToV8Value(context, field, isolate).ToLocal(&field_local)) {
93+
return {};
94+
}
95+
if (!content_object->Has(context, field_local).To(&has_field)) {
96+
return {};
97+
}
98+
if (!has_field) {
99+
return false;
100+
}
101+
if (!content_object->Get(context, field_local).ToLocal(&value) ||
102+
!value->IsBoolean()) {
103+
return {};
104+
}
105+
return value->BooleanValue(isolate);
106+
}
107+
80108
} // 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(std::string_view field);
22+
std::optional<bool> GetTopLevelBoolField(std::string_view field);
2223

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

src/node_sea.cc

+79-12
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,41 @@ 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+
enum class SeaFlags : uint32_t {
44+
kDefault = 0,
45+
kDisableExperimentalSeaWarning = 1 << 0,
46+
};
47+
48+
SeaFlags operator|(SeaFlags x, SeaFlags y) {
49+
return static_cast<SeaFlags>(static_cast<uint32_t>(x) |
50+
static_cast<uint32_t>(y));
51+
}
52+
53+
SeaFlags operator&(SeaFlags x, SeaFlags y) {
54+
return static_cast<SeaFlags>(static_cast<uint32_t>(x) &
55+
static_cast<uint32_t>(y));
56+
}
57+
58+
SeaFlags operator|=(/* NOLINT (runtime/references) */ SeaFlags& x, SeaFlags y) {
59+
return x = x | y;
60+
}
61+
62+
struct SeaResource {
63+
SeaFlags flags = SeaFlags::kDefault;
64+
std::string_view code;
65+
static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags);
66+
};
67+
68+
SeaResource FindSingleExecutableResource() {
4369
CHECK(IsSingleExecutable());
44-
static const std::string_view sea_code = []() -> std::string_view {
70+
static const SeaResource sea_resource = []() -> SeaResource {
4571
size_t size;
4672
#ifdef __APPLE__
4773
postject_options options;
@@ -55,18 +81,40 @@ std::string_view FindSingleExecutableCode() {
5581
#endif
5682
uint32_t first_word = reinterpret_cast<const uint32_t*>(code)[0];
5783
CHECK_EQ(first_word, kMagic);
84+
SeaFlags flags{
85+
reinterpret_cast<const SeaFlags*>(code + sizeof(first_word))[0]};
5886
// TODO(joyeecheung): do more checks here e.g. matching the versions.
59-
return {code + sizeof(first_word), size - sizeof(first_word)};
87+
return {
88+
flags,
89+
{
90+
code + SeaResource::kHeaderSize,
91+
size - SeaResource::kHeaderSize,
92+
},
93+
};
6094
}();
61-
return sea_code;
95+
return sea_resource;
96+
}
97+
98+
} // namespace
99+
100+
std::string_view FindSingleExecutableCode() {
101+
SeaResource sea_resource = FindSingleExecutableResource();
102+
return sea_resource.code;
62103
}
63104

64105
bool IsSingleExecutable() {
65106
return postject_has_resource();
66107
}
67108

68-
void IsSingleExecutable(const FunctionCallbackInfo<Value>& args) {
69-
args.GetReturnValue().Set(IsSingleExecutable());
109+
void IsExperimentalSeaWarningNeeded(const FunctionCallbackInfo<Value>& args) {
110+
if (!IsSingleExecutable()) {
111+
args.GetReturnValue().Set(false);
112+
return;
113+
}
114+
115+
SeaResource sea_resource = FindSingleExecutableResource();
116+
args.GetReturnValue().Set(!static_cast<bool>(
117+
sea_resource.flags & SeaFlags::kDisableExperimentalSeaWarning));
70118
}
71119

72120
std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv) {
@@ -90,6 +138,7 @@ namespace {
90138
struct SeaConfig {
91139
std::string main_path;
92140
std::string output_path;
141+
SeaFlags flags = SeaFlags::kDefault;
93142
};
94143

95144
std::optional<SeaConfig> ParseSingleExecutableConfig(
@@ -112,7 +161,8 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
112161
return std::nullopt;
113162
}
114163

115-
result.main_path = parser.GetTopLevelField("main").value_or(std::string());
164+
result.main_path =
165+
parser.GetTopLevelStringField("main").value_or(std::string());
116166
if (result.main_path.empty()) {
117167
FPrintF(stderr,
118168
"\"main\" field of %s is not a non-empty string\n",
@@ -121,14 +171,26 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
121171
}
122172

123173
result.output_path =
124-
parser.GetTopLevelField("output").value_or(std::string());
174+
parser.GetTopLevelStringField("output").value_or(std::string());
125175
if (result.output_path.empty()) {
126176
FPrintF(stderr,
127177
"\"output\" field of %s is not a non-empty string\n",
128178
config_path);
129179
return std::nullopt;
130180
}
131181

182+
std::optional<bool> disable_experimental_sea_warning =
183+
parser.GetTopLevelBoolField("disableExperimentalSEAWarning");
184+
if (!disable_experimental_sea_warning.has_value()) {
185+
FPrintF(stderr,
186+
"\"disableExperimentalSEAWarning\" field of %s is not a Boolean\n",
187+
config_path);
188+
return std::nullopt;
189+
}
190+
if (disable_experimental_sea_warning.value()) {
191+
result.flags |= SeaFlags::kDisableExperimentalSeaWarning;
192+
}
193+
132194
return result;
133195
}
134196

@@ -144,9 +206,11 @@ bool GenerateSingleExecutableBlob(const SeaConfig& config) {
144206

145207
std::vector<char> sink;
146208
// TODO(joyeecheung): reuse the SnapshotSerializerDeserializer for this.
147-
sink.reserve(sizeof(kMagic) + main_script.size());
209+
sink.reserve(SeaResource::kHeaderSize + main_script.size());
148210
const char* pos = reinterpret_cast<const char*>(&kMagic);
149211
sink.insert(sink.end(), pos, pos + sizeof(kMagic));
212+
pos = reinterpret_cast<const char*>(&(config.flags));
213+
sink.insert(sink.end(), pos, pos + sizeof(SeaFlags));
150214
sink.insert(
151215
sink.end(), main_script.data(), main_script.data() + main_script.size());
152216

@@ -181,11 +245,14 @@ void Initialize(Local<Object> target,
181245
Local<Value> unused,
182246
Local<Context> context,
183247
void* priv) {
184-
SetMethod(context, target, "isSea", IsSingleExecutable);
248+
SetMethod(context,
249+
target,
250+
"isExperimentalSeaWarningNeeded",
251+
IsExperimentalSeaWarningNeeded);
185252
}
186253

187254
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
188-
registry->Register(IsSingleExecutable);
255+
registry->Register(IsExperimentalSeaWarningNeeded);
189256
}
190257

191258
} // namespace sea

test/common/README.md

+16
Original file line numberDiff line numberDiff line change
@@ -991,6 +991,22 @@ Validates the schema of a diagnostic report file whose path is specified in
991991
Validates the schema of a diagnostic report whose content is specified in
992992
`report`. If the report fails validation, an exception is thrown.
993993

994+
## SEA Module
995+
996+
The `sea` module provides helper functions for testing Single Executable
997+
Application functionality.
998+
999+
### `skipIfSingleExecutableIsNotSupported()`
1000+
1001+
Skip the rest of the tests if single executable applications are not supported
1002+
in the current configuration.
1003+
1004+
### `injectAndCodeSign(targetExecutable, resource)`
1005+
1006+
Uses Postect to inject the contents of the file at the path `resource` into
1007+
the target executable file at the path `targetExecutable` and ultimately code
1008+
sign the final binary.
1009+
9941010
## tick Module
9951011

9961012
The `tick` module provides a helper function that can be used to call a callback

test/common/sea.js

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const fixtures = require('../common/fixtures');
5+
6+
const { readFileSync } = require('fs');
7+
const { execFileSync } = require('child_process');
8+
9+
function skipIfSingleExecutableIsNotSupported() {
10+
if (!process.config.variables.single_executable_application)
11+
common.skip('Single Executable Application support has been disabled.');
12+
13+
if (!['darwin', 'win32', 'linux'].includes(process.platform))
14+
common.skip(`Unsupported platform ${process.platform}.`);
15+
16+
if (process.platform === 'linux' && process.config.variables.is_debug === 1)
17+
common.skip('Running the resultant binary fails with `Couldn\'t read target executable"`.');
18+
19+
if (process.config.variables.node_shared)
20+
common.skip('Running the resultant binary fails with ' +
21+
'`/home/iojs/node-tmp/.tmp.2366/sea: error while loading shared libraries: ' +
22+
'libnode.so.112: cannot open shared object file: No such file or directory`.');
23+
24+
if (process.config.variables.icu_gyp_path === 'tools/icu/icu-system.gyp')
25+
common.skip('Running the resultant binary fails with ' +
26+
'`/home/iojs/node-tmp/.tmp.2379/sea: error while loading shared libraries: ' +
27+
'libicui18n.so.71: cannot open shared object file: No such file or directory`.');
28+
29+
if (!process.config.variables.node_use_openssl || process.config.variables.node_shared_openssl)
30+
common.skip('Running the resultant binary fails with `Node.js is not compiled with OpenSSL crypto support`.');
31+
32+
if (process.config.variables.want_separate_host_toolset !== 0)
33+
common.skip('Running the resultant binary fails with `Segmentation fault (core dumped)`.');
34+
35+
if (process.platform === 'linux') {
36+
const osReleaseText = readFileSync('/etc/os-release', { encoding: 'utf-8' });
37+
const isAlpine = /^NAME="Alpine Linux"/m.test(osReleaseText);
38+
if (isAlpine) common.skip('Alpine Linux is not supported.');
39+
40+
if (process.arch === 's390x') {
41+
common.skip('On s390x, postject fails with `memory access out of bounds`.');
42+
}
43+
44+
if (process.arch === 'ppc64') {
45+
common.skip('On ppc64, this test times out.');
46+
}
47+
}
48+
}
49+
50+
function injectAndCodeSign(targetExecutable, resource) {
51+
const postjectFile = fixtures.path('postject-copy', 'node_modules', 'postject', 'dist', 'cli.js');
52+
execFileSync(process.execPath, [
53+
postjectFile,
54+
targetExecutable,
55+
'NODE_SEA_BLOB',
56+
resource,
57+
'--sentinel-fuse', 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2',
58+
...process.platform === 'darwin' ? [ '--macho-segment-name', 'NODE_SEA' ] : [],
59+
]);
60+
61+
if (process.platform === 'darwin') {
62+
execFileSync('codesign', [ '--sign', '-', targetExecutable ]);
63+
execFileSync('codesign', [ '--verify', targetExecutable ]);
64+
} else if (process.platform === 'win32') {
65+
let signtoolFound = false;
66+
try {
67+
execFileSync('where', [ 'signtool' ]);
68+
signtoolFound = true;
69+
} catch (err) {
70+
console.log(err.message);
71+
}
72+
if (signtoolFound) {
73+
let certificatesFound = false;
74+
try {
75+
execFileSync('signtool', [ 'sign', '/fd', 'SHA256', targetExecutable ]);
76+
certificatesFound = true;
77+
} catch (err) {
78+
if (!/SignTool Error: No certificates were found that met all the given criteria/.test(err)) {
79+
throw err;
80+
}
81+
}
82+
if (certificatesFound) {
83+
execFileSync('signtool', 'verify', '/pa', 'SHA256', targetExecutable);
84+
}
85+
}
86+
}
87+
}
88+
89+
module.exports = {
90+
skipIfSingleExecutableIsNotSupported,
91+
injectAndCodeSign,
92+
};

0 commit comments

Comments
 (0)