├── .eslintrc ├── .evergreen.yml ├── .evergreen ├── install-node.sh └── use-node.sh ├── .github └── workflows │ ├── codeql.yml │ └── nodejs.yml ├── .gitignore ├── .npmignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── bin └── boxednode.js ├── package.json ├── resources ├── add-node.h ├── add-node_api.h ├── entry-point-trampoline.js └── main-template.cc ├── src ├── executable-metadata.ts ├── helpers.ts ├── index.ts ├── logger.ts └── native-addons.ts ├── test ├── compile-main-template-only.sh ├── index.ts └── resources │ ├── .gitignore │ ├── example.js │ └── snapshot-echo-args.js └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "semistandard", 4 | "plugin:promise/recommended", 5 | "plugin:@typescript-eslint/recommended" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.evergreen.yml: -------------------------------------------------------------------------------- 1 | exec_timeout_secs: 10800 2 | 3 | functions: 4 | checkout: 5 | - command: git.get_project 6 | params: 7 | directory: src 8 | install_node: 9 | - command: shell.exec 10 | params: 11 | working_dir: src 12 | shell: bash 13 | script: | 14 | set -e 15 | set -x 16 | 17 | export NODE_VERSION=20.13.0 18 | bash .evergreen/install-node.sh 19 | install: 20 | - command: shell.exec 21 | params: 22 | working_dir: src 23 | shell: bash 24 | script: | 25 | set -e 26 | set -x 27 | 28 | . .evergreen/use-node.sh 29 | npm install 30 | check: 31 | - command: shell.exec 32 | params: 33 | working_dir: src 34 | shell: bash 35 | script: | 36 | set -e 37 | set -x 38 | 39 | . .evergreen/use-node.sh 40 | npm run build 41 | npm run lint 42 | test: 43 | - command: shell.exec 44 | params: 45 | working_dir: src 46 | shell: bash 47 | env: 48 | TEST_NODE_VERSION: ${node_version} 49 | OKTA_TEST_CONFIG: ${okta_test_config} 50 | OKTA_TEST_CREDENTIALS: ${okta_test_credentials} 51 | AZURE_TEST_CONFIG: ${azure_test_config} 52 | AZURE_TEST_CREDENTIALS: ${azure_test_credentials} 53 | DISTRO_ID: ${distro_id} 54 | script: | 55 | set -e 56 | set -x 57 | 58 | rm -rf /tmp/m && mkdir -pv /tmp/m # Node.js compilation can fail on long path prefixes 59 | trap "rm -rf /tmp/m" EXIT 60 | export TMP=/tmp/m 61 | export TMPDIR=/tmp/m 62 | 63 | # The CI machines we have for Windows and x64 macOS are not 64 | # able to compile OpenSSL with assembly support, 65 | # so we revert back to the slower version. 66 | if [ "$OS" == "Windows_NT" ]; then 67 | export PATH="/cygdrive/c/python/Python310/Scripts:/cygdrive/c/python/Python310:/cygdrive/c/Python310/Scripts:/cygdrive/c/Python310:$PATH" 68 | export BOXEDNODE_CONFIGURE_ARGS='openssl-no-asm' 69 | elif uname -a | grep -q 'Darwin.*x86_64'; then 70 | export BOXEDNODE_CONFIGURE_ARGS='--openssl-no-asm' 71 | fi 72 | 73 | . .evergreen/use-node.sh 74 | npm run build 75 | TEST_NODE_VERSION="$TEST_NODE_VERSION" npm run test-ci 76 | 77 | tasks: 78 | - name: test_n14 79 | commands: 80 | - func: checkout 81 | - func: install_node 82 | - func: install 83 | - func: test 84 | vars: 85 | node_version: "14.21.3" 86 | - name: test_n16 87 | commands: 88 | - func: checkout 89 | - func: install_node 90 | - func: install 91 | - func: test 92 | vars: 93 | node_version: "16.20.1" 94 | - name: test_n18 95 | commands: 96 | - func: checkout 97 | - func: install_node 98 | - func: install 99 | - func: test 100 | vars: 101 | node_version: "18.17.0" 102 | - name: test_n20 103 | commands: 104 | - func: checkout 105 | - func: install_node 106 | - func: install 107 | - func: test 108 | vars: 109 | node_version: "20.13.0" 110 | - name: check 111 | commands: 112 | - func: checkout 113 | - func: install_node 114 | - func: install 115 | - func: check 116 | 117 | buildvariants: 118 | - name: ubuntu_x64_test 119 | display_name: 'Ubuntu 20.04 x64' 120 | run_on: ubuntu2004-large 121 | tasks: 122 | - test_n14 123 | - test_n16 124 | - test_n18 125 | - test_n20 126 | - check 127 | - name: macos_x64_test 128 | display_name: 'macOS 11.00 x64' 129 | run_on: macos-1100 130 | tasks: 131 | - test_n14 132 | - test_n16 133 | - test_n18 134 | - test_n20 135 | - name: macos_arm64_test 136 | display_name: 'macOS 11.00 arm64' 137 | run_on: macos-1100-arm64 138 | tasks: 139 | - test_n14 140 | - test_n16 141 | - test_n18 142 | - test_n20 143 | - name: windows_x64_test 144 | display_name: 'Windows x64' 145 | run_on: windows-vsCurrent-xlarge 146 | tasks: 147 | - test_n14 148 | - test_n16 149 | - test_n18 150 | - test_n20 151 | -------------------------------------------------------------------------------- /.evergreen/install-node.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # adapted from the Node.js driver's script for installing Node.js 3 | set -e 4 | set -x 5 | 6 | export BASEDIR="$PWD" 7 | mkdir -p .deps 8 | cd .deps 9 | 10 | NVM_URL="https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh" 11 | 12 | # this needs to be explicitly exported for the nvm install below 13 | export NVM_DIR="$PWD/nvm" 14 | export XDG_CONFIG_HOME=$PWD 15 | 16 | # install Node.js on Windows 17 | if [[ "$OS" == "Windows_NT" ]]; then 18 | curl -o node.zip "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-win-x64.zip" 19 | unzip node.zip 20 | mkdir -p node/bin 21 | mv -v node-v$NODE_VERSION-win-x64/* node/bin 22 | chmod a+x node/bin/* 23 | export PATH="$PWD/node/bin:$PATH" 24 | # install Node.js on Linux/MacOS 25 | else 26 | curl -o- $NVM_URL | bash 27 | set +x 28 | [ -s "${NVM_DIR}/nvm.sh" ] && source "${NVM_DIR}/nvm.sh" 29 | nvm install --no-progress "$NODE_VERSION" 30 | fi 31 | 32 | which node && node -v || echo "node not found, PATH=$PATH" 33 | which npm && npm -v || echo "npm not found, PATH=$PATH" 34 | -------------------------------------------------------------------------------- /.evergreen/use-node.sh: -------------------------------------------------------------------------------- 1 | if [[ "$OS" == "Windows_NT" ]]; then 2 | export PATH="$PWD/.deps/node/bin:$PATH" 3 | else 4 | export NVM_DIR="$PWD/.deps/nvm" 5 | [ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh" 6 | fi 7 | 8 | echo "updated PATH=$PATH" 9 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | 2 | # For most projects, this workflow file will not need changing; you simply need 3 | # to commit it to your repository. 4 | # 5 | # You may wish to alter this file to override the set of languages analyzed, 6 | # or to provide custom queries or build logic. 7 | # 8 | # ******** NOTE ******** 9 | # We have attempted to detect the languages in your repository. Please check 10 | # the `language` matrix defined below to confirm you have the correct set of 11 | # supported CodeQL languages. 12 | # 13 | name: "CodeQL" 14 | 15 | on: 16 | push: 17 | branches: [ "main" ] 18 | pull_request: 19 | # The branches below must be a subset of the branches above 20 | branches: [ "main" ] 21 | schedule: 22 | - cron: '30 14 * * 4' 23 | 24 | jobs: 25 | analyze: 26 | name: Analyze 27 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 28 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 29 | permissions: 30 | actions: read 31 | contents: read 32 | security-events: write 33 | 34 | strategy: 35 | fail-fast: false 36 | matrix: 37 | language: 38 | - 'javascript' 39 | - 'cpp' 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v3 44 | 45 | - name: Use Node.js v18.x 46 | if: matrix.language == 'cpp' 47 | uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 48 | with: 49 | node-version: 18.x 50 | 51 | - name: Use Node.js v18.x 52 | if: matrix.language == 'cpp' 53 | run: | 54 | npm install 55 | npm run build 56 | 57 | # Initializes the CodeQL tools for scanning. 58 | - name: Initialize CodeQL 59 | uses: github/codeql-action/init@v2 60 | with: 61 | languages: ${{ matrix.language }} 62 | # If you wish to specify custom queries, you can do so here or in a config file. 63 | # By default, queries listed here will override any specified in a config file. 64 | # Prefix the list here with "+" to use these queries and those in the config file. 65 | 66 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 67 | # queries: security-extended,security-and-quality 68 | config: | 69 | paths-ignore: 70 | - '**/*.test.js' 71 | - '**/*.spec.js' 72 | - '**/*.test.ts' 73 | - '**/*.spec.ts' 74 | - '**/*.test.tsx' 75 | - '**/*.spec.tsx' 76 | 77 | - name: Build cpp 78 | if: matrix.language == 'cpp' 79 | run: | 80 | ./test/compile-main-template-only.sh 81 | 82 | - name: Perform CodeQL Analysis 83 | uses: github/codeql-action/analyze@v2 84 | with: 85 | category: "/language:${{matrix.language}}" 86 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | on: [pull_request] 2 | 3 | name: CI 4 | 5 | defaults: 6 | run: 7 | shell: bash 8 | 9 | jobs: 10 | test-posix: 11 | name: Unix tests 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: [ubuntu-latest] 16 | node-version: [14.x, 16.x, 18.x, 20.x] 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v2 22 | with: 23 | check-latest: true 24 | node-version: ${{ matrix.node-version }} 25 | - name: Install npm@8.x 26 | if: ${{ matrix.node-version == '14.x' }} 27 | run: npm install -g npm@8.x 28 | - name: Install Dependencies 29 | run: npm install 30 | - name: Test 31 | run: npm test 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .lock-wscript 3 | .idea/ 4 | .vscode/ 5 | *.iml 6 | .nvmrc 7 | .nyc_output 8 | *.swp 9 | lerna-debug.log 10 | lib-cov 11 | npm-debug.log 12 | .idea/ 13 | coverage/ 14 | dist/ 15 | node_modules/ 16 | .lock-wscript 17 | .cache/ 18 | expansions.yaml 19 | tmp/expansions.yaml 20 | .evergreen/mongodb 21 | tmp/ 22 | .esm-wrapper.mjs 23 | /lib/ 24 | package-lock.json 25 | main-template-build 26 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | test/ 3 | .nyc_output/ 4 | .github/ 5 | .evergreen/ 6 | .evergreen.yml 7 | tsconfig.json 8 | .eslintrc 9 | main-template-build/ 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | Copyright 2020 MongoDB Inc. 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📦 boxednode – Ship a JS file with Node.js in a box 2 | 3 | Take 4 | 5 | 1. A JavaScript file 6 | 2. Node.js 7 | 8 | and pack them up as a single binary. 9 | 10 | For example: 11 | 12 | ```sh 13 | $ cat example.js 14 | console.log('Hello, world!'); 15 | $ boxednode -s example.js -t example 16 | $ ./example 17 | Hello, world! 18 | ``` 19 | 20 | ## CLI usage 21 | 22 | ```sh 23 | Options: 24 | --version Show version number [boolean] 25 | -c, --clean Clean up temporary directory after success [boolean] 26 | -s, --source Source .js file [string] [required] 27 | -t, --target Target executable file [string] [required] 28 | -n, --node-version Node.js version or semver version range 29 | [string] [default: "*"] 30 | -C, --configure-args Extra ./configure or vcbuild arguments, comma-separated 31 | [string] 32 | -M, --make-args Extra make or vcbuild arguments, comma-separated[string] 33 | --tmpdir Temporary directory for compiling Node.js source[string] 34 | --help Show help [boolean] 35 | ``` 36 | 37 | Node.js versions may be specific versions, semver ranges, or any of the aliases 38 | supported by https://github.com/pkgjs/nv/. 39 | 40 | ## Programmatic API 41 | 42 | ```js 43 | type CompilationOptions = { 44 | // Single Node.js version, semver range or shorthand alias to pick from 45 | nodeVersionRange: string; 46 | 47 | // Optional temporary directory for storing and compiling Node.js source 48 | tmpdir?: string; 49 | 50 | // A single .js file that serves as the entry point for the generated binary 51 | sourceFile: string; 52 | 53 | // The file path to the target binary 54 | targetFile: string; 55 | 56 | // Optional list of extra arguments to be passed to `./configure` or `vcbuild` 57 | configureArgs?: string[]; 58 | 59 | // Optional list of extra arguments to be passed to `make` or `vcbuild` 60 | makeArgs?: string[]; 61 | 62 | // If true, remove the temporary directory created earlier when done 63 | clean?: boolean; 64 | 65 | // Environment variables for build processes. Defaults to inheriting 66 | // environment variables. 67 | env?: { [name: string]: string | undefined }; 68 | 69 | // Specify the entrypoint target name. If this is 'foo', then the resulting 70 | // binary will be able to load the source file as 'require("foo/foo")'. 71 | // This defaults to the basename of sourceFile, e.g. 'bar' for '/path/bar.js'. 72 | namespace?: string; 73 | 74 | // A list of native addons to link in. 75 | addons?: AddonConfig[]; 76 | 77 | // Make sure the binary works for addons that use the `bindings` npm package, 78 | // which would otherwise not be compatible with a single-binary model. 79 | // By default, this is enabled if any addons are specified and 80 | // disabled otherwise. 81 | // (This will make `fs.accessSync('/node_modules')` not throw an exception.) 82 | enableBindingsPatch?: boolean; 83 | 84 | // A custom hook that is run just before starting the compile step. 85 | preCompileHook?: (nodeSourceTree: string, options: CompilationOptions) => void | Promise; 86 | 87 | // A list of attributes to set on the generated executable. This is currently 88 | // only being used on Windows. 89 | executableMetadata?: ExecutableMetadata; 90 | }; 91 | 92 | type AddonConfig = { 93 | // Path to the root directory of the target addon, i.e. the one containing 94 | // a binding.gyp file. 95 | path: string; 96 | 97 | // A regular expression to match for `require()` calls from the main file. 98 | // `require(str)` will return the linked binding if `str` matches. 99 | // This will *not* be the same as `require(path)`, which usually is a JS 100 | // wrapper around this. 101 | requireRegexp: RegExp; 102 | }; 103 | 104 | type ExecutableMetadata = { 105 | // Sets Windows .exe InternalName and ProductName 106 | name?: string; 107 | 108 | // Sets Windows .exe FileDescription 109 | description?: string; 110 | 111 | // Sets Windows .exe FileVersion and ProductVersion 112 | version?: string; 113 | 114 | // Sets Windows .exe CompanyName 115 | manufacturer?: string; 116 | 117 | // Sets Windows .exe LegalCopyright 118 | copyright?: string; 119 | 120 | // Provides the path to a .ico file to use for the 121 | // Windows .exe file. 122 | icon?: string; 123 | }; 124 | 125 | export function compileJSFileAsBinary(options: CompilationOptions); 126 | ``` 127 | 128 | The `BOXEDNODE_CONFIGURE_ARGS` environment variable will be read as a 129 | comma-separated list of strings and added to `configureArgs`, and likewise 130 | `BOXEDNODE_MAKE_ARGS` to `makeArgs`. 131 | 132 | ## Why this solution 133 | 134 | We needed a simple and reliable way to create shippable binaries from a source 135 | file. 136 | 137 | Unlike others, this solution: 138 | 139 | - Works for Node.js v12.x and above, without being tied to specific versions 140 | - Uses only officially supported, stable Node.js APIs 141 | - Creates binaries that are not bloated with extra features 142 | - Creates binaries that can be signed and notarized on macOS 143 | - Supports linking native addons into the binary 144 | 145 | ## Prerequisites 146 | 147 | This package compiles Node.js from source. See the Node.js 148 | [BUILDING.md file](https://github.com/nodejs/node/blob/master/BUILDING.md) for 149 | a complete list of tools that may be necessary. 150 | 151 | ## Releasing 152 | 153 | To release a new version, run the following command in main: 154 | 155 | ```sh 156 | npm version [patch|minor|major] && npm it & npm publish && git push origin main --tags 157 | ``` 158 | 159 | ## Not supported 160 | 161 | - Multiple JS files 162 | 163 | ## Similar projects 164 | 165 | - [pkg](https://www.npmjs.com/package/pkg) 166 | - [nexe](https://www.npmjs.com/package/nexe) 167 | 168 | ## License 169 | 170 | [Apache-2.0](./LICENSE) 171 | -------------------------------------------------------------------------------- /bin/boxednode.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | 'use strict'; 4 | const { compileJSFileAsBinary } = require('..'); 5 | const argv = require('yargs') 6 | .option('clean', { 7 | alias: 'c', type: 'boolean', desc: 'Clean up temporary directory after success' 8 | }) 9 | .option('source', { 10 | alias: 's', type: 'string', demandOption: true, desc: 'Source .js file' 11 | }) 12 | .option('target', { 13 | alias: 't', type: 'string', demandOption: true, desc: 'Target executable file' 14 | }) 15 | .option('node-version', { 16 | alias: 'n', type: 'string', desc: 'Node.js version or semver version range or .tar.gz file url', default: '*' 17 | }) 18 | .option('configure-args', { 19 | alias: 'C', type: 'string', desc: 'Extra ./configure or vcbuild arguments, comma-separated' 20 | }) 21 | .option('make-args', { 22 | alias: 'M', type: 'string', desc: 'Extra make or vcbuild arguments, comma-separated' 23 | }) 24 | .option('tmpdir', { 25 | type: 'string', desc: 'Temporary directory for compiling Node.js source' 26 | }) 27 | .option('namespace', { 28 | alias: 'N', type: 'string', desc: 'Module identifier for the generated binary' 29 | }) 30 | .option('use-legacy-default-uv-loop', { 31 | type: 'boolean', desc: 'Use the global singleton libuv event loop rather than a separate local one' 32 | }) 33 | .option('use-code-cache', { 34 | alias: 'H', type: 'boolean', desc: 'Use Node.js code cache support to speed up startup' 35 | }) 36 | .option('use-node-snapshot', { 37 | alias: 'S', type: 'boolean', desc: 'Use experimental Node.js snapshot support' 38 | }) 39 | .example('$0 -s myProject.js -t myProject.exe -n ^14.0.0', 40 | 'Create myProject.exe from myProject.js using Node.js v14') 41 | .help() 42 | .argv; 43 | 44 | (async function main () { 45 | try { 46 | await compileJSFileAsBinary({ 47 | nodeVersionRange: argv.n, 48 | sourceFile: argv.s, 49 | targetFile: argv.t, 50 | tmpdir: argv.tmpdir, 51 | clean: argv.c, 52 | configureArgs: (argv.C || '').split(',').filter(Boolean), 53 | makeArgs: (argv.M || '').split(',').filter(Boolean), 54 | namespace: argv.N, 55 | useLegacyDefaultUvLoop: argv.useLegacyDefaultUvLoop, 56 | useCodeCache: argv.H, 57 | useNodeSnapshot: argv.S, 58 | compressBlobs: argv.Z 59 | }); 60 | } catch (err) { 61 | console.error(err); 62 | process.exitCode = 1; 63 | } 64 | })(); 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boxednode", 3 | "version": "2.4.4", 4 | "description": "Create a shippable binary from a JS file", 5 | "main": "lib/index.js", 6 | "exports": { 7 | "require": "./lib/index.js", 8 | "import": "./.esm-wrapper.mjs" 9 | }, 10 | "bin": { 11 | "boxednode": "bin/boxednode.js" 12 | }, 13 | "engines": { 14 | "node": ">= 12.4.0" 15 | }, 16 | "scripts": { 17 | "lint": "eslint **/*.ts bin/*.js", 18 | "test": "npm run lint && npm run build && npm run test-ci", 19 | "test-ci": "nyc mocha --colors -r ts-node/register test/*.ts", 20 | "build": "npm run compile-ts && gen-esm-wrapper . ./.esm-wrapper.mjs", 21 | "prepack": "npm run build", 22 | "compile-ts": "tsc -p tsconfig.json" 23 | }, 24 | "keywords": [ 25 | "node.js", 26 | "binary", 27 | "packaging", 28 | "shipping" 29 | ], 30 | "author": "Anna Henningsen ", 31 | "homepage": "https://github.com/mongodb-js/boxednode", 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/mongodb-js/boxednode.git" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/mongodb-js/boxednode/issues" 38 | }, 39 | "license": "Apache-2.0", 40 | "devDependencies": { 41 | "@types/mocha": "^8.0.3", 42 | "@types/node": "^14.11.1", 43 | "@typescript-eslint/eslint-plugin": "^4.2.0", 44 | "@typescript-eslint/parser": "^4.2.0", 45 | "actual-crash": "1.0.3", 46 | "eslint": "^7.9.0", 47 | "eslint-config-semistandard": "^15.0.1", 48 | "eslint-config-standard": "^14.1.1", 49 | "eslint-plugin-import": "^2.22.0", 50 | "eslint-plugin-node": "^11.1.0", 51 | "eslint-plugin-promise": "^4.2.1", 52 | "eslint-plugin-standard": "^4.0.1", 53 | "gen-esm-wrapper": "^1.1.0", 54 | "mocha": "^10.0.0", 55 | "nyc": "^15.1.0", 56 | "ts-node": "^10.8.1", 57 | "typescript": "^4.0.3", 58 | "weak-napi": "2.0.2" 59 | }, 60 | "dependencies": { 61 | "@pkgjs/nv": "^0.2.1", 62 | "chalk": "^4.1.0", 63 | "cli-progress": "^3.8.2", 64 | "gyp-parser": "^1.0.4", 65 | "node-fetch": "^2.6.1", 66 | "node-gyp": "^9.0.0", 67 | "pkg-up": "^3.1.0", 68 | "rimraf": "^3.0.2", 69 | "semver": "^7.3.2", 70 | "tar": "^6.0.5", 71 | "yargs": "^16.0.3" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /resources/add-node.h: -------------------------------------------------------------------------------- 1 | #ifdef BUILDING_BOXEDNODE_EXTENSION 2 | #undef NODE_MODULE_X 3 | #define NODE_MODULE_X(modname, regfunc, priv, flags) \ 4 | extern "C" { \ 5 | static node::node_module _module = \ 6 | { \ 7 | NODE_MODULE_VERSION, \ 8 | flags, \ 9 | NULL, /* NOLINT (readability/null_usage) */ \ 10 | __FILE__, \ 11 | (node::addon_register_func) (regfunc), \ 12 | NULL, /* NOLINT (readability/null_usage) */ \ 13 | NODE_STRINGIFY(BOXEDNODE_MODULE_NAME), \ 14 | priv, \ 15 | NULL /* NOLINT (readability/null_usage) */ \ 16 | }; \ 17 | void BOXEDNODE_REGISTER_FUNCTION( \ 18 | const void** node_mod, const void**) { \ 19 | *node_mod = &_module; \ 20 | } \ 21 | } 22 | 23 | #undef NODE_MODULE_CONTEXT_AWARE_X 24 | #define NODE_MODULE_CONTEXT_AWARE_X(modname, regfunc, priv, flags) \ 25 | extern "C" { \ 26 | static node::node_module _module = \ 27 | { \ 28 | NODE_MODULE_VERSION, \ 29 | flags, \ 30 | NULL, /* NOLINT (readability/null_usage) */ \ 31 | __FILE__, \ 32 | NULL, /* NOLINT (readability/null_usage) */ \ 33 | (node::addon_context_register_func) (regfunc), \ 34 | NODE_STRINGIFY(BOXEDNODE_MODULE_NAME), \ 35 | priv, \ 36 | NULL /* NOLINT (readability/null_usage) */ \ 37 | }; \ 38 | void BOXEDNODE_REGISTER_FUNCTION( \ 39 | const void** node_mod, const void**) { \ 40 | *node_mod = &_module; \ 41 | } \ 42 | } 43 | 44 | #undef NODE_MODULE 45 | #define NODE_MODULE(modname, regfunc) \ 46 | NODE_MODULE_X(modname, regfunc, NULL, 0x2) 47 | 48 | #undef NODE_MODULE_CONTEXT_AWARE 49 | #define NODE_MODULE_CONTEXT_AWARE(modname, regfunc) \ 50 | NODE_MODULE_CONTEXT_AWARE_X(modname, regfunc, NULL, 0x2) 51 | 52 | #undef NODE_MODULE_DECL 53 | #define NODE_MODULE_DECL /* nothing */ 54 | 55 | #undef NODE_MODULE_INITIALIZER_BASE 56 | #define NODE_MODULE_INITIALIZER_BASE node_register_module_v 57 | 58 | #undef NODE_MODULE_INITIALIZER_X 59 | #define NODE_MODULE_INITIALIZER_X(base, version) \ 60 | NODE_MODULE_INITIALIZER_X_HELPER(base, version) 61 | 62 | #undef NODE_MODULE_INITIALIZER_X_HELPER 63 | #define NODE_MODULE_INITIALIZER_X_HELPER(base, version) base##version 64 | 65 | #undef NODE_MODULE_INITIALIZER 66 | #define NODE_MODULE_INITIALIZER \ 67 | NODE_MODULE_INITIALIZER_X(NODE_MODULE_INITIALIZER_BASE, \ 68 | NODE_MODULE_VERSION) 69 | 70 | #undef NODE_MODULE_INIT 71 | #define NODE_MODULE_INIT() \ 72 | extern "C" NODE_MODULE_EXPORT void \ 73 | NODE_MODULE_INITIALIZER(v8::Local exports, \ 74 | v8::Local module, \ 75 | v8::Local context); \ 76 | NODE_MODULE_CONTEXT_AWARE(NODE_GYP_MODULE_NAME, \ 77 | NODE_MODULE_INITIALIZER) \ 78 | void NODE_MODULE_INITIALIZER(v8::Local exports, \ 79 | v8::Local module, \ 80 | v8::Local context) 81 | 82 | #endif // BUILDING_BOXEDNODE_EXTENSION 83 | -------------------------------------------------------------------------------- /resources/add-node_api.h: -------------------------------------------------------------------------------- 1 | #ifdef BUILDING_BOXEDNODE_EXTENSION 2 | 3 | #undef EXTERN_C_START 4 | #undef EXTERN_C_END 5 | #ifdef __cplusplus 6 | #define EXTERN_C_START extern "C" { 7 | #define EXTERN_C_END } 8 | #else 9 | #define EXTERN_C_START 10 | #define EXTERN_C_END 11 | #endif 12 | 13 | #ifndef NODE_STRINGIFY 14 | # define NODE_STRINGIFY(n) NODE_STRINGIFY_HELPER(n) 15 | # define NODE_STRINGIFY_HELPER(n) #n 16 | #endif 17 | 18 | #undef NAPI_MODULE_X 19 | #define NAPI_MODULE_X(modname, regfunc, priv, flags) \ 20 | EXTERN_C_START \ 21 | static napi_module _module = \ 22 | { \ 23 | NAPI_MODULE_VERSION, \ 24 | flags, \ 25 | __FILE__, \ 26 | regfunc, \ 27 | NODE_STRINGIFY(BOXEDNODE_MODULE_NAME), \ 28 | priv, \ 29 | {0}, \ 30 | }; \ 31 | void BOXEDNODE_REGISTER_FUNCTION( \ 32 | const void**, const void** napi_mod) { \ 33 | *napi_mod = &_module; \ 34 | } \ 35 | EXTERN_C_END 36 | 37 | #undef NAPI_MODULE_INITIALIZER_X 38 | #define NAPI_MODULE_INITIALIZER_X(base, version) \ 39 | NAPI_MODULE_INITIALIZER_X_HELPER(base, version) 40 | #undef NAPI_MODULE_INITIALIZER_X_HELPER 41 | #define NAPI_MODULE_INITIALIZER_X_HELPER(base, version) base##version 42 | 43 | #undef NAPI_MODULE 44 | #define NAPI_MODULE(modname, regfunc) \ 45 | NAPI_MODULE_X(modname, regfunc, NULL, 0x2) 46 | 47 | #undef NAPI_MODULE_INITIALIZER_BASE 48 | #define NAPI_MODULE_INITIALIZER_BASE napi_register_module_v 49 | 50 | #undef NAPI_MODULE_INITIALIZER 51 | #define NAPI_MODULE_INITIALIZER \ 52 | NAPI_MODULE_INITIALIZER_X(NAPI_MODULE_INITIALIZER_BASE, \ 53 | NAPI_MODULE_VERSION) 54 | 55 | #undef NAPI_MODULE_INIT 56 | #define NAPI_MODULE_INIT() \ 57 | EXTERN_C_START \ 58 | NAPI_MODULE_EXPORT napi_value \ 59 | NAPI_MODULE_INITIALIZER(napi_env env, napi_value exports); \ 60 | EXTERN_C_END \ 61 | NAPI_MODULE(NODE_GYP_MODULE_NAME, NAPI_MODULE_INITIALIZER) \ 62 | napi_value NAPI_MODULE_INITIALIZER(napi_env env, \ 63 | napi_value exports) 64 | 65 | #endif // BUILDING_BOXEDNODE_EXTENSION 66 | -------------------------------------------------------------------------------- /resources/entry-point-trampoline.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Module = require('module'); 3 | const vm = require('vm'); 4 | const v8 = require('v8'); 5 | const path = require('path'); 6 | const assert = require('assert'); 7 | const { 8 | requireMappings, 9 | enableBindingsPatch 10 | } = REPLACE_WITH_BOXEDNODE_CONFIG; 11 | const hydatedRequireMappings = 12 | requireMappings.map(([re, reFlags, linked]) => [new RegExp(re, reFlags), linked]); 13 | 14 | if (process.argv[2] === '--') process.argv.splice(2, 1); 15 | 16 | if (enableBindingsPatch) { 17 | // Hack around various deficiencies in https://github.com/TooTallNate/node-bindings 18 | const fs = require('fs'); 19 | const origFsAccessSync = fs.accessSync; 20 | fs.accessSync = (filename, ...args) => { 21 | if (path.basename(filename) === 'node_modules' && 22 | path.join(path.dirname(filename), '..') === path.dirname(filename)) { 23 | return; 24 | } 25 | return origFsAccessSync.call(fs, filename, ...args); 26 | }; 27 | 28 | let epst = Error.prepareStackTrace; 29 | Object.defineProperty(Error, 'prepareStackTrace', { 30 | configurable: true, 31 | get() { 32 | return epst; 33 | }, 34 | set(v) { 35 | if (typeof v !== 'function') { 36 | epst = v; 37 | return; 38 | } 39 | epst = function(error, stack) { 40 | stack = stack.map(entry => { 41 | if (!entry) return entry; 42 | const origGetFileName = entry.getFileName; 43 | Object.defineProperty(entry, 'getFileName', { 44 | configurable: true, 45 | value: function(...args) { 46 | return origGetFileName.call(this, ...args) || ''; 47 | } 48 | }); 49 | return entry; 50 | }) 51 | return v.call(this, error, stack); 52 | }; 53 | } 54 | }); 55 | } 56 | 57 | const outerRequire = require; 58 | module.exports = (src, codeCacheMode, codeCache) => { 59 | const __filename = process.execPath; 60 | const __dirname = path.dirname(process.execPath); 61 | let innerRequire; 62 | const exports = {}; 63 | const isBuildingSnapshot = () => !!v8?.startupSnapshot?.isBuildingSnapshot(); 64 | const usesSnapshot = isBuildingSnapshot(); 65 | 66 | if (usesSnapshot) { 67 | innerRequire = outerRequire; // Node.js snapshots currently do not support userland require() 68 | v8.startupSnapshot.addDeserializeCallback(() => { 69 | if (process.argv[1] === '--boxednode-snapshot-argv-fixup') { 70 | process.argv.splice(1, 1, process.execPath); 71 | } 72 | }); 73 | } else { 74 | innerRequire = Module.createRequire(__filename); 75 | } 76 | 77 | function require(module) { 78 | for (const [ re, linked ] of hydatedRequireMappings) { 79 | try { 80 | if (re.test(module)) 81 | return process._linkedBinding(linked); 82 | } catch {} 83 | } 84 | return innerRequire(module); 85 | } 86 | Object.defineProperties(require, Object.getOwnPropertyDescriptors(innerRequire)); 87 | Object.setPrototypeOf(require, Object.getPrototypeOf(innerRequire)); 88 | 89 | process.argv.unshift(__filename); 90 | process.boxednode = { usesSnapshot }; 91 | 92 | const module = { 93 | exports, 94 | children: [], 95 | filename: __filename, 96 | id: __filename, 97 | path: __dirname, 98 | require 99 | }; 100 | 101 | let mainFunction; 102 | if (usesSnapshot) { 103 | mainFunction = eval(`(function(__filename, __dirname, require, exports, module) {\n${src}\n})`); 104 | } else { 105 | mainFunction = vm.compileFunction(src, [ 106 | '__filename', '__dirname', 'require', 'exports', 'module' 107 | ], { 108 | filename: __filename, 109 | cachedData: codeCache.length > 0 ? codeCache : undefined, 110 | produceCachedData: codeCacheMode === 'generate' 111 | }); 112 | if (codeCacheMode === 'generate') { 113 | assert.strictEqual(mainFunction.cachedDataProduced, true); 114 | require('fs').writeFileSync('intermediate.out', mainFunction.cachedData); 115 | return; 116 | } 117 | } 118 | 119 | process.boxednode.hasCodeCache = codeCache.length > 0; 120 | // https://github.com/nodejs/node/pull/46320 121 | process.boxednode.rejectedCodeCache = mainFunction.cachedDataRejected; 122 | 123 | let jsTimingEntries = []; 124 | if (usesSnapshot) { 125 | v8.startupSnapshot.addDeserializeCallback(() => { 126 | jsTimingEntries = []; 127 | }); 128 | } 129 | process.boxednode.markTime = (category, label) => { 130 | jsTimingEntries.push([category, label, process.hrtime.bigint()]); 131 | }; 132 | process.boxednode.getTimingData = () => { 133 | if (isBuildingSnapshot()) { 134 | throw new Error('getTimingData() is not available during snapshot building'); 135 | } 136 | const data = [ 137 | ...jsTimingEntries, 138 | ...process._linkedBinding('boxednode_linked_bindings').getTimingData() 139 | ].sort((a, b) => Number(a[2] - b[2])); 140 | // Adjust times so that process initialization happens at time 0 141 | return data.map(([category, label, time]) => [category, label, Number(time - data[0][2])]); 142 | }; 143 | 144 | mainFunction(__filename, __dirname, require, exports, module); 145 | return module.exports; 146 | }; 147 | -------------------------------------------------------------------------------- /resources/main-template.cc: -------------------------------------------------------------------------------- 1 | // This is based on the source code provided as an example in 2 | // https://nodejs.org/api/embedding.html. 3 | 4 | #undef NDEBUG 5 | 6 | #include "node.h" 7 | #include "node_api.h" 8 | #include "uv.h" 9 | #include "brotli/decode.h" 10 | #include 11 | #if HAVE_OPENSSL 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #endif 20 | #include // injected code may refer to std::underlying_type 21 | #include 22 | 23 | using namespace node; 24 | using namespace v8; 25 | 26 | // 18.11.0 is the minimum version that has https://github.com/nodejs/node/pull/44121 27 | #if !NODE_VERSION_AT_LEAST(18, 11, 0) 28 | #define USE_OWN_LEGACY_PROCESS_INITIALIZATION 1 29 | #endif 30 | 31 | // 20.0.0 will have https://github.com/nodejs/node/pull/45888, possibly the PR 32 | // will be backported to older versions but for now this is the one where we 33 | // can be sure of its presence. 34 | #if NODE_VERSION_AT_LEAST(20, 0, 0) 35 | #define NODE_VERSION_SUPPORTS_EMBEDDER_SNAPSHOT 1 36 | #endif 37 | 38 | // 20.13.0 has https://github.com/nodejs/node/pull/52595 for better startup snapshot 39 | // initialization performance. 40 | #if NODE_VERSION_AT_LEAST(20, 13, 0) 41 | #define NODE_VERSION_SUPPORTS_STRING_VIEW_SNAPSHOT 1 42 | #endif 43 | 44 | // Snapshot config is supported since https://github.com/nodejs/node/pull/50453 45 | #if NODE_VERSION_AT_LEAST(20, 12, 0) && !defined(BOXEDNODE_SNAPSHOT_CONFIG_FLAGS) 46 | #define BOXEDNODE_SNAPSHOT_CONFIG_FLAGS (SnapshotFlags::kWithoutCodeCache) 47 | #endif 48 | 49 | // 18.1.0 is the current minimum version that has https://github.com/nodejs/node/pull/42809, 50 | // which introduced crashes when using workers, and later 18.9.0 is the current 51 | // minimum version to contain https://github.com/nodejs/node/pull/44252, which 52 | // introcued crashes when using the vm module. 53 | // We should be able to remove this restriction again once Node.js stops relying 54 | // on global state for determining whether snapshots are enabled or not 55 | // (after https://github.com/nodejs/node/pull/45888, hopefully). 56 | #if NODE_VERSION_AT_LEAST(18, 1, 0) && !defined(NODE_VERSION_SUPPORTS_EMBEDDER_SNAPSHOT) 57 | #define PASS_NO_NODE_SNAPSHOT_OPTION 1 58 | #endif 59 | 60 | #ifdef USE_OWN_LEGACY_PROCESS_INITIALIZATION 61 | namespace boxednode { 62 | void InitializeOncePerProcess(); 63 | void TearDownOncePerProcess(); 64 | } 65 | #endif 66 | namespace boxednode { 67 | namespace { 68 | struct TimingEntry { 69 | const char* const category; 70 | const char* const label; 71 | uint64_t const time; 72 | TimingEntry* next = nullptr; 73 | ~TimingEntry() { 74 | delete next; 75 | } 76 | }; 77 | TimingEntry start_time_entry { "Node.js Instance", "Process initialization", uv_hrtime() }; 78 | std::atomic current_time_entry { &start_time_entry }; 79 | 80 | void MarkTime(const char* category, const char* label) { 81 | TimingEntry* new_entry = new TimingEntry {category, label, uv_hrtime() }; 82 | do { 83 | new_entry->next = current_time_entry.load(); 84 | } while(!current_time_entry.compare_exchange_strong(new_entry->next, new_entry)); 85 | } 86 | } // anonymous namespace 87 | 88 | Local GetBoxednodeMainScriptSource(Isolate* isolate); 89 | Local GetBoxednodeCodeCacheBuffer(Isolate* isolate); 90 | std::vector GetBoxednodeSnapshotBlobVector(); 91 | #ifdef NODE_VERSION_SUPPORTS_STRING_VIEW_SNAPSHOT 92 | std::optional GetBoxednodeSnapshotBlobSV(); 93 | #endif 94 | 95 | void GetTimingData(const FunctionCallbackInfo& info) { 96 | Isolate* isolate = info.GetIsolate(); 97 | TimingEntry* head = current_time_entry.load(); 98 | std::vector> entries; 99 | while (head != nullptr) { 100 | Local elements[] = { 101 | String::NewFromUtf8(isolate, head->category).ToLocalChecked(), 102 | String::NewFromUtf8(isolate, head->label).ToLocalChecked(), 103 | BigInt::NewFromUnsigned(isolate, head->time) 104 | }; 105 | entries.push_back(Array::New(isolate, elements, sizeof(elements)/sizeof(elements[0]))); 106 | head = head->next; 107 | } 108 | Local retval = Array::New(isolate, entries.data(), entries.size()); 109 | info.GetReturnValue().Set(retval); 110 | } 111 | 112 | void boxednode_linked_bindings_register( 113 | Local exports, 114 | Local module, 115 | Local context, 116 | void* priv) { 117 | NODE_SET_METHOD(exports, "getTimingData", GetTimingData); 118 | } 119 | 120 | } 121 | 122 | extern "C" { 123 | typedef void (*register_boxednode_linked_module)(const void**, const void**); 124 | 125 | REPLACE_DECLARE_LINKED_MODULES 126 | } 127 | 128 | #if __cplusplus >= 201703L 129 | [[maybe_unused]] 130 | #endif 131 | static register_boxednode_linked_module boxednode_linked_modules[] = { 132 | REPLACE_DEFINE_LINKED_MODULES 133 | nullptr // Make sure the array is not empty, for MSVC 134 | }; 135 | 136 | static MaybeLocal LoadBoxednodeEnvironment(Local context) { 137 | Environment* env = GetCurrentEnvironment(context); 138 | return LoadEnvironment(env, 139 | #ifdef BOXEDNODE_CONSUME_SNAPSHOT 140 | node::StartExecutionCallback{} 141 | #else 142 | [&](const StartExecutionCallbackInfo& info) -> MaybeLocal { 143 | Isolate* isolate = context->GetIsolate(); 144 | HandleScope handle_scope(isolate); 145 | Local entrypoint_name = String::NewFromUtf8( 146 | isolate, 147 | REPLACE_WITH_ENTRY_POINT) 148 | .ToLocalChecked(); 149 | Local entrypoint_ret; 150 | if (!info.native_require->Call( 151 | context, 152 | Null(isolate), 153 | 1, 154 | &entrypoint_name 155 | ).ToLocal(&entrypoint_ret)) { 156 | return {}; // JS exception. 157 | } 158 | assert(entrypoint_ret->IsFunction()); 159 | Local trampoline_args[] = { 160 | boxednode::GetBoxednodeMainScriptSource(isolate), 161 | String::NewFromUtf8Literal(isolate, BOXEDNODE_CODE_CACHE_MODE), 162 | boxednode::GetBoxednodeCodeCacheBuffer(isolate), 163 | }; 164 | boxednode::MarkTime("Node.js Instance", "Calling entrypoint"); 165 | if (entrypoint_ret.As()->Call( 166 | context, 167 | Null(isolate), 168 | sizeof(trampoline_args) / sizeof(trampoline_args[0]), 169 | trampoline_args).IsEmpty()) { 170 | return {}; // JS exception. 171 | } 172 | boxednode::MarkTime("Node.js Instance", "Called entrypoint"); 173 | return Null(isolate); 174 | } 175 | #endif 176 | ); 177 | } 178 | 179 | #ifdef BOXEDNODE_GENERATE_SNAPSHOT 180 | static int RunNodeInstance(MultiIsolatePlatform* platform, 181 | const std::vector& args, 182 | const std::vector& exec_args) { 183 | int exit_code = 0; 184 | std::vector errors; 185 | std::unique_ptr setup = 186 | CommonEnvironmentSetup::CreateForSnapshotting( 187 | platform, 188 | &errors, 189 | args, 190 | exec_args 191 | #ifdef BOXEDNODE_SNAPSHOT_CONFIG_FLAGS 192 | , SnapshotConfig { BOXEDNODE_SNAPSHOT_CONFIG_FLAGS, std::nullopt } 193 | #endif 194 | ); 195 | 196 | Isolate* isolate = setup->isolate(); 197 | 198 | { 199 | Locker locker(isolate); 200 | Isolate::Scope isolate_scope(isolate); 201 | 202 | HandleScope handle_scope(isolate); 203 | Local context = setup->context(); 204 | Context::Scope context_scope(context); 205 | if (LoadBoxednodeEnvironment(context).IsEmpty()) 206 | return 1; 207 | exit_code = SpinEventLoop(setup->env()).FromMaybe(1); 208 | } 209 | 210 | { 211 | FILE* fp = fopen("intermediate.out", "wb"); 212 | setup->CreateSnapshot()->ToFile(fp); 213 | fclose(fp); 214 | } 215 | return exit_code; 216 | } 217 | #else // BOXEDNODE_GENERATE_SNAPSHOT 218 | static int RunNodeInstance(MultiIsolatePlatform* platform, 219 | const std::vector& args, 220 | const std::vector& exec_args) { 221 | int exit_code = 0; 222 | uv_loop_t* loop; 223 | #ifndef BOXEDNODE_USE_DEFAULT_UV_LOOP 224 | // Set up a libuv event loop. 225 | uv_loop_t loop_; 226 | loop = &loop_; 227 | int ret = uv_loop_init(loop); 228 | if (ret != 0) { 229 | fprintf(stderr, "%s: Failed to initialize loop: %s\n", 230 | args[0].c_str(), 231 | uv_err_name(ret)); 232 | return 1; 233 | } 234 | #else 235 | loop = uv_default_loop(); 236 | #endif 237 | boxednode::MarkTime("Node.js Instance", "Initialized Loop"); 238 | 239 | std::shared_ptr allocator = 240 | ArrayBufferAllocator::Create(); 241 | 242 | #ifdef BOXEDNODE_CONSUME_SNAPSHOT 243 | assert(EmbedderSnapshotData::CanUseCustomSnapshotPerIsolate()); 244 | node::EmbedderSnapshotData::Pointer snapshot_blob; 245 | #ifdef NODE_VERSION_SUPPORTS_STRING_VIEW_SNAPSHOT 246 | if (const auto snapshot_blob_sv = boxednode::GetBoxednodeSnapshotBlobSV()) { 247 | snapshot_blob = EmbedderSnapshotData::FromBlob(snapshot_blob_sv.value()); 248 | } 249 | #endif 250 | if (!snapshot_blob) { 251 | std::vector snapshot_blob_vec = boxednode::GetBoxednodeSnapshotBlobVector(); 252 | boxednode::MarkTime("Node.js Instance", "Decoded snapshot"); 253 | snapshot_blob = EmbedderSnapshotData::FromBlob(snapshot_blob_vec); 254 | } 255 | boxednode::MarkTime("Node.js Instance", "Read snapshot"); 256 | Isolate* isolate = NewIsolate(allocator, loop, platform, snapshot_blob.get()); 257 | #elif NODE_VERSION_AT_LEAST(14, 0, 0) 258 | Isolate* isolate = NewIsolate(allocator, loop, platform); 259 | #else 260 | Isolate* isolate = NewIsolate(allocator.get(), loop, platform); 261 | #endif 262 | if (isolate == nullptr) { 263 | fprintf(stderr, "%s: Failed to initialize V8 Isolate\n", args[0].c_str()); 264 | return 1; 265 | } 266 | boxednode::MarkTime("Node.js Instance", "Created Isolate"); 267 | 268 | { 269 | Locker locker(isolate); 270 | Isolate::Scope isolate_scope(isolate); 271 | 272 | // Create a node::IsolateData instance that will later be released using 273 | // node::FreeIsolateData(). 274 | std::unique_ptr isolate_data( 275 | node::CreateIsolateData(isolate, loop, platform, allocator.get() 276 | #ifdef BOXEDNODE_CONSUME_SNAPSHOT 277 | , snapshot_blob.get() 278 | #endif 279 | ), 280 | node::FreeIsolateData); 281 | 282 | boxednode::MarkTime("Node.js Instance", "Created IsolateData"); 283 | HandleScope handle_scope(isolate); 284 | Local context; 285 | #ifndef BOXEDNODE_CONSUME_SNAPSHOT 286 | // Set up a new v8::Context. 287 | context = node::NewContext(isolate); 288 | 289 | if (context.IsEmpty()) { 290 | fprintf(stderr, "%s: Failed to initialize V8 Context\n", args[0].c_str()); 291 | return 1; 292 | } 293 | 294 | // The v8::Context needs to be entered when node::CreateEnvironment() and 295 | // node::LoadEnvironment() are being called. 296 | Context::Scope context_scope(context); 297 | #endif 298 | boxednode::MarkTime("Node.js Instance", "Created Context"); 299 | 300 | // Create a node::Environment instance that will later be released using 301 | // node::FreeEnvironment(). 302 | std::unique_ptr env( 303 | node::CreateEnvironment(isolate_data.get(), context, args, exec_args), 304 | node::FreeEnvironment); 305 | #ifdef BOXEDNODE_CONSUME_SNAPSHOT 306 | assert(context.IsEmpty()); 307 | context = GetMainContext(env.get()); 308 | assert(!context.IsEmpty()); 309 | Context::Scope context_scope(context); 310 | #endif 311 | assert(isolate->InContext()); 312 | boxednode::MarkTime("Node.js Instance", "Created Environment"); 313 | 314 | const void* node_mod; 315 | const void* napi_mod; 316 | 317 | for (register_boxednode_linked_module reg : boxednode_linked_modules) { 318 | if (reg == nullptr) continue; 319 | node_mod = nullptr; 320 | napi_mod = nullptr; 321 | reg(&node_mod, &napi_mod); 322 | if (node_mod != nullptr) 323 | AddLinkedBinding(env.get(), *static_cast(node_mod)); 324 | #if NODE_VERSION_AT_LEAST(14, 13, 0) 325 | if (napi_mod != nullptr) 326 | AddLinkedBinding(env.get(), *static_cast(napi_mod)); 327 | #endif 328 | } 329 | AddLinkedBinding( 330 | env.get(), 331 | "boxednode_linked_bindings", 332 | boxednode::boxednode_linked_bindings_register, nullptr); 333 | boxednode::MarkTime("Boxednode Binding", "Added bindings"); 334 | 335 | // Set up the Node.js instance for execution, and run code inside of it. 336 | // There is also a variant that takes a callback and provides it with 337 | // the `require` and `process` objects, so that it can manually compile 338 | // and run scripts as needed. 339 | // The `require` function inside this script does *not* access the file 340 | // system, and can only load built-in Node.js modules. 341 | // `module.createRequire()` is being used to create one that is able to 342 | // load files from the disk, and uses the standard CommonJS file loader 343 | // instead of the internal-only `require` function. 344 | if (LoadBoxednodeEnvironment(context).IsEmpty()) { 345 | return 1; // There has been a JS exception. 346 | } 347 | boxednode::MarkTime("Boxednode Binding", "Loaded Environment, entering loop"); 348 | 349 | { 350 | // SealHandleScope protects against handle leaks from callbacks. 351 | SealHandleScope seal(isolate); 352 | bool more; 353 | do { 354 | uv_run(loop, UV_RUN_DEFAULT); 355 | 356 | // V8 tasks on background threads may end up scheduling new tasks in the 357 | // foreground, which in turn can keep the event loop going. For example, 358 | // WebAssembly.compile() may do so. 359 | platform->DrainTasks(isolate); 360 | 361 | // If there are new tasks, continue. 362 | more = uv_loop_alive(loop); 363 | if (more) continue; 364 | 365 | // node::EmitBeforeExit() is used to emit the 'beforeExit' event on 366 | // the `process` object. 367 | node::EmitBeforeExit(env.get()); 368 | 369 | // 'beforeExit' can also schedule new work that keeps the event loop 370 | // running. 371 | more = uv_loop_alive(loop); 372 | } while (more == true); 373 | } 374 | 375 | // node::EmitExit() returns the current exit code. 376 | exit_code = node::EmitExit(env.get()); 377 | 378 | // node::Stop() can be used to explicitly stop the event loop and keep 379 | // further JavaScript from running. It can be called from any thread, 380 | // and will act like worker.terminate() if called from another thread. 381 | node::Stop(env.get()); 382 | } 383 | 384 | // Unregister the Isolate with the platform and add a listener that is called 385 | // when the Platform is done cleaning up any state it had associated with 386 | // the Isolate. 387 | bool platform_finished = false; 388 | platform->AddIsolateFinishedCallback(isolate, [](void* data) { 389 | *static_cast(data) = true; 390 | }, &platform_finished); 391 | platform->UnregisterIsolate(isolate); 392 | isolate->Dispose(); 393 | 394 | // Wait until the platform has cleaned up all relevant resources. 395 | while (!platform_finished) 396 | uv_run(loop, UV_RUN_ONCE); 397 | #ifndef BOXEDNODE_USE_DEFAULT_UV_LOOP 398 | int err = uv_loop_close(loop); 399 | assert(err == 0); 400 | #endif 401 | 402 | return exit_code; 403 | } 404 | #endif // BOXEDNODE_GENERATE_SNAPSHOT 405 | 406 | static int BoxednodeMain(std::vector args) { 407 | std::vector exec_args; 408 | std::vector errors; 409 | 410 | if (args.size() > 0) { 411 | args.insert(args.begin() + 1, "--"); 412 | #ifdef PASS_NO_NODE_SNAPSHOT_OPTION 413 | args.insert(args.begin() + 1, "--no-node-snapshot"); 414 | #endif 415 | } 416 | 417 | // Parse Node.js CLI options, and print any errors that have occurred while 418 | // trying to parse them. 419 | #ifdef USE_OWN_LEGACY_PROCESS_INITIALIZATION 420 | boxednode::InitializeOncePerProcess(); 421 | int exit_code = node::InitializeNodeWithArgs(&args, &exec_args, &errors); 422 | for (const std::string& error : errors) 423 | fprintf(stderr, "%s: %s\n", args[0].c_str(), error.c_str()); 424 | if (exit_code != 0) { 425 | return exit_code; 426 | } 427 | #else 428 | #if OPENSSL_VERSION_MAJOR >= 3 429 | if (args.size() > 1) 430 | args.insert(args.begin() + 1, "--openssl-shared-config"); 431 | #endif 432 | boxednode::MarkTime("Node.js Instance", "Start InitializeOncePerProcess"); 433 | auto result = node::InitializeOncePerProcess(args, { 434 | node::ProcessInitializationFlags::kNoInitializeV8, 435 | node::ProcessInitializationFlags::kNoInitializeNodeV8Platform, 436 | node::ProcessInitializationFlags::kNoPrintHelpOrVersionOutput 437 | }); 438 | boxednode::MarkTime("Node.js Instance", "Finished InitializeOncePerProcess"); 439 | for (const std::string& error : result->errors()) 440 | fprintf(stderr, "%s: %s\n", args[0].c_str(), error.c_str()); 441 | if (result->exit_code() != 0) { 442 | return result->exit_code(); 443 | } 444 | args = result->args(); 445 | exec_args = result->exec_args(); 446 | #endif 447 | 448 | #ifdef BOXEDNODE_CONSUME_SNAPSHOT 449 | if (args.size() > 0) { 450 | args.insert(args.begin() + 1, "--boxednode-snapshot-argv-fixup"); 451 | } 452 | #endif 453 | 454 | // Create a v8::Platform instance. `MultiIsolatePlatform::Create()` is a way 455 | // to create a v8::Platform instance that Node.js can use when creating 456 | // Worker threads. When no `MultiIsolatePlatform` instance is present, 457 | // Worker threads are disabled. 458 | std::unique_ptr platform = 459 | MultiIsolatePlatform::Create(4); 460 | V8::InitializePlatform(platform.get()); 461 | V8::Initialize(); 462 | 463 | boxednode::MarkTime("Node.js Instance", "Initialized V8"); 464 | // See below for the contents of this function. 465 | int ret = RunNodeInstance(platform.get(), args, exec_args); 466 | 467 | V8::Dispose(); 468 | #ifdef USE_OWN_LEGACY_PROCESS_INITIALIZATION 469 | V8::ShutdownPlatform(); 470 | boxednode::TearDownOncePerProcess(); 471 | #else 472 | V8::DisposePlatform(); 473 | node::TearDownOncePerProcess(); 474 | #endif 475 | return ret; 476 | } 477 | 478 | #ifdef _WIN32 479 | int wmain(int argc, wchar_t* wargv[]) { 480 | // Convert argv to UTF8 481 | std::vector args; 482 | for (int i = 0; i < argc; i++) { 483 | DWORD size = WideCharToMultiByte(CP_UTF8, 484 | 0, 485 | wargv[i], 486 | -1, 487 | nullptr, 488 | 0, 489 | nullptr, 490 | nullptr); 491 | assert(size > 0); 492 | std::string arg(size, '\0'); 493 | DWORD result = WideCharToMultiByte(CP_UTF8, 494 | 0, 495 | wargv[i], 496 | -1, 497 | &arg[0], 498 | size, 499 | nullptr, 500 | nullptr); 501 | assert(result > 0); 502 | arg.resize(result - 1); 503 | args.emplace_back(std::move(arg)); 504 | } 505 | return BoxednodeMain(std::move(args)); 506 | } 507 | 508 | #else 509 | int main(int argc, char** argv) { 510 | argv = uv_setup_args(argc, argv); 511 | std::vector args(argv, argv + argc); 512 | boxednode::MarkTime("Node.js Instance", "Enter BoxednodeMain"); 513 | return BoxednodeMain(std::move(args)); 514 | } 515 | #endif 516 | 517 | // The code below is mostly lifted directly from node.cc 518 | #ifdef USE_OWN_LEGACY_PROCESS_INITIALIZATION 519 | 520 | #if defined(__APPLE__) || defined(__linux__) || defined(_WIN32) 521 | #define NODE_USE_V8_WASM_TRAP_HANDLER 1 522 | #else 523 | #define NODE_USE_V8_WASM_TRAP_HANDLER 0 524 | #endif 525 | 526 | #if NODE_USE_V8_WASM_TRAP_HANDLER 527 | #if defined(_WIN32) 528 | #include "v8-wasm-trap-handler-win.h" 529 | #else 530 | #include 531 | #include "v8-wasm-trap-handler-posix.h" 532 | #endif 533 | #endif // NODE_USE_V8_WASM_TRAP_HANDLER 534 | 535 | #if NODE_USE_V8_WASM_TRAP_HANDLER && defined(_WIN32) 536 | static PVOID old_vectored_exception_handler; 537 | #endif 538 | 539 | #if defined(_MSC_VER) 540 | #include 541 | #include 542 | #define STDIN_FILENO 0 543 | #else 544 | #include 545 | #include // getrlimit, setrlimit 546 | #include // tcgetattr, tcsetattr 547 | #include // STDIN_FILENO, STDERR_FILENO 548 | #endif 549 | 550 | #include 551 | #include 552 | 553 | namespace boxednode { 554 | 555 | #if HAVE_OPENSSL 556 | static void CheckEntropy() { 557 | for (;;) { 558 | int status = RAND_status(); 559 | assert(status >= 0); // Cannot fail. 560 | if (status != 0) 561 | break; 562 | 563 | // Give up, RAND_poll() not supported. 564 | if (RAND_poll() == 0) 565 | break; 566 | } 567 | } 568 | 569 | static bool EntropySource(unsigned char* buffer, size_t length) { 570 | // Ensure that OpenSSL's PRNG is properly seeded. 571 | CheckEntropy(); 572 | // RAND_bytes() can return 0 to indicate that the entropy data is not truly 573 | // random. That's okay, it's still better than V8's stock source of entropy, 574 | // which is /dev/urandom on UNIX platforms and the current time on Windows. 575 | return RAND_bytes(buffer, length) != -1; 576 | } 577 | #endif 578 | 579 | void ResetStdio(); 580 | 581 | #ifdef __POSIX__ 582 | static constexpr unsigned kMaxSignal = 32; 583 | 584 | typedef void (*sigaction_cb)(int signo, siginfo_t* info, void* ucontext); 585 | 586 | void SignalExit(int signo, siginfo_t* info, void* ucontext) { 587 | ResetStdio(); 588 | raise(signo); 589 | } 590 | #endif 591 | 592 | #if NODE_USE_V8_WASM_TRAP_HANDLER 593 | #if defined(_WIN32) 594 | static LONG TrapWebAssemblyOrContinue(EXCEPTION_POINTERS* exception) { 595 | if (v8::TryHandleWebAssemblyTrapWindows(exception)) { 596 | return EXCEPTION_CONTINUE_EXECUTION; 597 | } 598 | return EXCEPTION_CONTINUE_SEARCH; 599 | } 600 | #else 601 | static std::atomic previous_sigsegv_action; 602 | 603 | void TrapWebAssemblyOrContinue(int signo, siginfo_t* info, void* ucontext) { 604 | if (!v8::TryHandleWebAssemblyTrapPosix(signo, info, ucontext)) { 605 | sigaction_cb prev = previous_sigsegv_action.load(); 606 | if (prev != nullptr) { 607 | prev(signo, info, ucontext); 608 | } else { 609 | // Reset to the default signal handler, i.e. cause a hard crash. 610 | struct sigaction sa; 611 | memset(&sa, 0, sizeof(sa)); 612 | sa.sa_handler = SIG_DFL; 613 | int ret = sigaction(signo, &sa, nullptr); 614 | assert(ret == 0); 615 | 616 | ResetStdio(); 617 | raise(signo); 618 | } 619 | } 620 | } 621 | #endif // defined(_WIN32) 622 | #endif // NODE_USE_V8_WASM_TRAP_HANDLER 623 | 624 | #ifdef __POSIX__ 625 | void RegisterSignalHandler(int signal, 626 | sigaction_cb handler, 627 | bool reset_handler) { 628 | assert(handler != nullptr); 629 | #if NODE_USE_V8_WASM_TRAP_HANDLER 630 | if (signal == SIGSEGV) { 631 | assert(previous_sigsegv_action.is_lock_free()); 632 | assert(!reset_handler); 633 | previous_sigsegv_action.store(handler); 634 | return; 635 | } 636 | #endif // NODE_USE_V8_WASM_TRAP_HANDLER 637 | struct sigaction sa; 638 | memset(&sa, 0, sizeof(sa)); 639 | sa.sa_sigaction = handler; 640 | sa.sa_flags = reset_handler ? SA_RESETHAND : 0; 641 | sigfillset(&sa.sa_mask); 642 | int ret = sigaction(signal, &sa, nullptr); 643 | assert(ret == 0); 644 | } 645 | #endif // __POSIX__ 646 | 647 | #ifdef __POSIX__ 648 | static struct { 649 | int flags; 650 | bool isatty; 651 | struct stat stat; 652 | struct termios termios; 653 | } stdio[1 + STDERR_FILENO]; 654 | #endif // __POSIX__ 655 | 656 | 657 | inline void PlatformInit() { 658 | #ifdef __POSIX__ 659 | #if HAVE_INSPECTOR 660 | sigset_t sigmask; 661 | sigemptyset(&sigmask); 662 | sigaddset(&sigmask, SIGUSR1); 663 | const int err = pthread_sigmask(SIG_SETMASK, &sigmask, nullptr); 664 | #endif // HAVE_INSPECTOR 665 | 666 | // Make sure file descriptors 0-2 are valid before we start logging anything. 667 | for (auto& s : stdio) { 668 | const int fd = &s - stdio; 669 | if (fstat(fd, &s.stat) == 0) 670 | continue; 671 | // Anything but EBADF means something is seriously wrong. We don't 672 | // have to special-case EINTR, fstat() is not interruptible. 673 | if (errno != EBADF) 674 | assert(0); 675 | if (fd != open("/dev/null", O_RDWR)) 676 | assert(0); 677 | if (fstat(fd, &s.stat) != 0) 678 | assert(0); 679 | } 680 | 681 | #if HAVE_INSPECTOR 682 | CHECK_EQ(err, 0); 683 | #endif // HAVE_INSPECTOR 684 | 685 | // TODO(addaleax): NODE_SHARED_MODE does not really make sense here. 686 | #ifndef NODE_SHARED_MODE 687 | // Restore signal dispositions, the parent process may have changed them. 688 | struct sigaction act; 689 | memset(&act, 0, sizeof(act)); 690 | 691 | // The hard-coded upper limit is because NSIG is not very reliable; on Linux, 692 | // it evaluates to 32, 34 or 64, depending on whether RT signals are enabled. 693 | // Counting up to SIGRTMIN doesn't work for the same reason. 694 | for (unsigned nr = 1; nr < kMaxSignal; nr += 1) { 695 | if (nr == SIGKILL || nr == SIGSTOP) 696 | continue; 697 | act.sa_handler = (nr == SIGPIPE || nr == SIGXFSZ) ? SIG_IGN : SIG_DFL; 698 | int ret = sigaction(nr, &act, nullptr); 699 | assert(ret == 0); 700 | } 701 | #endif // !NODE_SHARED_MODE 702 | 703 | // Record the state of the stdio file descriptors so we can restore it 704 | // on exit. Needs to happen before installing signal handlers because 705 | // they make use of that information. 706 | for (auto& s : stdio) { 707 | const int fd = &s - stdio; 708 | int err; 709 | 710 | do 711 | s.flags = fcntl(fd, F_GETFL); 712 | while (s.flags == -1 && errno == EINTR); // NOLINT 713 | assert(s.flags != -1); 714 | 715 | if (uv_guess_handle(fd) != UV_TTY) continue; 716 | s.isatty = true; 717 | 718 | do 719 | err = tcgetattr(fd, &s.termios); 720 | while (err == -1 && errno == EINTR); // NOLINT 721 | assert(err == 0); 722 | } 723 | 724 | RegisterSignalHandler(SIGINT, SignalExit, true); 725 | RegisterSignalHandler(SIGTERM, SignalExit, true); 726 | 727 | #if NODE_USE_V8_WASM_TRAP_HANDLER 728 | #if defined(_WIN32) 729 | { 730 | constexpr ULONG first = TRUE; 731 | old_vectored_exception_handler = 732 | AddVectoredExceptionHandler(first, TrapWebAssemblyOrContinue); 733 | } 734 | #else 735 | // Tell V8 to disable emitting WebAssembly 736 | // memory bounds checks. This means that we have 737 | // to catch the SIGSEGV in TrapWebAssemblyOrContinue 738 | // and pass the signal context to V8. 739 | { 740 | struct sigaction sa; 741 | memset(&sa, 0, sizeof(sa)); 742 | sa.sa_sigaction = TrapWebAssemblyOrContinue; 743 | sa.sa_flags = SA_SIGINFO; 744 | int ret = sigaction(SIGSEGV, &sa, nullptr); 745 | assert(ret == 0); 746 | } 747 | #endif // defined(_WIN32) 748 | V8::EnableWebAssemblyTrapHandler(false); 749 | #endif // NODE_USE_V8_WASM_TRAP_HANDLER 750 | 751 | // Raise the open file descriptor limit. 752 | struct rlimit lim; 753 | if (getrlimit(RLIMIT_NOFILE, &lim) == 0 && lim.rlim_cur != lim.rlim_max) { 754 | // Do a binary search for the limit. 755 | rlim_t min = lim.rlim_cur; 756 | rlim_t max = 1 << 20; 757 | // But if there's a defined upper bound, don't search, just set it. 758 | if (lim.rlim_max != RLIM_INFINITY) { 759 | min = lim.rlim_max; 760 | max = lim.rlim_max; 761 | } 762 | do { 763 | lim.rlim_cur = min + (max - min) / 2; 764 | if (setrlimit(RLIMIT_NOFILE, &lim)) { 765 | max = lim.rlim_cur; 766 | } else { 767 | min = lim.rlim_cur; 768 | } 769 | } while (min + 1 < max); 770 | } 771 | #endif // __POSIX__ 772 | #ifdef _WIN32 773 | for (int fd = 0; fd <= 2; ++fd) { 774 | auto handle = reinterpret_cast(_get_osfhandle(fd)); 775 | if (handle == INVALID_HANDLE_VALUE || 776 | GetFileType(handle) == FILE_TYPE_UNKNOWN) { 777 | // Ignore _close result. If it fails or not depends on used Windows 778 | // version. We will just check _open result. 779 | _close(fd); 780 | if (fd != _open("nul", _O_RDWR)) 781 | assert(0); 782 | } 783 | } 784 | #endif // _WIN32 785 | } 786 | 787 | 788 | // Safe to call more than once and from signal handlers. 789 | void ResetStdio() { 790 | uv_tty_reset_mode(); 791 | #ifdef __POSIX__ 792 | for (auto& s : stdio) { 793 | const int fd = &s - stdio; 794 | 795 | struct stat tmp; 796 | if (-1 == fstat(fd, &tmp)) { 797 | assert(errno == EBADF); // Program closed file descriptor. 798 | continue; 799 | } 800 | 801 | bool is_same_file = 802 | (s.stat.st_dev == tmp.st_dev && s.stat.st_ino == tmp.st_ino); 803 | if (!is_same_file) continue; // Program reopened file descriptor. 804 | 805 | int flags; 806 | do 807 | flags = fcntl(fd, F_GETFL); 808 | while (flags == -1 && errno == EINTR); // NOLINT 809 | assert(flags != -1); 810 | 811 | // Restore the O_NONBLOCK flag if it changed. 812 | if (O_NONBLOCK & (flags ^ s.flags)) { 813 | flags &= ~O_NONBLOCK; 814 | flags |= s.flags & O_NONBLOCK; 815 | 816 | int err; 817 | do 818 | err = fcntl(fd, F_SETFL, flags); 819 | while (err == -1 && errno == EINTR); // NOLINT 820 | assert(err != -1); 821 | } 822 | 823 | if (s.isatty) { 824 | sigset_t sa; 825 | int err, ret; 826 | 827 | // We might be a background job that doesn't own the TTY so block SIGTTOU 828 | // before making the tcsetattr() call, otherwise that signal suspends us. 829 | sigemptyset(&sa); 830 | sigaddset(&sa, SIGTTOU); 831 | 832 | ret = pthread_sigmask(SIG_BLOCK, &sa, nullptr); 833 | assert(ret == 0); 834 | do 835 | err = tcsetattr(fd, TCSANOW, &s.termios); 836 | while (err == -1 && errno == EINTR); // NOLINT 837 | ret = pthread_sigmask(SIG_UNBLOCK, &sa, nullptr); 838 | assert(ret == 0); 839 | 840 | // Normally we expect err == 0. But if macOS App Sandbox is enabled, 841 | // tcsetattr will fail with err == -1 and errno == EPERM. 842 | if (err != 0) { 843 | assert(err == -1 && errno == EPERM); 844 | } 845 | } 846 | } 847 | #endif // __POSIX__ 848 | } 849 | 850 | static void InitializeOpenSSL() { 851 | #if HAVE_OPENSSL && !defined(OPENSSL_IS_BORINGSSL) 852 | // In the case of FIPS builds we should make sure 853 | // the random source is properly initialized first. 854 | #if OPENSSL_VERSION_MAJOR >= 3 855 | // Use OPENSSL_CONF environment variable is set. 856 | const char* conf_file = getenv("OPENSSL_CONF"); 857 | 858 | OPENSSL_INIT_SETTINGS* settings = OPENSSL_INIT_new(); 859 | OPENSSL_INIT_set_config_filename(settings, conf_file); 860 | OPENSSL_INIT_set_config_appname(settings, "openssl_conf"); 861 | OPENSSL_INIT_set_config_file_flags(settings, 862 | CONF_MFLAGS_IGNORE_MISSING_FILE); 863 | 864 | OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CONFIG, settings); 865 | OPENSSL_INIT_free(settings); 866 | 867 | if (ERR_peek_error() != 0) { 868 | fprintf(stderr, "OpenSSL configuration error:\n"); 869 | ERR_print_errors_fp(stderr); 870 | exit(1); 871 | } 872 | #else // OPENSSL_VERSION_MAJOR < 3 873 | if (FIPS_mode()) { 874 | OPENSSL_init(); 875 | } 876 | #endif 877 | V8::SetEntropySource(boxednode::EntropySource); 878 | #endif 879 | } 880 | 881 | void InitializeOncePerProcess() { 882 | atexit(ResetStdio); 883 | PlatformInit(); 884 | InitializeOpenSSL(); 885 | } 886 | 887 | void TearDownOncePerProcess() { 888 | #if NODE_USE_V8_WASM_TRAP_HANDLER && defined(_WIN32) 889 | RemoveVectoredExceptionHandler(old_vectored_exception_handler); 890 | #endif 891 | } 892 | 893 | } // namespace boxednode 894 | 895 | #endif // USE_OWN_LEGACY_PROCESS_INITIALIZATION 896 | 897 | namespace boxednode { 898 | REPLACE_WITH_MAIN_SCRIPT_SOURCE_GETTER 899 | } 900 | -------------------------------------------------------------------------------- /src/executable-metadata.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import path from 'path'; 3 | import semver from 'semver'; 4 | 5 | export type ExecutableMetadata = { 6 | name?: string, // exe rc InternalName and ProductName 7 | description?: string, // exe rc FileDescription 8 | version?: string, // exe rc FileVersion and ProductVersion 9 | manufacturer?: string, // exe rc CompanyName 10 | copyright?: string, // exe rc LegalCopyright 11 | icon?: string // ico file path 12 | }; 13 | 14 | export async function generateRCFile ( 15 | resourcePath: string, 16 | executableFilename: string, 17 | data: ExecutableMetadata = {}): Promise { 18 | const S = JSON.stringify; 19 | let result = '#include "winresrc.h"\n'; 20 | if (data.icon) { 21 | await fs.copyFile(data.icon, path.join(resourcePath, 'boxednode.ico')); 22 | result += '1 ICON boxednode.ico\n'; 23 | } 24 | const version = semver.parse(data.version || '0.0.0'); 25 | result += ` 26 | 27 | // Version resource 28 | VS_VERSION_INFO VERSIONINFO 29 | FILEVERSION ${version.major},${version.minor},${version.patch},0 30 | PRODUCTVERSION ${version.major},${version.minor},${version.patch},0 31 | FILEFLAGSMASK 0x3fL 32 | #ifdef _DEBUG 33 | FILEFLAGS VS_FF_DEBUG 34 | #else 35 | # ifdef NODE_VERSION_IS_RELEASE 36 | FILEFLAGS 0x0L 37 | # else 38 | FILEFLAGS VS_FF_PRERELEASE 39 | # endif 40 | #endif 41 | 42 | FILEOS VOS_NT_WINDOWS32 43 | FILETYPE VFT_APP 44 | FILESUBTYPE 0x0L 45 | BEGIN 46 | BLOCK "StringFileInfo" 47 | BEGIN 48 | BLOCK "040904b0" 49 | BEGIN 50 | VALUE "FileVersion", ${S(version.version)} 51 | VALUE "ProductVersion", ${S(version.version)}`; 52 | if (data.manufacturer) { 53 | result += ` 54 | VALUE "CompanyName", ${S(data.manufacturer)}`; 55 | } 56 | if (data.name) { 57 | result += ` 58 | VALUE "InternalName", ${S(data.name)} 59 | VALUE "ProductName", ${S(data.name)}`; 60 | } 61 | if (data.copyright) { 62 | result += ` 63 | VALUE "LegalCopyright", ${S(data.copyright)}`; 64 | } 65 | if (data.description) { 66 | result += ` 67 | VALUE "FileDescription", ${S(data.description)}`; 68 | } 69 | result += ` 70 | VALUE "OriginalFilename", ${S(path.basename(executableFilename))} 71 | END 72 | END 73 | BLOCK "VarFileInfo" 74 | BEGIN 75 | VALUE "Translation", 0x409, 1200 76 | END 77 | END 78 | `; 79 | return result; 80 | } 81 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import { Logger } from './logger'; 3 | import crypto from 'crypto'; 4 | import childProcess from 'child_process'; 5 | import { promisify } from 'util'; 6 | import tar from 'tar'; 7 | import stream from 'stream'; 8 | import zlib from 'zlib'; 9 | import { once } from 'events'; 10 | 11 | export const pipeline = promisify(stream.pipeline); 12 | 13 | export type ProcessEnv = { [name: string]: string | undefined }; 14 | 15 | export type BuildCommandOptions = { 16 | cwd: string, 17 | logger: Logger, 18 | env: ProcessEnv, 19 | }; 20 | 21 | // Run a build command, e.g. `./configure`, `make`, `vcbuild`, etc. 22 | export async function spawnBuildCommand ( 23 | command: string[], 24 | options: BuildCommandOptions): Promise { 25 | options.logger.stepStarting(`Running ${command.join(' ')}`); 26 | // Fun stuff: Sometime between Node.js 14.15.0 and 14.16.0, 27 | // the case handling of PATH on win32 changed, and the build 28 | // will fail if the env var's case is e.g. Path instead of PATH. 29 | // We normalize to PATH here. 30 | const env = options.env; 31 | if (process.platform === 'win32') { 32 | const PATH = env.PATH ?? env.Path ?? env.path; 33 | delete env.PATH; 34 | delete env.Path; 35 | delete env.path; 36 | env.PATH = PATH; 37 | } 38 | // We're not using childProcess.exec* because we do want to pass the output 39 | // through here and not handle it ourselves. 40 | const proc = childProcess.spawn(command[0], command.slice(1), { 41 | stdio: 'inherit', 42 | ...options, 43 | env 44 | }); 45 | const [code] = await once(proc, 'exit'); 46 | if (code !== 0) { 47 | throw new Error(`Command failed: ${command.join(' ')} (code ${code})`); 48 | } 49 | options.logger.stepCompleted(); 50 | } 51 | 52 | export async function copyRecursive (sourceDir: string, targetDir: string): Promise { 53 | await fs.mkdir(targetDir, { recursive: true }); 54 | await pipeline( 55 | tar.c({ 56 | cwd: sourceDir, 57 | gzip: false 58 | }, ['./']), 59 | tar.x({ 60 | cwd: targetDir 61 | }) 62 | ); 63 | } 64 | 65 | export function objhash (value: unknown): string { 66 | return crypto.createHash('sha256') 67 | .update(JSON.stringify(value)) 68 | .digest('hex') 69 | .slice(0, 32); 70 | } 71 | 72 | export function npm (): string[] { 73 | if (process.env.npm_execpath) { 74 | return [process.execPath, process.env.npm_execpath]; 75 | } else { 76 | return ['npm']; 77 | } 78 | } 79 | 80 | export function createCppJsStringDefinition (fnName: string, source: string): string { 81 | if (!source.length) { 82 | return `Local ${fnName}(Isolate* isolate) { return String::Empty(isolate); }`; 83 | } 84 | 85 | const sourceAsCharCodeArray = new Uint16Array(source.length); 86 | let isAllLatin1 = true; 87 | for (let i = 0; i < source.length; i++) { 88 | const charCode = source.charCodeAt(i); 89 | sourceAsCharCodeArray[i] = charCode; 90 | isAllLatin1 &&= charCode <= 0xFF; 91 | } 92 | 93 | return ` 94 | static const ${isAllLatin1 ? 'uint8_t' : 'uint16_t'} ${fnName}_source_[] = { 95 | ${sourceAsCharCodeArray} 96 | }; 97 | static_assert( 98 | ${sourceAsCharCodeArray.length} <= v8::String::kMaxLength, 99 | "main script source exceeds max string length"); 100 | Local ${fnName}(Isolate* isolate) { 101 | return v8::String::NewFrom${isAllLatin1 ? 'One' : 'Two'}Byte( 102 | isolate, 103 | ${fnName}_source_, 104 | v8::NewStringType::kNormal, 105 | ${sourceAsCharCodeArray.length}).ToLocalChecked(); 106 | } 107 | `; 108 | } 109 | 110 | export async function createUncompressedBlobDefinition (fnName: string, source: Uint8Array): Promise { 111 | return ` 112 | static const uint8_t ${fnName}_source_[] = { 113 | ${Uint8Array.prototype.toString.call(source) || '0'} 114 | }; 115 | 116 | #ifdef NODE_VERSION_SUPPORTS_STRING_VIEW_SNAPSHOT 117 | std::optional ${fnName}SV() { 118 | return { 119 | { 120 | reinterpret_cast(&${fnName}_source_[0]), 121 | ${source.length} 122 | } 123 | }; 124 | } 125 | #endif 126 | 127 | std::vector ${fnName}Vector() { 128 | return std::vector( 129 | reinterpret_cast(&${fnName}_source_[0]), 130 | reinterpret_cast(&${fnName}_source_[${source.length}])); 131 | } 132 | 133 | ${blobTypedArrayAccessors(fnName, source.length)}`; 134 | } 135 | 136 | export async function createCompressedBlobDefinition (fnName: string, source: Uint8Array): Promise { 137 | const compressed = await promisify(zlib.brotliCompress)(source, { 138 | params: { 139 | [zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY, 140 | [zlib.constants.BROTLI_PARAM_SIZE_HINT]: source.length 141 | } 142 | }); 143 | return ` 144 | static const uint8_t ${fnName}_source_[] = { 145 | ${Uint8Array.prototype.toString.call(compressed) || '0'} 146 | }; 147 | 148 | #if __cplusplus >= 201703L 149 | [[maybe_unused]] 150 | #endif 151 | static void ${fnName}_Read(char* dst) { 152 | size_t decoded_size = ${source.length}; 153 | const auto result = BrotliDecoderDecompress( 154 | ${compressed.length}, 155 | ${fnName}_source_, 156 | &decoded_size, 157 | reinterpret_cast(&dst[0])); 158 | assert(result == BROTLI_DECODER_RESULT_SUCCESS); 159 | assert(decoded_size == ${source.length}); 160 | } 161 | 162 | std::vector ${fnName}Vector() { 163 | ${source.length === 0 ? 'return {};' : ` 164 | std::vector dst(${source.length}); 165 | ${fnName}_Read(&dst[0]); 166 | return dst;`} 167 | } 168 | 169 | #ifdef NODE_VERSION_SUPPORTS_STRING_VIEW_SNAPSHOT 170 | std::optional ${fnName}SV() { 171 | return {}; 172 | } 173 | #endif 174 | 175 | ${blobTypedArrayAccessors(fnName, source.length)} 176 | `; 177 | } 178 | 179 | function blobTypedArrayAccessors (fnName: string, sourceLength: number): string { 180 | return ` 181 | std::shared_ptr ${fnName}BackingStore() { 182 | std::vector* str = new std::vector(std::move(${fnName}Vector())); 183 | return v8::SharedArrayBuffer::NewBackingStore( 184 | &str->front(), 185 | str->size(), 186 | [](void*, size_t, void* deleter_data) { 187 | delete static_cast*>(deleter_data); 188 | }, 189 | static_cast(str)); 190 | } 191 | 192 | v8::Local ${fnName}Buffer(v8::Isolate* isolate) { 193 | ${sourceLength === 0 ? ` 194 | auto array_buffer = v8::SharedArrayBuffer::New(isolate, 0); 195 | ` : ` 196 | auto array_buffer = v8::SharedArrayBuffer::New(isolate, ${fnName}BackingStore()); 197 | `} 198 | return v8::Uint8Array::New(array_buffer, 0, array_buffer->ByteLength()); 199 | } 200 | `; 201 | } 202 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { Logger, LoggerImpl } from './logger'; 3 | import fetch from 'node-fetch'; 4 | import tar from 'tar'; 5 | import path from 'path'; 6 | import zlib from 'zlib'; 7 | import os from 'os'; 8 | import rimraf from 'rimraf'; 9 | import crypto from 'crypto'; 10 | import { promisify } from 'util'; 11 | import { promises as fs, createReadStream, createWriteStream } from 'fs'; 12 | import { AddonConfig, loadGYPConfig, storeGYPConfig, modifyAddonGyp } from './native-addons'; 13 | import { ExecutableMetadata, generateRCFile } from './executable-metadata'; 14 | import { spawnBuildCommand, ProcessEnv, pipeline, createCppJsStringDefinition, createCompressedBlobDefinition, createUncompressedBlobDefinition } from './helpers'; 15 | import { Readable } from 'stream'; 16 | import nv from '@pkgjs/nv'; 17 | import { fileURLToPath, URL } from 'url'; 18 | import { execFile } from 'child_process'; 19 | import { once } from 'events'; 20 | 21 | // Download and unpack a tarball containing the code for a specific Node.js version. 22 | async function getNodeSourceForVersion (range: string, dir: string, logger: Logger, retries = 2): Promise { 23 | logger.stepStarting(`Looking for Node.js version matching ${JSON.stringify(range)}`); 24 | 25 | let inputIsFileUrl = false; 26 | try { 27 | inputIsFileUrl = new URL(range).protocol === 'file:'; 28 | } catch { /* not a valid URL */ } 29 | 30 | if (inputIsFileUrl) { 31 | logger.stepStarting(`Extracting tarball from ${range} to ${dir}`); 32 | await fs.mkdir(dir, { recursive: true }); 33 | await pipeline( 34 | createReadStream(fileURLToPath(range)), 35 | zlib.createGunzip(), 36 | tar.x({ 37 | cwd: dir 38 | }) 39 | ); 40 | logger.stepCompleted(); 41 | const filesInDir = await fs.readdir(dir, { withFileTypes: true }); 42 | const dirsInDir = filesInDir.filter(f => f.isDirectory()); 43 | if (dirsInDir.length !== 1) { 44 | throw new Error('Node.js tarballs should contain exactly one directory'); 45 | } 46 | return path.join(dir, dirsInDir[0].name); 47 | } 48 | 49 | let releaseBaseUrl: string; 50 | let version: string; 51 | if (range.match(/-nightly\d+/)) { 52 | version = range.startsWith('v') ? range : `v${range}`; 53 | releaseBaseUrl = `https://nodejs.org/download/nightly/${version}`; 54 | } else { 55 | const ver = (await nv(range)).pop(); 56 | if (!ver) { 57 | throw new Error(`No node version found for ${range}`); 58 | } 59 | version = `v${ver.version}`; 60 | 61 | releaseBaseUrl = `https://nodejs.org/download/release/${version}`; 62 | } 63 | 64 | const tarballName = `node-${version}.tar.gz`; 65 | const cachedTarballPath = path.join(dir, tarballName); 66 | 67 | let hasCachedTarball = false; 68 | try { 69 | hasCachedTarball = (await fs.stat(cachedTarballPath)).size > 0; 70 | } catch {} 71 | if (hasCachedTarball) { 72 | const shaSumsUrl = `${releaseBaseUrl}/SHASUMS256.txt`; 73 | logger.stepStarting(`Verifying existing tarball via ${shaSumsUrl}`); 74 | const [expectedSha, realSha] = await Promise.all([ 75 | (async () => { 76 | try { 77 | const shaSums = await fetch(shaSumsUrl); 78 | if (!shaSums.ok) return; 79 | const text = await shaSums.text(); 80 | for (const line of text.split('\n')) { 81 | if (line.trim().endsWith(tarballName)) { 82 | return line.match(/^([0-9a-fA-F]+)\b/)[0]; 83 | } 84 | } 85 | } catch {} 86 | })(), 87 | (async () => { 88 | const hash = crypto.createHash('sha256'); 89 | await pipeline(createReadStream(cachedTarballPath), hash); 90 | return hash.digest('hex'); 91 | })() 92 | ]); 93 | if (expectedSha === realSha) { 94 | logger.stepStarting('Unpacking existing tarball'); 95 | } else { 96 | logger.stepFailed(new Error( 97 | `SHA256 mismatch: got ${realSha}, expected ${expectedSha}`)); 98 | hasCachedTarball = false; 99 | } 100 | } 101 | 102 | let tarballStream: Readable; 103 | let tarballWritePromise: Promise | undefined; 104 | if (hasCachedTarball) { 105 | tarballStream = createReadStream(cachedTarballPath); 106 | } else { 107 | const url = `${releaseBaseUrl}/${tarballName}`; 108 | logger.stepStarting(`Downloading from ${url}`); 109 | 110 | const tarball = await fetch(url); 111 | 112 | if (!tarball.ok) { 113 | throw new Error(`Could not download Node.js source tarball: ${tarball.statusText}`); 114 | } 115 | 116 | logger.stepStarting(`Unpacking tarball to ${dir}`); 117 | await fs.mkdir(dir, { recursive: true }); 118 | 119 | const contentLength = +tarball.headers.get('Content-Length'); 120 | if (contentLength) { 121 | logger.startProgress(contentLength); 122 | let downloaded = 0; 123 | tarball.body.on('data', (chunk) => { 124 | downloaded += chunk.length; 125 | logger.doProgress(downloaded); 126 | }); 127 | } 128 | 129 | tarballStream = tarball.body; 130 | // It is important that this happens in the same tick as the streaming 131 | // unpack below in order not to lose any data. 132 | tarballWritePromise = 133 | pipeline(tarball.body, createWriteStream(cachedTarballPath)); 134 | } 135 | 136 | // Streaming unpack. This will create the directory `${dir}/node-${version}` 137 | // with the Node.js source tarball contents in it. 138 | try { 139 | await Promise.race([ 140 | Promise.all([ 141 | pipeline( 142 | tarballStream, 143 | zlib.createGunzip(), 144 | tar.x({ 145 | cwd: dir 146 | }) 147 | ), 148 | tarballWritePromise 149 | ]), 150 | // Unclear why this can happen, but it looks in CI like it does 151 | once(process, 'beforeExit').then(() => { 152 | throw new Error('premature exit from the event loop'); 153 | }) 154 | ]); 155 | } catch (err) { 156 | if (retries > 0) { 157 | logger.stepFailed(err); 158 | logger.stepStarting('Re-trying'); 159 | return await getNodeSourceForVersion(range, dir, logger, retries - 1); 160 | } 161 | throw err; 162 | } 163 | 164 | logger.stepCompleted(); 165 | 166 | return path.join(dir, `node-${version}`); 167 | } 168 | 169 | async function getNodeVersionFromSourceDirectory (dir: string): Promise<[number, number, number]> { 170 | const versionFile = await fs.readFile(path.join(dir, 'src', 'node_version.h'), 'utf8'); 171 | 172 | const major = +versionFile.match(/^#define\s+NODE_MAJOR_VERSION\s+(?\d+)\s*$/m)?.groups?.version; 173 | const minor = +versionFile.match(/^#define\s+NODE_MINOR_VERSION\s+(?\d+)\s*$/m)?.groups?.version; 174 | const patch = +versionFile.match(/^#define\s+NODE_PATCH_VERSION\s+(?\d+)\s*$/m)?.groups?.version; 175 | return [major, minor, patch]; 176 | } 177 | 178 | // Compile a Node.js build in a given directory from source 179 | async function compileNode ( 180 | sourcePath: string, 181 | linkedJSModules: string[], 182 | buildArgs: string[], 183 | makeArgs: string[], 184 | env: ProcessEnv, 185 | logger: Logger): Promise { 186 | logger.stepStarting('Compiling Node.js from source'); 187 | const cpus = os.cpus().length; 188 | const options = { 189 | cwd: sourcePath, 190 | logger: logger, 191 | env: env 192 | }; 193 | 194 | // Node.js 19.4.0 is currently the minimum version that has https://github.com/nodejs/node/pull/45887. 195 | // We want to disable the shared-ro-heap flag since it would require 196 | // all snapshots used by Node.js to be equal, something that we don't 197 | // want to or need to guarantee as embedders. 198 | const nodeVersion = await getNodeVersionFromSourceDirectory(sourcePath); 199 | if (nodeVersion[0] > 19 || (nodeVersion[0] === 19 && nodeVersion[1] >= 4)) { 200 | if (process.platform !== 'win32') { 201 | buildArgs = ['--disable-shared-readonly-heap', ...buildArgs]; 202 | } else { 203 | buildArgs = ['no-shared-roheap', ...buildArgs]; 204 | } 205 | } 206 | 207 | if (process.platform !== 'win32') { 208 | const configure: string[] = ['./configure', ...buildArgs]; 209 | for (const module of linkedJSModules) { 210 | configure.push('--link-module', module); 211 | } 212 | await spawnBuildCommand(configure, options); 213 | if (configure.includes('--fully-static') || configure.includes('--partly-static')) { 214 | // https://github.com/nodejs/node/issues/41497#issuecomment-1013137433 215 | for (const file of [ 216 | 'out/tools/v8_gypfiles/gen-regexp-special-case.target.mk', 217 | 'out/test_crypto_engine.target.mk' 218 | ]) { 219 | const target = path.join(sourcePath, file); 220 | try { 221 | await fs.stat(target); 222 | } catch { 223 | continue; 224 | } 225 | let source = await fs.readFile(target, 'utf8'); 226 | source = source.replace(/-static/g, ''); 227 | await fs.writeFile(target, source); 228 | } 229 | } 230 | 231 | const make = ['make', ...makeArgs]; 232 | if (!make.some((arg) => /^-j/.test(arg))) { make.push(`-j${cpus}`); } 233 | 234 | if (!make.some((arg) => /^V=/.test(arg))) { make.push('V='); } 235 | 236 | await spawnBuildCommand(make, options); 237 | 238 | return path.join(sourcePath, 'out', 'Release', 'node'); 239 | } else { 240 | // On Windows, running vcbuild multiple times may result in errors 241 | // when the source data changes in between runs. 242 | await fs.rm(path.join(sourcePath, 'out', 'Release'), { 243 | recursive: true, 244 | force: true 245 | }); 246 | 247 | // These defaults got things to work locally. We only include them if no 248 | // conflicting arguments have been passed manually. 249 | const vcbuildArgs: string[] = [...buildArgs, ...makeArgs, 'projgen']; 250 | if (!vcbuildArgs.includes('debug') && !vcbuildArgs.includes('release')) { vcbuildArgs.push('release'); } 251 | if (!vcbuildArgs.some((arg) => /^vs/.test(arg))) { vcbuildArgs.push('vs2019'); } 252 | 253 | for (const module of linkedJSModules) { 254 | vcbuildArgs.push('link-module', module); 255 | } 256 | await spawnBuildCommand(['cmd', '/c', '.\\vcbuild.bat', ...vcbuildArgs], options); 257 | 258 | return path.join(sourcePath, 'Release', 'node.exe'); 259 | } 260 | } 261 | 262 | type CompilationOptions = { 263 | nodeVersionRange: string, 264 | tmpdir?: string, 265 | sourceFile: string, 266 | targetFile: string, 267 | configureArgs?: string[], 268 | makeArgs?: string[], 269 | logger?: Logger, 270 | clean?: boolean, 271 | env?: ProcessEnv, 272 | namespace?: string, 273 | addons?: AddonConfig[], 274 | enableBindingsPatch?: boolean, 275 | useLegacyDefaultUvLoop?: boolean; 276 | useCodeCache?: boolean, 277 | useNodeSnapshot?: boolean, 278 | compressBlobs?: boolean, 279 | nodeSnapshotConfigFlags?: string[], // e.g. 'WithoutCodeCache' 280 | executableMetadata?: ExecutableMetadata, 281 | preCompileHook?: (nodeSourceTree: string, options: CompilationOptions) => void | Promise 282 | } 283 | 284 | async function compileJSFileAsBinaryImpl (options: CompilationOptions, logger: Logger): Promise { 285 | if (!options.sourceFile.endsWith('.js')) { 286 | throw new Error(`Only .js files can be compiled (got: ${options.sourceFile})`); 287 | } 288 | await fs.access(options.sourceFile); 289 | 290 | // We'll put the source file in a namespaced path in the target directory. 291 | // For example, if the file name is `myproject.js`, then it will be available 292 | // for importing as `require('myproject/myproject')`. 293 | const namespace = options.namespace || path.basename(options.sourceFile, '.js'); 294 | if (!options.tmpdir) { 295 | // We're not adding random data here, so that the paths can be part of a 296 | // compile caching mechanism like sccache. 297 | options.tmpdir = path.join(os.tmpdir(), 'boxednode', namespace); 298 | } 299 | 300 | const nodeSourcePath = await getNodeSourceForVersion( 301 | options.nodeVersionRange, options.tmpdir, logger); 302 | const nodeVersion = await getNodeVersionFromSourceDirectory(nodeSourcePath); 303 | 304 | const requireMappings: [RegExp, string][] = []; 305 | const extraJSSourceFiles: string[] = []; 306 | const enableBindingsPatch = options.enableBindingsPatch ?? options.addons?.length > 0; 307 | 308 | const jsMainSource = await fs.readFile(options.sourceFile, 'utf8'); 309 | const registerFunctions: string[] = []; 310 | 311 | // We use the official embedder API for stability, which is available in all 312 | // supported versions of Node.js. 313 | { 314 | const extraGypDependencies: string[] = []; 315 | for (const addon of (options.addons || [])) { 316 | const addonResult = await modifyAddonGyp( 317 | addon, nodeSourcePath, options.env || process.env, logger); 318 | for (const { linkedModuleName, targetName, registerFunction } of addonResult) { 319 | requireMappings.push([addon.requireRegexp, linkedModuleName]); 320 | extraGypDependencies.push(targetName); 321 | registerFunctions.push(registerFunction); 322 | } 323 | } 324 | 325 | logger.stepStarting('Finalizing linked addons processing'); 326 | const nodeGypPath = path.join(nodeSourcePath, 'node.gyp'); 327 | const nodeGyp = await loadGYPConfig(nodeGypPath); 328 | const mainTarget = nodeGyp.targets.find( 329 | (target) => ['<(node_core_target_name)', 'node'].includes(target.target_name)); 330 | mainTarget.dependencies = [...(mainTarget.dependencies || []), ...extraGypDependencies]; 331 | await storeGYPConfig(nodeGypPath, nodeGyp); 332 | 333 | for (const header of ['node.h', 'node_api.h']) { 334 | const source = ( 335 | await fs.readFile(path.join(nodeSourcePath, 'src', header), 'utf8') + 336 | await fs.readFile(path.join(__dirname, '..', 'resources', `add-${header}`), 'utf8') 337 | ); 338 | await fs.writeFile(path.join(nodeSourcePath, 'src', header), source); 339 | } 340 | logger.stepCompleted(); 341 | } 342 | 343 | logger.stepStarting('Inserting custom code into Node.js source'); 344 | let entryPointTrampolineSource = await fs.readFile( 345 | path.join(__dirname, '..', 'resources', 'entry-point-trampoline.js'), 'utf8'); 346 | entryPointTrampolineSource = entryPointTrampolineSource.replace( 347 | /\bREPLACE_WITH_BOXEDNODE_CONFIG\b/g, 348 | JSON.stringify({ 349 | requireMappings: requireMappings.map(([re, linked]) => [re.source, re.flags, linked]), 350 | enableBindingsPatch 351 | })); 352 | 353 | /** 354 | * Since Node 20.x, external source code linked from `lib` directory started 355 | * failing the Node.js build process because of the file being linked multiple 356 | * times which is why we do not link the external files anymore from `lib` 357 | * directory and instead from a different directory, `lib-boxednode`. This 358 | * however does not work for any node version < 20 which is why we are 359 | * conditionally generating the entry point and configure params here based on 360 | * Node version. 361 | */ 362 | const { customCodeSource, customCodeConfigureParam, customCodeEntryPoint } = nodeVersion[0] >= 20 363 | ? { 364 | customCodeSource: path.join(nodeSourcePath, 'lib-boxednode', `${namespace}.js`), 365 | customCodeConfigureParam: `./lib-boxednode/${namespace}.js`, 366 | customCodeEntryPoint: `lib-boxednode/${namespace}` 367 | } : { 368 | customCodeSource: path.join(nodeSourcePath, 'lib', namespace, `${namespace}.js`), 369 | customCodeConfigureParam: `./lib/${namespace}/${namespace}.js`, 370 | customCodeEntryPoint: `${namespace}/${namespace}` 371 | }; 372 | 373 | await fs.mkdir(path.dirname(customCodeSource), { recursive: true }); 374 | await fs.writeFile(customCodeSource, entryPointTrampolineSource); 375 | extraJSSourceFiles.push(customCodeConfigureParam); 376 | logger.stepCompleted(); 377 | 378 | logger.stepStarting('Storing executable metadata'); 379 | const resPath = path.join(nodeSourcePath, 'src', 'res'); 380 | await fs.writeFile( 381 | path.join(resPath, 'node.rc'), 382 | await generateRCFile(resPath, options.targetFile, options.executableMetadata)); 383 | logger.stepCompleted(); 384 | 385 | if (options.preCompileHook) { 386 | logger.stepStarting('Running pre-compile hook'); 387 | await options.preCompileHook(nodeSourcePath, options); 388 | logger.stepCompleted(); 389 | } 390 | 391 | const createBlobDefinition = options.compressBlobs 392 | ? createCompressedBlobDefinition 393 | : createUncompressedBlobDefinition; 394 | 395 | async function writeMainFileAndCompile ({ 396 | codeCacheBlob = new Uint8Array(0), 397 | codeCacheMode = 'ignore', 398 | snapshotBlob = new Uint8Array(0), 399 | snapshotMode = 'ignore' 400 | }: { 401 | codeCacheBlob?: Uint8Array, 402 | codeCacheMode?: 'ignore' | 'generate' | 'consume', 403 | snapshotBlob?: Uint8Array, 404 | snapshotMode?: 'ignore' | 'generate' | 'consume' 405 | } = {}): Promise { 406 | logger.stepStarting('Handling main file source'); 407 | let mainSource = await fs.readFile( 408 | path.join(__dirname, '..', 'resources', 'main-template.cc'), 'utf8'); 409 | mainSource = mainSource.replace(/\bREPLACE_WITH_ENTRY_POINT\b/g, 410 | JSON.stringify(customCodeEntryPoint)); 411 | mainSource = mainSource.replace(/\bREPLACE_DECLARE_LINKED_MODULES\b/g, 412 | registerFunctions.map((fn) => `void ${fn}(const void**,const void**);\n`).join('')); 413 | mainSource = mainSource.replace(/\bREPLACE_DEFINE_LINKED_MODULES\b/g, 414 | registerFunctions.map((fn) => `${fn},`).join('')); 415 | mainSource = mainSource.replace(/\bREPLACE_WITH_MAIN_SCRIPT_SOURCE_GETTER\b/g, 416 | createCppJsStringDefinition('GetBoxednodeMainScriptSource', snapshotMode !== 'consume' ? jsMainSource : '') + '\n' + 417 | await createBlobDefinition('GetBoxednodeCodeCache', codeCacheBlob) + '\n' + 418 | await createBlobDefinition('GetBoxednodeSnapshotBlob', snapshotBlob)); 419 | mainSource = mainSource.replace(/\bBOXEDNODE_CODE_CACHE_MODE\b/g, 420 | JSON.stringify(codeCacheMode)); 421 | if (options.useLegacyDefaultUvLoop) { 422 | mainSource = `#define BOXEDNODE_USE_DEFAULT_UV_LOOP 1\n${mainSource}`; 423 | } 424 | if (snapshotMode === 'generate') { 425 | mainSource = `#define BOXEDNODE_GENERATE_SNAPSHOT 1\n${mainSource}`; 426 | } 427 | if (snapshotMode === 'consume') { 428 | mainSource = `#define BOXEDNODE_CONSUME_SNAPSHOT 1\n${mainSource}`; 429 | } 430 | if (options.nodeSnapshotConfigFlags) { 431 | const flags = [ 432 | '0', 433 | ...options.nodeSnapshotConfigFlags.map(flag => 434 | `static_cast::type>(SnapshotFlags::k${flag})`) 435 | ].join(' | '); 436 | mainSource = `#define BOXEDNODE_SNAPSHOT_CONFIG_FLAGS (static_cast(${flags}))\n${mainSource}`; 437 | } 438 | await fs.writeFile(path.join(nodeSourcePath, 'src', 'node_main.cc'), mainSource); 439 | logger.stepCompleted(); 440 | 441 | return await compileNode( 442 | nodeSourcePath, 443 | extraJSSourceFiles, 444 | options.configureArgs, 445 | options.makeArgs, 446 | options.env || process.env, 447 | logger); 448 | } 449 | 450 | let binaryPath: string; 451 | if (!options.useCodeCache && !options.useNodeSnapshot) { 452 | binaryPath = await writeMainFileAndCompile(); 453 | } else { 454 | binaryPath = await writeMainFileAndCompile({ 455 | codeCacheMode: options.useNodeSnapshot ? 'ignore' : 'generate', 456 | snapshotMode: options.useNodeSnapshot ? 'generate' : 'ignore' 457 | }); 458 | const intermediateFile = path.join(nodeSourcePath, 'intermediate.out'); 459 | logger.stepStarting('Running code cache/snapshot generation'); 460 | await fs.rm(intermediateFile, { force: true }); 461 | await promisify(execFile)(binaryPath, { cwd: nodeSourcePath }); 462 | const result = await fs.readFile(intermediateFile); 463 | if (result.length === 0) { 464 | throw new Error('Empty code cache/snapshot result'); 465 | } 466 | logger.stepCompleted(); 467 | binaryPath = await writeMainFileAndCompile(options.useNodeSnapshot ? { 468 | snapshotBlob: result, 469 | snapshotMode: 'consume' 470 | } : { 471 | codeCacheBlob: result, 472 | codeCacheMode: 'consume' 473 | }); 474 | } 475 | 476 | logger.stepStarting(`Moving resulting binary to ${options.targetFile}`); 477 | await fs.mkdir(path.dirname(options.targetFile), { recursive: true }); 478 | await fs.copyFile(binaryPath, options.targetFile); 479 | logger.stepCompleted(); 480 | 481 | if (options.clean) { 482 | logger.stepStarting('Cleaning temporary directory'); 483 | await promisify(rimraf)(options.tmpdir, { glob: false }); 484 | logger.stepCompleted(); 485 | } 486 | } 487 | 488 | // Allow specifying arguments to make/configure/vcbuild through env vars, 489 | // either as a comma-separated list or as a JSON array 490 | function parseEnvVarArgList (value: string | undefined): string[] { 491 | if (!value) return []; 492 | try { 493 | return JSON.parse(value); 494 | } catch { 495 | return value.split(','); 496 | } 497 | } 498 | 499 | export async function compileJSFileAsBinary (options: Readonly): Promise { 500 | const logger = options.logger || new LoggerImpl(); 501 | 502 | const configureArgs = [...(options.configureArgs || [])]; 503 | configureArgs.push(...parseEnvVarArgList(process.env.BOXEDNODE_CONFIGURE_ARGS)); 504 | 505 | const makeArgs = [...(options.makeArgs || [])]; 506 | makeArgs.push(...parseEnvVarArgList(process.env.BOXEDNODE_MAKE_ARGS)); 507 | 508 | try { 509 | await compileJSFileAsBinaryImpl({ 510 | ...options, 511 | configureArgs, 512 | makeArgs 513 | }, logger); 514 | } catch (err) { 515 | logger.stepFailed(err); 516 | throw err; 517 | } 518 | } 519 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import cliProgress from 'cli-progress'; 3 | 4 | export interface Logger { 5 | stepStarting(info: string): void; 6 | stepCompleted(): void; 7 | stepFailed(err: Error): void; 8 | startProgress(maximum: number): void; 9 | doProgress(current: number): void; 10 | } 11 | 12 | export class LoggerImpl implements Logger { 13 | private currentStep = ''; 14 | private cliProgress : cliProgress.SingleBar | null = null; 15 | 16 | stepStarting (info: string): void { 17 | if (this.currentStep) { 18 | this.stepCompleted(); 19 | } 20 | this.currentStep = info; 21 | console.warn(`${chalk.yellow('→')} ${info} ...`); 22 | } 23 | 24 | _stepDone (): void { 25 | this.currentStep = ''; 26 | if (this.cliProgress) { 27 | this.cliProgress.stop(); 28 | this.cliProgress = null; 29 | } 30 | } 31 | 32 | stepCompleted (): void { 33 | const doneText = this.currentStep; 34 | this._stepDone(); 35 | console.warn(chalk.green(` ✓ Completed: ${doneText}`)); 36 | } 37 | 38 | stepFailed (err: Error): void { 39 | this._stepDone(); 40 | console.warn(chalk.red(` ✖ Failed: ${err.message}`)); 41 | } 42 | 43 | startProgress (maximum: number): void { 44 | this.cliProgress = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic); 45 | this.cliProgress.start(maximum, 0); 46 | } 47 | 48 | doProgress (current: number): void { 49 | this.cliProgress.update(current); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/native-addons.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable dot-notation */ 2 | import { randomBytes } from 'crypto'; 3 | import { promises as fs } from 'fs'; 4 | import { parse } from 'gyp-parser'; 5 | import path from 'path'; 6 | import pkgUp from 'pkg-up'; 7 | import { Logger } from './logger'; 8 | import { copyRecursive, ProcessEnv, objhash, spawnBuildCommand, npm } from './helpers'; 9 | 10 | export type AddonConfig = { 11 | path: string, 12 | requireRegexp: RegExp 13 | } 14 | 15 | export type AddonResult = { 16 | targetName: string, 17 | registerFunction: string, 18 | linkedModuleName: string 19 | } 20 | 21 | type GypConfig = { 22 | targets?: GypConfig[], 23 | ['defines']?: string[], 24 | ['defines!']?: string[], 25 | type?: string, 26 | dependencies?: string[], 27 | ['target_name']?: string, 28 | includes?: string[], 29 | variables?: Record 30 | }; 31 | 32 | export async function loadGYPConfig (filename: string): Promise { 33 | try { 34 | return parse(await fs.readFile(filename, 'utf8')); 35 | } catch (err) { 36 | throw new Error(`Cannot read ${filename}: ${err.message}`); 37 | } 38 | } 39 | 40 | export async function storeGYPConfig (filename: string, config: GypConfig): Promise { 41 | return await fs.writeFile(filename, JSON.stringify(config, null, ' ')); 42 | } 43 | 44 | function turnIntoStaticLibrary (config: GypConfig, addonId: string): AddonResult[] { 45 | if (!Array.isArray(config.targets)) return []; 46 | const result: AddonResult[] = []; 47 | 48 | for (const target of config.targets) { 49 | if (!target.type || target.type === 'loadable_module') { 50 | target.type = 'static_library'; 51 | } 52 | const registerFunction = `boxednode_${target.target_name}_register_${addonId}`; 53 | const linkedModuleName = `boxednode_${target.target_name}_${addonId}`; 54 | const posDefines = new Set(target['defines'] || []); 55 | const negDefines = new Set(target['defines!'] || []); 56 | for (const dontWant of [ 57 | 'USING_UV_SHARED=1', 'USING_V8_SHARED=1', 'BUILDING_NODE_EXTENSION' 58 | ]) { 59 | negDefines.add(dontWant); 60 | posDefines.delete(dontWant); 61 | } 62 | for (const want of [ 63 | 'BUILDING_BOXEDNODE_EXTENSION', 64 | `BOXEDNODE_REGISTER_FUNCTION=${registerFunction}`, 65 | `BOXEDNODE_MODULE_NAME=${linkedModuleName}`, 66 | `NAPI_CPP_CUSTOM_NAMESPACE=i${randomBytes(12).toString('hex')}` 67 | ]) { 68 | negDefines.delete(want); 69 | posDefines.add(want); 70 | } 71 | target['defines'] = [...posDefines]; 72 | target['defines!'] = [...negDefines]; 73 | target['win_delay_load_hook'] = 'false'; 74 | 75 | // Remove node-addon-api gyp dummy, which inserts nothing.c into 76 | // the build tree, which can conflict with other target's nothing.c 77 | // files. 78 | target['dependencies'] = target['dependencies']?.filter( 79 | dep => !/require\s*\(.+node-addon-api.+\)\s*\.\s*gyp/.test(dep)) ?? []; 80 | 81 | result.push({ 82 | targetName: target.target_name, 83 | registerFunction, 84 | linkedModuleName 85 | }); 86 | } 87 | return result; 88 | } 89 | 90 | async function prepForUsageWithNode ( 91 | config: GypConfig, 92 | nodeSourcePath: string): Promise { 93 | const nodeGypDir = path.dirname(await pkgUp({ cwd: require.resolve('node-gyp') })); 94 | (config.includes = config.includes || []).push( 95 | path.join(nodeGypDir, 'addon.gypi') 96 | ); 97 | config.variables = { 98 | ...(config.variables || {}), 99 | 'node_root_dir%': nodeSourcePath, 100 | 'standalone_static_library%': '1', 101 | 'node_engine%': 'v8', 102 | 'node_gyp_dir%': nodeGypDir, 103 | 'library%': 'static_library', 104 | 'visibility%': 'default', 105 | 'module_root_dir%': nodeSourcePath, 106 | // Not what node-gyp is going for, but that's okay. 107 | 'node_lib_file%': 'kernel32.lib', 108 | 'win_delay_load_hook%': 'false' 109 | }; 110 | } 111 | 112 | export async function modifyAddonGyp ( 113 | addon: AddonConfig, 114 | nodeSourcePath: string, 115 | env: ProcessEnv, 116 | logger: Logger): Promise { 117 | logger.stepStarting(`Copying addon at ${addon.path}`); 118 | const addonId = objhash(addon); 119 | const addonPath = path.resolve(nodeSourcePath, 'deps', addonId); 120 | await copyRecursive(addon.path, addonPath); 121 | logger.stepCompleted(); 122 | 123 | await spawnBuildCommand([...npm(), 'install', '--ignore-scripts', '--production'], { 124 | cwd: addonPath, 125 | logger, 126 | env 127 | }); 128 | 129 | logger.stepStarting(`Preparing addon at ${addon.path}`); 130 | const sourceGYP = path.resolve(addonPath, 'binding.gyp'); 131 | const targetGYP = path.resolve(addonPath, '.boxednode.gyp'); 132 | 133 | const config = await loadGYPConfig(sourceGYP); 134 | const addonResult = turnIntoStaticLibrary(config, addonId); 135 | await prepForUsageWithNode(config, nodeSourcePath); 136 | await storeGYPConfig(targetGYP, config); 137 | logger.stepCompleted(); 138 | 139 | const targetGYPRelative = path.relative(nodeSourcePath, targetGYP); 140 | return addonResult.map(({ targetName, registerFunction, linkedModuleName }) => ({ 141 | targetName: `${targetGYPRelative}:${targetName}`, 142 | registerFunction, 143 | linkedModuleName 144 | })); 145 | } 146 | -------------------------------------------------------------------------------- /test/compile-main-template-only.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # script to only build the code in resources/ 4 | # for CodeQL testing 5 | 6 | set -e 7 | set -x 8 | cd "$(dirname $0)/.." 9 | if [ ! -e main-template-build ]; then 10 | mkdir main-template-build 11 | pushd main-template-build 12 | curl -O https://nodejs.org/dist/v20.12.0/node-v20.12.0.tar.xz 13 | tar --strip-components=1 -xf node-*.tar.xz 14 | popd 15 | fi 16 | 17 | g++ \ 18 | -Imain-template-build/src \ 19 | -Imain-template-build/deps/v8/include \ 20 | -Imain-template-build/deps/uv/include \ 21 | -DREPLACE_DECLARE_LINKED_MODULES= \ 22 | -DREPLACE_DEFINE_LINKED_MODULES= \ 23 | -DREPLACE_WITH_ENTRY_POINT='"placeholder"' \ 24 | -DBOXEDNODE_CODE_CACHE_MODE='"placeholder"' \ 25 | -DREPLACE_WITH_MAIN_SCRIPT_SOURCE_GETTER= \ 26 | -fPIC -shared \ 27 | -o main-template-build/out.so \ 28 | -include resources/add-node_api.h \ 29 | -include resources/add-node.h \ 30 | resources/main-template.cc 31 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import { compileJSFileAsBinary } from '..'; 2 | import path from 'path'; 3 | import os from 'os'; 4 | import assert from 'assert'; 5 | import childProcess from 'child_process'; 6 | import semver from 'semver'; 7 | import { promisify } from 'util'; 8 | import pkgUp from 'pkg-up'; 9 | import { promises as fs } from 'fs'; 10 | 11 | const execFile = promisify(childProcess.execFile); 12 | const exeSuffix = process.platform === 'win32' ? '.exe' : ''; 13 | 14 | describe('basic functionality', () => { 15 | // Test the currently running Node.js version. Other versions can be checked 16 | // manually that way, or through the CI matrix. 17 | const version = process.env.TEST_NODE_VERSION || process.version.slice(1).replace(/-.*$/, ''); 18 | 19 | describe(`On Node v${version}`, function () { 20 | it('works in a simple case', async function () { 21 | this.timeout(2 * 60 * 60 * 1000); // 2 hours 22 | await compileJSFileAsBinary({ 23 | nodeVersionRange: version, 24 | sourceFile: path.resolve(__dirname, 'resources/example.js'), 25 | targetFile: path.resolve(__dirname, `resources/example${exeSuffix}`) 26 | }); 27 | 28 | { 29 | const { stdout } = await execFile( 30 | path.resolve(__dirname, `resources/example${exeSuffix}`), [], 31 | { encoding: 'utf8' }); 32 | assert.strictEqual(stdout, 'Hello world!\n'); 33 | } 34 | 35 | { 36 | const { stdout } = await execFile( 37 | path.resolve(__dirname, `resources/example${exeSuffix}`), ['42'], 38 | { encoding: 'utf8' }); 39 | assert.strictEqual(stdout, '42\n'); 40 | } 41 | 42 | { 43 | const { stdout } = await execFile( 44 | path.resolve(__dirname, `resources/example${exeSuffix}`), ['"🐈"'], 45 | { encoding: 'utf8' }); 46 | assert.strictEqual(stdout, '🐈\n'); 47 | } 48 | 49 | { 50 | const { stdout } = await execFile( 51 | path.resolve(__dirname, `resources/example${exeSuffix}`), ['process.argv.length'], 52 | { encoding: 'utf8' }); 53 | assert.strictEqual(stdout, '3\n'); 54 | } 55 | 56 | { 57 | const { stdout } = await execFile( 58 | path.resolve(__dirname, `resources/example${exeSuffix}`), ['process.argv[1] === process.execPath'], 59 | { encoding: 'utf8' }); 60 | assert.strictEqual(stdout, 'true\n'); 61 | } 62 | 63 | { 64 | const { stdout } = await execFile( 65 | path.resolve(__dirname, `resources/example${exeSuffix}`), ['require("vm").runInNewContext("21*2")'], 66 | { encoding: 'utf8' }); 67 | assert.strictEqual(stdout, '42\n'); 68 | } 69 | 70 | { 71 | const { stdout } = await execFile( 72 | path.resolve(__dirname, `resources/example${exeSuffix}`), ['JSON.stringify(process.boxednode)'], 73 | { encoding: 'utf8' }); 74 | const parsed = JSON.parse(stdout); 75 | assert.strictEqual(parsed.hasCodeCache, false); 76 | assert([false, undefined].includes(parsed.rejectedCodeCache)); 77 | } 78 | 79 | { 80 | const { stdout } = await execFile( 81 | path.resolve(__dirname, `resources/example${exeSuffix}`), [ 82 | 'new (require("worker_threads").Worker)' + 83 | '("require(`worker_threads`).parentPort.postMessage(21*2)", {eval:true})' + 84 | '.once("message", console.log);0' 85 | ], 86 | { encoding: 'utf8' }); 87 | assert.strictEqual(stdout, '0\n42\n'); 88 | } 89 | 90 | if (process.platform !== 'win32') { 91 | const proc = childProcess.spawn( 92 | path.resolve(__dirname, `resources/example${exeSuffix}`), 93 | ['process.title = "bananananana"; setInterval(() => {}, 1000);']); 94 | 95 | const { stdout } = await execFile('ps', ['aux'], { encoding: 'utf8' }); 96 | assert(stdout.includes('bananananana'), `Missed process.title change in ${stdout}`); 97 | proc.kill(); 98 | } 99 | 100 | { 101 | const { stdout } = await execFile( 102 | path.resolve(__dirname, `resources/example${exeSuffix}`), [ 103 | 'process.boxednode.markTime("Whatever", "running js");JSON.stringify(process.boxednode.getTimingData())' 104 | ], 105 | { encoding: 'utf8' }); 106 | const timingData = JSON.parse(stdout); 107 | assert.strictEqual(timingData[0][0], 'Node.js Instance'); 108 | assert.strictEqual(timingData[0][1], 'Process initialization'); 109 | assert.strictEqual(timingData[timingData.length - 1][0], 'Whatever'); 110 | assert.strictEqual(timingData[timingData.length - 1][1], 'running js'); 111 | } 112 | }); 113 | 114 | it('works with a Nan addon', async function () { 115 | if (semver.lt(version, '12.19.0')) { 116 | return this.skip(); // no addon support available 117 | } 118 | 119 | this.timeout(2 * 60 * 60 * 1000); // 2 hours 120 | await compileJSFileAsBinary({ 121 | nodeVersionRange: version, 122 | sourceFile: path.resolve(__dirname, 'resources/example.js'), 123 | targetFile: path.resolve(__dirname, `resources/example${exeSuffix}`), 124 | addons: [ 125 | { 126 | path: path.dirname(await pkgUp({ cwd: require.resolve('actual-crash') })), 127 | requireRegexp: /crash\.node$/ 128 | } 129 | ] 130 | }); 131 | 132 | { 133 | const { stdout } = await execFile( 134 | path.resolve(__dirname, `resources/example${exeSuffix}`), 135 | ['typeof require("actual-crash.node").crash'], 136 | { encoding: 'utf8' }); 137 | assert.strictEqual(stdout, 'function\n'); 138 | } 139 | }); 140 | 141 | it('works with a N-API addon', async function () { 142 | if (semver.lt(version, '14.13.0')) { 143 | return this.skip(); // no N-API addon support available 144 | } 145 | 146 | this.timeout(2 * 60 * 60 * 1000); // 2 hours 147 | await compileJSFileAsBinary({ 148 | nodeVersionRange: version, 149 | sourceFile: path.resolve(__dirname, 'resources/example.js'), 150 | targetFile: path.resolve(__dirname, `resources/example${exeSuffix}`), 151 | addons: [ 152 | { 153 | path: path.dirname(await pkgUp({ cwd: require.resolve('weak-napi') })), 154 | requireRegexp: /weakref\.node$/ 155 | } 156 | ] 157 | }); 158 | 159 | { 160 | const { stdout } = await execFile( 161 | path.resolve(__dirname, `resources/example${exeSuffix}`), 162 | ['typeof require("weakref.node").WeakTag'], 163 | { encoding: 'utf8' }); 164 | assert.strictEqual(stdout, 'function\n'); 165 | } 166 | }); 167 | 168 | it('passes through env vars and runs the pre-compile hook', async function () { 169 | this.timeout(2 * 60 * 60 * 1000); // 2 hours 170 | let ranPreCompileHook = false; 171 | async function preCompileHook (nodeSourceTree: string) { 172 | ranPreCompileHook = true; 173 | await fs.access(path.join(nodeSourceTree, 'lib', 'net.js')); 174 | } 175 | try { 176 | await compileJSFileAsBinary({ 177 | nodeVersionRange: version, 178 | sourceFile: path.resolve(__dirname, 'resources/example.js'), 179 | targetFile: path.resolve(__dirname, `resources/example${exeSuffix}`), 180 | env: { CC: 'false', CXX: 'false' }, 181 | preCompileHook 182 | }); 183 | } catch (err) { 184 | assert.strictEqual(ranPreCompileHook, true); 185 | return; 186 | } 187 | 188 | throw new Error('unreachable'); 189 | }); 190 | 191 | it('works with code caching support', async function () { 192 | this.timeout(2 * 60 * 60 * 1000); // 2 hours 193 | await compileJSFileAsBinary({ 194 | nodeVersionRange: version, 195 | sourceFile: path.resolve(__dirname, 'resources/example.js'), 196 | targetFile: path.resolve(__dirname, `resources/example${exeSuffix}`), 197 | useCodeCache: true 198 | }); 199 | 200 | { 201 | const { stdout } = await execFile( 202 | path.resolve(__dirname, `resources/example${exeSuffix}`), [], 203 | { encoding: 'utf8' }); 204 | assert.strictEqual(stdout, 'Hello world!\n'); 205 | } 206 | 207 | { 208 | const { stdout } = await execFile( 209 | path.resolve(__dirname, `resources/example${exeSuffix}`), ['JSON.stringify(process.boxednode)'], 210 | { encoding: 'utf8' }); 211 | const parsed = JSON.parse(stdout); 212 | assert.strictEqual(parsed.hasCodeCache, true); 213 | assert([false, undefined].includes(parsed.rejectedCodeCache)); 214 | } 215 | }); 216 | 217 | for (const compressBlobs of [false, true]) { 218 | it(`works with snapshot support (compressBlobs = ${compressBlobs})`, async function () { 219 | this.timeout(2 * 60 * 60 * 1000); // 2 hours 220 | await compileJSFileAsBinary({ 221 | nodeVersionRange: '^20.13.0', 222 | sourceFile: path.resolve(__dirname, 'resources/snapshot-echo-args.js'), 223 | targetFile: path.resolve(__dirname, `resources/snapshot-echo-args${exeSuffix}`), 224 | useNodeSnapshot: true, 225 | compressBlobs, 226 | nodeSnapshotConfigFlags: ['WithoutCodeCache'], 227 | // the nightly path name is too long for Windows... 228 | tmpdir: process.platform === 'win32' ? path.join(os.tmpdir(), 'bn') : undefined 229 | }); 230 | 231 | { 232 | const { stdout } = await execFile( 233 | path.resolve(__dirname, `resources/snapshot-echo-args${exeSuffix}`), ['a', 'b', 'c'], 234 | { encoding: 'utf8' }); 235 | const { currentArgv, originalArgv, timingData } = JSON.parse(stdout); 236 | assert(currentArgv[0].includes('snapshot-echo-args')); 237 | assert(currentArgv[1].includes('snapshot-echo-args')); 238 | assert.deepStrictEqual(currentArgv.slice(2), ['a', 'b', 'c']); 239 | assert.strictEqual(originalArgv.length, 2); // [execPath, execPath] 240 | assert.strictEqual(timingData[0][0], 'Node.js Instance'); 241 | assert.strictEqual(timingData[0][1], 'Process initialization'); 242 | } 243 | }); 244 | } 245 | }); 246 | }); 247 | -------------------------------------------------------------------------------- /test/resources/.gitignore: -------------------------------------------------------------------------------- 1 | /example 2 | /example.exe 3 | /snapshot-echo-args 4 | /snapshot-echo-args.exe 5 | -------------------------------------------------------------------------------- /test/resources/example.js: -------------------------------------------------------------------------------- 1 | if (process.argv[2]) { 2 | console.log(eval(process.argv[2])); 3 | return; 4 | } 5 | console.log('Hello world!'); 6 | -------------------------------------------------------------------------------- /test/resources/snapshot-echo-args.js: -------------------------------------------------------------------------------- 1 | const { 2 | setDeserializeMainFunction, 3 | } = require('v8').startupSnapshot; 4 | 5 | const originalArgv = [...process.argv]; 6 | 7 | setDeserializeMainFunction(() => { 8 | console.log(JSON.stringify({ 9 | currentArgv: process.argv, 10 | originalArgv, 11 | timingData: process.boxednode.getTimingData() 12 | })); 13 | }); 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "downlevelIteration": true, 5 | "sourceMap": true, 6 | "strictNullChecks": false, 7 | "declaration": true, 8 | "removeComments": true, 9 | "target": "es2018", 10 | "lib": ["es2018"], 11 | "outDir": "./lib", 12 | "moduleResolution": "node", 13 | "module": "commonjs" 14 | }, 15 | "include": [ 16 | "./src/**/*" 17 | ], 18 | "exclude": [ 19 | "./src/**/*.spec.*" 20 | ] 21 | } 22 | --------------------------------------------------------------------------------