From 4003d16d478d7b76f2a2182cca63798555eb6f8d Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Mon, 9 Mar 2026 11:12:39 -0700 Subject: [PATCH 1/9] sea: add --build-sea to generate SEA directly with Node.js binary Instead of relying on a WASM build of postject to perform the injection, add LIEF as dependency and generate the SEA directly from core via a new CLI option --build-sea which takes the SEA config. This simplifies SEA generation for users and makes it easier to debug/maintain the SEA building process. PR-URL: https://github.com/nodejs/node/pull/61167 Reviewed-By: Anna Henningsen Reviewed-By: Chengzhong Wu --- .github/CODEOWNERS | 2 + .gitignore | 2 + LICENSE | 3 +- configure.py | 14 + deps/LIEF/lief.gyp | 495 ++++++++++++++++++ doc/api/cli.md | 17 + doc/api/single-executable-applications.md | 235 +++++---- doc/node.1 | 3 + node.gyp | 5 + src/node.cc | 17 +- src/node_metadata.cc | 11 + src/node_metadata.h | 9 +- src/node_options.cc | 13 + src/node_options.h | 1 + src/node_sea.cc | 63 +-- src/node_sea.h | 22 +- src/node_sea_bin.cc | 431 ++++++++++++++- test/common/sea.js | 63 +++ test/fixtures/sea/already-exists/sea-2.js | 1 + .../sea/already-exists/sea-config-2.json | 6 + .../sea/already-exists/sea-config.json | 5 + test/fixtures/sea/already-exists/sea.js | 1 + test/fixtures/sea/basic/requirable.js | 3 + test/fixtures/sea/basic/sea-config.json | 6 + test/fixtures/sea/basic/sea.js | 64 +++ .../sea/executable-field/sea-config.json | 6 + test/fixtures/sea/executable-field/sea.js | 3 + test/parallel/test-process-versions.js | 9 + test/sea/sea.status | 5 + test/sea/test-build-sea-already-exists.js | 20 + test/sea/test-build-sea-config-not-found.js | 38 ++ test/sea/test-build-sea-executable-field.js | 47 ++ test/sea/test-build-sea-invalid-assets.js | 77 +++ .../test-build-sea-invalid-boolean-fields.js | 74 +++ test/sea/test-build-sea-invalid-exec-argv.js | 95 ++++ test/sea/test-build-sea-invalid-executable.js | 99 ++++ test/sea/test-build-sea-invalid-json.js | 23 + test/sea/test-build-sea-missing-main.js | 91 ++++ test/sea/test-build-sea-missing-output.js | 64 +++ test/sea/test-build-sea.js | 33 ++ 40 files changed, 2039 insertions(+), 137 deletions(-) create mode 100644 deps/LIEF/lief.gyp create mode 100644 test/fixtures/sea/already-exists/sea-2.js create mode 100644 test/fixtures/sea/already-exists/sea-config-2.json create mode 100644 test/fixtures/sea/already-exists/sea-config.json create mode 100644 test/fixtures/sea/already-exists/sea.js create mode 100644 test/fixtures/sea/basic/requirable.js create mode 100644 test/fixtures/sea/basic/sea-config.json create mode 100644 test/fixtures/sea/basic/sea.js create mode 100644 test/fixtures/sea/executable-field/sea-config.json create mode 100644 test/fixtures/sea/executable-field/sea.js create mode 100644 test/sea/test-build-sea-already-exists.js create mode 100644 test/sea/test-build-sea-config-not-found.js create mode 100644 test/sea/test-build-sea-executable-field.js create mode 100644 test/sea/test-build-sea-invalid-assets.js create mode 100644 test/sea/test-build-sea-invalid-boolean-fields.js create mode 100644 test/sea/test-build-sea-invalid-exec-argv.js create mode 100644 test/sea/test-build-sea-invalid-executable.js create mode 100644 test/sea/test-build-sea-invalid-json.js create mode 100644 test/sea/test-build-sea-missing-main.js create mode 100644 test/sea/test-build-sea-missing-output.js create mode 100644 test/sea/test-build-sea.js diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0164937307a770..6f2749a758891e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -153,6 +153,7 @@ /test/parallel/test-runner-* @nodejs/test_runner # Single Executable Applications +/deps/LIEF @nodejs/single-executable /deps/postject @nodejs/single-executable /doc/api/single-executable-applications.md @nodejs/single-executable /doc/contributing/maintaining/maintaining-single-executable-application-support.md @nodejs/single-executable @@ -160,6 +161,7 @@ /test/fixtures/postject-copy @nodejs/single-executable /test/sea @nodejs/single-executable /tools/dep_updaters/update-postject.sh @nodejs/single-executable +/tools/dep_updaters/update-lief.sh @nodejs/single-executable # Permission Model /doc/api/permissions.md @nodejs/security-wg diff --git a/.gitignore b/.gitignore index 221e4f4062486a..d283bce868da6c 100644 --- a/.gitignore +++ b/.gitignore @@ -145,6 +145,8 @@ tools/*/*.i.tmp # Ignore dependencies fetched by tools/v8/fetch_deps.py /deps/.cipd !deps/LIEF/** +deps/LIEF/*.vcxproj* +deps/LIEF/*.sln # === Rules for Windows vcbuild.bat === /temp-vcbuild diff --git a/LICENSE b/LICENSE index 403ea3c5717087..2837954aa89579 100644 --- a/LICENSE +++ b/LICENSE @@ -1262,7 +1262,8 @@ The externally maintained libraries used by Node.js are: SOFTWARE. """ -- postject, located at test/fixtures/postject-copy, is licensed as follows: +- postject, located at test/fixtures/postject-copy and used as a basis for + src/node_sea_bin.cc, is licensed as follows: """ Postject is licensed for use as follows: diff --git a/configure.py b/configure.py index 98a8b147e4cbfd..9073b170899cac 100755 --- a/configure.py +++ b/configure.py @@ -898,6 +898,12 @@ default=None, help='do not install the bundled Amaro (TypeScript utils)') +parser.add_argument('--without-lief', + action='store_true', + dest='without_lief', + default=None, + help='build without LIEF (Library for instrumenting executable formats)') + parser.add_argument('--without-npm', action='store_true', dest='without_npm', @@ -1689,6 +1695,14 @@ def configure_node(o): o['variables']['single_executable_application'] = b(not options.disable_single_executable_application) if options.disable_single_executable_application: o['defines'] += ['DISABLE_SINGLE_EXECUTABLE_APPLICATION'] + o['variables']['node_use_lief'] = 'false' + else: + if (options.without_lief is not None): + o['variables']['node_use_lief'] = b(not options.without_lief) + elif flavor in ('mac', 'linux', 'win'): + o['variables']['node_use_lief'] = 'true' + else: + o['variables']['node_use_lief'] = 'false' o['variables']['node_with_ltcg'] = b(options.with_ltcg) if flavor != 'win' and options.with_ltcg: diff --git a/deps/LIEF/lief.gyp b/deps/LIEF/lief.gyp new file mode 100644 index 00000000000000..3864c0e538a588 --- /dev/null +++ b/deps/LIEF/lief.gyp @@ -0,0 +1,495 @@ +{ + 'target_defaults': { + 'default_configurations': 'Release', + 'configurations': { + # Don't define DEBUG, it conflicts with the FILE_FLAGS enum. + 'Debug': { + 'defines!': [ 'DEBUG', 'NDEBUG' ], + }, + 'Release': { + 'defines!': [ 'DEBUG' ], + 'defines': [ 'NDEBUG' ], + }, + }, + }, + 'variables': { + 'lief_sources': [ + # Root + 'src/Object.cpp', + 'src/endianness_support.cpp', + 'src/Visitor.cpp', + 'src/errors.cpp', + 'src/hash_stream.cpp', + 'src/internal_utils.cpp', + 'src/iostream.cpp', + 'src/json_api.cpp', + 'src/logging.cpp', + 'src/paging.cpp', + 'src/utils.cpp', + 'src/range.cpp', + 'src/visitors/hash.cpp', + # BinaryStream + 'src/BinaryStream/ASN1Reader.cpp', + 'src/BinaryStream/BinaryStream.cpp', + 'src/BinaryStream/FileStream.cpp', + 'src/BinaryStream/MemoryStream.cpp', + 'src/BinaryStream/SpanStream.cpp', + 'src/BinaryStream/VectorStream.cpp', + # Abstract + 'src/Abstract/Binary.cpp', + 'src/Abstract/Symbol.cpp', + 'src/Abstract/Header.cpp', + 'src/Abstract/Section.cpp', + 'src/Abstract/Parser.cpp', + 'src/Abstract/Relocation.cpp', + 'src/Abstract/Function.cpp', + 'src/Abstract/hash.cpp', + 'src/Abstract/json_api.cpp', + 'src/Abstract/debug_info.cpp', + # Platforms + 'src/platforms/android/version.cpp', + # ELF core + 'src/ELF/Binary.cpp', + 'src/ELF/Builder.cpp', + 'src/ELF/endianness_support.cpp', + 'src/ELF/DataHandler/Handler.cpp', + 'src/ELF/DataHandler/Node.cpp', + 'src/ELF/DynamicEntry.cpp', + 'src/ELF/DynamicEntryArray.cpp', + 'src/ELF/DynamicEntryFlags.cpp', + 'src/ELF/DynamicEntryLibrary.cpp', + 'src/ELF/DynamicEntryRpath.cpp', + 'src/ELF/DynamicEntryRunPath.cpp', + 'src/ELF/DynamicSharedObject.cpp', + 'src/ELF/EnumToString.cpp', + 'src/ELF/GnuHash.cpp', + 'src/ELF/Header.cpp', + 'src/ELF/Layout.cpp', + 'src/ELF/Note.cpp', + 'src/ELF/Parser.cpp', + 'src/ELF/ProcessorFlags.cpp', + 'src/ELF/Relocation.cpp', + 'src/ELF/RelocationSizes.cpp', + 'src/ELF/RelocationStrings.cpp', + 'src/ELF/Section.cpp', + 'src/ELF/Segment.cpp', + 'src/ELF/Symbol.cpp', + 'src/ELF/SymbolVersion.cpp', + 'src/ELF/SymbolVersionAux.cpp', + 'src/ELF/SymbolVersionAuxRequirement.cpp', + 'src/ELF/SymbolVersionDefinition.cpp', + 'src/ELF/SymbolVersionRequirement.cpp', + 'src/ELF/SysvHash.cpp', + 'src/ELF/hash.cpp', + 'src/ELF/utils.cpp', + 'src/ELF/json_api.cpp', + # ELF NoteDetails + 'src/ELF/NoteDetails/NoteAbi.cpp', + 'src/ELF/NoteDetails/AndroidIdent.cpp', + 'src/ELF/NoteDetails/NoteGnuProperty.cpp', + 'src/ELF/NoteDetails/QNXStack.cpp', + 'src/ELF/NoteDetails/core/CoreAuxv.cpp', + 'src/ELF/NoteDetails/core/CoreFile.cpp', + 'src/ELF/NoteDetails/core/CorePrPsInfo.cpp', + 'src/ELF/NoteDetails/core/CorePrStatus.cpp', + 'src/ELF/NoteDetails/core/CoreSigInfo.cpp', + 'src/ELF/NoteDetails/properties/AArch64Feature.cpp', + 'src/ELF/NoteDetails/properties/AArch64PAuth.cpp', + 'src/ELF/NoteDetails/properties/StackSize.cpp', + 'src/ELF/NoteDetails/properties/X86Feature.cpp', + 'src/ELF/NoteDetails/properties/X86ISA.cpp', + # PE core + 'src/PE/Binary.cpp', + 'src/PE/Builder.cpp', + 'src/PE/CodeIntegrity.cpp', + 'src/PE/CodePage.cpp', + 'src/PE/DataDirectory.cpp', + 'src/PE/DelayImport.cpp', + 'src/PE/DelayImportEntry.cpp', + 'src/PE/DosHeader.cpp', + 'src/PE/EnumToString.cpp', + 'src/PE/ExceptionInfo.cpp', + 'src/PE/Export.cpp', + 'src/PE/ExportEntry.cpp', + 'src/PE/Factory.cpp', + 'src/PE/Header.cpp', + 'src/PE/Import.cpp', + 'src/PE/ImportEntry.cpp', + 'src/PE/OptionalHeader.cpp', + 'src/PE/Parser.cpp', + 'src/PE/ParserConfig.cpp', + 'src/PE/Relocation.cpp', + 'src/PE/RelocationEntry.cpp', + 'src/PE/ResourceData.cpp', + 'src/PE/ResourceDirectory.cpp', + 'src/PE/ResourceNode.cpp', + 'src/PE/ResourcesManager.cpp', + 'src/PE/RichEntry.cpp', + 'src/PE/RichHeader.cpp', + 'src/PE/Section.cpp', + 'src/PE/TLS.cpp', + 'src/PE/checksum.cpp', + 'src/PE/endianness_support.cpp', + 'src/PE/hash.cpp', + 'src/PE/json_api.cpp', + 'src/PE/layout_check.cpp', + 'src/PE/utils.cpp', + # PE signature + 'src/PE/signature/Attribute.cpp', + 'src/PE/signature/ContentInfo.cpp', + 'src/PE/signature/GenericContent.cpp', + 'src/PE/signature/OIDToString.cpp', + 'src/PE/signature/PKCS9TSTInfo.cpp', + 'src/PE/signature/RsaInfo.cpp', + 'src/PE/signature/Signature.cpp', + 'src/PE/signature/SignatureParser.cpp', + 'src/PE/signature/SignerInfo.cpp', + 'src/PE/signature/SpcIndirectData.cpp', + 'src/PE/signature/x509.cpp', + 'src/PE/signature/attributes/ContentType.cpp', + 'src/PE/signature/attributes/GenericType.cpp', + 'src/PE/signature/attributes/MsCounterSign.cpp', + 'src/PE/signature/attributes/MsSpcNestedSignature.cpp', + 'src/PE/signature/attributes/MsSpcStatementType.cpp', + 'src/PE/signature/attributes/MsManifestBinaryID.cpp', + 'src/PE/signature/attributes/PKCS9AtSequenceNumber.cpp', + 'src/PE/signature/attributes/PKCS9CounterSignature.cpp', + 'src/PE/signature/attributes/PKCS9MessageDigest.cpp', + 'src/PE/signature/attributes/PKCS9SigningTime.cpp', + 'src/PE/signature/attributes/SpcSpOpusInfo.cpp', + 'src/PE/signature/attributes/SpcRelaxedPeMarkerCheck.cpp', + 'src/PE/signature/attributes/SigningCertificateV2.cpp', + # PE LoadConfigurations + 'src/PE/LoadConfigurations/EnclaveConfiguration.cpp', + 'src/PE/LoadConfigurations/EnclaveImport.cpp', + 'src/PE/LoadConfigurations/LoadConfiguration.cpp', + 'src/PE/LoadConfigurations/VolatileMetadata.cpp', + 'src/PE/LoadConfigurations/CHPEMetadata/Metadata.cpp', + 'src/PE/LoadConfigurations/CHPEMetadata/MetadataARM64.cpp', + 'src/PE/LoadConfigurations/CHPEMetadata/MetadataX86.cpp', + 'src/PE/LoadConfigurations/DynamicRelocation/DynamicFixup.cpp', + 'src/PE/LoadConfigurations/DynamicRelocation/DynamicFixupARM64Kernel.cpp', + 'src/PE/LoadConfigurations/DynamicRelocation/DynamicFixupARM64X.cpp', + 'src/PE/LoadConfigurations/DynamicRelocation/DynamicFixupControlTransfer.cpp', + 'src/PE/LoadConfigurations/DynamicRelocation/DynamicFixupGeneric.cpp', + 'src/PE/LoadConfigurations/DynamicRelocation/DynamicRelocationBase.cpp', + 'src/PE/LoadConfigurations/DynamicRelocation/DynamicRelocationV1.cpp', + 'src/PE/LoadConfigurations/DynamicRelocation/DynamicRelocationV2.cpp', + 'src/PE/LoadConfigurations/DynamicRelocation/FunctionOverride.cpp', + 'src/PE/LoadConfigurations/DynamicRelocation/FunctionOverrideInfo.cpp', + # PE debug/exceptions + 'src/PE/debug/CodeView.cpp', + 'src/PE/debug/CodeViewPDB.cpp', + 'src/PE/debug/Debug.cpp', + 'src/PE/debug/ExDllCharacteristics.cpp', + 'src/PE/debug/FPO.cpp', + 'src/PE/debug/PDBChecksum.cpp', + 'src/PE/debug/Pogo.cpp', + 'src/PE/debug/PogoEntry.cpp', + 'src/PE/debug/Repro.cpp', + 'src/PE/debug/VCFeature.cpp', + 'src/PE/exceptions_info/RuntimeFunctionX64.cpp', + 'src/PE/exceptions_info/RuntimeFunctionAArch64.cpp', + 'src/PE/exceptions_info/UnwindCodeX64.cpp', + 'src/PE/exceptions_info/UnwindAArch64Decoder.cpp', + 'src/PE/exceptions_info/AArch64/PackedFunction.cpp', + 'src/PE/exceptions_info/AArch64/UnpackedFunction.cpp', + # PE resources + 'src/PE/resources/AcceleratorCodes.cpp', + 'src/PE/resources/ResourceAccelerator.cpp', + 'src/PE/resources/ResourceDialog.cpp', + 'src/PE/resources/ResourceDialogExtended.cpp', + 'src/PE/resources/ResourceDialogRegular.cpp', + 'src/PE/resources/ResourceIcon.cpp', + 'src/PE/resources/ResourceStringFileInfo.cpp', + 'src/PE/resources/ResourceStringTable.cpp', + 'src/PE/resources/ResourceVar.cpp', + 'src/PE/resources/ResourceVarFileInfo.cpp', + 'src/PE/resources/ResourceVersion.cpp', + # COFF (required by PE) + 'src/COFF/Binary.cpp', + 'src/COFF/utils.cpp', + 'src/COFF/Parser.cpp', + 'src/COFF/Header.cpp', + 'src/COFF/Section.cpp', + 'src/COFF/Relocation.cpp', + 'src/COFF/BigObjHeader.cpp', + 'src/COFF/RegularHeader.cpp', + 'src/COFF/Symbol.cpp', + 'src/COFF/AuxiliarySymbol.cpp', + 'src/COFF/AuxiliarySymbols/AuxiliarybfAndefSymbol.cpp', + 'src/COFF/AuxiliarySymbols/AuxiliaryCLRToken.cpp', + 'src/COFF/AuxiliarySymbols/AuxiliaryFile.cpp', + 'src/COFF/AuxiliarySymbols/AuxiliaryFunctionDefinition.cpp', + 'src/COFF/AuxiliarySymbols/AuxiliarySectionDefinition.cpp', + 'src/COFF/AuxiliarySymbols/AuxiliaryWeakExternal.cpp', + # Mach-O + 'src/MachO/AtomInfo.cpp', + 'src/MachO/Binary.cpp', + 'src/MachO/BinaryParser.cpp', + 'src/MachO/BindingInfo.cpp', + 'src/MachO/BindingInfoIterator.cpp', + 'src/MachO/BuildToolVersion.cpp', + 'src/MachO/BuildVersion.cpp', + 'src/MachO/Builder.cpp', + 'src/MachO/ChainedBindingInfo.cpp', + 'src/MachO/ChainedBindingInfoList.cpp', + 'src/MachO/ChainedFixup.cpp', + 'src/MachO/ChainedPointerAnalysis.cpp', + 'src/MachO/CodeSignature.cpp', + 'src/MachO/CodeSignatureDir.cpp', + 'src/MachO/DataCodeEntry.cpp', + 'src/MachO/DataInCode.cpp', + 'src/MachO/DyldBindingInfo.cpp', + 'src/MachO/DyldChainedFixups.cpp', + 'src/MachO/DyldChainedFixupsCreator.cpp', + 'src/MachO/DyldChainedFormat.cpp', + 'src/MachO/DyldEnvironment.cpp', + 'src/MachO/DyldExportsTrie.cpp', + 'src/MachO/DyldInfo.cpp', + 'src/MachO/DylibCommand.cpp', + 'src/MachO/DylinkerCommand.cpp', + 'src/MachO/DynamicSymbolCommand.cpp', + 'src/MachO/EncryptionInfo.cpp', + 'src/MachO/EnumToString.cpp', + 'src/MachO/ExportInfo.cpp', + 'src/MachO/FatBinary.cpp', + 'src/MachO/FilesetCommand.cpp', + 'src/MachO/FunctionStarts.cpp', + 'src/MachO/FunctionVariants.cpp', + 'src/MachO/FunctionVariantFixups.cpp', + 'src/MachO/Header.cpp', + 'src/MachO/IndirectBindingInfo.cpp', + 'src/MachO/LinkEdit.cpp', + 'src/MachO/LinkerOptHint.cpp', + 'src/MachO/LoadCommand.cpp', + 'src/MachO/MainCommand.cpp', + 'src/MachO/NoteCommand.cpp', + 'src/MachO/Parser.cpp', + 'src/MachO/ParserConfig.cpp', + 'src/MachO/RPathCommand.cpp', + 'src/MachO/Relocation.cpp', + 'src/MachO/RelocationDyld.cpp', + 'src/MachO/RelocationFixup.cpp', + 'src/MachO/RelocationObject.cpp', + 'src/MachO/Routine.cpp', + 'src/MachO/Section.cpp', + 'src/MachO/SegmentCommand.cpp', + 'src/MachO/SegmentSplitInfo.cpp', + 'src/MachO/SourceVersion.cpp', + 'src/MachO/Stub.cpp', + 'src/MachO/SubClient.cpp', + 'src/MachO/SubFramework.cpp', + 'src/MachO/Symbol.cpp', + 'src/MachO/SymbolCommand.cpp', + 'src/MachO/ThreadCommand.cpp', + 'src/MachO/TrieNode.cpp', + 'src/MachO/TwoLevelHints.cpp', + 'src/MachO/UUIDCommand.cpp', + 'src/MachO/UnknownCommand.cpp', + 'src/MachO/VersionMin.cpp', + 'src/MachO/endianness_support.cpp', + 'src/MachO/exports_trie.cpp', + 'src/MachO/hash.cpp', + 'src/MachO/json_api.cpp', + 'src/MachO/layout_check.cpp', + 'src/MachO/utils.cpp', + # Stubs when extended features are disabled + 'src/DWARF/dwarf.cpp', + 'src/PDB/pdb.cpp', + 'src/ObjC/objc.cpp', + 'src/dyld-shared-cache/dyldsc.cpp', + 'src/asm/asm.cpp', + ], + 'lief_third_party_sources': [ + # mbedTLS sources (extracted and checked in under third-party) + 'third-party/mbedtls/library/aes.c', + 'third-party/mbedtls/library/aesce.c', + 'third-party/mbedtls/library/aesni.c', + 'third-party/mbedtls/library/aria.c', + 'third-party/mbedtls/library/asn1parse.c', + 'third-party/mbedtls/library/asn1write.c', + 'third-party/mbedtls/library/base64.c', + 'third-party/mbedtls/library/bignum.c', + 'third-party/mbedtls/library/bignum_core.c', + 'third-party/mbedtls/library/bignum_mod.c', + 'third-party/mbedtls/library/bignum_mod_raw.c', + 'third-party/mbedtls/library/block_cipher.c', + 'third-party/mbedtls/library/camellia.c', + 'third-party/mbedtls/library/ccm.c', + 'third-party/mbedtls/library/chacha20.c', + 'third-party/mbedtls/library/chachapoly.c', + 'third-party/mbedtls/library/cipher.c', + 'third-party/mbedtls/library/cipher_wrap.c', + 'third-party/mbedtls/library/cmac.c', + 'third-party/mbedtls/library/constant_time.c', + 'third-party/mbedtls/library/ctr_drbg.c', + 'third-party/mbedtls/library/debug.c', + 'third-party/mbedtls/library/des.c', + 'third-party/mbedtls/library/dhm.c', + 'third-party/mbedtls/library/ecdh.c', + 'third-party/mbedtls/library/ecdsa.c', + 'third-party/mbedtls/library/ecjpake.c', + 'third-party/mbedtls/library/ecp.c', + 'third-party/mbedtls/library/ecp_curves.c', + 'third-party/mbedtls/library/ecp_curves_new.c', + 'third-party/mbedtls/library/entropy.c', + 'third-party/mbedtls/library/entropy_poll.c', + 'third-party/mbedtls/library/error.c', + 'third-party/mbedtls/library/gcm.c', + 'third-party/mbedtls/library/hkdf.c', + 'third-party/mbedtls/library/hmac_drbg.c', + 'third-party/mbedtls/library/lmots.c', + 'third-party/mbedtls/library/lms.c', + 'third-party/mbedtls/library/md.c', + 'third-party/mbedtls/library/md5.c', + 'third-party/mbedtls/library/memory_buffer_alloc.c', + 'third-party/mbedtls/library/mps_reader.c', + 'third-party/mbedtls/library/mps_trace.c', + 'third-party/mbedtls/library/net_sockets.c', + 'third-party/mbedtls/library/nist_kw.c', + 'third-party/mbedtls/library/oid.c', + 'third-party/mbedtls/library/padlock.c', + 'third-party/mbedtls/library/pem.c', + 'third-party/mbedtls/library/pk.c', + 'third-party/mbedtls/library/pk_ecc.c', + 'third-party/mbedtls/library/pk_wrap.c', + 'third-party/mbedtls/library/pkcs12.c', + 'third-party/mbedtls/library/pkcs5.c', + 'third-party/mbedtls/library/pkcs7.c', + 'third-party/mbedtls/library/pkparse.c', + 'third-party/mbedtls/library/pkwrite.c', + 'third-party/mbedtls/library/platform.c', + 'third-party/mbedtls/library/platform_util.c', + 'third-party/mbedtls/library/poly1305.c', + 'third-party/mbedtls/library/psa_crypto.c', + 'third-party/mbedtls/library/psa_crypto_aead.c', + 'third-party/mbedtls/library/psa_crypto_cipher.c', + 'third-party/mbedtls/library/psa_crypto_client.c', + 'third-party/mbedtls/library/psa_crypto_driver_wrappers_no_static.c', + 'third-party/mbedtls/library/psa_crypto_ecp.c', + 'third-party/mbedtls/library/psa_crypto_ffdh.c', + 'third-party/mbedtls/library/psa_crypto_hash.c', + 'third-party/mbedtls/library/psa_crypto_mac.c', + 'third-party/mbedtls/library/psa_crypto_pake.c', + 'third-party/mbedtls/library/psa_crypto_rsa.c', + 'third-party/mbedtls/library/psa_crypto_se.c', + 'third-party/mbedtls/library/psa_crypto_slot_management.c', + 'third-party/mbedtls/library/psa_crypto_storage.c', + 'third-party/mbedtls/library/psa_its_file.c', + 'third-party/mbedtls/library/psa_util.c', + 'third-party/mbedtls/library/ripemd160.c', + 'third-party/mbedtls/library/rsa.c', + 'third-party/mbedtls/library/rsa_alt_helpers.c', + 'third-party/mbedtls/library/sha1.c', + 'third-party/mbedtls/library/sha256.c', + 'third-party/mbedtls/library/sha3.c', + 'third-party/mbedtls/library/sha512.c', + 'third-party/mbedtls/library/ssl_cache.c', + 'third-party/mbedtls/library/ssl_ciphersuites.c', + 'third-party/mbedtls/library/ssl_client.c', + 'third-party/mbedtls/library/ssl_cookie.c', + 'third-party/mbedtls/library/ssl_debug_helpers_generated.c', + 'third-party/mbedtls/library/ssl_msg.c', + 'third-party/mbedtls/library/ssl_ticket.c', + 'third-party/mbedtls/library/ssl_tls.c', + 'third-party/mbedtls/library/ssl_tls12_client.c', + 'third-party/mbedtls/library/ssl_tls12_server.c', + 'third-party/mbedtls/library/ssl_tls13_client.c', + 'third-party/mbedtls/library/ssl_tls13_generic.c', + 'third-party/mbedtls/library/ssl_tls13_keys.c', + 'third-party/mbedtls/library/ssl_tls13_server.c', + 'third-party/mbedtls/library/threading.c', + 'third-party/mbedtls/library/timing.c', + 'third-party/mbedtls/library/version.c', + 'third-party/mbedtls/library/version_features.c', + 'third-party/mbedtls/library/x509.c', + 'third-party/mbedtls/library/x509_create.c', + 'third-party/mbedtls/library/x509_crl.c', + 'third-party/mbedtls/library/x509_crt.c', + 'third-party/mbedtls/library/x509_csr.c', + 'third-party/mbedtls/library/x509write.c', + 'third-party/mbedtls/library/x509write_crt.c', + 'third-party/mbedtls/library/x509write_csr.c', + ] + }, + 'targets': [ + { + 'target_name': 'liblief', + 'toolsets': ['host', 'target'], + 'type': 'static_library', + 'includes': [], + 'include_dirs': [ + '.', + #include + #include + 'include', + 'src', + # Extracted third-party (checked into source by update script) + #include "mbedtls/private_access.h" + 'third-party/mbedtls/include', + 'third-party/mbedtls/library', + #include "spdlog/fmt/..." + 'third-party/spdlog/include', + #include + 'third-party/frozen/include', + #include + "include/LIEF/third-party", + #include "utf8/unchecked.h" + "include/LIEF/third-party/internal", + ], + 'direct_dependent_settings': { + 'include_dirs': [ 'include' ], + }, + 'defines': [ + 'LIEF_STATIC', + 'testtttt' + 'MBEDTLS_CONFIG_FILE="config/mbedtls/config.h"', + 'MBEDTLS_NO_PLATFORM_ENTROPY', + 'SPDLOG_DISABLE_DEFAULT_LOGGER', + 'SPDLOG_NO_EXCEPTIONS', + 'SPDLOG_FUNCTION=""', + '_GLIBCXX_USE_CXX11_ABI=1', + ], + 'cflags': [ + '-fPIC' + ], + # We need c++17 to compile without std::format and avoid conflicts with spdlog. + 'msvs_settings': { + 'VCCLCompilerTool': { + 'LanguageStandard': 'stdcpp17', + }, + }, + 'cflags_cc': [ + '-std=gnu++17', + '-fPIC', + '-fvisibility=hidden', + '-fvisibility-inlines-hidden', + '-Wall', + '-Wextra', + '-Wpedantic', + '-Wno-expansion-to-defined', + '-Werror=return-type', + '-fno-exceptions' + ], + 'xcode_settings': { + 'OTHER_CPLUSPLUSFLAGS': [ + '-std=gnu++17', + '-fPIC', + '-fvisibility=hidden', + '-fvisibility-inlines-hidden', + '-Wall', + '-Wextra', + '-Wpedantic', + '-Wno-expansion-to-defined', + '-Werror=return-type', + '-fno-exceptions' + ], + }, + 'sources': [ + '<@(lief_sources)', + '<@(lief_third_party_sources)', + ], + } + ] +} diff --git a/doc/api/cli.md b/doc/api/cli.md index f05686608297e5..f31963a9d5cb94 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -363,6 +363,23 @@ Error: Access to this API has been restricted } ``` +### `--build-sea=config` + + + +> Stability: 1.1 - Active development + +Generates a [single executable application][] from a JSON +configuration file. The argument must be a path to the configuration file. If +the path is not absolute, it is resolved relative to the current working +directory. + +For configuration fields, cross-platform notes, and asset APIs, see +the [single executable application][] documentation. + ### `--build-snapshot` + +When using `"mainFormat": "module"`, `import()` can be used to dynamically +load built-in modules. Attempting to use `import()` to load modules from +the file system will throw an error. + ### Using native addons in the injected main script Native addons can be bundled as assets into the single-executable application @@ -597,6 +639,7 @@ start a discussion at to help us document them. [CommonJS]: modules.md#modules-commonjs-modules +[ECMAScript Modules]: esm.md#modules-ecmascript-modules [ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format [Generating single executable preparation blobs]: #generating-single-executable-preparation-blobs [Mach-O]: https://en.wikipedia.org/wiki/Mach-O diff --git a/src/node_sea.cc b/src/node_sea.cc index bffdc72d1d1791..85f5eb118c3845 100644 --- a/src/node_sea.cc +++ b/src/node_sea.cc @@ -84,6 +84,11 @@ size_t SeaSerializer::Write(const SeaResource& sea) { static_cast(sea.exec_argv_extension)); written_total += WriteArithmetic(static_cast(sea.exec_argv_extension)); + + Debug("Write SEA main code format %u\n", + static_cast(sea.main_code_format)); + written_total += + WriteArithmetic(static_cast(sea.main_code_format)); DCHECK_EQ(written_total, SeaResource::kHeaderSize); Debug("Write SEA code path %p, size=%zu\n", @@ -161,6 +166,11 @@ SeaResource SeaDeserializer::Read() { SeaExecArgvExtension exec_argv_extension = static_cast(extension_value); Debug("Read SEA resource exec argv extension %u\n", extension_value); + + uint8_t format_value = ReadArithmetic(); + CHECK_LE(format_value, static_cast(ModuleFormat::kModule)); + ModuleFormat main_code_format = static_cast(format_value); + Debug("Read SEA main code format %u\n", format_value); CHECK_EQ(read_total, SeaResource::kHeaderSize); std::string_view code_path = @@ -219,6 +229,7 @@ SeaResource SeaDeserializer::Read() { exec_argv_extension, code_path, code, + main_code_format, code_cache, assets, exec_argv}; @@ -501,6 +512,25 @@ std::optional ParseSingleExecutableConfig( config_path); return std::nullopt; } + } else if (key == "mainFormat") { + std::string_view format_str; + if (field.value().get_string().get(format_str)) { + FPrintF(stderr, + "\"mainFormat\" field of %s is not a string\n", + config_path); + return std::nullopt; + } + if (format_str == "commonjs") { + result.main_format = ModuleFormat::kCommonJS; + } else if (format_str == "module") { + result.main_format = ModuleFormat::kModule; + } else { + FPrintF(stderr, + "\"mainFormat\" field of %s must be one of " + "\"commonjs\" or \"module\"\n", + config_path); + return std::nullopt; + } } } @@ -512,6 +542,23 @@ std::optional ParseSingleExecutableConfig( "\"useCodeCache\" is redundant when \"useSnapshot\" is true\n"); } + // TODO(joyeecheung): support ESM with useSnapshot and useCodeCache. + if (result.main_format == ModuleFormat::kModule && + static_cast(result.flags & SeaFlags::kUseSnapshot)) { + FPrintF(stderr, + "\"mainFormat\": \"module\" is not supported when " + "\"useSnapshot\" is true\n"); + return std::nullopt; + } + + if (result.main_format == ModuleFormat::kModule && + static_cast(result.flags & SeaFlags::kUseCodeCache)) { + FPrintF(stderr, + "\"mainFormat\": \"module\" is not supported when " + "\"useCodeCache\" is true\n"); + return std::nullopt; + } + if (result.main_path.empty()) { FPrintF(stderr, "\"main\" field of %s is not a non-empty string\n", @@ -709,6 +756,7 @@ ExitCode GenerateSingleExecutableBlob( builds_snapshot_from_main ? std::string_view{snapshot_blob.data(), snapshot_blob.size()} : std::string_view{main_script.data(), main_script.size()}, + config.main_format, optional_sv_code_cache, assets_view, exec_argv_view}; @@ -792,20 +840,25 @@ void GetAssetKeys(const FunctionCallbackInfo& args) { } MaybeLocal LoadSingleExecutableApplication( - const StartExecutionCallbackInfo& info) { + const StartExecutionCallbackInfoWithModule& info) { // Here we are currently relying on the fact that in NodeMainInstance::Run(), // env->context() is entered. - Local context = Isolate::GetCurrent()->GetCurrentContext(); - Environment* env = Environment::GetCurrent(context); + Environment* env = info.env(); + Local context = env->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 main_script = - ToV8Value(env->context(), sea.main_code_or_snapshot).ToLocalChecked(); - return info.run_cjs->Call( - env->context(), Null(env->isolate()), 1, &main_script); + ToV8Value(context, sea.main_code_or_snapshot).ToLocalChecked(); + Local kind = + v8::Integer::New(env->isolate(), static_cast(sea.main_code_format)); + Local resource_name = + ToV8Value(context, env->exec_path()).ToLocalChecked(); + Local args[] = {main_script, kind, resource_name}; + return info.run_module()->Call( + env->context(), Null(env->isolate()), arraysize(args), args); } bool MaybeLoadSingleExecutableApplication(Environment* env) { @@ -821,7 +874,7 @@ bool MaybeLoadSingleExecutableApplication(Environment* env) { // 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{}); + LoadEnvironment(env, StartExecutionCallbackWithModule{}); return true; } diff --git a/src/node_sea.h b/src/node_sea.h index 34596972b60219..dd0b89db841eed 100644 --- a/src/node_sea.h +++ b/src/node_sea.h @@ -11,6 +11,7 @@ #include #include +#include "node.h" #include "node_exit_code.h" namespace node { @@ -43,6 +44,7 @@ struct SeaConfig { std::string executable_path; SeaFlags flags = SeaFlags::kDefault; SeaExecArgvExtension exec_argv_extension = SeaExecArgvExtension::kEnv; + ModuleFormat main_format = ModuleFormat::kCommonJS; std::unordered_map assets; std::vector exec_argv; }; @@ -52,6 +54,7 @@ struct SeaResource { SeaExecArgvExtension exec_argv_extension = SeaExecArgvExtension::kEnv; std::string_view code_path; std::string_view main_code_or_snapshot; + ModuleFormat main_code_format = ModuleFormat::kCommonJS; std::optional code_cache; std::unordered_map assets; std::vector exec_argv; @@ -59,8 +62,9 @@ struct SeaResource { bool use_snapshot() const; bool use_code_cache() const; - static constexpr size_t kHeaderSize = - sizeof(kMagic) + sizeof(SeaFlags) + sizeof(SeaExecArgvExtension); + static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags) + + sizeof(SeaExecArgvExtension) + + sizeof(ModuleFormat); }; bool IsSingleExecutable(); diff --git a/test/fixtures/sea/esm/sea-config.json b/test/fixtures/sea/esm/sea-config.json new file mode 100644 index 00000000000000..e5ee27ff7f4c85 --- /dev/null +++ b/test/fixtures/sea/esm/sea-config.json @@ -0,0 +1,6 @@ +{ + "main": "sea.mjs", + "output": "sea", + "mainFormat": "module", + "disableExperimentalSEAWarning": true +} diff --git a/test/fixtures/sea/esm/sea.mjs b/test/fixtures/sea/esm/sea.mjs new file mode 100644 index 00000000000000..c8c9fe0ca1d571 --- /dev/null +++ b/test/fixtures/sea/esm/sea.mjs @@ -0,0 +1,24 @@ +import assert from 'node:assert'; +import { createRequire } from 'node:module'; +import { pathToFileURL } from 'node:url'; +import { dirname } from 'node:path'; + +// Test createRequire with process.execPath. +const assert2 = createRequire(process.execPath)('node:assert'); +assert.strictEqual(assert2.strict, assert.strict); + +// Test import.meta properties. This should be in sync with the CommonJS entry +// point's corresponding values. +assert.strictEqual(import.meta.url, pathToFileURL(process.execPath).href); +assert.strictEqual(import.meta.filename, process.execPath); +assert.strictEqual(import.meta.dirname, dirname(process.execPath)); +assert.strictEqual(import.meta.main, true); +// TODO(joyeecheung): support import.meta.resolve when we also support +// require.resolve in CommonJS entry points, the behavior of the two +// should be in sync. + +// Test import() with a built-in module. +const { strict } = await import('node:assert'); +assert.strictEqual(strict, assert.strict); + +console.log('ESM SEA executed successfully'); diff --git a/test/sea/test-single-executable-application-esm.js b/test/sea/test-single-executable-application-esm.js new file mode 100644 index 00000000000000..9f7366cb0e2405 --- /dev/null +++ b/test/sea/test-single-executable-application-esm.js @@ -0,0 +1,33 @@ +'use strict'; + +require('../common'); + +const { + buildSEA, + skipIfBuildSEAIsNotSupported, +} = require('../common/sea'); + +skipIfBuildSEAIsNotSupported(); + +// This tests the creation of a single executable application with an ESM +// entry point using the "mainFormat": "module" configuration. + +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); + +tmpdir.refresh(); + +const outputFile = buildSEA(fixtures.path('sea', 'esm')); + +spawnSyncAndExitWithoutError( + outputFile, + { + env: { + NODE_DEBUG_NATIVE: 'SEA', + ...process.env, + }, + }, + { + stdout: /ESM SEA executed successfully/, + }); From fb276ad20ed0762ae75f128b3862d298b3b03aa7 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Tue, 10 Mar 2026 11:49:32 -0700 Subject: [PATCH 6/9] src: support import() and import.meta in embedder-run modules This adds a embedder_module_hdo for identifying embedder-run modules in the dynamic import handler and import.meta initializer, and a SourceTextModuleTypes for customizing source text module compilation in the JS land via compileSourceTextModule(). Also, refactors the existing embedder module compilation code to reuse the builtin resolution logic. PR-URL: https://github.com/nodejs/node/pull/61654 Reviewed-By: Chengzhong Wu Reviewed-By: Aditi Singh --- lib/internal/main/embedding.js | 60 ++++--------------------------- lib/internal/modules/esm/utils.js | 42 +++++++++++++++++++++- lib/internal/modules/helpers.js | 39 ++++++++++++++++++++ src/env_properties.h | 1 + 4 files changed, 88 insertions(+), 54 deletions(-) diff --git a/lib/internal/main/embedding.js b/lib/internal/main/embedding.js index 91a12f755e6abc..fd0cc810363c2a 100644 --- a/lib/internal/main/embedding.js +++ b/lib/internal/main/embedding.js @@ -15,16 +15,12 @@ const { const { isExperimentalSeaWarningNeeded, isSea } = internalBinding('sea'); const { emitExperimentalWarning } = require('internal/util'); const { emitWarningSync } = require('internal/process/warning'); -const { BuiltinModule } = require('internal/bootstrap/realm'); -const { normalizeRequirableId } = BuiltinModule; const { Module } = require('internal/modules/cjs/loader'); const { compileFunctionForCJSLoader } = internalBinding('contextify'); const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache'); -const { codes: { - ERR_UNKNOWN_BUILTIN_MODULE, -} } = require('internal/errors'); const { pathToFileURL } = require('internal/url'); -const { loadBuiltinModule } = require('internal/modules/helpers'); +const { loadBuiltinModuleForEmbedder } = require('internal/modules/helpers'); +const { compileSourceTextModuleForEmbedder } = require('internal/modules/esm/utils'); const { moduleFormats } = internalBinding('modules'); const assert = require('internal/assert'); const path = require('path'); @@ -34,7 +30,6 @@ const path = require('path'); prepareMainThreadExecution(false, true); const isLoadingSea = isSea(); -const isBuiltinWarningNeeded = isLoadingSea && isExperimentalSeaWarningNeeded(); if (isExperimentalSeaWarningNeeded()) { emitExperimentalWarning('Single executable application'); } @@ -103,28 +98,8 @@ function embedderRunCjs(content, filename) { ); } -let warnedAboutBuiltins = false; -function warnNonBuiltinInSEA() { - if (isBuiltinWarningNeeded && !warnedAboutBuiltins) { - emitWarningSync( - 'Currently the require() provided to the main script embedded into ' + - 'single-executable applications only supports loading built-in modules.\n' + - 'To load a module from disk after the single executable application is ' + - 'launched, use require("module").createRequire().\n' + - 'Support for bundled module loading or virtual file systems are under ' + - 'discussions in https://github.com/nodejs/single-executable'); - warnedAboutBuiltins = true; - } -} - function embedderRequire(id) { - const normalizedId = normalizeRequirableId(id); - - if (!normalizedId) { - warnNonBuiltinInSEA(); - throw new ERR_UNKNOWN_BUILTIN_MODULE(id); - } - return require(normalizedId); + return loadBuiltinModuleForEmbedder(id).exports; } function embedderRunESM(content, filename) { @@ -134,31 +109,10 @@ function embedderRunESM(content, filename) { } else { resourceName = filename; } - const { compileSourceTextModule } = require('internal/modules/esm/utils'); - // TODO(joyeecheung): support code cache, dynamic import() and import.meta. - const wrap = compileSourceTextModule(resourceName, content); - // Cache the source map for the module if present. - if (wrap.sourceMapURL) { - maybeCacheSourceMap(resourceName, content, wrap, false, undefined, wrap.sourceMapURL); - } - const requests = wrap.getModuleRequests(); - const modules = []; - for (let i = 0; i < requests.length; ++i) { - const { specifier } = requests[i]; - const normalizedId = normalizeRequirableId(specifier); - if (!normalizedId) { - warnNonBuiltinInSEA(); - throw new ERR_UNKNOWN_BUILTIN_MODULE(specifier); - } - const mod = loadBuiltinModule(normalizedId); - if (!mod) { - throw new ERR_UNKNOWN_BUILTIN_MODULE(specifier); - } - modules.push(mod.getESMFacade()); - } - wrap.link(modules); - wrap.instantiate(); - wrap.evaluate(-1, false); + // TODO(joyeecheung): allow configuration from node::ModuleData, + // either via a more generic context object, or something like import.meta extensions. + const context = { isMain: true, __proto__: null }; + const wrap = compileSourceTextModuleForEmbedder(resourceName, content, context); // TODO(joyeecheung): we may want to return the v8::Module via a vm.SourceTextModule // when vm.SourceTextModule stablizes, or put it in an out parameter. diff --git a/lib/internal/modules/esm/utils.js b/lib/internal/modules/esm/utils.js index 0af25ebbf6c3f2..aa8e1c1b7e25e2 100644 --- a/lib/internal/modules/esm/utils.js +++ b/lib/internal/modules/esm/utils.js @@ -14,6 +14,7 @@ const { }, } = internalBinding('util'); const { + embedder_module_hdo, source_text_module_default_hdo, vm_dynamic_import_default_internal, vm_dynamic_import_main_context_default, @@ -43,6 +44,7 @@ const { const assert = require('internal/assert'); const { normalizeReferrerURL, + loadBuiltinModuleForEmbedder, } = require('internal/modules/helpers'); let defaultConditions; @@ -200,7 +202,8 @@ function defaultInitializeImportMetaForModule(meta, wrap) { * @param {ModuleWrap} wrap - The ModuleWrap of the SourceTextModule where `import.meta` is referenced. */ function initializeImportMetaObject(symbol, meta, wrap) { - if (symbol === source_text_module_default_hdo) { + if (symbol === source_text_module_default_hdo || + symbol === embedder_module_hdo) { defaultInitializeImportMetaForModule(meta, wrap); return; } @@ -266,6 +269,10 @@ async function importModuleDynamicallyCallback(referrerSymbol, specifier, phase, if (referrerSymbol === source_text_module_default_hdo) { return defaultImportModuleDynamicallyForModule(specifier, phase, attributes, referrerName); } + // For embedder entry point ESM, only allow built-in modules. + if (referrerSymbol === embedder_module_hdo) { + return loadBuiltinModuleForEmbedder(specifier).getESMFacade().getNamespace(); + } if (moduleRegistries.has(referrerSymbol)) { const { importModuleDynamically, callbackReferrer } = moduleRegistries.get(referrerSymbol); @@ -334,6 +341,37 @@ function compileSourceTextModule(url, source, cascadedLoader, context = kEmptyOb } +/** + * Compile, link, instantiate and evaluate a SourceTextModule for embedder ESM entry point. + * This resolves only built-in modules and uses the embedder_module_hdo for import.meta + * and dynamic import() support. + * @param {string} url URL of the module. + * @param {string} source Source code of the module. + * @param {{ isMain?: boolean }|undefined} context - context object containing module metadata. + * @returns {ModuleWrap} + */ +function compileSourceTextModuleForEmbedder(url, source, context = kEmptyObject) { + const wrap = new ModuleWrap(url, undefined, source, 0, 0, embedder_module_hdo); + + const { isMain } = context; + if (isMain) { + wrap.isMain = true; + } + + // Cache the source map for the module if present. + if (wrap.sourceMapURL) { + maybeCacheSourceMap(url, source, wrap, false, wrap.sourceURL, wrap.sourceMapURL); + } + + // For embedder ESM, handle linking and evaluation. + const requests = wrap.getModuleRequests(); + const modules = requests.map(({ specifier }) => loadBuiltinModuleForEmbedder(specifier).getESMFacade()); + wrap.link(modules); + wrap.instantiate(); + wrap.evaluate(-1, false); + return wrap; +} + const kImportInImportedESM = Symbol('kImportInImportedESM'); const kImportInRequiredESM = Symbol('kImportInRequiredESM'); const kRequireInImportedCJS = Symbol('kRequireInImportedCJS'); @@ -344,11 +382,13 @@ const kRequireInImportedCJS = Symbol('kRequireInImportedCJS'); const requestTypes = { kImportInImportedESM, kImportInRequiredESM, kRequireInImportedCJS }; module.exports = { + embedder_module_hdo, registerModule, initializeESM, getDefaultConditions, getConditionsSet, shouldSpawnLoaderHookWorker, compileSourceTextModule, + compileSourceTextModuleForEmbedder, requestTypes, }; diff --git a/lib/internal/modules/helpers.js b/lib/internal/modules/helpers.js index e2cdc0c5bba74b..209e6f800cd564 100644 --- a/lib/internal/modules/helpers.js +++ b/lib/internal/modules/helpers.js @@ -15,6 +15,7 @@ const { const { ERR_INVALID_ARG_TYPE, ERR_INVALID_RETURN_PROPERTY_VALUE, + ERR_UNKNOWN_BUILTIN_MODULE, } = require('internal/errors').codes; const { BuiltinModule } = require('internal/bootstrap/realm'); @@ -126,6 +127,43 @@ function loadBuiltinModule(id) { return mod; } +let isSEABuiltinWarningNeeded_; +function isSEABuiltinWarningNeeded() { + if (isSEABuiltinWarningNeeded_ === undefined) { + const { isExperimentalSeaWarningNeeded, isSea } = internalBinding('sea'); + isSEABuiltinWarningNeeded_ = isSea() && isExperimentalSeaWarningNeeded(); + } + return isSEABuiltinWarningNeeded_; +} + +let warnedAboutBuiltins = false; +/** + * Load a built-in module for embedder/SEA modules. + * @param {string} id + * @returns {import('internal/bootstrap/realm.js').BuiltinModule} + */ +function loadBuiltinModuleForEmbedder(id) { + const normalized = BuiltinModule.normalizeRequirableId(id); + if (normalized) { + const mod = loadBuiltinModule(normalized); + if (mod) { + return mod; + } + } + if (isSEABuiltinWarningNeeded() && !warnedAboutBuiltins) { + const { emitWarningSync } = require('internal/process/warning'); + emitWarningSync( + 'Currently the require() provided to the main script embedded into ' + + 'single-executable applications only supports loading built-in modules.\n' + + 'To load a module from disk after the single executable application is ' + + 'launched, use require("module").createRequire().\n' + + 'Support for bundled module loading or virtual file systems are under ' + + 'discussions in https://github.com/nodejs/single-executable'); + warnedAboutBuiltins = true; + } + throw new ERR_UNKNOWN_BUILTIN_MODULE(id); +} + /** @type {Module} */ let $Module = null; /** @@ -470,6 +508,7 @@ module.exports = { getCompileCacheDir, initializeCjsConditions, loadBuiltinModule, + loadBuiltinModuleForEmbedder, makeRequireFunction, normalizeReferrerURL, stringify, diff --git a/src/env_properties.h b/src/env_properties.h index 454750db0113d2..b0854f22962ca2 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -57,6 +57,7 @@ V(onpskexchange_symbol, "onpskexchange") \ V(resource_symbol, "resource_symbol") \ V(trigger_async_id_symbol, "trigger_async_id_symbol") \ + V(embedder_module_hdo, "embedder_module_hdo") \ V(source_text_module_default_hdo, "source_text_module_default_hdo") \ V(vm_context_no_contextify, "vm_context_no_contextify") \ V(vm_dynamic_import_default_internal, "vm_dynamic_import_default_internal") \ From 99b98d713c397e10c98be2c4d970164ccd41cf14 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Mon, 26 Jan 2026 16:08:45 +0100 Subject: [PATCH 7/9] build: add `--shared-lief` configure flag PR-URL: https://github.com/nodejs/node/pull/61536 Reviewed-By: Chengzhong Wu Reviewed-By: Colin Ihrig Reviewed-By: Joyee Cheung --- configure.py | 31 +++++++++++++++++++++++++++++++ node.gyp | 6 +++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/configure.py b/configure.py index 9073b170899cac..e52cb28fa0cf5c 100755 --- a/configure.py +++ b/configure.py @@ -349,6 +349,28 @@ dest='shared_libuv_libpath', help='a directory to search for the shared libuv DLL') +shared_optgroup.add_argument('--shared-lief', + action='store_true', + dest='shared_lief', + default=None, + help='link to a shared lief DLL instead of static linking') + +shared_optgroup.add_argument('--shared-lief-includes', + action='store', + dest='shared_lief_includes', + help='directory containing lief header files') + +shared_optgroup.add_argument('--shared-lief-libname', + action='store', + dest='shared_lief_libname', + default='LIEF', + help='alternative lib name to link to [default: %(default)s]') + +shared_optgroup.add_argument('--shared-lief-libpath', + action='store', + dest='shared_lief_libpath', + help='a directory to search for the shared lief DLL') + shared_optgroup.add_argument('--shared-nbytes', action='store_true', dest='shared_nbytes', @@ -1947,6 +1969,14 @@ def without_ssl_error(option): configure_library('openssl', o) +def configure_lief(o): + if options.without_lief: + if options.shared_lief: + error('--without-lief is incompatible with --shared-lief') + return + + configure_library('lief', o, pkgname='LIEF') + def configure_sqlite(o): o['variables']['node_use_sqlite'] = b(not options.without_sqlite) if options.without_sqlite: @@ -2404,6 +2434,7 @@ def make_bin_override(): configure_library('nghttp2', output, pkgname='libnghttp2') configure_library('nghttp3', output, pkgname='libnghttp3') configure_library('ngtcp2', output, pkgname='libngtcp2') +configure_lief(output); configure_sqlite(output); configure_library('uvwasi', output) configure_library('zstd', output, pkgname='libzstd') diff --git a/node.gyp b/node.gyp index 12a385c7f207cf..4dc42995d921c8 100644 --- a/node.gyp +++ b/node.gyp @@ -18,6 +18,7 @@ 'node_shared_hdr_histogram%': 'false', 'node_shared_http_parser%': 'false', 'node_shared_libuv%': 'false', + 'node_shared_lief%': 'false', 'node_shared_merve%': 'false', 'node_shared_nbytes%': 'false', 'node_shared_nghttp2%': 'false', @@ -996,10 +997,13 @@ '<@(node_quic_sources)', ], }], - [ 'node_use_lief=="true"', { + [ 'node_use_lief=="true" and node_shared_lief=="false"', { 'defines': [ 'HAVE_LIEF=1' ], 'dependencies': [ 'deps/LIEF/lief.gyp:liblief' ], }], + [ 'node_use_lief=="true" and node_shared_lief=="true"', { + 'defines': [ 'HAVE_LIEF=1' ], + }], [ 'node_use_sqlite=="true"', { 'sources': [ '<@(node_sqlite_sources)', From 93e197ffb65640faf6cd7a59ba0eb8597d7cca61 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Tue, 10 Mar 2026 23:37:10 +0100 Subject: [PATCH 8/9] sea: support code cache for ESM entrypoint in SEA The initial support for ESM entrypoint in SEA didn't support code cache. This patch implements that by following a path similar to how code cache in CJS SEA entrypoint is supported: at build time we generate the code cache from C++ and put it into the sea blob, and at runtime we consume it via a special case in compilation routines - for CJS this was CompileFunctionForCJSLoader, in the case of SourceTextModule, it's in Module::New. PR-URL: https://github.com/nodejs/node/pull/62158 Refs: https://github.com/nodejs/node/pull/61813 Reviewed-By: Matteo Collina Reviewed-By: Anna Henningsen --- doc/api/single-executable-applications.md | 3 +- src/module_wrap.cc | 43 ++++++-- src/node_sea.cc | 98 ++++++++++++------- .../sea/esm-code-cache/sea-config.json | 7 ++ test/fixtures/sea/esm-code-cache/sea.mjs | 20 ++++ ...e-executable-application-esm-code-cache.js | 34 +++++++ 6 files changed, 160 insertions(+), 45 deletions(-) create mode 100644 test/fixtures/sea/esm-code-cache/sea-config.json create mode 100644 test/fixtures/sea/esm-code-cache/sea.mjs create mode 100644 test/sea/test-single-executable-application-esm-code-cache.js diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index f40d3305ecb9ca..5774199934e618 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -393,8 +393,7 @@ The accepted values are: If the `mainFormat` field is not specified, it defaults to `"commonjs"`. -Currently, `"mainFormat": "module"` cannot be used together with `"useSnapshot"` -or `"useCodeCache"`. +Currently, `"mainFormat": "module"` cannot be used together with `"useSnapshot"`. ### Module loading in the injected main script diff --git a/src/module_wrap.cc b/src/module_wrap.cc index 5703590b5ee2a9..98012b5d48ed8a 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -1,5 +1,6 @@ #include "module_wrap.h" +#include "debug_utils-inl.h" #include "env.h" #include "memory_tracker-inl.h" #include "node_contextify.h" @@ -7,6 +8,8 @@ #include "node_external_reference.h" #include "node_internals.h" #include "node_process-inl.h" +#include "node_sea.h" +#include "node_url.h" #include "node_watchdog.h" #include "util-inl.h" @@ -397,6 +400,20 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { new ScriptCompiler::CachedData(data + cached_data_buf->ByteOffset(), cached_data_buf->ByteLength()); } +#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION + // For embedder ESM in a SEA, use the bundled code cache if available. + if (id_symbol == realm->isolate_data()->embedder_module_hdo() && + sea::IsSingleExecutable()) { + sea::SeaResource sea = sea::FindSingleExecutableResource(); + if (sea.use_code_cache()) { + std::string_view data = sea.code_cache.value(); + user_cached_data = new ScriptCompiler::CachedData( + reinterpret_cast(data.data()), + static_cast(data.size()), + ScriptCompiler::CachedData::BufferNotOwned); + } + } +#endif // !DISABLE_SINGLE_EXECUTABLE_APPLICATION Local source_text = args[2].As(); bool cache_rejected = false; @@ -421,12 +438,26 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { return; } - if (user_cached_data.has_value() && user_cached_data.value() != nullptr && - cache_rejected) { - THROW_ERR_VM_MODULE_CACHED_DATA_REJECTED( - realm, "cachedData buffer was rejected"); - try_catch.ReThrow(); - return; + if (user_cached_data.has_value() && user_cached_data.value() != nullptr) { +#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION + if (id_symbol == realm->isolate_data()->embedder_module_hdo() && + sea::IsSingleExecutable()) { + if (cache_rejected) { + per_process::Debug(DebugCategory::SEA, + "SEA module code cache rejected\n"); + ProcessEmitWarningSync(realm->env(), "Code cache data rejected."); + } else { + per_process::Debug(DebugCategory::SEA, + "SEA module code cache accepted\n"); + } + } else // NOLINT(readability/braces) +#endif // !DISABLE_SINGLE_EXECUTABLE_APPLICATION + if (cache_rejected) { + THROW_ERR_VM_MODULE_CACHED_DATA_REJECTED( + realm, "cachedData buffer was rejected"); + try_catch.ReThrow(); + return; + } } if (that->Set(context, diff --git a/src/node_sea.cc b/src/node_sea.cc index 85f5eb118c3845..0277127071ef68 100644 --- a/src/node_sea.cc +++ b/src/node_sea.cc @@ -24,6 +24,7 @@ using v8::Array; using v8::ArrayBuffer; using v8::BackingStore; using v8::Context; +using v8::Data; using v8::Function; using v8::FunctionCallbackInfo; using v8::HandleScope; @@ -31,11 +32,13 @@ using v8::Isolate; using v8::Local; using v8::LocalVector; using v8::MaybeLocal; +using v8::Module; using v8::NewStringType; using v8::Object; using v8::ScriptCompiler; using v8::ScriptOrigin; using v8::String; +using v8::UnboundModuleScript; using v8::Value; namespace node { @@ -542,7 +545,7 @@ std::optional ParseSingleExecutableConfig( "\"useCodeCache\" is redundant when \"useSnapshot\" is true\n"); } - // TODO(joyeecheung): support ESM with useSnapshot and useCodeCache. + // TODO(joyeecheung): support ESM with useSnapshot. if (result.main_format == ModuleFormat::kModule && static_cast(result.flags & SeaFlags::kUseSnapshot)) { FPrintF(stderr, @@ -551,14 +554,6 @@ std::optional ParseSingleExecutableConfig( return std::nullopt; } - if (result.main_format == ModuleFormat::kModule && - static_cast(result.flags & SeaFlags::kUseCodeCache)) { - FPrintF(stderr, - "\"mainFormat\": \"module\" is not supported when " - "\"useCodeCache\" is true\n"); - return std::nullopt; - } - if (result.main_path.empty()) { FPrintF(stderr, "\"main\" field of %s is not a non-empty string\n", @@ -616,7 +611,8 @@ ExitCode GenerateSnapshotForSEA(const SeaConfig& config, } std::optional GenerateCodeCache(std::string_view main_path, - std::string_view main_script) { + std::string_view main_script, + ModuleFormat format) { RAIIIsolate raii_isolate(SnapshotBuilder::GetEmbeddedSnapshotData()); Isolate* isolate = raii_isolate.get(); @@ -647,34 +643,62 @@ std::optional GenerateCodeCache(std::string_view main_path, return std::nullopt; } - LocalVector 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 maybe_fn = - ScriptCompiler::CompileFunction(context, - &script_source, - parameters.size(), - parameters.data(), - 0, - nullptr); - Local fn; - if (!maybe_fn.ToLocal(&fn)) { - return std::nullopt; + std::unique_ptr cache; + + if (format == ModuleFormat::kModule) { + // Using empty host defined options is fine as it is not part of the cache + // key and will be reset after deserialization. + ScriptOrigin origin(filename, + 0, // line offset + 0, // column offset + true, // is cross origin + -1, // script id + Local(), // source map URL + false, // is opaque + false, // is WASM + true, // is ES Module + Local()); // host defined options + ScriptCompiler::Source source(content, origin); + Local module; + if (!ScriptCompiler::CompileModule(isolate, &source).ToLocal(&module)) { + return std::nullopt; + } + Local unbound = module->GetUnboundModuleScript(); + cache.reset(ScriptCompiler::CreateCodeCache(unbound)); + } else { + // 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 + // TODO(joyeecheung): this likely has been fixed by + // https://chromium-review.googlesource.com/c/v8/v8/+/5401780 - add a test + // and update docs. + LocalVector 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); + Local fn; + if (!ScriptCompiler::CompileFunction(context, + &script_source, + parameters.size(), + parameters.data(), + 0, + nullptr) + .ToLocal(&fn)) { + return std::nullopt; + } + cache.reset(ScriptCompiler::CreateCodeCacheForFunction(fn)); } - // 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 cache{ - ScriptCompiler::CreateCodeCacheForFunction(fn)}; + if (!cache) { + return std::nullopt; + } std::string code_cache(cache->data, cache->data + cache->length); return code_cache; } @@ -728,7 +752,7 @@ ExitCode GenerateSingleExecutableBlob( std::string code_cache; if (static_cast(config.flags & SeaFlags::kUseCodeCache)) { std::optional optional_code_cache = - GenerateCodeCache(config.main_path, main_script); + GenerateCodeCache(config.main_path, main_script, config.main_format); if (!optional_code_cache.has_value()) { FPrintF(stderr, "Cannot generate V8 code cache\n"); return ExitCode::kGenericUserError; diff --git a/test/fixtures/sea/esm-code-cache/sea-config.json b/test/fixtures/sea/esm-code-cache/sea-config.json new file mode 100644 index 00000000000000..53ba60cc157bde --- /dev/null +++ b/test/fixtures/sea/esm-code-cache/sea-config.json @@ -0,0 +1,7 @@ +{ + "main": "sea.mjs", + "output": "sea", + "mainFormat": "module", + "useCodeCache": true, + "disableExperimentalSEAWarning": true +} diff --git a/test/fixtures/sea/esm-code-cache/sea.mjs b/test/fixtures/sea/esm-code-cache/sea.mjs new file mode 100644 index 00000000000000..b2605a30ed0b63 --- /dev/null +++ b/test/fixtures/sea/esm-code-cache/sea.mjs @@ -0,0 +1,20 @@ +import assert from 'node:assert'; +import { createRequire } from 'node:module'; +import { pathToFileURL } from 'node:url'; +import { dirname } from 'node:path'; + +// Test createRequire with process.execPath. +const assert2 = createRequire(process.execPath)('node:assert'); +assert.strictEqual(assert2.strict, assert.strict); + +// Test import.meta properties. +assert.strictEqual(import.meta.url, pathToFileURL(process.execPath).href); +assert.strictEqual(import.meta.filename, process.execPath); +assert.strictEqual(import.meta.dirname, dirname(process.execPath)); +assert.strictEqual(import.meta.main, true); + +// Test import() with a built-in module. +const { strict } = await import('node:assert'); +assert.strictEqual(strict, assert.strict); + +console.log('ESM SEA with code cache executed successfully'); diff --git a/test/sea/test-single-executable-application-esm-code-cache.js b/test/sea/test-single-executable-application-esm-code-cache.js new file mode 100644 index 00000000000000..1fe6dff7a3da25 --- /dev/null +++ b/test/sea/test-single-executable-application-esm-code-cache.js @@ -0,0 +1,34 @@ +'use strict'; + +// This tests the creation of a single executable application with an ESM +// entry point using "mainFormat": "module" and "useCodeCache": true. + +require('../common'); + +const { + buildSEA, + skipIfBuildSEAIsNotSupported, +} = require('../common/sea'); + +skipIfBuildSEAIsNotSupported(); + +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); + +tmpdir.refresh(); + +const outputFile = buildSEA(fixtures.path('sea', 'esm-code-cache')); + +spawnSyncAndExitWithoutError( + outputFile, + { + env: { + NODE_DEBUG_NATIVE: 'SEA', + ...process.env, + }, + }, + { + stdout: /ESM SEA with code cache executed successfully/, + stderr: /SEA module code cache accepted/, + }); From 2536c5dad63f3324dee2ed36e4d39230534461b2 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Tue, 3 Feb 2026 21:10:23 +0100 Subject: [PATCH 9/9] src: consolidate C++ ReadFileSync/WriteFileSync utilities This patch moves `ReadFileSync` and `WriteFileSync` from `src/util.cc` to `src/node_file_utils.cc`, consolidates the implementation to reuse code, and adds a few more enhancements: For `ReadFileSync`: - Use fstat-based pre-allocation to minimize buffer resizing and repeated reads for bigger files. - Handle various potential overflows in size conversions. - Handle fallback for 0-byte special files. For `WriteFileSync`: - Handle potential partial writes for big enough files and support non-seekable files (with -1 as offset). - Handle 0-byte writes correctly. In both cases, this now avoids hard aborts and return error code for the caller to handle as much as possible, except `std::vector ReadFileSync(FILE* fp)` which is part of the embedder API. This patch uses the new `ReadFileSync` to address a TODO in node_sea.bin.cc. PR-URL: https://github.com/nodejs/node/pull/61662 Reviewed-By: Rafael Gonzaga Reviewed-By: Stephen Belanger Reviewed-By: Anna Henningsen Reviewed-By: James M Snell --- node.gyp | 1 + src/node_file_utils.cc | 248 +++++++++++++++++++++++++++++++++++++++++ src/node_file_utils.h | 39 +++++++ src/node_internals.h | 6 +- src/node_sea_bin.cc | 7 +- src/util.cc | 90 --------------- src/util.h | 6 - 7 files changed, 291 insertions(+), 106 deletions(-) create mode 100644 src/node_file_utils.cc create mode 100644 src/node_file_utils.h diff --git a/node.gyp b/node.gyp index 4dc42995d921c8..99e85e7277ce8a 100644 --- a/node.gyp +++ b/node.gyp @@ -122,6 +122,7 @@ 'src/node_errors.cc', 'src/node_external_reference.cc', 'src/node_file.cc', + 'src/node_file_utils.cc', 'src/node_http_parser.cc', 'src/node_http2.cc', 'src/node_i18n.cc', diff --git a/src/node_file_utils.cc b/src/node_file_utils.cc new file mode 100644 index 00000000000000..5f0ef149213573 --- /dev/null +++ b/src/node_file_utils.cc @@ -0,0 +1,248 @@ +#include "node_file_utils.h" + +#include +#include +#include +#include +#include +#include + +#include "util-inl.h" + +#ifdef _WIN32 +#include // _S_IREAD _S_IWRITE +#ifndef S_IRUSR +#define S_IRUSR _S_IREAD +#endif // S_IRUSR +#ifndef S_IWUSR +#define S_IWUSR _S_IWRITE +#endif // S_IWUSR +#endif + +namespace node { + +int WriteFileSync(const char* path, uv_buf_t buf) { + return WriteFileSync(path, &buf, 1); +} + +constexpr int64_t kCurrentFileOffset = -1; +int WriteFileSync(const char* path, uv_buf_t* bufs, size_t buf_count) { + uv_fs_t req; + int fd = uv_fs_open(nullptr, + &req, + path, + O_WRONLY | O_CREAT | O_TRUNC, + S_IWUSR | S_IRUSR, + nullptr); + uv_fs_req_cleanup(&req); + if (fd < 0) { + return fd; + } + + // Handle potential partial writes by looping until all data is written. + std::vector iovs(bufs, bufs + buf_count); + size_t idx = 0; + + while (idx < iovs.size()) { + // Skip empty buffers. + if (iovs[idx].len == 0) { + idx++; + continue; + } + + uv_fs_write(nullptr, + &req, + fd, + iovs.data() + idx, + iovs.size() - idx, + kCurrentFileOffset, + nullptr); + if (req.result <= 0) { // Error during write. + // UV_EIO should not happen unless the file system is full. + int err = req.result < 0 ? req.result : UV_EIO; + uv_fs_req_cleanup(&req); + uv_fs_close(nullptr, &req, fd, nullptr); + uv_fs_req_cleanup(&req); + return err; + } + size_t written = req.result; + uv_fs_req_cleanup(&req); + + // Consume written bytes from buffers. + while (written > 0 && idx < iovs.size()) { + if (written >= iovs[idx].len) { + written -= iovs[idx].len; + idx++; + } else { + iovs[idx].base += written; + iovs[idx].len -= written; + written = 0; + } + } + } + + int err = uv_fs_close(nullptr, &req, fd, nullptr); + uv_fs_req_cleanup(&req); + return err; +} + +int WriteFileSync(v8::Isolate* isolate, + const char* path, + v8::Local string) { + node::Utf8Value utf8(isolate, string); + uv_buf_t buf = uv_buf_init(utf8.out(), utf8.length()); + return WriteFileSync(path, buf); +} + +// Default size used if fstat reports a file size of 0 for special files. +static constexpr size_t kDefaultReadSize = 8192; + +// The resize_buffer callback is called with the file size after fstat, and must +// return a pointer to a buffer of at least that size. If the file grows during +// reading, resize_buffer may be called again with a larger size; the callback +// must preserve existing content and release old storage if needed. +// After reading completes, resize_buffer may be called with the actual bytes +// read. +template +int ReadFileSyncImpl(const char* path, ResizeBuffer resize_buffer) { + uv_fs_t req; + + uv_file file = uv_fs_open(nullptr, &req, path, O_RDONLY, 0, nullptr); + if (req.result < 0) { + int err = req.result; + uv_fs_req_cleanup(&req); + return err; + } + uv_fs_req_cleanup(&req); + + // Get the file size first, which should be cheap enough on an already opened + // files, and saves us from repeated reallocations/reads. + int err = uv_fs_fstat(nullptr, &req, file, nullptr); + if (err < 0) { + uv_fs_req_cleanup(&req); + uv_fs_close(nullptr, &req, file, nullptr); + uv_fs_req_cleanup(&req); + return err; + } + // SIZE_MAX is ~18 exabytes on 64-bit and ~4 GB on 32-bit systems. + // In both cases, the process is unlikely to have that much memory + // to hold the file content, so we just error with UV_EFBIG. + if (req.statbuf.st_size > static_cast(SIZE_MAX)) { + uv_fs_req_cleanup(&req); + uv_fs_close(nullptr, &req, file, nullptr); + uv_fs_req_cleanup(&req); + return UV_EFBIG; + } + size_t size = static_cast(req.statbuf.st_size); + uv_fs_req_cleanup(&req); + + // If the file is reported as 0 bytes for special files, use a default + // size to start reading. + if (size == 0) { + size = kDefaultReadSize; + } + + char* buffer = resize_buffer(size); + if (buffer == nullptr) { + uv_fs_close(nullptr, &req, file, nullptr); + uv_fs_req_cleanup(&req); + return UV_ENOMEM; + } + size_t total_read = 0; + while (true) { + size_t remaining = size - total_read; + // On Windows, uv_buf_t uses ULONG which may truncate the + // length for large buffers. Limit the individual read request size to + // INT_MAX to be safe. The loop will issue multiple reads for larger files. + if (remaining > INT_MAX) { + remaining = INT_MAX; + } + uv_buf_t buf = uv_buf_init(buffer + total_read, remaining); + uv_fs_read( + nullptr, &req, file, &buf, 1 /* nbufs */, kCurrentFileOffset, nullptr); + ssize_t bytes_read = req.result; + uv_fs_req_cleanup(&req); + if (bytes_read < 0) { + uv_fs_close(nullptr, &req, file, nullptr); + uv_fs_req_cleanup(&req); + return bytes_read; + } + if (bytes_read == 0) { + // EOF, stop reading. + break; + } + total_read += bytes_read; + if (total_read == size) { + // Buffer is full, the file may have grown during reading. + // Try increasing the buffer size and reading more. + if (size == SIZE_MAX) { + uv_fs_close(nullptr, &req, file, nullptr); + uv_fs_req_cleanup(&req); + return UV_EFBIG; + } + if (size > SIZE_MAX / 2) { + size = SIZE_MAX; + } else { + size *= 2; + } + buffer = resize_buffer(size); + if (buffer == nullptr) { + uv_fs_close(nullptr, &req, file, nullptr); + uv_fs_req_cleanup(&req); + return UV_ENOMEM; + } + } + } + + int close_err = uv_fs_close(nullptr, &req, file, nullptr); + uv_fs_req_cleanup(&req); + if (close_err < 0) { + return close_err; + } + + // Truncate the actual size read if necessary. + if (total_read != size) { + buffer = resize_buffer(total_read); + if (buffer == nullptr && total_read != 0) { + return UV_ENOMEM; + } + } + return 0; +} + +int ReadFileSync(const char* path, std::string* result) { + return ReadFileSyncImpl(path, [result](size_t size) { + result->resize(size); + return result->data(); + }); +} + +// Legacy interface. TODO(joyeecheung): update the callers to pass path first, +// output parameters second. +int ReadFileSync(std::string* result, const char* path) { + return ReadFileSync(path, result); +} + +int ReadFileSync(const char* path, std::vector* result) { + return ReadFileSyncImpl(path, [result](size_t size) { + result->resize(size); + return reinterpret_cast(result->data()); + }); +} + +std::vector ReadFileSync(FILE* fp) { + CHECK_EQ(ftell(fp), 0); + int err = fseek(fp, 0, SEEK_END); + CHECK_EQ(err, 0); + size_t size = ftell(fp); + CHECK_NE(size, static_cast(-1L)); + err = fseek(fp, 0, SEEK_SET); + CHECK_EQ(err, 0); + + std::vector contents(size); + size_t num_read = fread(contents.data(), size, 1, fp); + CHECK_EQ(num_read, 1); + return contents; +} + +} // namespace node diff --git a/src/node_file_utils.h b/src/node_file_utils.h new file mode 100644 index 00000000000000..d998c59a7015cf --- /dev/null +++ b/src/node_file_utils.h @@ -0,0 +1,39 @@ +#ifndef SRC_NODE_FILE_UTILS_H_ +#define SRC_NODE_FILE_UTILS_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include +#include +#include +#include + +#include "uv.h" +#include "v8.h" + +namespace node { + +// Synchronously writes to a file. If the file exists, it is replaced +// (truncated). +int WriteFileSync(const char* path, uv_buf_t buf); +int WriteFileSync(const char* path, uv_buf_t* bufs, size_t buf_count); +int WriteFileSync(v8::Isolate* isolate, + const char* path, + v8::Local string); + +// Synchronously reads the entire contents of a file. +int ReadFileSync(const char* path, std::string* result); +int ReadFileSync(const char* path, std::vector* result); + +// Legacy interface. TODO(joyeecheung): update the callers to pass path first, +// output parameters second. +int ReadFileSync(std::string* result, const char* path); + +// This is currently only used by embedder APIs that take a FILE*. +std::vector ReadFileSync(FILE* fp); + +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_NODE_FILE_UTILS_H_ diff --git a/src/node_internals.h b/src/node_internals.h index 8e930a6fecd658..be76c96665bb6d 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -27,6 +27,7 @@ #include "env.h" #include "node.h" #include "node_binding.h" +#include "node_file_utils.h" #include "node_mutex.h" #include "tracing/trace_event.h" #include "util.h" @@ -410,11 +411,6 @@ typedef struct tm TIME_TYPE; #endif double GetCurrentTimeInMicroseconds(); -int WriteFileSync(const char* path, uv_buf_t* bufs, size_t buf_count); -int WriteFileSync(const char* path, uv_buf_t buf); -int WriteFileSync(v8::Isolate* isolate, - const char* path, - v8::Local string); class DiagnosticFilename { public: diff --git a/src/node_sea_bin.cc b/src/node_sea_bin.cc index e0993607cb7ba0..ef5dab3a25c0c5 100644 --- a/src/node_sea_bin.cc +++ b/src/node_sea_bin.cc @@ -405,8 +405,8 @@ ExitCode BuildSingleExecutable(const std::string& sea_config_path, int src_mode = static_cast(req.statbuf.st_mode); uv_fs_req_cleanup(&req); - std::string exe; - r = ReadFileSync(&exe, config.executable_path.c_str()); + std::vector exe_data; + r = ReadFileSync(config.executable_path.c_str(), &exe_data); if (r != 0) { FPrintF(stderr, "Error: Couldn't read executable %s: %s\n", @@ -415,9 +415,6 @@ ExitCode BuildSingleExecutable(const std::string& sea_config_path, return ExitCode::kGenericUserError; } - // TODO(joyeecheung): add a variant of ReadFileSync that reads into - // vector directly and avoid this copy. - std::vector exe_data(exe.begin(), exe.end()); std::vector sea_blob; ExitCode code = GenerateSingleExecutableBlob(&sea_blob, config, args, exec_args); diff --git a/src/util.cc b/src/util.cc index 660cfff6b8a0c5..914fd1447ceeef 100644 --- a/src/util.cc +++ b/src/util.cc @@ -225,96 +225,6 @@ double GetCurrentTimeInMicroseconds() { return kMicrosecondsPerSecond * tv.tv_sec + tv.tv_usec; } -int WriteFileSync(const char* path, uv_buf_t buf) { - return WriteFileSync(path, &buf, 1); -} - -int WriteFileSync(const char* path, uv_buf_t* bufs, size_t buf_count) { - uv_fs_t req; - int fd = uv_fs_open(nullptr, - &req, - path, - O_WRONLY | O_CREAT | O_TRUNC, - S_IWUSR | S_IRUSR, - nullptr); - uv_fs_req_cleanup(&req); - if (fd < 0) { - return fd; - } - - int err = uv_fs_write(nullptr, &req, fd, bufs, buf_count, 0, nullptr); - uv_fs_req_cleanup(&req); - if (err < 0) { - return err; - } - - err = uv_fs_close(nullptr, &req, fd, nullptr); - uv_fs_req_cleanup(&req); - return err; -} - -int WriteFileSync(v8::Isolate* isolate, - const char* path, - v8::Local string) { - node::Utf8Value utf8(isolate, string); - uv_buf_t buf = uv_buf_init(utf8.out(), utf8.length()); - return WriteFileSync(path, buf); -} - -int ReadFileSync(std::string* result, const char* path) { - uv_fs_t req; - auto defer_req_cleanup = OnScopeLeave([&req]() { - uv_fs_req_cleanup(&req); - }); - - uv_file file = uv_fs_open(nullptr, &req, path, O_RDONLY, 0, nullptr); - if (req.result < 0) { - // req will be cleaned up by scope leave. - return req.result; - } - uv_fs_req_cleanup(&req); - - auto defer_close = OnScopeLeave([file]() { - uv_fs_t close_req; - CHECK_EQ(0, uv_fs_close(nullptr, &close_req, file, nullptr)); - uv_fs_req_cleanup(&close_req); - }); - - *result = std::string(""); - char buffer[4096]; - uv_buf_t buf = uv_buf_init(buffer, sizeof(buffer)); - - while (true) { - const int r = - uv_fs_read(nullptr, &req, file, &buf, 1, result->length(), nullptr); - if (req.result < 0) { - // req will be cleaned up by scope leave. - return req.result; - } - uv_fs_req_cleanup(&req); - if (r <= 0) { - break; - } - result->append(buf.base, r); - } - return 0; -} - -std::vector ReadFileSync(FILE* fp) { - CHECK_EQ(ftell(fp), 0); - int err = fseek(fp, 0, SEEK_END); - CHECK_EQ(err, 0); - size_t size = ftell(fp); - CHECK_NE(size, static_cast(-1L)); - err = fseek(fp, 0, SEEK_SET); - CHECK_EQ(err, 0); - - std::vector contents(size); - size_t num_read = fread(contents.data(), size, 1, fp); - CHECK_EQ(num_read, 1); - return contents; -} - void DiagnosticFilename::LocalTime(TIME_TYPE* tm_struct) { #ifdef _WIN32 GetLocalTime(tm_struct); diff --git a/src/util.h b/src/util.h index 81d08c27fb7037..674ee80e11e692 100644 --- a/src/util.h +++ b/src/util.h @@ -859,12 +859,6 @@ std::unique_ptr static_unique_pointer_cast(std::unique_ptr&& ptr) { #define MAYBE_FIELD_PTR(ptr, field) ptr == nullptr ? nullptr : &(ptr->field) -// Returns a non-zero code if it fails to open or read the file, -// aborts if it fails to close the file. -int ReadFileSync(std::string* result, const char* path); -// Reads all contents of a FILE*, aborts if it fails. -std::vector ReadFileSync(FILE* fp); - v8::Local NewFunctionTemplate( v8::Isolate* isolate, v8::FunctionCallback callback,