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..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', @@ -898,6 +920,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 +1717,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: @@ -1933,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: @@ -2390,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/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 @@ -485,7 +463,7 @@ to a temporary file and loading it with `process.dlopen()`. ```json { "main": "/path/to/bundled/script.js", - "output": "/path/to/write/the/generated/blob.blob", + "output": "/path/to/write/the/generated/executable", "assets": { "myaddon.node": "/path/to/myaddon/build/Release/myaddon.node" } @@ -515,19 +493,134 @@ a non-container Linux arm64 environment to work around this issue. ### Single executable application creation process -A tool aiming to create a single executable Node.js application must -inject the contents of the blob prepared with `--experimental-sea-config"` -into: +The process documented here is subject to change. + +#### Generating single executable preparation blobs + +To build a single executable application, Node.js would first generate a blob +that contains all the necessary information to run the bundled script. +When using `--build-sea`, this step is done internally along with the injection. + +##### Dumping the preparation blob to disk -* a resource named `NODE_SEA_BLOB` if the `node` binary is a [PE][] file -* a section named `NODE_SEA_BLOB` in the `NODE_SEA` segment if the `node` binary - is a [Mach-O][] file -* a note named `NODE_SEA_BLOB` if the `node` binary is an [ELF][] file +Before `--build-sea` was introduced, an older workflow was introduced to write the +preparation blob to disk for injection by external tools. This can still +be used for verification purposes. -Search the binary for the +To dump the preparation blob to disk for verification, use `--experimental-sea-config`. +This writes a file that can be injected into a Node.js binary using tools like [postject][]. + +The configuration is similar to that of `--build-sea`, except that the +`output` field specifies the path to write the generated blob file instead of +the final executable. + +```json +{ + "main": "/path/to/bundled/script.js", + // Instead of the final executable, this is the path to write the blob. + "output": "/path/to/write/the/generated/blob.blob" +} +``` + +#### Injecting the preparation blob into the `node` binary + +To complete the creation of a single executable application, the generated blob +needs to be injected into a copy of the `node` binary, as documented below. + +When using `--build-sea`, this step is done internally along with the blob generation. + +* If the `node` binary is a [PE][] file, the blob should be injected as a resource + named `NODE_SEA_BLOB`. +* If the `node` binary is a [Mach-O][] file, the blob should be injected as a section + named `NODE_SEA_BLOB` in the `NODE_SEA` segment. +* If the `node` binary is an [ELF][] file, the blob should be injected as a note + named `NODE_SEA_BLOB`. + +Then, the SEA building process searches the binary for the `NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0` [fuse][] string and flip the last character to `1` to indicate that a resource has been injected. +##### Injecting the preparation blob manually + +Before `--build-sea` was introduced, an older workflow was introduced to allow +external tools to inject the generated blob into a copy of the `node` binary. + +For example, with [postject][]: + +1. Create a copy of the `node` executable and name it according to your needs: + + * On systems other than Windows: + + ```bash + cp $(command -v node) hello + ``` + + * On Windows: + + ```text + node -e "require('fs').copyFileSync(process.execPath, 'hello.exe')" + ``` + + The `.exe` extension is necessary. + +2. Remove the signature of the binary (macOS and Windows only): + + * On macOS: + + ```bash + codesign --remove-signature hello + ``` + + * On Windows (optional): + + [signtool][] can be used from the installed [Windows SDK][]. If this step is + skipped, ignore any signature-related warning from postject. + + ```powershell + signtool remove /s hello.exe + ``` + +3. Inject the blob into the copied binary by running `postject` with + the following options: + + * `hello` / `hello.exe` - The name of the copy of the `node` executable + created in step 4. + * `NODE_SEA_BLOB` - The name of the resource / note / section in the binary + where the contents of the blob will be stored. + * `sea-prep.blob` - The name of the blob created in step 1. + * `--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2` - The + [fuse][] used by the Node.js project to detect if a file has been injected. + * `--macho-segment-name NODE_SEA` (only needed on macOS) - The name of the + segment in the binary where the contents of the blob will be + stored. + + To summarize, here is the required command for each platform: + + * On Linux: + ```bash + npx postject hello NODE_SEA_BLOB sea-prep.blob \ + --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 + ``` + + * On Windows - PowerShell: + ```powershell + npx postject hello.exe NODE_SEA_BLOB sea-prep.blob ` + --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 + ``` + + * On Windows - Command Prompt: + ```text + npx postject hello.exe NODE_SEA_BLOB sea-prep.blob ^ + --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 + ``` + + * On macOS: + ```bash + npx postject hello NODE_SEA_BLOB sea-prep.blob \ + --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \ + --macho-segment-name NODE_SEA + ``` + ### Platform support Single-executable support is tested regularly on CI only on the following @@ -546,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/doc/node.1 b/doc/node.1 index 9a0f5beb5b995f..4a67174281e59e 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -94,6 +94,9 @@ Allow execution of WASI when using the permission model. .It Fl -allow-worker Allow creating worker threads when using the permission model. . +.It Fl -build-sea +Build a Node.js single executable application +. .It Fl -completion-bash Print source-able bash completion script for Node.js. . 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/node.gyp b/node.gyp index f5cd416b5fe7a5..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', @@ -30,6 +31,7 @@ 'node_snapshot_main%': '', 'node_use_amaro%': 'true', 'node_use_bundled_v8%': 'true', + 'node_use_lief%': 'false', 'node_use_node_snapshot%': 'false', 'node_use_openssl%': 'true', 'node_use_sqlite%': 'true', @@ -995,6 +997,13 @@ '<@(node_quic_sources)', ], }], + [ '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)', 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") \ diff --git a/src/node.cc b/src/node.cc index 0bc086ccd1ff44..73a4a76acc8035 100644 --- a/src/node.cc +++ b/src/node.cc @@ -1554,14 +1554,21 @@ static ExitCode StartInternal(int argc, char** argv) { uv_loop_configure(uv_default_loop(), UV_METRICS_IDLE_TIME); std::string sea_config = per_process::cli_options->experimental_sea_config; if (!sea_config.empty()) { -#if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION) - return sea::BuildSingleExecutableBlob( - sea_config, result->args(), result->exec_args()); -#else +#if defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION) fprintf(stderr, "Single executable application is disabled.\n"); return ExitCode::kGenericUserError; -#endif // !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION) +#else + return sea::WriteSingleExecutableBlob( + sea_config, result->args(), result->exec_args()); +#endif } + + sea_config = per_process::cli_options->build_sea; + if (!sea_config.empty()) { + return sea::BuildSingleExecutable( + sea_config, result->args(), result->exec_args()); + } + // --build-snapshot indicates that we are in snapshot building mode. if (per_process::cli_options->per_isolate->build_snapshot) { if (per_process::cli_options->per_isolate->build_snapshot_config.empty() && diff --git a/src/node_metadata.cc b/src/node_metadata.cc index a6063ce4d25409..6156aea9a3b3cf 100644 --- a/src/node_metadata.cc +++ b/src/node_metadata.cc @@ -47,6 +47,10 @@ #include #endif // NODE_HAVE_I18N_SUPPORT +#if HAVE_LIEF +#include "LIEF/version.h" +#endif + namespace node { namespace per_process { @@ -109,6 +113,13 @@ Metadata::Versions::Versions() { modules = NODE_STRINGIFY(NODE_MODULE_VERSION); nghttp2 = NGHTTP2_VERSION; napi = NODE_STRINGIFY(NODE_API_SUPPORTED_VERSION_MAX); + +#if HAVE_LIEF + lief = (std::to_string(LIEF_VERSION_MAJOR) + "." + + std::to_string(LIEF_VERSION_MINOR) + "." + + std::to_string(LIEF_VERSION_PATCH)); +#endif + llhttp = NODE_STRINGIFY(LLHTTP_VERSION_MAJOR) "." diff --git a/src/node_metadata.h b/src/node_metadata.h index 7f6268a1d83d31..f2f44c53487cde 100644 --- a/src/node_metadata.h +++ b/src/node_metadata.h @@ -93,12 +93,19 @@ namespace node { #define NODE_VERSIONS_KEY_SQLITE(V) #endif +#if HAVE_LIEF +#define NODE_VERSIONS_KEY_LIEF(V) V(lief) +#else +#define NODE_VERSIONS_KEY_LIEF(V) +#endif + #define NODE_VERSIONS_KEYS(V) \ NODE_VERSIONS_KEYS_BASE(V) \ NODE_VERSIONS_KEY_CRYPTO(V) \ NODE_VERSIONS_KEY_INTL(V) \ NODE_VERSIONS_KEY_QUIC(V) \ - NODE_VERSIONS_KEY_SQLITE(V) + NODE_VERSIONS_KEY_SQLITE(V) \ + NODE_VERSIONS_KEY_LIEF(V) #define V(key) +1 constexpr int NODE_VERSIONS_KEY_COUNT = NODE_VERSIONS_KEYS(V); diff --git a/src/node_options.cc b/src/node_options.cc index f6f81f50c8bd91..de79b7fbe92ba8 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -101,6 +101,15 @@ void PerProcessOptions::CheckOptions(std::vector* errors, #endif // V8_ENABLE_SANDBOX #endif // HAVE_OPENSSL + if (!build_sea.empty()) { +#if defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION) + errors->push_back("Single executable application is disabled.\n"); +#elif !defined(HAVE_LIEF) + errors->push_back( + "Node.js must be built with LIEF to build support --build-sea.\n"); +#endif // !defined(HAVE_LIEF) + } + if (use_largepages != "off" && use_largepages != "on" && use_largepages != "silent") { @@ -1417,6 +1426,10 @@ PerProcessOptionsParser::PerProcessOptionsParser( "performance.", &PerProcessOptions::disable_wasm_trap_handler, kAllowedInEnvvar); + + AddOption("--build-sea", + "Build a Node.js single executable application", + &PerProcessOptions::build_sea); } inline std::string RemoveBrackets(const std::string& host) { diff --git a/src/node_options.h b/src/node_options.h index 887ead81d4b8bd..078353fc26c3d8 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -338,6 +338,7 @@ class PerProcessOptions : public Options { std::string experimental_sea_config; std::string run; + std::string build_sea; #ifdef NODE_HAVE_I18N_SUPPORT std::string icu_data_dir; #endif diff --git a/src/node_sea.cc b/src/node_sea.cc index bea8d27e243853..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}; @@ -247,7 +258,6 @@ SeaResource FindSingleExecutableResource() { return sea_resource; } - void IsSea(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(IsSingleExecutable()); } @@ -334,17 +344,6 @@ std::tuple FixupArgsForSEA(int argc, char** argv) { 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 assets; - std::vector exec_argv; -}; - std::optional ParseSingleExecutableConfig( const std::string& config_path) { std::string config; @@ -402,6 +401,14 @@ std::optional ParseSingleExecutableConfig( config_path); return std::nullopt; } + } else if (key == "executable") { + if (field.value().get_string().get(result.executable_path) || + result.executable_path.empty()) { + FPrintF(stderr, + "\"executable\" 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)) { @@ -505,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; + } } } @@ -516,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", @@ -533,6 +576,7 @@ std::optional ParseSingleExecutableConfig( return result; } +namespace { ExitCode GenerateSnapshotForSEA(const SeaConfig& config, const std::vector& args, const std::vector& exec_args, @@ -650,7 +694,10 @@ int BuildAssets(const std::unordered_map& config, return 0; } +} // anonymous namespace + ExitCode GenerateSingleExecutableBlob( + std::vector* out, const SeaConfig& config, const std::vector& args, const std::vector& exec_args) { @@ -709,15 +756,35 @@ 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}; SeaSerializer serializer; serializer.Write(sea); + std::swap(*out, serializer.sink); + return ExitCode::kNoFailure; +} + +ExitCode WriteSingleExecutableBlob(const std::string& config_path, + const std::vector& args, + const std::vector& exec_args) { + std::optional config_opt = + ParseSingleExecutableConfig(config_path); + if (!config_opt.has_value()) { + return ExitCode::kGenericUserError; + } - uv_buf_t buf = uv_buf_init(serializer.sink.data(), serializer.sink.size()); - r = WriteFileSync(config.output_path.c_str(), buf); + SeaConfig config = config_opt.value(); + std::vector blob; + ExitCode exit_code = + GenerateSingleExecutableBlob(&blob, config, args, exec_args); + if (exit_code != ExitCode::kNoFailure) { + return exit_code; + } + uv_buf_t buf = uv_buf_init(blob.data(), blob.size()); + int 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); @@ -730,22 +797,6 @@ ExitCode GenerateSingleExecutableBlob( return ExitCode::kNoFailure; } -} // anonymous namespace - -ExitCode BuildSingleExecutableBlob(const std::string& config_path, - const std::vector& args, - const std::vector& exec_args) { - std::optional 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& args) { CHECK_EQ(args.Length(), 1); CHECK(args[0]->IsString()); @@ -789,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) { @@ -818,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 e91916096b8a9e..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 { @@ -37,11 +38,23 @@ enum class SeaExecArgvExtension : uint8_t { kCli = 2, }; +struct SeaConfig { + std::string main_path; + std::string output_path; + 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; +}; + struct SeaResource { SeaFlags flags = SeaFlags::kDefault; 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; @@ -49,15 +62,16 @@ 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(); std::string_view FindSingleExecutableBlob(); SeaResource FindSingleExecutableResource(); std::tuple FixupArgsForSEA(int argc, char** argv); -node::ExitCode BuildSingleExecutableBlob( +node::ExitCode WriteSingleExecutableBlob( const std::string& config_path, const std::vector& args, const std::vector& exec_args); @@ -67,6 +81,16 @@ node::ExitCode BuildSingleExecutableBlob( // Otherwise returns false and the caller is expected to call LoadEnvironment() // differently. bool MaybeLoadSingleExecutableApplication(Environment* env); +std::optional ParseSingleExecutableConfig( + const std::string& config_path); +ExitCode BuildSingleExecutable(const std::string& sea_config_path, + const std::vector& args, + const std::vector& exec_args); +ExitCode GenerateSingleExecutableBlob( + std::vector* out, + const SeaConfig& config, + const std::vector& args, + const std::vector& exec_args); } // namespace sea } // namespace node diff --git a/src/node_sea_bin.cc b/src/node_sea_bin.cc index 6a85218dfddbf2..e0993607cb7ba0 100644 --- a/src/node_sea_bin.cc +++ b/src/node_sea_bin.cc @@ -1,36 +1,47 @@ #include "node_sea.h" #ifdef HAVE_LIEF +// Temporarily undefine DEBUG because LIEF uses it as an enum name. +#if defined(DEBUG) +#define SAVED_DEBUG_VALUE DEBUG +#undef DEBUG #include "LIEF/LIEF.hpp" +#define DEBUG SAVED_DEBUG_VALUE +#undef SAVED_DEBUG_VALUE +#else // defined(DEBUG) +#include "LIEF/LIEF.hpp" +#endif // defined(DEBUG) #endif // HAVE_LIEF #include "debug_utils-inl.h" #include "env-inl.h" +#include "node_exit_code.h" +#include "simdutf.h" #include "util-inl.h" #include -#include -#include #include -#include #include -#include -#include -#include #include +#include + +// Split them into two for use later to avoid conflicting with postject. +#define SEA_SENTINEL_PREFIX "NODE_SEA_FUSE" +#define SEA_SENTINEL_TAIL "fce680ab2cc467b6e072b8b5df1996b2" // 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" +#define POSTJECT_SENTINEL_FUSE SEA_SENTINEL_PREFIX "_" SEA_SENTINEL_TAIL #include "postject-api.h" #undef POSTJECT_SENTINEL_FUSE namespace node { namespace sea { +// TODO(joyeecheung): use LIEF to locate it directly. std::string_view FindSingleExecutableBlob() { #if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION) CHECK(IsSingleExecutable()); @@ -62,5 +73,415 @@ bool IsSingleExecutable() { return postject_has_resource(); } +#ifdef HAVE_LIEF +static constexpr const char* kSEAResourceName = "NODE_SEA_BLOB"; +static constexpr const char* kELFSectionName = ".note.node.sea"; +static constexpr const char* kMachoSegmentName = "NODE_SEA"; + +enum class InjectResult { kAlreadyExists, kError, kSuccess, kUnknownFormat }; + +struct InjectOutput { + InjectResult result; + std::vector data; // Empty if result != kSuccess + std::string error_message; // Populated when result != kSuccess +}; + +template +static void DebugLog(const char* format, Args&&... args) { + per_process::Debug(DebugCategory::SEA, "[SEA] "); + per_process::Debug(DebugCategory::SEA, format, std::forward(args)...); +} + +InjectOutput InjectIntoELF(const std::vector& executable, + const std::string& note_name, + const std::vector& data) { + DebugLog("Parsing ELF binary for injection...\n"); + std::unique_ptr binary = + LIEF::ELF::Parser::parse(executable); + if (!binary) { + return {InjectResult::kError, {}, "Failed to parse ELF binary"}; + } + + constexpr uint32_t kNoteType = 0; + + std::unique_ptr existing_note; + DebugLog("Searching for existing note \'%s\'\n", note_name); + for (const auto& n : binary->notes()) { + // LIEF can return a length longer than it actually is, so use compare here. + if (n.name().compare(0, note_name.size(), note_name) == 0) { + DebugLog("Found existing note %s\n", note_name); + return {InjectResult::kAlreadyExists, + {}, + SPrintF("note section %s already exists in the ELF executable", + note_name)}; + } + } + + DebugLog("No existing note found. Proceeding to add new note.\n"); + + auto new_note = + LIEF::ELF::Note::create(note_name, kNoteType, data, kELFSectionName); + if (!new_note) { + return {InjectResult::kError, + {}, + SPrintF("Failed to create new ELF note %s", note_name)}; + } + binary->add(*new_note); + + LIEF::ELF::Builder::config_t cfg; + cfg.notes = true; // Ensure notes are rebuilt + cfg.dynamic_section = true; // Ensure PT_DYNAMIC is rebuilt + + DebugLog("Building modified ELF binary with new note...\n"); + LIEF::ELF::Builder builder(*binary, cfg); + builder.build(); + if (builder.get_build().empty()) { + return {InjectResult::kError, {}, "Failed to build modified ELF binary"}; + } + return InjectOutput{InjectResult::kSuccess, builder.get_build(), ""}; +} + +InjectOutput InjectIntoMachO(const std::vector& executable, + const std::string& segment_name, + const std::string& section_name, + const std::vector& data) { + DebugLog("Parsing Mach-O binary for injection...\n"); + std::unique_ptr fat_binary = + LIEF::MachO::Parser::parse(executable); + if (!fat_binary) { + return {InjectResult::kError, {}, "Failed to parse Mach-O binary"}; + } + + // Inject into all Mach-O binaries if there's more than one in a fat binary + DebugLog( + "Searching for existing section %s/%s\n", segment_name, section_name); + for (auto& binary : *fat_binary) { + LIEF::MachO::SegmentCommand* segment = binary.get_segment(segment_name); + if (!segment) { + // Create the segment and mark it read-only + LIEF::MachO::SegmentCommand new_segment(segment_name); + // Use SegmentCommand::VM_PROTECTIONS enum values (READ) + new_segment.max_protection(static_cast( + LIEF::MachO::SegmentCommand::VM_PROTECTIONS::READ)); + new_segment.init_protection(static_cast( + LIEF::MachO::SegmentCommand::VM_PROTECTIONS::READ)); + LIEF::MachO::Section section(section_name, data); + new_segment.add_section(section); + binary.add(new_segment); + } else { + // Check if the section exists + LIEF::MachO::Section* existing_section = + segment->get_section(section_name); + if (existing_section) { + // TODO(joyeecheung): support overwrite. + return {InjectResult::kAlreadyExists, + {}, + SPrintF("Segment/section %s/%s already exists in the Mach-O " + "executable", + segment_name, + section_name)}; + } + LIEF::MachO::Section section(section_name, data); + binary.add_section(*segment, section); + } + + // It will need to be signed again anyway, so remove the signature + if (binary.has_code_signature()) { + DebugLog("Removing existing code signature\n"); + if (binary.remove_signature()) { + DebugLog("Code signature removed successfully\n"); + } else { + return {InjectResult::kError, + {}, + "Failed to remove existing code signature"}; + } + } + } + + return InjectOutput{InjectResult::kSuccess, fat_binary->raw(), ""}; +} + +InjectOutput InjectIntoPE(const std::vector& executable, + const std::string& resource_name, + const std::vector& data) { + DebugLog("Parsing PE binary for injection...\n"); + std::unique_ptr binary = + LIEF::PE::Parser::parse(executable); + if (!binary) { + return {InjectResult::kError, {}, "Failed to parse PE binary"}; + } + + // TODO(postject) - lief.PE.ResourcesManager doesn't support RCDATA it seems, + // add support so this is simpler? + if (!binary->has_resources()) { + // TODO(postject) - Handle this edge case by creating the resource tree + return { + InjectResult::kError, {}, "PE binary has no resources, cannot inject"}; + } + + LIEF::PE::ResourceNode* resources = binary->resources(); + LIEF::PE::ResourceNode* rcdata_node = nullptr; + LIEF::PE::ResourceNode* id_node = nullptr; + + // First level => Type (ResourceDirectory node) + DebugLog("Locating/creating RCDATA resource node\n"); + constexpr uint32_t RCDATA_ID = + static_cast(LIEF::PE::ResourcesManager::TYPE::RCDATA); + auto rcdata_node_iter = std::find_if(std::begin(resources->childs()), + std::end(resources->childs()), + [](const LIEF::PE::ResourceNode& node) { + return node.id() == RCDATA_ID; + }); + if (rcdata_node_iter != std::end(resources->childs())) { + DebugLog("Found existing RCDATA resource node\n"); + rcdata_node = &*rcdata_node_iter; + } else { + DebugLog("Creating new RCDATA resource node\n"); + LIEF::PE::ResourceDirectory new_rcdata_node; + new_rcdata_node.id(RCDATA_ID); + rcdata_node = &resources->add_child(new_rcdata_node); + } + + // Second level => ID (ResourceDirectory node) + DebugLog("Locating/creating ID resource node for %s\n", resource_name); + DCHECK(simdutf::validate_ascii(resource_name.data(), resource_name.size())); + std::u16string resource_name_u16(resource_name.begin(), resource_name.end()); + auto id_node_iter = + std::find_if(std::begin(rcdata_node->childs()), + std::end(rcdata_node->childs()), + [resource_name_u16](const LIEF::PE::ResourceNode& node) { + return node.name() == resource_name_u16; + }); + if (id_node_iter != std::end(rcdata_node->childs())) { + DebugLog("Found existing ID resource node for %s\n", resource_name); + id_node = &*id_node_iter; + } else { + // TODO(postject) - This isn't documented, but if this isn't set then + // LIEF won't save the name. Seems like LIEF should be able + // to automatically handle this if you've set the node's name + DebugLog("Creating new ID resource node for %s\n", resource_name); + LIEF::PE::ResourceDirectory new_id_node; + new_id_node.name(resource_name); + new_id_node.id(0x80000000); + id_node = &rcdata_node->add_child(new_id_node); + } + + // Third level => Lang (ResourceData node) + DebugLog("Locating existing language resource node for %s\n", resource_name); + if (id_node->childs() != std::end(id_node->childs())) { + DebugLog("Found existing language resource node for %s\n", resource_name); + return {InjectResult::kAlreadyExists, + {}, + SPrintF("Resource %s already exists in the PE executable", + resource_name)}; + } + + LIEF::PE::ResourceData lang_node(data); + id_node->add_child(lang_node); + + DebugLog("Rebuilding PE resources with new data for %s\n", resource_name); + // Write out the binary, only modifying the resources + LIEF::PE::Builder::config_t cfg; + cfg.resources = true; + cfg.rsrc_section = ".rsrc"; // ensure section name + LIEF::PE::Builder builder(*binary, cfg); + if (!builder.build()) { + return {InjectResult::kError, {}, "Failed to build modified PE binary"}; + } + + return InjectOutput{InjectResult::kSuccess, builder.get_build(), ""}; +} + +void MarkSentinel(InjectOutput* output, const std::string& sentinel_fuse) { + if (output == nullptr || output->result != InjectResult::kSuccess) return; + std::string_view fuse(sentinel_fuse); + std::string_view data_view(reinterpret_cast(output->data.data()), + output->data.size()); + + size_t first_pos = data_view.find(fuse); + DebugLog("Searching for fuse: %s\n", sentinel_fuse); + if (first_pos == std::string::npos) { + output->result = InjectResult::kError; + output->error_message = SPrintF("sentinel %s not found", sentinel_fuse); + return; + } + + size_t last_pos = data_view.rfind(fuse); + if (first_pos != last_pos) { + output->result = InjectResult::kError; + output->error_message = + SPrintF("found more than one occurrence of sentinel %s", sentinel_fuse); + return; + } + + size_t colon_pos = first_pos + fuse.size(); + if (colon_pos >= data_view.size() || data_view[colon_pos] != ':') { + output->result = InjectResult::kError; + output->error_message = + SPrintF("missing ':' after sentinel %s", sentinel_fuse); + return; + } + + size_t idx = colon_pos + 1; + // Expecting ':0' or ':1' after the fuse + if (idx >= data_view.size()) { + output->result = InjectResult::kError; + output->error_message = "Sentinel index out of range"; + return; + } + + DebugLog("Found fuse: %s\n", data_view.substr(first_pos, fuse.size() + 2)); + + if (data_view[idx] == '0') { + DebugLog("Marking sentinel as 1\n"); + output->data.data()[idx] = '1'; + } else if (data_view[idx] == '1') { + output->result = InjectResult::kAlreadyExists; + output->error_message = "Sentinel is already marked"; + return; + } else { + output->result = InjectResult::kError; + output->error_message = SPrintF("Sentinel has invalid value %d", + static_cast(data_view[idx])); + return; + } + DebugLog("Processed fuse: %s\n", + data_view.substr(first_pos, fuse.size() + 2)); + + return; +} + +InjectOutput InjectResource(const std::vector& exe, + const std::string& resource_name, + const std::vector& res, + const std::string& macho_segment_name) { + if (LIEF::ELF::is_elf(exe)) { + return InjectIntoELF(exe, resource_name, res); + } else if (LIEF::MachO::is_macho(exe)) { + std::string sec = resource_name; + if (!(sec.rfind("__", 0) == 0)) sec = "__" + sec; + return InjectIntoMachO(exe, macho_segment_name, sec, res); + } else if (LIEF::PE::is_pe(exe)) { + std::string upper_name = resource_name; + // Convert resource name to uppercase as PE resource names are + // case-insensitive. + std::transform(upper_name.begin(), + upper_name.end(), + upper_name.begin(), + [](unsigned char c) { return std::toupper(c); }); + return InjectIntoPE(exe, upper_name, res); + } + + return {InjectResult::kUnknownFormat, + {}, + "Executable must be a supported format: ELF, PE, or Mach-O"}; +} + +ExitCode BuildSingleExecutable(const std::string& sea_config_path, + const std::vector& args, + const std::vector& exec_args) { + std::optional opt_config = + ParseSingleExecutableConfig(sea_config_path); + if (!opt_config.has_value()) { + return ExitCode::kGenericUserError; + } + + SeaConfig config = opt_config.value(); + if (config.executable_path.empty()) { + config.executable_path = Environment::GetExecPath(args); + } + + // Get file permissions from source executable to copy over later. + uv_fs_t req; + int r = uv_fs_stat(nullptr, &req, config.executable_path.c_str(), nullptr); + if (r != 0) { + FPrintF(stderr, + "Error: Couldn't stat executable %s: %s\n", + config.executable_path, + uv_strerror(r)); + uv_fs_req_cleanup(&req); + return ExitCode::kGenericUserError; + } + 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()); + if (r != 0) { + FPrintF(stderr, + "Error: Couldn't read executable %s: %s\n", + config.executable_path, + uv_strerror(r)); + 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); + if (code != ExitCode::kNoFailure) { + return code; + } + // TODO(joyeecheung): refactor serializer implementation and avoid copying + std::vector sea_blob_u8(sea_blob.begin(), sea_blob.end()); + // For backward compatibility with postject, we construct the sentinel fuse + // at runtime instead using a constant. + std::string fuse = std::string(SEA_SENTINEL_PREFIX) + "_" + SEA_SENTINEL_TAIL; + InjectOutput out = InjectResource( + exe_data, kSEAResourceName, sea_blob_u8, kMachoSegmentName); + if (out.result == InjectResult::kSuccess) { + MarkSentinel(&out, fuse); + } + + if (out.result != InjectResult::kSuccess) { + if (!out.error_message.empty()) { + FPrintF(stderr, "Error: %s\n", out.error_message); + } + return ExitCode::kGenericUserError; + } + + uv_buf_t buf = uv_buf_init(reinterpret_cast(out.data.data()), + static_cast(out.data.size())); + r = WriteFileSync(config.output_path.c_str(), buf); + if (r != 0) { + FPrintF(stderr, + "Error: Couldn't write output executable: %s: %s\n", + config.output_path, + uv_strerror(r)); + return ExitCode::kGenericUserError; + } + + // Copy file permissions (including execute bit) from source executable + r = uv_fs_chmod(nullptr, &req, config.output_path.c_str(), src_mode, nullptr); + uv_fs_req_cleanup(&req); + if (r != 0) { + FPrintF(stderr, + "Warning: Couldn't set permissions %d on %s: %s\n", + src_mode, + config.output_path, + uv_strerror(r)); + } + + FPrintF(stdout, + "Generated single executable %s + %s -> %s\n", + config.executable_path, + sea_config_path, + config.output_path); + return ExitCode::kNoFailure; +} +#else +ExitCode BuildSingleExecutable(const std::string& sea_config_path, + const std::vector& args, + const std::vector& exec_args) { + FPrintF( + stderr, + "Error: Node.js must be built with the LIEF library to support built-in" + " single executable applications.\n"); + return ExitCode::kGenericUserError; +} +#endif // HAVE_LIEF + } // namespace sea } // namespace node diff --git a/test/common/sea.js b/test/common/sea.js index 2ba5de286fdddb..d974be1b61c5b4 100644 --- a/test/common/sea.js +++ b/test/common/sea.js @@ -11,8 +11,15 @@ const assert = require('assert'); const { readFileSync, copyFileSync, statSync } = require('fs'); const { spawnSyncAndExitWithoutError, + spawnSyncAndAssert, } = require('../common/child_process'); +function skipIfBuildSEAIsNotSupported() { + if (!process.config.variables.node_use_lief) + common.skip('Node.js was not built with LIEF support.'); + skipIfSingleExecutableIsNotSupported(); +} + function skipIfSingleExecutableIsNotSupported() { if (!process.config.variables.single_executable_application) common.skip('Single Executable Application support has been disabled.'); @@ -73,6 +80,59 @@ function skipIfSingleExecutableIsNotSupported() { } } +function buildSEA(fixtureDir, options = {}) { + const { + workingDir = tmpdir.path, + configPath = 'sea-config.json', + verifyWorkflow = false, + failure, + } = options; + + // Copy fixture files to working directory if they are different. + if (fixtureDir !== workingDir) { + fs.cpSync(fixtureDir, workingDir, { recursive: true }); + } + + // Parse the config to get the output file path, if on Windows, ensure it ends with .exe + const config = JSON.parse(fs.readFileSync(path.resolve(workingDir, configPath))); + assert.strictEqual(typeof config.output, 'string'); + if (process.platform === 'win32') { + if (!config.output.endsWith('.exe')) { + config.output += '.exe'; + } + if (config.executable && !config.executable.endsWith('.exe')) { + config.executable += '.exe'; + } + fs.writeFileSync(path.resolve(workingDir, configPath), JSON.stringify(config, null, 2)); + } + + // Build the SEA. + const child = spawnSyncAndAssert(process.execPath, ['--build-sea', configPath], { + cwd: workingDir, + env: { + NODE_DEBUG_NATIVE: 'SEA', + ...process.env, + }, + }, failure === undefined ? { + status: 0, + signal: null, + } : { + stderr: failure, + status: 1, + }); + + if (failure !== undefined) { + // Log more information, otherwise it's hard to debug failures from CI. + console.log(child.stderr.toString()); + return child; + } + + const outputFile = path.resolve(workingDir, config.output); + assert(fs.existsSync(outputFile), `Expected SEA output file ${outputFile} to exist`); + signSEA(outputFile, verifyWorkflow); + return outputFile; +} + function generateSEA(fixtureDir, options = {}) { const { workingDir = tmpdir.path, @@ -181,6 +241,9 @@ function signSEA(targetExecutable, verifyWorkflow = false) { } module.exports = { + skipIfBuildSEAIsNotSupported, skipIfSingleExecutableIsNotSupported, generateSEA, + signSEA, + buildSEA, }; diff --git a/test/fixtures/sea/already-exists/sea-2.js b/test/fixtures/sea/already-exists/sea-2.js new file mode 100644 index 00000000000000..bb918f71ab2b74 --- /dev/null +++ b/test/fixtures/sea/already-exists/sea-2.js @@ -0,0 +1 @@ +console.log('Hello from SEA 2!'); diff --git a/test/fixtures/sea/already-exists/sea-config-2.json b/test/fixtures/sea/already-exists/sea-config-2.json new file mode 100644 index 00000000000000..3acaa61f27125e --- /dev/null +++ b/test/fixtures/sea/already-exists/sea-config-2.json @@ -0,0 +1,6 @@ +{ + "main": "sea-2.js", + "output": "sea-2", + "executable": "sea", + "disableExperimentalSEAWarning": true +} diff --git a/test/fixtures/sea/already-exists/sea-config.json b/test/fixtures/sea/already-exists/sea-config.json new file mode 100644 index 00000000000000..9042b70cdabd9f --- /dev/null +++ b/test/fixtures/sea/already-exists/sea-config.json @@ -0,0 +1,5 @@ +{ + "main": "sea.js", + "output": "sea", + "disableExperimentalSEAWarning": true +} diff --git a/test/fixtures/sea/already-exists/sea.js b/test/fixtures/sea/already-exists/sea.js new file mode 100644 index 00000000000000..bcafbc2d1d9f68 --- /dev/null +++ b/test/fixtures/sea/already-exists/sea.js @@ -0,0 +1 @@ +console.log('Hello from SEA!'); diff --git a/test/fixtures/sea/basic/requirable.js b/test/fixtures/sea/basic/requirable.js new file mode 100644 index 00000000000000..69f1fd8c3776c1 --- /dev/null +++ b/test/fixtures/sea/basic/requirable.js @@ -0,0 +1,3 @@ +module.exports = { + hello: 'world', +}; diff --git a/test/fixtures/sea/basic/sea-config.json b/test/fixtures/sea/basic/sea-config.json new file mode 100644 index 00000000000000..e1b7672e61fa4d --- /dev/null +++ b/test/fixtures/sea/basic/sea-config.json @@ -0,0 +1,6 @@ + +{ + "main": "sea.js", + "output": "sea", + "disableExperimentalSEAWarning": true +} diff --git a/test/fixtures/sea/basic/sea.js b/test/fixtures/sea/basic/sea.js new file mode 100644 index 00000000000000..65bb8d37e019a2 --- /dev/null +++ b/test/fixtures/sea/basic/sea.js @@ -0,0 +1,64 @@ +const { Module: { createRequire } } = require('module'); +const createdRequire = createRequire(__filename); + +// Although, require('../common') works locally, that couldn't be used here +// because we set NODE_TEST_DIR=/Users/iojs/node-tmp on Jenkins CI. +const { expectWarning, mustNotCall } = createdRequire(process.env.COMMON_DIRECTORY); + +const builtinWarning = +`Currently the require() provided to the main script embedded into single-executable applications only supports loading built-in modules. +To load a module from disk after the single executable application is launched, use require("module").createRequire(). +Support for bundled module loading or virtual file systems are under discussions in https://github.com/nodejs/single-executable`; + +// This additionally makes sure that no unexpected warnings are emitted. +if (!createdRequire('./sea-config.json').disableExperimentalSEAWarning) { + expectWarning('Warning', builtinWarning); // Triggered by require() calls below. + expectWarning('ExperimentalWarning', + 'Single executable application is an experimental feature and ' + + 'might change at any time'); + // Any unexpected warning would throw this error: + // https://github.com/nodejs/node/blob/c301404105a7256b79a0b8c4522ce47af96dfa17/test/common/index.js#L697-L700. +} + +// Should be possible to require core modules that optionally require the +// "node:" scheme. +const { deepStrictEqual, strictEqual, throws } = require('assert'); +const { dirname } = require('node:path'); + +// Checks that the source filename is used in the error stack trace. +strictEqual(new Error('lol').stack.split('\n')[1], ' at sea.js:29:13'); + +// Should be possible to require a core module that requires using the "node:" +// scheme. +{ + const { test } = require('node:test'); + strictEqual(typeof test, 'function'); +} + +// Should not be possible to require a core module without the "node:" scheme if +// it requires using the "node:" scheme. +throws(() => require('test'), { + code: 'ERR_UNKNOWN_BUILTIN_MODULE', +}); + +deepStrictEqual(process.argv, [process.execPath, process.execPath, '-a', '--b=c', 'd']); + +strictEqual(require.cache, undefined); +strictEqual(require.extensions, undefined); +strictEqual(require.main, module); +strictEqual(require.resolve, undefined); + +strictEqual(__filename, process.execPath); +strictEqual(__dirname, dirname(process.execPath)); +strictEqual(module.exports, exports); + +throws(() => require('./requirable.js'), { + code: 'ERR_UNKNOWN_BUILTIN_MODULE', +}); + +const requirable = createdRequire('./requirable.js'); +deepStrictEqual(requirable, { + hello: 'world', +}); + +console.log('Hello, world! 😊'); 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/fixtures/sea/executable-field/sea-config.json b/test/fixtures/sea/executable-field/sea-config.json new file mode 100644 index 00000000000000..e951d917d15a8b --- /dev/null +++ b/test/fixtures/sea/executable-field/sea-config.json @@ -0,0 +1,6 @@ +{ + "main": "sea.js", + "output": "sea", + "executable": "copy", + "disableExperimentalSEAWarning": true +} diff --git a/test/fixtures/sea/executable-field/sea.js b/test/fixtures/sea/executable-field/sea.js new file mode 100644 index 00000000000000..a638268d064b84 --- /dev/null +++ b/test/fixtures/sea/executable-field/sea.js @@ -0,0 +1,3 @@ +'use strict'; + +console.log('Hello from SEA with executable field!'); diff --git a/test/parallel/test-process-versions.js b/test/parallel/test-process-versions.js index a8f78a77660839..f5bff03632ab9d 100644 --- a/test/parallel/test-process-versions.js +++ b/test/parallel/test-process-versions.js @@ -28,6 +28,7 @@ const expected_keys = [ const hasUndici = process.config.variables.node_builtin_shareable_builtins.includes('deps/undici/undici.js'); const hasAmaro = process.config.variables.node_builtin_shareable_builtins.includes('deps/amaro/dist/index.js'); +const hasLief = process.config.variables.node_use_lief; if (process.config.variables.node_use_amaro) { if (hasAmaro) { @@ -38,6 +39,10 @@ if (hasUndici) { expected_keys.push('undici'); } +if (hasLief) { + expected_keys.push('lief'); +} + if (common.hasCrypto) { expected_keys.push('openssl'); expected_keys.push('ncrypto'); @@ -79,6 +84,10 @@ assert.match(process.versions.nbytes, commonTemplate); assert.match(process.versions.zlib, /^\d+(?:\.\d+){1,3}(?:-.*)?$/); assert.match(process.versions.zstd, commonTemplate); +if (process.config.variables.node_use_lief) { + assert.match(process.versions.lief, commonTemplate); +} + if (hasUndici) { assert.match(process.versions.undici, commonTemplate); } diff --git a/test/sea/sea.status b/test/sea/sea.status index 96d7827741fcc7..e0996ef14588c1 100644 --- a/test/sea/sea.status +++ b/test/sea/sea.status @@ -9,6 +9,7 @@ prefix sea [$system==macos && $arch==x64] # https://github.com/nodejs/node/issues/59553 test-single-executable-application*: SKIP +test-build-sea*: SKIP [$system==linux && $arch==ppc64] # https://github.com/nodejs/node/issues/59561 @@ -24,3 +25,7 @@ test-build-sea*: SKIP # https://github.com/nodejs/node/issues/49630 test-single-executable-application-snapshot: PASS, FLAKY test-single-executable-application-snapshot-and-code-cache: PASS, FLAKY + +[$system==solaris || $system==aix] +# Cannot compile LIEF on these platforms +test-build-sea*: SKIP diff --git a/test/sea/test-build-sea-already-exists.js b/test/sea/test-build-sea-already-exists.js new file mode 100644 index 00000000000000..9c5ee914861fa2 --- /dev/null +++ b/test/sea/test-build-sea-already-exists.js @@ -0,0 +1,20 @@ +// This tests that --build-sea fails when the output file already contains a SEA. +// TODO(joyeecheung): support an option that allows overwriting the existing content. +'use strict'; + +require('../common'); +const tmpdir = require('../common/tmpdir'); +const { buildSEA, skipIfBuildSEAIsNotSupported } = require('../common/sea'); +const fixtures = require('../common/fixtures'); +skipIfBuildSEAIsNotSupported(); + +tmpdir.refresh(); + +const fixtureDir = fixtures.path('sea', 'already-exists'); + +// First, build a valid SEA. +buildSEA(fixtureDir); +buildSEA(fixtureDir, { + configPath: 'sea-config-2.json', + failure: /already exists/, +}); diff --git a/test/sea/test-build-sea-config-not-found.js b/test/sea/test-build-sea-config-not-found.js new file mode 100644 index 00000000000000..05cba9cf709164 --- /dev/null +++ b/test/sea/test-build-sea-config-not-found.js @@ -0,0 +1,38 @@ +// This tests --build-sea when the config file doesn't exist. + +'use strict'; + +require('../common'); +const tmpdir = require('../common/tmpdir'); +const { skipIfBuildSEAIsNotSupported } = require('../common/sea'); +const { spawnSyncAndAssert } = require('../common/child_process'); + +skipIfBuildSEAIsNotSupported(); + +// Test: Config file doesn't exist (relative path) +{ + tmpdir.refresh(); + const config = 'non-existent-relative.json'; + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /Cannot read single executable configuration from non-existent-relative\.json/, + }); +} + +// Test: Config file doesn't exist (absolute path) +{ + tmpdir.refresh(); + const config = tmpdir.resolve('non-existent-absolute.json'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /Cannot read single executable configuration from .*non-existent-absolute\.json/, + }); +} diff --git a/test/sea/test-build-sea-custom-argv0.js b/test/sea/test-build-sea-custom-argv0.js new file mode 100644 index 00000000000000..2c8b6b83e02cdb --- /dev/null +++ b/test/sea/test-build-sea-custom-argv0.js @@ -0,0 +1,34 @@ +'use strict'; +// This tests --build-sea with a custom argv0 value. + +require('../common'); + +const { skipIfBuildSEAIsNotSupported } = require('../common/sea'); + +skipIfBuildSEAIsNotSupported(); + +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); +const { copyFileSync } = require('fs'); + +const { spawnSyncAndAssert } = require('../common/child_process'); +tmpdir.refresh(); + +copyFileSync( + fixtures.path('sea', 'basic', 'sea-config.json'), + tmpdir.resolve('sea-config.json'), +); + +copyFileSync( + fixtures.path('sea', 'basic', 'sea.js'), + tmpdir.resolve('sea.js'), +); + +spawnSyncAndAssert( + process.execPath, + ['--build-sea', tmpdir.resolve('sea-config.json')], { + cwd: tmpdir.path, + argv0: 'argv0', + }, { + stdout: /Generated single executable/, + }); diff --git a/test/sea/test-build-sea-executable-field.js b/test/sea/test-build-sea-executable-field.js new file mode 100644 index 00000000000000..279998b1fe5b85 --- /dev/null +++ b/test/sea/test-build-sea-executable-field.js @@ -0,0 +1,47 @@ +// This tests --build-sea with the "executable" field pointing to a copied Node.js binary. + +'use strict'; + +require('../common'); +const { buildSEA, skipIfBuildSEAIsNotSupported } = require('../common/sea'); +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); +const { spawnSyncAndAssert } = require('../common/child_process'); +const fs = require('fs'); + +skipIfBuildSEAIsNotSupported(); + +tmpdir.refresh(); + +// Copy fixture files to working directory. +const fixtureDir = fixtures.path('sea', 'executable-field'); +fs.cpSync(fixtureDir, tmpdir.path, { recursive: true }); + +// Copy the Node.js executable to the working directory. +const executableName = process.platform === 'win32' ? 'copy.exe' : 'copy'; +fs.copyFileSync(process.execPath, tmpdir.resolve(executableName)); + +// Update the config to use the correct executable name and output extension. +const configPath = tmpdir.resolve('sea-config.json'); +const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); +config.executable = executableName; +if (process.platform === 'win32') { + if (!config.output.endsWith('.exe')) { + config.output += '.exe'; + } +} +fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + +// Build the SEA. +const outputFile = buildSEA(tmpdir.path, { workingDir: tmpdir.path }); + +spawnSyncAndAssert( + outputFile, + [], + { + cwd: tmpdir.path, + }, + { + stdout: 'Hello from SEA with executable field!\n', + }, +); diff --git a/test/sea/test-build-sea-invalid-assets.js b/test/sea/test-build-sea-invalid-assets.js new file mode 100644 index 00000000000000..ba9abec4323582 --- /dev/null +++ b/test/sea/test-build-sea-invalid-assets.js @@ -0,0 +1,77 @@ +// This tests --build-sea when the "assets" field is invalid. + +'use strict'; + +require('../common'); +const tmpdir = require('../common/tmpdir'); +const { skipIfBuildSEAIsNotSupported } = require('../common/sea'); +const { writeFileSync } = require('fs'); +const { spawnSyncAndAssert } = require('../common/child_process'); + +skipIfBuildSEAIsNotSupported(); + +// Test: Invalid "assets" type (should be object) +{ + tmpdir.refresh(); + const config = tmpdir.resolve('invalid-assets-type.json'); + writeFileSync(config, ` +{ + "main": "bundle.js", + "output": "sea", + "assets": ["a.txt", "b.txt"] +} + `, 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /"assets" field of .*invalid-assets-type\.json is not a map of strings/, + }); +} + +// Test: Invalid asset value type (should be string) +{ + tmpdir.refresh(); + const config = tmpdir.resolve('invalid-asset-value.json'); + writeFileSync(config, ` +{ + "main": "bundle.js", + "output": "sea", + "assets": {"key": 123} +} + `, 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /"assets" field of .*invalid-asset-value\.json is not a map of strings/, + }); +} + +// Test: Non-existent asset file +{ + tmpdir.refresh(); + const config = tmpdir.resolve('nonexistent-asset.json'); + const main = tmpdir.resolve('bundle.js'); + writeFileSync(main, 'console.log("hello")', 'utf-8'); + const configJson = JSON.stringify({ + main, + output: 'sea', + assets: { + 'missing': 'nonexistent-asset.txt', + }, + }); + writeFileSync(config, configJson, 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /Cannot read asset.*nonexistent-asset\.txt/, + }); +} diff --git a/test/sea/test-build-sea-invalid-boolean-fields.js b/test/sea/test-build-sea-invalid-boolean-fields.js new file mode 100644 index 00000000000000..1bf7c693b8a164 --- /dev/null +++ b/test/sea/test-build-sea-invalid-boolean-fields.js @@ -0,0 +1,74 @@ +// This tests --build-sea when boolean fields have invalid types. + +'use strict'; + +require('../common'); +const tmpdir = require('../common/tmpdir'); +const { skipIfBuildSEAIsNotSupported } = require('../common/sea'); +const { writeFileSync } = require('fs'); +const { spawnSyncAndAssert } = require('../common/child_process'); + +skipIfBuildSEAIsNotSupported(); + +// Test: Invalid "disableExperimentalSEAWarning" type (should be Boolean) +{ + tmpdir.refresh(); + const config = tmpdir.resolve('invalid-disableExperimentalSEAWarning.json'); + writeFileSync(config, ` +{ + "main": "bundle.js", + "output": "sea", + "disableExperimentalSEAWarning": "true" +} + `, 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /"disableExperimentalSEAWarning" field of .*invalid-disableExperimentalSEAWarning\.json is not a Boolean/, + }); +} + +// Test: Invalid "useSnapshot" type (should be Boolean) +{ + tmpdir.refresh(); + const config = tmpdir.resolve('invalid-useSnapshot.json'); + writeFileSync(config, ` +{ + "main": "bundle.js", + "output": "sea", + "useSnapshot": "false" +} + `, 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /"useSnapshot" field of .*invalid-useSnapshot\.json is not a Boolean/, + }); +} + +// Test: Invalid "useCodeCache" type (should be Boolean) +{ + tmpdir.refresh(); + const config = tmpdir.resolve('invalid-useCodeCache.json'); + writeFileSync(config, ` +{ + "main": "bundle.js", + "output": "sea", + "useCodeCache": 1 +} + `, 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /"useCodeCache" field of .*invalid-useCodeCache\.json is not a Boolean/, + }); +} diff --git a/test/sea/test-build-sea-invalid-exec-argv.js b/test/sea/test-build-sea-invalid-exec-argv.js new file mode 100644 index 00000000000000..902a72f56310ab --- /dev/null +++ b/test/sea/test-build-sea-invalid-exec-argv.js @@ -0,0 +1,95 @@ +// This tests --build-sea when the "execArgv" or "execArgvExtension" fields are invalid. + +'use strict'; + +require('../common'); +const tmpdir = require('../common/tmpdir'); +const { skipIfBuildSEAIsNotSupported } = require('../common/sea'); +const { writeFileSync } = require('fs'); +const { spawnSyncAndAssert } = require('../common/child_process'); + +skipIfBuildSEAIsNotSupported(); + +// Test: Invalid "execArgv" type (should be array) +{ + tmpdir.refresh(); + const config = tmpdir.resolve('invalid-execArgv-type.json'); + writeFileSync(config, ` +{ + "main": "bundle.js", + "output": "sea", + "execArgv": "--no-warnings" +} + `, 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /"execArgv" field of .*invalid-execArgv-type\.json is not an array of strings/, + }); +} + +// Test: Invalid execArgv element type (should be string) +{ + tmpdir.refresh(); + const config = tmpdir.resolve('invalid-execArgv-element.json'); + writeFileSync(config, ` +{ + "main": "bundle.js", + "output": "sea", + "execArgv": ["--no-warnings", 123] +} + `, 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /"execArgv" field of .*invalid-execArgv-element\.json is not an array of strings/, + }); +} + +// Test: Invalid "execArgvExtension" type (should be string) +{ + tmpdir.refresh(); + const config = tmpdir.resolve('invalid-execArgvExtension-type.json'); + writeFileSync(config, ` +{ + "main": "bundle.js", + "output": "sea", + "execArgvExtension": true +} + `, 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /"execArgvExtension" field of .*invalid-execArgvExtension-type\.json is not a string/, + }); +} + +// Test: Invalid "execArgvExtension" value (must be "none", "env", or "cli") +{ + tmpdir.refresh(); + const config = tmpdir.resolve('invalid-execArgvExtension-value.json'); + writeFileSync(config, ` +{ + "main": "bundle.js", + "output": "sea", + "execArgvExtension": "invalid" +} + `, 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /"execArgvExtension" field of .*invalid-execArgvExtension-value\.json must be one of "none", "env", or "cli"/, + }); +} diff --git a/test/sea/test-build-sea-invalid-executable.js b/test/sea/test-build-sea-invalid-executable.js new file mode 100644 index 00000000000000..e94a6737aa4bec --- /dev/null +++ b/test/sea/test-build-sea-invalid-executable.js @@ -0,0 +1,99 @@ +// This tests --build-sea when the "executable" field is invalid. + +'use strict'; + +require('../common'); +const tmpdir = require('../common/tmpdir'); +const { skipIfBuildSEAIsNotSupported } = require('../common/sea'); +const { writeFileSync } = require('fs'); +const { spawnSyncAndAssert } = require('../common/child_process'); + +skipIfBuildSEAIsNotSupported(); + +// Test: Invalid "executable" type (should be string) +{ + tmpdir.refresh(); + const config = tmpdir.resolve('invalid-executable-type.json'); + writeFileSync(config, ` +{ + "main": "bundle.js", + "output": "sea", + "executable": 123 +} + `, 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /"executable" field of .*invalid-executable-type\.json is not a non-empty string/, + }); +} + +// Test: "executable" field is empty string +{ + tmpdir.refresh(); + const config = tmpdir.resolve('empty-executable.json'); + writeFileSync(config, ` +{ + "main": "bundle.js", + "output": "sea", + "executable": "" +} + `, 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /"executable" field of .*empty-executable\.json is not a non-empty string/, + }); +} + +// Test: Non-existent executable path +{ + tmpdir.refresh(); + const config = tmpdir.resolve('nonexistent-executable.json'); + const main = tmpdir.resolve('bundle.js'); + writeFileSync(main, 'console.log("hello")', 'utf-8'); + const configJson = JSON.stringify({ + main, + output: 'sea', + executable: '/nonexistent/path/to/node', + }); + writeFileSync(config, configJson, 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /Couldn't stat executable.*\/nonexistent\/path\/to\/node: no such file or directory/, + }); +} + +// Test: Executable is not a valid binary format (text file) +{ + tmpdir.refresh(); + const config = tmpdir.resolve('invalid-executable-format.json'); + const main = tmpdir.resolve('bundle.js'); + const invalidExe = tmpdir.resolve('invalid-exe.txt'); + writeFileSync(main, 'console.log("hello")', 'utf-8'); + writeFileSync(invalidExe, 'this is not a valid executable', 'utf-8'); + const configJson = JSON.stringify({ + main, + output: 'sea', + executable: invalidExe, + }); + writeFileSync(config, configJson, 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /Executable must be a supported format: ELF, PE, or Mach-O/, + }); +} diff --git a/test/sea/test-build-sea-invalid-json.js b/test/sea/test-build-sea-invalid-json.js new file mode 100644 index 00000000000000..e4703b2392ee82 --- /dev/null +++ b/test/sea/test-build-sea-invalid-json.js @@ -0,0 +1,23 @@ +// This tests --build-sea when the config file contains invalid JSON. + +'use strict'; + +require('../common'); +const tmpdir = require('../common/tmpdir'); +const { skipIfBuildSEAIsNotSupported } = require('../common/sea'); +const { writeFileSync } = require('fs'); +const { spawnSyncAndAssert } = require('../common/child_process'); + +skipIfBuildSEAIsNotSupported(); + +tmpdir.refresh(); +const config = tmpdir.resolve('invalid.json'); +writeFileSync(config, '\n{\n"main"', 'utf8'); +spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /INCOMPLETE_ARRAY_OR_OBJECT/, + }); diff --git a/test/sea/test-build-sea-missing-main.js b/test/sea/test-build-sea-missing-main.js new file mode 100644 index 00000000000000..bc93f54a19a899 --- /dev/null +++ b/test/sea/test-build-sea-missing-main.js @@ -0,0 +1,91 @@ +// This tests --build-sea when the "main" field is missing or invalid. + +'use strict'; + +require('../common'); +const tmpdir = require('../common/tmpdir'); +const { skipIfBuildSEAIsNotSupported } = require('../common/sea'); +const { writeFileSync } = require('fs'); +const { spawnSyncAndAssert } = require('../common/child_process'); + +skipIfBuildSEAIsNotSupported(); + +// Test: Empty config (missing main and output) +{ + tmpdir.refresh(); + const config = tmpdir.resolve('empty.json'); + writeFileSync(config, '{}', 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /"main" field of .*empty\.json is not a non-empty string/, + }); +} + +// Test: Missing "main" field +{ + tmpdir.refresh(); + const config = tmpdir.resolve('no-main.json'); + writeFileSync(config, '{"output": "sea"}', 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /"main" field of .*no-main\.json is not a non-empty string/, + }); +} + +// Test: "main" field is empty string +{ + tmpdir.refresh(); + const config = tmpdir.resolve('empty-main.json'); + writeFileSync(config, '{"main": "", "output": "sea"}', 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /"main" field of .*empty-main\.json is not a non-empty string/, + }); +} + +// Test: Non-existent main script (relative path) +{ + tmpdir.refresh(); + const config = tmpdir.resolve('nonexistent-main-relative.json'); + writeFileSync(config, '{"main": "bundle.js", "output": "sea"}', 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /Cannot read main script .*bundle\.js/, + }); +} + +// Test: Non-existent main script (absolute path) +{ + tmpdir.refresh(); + const config = tmpdir.resolve('nonexistent-main-absolute.json'); + const main = tmpdir.resolve('bundle.js'); + const configJson = JSON.stringify({ + main, + output: 'sea', + }); + writeFileSync(config, configJson, 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /Cannot read main script .*bundle\.js/, + }); +} diff --git a/test/sea/test-build-sea-missing-output.js b/test/sea/test-build-sea-missing-output.js new file mode 100644 index 00000000000000..6568c5a2109f58 --- /dev/null +++ b/test/sea/test-build-sea-missing-output.js @@ -0,0 +1,64 @@ +// This tests --build-sea when the "output" field is missing or invalid. + +'use strict'; + +require('../common'); +const tmpdir = require('../common/tmpdir'); +const { skipIfBuildSEAIsNotSupported } = require('../common/sea'); +const { writeFileSync, mkdirSync } = require('fs'); +const { spawnSyncAndAssert } = require('../common/child_process'); + +skipIfBuildSEAIsNotSupported(); + +// Test: Missing "output" field +{ + tmpdir.refresh(); + const config = tmpdir.resolve('no-output.json'); + writeFileSync(config, '{"main": "bundle.js"}', 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /"output" field of .*no-output\.json is not a non-empty string/, + }); +} + +// Test: "output" field is empty string +{ + tmpdir.refresh(); + const config = tmpdir.resolve('empty-output.json'); + writeFileSync(config, '{"main": "bundle.js", "output": ""}', 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /"output" field of .*empty-output\.json is not a non-empty string/, + }); +} + +// Test: Output path is a directory +{ + tmpdir.refresh(); + const config = tmpdir.resolve('output-is-dir.json'); + const main = tmpdir.resolve('bundle.js'); + const output = tmpdir.resolve('output-dir'); + mkdirSync(output); + writeFileSync(main, 'console.log("hello")', 'utf-8'); + const configJson = JSON.stringify({ + main, + output, + }); + writeFileSync(config, configJson, 'utf8'); + spawnSyncAndAssert( + process.execPath, + ['--build-sea', config], { + cwd: tmpdir.path, + }, { + status: 1, + stderr: /Couldn't write output executable.*output-dir/, + }); +} diff --git a/test/sea/test-build-sea.js b/test/sea/test-build-sea.js new file mode 100644 index 00000000000000..fb9004284bd003 --- /dev/null +++ b/test/sea/test-build-sea.js @@ -0,0 +1,33 @@ +'use strict'; +// This tests building SEA using --build-sea. + +require('../common'); + +const { + buildSEA, + skipIfBuildSEAIsNotSupported, +} = require('../common/sea'); + +skipIfBuildSEAIsNotSupported(); + +const { join } = require('path'); +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); + +const { spawnSyncAndAssert } = require('../common/child_process'); +tmpdir.refresh(); +const outputFile = buildSEA(fixtures.path('sea', 'basic')); + +spawnSyncAndAssert( + outputFile, + [ '-a', '--b=c', 'd' ], + { + env: { + COMMON_DIRECTORY: join(__dirname, '..', 'common'), + NODE_DEBUG_NATIVE: 'SEA', + ...process.env, + }, + }, + { + stdout: 'Hello, world! 😊\n', + }); diff --git a/test/sea/test-single-executable-application-asset-keys-empty.js b/test/sea/test-single-executable-application-asset-keys-empty.js index 5d7a2df48badcd..5cb06e366de075 100644 --- a/test/sea/test-single-executable-application-asset-keys-empty.js +++ b/test/sea/test-single-executable-application-asset-keys-empty.js @@ -6,11 +6,11 @@ require('../common'); const { - generateSEA, - skipIfSingleExecutableIsNotSupported, + buildSEA, + skipIfBuildSEAIsNotSupported, } = require('../common/sea'); -skipIfSingleExecutableIsNotSupported(); +skipIfBuildSEAIsNotSupported(); const tmpdir = require('../common/tmpdir'); @@ -21,7 +21,7 @@ const fixtures = require('../common/fixtures'); tmpdir.refresh(); -const outputFile = generateSEA(fixtures.path('sea', 'asset-keys-empty')); +const outputFile = buildSEA(fixtures.path('sea', 'asset-keys-empty')); spawnSyncAndAssert( outputFile, diff --git a/test/sea/test-single-executable-application-asset-keys.js b/test/sea/test-single-executable-application-asset-keys.js index ce30b7ff252048..dd899a3fcc98ef 100644 --- a/test/sea/test-single-executable-application-asset-keys.js +++ b/test/sea/test-single-executable-application-asset-keys.js @@ -6,11 +6,11 @@ require('../common'); const { - generateSEA, - skipIfSingleExecutableIsNotSupported, + buildSEA, + skipIfBuildSEAIsNotSupported, } = require('../common/sea'); -skipIfSingleExecutableIsNotSupported(); +skipIfBuildSEAIsNotSupported(); const tmpdir = require('../common/tmpdir'); @@ -21,7 +21,7 @@ const fixtures = require('../common/fixtures'); tmpdir.refresh(); -const outputFile = generateSEA(fixtures.path('sea', 'asset-keys')); +const outputFile = buildSEA(fixtures.path('sea', 'asset-keys')); spawnSyncAndAssert( outputFile, diff --git a/test/sea/test-single-executable-application-assets-raw.js b/test/sea/test-single-executable-application-assets-raw.js index 76a73745098fdd..80ad4f38fbf6ff 100644 --- a/test/sea/test-single-executable-application-assets-raw.js +++ b/test/sea/test-single-executable-application-assets-raw.js @@ -2,11 +2,11 @@ require('../common'); const { - generateSEA, - skipIfSingleExecutableIsNotSupported, + buildSEA, + skipIfBuildSEAIsNotSupported, } = require('../common/sea'); -skipIfSingleExecutableIsNotSupported(); +skipIfBuildSEAIsNotSupported(); // This tests the snapshot support in single executable applications. const tmpdir = require('../common/tmpdir'); @@ -17,7 +17,7 @@ const { const fixtures = require('../common/fixtures'); tmpdir.refresh(); -const outputFile = generateSEA(fixtures.path('sea', 'assets-raw')); +const outputFile = buildSEA(fixtures.path('sea', 'assets-raw')); spawnSyncAndExitWithoutError( outputFile, diff --git a/test/sea/test-single-executable-application-assets.js b/test/sea/test-single-executable-application-assets.js index db1492be015794..23b8369fc785c2 100644 --- a/test/sea/test-single-executable-application-assets.js +++ b/test/sea/test-single-executable-application-assets.js @@ -4,11 +4,11 @@ require('../common'); const { - generateSEA, - skipIfSingleExecutableIsNotSupported, + buildSEA, + skipIfBuildSEAIsNotSupported, } = require('../common/sea'); -skipIfSingleExecutableIsNotSupported(); +skipIfBuildSEAIsNotSupported(); const tmpdir = require('../common/tmpdir'); const { @@ -17,7 +17,7 @@ const { const fixtures = require('../common/fixtures'); tmpdir.refresh(); -const outputFile = generateSEA(fixtures.path('sea', 'assets')); +const outputFile = buildSEA(fixtures.path('sea', 'assets')); spawnSyncAndAssert( outputFile, diff --git a/test/sea/test-single-executable-application-disable-experimental-sea-warning.js b/test/sea/test-single-executable-application-disable-experimental-sea-warning.js index d0cfe934d30d15..3528d8d459c17c 100644 --- a/test/sea/test-single-executable-application-disable-experimental-sea-warning.js +++ b/test/sea/test-single-executable-application-disable-experimental-sea-warning.js @@ -3,11 +3,11 @@ require('../common'); const { - generateSEA, - skipIfSingleExecutableIsNotSupported, + buildSEA, + skipIfBuildSEAIsNotSupported, } = require('../common/sea'); -skipIfSingleExecutableIsNotSupported(); +skipIfBuildSEAIsNotSupported(); // This tests the creation of a single executable application which has the // experimental SEA warning disabled. @@ -19,7 +19,7 @@ const fixtures = require('../common/fixtures'); tmpdir.refresh(); -const outputFile = generateSEA(fixtures.path('sea', 'disable-experimental-warning')); +const outputFile = buildSEA(fixtures.path('sea', 'disable-experimental-warning')); spawnSyncAndAssert( outputFile, diff --git a/test/sea/test-single-executable-application-empty.js b/test/sea/test-single-executable-application-empty.js index 0f31dab4a35fa0..c0b8e6db3e260d 100644 --- a/test/sea/test-single-executable-application-empty.js +++ b/test/sea/test-single-executable-application-empty.js @@ -3,11 +3,11 @@ const common = require('../common'); const { - generateSEA, - skipIfSingleExecutableIsNotSupported, + buildSEA, + skipIfBuildSEAIsNotSupported, } = require('../common/sea'); -skipIfSingleExecutableIsNotSupported(); +skipIfBuildSEAIsNotSupported(); // This tests the creation of a single executable application with an empty // script. @@ -20,7 +20,7 @@ tmpdir.refresh(); let outputFile; try { - outputFile = generateSEA(fixtures.path('sea', 'empty'), { + outputFile = buildSEA(fixtures.path('sea', 'empty'), { verifyWorkflow: true, }); } catch (e) { 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/, + }); diff --git a/test/sea/test-single-executable-application-exec-argv-empty.js b/test/sea/test-single-executable-application-exec-argv-empty.js index e02f8ee6c8978e..19d6e1a916586f 100644 --- a/test/sea/test-single-executable-application-exec-argv-empty.js +++ b/test/sea/test-single-executable-application-exec-argv-empty.js @@ -3,11 +3,11 @@ require('../common'); const { - generateSEA, - skipIfSingleExecutableIsNotSupported, + buildSEA, + skipIfBuildSEAIsNotSupported, } = require('../common/sea'); -skipIfSingleExecutableIsNotSupported(); +skipIfBuildSEAIsNotSupported(); // This tests the execArgv functionality with empty array in single executable applications. @@ -18,7 +18,7 @@ const fixtures = require('../common/fixtures'); tmpdir.refresh(); -const outputFile = generateSEA(fixtures.path('sea', 'exec-argv-empty')); +const outputFile = buildSEA(fixtures.path('sea', 'exec-argv-empty')); // Test that empty execArgv work correctly spawnSyncAndAssert( diff --git a/test/sea/test-single-executable-application-exec-argv-extension-cli.js b/test/sea/test-single-executable-application-exec-argv-extension-cli.js index 738a2bc98b6c30..3999cf0cbaecf6 100644 --- a/test/sea/test-single-executable-application-exec-argv-extension-cli.js +++ b/test/sea/test-single-executable-application-exec-argv-extension-cli.js @@ -3,11 +3,11 @@ require('../common'); const { - generateSEA, - skipIfSingleExecutableIsNotSupported, + buildSEA, + skipIfBuildSEAIsNotSupported, } = require('../common/sea'); -skipIfSingleExecutableIsNotSupported(); +skipIfBuildSEAIsNotSupported(); // This tests the execArgvExtension "cli" mode in single executable applications. @@ -18,7 +18,7 @@ const fixtures = require('../common/fixtures'); tmpdir.refresh(); -const outputFile = generateSEA(fixtures.path('sea', 'exec-argv-extension-cli')); +const outputFile = buildSEA(fixtures.path('sea', 'exec-argv-extension-cli')); // Test that --node-options works with execArgvExtension: "cli" spawnSyncAndAssert( diff --git a/test/sea/test-single-executable-application-exec-argv-extension-env.js b/test/sea/test-single-executable-application-exec-argv-extension-env.js index 6d01c50d5c010e..0dc8a0415e0391 100644 --- a/test/sea/test-single-executable-application-exec-argv-extension-env.js +++ b/test/sea/test-single-executable-application-exec-argv-extension-env.js @@ -3,11 +3,11 @@ require('../common'); const { - generateSEA, - skipIfSingleExecutableIsNotSupported, + buildSEA, + skipIfBuildSEAIsNotSupported, } = require('../common/sea'); -skipIfSingleExecutableIsNotSupported(); +skipIfBuildSEAIsNotSupported(); // This tests the execArgvExtension "env" mode (default) in single executable applications. @@ -19,7 +19,7 @@ const fixtures = require('../common/fixtures'); tmpdir.refresh(); -const outputFile = generateSEA(fixtures.path('sea', 'exec-argv-extension-env')); +const outputFile = buildSEA(fixtures.path('sea', 'exec-argv-extension-env')); // Test that NODE_OPTIONS works with execArgvExtension: "env" (default behavior) spawnSyncAndAssert( diff --git a/test/sea/test-single-executable-application-exec-argv-extension-none.js b/test/sea/test-single-executable-application-exec-argv-extension-none.js index 1b96e3ac8d7381..bb1199dc56d3fb 100644 --- a/test/sea/test-single-executable-application-exec-argv-extension-none.js +++ b/test/sea/test-single-executable-application-exec-argv-extension-none.js @@ -3,11 +3,11 @@ require('../common'); const { - generateSEA, - skipIfSingleExecutableIsNotSupported, + buildSEA, + skipIfBuildSEAIsNotSupported, } = require('../common/sea'); -skipIfSingleExecutableIsNotSupported(); +skipIfBuildSEAIsNotSupported(); // This tests the execArgvExtension "none" mode in single executable applications. @@ -18,7 +18,7 @@ const fixtures = require('../common/fixtures'); tmpdir.refresh(); -const outputFile = generateSEA(fixtures.path('sea', 'exec-argv-extension-none')); +const outputFile = buildSEA(fixtures.path('sea', 'exec-argv-extension-none')); // Test that NODE_OPTIONS is ignored with execArgvExtension: "none" spawnSyncAndAssert( diff --git a/test/sea/test-single-executable-application-exec-argv.js b/test/sea/test-single-executable-application-exec-argv.js index 0d90a033633f8b..0a558ecf973984 100644 --- a/test/sea/test-single-executable-application-exec-argv.js +++ b/test/sea/test-single-executable-application-exec-argv.js @@ -3,11 +3,11 @@ require('../common'); const { - generateSEA, - skipIfSingleExecutableIsNotSupported, + buildSEA, + skipIfBuildSEAIsNotSupported, } = require('../common/sea'); -skipIfSingleExecutableIsNotSupported(); +skipIfBuildSEAIsNotSupported(); // This tests the execArgv functionality with multiple arguments in single executable applications. @@ -19,7 +19,7 @@ const assert = require('assert'); tmpdir.refresh(); -const outputFile = generateSEA(fixtures.path('sea', 'exec-argv')); +const outputFile = buildSEA(fixtures.path('sea', 'exec-argv')); // Test that multiple execArgv are properly applied spawnSyncAndAssert( diff --git a/test/sea/test-single-executable-application-inspect-in-sea-flags.js b/test/sea/test-single-executable-application-inspect-in-sea-flags.js index 37cc969f9b286c..46f438f5055c59 100644 --- a/test/sea/test-single-executable-application-inspect-in-sea-flags.js +++ b/test/sea/test-single-executable-application-inspect-in-sea-flags.js @@ -11,15 +11,15 @@ const tmpdir = require('../common/tmpdir'); const fixtures = require('../common/fixtures'); const { - generateSEA, - skipIfSingleExecutableIsNotSupported, + buildSEA, + skipIfBuildSEAIsNotSupported, } = require('../common/sea'); -skipIfSingleExecutableIsNotSupported(); +skipIfBuildSEAIsNotSupported(); tmpdir.refresh(); -const outputFile = generateSEA(fixtures.path('sea', 'inspect-in-sea-flags')); +const outputFile = buildSEA(fixtures.path('sea', 'inspect-in-sea-flags')); // Spawn the SEA with inspect option spawnSyncAndAssert( diff --git a/test/sea/test-single-executable-application-inspect.js b/test/sea/test-single-executable-application-inspect.js index d76d87c3e51be4..4109b649b57ad0 100644 --- a/test/sea/test-single-executable-application-inspect.js +++ b/test/sea/test-single-executable-application-inspect.js @@ -10,16 +10,16 @@ const tmpdir = require('../common/tmpdir'); const fixtures = require('../common/fixtures'); const { - generateSEA, - skipIfSingleExecutableIsNotSupported, + buildSEA, + skipIfBuildSEAIsNotSupported, } = require('../common/sea'); -skipIfSingleExecutableIsNotSupported(); +skipIfBuildSEAIsNotSupported(); common.skipIfInspectorDisabled(); tmpdir.refresh(); -const outputFile = generateSEA(fixtures.path('sea', 'inspect')); +const outputFile = buildSEA(fixtures.path('sea', 'inspect')); // Spawn the SEA with inspect option const seaProcess = spawn(outputFile, [], { diff --git a/test/sea/test-single-executable-application-snapshot-and-code-cache.js b/test/sea/test-single-executable-application-snapshot-and-code-cache.js index 2ce4c2301eb28f..2acfcc21386912 100644 --- a/test/sea/test-single-executable-application-snapshot-and-code-cache.js +++ b/test/sea/test-single-executable-application-snapshot-and-code-cache.js @@ -3,11 +3,11 @@ require('../common'); const { - generateSEA, - skipIfSingleExecutableIsNotSupported, + buildSEA, + skipIfBuildSEAIsNotSupported, } = require('../common/sea'); -skipIfSingleExecutableIsNotSupported(); +skipIfBuildSEAIsNotSupported(); // This tests "useCodeCache" is ignored when "useSnapshot" is true. @@ -20,7 +20,7 @@ const fixtures = require('../common/fixtures'); { tmpdir.refresh(); - const outputFile = generateSEA(fixtures.path('sea', 'snapshot-and-code-cache')); + const outputFile = buildSEA(fixtures.path('sea', 'snapshot-and-code-cache')); spawnSyncAndAssert( outputFile, diff --git a/test/sea/test-single-executable-application-snapshot-worker.js b/test/sea/test-single-executable-application-snapshot-worker.js index 42242abf3a78a6..b3cd24df02275a 100644 --- a/test/sea/test-single-executable-application-snapshot-worker.js +++ b/test/sea/test-single-executable-application-snapshot-worker.js @@ -3,11 +3,11 @@ require('../common'); const { - generateSEA, - skipIfSingleExecutableIsNotSupported, + buildSEA, + skipIfBuildSEAIsNotSupported, } = require('../common/sea'); -skipIfSingleExecutableIsNotSupported(); +skipIfBuildSEAIsNotSupported(); // This tests the snapshot support in single executable applications. @@ -20,7 +20,7 @@ const fixtures = require('../common/fixtures'); { tmpdir.refresh(); - const outputFile = generateSEA(fixtures.path('sea', 'snapshot-worker')); + const outputFile = buildSEA(fixtures.path('sea', 'snapshot-worker')); spawnSyncAndAssert( outputFile, diff --git a/test/sea/test-single-executable-application-snapshot.js b/test/sea/test-single-executable-application-snapshot.js index f6e5211386017f..14f4786bce1be0 100644 --- a/test/sea/test-single-executable-application-snapshot.js +++ b/test/sea/test-single-executable-application-snapshot.js @@ -3,11 +3,11 @@ require('../common'); const { - generateSEA, - skipIfSingleExecutableIsNotSupported, + buildSEA, + skipIfBuildSEAIsNotSupported, } = require('../common/sea'); -skipIfSingleExecutableIsNotSupported(); +skipIfBuildSEAIsNotSupported(); // This tests the snapshot support in single executable applications. @@ -48,7 +48,7 @@ const assert = require('assert'); { tmpdir.refresh(); - const outputFile = generateSEA(fixtures.path('sea', 'snapshot')); + const outputFile = buildSEA(fixtures.path('sea', 'snapshot')); spawnSyncAndAssert( outputFile, diff --git a/test/sea/test-single-executable-application-use-code-cache.js b/test/sea/test-single-executable-application-use-code-cache.js index fb857986a49432..181ac8c02c82c7 100644 --- a/test/sea/test-single-executable-application-use-code-cache.js +++ b/test/sea/test-single-executable-application-use-code-cache.js @@ -3,11 +3,11 @@ require('../common'); const { - generateSEA, - skipIfSingleExecutableIsNotSupported, + buildSEA, + skipIfBuildSEAIsNotSupported, } = require('../common/sea'); -skipIfSingleExecutableIsNotSupported(); +skipIfBuildSEAIsNotSupported(); // This tests the creation of a single executable application which uses the // V8 code cache. @@ -19,7 +19,7 @@ const fixtures = require('../common/fixtures'); tmpdir.refresh(); -const outputFile = generateSEA(fixtures.path('sea', 'use-code-cache')); +const outputFile = buildSEA(fixtures.path('sea', 'use-code-cache')); spawnSyncAndAssert( outputFile,