| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 1a7fc18 commit c4596b9
14 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -150,6 +150,7 @@ | |||
| 150 | 150 | /src/node_sea* @nodejs/single-executable | |
| 151 | 151 | /test/fixtures/postject-copy @nodejs/single-executable | |
| 152 | 152 | /test/parallel/test-single-executable-* @nodejs/single-executable | |
| 153 | + /test/sequential/test-single-executable-* @nodejs/single-executable | ||
| 153 | 154 | /tools/dep_updaters/update-postject.sh @nodejs/single-executable | |
| 154 | 155 | ||
| 155 | 156 | # Permission Model | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -164,7 +164,8 @@ The configuration currently reads the following top-level fields: | |||
| 164 | 164 | ```json | |
| 165 | 165 | { | |
| 166 | 166 | "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 | ||
| 168 | 169 | } | |
| 169 | 170 | ``` | |
| 170 | 171 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -3,15 +3,15 @@ const { | |||
| 3 | 3 | prepareMainThreadExecution, | |
| 4 | 4 | markBootstrapComplete, | |
| 5 | 5 | } = require('internal/process/pre_execution'); | |
| 6 | - const { isSea } = internalBinding('sea'); | ||
| 6 | + const { isExperimentalSeaWarningNeeded } = internalBinding('sea'); | ||
| 7 | 7 | const { emitExperimentalWarning } = require('internal/util'); | |
| 8 | 8 | const { embedderRequire, embedderRunCjs } = require('internal/util/embedding'); | |
| 9 | 9 | const { getEmbedderEntryFunction } = internalBinding('mksnapshot'); | |
| 10 | 10 | ||
| 11 | 11 | prepareMainThreadExecution(false, true); | |
| 12 | 12 | markBootstrapComplete(); | |
| 13 | 13 | ||
| 14 | - if (isSea()) { | ||
| 14 | + if (isExperimentalSeaWarningNeeded()) { | ||
| 15 | 15 | emitExperimentalWarning('Single executable application'); | |
| 16 | 16 | } | |
| 17 | 17 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -58,23 +58,51 @@ bool JSONParser::Parse(const std::string& content) { | |||
| 58 | 58 | return true; | |
| 59 | 59 | } | |
| 60 | 60 | ||
| 61 | - std::optional<std::string> JSONParser::GetTopLevelField( | ||
| 62 | - const std::string& field) { | ||
| 61 | + std::optional<std::string> JSONParser::GetTopLevelStringField( | ||
| 62 | + std::string_view field) { | ||
| 63 | 63 | Isolate* isolate = isolate_.get(); | |
| 64 | 64 | Local<Context> context = context_.Get(isolate); | |
| 65 | 65 | Local<Object> content_object = content_.Get(isolate); | |
| 66 | 66 | Local<Value> value; | |
| 67 | 67 | // It's not a real script, so don't print the source line. | |
| 68 | 68 | errors::PrinterTryCatch bootstrapCatch( | |
| 69 | 69 | 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) || | ||
| 73 | 75 | !value->IsString()) { | |
| 74 | 76 | return {}; | |
| 75 | 77 | } | |
| 76 | 78 | Utf8Value utf8_value(isolate, value); | |
| 77 | 79 | return utf8_value.ToString(); | |
| 78 | 80 | } | |
| 79 | 81 | ||
| 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 | + | ||
| 80 | 108 | } // namespace node | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -18,7 +18,8 @@ class JSONParser { | |||
| 18 | 18 | JSONParser(); | |
| 19 | 19 | ~JSONParser() {} | |
| 20 | 20 | 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); | ||
| 22 | 23 | ||
| 23 | 24 | private: | |
| 24 | 25 | // We might want a lighter-weight JSON parser for this use case. But for now | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -33,15 +33,41 @@ using v8::Value; | |||
| 33 | 33 | namespace node { | |
| 34 | 34 | namespace sea { | |
| 35 | 35 | ||
| 36 | + namespace { | ||
| 36 | 37 | // A special number that will appear at the beginning of the single executable | |
| 37 | 38 | // preparation blobs ready to be injected into the binary. We use this to check | |
| 38 | 39 | // that the data given to us are intended for building single executable | |
| 39 | 40 | // applications. | |
| 40 | - static const uint32_t kMagic = 0x143da20; | ||
| 41 | + const uint32_t kMagic = 0x143da20; | ||
| 41 | 42 | ||
| 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() { | ||
| 43 | 69 | CHECK(IsSingleExecutable()); | |
| 44 | - static const std::string_view sea_code = []() -> std::string_view { | ||
| 70 | + static const SeaResource sea_resource = []() -> SeaResource { | ||
| 45 | 71 | size_t size; | |
| 46 | 72 | #ifdef __APPLE__ | |
| 47 | 73 | postject_options options; | |
@@ -55,18 +81,40 @@ std::string_view FindSingleExecutableCode() { | |||
| 55 | 81 | #endif | |
| 56 | 82 | uint32_t first_word = reinterpret_cast<const uint32_t*>(code)[0]; | |
| 57 | 83 | CHECK_EQ(first_word, kMagic); | |
| 84 | + SeaFlags flags{ | ||
| 85 | + reinterpret_cast<const SeaFlags*>(code + sizeof(first_word))[0]}; | ||
| 58 | 86 | // 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 | + }; | ||
| 60 | 94 | }(); | |
| 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; | ||
| 62 | 103 | } | |
| 63 | 104 | ||
| 64 | 105 | bool IsSingleExecutable() { | |
| 65 | 106 | return postject_has_resource(); | |
| 66 | 107 | } | |
| 67 | 108 | ||
| 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)); | ||
| 70 | 118 | } | |
| 71 | 119 | ||
| 72 | 120 | std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv) { | |
@@ -90,6 +138,7 @@ namespace { | |||
| 90 | 138 | struct SeaConfig { | |
| 91 | 139 | std::string main_path; | |
| 92 | 140 | std::string output_path; | |
| 141 | + SeaFlags flags = SeaFlags::kDefault; | ||
| 93 | 142 | }; | |
| 94 | 143 | ||
| 95 | 144 | std::optional<SeaConfig> ParseSingleExecutableConfig( | |
@@ -112,7 +161,8 @@ std::optional<SeaConfig> ParseSingleExecutableConfig( | |||
| 112 | 161 | return std::nullopt; | |
| 113 | 162 | } | |
| 114 | 163 | ||
| 115 | - result.main_path = parser.GetTopLevelField("main").value_or(std::string()); | ||
| 164 | + result.main_path = | ||
| 165 | + parser.GetTopLevelStringField("main").value_or(std::string()); | ||
| 116 | 166 | if (result.main_path.empty()) { | |
| 117 | 167 | FPrintF(stderr, | |
| 118 | 168 | "\"main\" field of %s is not a non-empty string\n", | |
@@ -121,14 +171,26 @@ std::optional<SeaConfig> ParseSingleExecutableConfig( | |||
| 121 | 171 | } | |
| 122 | 172 | ||
| 123 | 173 | result.output_path = | |
| 124 | - parser.GetTopLevelField("output").value_or(std::string()); | ||
| 174 | + parser.GetTopLevelStringField("output").value_or(std::string()); | ||
| 125 | 175 | if (result.output_path.empty()) { | |
| 126 | 176 | FPrintF(stderr, | |
| 127 | 177 | "\"output\" field of %s is not a non-empty string\n", | |
| 128 | 178 | config_path); | |
| 129 | 179 | return std::nullopt; | |
| 130 | 180 | } | |
| 131 | 181 | ||
| 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 | + | ||
| 132 | 194 | return result; | |
| 133 | 195 | } | |
| 134 | 196 | ||
@@ -144,9 +206,11 @@ bool GenerateSingleExecutableBlob(const SeaConfig& config) { | |||
| 144 | 206 | ||
| 145 | 207 | std::vector<char> sink; | |
| 146 | 208 | // TODO(joyeecheung): reuse the SnapshotSerializerDeserializer for this. | |
| 147 | - sink.reserve(sizeof(kMagic) + main_script.size()); | ||
| 209 | + sink.reserve(SeaResource::kHeaderSize + main_script.size()); | ||
| 148 | 210 | const char* pos = reinterpret_cast<const char*>(&kMagic); | |
| 149 | 211 | 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)); | ||
| 150 | 214 | sink.insert( | |
| 151 | 215 | sink.end(), main_script.data(), main_script.data() + main_script.size()); | |
| 152 | 216 | ||
@@ -181,11 +245,14 @@ void Initialize(Local<Object> target, | |||
| 181 | 245 | Local<Value> unused, | |
| 182 | 246 | Local<Context> context, | |
| 183 | 247 | void* priv) { | |
| 184 | - SetMethod(context, target, "isSea", IsSingleExecutable); | ||
| 248 | + SetMethod(context, | ||
| 249 | + target, | ||
| 250 | + "isExperimentalSeaWarningNeeded", | ||
| 251 | + IsExperimentalSeaWarningNeeded); | ||
| 185 | 252 | } | |
| 186 | 253 | ||
| 187 | 254 | void RegisterExternalReferences(ExternalReferenceRegistry* registry) { | |
| 188 | - registry->Register(IsSingleExecutable); | ||
| 255 | + registry->Register(IsExperimentalSeaWarningNeeded); | ||
| 189 | 256 | } | |
| 190 | 257 | ||
| 191 | 258 | } // namespace sea | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -991,6 +991,22 @@ Validates the schema of a diagnostic report file whose path is specified in | |||
| 991 | 991 | Validates the schema of a diagnostic report whose content is specified in | |
| 992 | 992 | `report`. If the report fails validation, an exception is thrown. | |
| 993 | 993 | ||
| 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 | + | ||
| 994 | 1010 | ## tick Module | |
| 995 | 1011 | ||
| 996 | 1012 | The `tick` module provides a helper function that can be used to call a callback | |
| Original file line number | Diff line number | Diff 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 | + }; | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments