This adds a new API to allow the bundled script in SEA to query the list of assets. PR-URL: https://github.com/nodejs/node/pull/59661 Refs: https://github.com/nodejs/single-executable/discussions/112 Reviewed-By: Darshan Sen <raisinten@gmail.com>
896 lines
30 KiB
C++
896 lines
30 KiB
C++
#include "node_sea.h"
|
|
|
|
#include "blob_serializer_deserializer-inl.h"
|
|
#include "debug_utils-inl.h"
|
|
#include "env-inl.h"
|
|
#include "node_contextify.h"
|
|
#include "node_errors.h"
|
|
#include "node_external_reference.h"
|
|
#include "node_internals.h"
|
|
#include "node_options.h"
|
|
#include "node_snapshot_builder.h"
|
|
#include "node_union_bytes.h"
|
|
#include "node_v8_platform-inl.h"
|
|
#include "simdjson.h"
|
|
#include "util-inl.h"
|
|
|
|
// The POSTJECT_SENTINEL_FUSE macro is a string of random characters selected by
|
|
// the Node.js project that is present only once in the entire binary. It is
|
|
// used by the postject_has_resource() function to efficiently detect if a
|
|
// resource has been injected. See
|
|
// https://github.com/nodejs/postject/blob/35343439cac8c488f2596d7c4c1dddfec1fddcae/postject-api.h#L42-L45.
|
|
#define POSTJECT_SENTINEL_FUSE "NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2"
|
|
#include "postject-api.h"
|
|
#undef POSTJECT_SENTINEL_FUSE
|
|
|
|
#include <memory>
|
|
#include <string_view>
|
|
#include <tuple>
|
|
#include <vector>
|
|
|
|
using node::ExitCode;
|
|
using v8::Array;
|
|
using v8::ArrayBuffer;
|
|
using v8::BackingStore;
|
|
using v8::Context;
|
|
using v8::Function;
|
|
using v8::FunctionCallbackInfo;
|
|
using v8::HandleScope;
|
|
using v8::Isolate;
|
|
using v8::Local;
|
|
using v8::LocalVector;
|
|
using v8::MaybeLocal;
|
|
using v8::NewStringType;
|
|
using v8::Object;
|
|
using v8::ScriptCompiler;
|
|
using v8::ScriptOrigin;
|
|
using v8::String;
|
|
using v8::Value;
|
|
|
|
namespace node {
|
|
namespace sea {
|
|
|
|
namespace {
|
|
|
|
SeaFlags operator|(SeaFlags x, SeaFlags y) {
|
|
return static_cast<SeaFlags>(static_cast<uint32_t>(x) |
|
|
static_cast<uint32_t>(y));
|
|
}
|
|
|
|
SeaFlags operator&(SeaFlags x, SeaFlags y) {
|
|
return static_cast<SeaFlags>(static_cast<uint32_t>(x) &
|
|
static_cast<uint32_t>(y));
|
|
}
|
|
|
|
SeaFlags operator|=(/* NOLINT (runtime/references) */ SeaFlags& x, SeaFlags y) {
|
|
return x = x | y;
|
|
}
|
|
|
|
class SeaSerializer : public BlobSerializer<SeaSerializer> {
|
|
public:
|
|
SeaSerializer()
|
|
: BlobSerializer<SeaSerializer>(
|
|
per_process::enabled_debug_list.enabled(DebugCategory::SEA)) {}
|
|
|
|
template <typename T,
|
|
std::enable_if_t<!std::is_same<T, std::string>::value>* = nullptr,
|
|
std::enable_if_t<!std::is_arithmetic<T>::value>* = nullptr>
|
|
size_t Write(const T& data);
|
|
};
|
|
|
|
template <>
|
|
size_t SeaSerializer::Write(const SeaResource& sea) {
|
|
sink.reserve(SeaResource::kHeaderSize + sea.main_code_or_snapshot.size());
|
|
|
|
Debug("Write SEA magic %x\n", kMagic);
|
|
size_t written_total = WriteArithmetic<uint32_t>(kMagic);
|
|
|
|
uint32_t flags = static_cast<uint32_t>(sea.flags);
|
|
Debug("Write SEA flags %x\n", flags);
|
|
written_total += WriteArithmetic<uint32_t>(flags);
|
|
|
|
Debug("Write SEA resource exec argv extension %u\n",
|
|
static_cast<uint8_t>(sea.exec_argv_extension));
|
|
written_total +=
|
|
WriteArithmetic<uint8_t>(static_cast<uint8_t>(sea.exec_argv_extension));
|
|
DCHECK_EQ(written_total, SeaResource::kHeaderSize);
|
|
|
|
Debug("Write SEA code path %p, size=%zu\n",
|
|
sea.code_path.data(),
|
|
sea.code_path.size());
|
|
written_total +=
|
|
WriteStringView(sea.code_path, StringLogMode::kAddressAndContent);
|
|
|
|
Debug("Write SEA resource %s %p, size=%zu\n",
|
|
sea.use_snapshot() ? "snapshot" : "code",
|
|
sea.main_code_or_snapshot.data(),
|
|
sea.main_code_or_snapshot.size());
|
|
written_total +=
|
|
WriteStringView(sea.main_code_or_snapshot,
|
|
sea.use_snapshot() ? StringLogMode::kAddressOnly
|
|
: StringLogMode::kAddressAndContent);
|
|
|
|
if (sea.code_cache.has_value()) {
|
|
Debug("Write SEA resource code cache %p, size=%zu\n",
|
|
sea.code_cache->data(),
|
|
sea.code_cache->size());
|
|
written_total +=
|
|
WriteStringView(sea.code_cache.value(), StringLogMode::kAddressOnly);
|
|
}
|
|
|
|
if (!sea.assets.empty()) {
|
|
Debug("Write SEA resource assets size %zu\n", sea.assets.size());
|
|
written_total += WriteArithmetic<size_t>(sea.assets.size());
|
|
for (auto const& [key, content] : sea.assets) {
|
|
Debug("Write SEA resource asset %s at %p, size=%zu\n",
|
|
key,
|
|
content.data(),
|
|
content.size());
|
|
written_total += WriteStringView(key, StringLogMode::kAddressAndContent);
|
|
written_total += WriteStringView(content, StringLogMode::kAddressOnly);
|
|
}
|
|
}
|
|
|
|
if (static_cast<bool>(sea.flags & SeaFlags::kIncludeExecArgv)) {
|
|
Debug("Write SEA resource exec argv size %zu\n", sea.exec_argv.size());
|
|
written_total += WriteArithmetic<size_t>(sea.exec_argv.size());
|
|
for (const auto& arg : sea.exec_argv) {
|
|
Debug("Write SEA resource exec arg %s at %p, size=%zu\n",
|
|
arg.data(),
|
|
arg.data(),
|
|
arg.size());
|
|
written_total += WriteStringView(arg, StringLogMode::kAddressAndContent);
|
|
}
|
|
}
|
|
return written_total;
|
|
}
|
|
|
|
class SeaDeserializer : public BlobDeserializer<SeaDeserializer> {
|
|
public:
|
|
explicit SeaDeserializer(std::string_view v)
|
|
: BlobDeserializer<SeaDeserializer>(
|
|
per_process::enabled_debug_list.enabled(DebugCategory::SEA), v) {}
|
|
|
|
template <typename T,
|
|
std::enable_if_t<!std::is_same<T, std::string>::value>* = nullptr,
|
|
std::enable_if_t<!std::is_arithmetic<T>::value>* = nullptr>
|
|
T Read();
|
|
};
|
|
|
|
template <>
|
|
SeaResource SeaDeserializer::Read() {
|
|
uint32_t magic = ReadArithmetic<uint32_t>();
|
|
Debug("Read SEA magic %x\n", magic);
|
|
|
|
CHECK_EQ(magic, kMagic);
|
|
SeaFlags flags(static_cast<SeaFlags>(ReadArithmetic<uint32_t>()));
|
|
Debug("Read SEA flags %x\n", static_cast<uint32_t>(flags));
|
|
|
|
uint8_t extension_value = ReadArithmetic<uint8_t>();
|
|
SeaExecArgvExtension exec_argv_extension =
|
|
static_cast<SeaExecArgvExtension>(extension_value);
|
|
Debug("Read SEA resource exec argv extension %u\n", extension_value);
|
|
CHECK_EQ(read_total, SeaResource::kHeaderSize);
|
|
|
|
std::string_view code_path =
|
|
ReadStringView(StringLogMode::kAddressAndContent);
|
|
Debug(
|
|
"Read SEA code path %p, size=%zu\n", code_path.data(), code_path.size());
|
|
|
|
bool use_snapshot = static_cast<bool>(flags & SeaFlags::kUseSnapshot);
|
|
std::string_view code =
|
|
ReadStringView(use_snapshot ? StringLogMode::kAddressOnly
|
|
: StringLogMode::kAddressAndContent);
|
|
|
|
Debug("Read SEA resource %s %p, size=%zu\n",
|
|
use_snapshot ? "snapshot" : "code",
|
|
code.data(),
|
|
code.size());
|
|
|
|
std::string_view code_cache;
|
|
if (static_cast<bool>(flags & SeaFlags::kUseCodeCache)) {
|
|
code_cache = ReadStringView(StringLogMode::kAddressOnly);
|
|
Debug("Read SEA resource code cache %p, size=%zu\n",
|
|
code_cache.data(),
|
|
code_cache.size());
|
|
}
|
|
|
|
std::unordered_map<std::string_view, std::string_view> assets;
|
|
if (static_cast<bool>(flags & SeaFlags::kIncludeAssets)) {
|
|
size_t assets_size = ReadArithmetic<size_t>();
|
|
Debug("Read SEA resource assets size %zu\n", assets_size);
|
|
for (size_t i = 0; i < assets_size; ++i) {
|
|
std::string_view key = ReadStringView(StringLogMode::kAddressAndContent);
|
|
std::string_view content = ReadStringView(StringLogMode::kAddressOnly);
|
|
Debug("Read SEA resource asset %s at %p, size=%zu\n",
|
|
key,
|
|
content.data(),
|
|
content.size());
|
|
assets.emplace(key, content);
|
|
}
|
|
}
|
|
|
|
std::vector<std::string_view> exec_argv;
|
|
if (static_cast<bool>(flags & SeaFlags::kIncludeExecArgv)) {
|
|
size_t exec_argv_size = ReadArithmetic<size_t>();
|
|
Debug("Read SEA resource exec args size %zu\n", exec_argv_size);
|
|
exec_argv.reserve(exec_argv_size);
|
|
for (size_t i = 0; i < exec_argv_size; ++i) {
|
|
std::string_view arg = ReadStringView(StringLogMode::kAddressAndContent);
|
|
Debug("Read SEA resource exec arg %s at %p, size=%zu\n",
|
|
arg.data(),
|
|
arg.data(),
|
|
arg.size());
|
|
exec_argv.emplace_back(arg);
|
|
}
|
|
}
|
|
return {flags,
|
|
exec_argv_extension,
|
|
code_path,
|
|
code,
|
|
code_cache,
|
|
assets,
|
|
exec_argv};
|
|
}
|
|
|
|
std::string_view FindSingleExecutableBlob() {
|
|
#if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION)
|
|
CHECK(IsSingleExecutable());
|
|
static const std::string_view result = []() -> std::string_view {
|
|
size_t size;
|
|
#ifdef __APPLE__
|
|
postject_options options;
|
|
postject_options_init(&options);
|
|
options.macho_segment_name = "NODE_SEA";
|
|
const char* blob = static_cast<const char*>(
|
|
postject_find_resource("NODE_SEA_BLOB", &size, &options));
|
|
#else
|
|
const char* blob = static_cast<const char*>(
|
|
postject_find_resource("NODE_SEA_BLOB", &size, nullptr));
|
|
#endif
|
|
return {blob, size};
|
|
}();
|
|
per_process::Debug(DebugCategory::SEA,
|
|
"Found SEA blob %p, size=%zu\n",
|
|
result.data(),
|
|
result.size());
|
|
return result;
|
|
#else
|
|
UNREACHABLE();
|
|
#endif // !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION)
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
bool SeaResource::use_snapshot() const {
|
|
return static_cast<bool>(flags & SeaFlags::kUseSnapshot);
|
|
}
|
|
|
|
bool SeaResource::use_code_cache() const {
|
|
return static_cast<bool>(flags & SeaFlags::kUseCodeCache);
|
|
}
|
|
|
|
SeaResource FindSingleExecutableResource() {
|
|
static const SeaResource sea_resource = []() -> SeaResource {
|
|
std::string_view blob = FindSingleExecutableBlob();
|
|
per_process::Debug(DebugCategory::SEA,
|
|
"Found SEA resource %p, size=%zu\n",
|
|
blob.data(),
|
|
blob.size());
|
|
SeaDeserializer deserializer(blob);
|
|
return deserializer.Read<SeaResource>();
|
|
}();
|
|
return sea_resource;
|
|
}
|
|
|
|
bool IsSingleExecutable() {
|
|
return postject_has_resource();
|
|
}
|
|
|
|
void IsSea(const FunctionCallbackInfo<Value>& args) {
|
|
args.GetReturnValue().Set(IsSingleExecutable());
|
|
}
|
|
|
|
void IsExperimentalSeaWarningNeeded(const FunctionCallbackInfo<Value>& args) {
|
|
bool is_building_sea =
|
|
!per_process::cli_options->experimental_sea_config.empty();
|
|
if (is_building_sea) {
|
|
args.GetReturnValue().Set(true);
|
|
return;
|
|
}
|
|
|
|
if (!IsSingleExecutable()) {
|
|
args.GetReturnValue().Set(false);
|
|
return;
|
|
}
|
|
|
|
SeaResource sea_resource = FindSingleExecutableResource();
|
|
args.GetReturnValue().Set(!static_cast<bool>(
|
|
sea_resource.flags & SeaFlags::kDisableExperimentalSeaWarning));
|
|
}
|
|
|
|
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.
|
|
if (IsSingleExecutable()) {
|
|
static std::vector<char*> new_argv;
|
|
static std::vector<std::string> exec_argv_storage;
|
|
static std::vector<std::string> cli_extension_args;
|
|
|
|
SeaResource sea_resource = FindSingleExecutableResource();
|
|
|
|
new_argv.clear();
|
|
exec_argv_storage.clear();
|
|
cli_extension_args.clear();
|
|
|
|
// Handle CLI extension mode for --node-options
|
|
if (sea_resource.exec_argv_extension == SeaExecArgvExtension::kCli) {
|
|
// Extract --node-options and filter argv
|
|
for (int i = 1; i < argc; ++i) {
|
|
if (strncmp(argv[i], "--node-options=", 15) == 0) {
|
|
std::string node_options = argv[i] + 15;
|
|
std::vector<std::string> errors;
|
|
cli_extension_args = ParseNodeOptionsEnvVar(node_options, &errors);
|
|
// Remove this argument by shifting the rest
|
|
for (int j = i; j < argc - 1; ++j) {
|
|
argv[j] = argv[j + 1];
|
|
}
|
|
argc--;
|
|
i--; // Adjust index since we removed an element
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reserve space for argv[0], exec argv, cli extension args, original argv,
|
|
// and nullptr
|
|
new_argv.reserve(argc + sea_resource.exec_argv.size() +
|
|
cli_extension_args.size() + 2);
|
|
new_argv.emplace_back(argv[0]);
|
|
|
|
// Insert exec argv from SEA config
|
|
if (!sea_resource.exec_argv.empty()) {
|
|
exec_argv_storage.reserve(sea_resource.exec_argv.size() +
|
|
cli_extension_args.size());
|
|
for (const auto& arg : sea_resource.exec_argv) {
|
|
exec_argv_storage.emplace_back(arg);
|
|
new_argv.emplace_back(exec_argv_storage.back().data());
|
|
}
|
|
}
|
|
|
|
// Insert CLI extension args
|
|
for (const auto& arg : cli_extension_args) {
|
|
exec_argv_storage.emplace_back(arg);
|
|
new_argv.emplace_back(exec_argv_storage.back().data());
|
|
}
|
|
|
|
// Add actual run time arguments
|
|
new_argv.insert(new_argv.end(), argv, argv + argc);
|
|
new_argv.emplace_back(nullptr);
|
|
argc = new_argv.size() - 1;
|
|
argv = new_argv.data();
|
|
}
|
|
|
|
return {argc, argv};
|
|
}
|
|
|
|
namespace {
|
|
|
|
struct SeaConfig {
|
|
std::string main_path;
|
|
std::string output_path;
|
|
SeaFlags flags = SeaFlags::kDefault;
|
|
SeaExecArgvExtension exec_argv_extension = SeaExecArgvExtension::kEnv;
|
|
std::unordered_map<std::string, std::string> assets;
|
|
std::vector<std::string> exec_argv;
|
|
};
|
|
|
|
std::optional<SeaConfig> ParseSingleExecutableConfig(
|
|
const std::string& config_path) {
|
|
std::string config;
|
|
int r = ReadFileSync(&config, config_path.c_str());
|
|
if (r != 0) {
|
|
const char* err = uv_strerror(r);
|
|
FPrintF(stderr,
|
|
"Cannot read single executable configuration from %s: %s\n",
|
|
config_path,
|
|
err);
|
|
return std::nullopt;
|
|
}
|
|
|
|
SeaConfig result;
|
|
|
|
simdjson::ondemand::parser parser;
|
|
simdjson::ondemand::document document;
|
|
simdjson::ondemand::object main_object;
|
|
simdjson::error_code error =
|
|
parser.iterate(simdjson::pad(config)).get(document);
|
|
|
|
if (!error) {
|
|
error = document.get_object().get(main_object);
|
|
}
|
|
if (error) {
|
|
FPrintF(stderr,
|
|
"Cannot parse JSON from %s: %s\n",
|
|
config_path,
|
|
simdjson::error_message(error));
|
|
return std::nullopt;
|
|
}
|
|
|
|
bool use_snapshot_value = false;
|
|
bool use_code_cache_value = false;
|
|
|
|
for (auto field : main_object) {
|
|
std::string_view key;
|
|
if (field.unescaped_key().get(key)) {
|
|
FPrintF(stderr, "Cannot read key from %s\n", config_path);
|
|
return std::nullopt;
|
|
}
|
|
if (key == "main") {
|
|
if (field.value().get_string().get(result.main_path) ||
|
|
result.main_path.empty()) {
|
|
FPrintF(stderr,
|
|
"\"main\" field of %s is not a non-empty string\n",
|
|
config_path);
|
|
return std::nullopt;
|
|
}
|
|
} else if (key == "output") {
|
|
if (field.value().get_string().get(result.output_path) ||
|
|
result.output_path.empty()) {
|
|
FPrintF(stderr,
|
|
"\"output\" field of %s is not a non-empty string\n",
|
|
config_path);
|
|
return std::nullopt;
|
|
}
|
|
} else if (key == "disableExperimentalSEAWarning") {
|
|
bool disable_experimental_sea_warning;
|
|
if (field.value().get_bool().get(disable_experimental_sea_warning)) {
|
|
FPrintF(
|
|
stderr,
|
|
"\"disableExperimentalSEAWarning\" field of %s is not a Boolean\n",
|
|
config_path);
|
|
return std::nullopt;
|
|
}
|
|
if (disable_experimental_sea_warning) {
|
|
result.flags |= SeaFlags::kDisableExperimentalSeaWarning;
|
|
}
|
|
} else if (key == "useSnapshot") {
|
|
if (field.value().get_bool().get(use_snapshot_value)) {
|
|
FPrintF(stderr,
|
|
"\"useSnapshot\" field of %s is not a Boolean\n",
|
|
config_path);
|
|
return std::nullopt;
|
|
}
|
|
if (use_snapshot_value) {
|
|
result.flags |= SeaFlags::kUseSnapshot;
|
|
}
|
|
} else if (key == "useCodeCache") {
|
|
if (field.value().get_bool().get(use_code_cache_value)) {
|
|
FPrintF(stderr,
|
|
"\"useCodeCache\" field of %s is not a Boolean\n",
|
|
config_path);
|
|
return std::nullopt;
|
|
}
|
|
if (use_code_cache_value) {
|
|
result.flags |= SeaFlags::kUseCodeCache;
|
|
}
|
|
} else if (key == "assets") {
|
|
simdjson::ondemand::object assets_object;
|
|
if (field.value().get_object().get(assets_object)) {
|
|
FPrintF(stderr,
|
|
"\"assets\" field of %s is not a map of strings\n",
|
|
config_path);
|
|
return std::nullopt;
|
|
}
|
|
simdjson::ondemand::value asset_value;
|
|
for (auto asset_field : assets_object) {
|
|
std::string_view key_str;
|
|
std::string_view value_str;
|
|
if (asset_field.unescaped_key().get(key_str) ||
|
|
asset_field.value().get(asset_value) ||
|
|
asset_value.get_string().get(value_str)) {
|
|
FPrintF(stderr,
|
|
"\"assets\" field of %s is not a map of strings\n",
|
|
config_path);
|
|
return std::nullopt;
|
|
}
|
|
|
|
result.assets.emplace(key_str, value_str);
|
|
}
|
|
|
|
if (!result.assets.empty()) {
|
|
result.flags |= SeaFlags::kIncludeAssets;
|
|
}
|
|
} else if (key == "execArgv") {
|
|
simdjson::ondemand::array exec_argv_array;
|
|
if (field.value().get_array().get(exec_argv_array)) {
|
|
FPrintF(stderr,
|
|
"\"execArgv\" field of %s is not an array of strings\n",
|
|
config_path);
|
|
return std::nullopt;
|
|
}
|
|
std::vector<std::string> exec_argv;
|
|
for (auto argv : exec_argv_array) {
|
|
std::string_view argv_str;
|
|
if (argv.get_string().get(argv_str)) {
|
|
FPrintF(stderr,
|
|
"\"execArgv\" field of %s is not an array of strings\n",
|
|
config_path);
|
|
return std::nullopt;
|
|
}
|
|
exec_argv.emplace_back(argv_str);
|
|
}
|
|
if (!exec_argv.empty()) {
|
|
result.flags |= SeaFlags::kIncludeExecArgv;
|
|
result.exec_argv = std::move(exec_argv);
|
|
}
|
|
} else if (key == "execArgvExtension") {
|
|
std::string_view extension_str;
|
|
if (field.value().get_string().get(extension_str)) {
|
|
FPrintF(stderr,
|
|
"\"execArgvExtension\" field of %s is not a string\n",
|
|
config_path);
|
|
return std::nullopt;
|
|
}
|
|
if (extension_str == "none") {
|
|
result.exec_argv_extension = SeaExecArgvExtension::kNone;
|
|
} else if (extension_str == "env") {
|
|
result.exec_argv_extension = SeaExecArgvExtension::kEnv;
|
|
} else if (extension_str == "cli") {
|
|
result.exec_argv_extension = SeaExecArgvExtension::kCli;
|
|
} else {
|
|
FPrintF(stderr,
|
|
"\"execArgvExtension\" field of %s must be one of "
|
|
"\"none\", \"env\", or \"cli\"\n",
|
|
config_path);
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (static_cast<bool>(result.flags & SeaFlags::kUseSnapshot) &&
|
|
static_cast<bool>(result.flags & SeaFlags::kUseCodeCache)) {
|
|
// TODO(joyeecheung): code cache in snapshot should be configured by
|
|
// separate snapshot configurations.
|
|
FPrintF(stderr,
|
|
"\"useCodeCache\" is redundant when \"useSnapshot\" is true\n");
|
|
}
|
|
|
|
if (result.main_path.empty()) {
|
|
FPrintF(stderr,
|
|
"\"main\" field of %s is not a non-empty string\n",
|
|
config_path);
|
|
return std::nullopt;
|
|
}
|
|
|
|
if (result.output_path.empty()) {
|
|
FPrintF(stderr,
|
|
"\"output\" field of %s is not a non-empty string\n",
|
|
config_path);
|
|
return std::nullopt;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
ExitCode GenerateSnapshotForSEA(const SeaConfig& config,
|
|
const std::vector<std::string>& args,
|
|
const std::vector<std::string>& exec_args,
|
|
const std::string& builder_script_content,
|
|
const SnapshotConfig& snapshot_config,
|
|
std::vector<char>* snapshot_blob) {
|
|
SnapshotData snapshot;
|
|
// TODO(joyeecheung): make the arguments configurable through the JSON
|
|
// config or a programmatic API.
|
|
std::vector<std::string> patched_args = {args[0], config.main_path};
|
|
ExitCode exit_code = SnapshotBuilder::Generate(&snapshot,
|
|
patched_args,
|
|
exec_args,
|
|
builder_script_content,
|
|
snapshot_config);
|
|
if (exit_code != ExitCode::kNoFailure) {
|
|
return exit_code;
|
|
}
|
|
auto& persistents = snapshot.env_info.principal_realm.persistent_values;
|
|
auto it = std::find_if(
|
|
persistents.begin(), persistents.end(), [](const PropInfo& prop) {
|
|
return prop.name == "snapshot_deserialize_main";
|
|
});
|
|
if (it == persistents.end()) {
|
|
FPrintF(
|
|
stderr,
|
|
"%s does not invoke "
|
|
"v8.startupSnapshot.setDeserializeMainFunction(), which is required "
|
|
"for snapshot scripts used to build single executable applications."
|
|
"\n",
|
|
config.main_path);
|
|
return ExitCode::kGenericUserError;
|
|
}
|
|
// We need the temporary variable for copy elision.
|
|
std::vector<char> temp = snapshot.ToBlob();
|
|
*snapshot_blob = std::move(temp);
|
|
return ExitCode::kNoFailure;
|
|
}
|
|
|
|
std::optional<std::string> GenerateCodeCache(std::string_view main_path,
|
|
std::string_view main_script) {
|
|
RAIIIsolate raii_isolate(SnapshotBuilder::GetEmbeddedSnapshotData());
|
|
Isolate* isolate = raii_isolate.get();
|
|
|
|
v8::Isolate::Scope isolate_scope(isolate);
|
|
HandleScope handle_scope(isolate);
|
|
|
|
Local<Context> context = Context::New(isolate);
|
|
Context::Scope context_scope(context);
|
|
|
|
errors::PrinterTryCatch bootstrapCatch(
|
|
isolate, errors::PrinterTryCatch::kPrintSourceLine);
|
|
|
|
Local<String> filename;
|
|
if (!String::NewFromUtf8(isolate,
|
|
main_path.data(),
|
|
NewStringType::kNormal,
|
|
main_path.length())
|
|
.ToLocal(&filename)) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
Local<String> content;
|
|
if (!String::NewFromUtf8(isolate,
|
|
main_script.data(),
|
|
NewStringType::kNormal,
|
|
main_script.length())
|
|
.ToLocal(&content)) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
LocalVector<String> parameters(
|
|
isolate,
|
|
{
|
|
FIXED_ONE_BYTE_STRING(isolate, "exports"),
|
|
FIXED_ONE_BYTE_STRING(isolate, "require"),
|
|
FIXED_ONE_BYTE_STRING(isolate, "module"),
|
|
FIXED_ONE_BYTE_STRING(isolate, "__filename"),
|
|
FIXED_ONE_BYTE_STRING(isolate, "__dirname"),
|
|
});
|
|
ScriptOrigin script_origin(filename, 0, 0, true);
|
|
ScriptCompiler::Source script_source(content, script_origin);
|
|
MaybeLocal<Function> maybe_fn =
|
|
ScriptCompiler::CompileFunction(context,
|
|
&script_source,
|
|
parameters.size(),
|
|
parameters.data(),
|
|
0,
|
|
nullptr);
|
|
Local<Function> fn;
|
|
if (!maybe_fn.ToLocal(&fn)) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
// TODO(RaisinTen): Using the V8 code cache prevents us from using `import()`
|
|
// in the SEA code. Support it.
|
|
// Refs: https://github.com/nodejs/node/pull/48191#discussion_r1213271430
|
|
std::unique_ptr<ScriptCompiler::CachedData> cache{
|
|
ScriptCompiler::CreateCodeCacheForFunction(fn)};
|
|
std::string code_cache(cache->data, cache->data + cache->length);
|
|
return code_cache;
|
|
}
|
|
|
|
int BuildAssets(const std::unordered_map<std::string, std::string>& config,
|
|
std::unordered_map<std::string, std::string>* assets) {
|
|
for (auto const& [key, path] : config) {
|
|
std::string blob;
|
|
int r = ReadFileSync(&blob, path.c_str());
|
|
if (r != 0) {
|
|
const char* err = uv_strerror(r);
|
|
FPrintF(stderr, "Cannot read asset %s: %s\n", path.c_str(), err);
|
|
return r;
|
|
}
|
|
assets->emplace(key, std::move(blob));
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
ExitCode GenerateSingleExecutableBlob(
|
|
const SeaConfig& config,
|
|
const std::vector<std::string>& args,
|
|
const std::vector<std::string>& exec_args) {
|
|
std::string main_script;
|
|
// TODO(joyeecheung): unify the file utils.
|
|
int r = ReadFileSync(&main_script, config.main_path.c_str());
|
|
if (r != 0) {
|
|
const char* err = uv_strerror(r);
|
|
FPrintF(stderr, "Cannot read main script %s:%s\n", config.main_path, err);
|
|
return ExitCode::kGenericUserError;
|
|
}
|
|
|
|
std::vector<char> snapshot_blob;
|
|
bool builds_snapshot_from_main =
|
|
static_cast<bool>(config.flags & SeaFlags::kUseSnapshot);
|
|
if (builds_snapshot_from_main) {
|
|
// TODO(joyeecheung): allow passing snapshot configuration in SEA configs.
|
|
SnapshotConfig snapshot_config;
|
|
snapshot_config.builder_script_path = main_script;
|
|
ExitCode exit_code = GenerateSnapshotForSEA(
|
|
config, args, exec_args, main_script, snapshot_config, &snapshot_blob);
|
|
if (exit_code != ExitCode::kNoFailure) {
|
|
return exit_code;
|
|
}
|
|
}
|
|
|
|
std::optional<std::string_view> optional_sv_code_cache;
|
|
std::string code_cache;
|
|
if (static_cast<bool>(config.flags & SeaFlags::kUseCodeCache)) {
|
|
std::optional<std::string> optional_code_cache =
|
|
GenerateCodeCache(config.main_path, main_script);
|
|
if (!optional_code_cache.has_value()) {
|
|
FPrintF(stderr, "Cannot generate V8 code cache\n");
|
|
return ExitCode::kGenericUserError;
|
|
}
|
|
code_cache = optional_code_cache.value();
|
|
optional_sv_code_cache = code_cache;
|
|
}
|
|
|
|
std::unordered_map<std::string, std::string> assets;
|
|
if (!config.assets.empty() && BuildAssets(config.assets, &assets) != 0) {
|
|
return ExitCode::kGenericUserError;
|
|
}
|
|
std::unordered_map<std::string_view, std::string_view> assets_view;
|
|
for (auto const& [key, content] : assets) {
|
|
assets_view.emplace(key, content);
|
|
}
|
|
std::vector<std::string_view> exec_argv_view;
|
|
for (const auto& arg : config.exec_argv) {
|
|
exec_argv_view.emplace_back(arg);
|
|
}
|
|
SeaResource sea{
|
|
config.flags,
|
|
config.exec_argv_extension,
|
|
config.main_path,
|
|
builds_snapshot_from_main
|
|
? std::string_view{snapshot_blob.data(), snapshot_blob.size()}
|
|
: std::string_view{main_script.data(), main_script.size()},
|
|
optional_sv_code_cache,
|
|
assets_view,
|
|
exec_argv_view};
|
|
|
|
SeaSerializer serializer;
|
|
serializer.Write(sea);
|
|
|
|
uv_buf_t buf = uv_buf_init(serializer.sink.data(), serializer.sink.size());
|
|
r = WriteFileSync(config.output_path.c_str(), buf);
|
|
if (r != 0) {
|
|
const char* err = uv_strerror(r);
|
|
FPrintF(stderr, "Cannot write output to %s:%s\n", config.output_path, err);
|
|
return ExitCode::kGenericUserError;
|
|
}
|
|
|
|
FPrintF(stderr,
|
|
"Wrote single executable preparation blob to %s\n",
|
|
config.output_path);
|
|
return ExitCode::kNoFailure;
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
ExitCode BuildSingleExecutableBlob(const std::string& config_path,
|
|
const std::vector<std::string>& args,
|
|
const std::vector<std::string>& exec_args) {
|
|
std::optional<SeaConfig> config_opt =
|
|
ParseSingleExecutableConfig(config_path);
|
|
if (config_opt.has_value()) {
|
|
ExitCode code =
|
|
GenerateSingleExecutableBlob(config_opt.value(), args, exec_args);
|
|
return code;
|
|
}
|
|
|
|
return ExitCode::kGenericUserError;
|
|
}
|
|
|
|
void GetAsset(const FunctionCallbackInfo<Value>& args) {
|
|
CHECK_EQ(args.Length(), 1);
|
|
CHECK(args[0]->IsString());
|
|
Utf8Value key(args.GetIsolate(), args[0]);
|
|
SeaResource sea_resource = FindSingleExecutableResource();
|
|
if (sea_resource.assets.empty()) {
|
|
return;
|
|
}
|
|
auto it = sea_resource.assets.find(*key);
|
|
if (it == sea_resource.assets.end()) {
|
|
return;
|
|
}
|
|
// We cast away the constness here, the JS land should ensure that
|
|
// the data is not mutated.
|
|
std::unique_ptr<v8::BackingStore> store = ArrayBuffer::NewBackingStore(
|
|
const_cast<char*>(it->second.data()),
|
|
it->second.size(),
|
|
[](void*, size_t, void*) {},
|
|
nullptr);
|
|
Local<ArrayBuffer> ab = ArrayBuffer::New(args.GetIsolate(), std::move(store));
|
|
args.GetReturnValue().Set(ab);
|
|
}
|
|
|
|
void GetAssetKeys(const FunctionCallbackInfo<Value>& args) {
|
|
CHECK_EQ(args.Length(), 0);
|
|
Isolate* isolate = args.GetIsolate();
|
|
SeaResource sea_resource = FindSingleExecutableResource();
|
|
|
|
Local<Context> context = isolate->GetCurrentContext();
|
|
LocalVector<Value> keys(isolate);
|
|
keys.reserve(sea_resource.assets.size());
|
|
for (const auto& [key, _] : sea_resource.assets) {
|
|
Local<Value> key_str;
|
|
if (!ToV8Value(context, key).ToLocal(&key_str)) {
|
|
return;
|
|
}
|
|
keys.push_back(key_str);
|
|
}
|
|
Local<Array> result = Array::New(isolate, keys.data(), keys.size());
|
|
args.GetReturnValue().Set(result);
|
|
}
|
|
|
|
MaybeLocal<Value> LoadSingleExecutableApplication(
|
|
const StartExecutionCallbackInfo& info) {
|
|
// Here we are currently relying on the fact that in NodeMainInstance::Run(),
|
|
// env->context() is entered.
|
|
Local<Context> context = Isolate::GetCurrent()->GetCurrentContext();
|
|
Environment* env = Environment::GetCurrent(context);
|
|
SeaResource sea = FindSingleExecutableResource();
|
|
|
|
CHECK(!sea.use_snapshot());
|
|
// TODO(joyeecheung): this should be an external string. Refactor UnionBytes
|
|
// and make it easy to create one based on static content on the fly.
|
|
Local<Value> main_script =
|
|
ToV8Value(env->context(), sea.main_code_or_snapshot).ToLocalChecked();
|
|
return info.run_cjs->Call(
|
|
env->context(), Null(env->isolate()), 1, &main_script);
|
|
}
|
|
|
|
bool MaybeLoadSingleExecutableApplication(Environment* env) {
|
|
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
|
|
if (!IsSingleExecutable()) {
|
|
return false;
|
|
}
|
|
|
|
SeaResource sea = FindSingleExecutableResource();
|
|
|
|
if (sea.use_snapshot()) {
|
|
// The SEA preparation blob building process should already enforce this,
|
|
// this check is just here to guard against the unlikely case where
|
|
// the SEA preparation blob has been manually modified by someone.
|
|
CHECK(!env->snapshot_deserialize_main().IsEmpty());
|
|
LoadEnvironment(env, StartExecutionCallback{});
|
|
return true;
|
|
}
|
|
|
|
LoadEnvironment(env, LoadSingleExecutableApplication);
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
void Initialize(Local<Object> target,
|
|
Local<Value> unused,
|
|
Local<Context> context,
|
|
void* priv) {
|
|
SetMethod(context, target, "isSea", IsSea);
|
|
SetMethod(context,
|
|
target,
|
|
"isExperimentalSeaWarningNeeded",
|
|
IsExperimentalSeaWarningNeeded);
|
|
SetMethod(context, target, "getAsset", GetAsset);
|
|
SetMethod(context, target, "getAssetKeys", GetAssetKeys);
|
|
}
|
|
|
|
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
|
registry->Register(IsSea);
|
|
registry->Register(IsExperimentalSeaWarningNeeded);
|
|
registry->Register(GetAsset);
|
|
registry->Register(GetAssetKeys);
|
|
}
|
|
|
|
} // namespace sea
|
|
} // namespace node
|
|
|
|
NODE_BINDING_CONTEXT_AWARE_INTERNAL(sea, node::sea::Initialize)
|
|
NODE_BINDING_EXTERNAL_REFERENCE(sea, node::sea::RegisterExternalReferences)
|