├── .dockerignore ├── .github └── workflows │ └── test.yaml ├── .gitignore ├── DEVELOPMENT.md ├── Dockerfile ├── LICENSE ├── README.md ├── make_wheels.py ├── nodejs-cmd ├── LICENSE ├── README.md ├── nodejs_cmd.py └── setup.py ├── requirements.txt └── tests ├── test_comand_line.py ├── test_node.py ├── test_node ├── test_args.js └── test_script.js └── test_npm.py /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !dist 3 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: {} 7 | 8 | concurrency: 9 | group: ${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | build-wheels: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-python@v4 18 | with: 19 | python-version: "3.10" 20 | cache: 'pip' 21 | - name: Install dependencies 22 | run: | 23 | pip install -r requirements.txt 24 | - name: Build Wheels 25 | run: | 26 | mkdir dist 27 | python make_wheels.py 28 | - name: Show built files 29 | run: | 30 | ls -l dist/* 31 | - uses: actions/upload-artifact@v3 32 | with: 33 | name: nodejs-pip-wheels 34 | path: dist/ 35 | if-no-files-found: error 36 | retention-days: 1 37 | 38 | test-non-linux: 39 | name: "Test ${{ matrix.os }} Python:${{ matrix.python-version }} NodeJS:${{ matrix.nodejs-version }}" 40 | runs-on: ${{ matrix.os }} 41 | needs: [build-wheels] 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | os: [windows-latest, macos-latest] 46 | nodejs-version: ['14.19.3', '16.15.1', '18.4.0'] 47 | python-version: ['3.7', '3.8', '3.9', '3.10'] 48 | 49 | steps: 50 | - uses: actions/checkout@v3 51 | - name: Set up Python ${{ matrix.python-version }} 52 | uses: actions/setup-python@v4 53 | with: 54 | python-version: ${{ matrix.python-version }} 55 | - uses: actions/download-artifact@v3 56 | with: 57 | name: nodejs-pip-wheels 58 | path: dist 59 | - name: Show available wheels 60 | run: | 61 | ls dist 62 | - name: Install Package (Linux) 63 | if: matrix.os == 'ubuntu-latest' 64 | run: | 65 | WHEELS_TO_INSTALL=$(find dist -name "*${{matrix.nodejs-version}}*py3*manylinux*x86_64.whl") 66 | echo "WHEELS_TO_INSTALL=${WHEELS_TO_INSTALL}" 67 | pip install ${WHEELS_TO_INSTALL} 68 | - name: Install Package (Mac OS) 69 | if: matrix.os == 'macos-latest' 70 | run: | 71 | WHEELS_TO_INSTALL=$(find dist -name "*${{matrix.nodejs-version}}*py3*macosx*x86_64.whl") 72 | echo "WHEELS_TO_INSTALL=${WHEELS_TO_INSTALL}" 73 | pip install ${WHEELS_TO_INSTALL} 74 | - name: Install Package (Windows) 75 | if: matrix.os == 'windows-latest' 76 | run: | 77 | pip install dist\nodejs_bin-${{matrix.nodejs-version}}a3-py3-none-win_amd64.whl 78 | - name: Test Package 79 | run: 80 | python -W error -m nodejs --version 81 | python -W error -m nodejs.npm --version 82 | python -W error -m nodejs.npx --version 83 | python -W error -m nodejs.corepack --version 84 | 85 | test-linux: 86 | name: "Test Docker OS:${{ matrix.os-variant }} Python:${{ matrix.python-version }} NodeJS:${{ matrix.nodejs-version }}" 87 | runs-on: ubuntu-latest 88 | needs: [build-wheels] 89 | strategy: 90 | fail-fast: false 91 | matrix: 92 | os-variant: [alpine, slim-buster, slim-bullseye] 93 | python-version: ['3.7', '3.8', '3.9', '3.10'] 94 | nodejs-version: ['14.19.3', '16.15.1', '18.4.0'] 95 | 96 | steps: 97 | - uses: actions/checkout@v3 98 | - name: Set up QEMU 99 | uses: docker/setup-qemu-action@v2 100 | with: 101 | platforms: arm64 102 | - name: Set up Docker Buildx 103 | uses: docker/setup-buildx-action@v2 104 | with: 105 | install: true 106 | - uses: actions/download-artifact@v3 107 | with: 108 | name: nodejs-pip-wheels 109 | path: dist 110 | - name: Docker build 111 | run: | 112 | if [[ ${{ matrix.os-variant }} =~ "alpine" ]]; then 113 | WHEEL_TO_INSTALL=nodejs_bin-${{ matrix.nodejs-version }}a3-py3-none-musllinux_1_1_x86_64.whl 114 | else 115 | WHEEL_TO_INSTALL=nodejs_bin-${{ matrix.nodejs-version }}a3-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl 116 | fi 117 | echo "WHEEL_TO_INSTALL=${WHEEL_TO_INSTALL}" 118 | docker build \ 119 | -f Dockerfile \ 120 | --build-arg PYTHON_VERSION=${{ matrix.python-version }} \ 121 | --build-arg OS_VARIANT=${{ matrix.os-variant }} \ 122 | --build-arg WHEEL_TO_INSTALL=${WHEEL_TO_INSTALL} \ 123 | . 124 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | nodejs-cmd/build 3 | nodejs-cmd/dist 4 | nodejs-cmd/*.egg-info 5 | .DS_Store 6 | env*/ 7 | __pycache__/ 8 | *.py[cod] 9 | venv 10 | package.json 11 | node_modules 12 | package-lock.json 13 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | Node.js PyPI distribution 2 | ===================== 3 | 4 | This repository contains the script used to repackage the [releases][nodejsdl] of [Node.js][nodejs] as [Python binary wheels][wheel]. This document is intended for maintainers; see the [package README][pkgreadme] for rationale and usage instructions. 5 | 6 | The repackaged artifacts are published as the [node-js PyPI package][pypi]. 7 | 8 | [nodejs]: https://nodejs.org/ 9 | [nodejsdl]: https://nodejs.org/en/download/ 10 | [wheel]: https://github.com/pypa/wheel 11 | [pkgreadme]: README.pypi.md 12 | [pypi]: https://pypi.org/project/nodejs-bin/ 13 | 14 | This tool is based on the work of the creators of the [Zig language][ziglang], see [the original][basedon]. Thank you! 15 | 16 | [ziglang]: https://ziglang.org 17 | [basedon]: https://github.com/ziglang/zig-pypi 18 | 19 | Preparation 20 | ----------- 21 | 22 | The script requires Python 3.5 or later. 23 | 24 | Install the dependencies: 25 | 26 | ```shell 27 | pip install -r requirements.txt 28 | ``` 29 | 30 | The `libarchive-c` Python library requires the native [libarchive][] library to be available. 31 | 32 | [libarchive]: https://libarchive.org/ 33 | 34 | Building wheels 35 | --------------- 36 | 37 | Run the repackaging script: 38 | 39 | ```shell 40 | python make_wheels.py 41 | ``` 42 | 43 | This command will download the Node.js release archives for every supported platform and convert them to binary wheels, which are placed under `dist/`. The Node.js version and platforms are configured in the script source. 44 | 45 | The process of converting release archives to binary wheels is deterministic, and the output of the script should be bit-for-bit identical regardless of the environment and platform it runs under. To this end, it prints the SHA256 hashes of inputs and outputs; the hashes of the inputs will match the ones on the [Node.js downloads page][nodejsdl], and the hashes of the outputs will match the ones on the [PyPI downloads page][pypidl]. 46 | 47 | [pypidl]: https://pypi.org/project/node-js/#files 48 | 49 | Uploading wheels 50 | ---------------- 51 | 52 | Run the publishing utility: 53 | 54 | ```shell 55 | twine dist/* 56 | ``` 57 | 58 | This command will upload the binary wheels built in the previous step to PyPI. 59 | 60 | License 61 | ------- 62 | 63 | This script is distributed under the terms of the [MIT (Expat) license](LICENSE.txt). -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VERSION=3.10 2 | ARG OS_VARIANT=bullseye-slim 3 | 4 | FROM python:${PYTHON_VERSION}-${OS_VARIANT} 5 | 6 | ARG PYTHON_VERSION 7 | ENV PYTHON_VERSION=${PYTHON_VERSION} 8 | ARG OS_VARIANT 9 | ENV OS_VARIANT=${OS_VARIANT} 10 | 11 | # This is required should be supplied as a build-arg 12 | ARG WHEEL_TO_INSTALL 13 | RUN test -n "${WHEEL_TO_INSTALL}" || (echo "Must supply WHEEL_TO_INSTALL as build arg"; exit 1) 14 | 15 | COPY dist/${WHEEL_TO_INSTALL} dist/${WHEEL_TO_INSTALL} 16 | 17 | # NodeJS needs libstdc++ to be present 18 | # https://github.com/nodejs/unofficial-builds/#builds 19 | RUN if echo "${OS_VARIANT}" | grep -e "alpine"; then \ 20 | apk add libstdc++; \ 21 | fi 22 | 23 | RUN pip install dist/${WHEEL_TO_INSTALL} 24 | 25 | RUN python -W error -m nodejs --version 26 | RUN python -W error -m nodejs.npm --version 27 | RUN python -W error -m nodejs.npx --version 28 | RUN python -W error -m nodejs.corepack --version 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (Expat) 2 | 3 | Copyright (c) 2021, whitequark 4 | Port for Node.js copyright (c) 2022, Sam Willis 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Node.js PyPI distribution 2 | ===================== 3 | 4 | [Node.js][nodejs] is an open-source, cross-platform, back-end JavaScript runtime environment that runs on the V8 engine and executes JavaScript code outside a web browser. 5 | 6 | The [nodejs-bin][pypi] Python package redistributes Node.js so that it can be used as a dependency of Python projects. With `nodejs-bin` you can call `nodejs`, `npm` and `npx` from both the [command line](#command-line-usage) and a [Python API](#python-api-usage). 7 | 8 | **Note: this is an unofficial Node.js distribution.** However, it _does_ use only official bits distributed by the official NodeJS maintainers from one of the following sources: 9 | 10 | * NodeJS official releases: https://nodejs.org/en/download/releases/ 11 | * NodeJS "unofficial" builds: https://github.com/nodejs/unofficial-builds/ 12 | 13 | **This is intended for use within Python virtual environments and containers, it should probably not be used for global installation.** 14 | 15 | This PyPI distribution is provided by . 16 | 17 | [nodejs]: https://nodejs.org/ 18 | [pypi]: https://pypi.org/project/nodejs-bin/ 19 | 20 | Install 21 | ------- 22 | 23 | To install: 24 | 25 | ```shell 26 | pip install nodejs-bin 27 | ``` 28 | 29 | By default the command line `node`, `npm` and `npx` commands are not installed to prevent collisions with already installed Node.js versions. To install them: 30 | 31 | ```shell 32 | pip install 'nodejs-bin[cmd]' 33 | ``` 34 | 35 | You can specify the Node.js version to install with: 36 | 37 | ```shell 38 | pip install nodejs-bin== 39 | 40 | # Example: 41 | pip install nodejs-bin==16.15.1 42 | ``` 43 | 44 | Command Line Usage 45 | ------------------ 46 | 47 | To run Node.js from the command line, use: 48 | 49 | ```shell 50 | python -m nodejs 51 | ``` 52 | 53 | `npm` and `npx` are also available as `nodejs.npm` and `nodejs.npx`: 54 | 55 | ```shell 56 | python -m nodejs.npm 57 | python -m nodejs.npx 58 | ``` 59 | 60 | If you installed the optional command line commands with `pip install 'nodejs-bin[cmd]'` (see above), you can use them directly from the command line as you would normally with Node.js: 61 | 62 | ```shell 63 | node 64 | npm 65 | npx 66 | ``` 67 | 68 | Python API Usage 69 | ---------------- 70 | 71 | `node-bin` has a simple Python API that wraps the Node.js command line with the [Python `subprocess`](https://docs.python.org/3/library/subprocess.html). 72 | 73 | For `node`, `npm` and `npx` there are `.call()`, `.run()` and `.Popen()` methods that match the equivalent `subprocess` methods. 74 | 75 | To run Node.js from a Python program and return the exit code: 76 | 77 | ```python 78 | from nodejs import node, npm, npx 79 | 80 | # Run Node.js and return the exit code. 81 | node.call(['script.js', 'arg1', ...], **kwargs) 82 | 83 | # Run npm and return the exit code. 84 | npm.call(['command', 'arg1', ...], **kwargs) 85 | 86 | # Run npx and return the exit code. 87 | npx.call(['command', 'arg1', ...], **kwargs) 88 | ``` 89 | 90 | The `call(args, **kwargs)` functions wrap [`subprocess.call()`](https://docs.python.org/3/library/subprocess.html#subprocess.call), passes though all `kwargs` and returns the exit code of the process. 91 | 92 | To run Node.js from a Python program and return a [CompletedProcess](https://docs.python.org/3/library/subprocess.html#subprocess.CompletedProcess) object: 93 | 94 | ```python 95 | from nodejs import node, npm, npx 96 | 97 | # Run Node.js and return the exit code. 98 | node.run(['script.js', 'arg1', ...], **kwargs) 99 | 100 | # Run npm and return the exit code. 101 | npm.run(['command', 'arg1', ...], **kwargs) 102 | 103 | # Run npx and return the exit code. 104 | npx.run(['command', 'arg1', ...], **kwargs) 105 | ``` 106 | 107 | The `run(args, **kwargs)` functions wrap [`subprocess.run()`](https://docs.python.org/3/library/subprocess.html#subprocess.run), passes though all `kwargs` and returns a `CompletedProcess`. 108 | 109 | Additionally, to start a Node.js process and return a `subprocess.Popen` object, you can use the `Popen(args, **kwargs)` functions: 110 | 111 | ```python 112 | from nodejs import node, npm, npx 113 | 114 | # Start Node.js and return the Popen object. 115 | node_process = node.Popen(['script.js', 'arg1', ...], **kwargs) 116 | 117 | # Start npm and return the Popen object. 118 | npm_process = npm.Popen(['command', 'arg1', ...], **kwargs) 119 | 120 | # Start npx and return the Popen object. 121 | npx_process = npx.Popen(['command', 'arg1', ...], **kwargs) 122 | ``` 123 | 124 | The `Popen(args, **kwargs)` functions wrap [`subprocess.Popen()`](https://docs.python.org/3/library/subprocess.html#subprocess.Popen), passes though all `kwargs` and returns a [`Popen` object](https://docs.python.org/3/library/subprocess.html#popen-objects). 125 | 126 | The `nodejs.node` api is also available as `nodejs.run` and `nodejs.call` and `nodejs.Popen`. 127 | 128 | Finally, there are a number of convenient attributes on the `nodejs` module: 129 | 130 | * `nodejs.node_version`: the version of Node.js that is installed. 131 | * `nodejs.path`: the path to the Node.js executable. 132 | 133 | 134 | Versions 135 | -------- 136 | 137 | nodejs-bin offers Node.js *Current* and *LTS* (long-term support) versions. See the [Node.js Documentation](https://nodejs.org/en/about/releases/) for more information. 138 | 139 | The full list of versions is available on PyPI is here: 140 | 141 | 142 | License 143 | ------- 144 | 145 | The [Node.js license](https://raw.githubusercontent.com/nodejs/node/master/LICENSE). -------------------------------------------------------------------------------- /make_wheels.py: -------------------------------------------------------------------------------- 1 | import os 2 | import hashlib 3 | import pathlib 4 | import urllib.request 5 | import libarchive 6 | from email.message import EmailMessage 7 | from wheel.wheelfile import WheelFile 8 | from zipfile import ZipInfo, ZIP_DEFLATED 9 | from inspect import cleandoc 10 | 11 | 12 | # Versions to build if run as a script: 13 | BUILD_VERSIONS = ('14.19.3', '16.15.1', '18.4.0') 14 | 15 | # Suffix to append to the Wheel 16 | # For pre release versions this should be 'aN', e.g. 'a1' 17 | # For release versions this should be '' 18 | # See https://peps.python.org/pep-0427/#file-name-convention for details. 19 | BUILD_SUFFIX = 'a3' 20 | 21 | # Main binary for node 22 | # Path of binary inn downloaded distribution to match 23 | NODE_BINS = ('bin/node', 'node.exe') 24 | 25 | # Other binaries 26 | # key: path of binary inn downloaded distribution to match 27 | # value: tuple of ( 28 | # , 29 | # 30 | # ) 31 | NODE_OTHER_BINS = { 32 | 'bin/npm': ('npm', True), 33 | 'npm.cmd': ('npm', False), 34 | 'bin/npx': ('npx', True), 35 | 'npx.cmd': ('npx', False), 36 | 'bin/corepack': ('corepack', True), 37 | 'corepack.cmd': ('corepack', False), 38 | } 39 | 40 | # Mapping of node platforms to Python platforms 41 | PLATFORMS = { 42 | 'win-x86': 'win32', 43 | 'win-x64': 'win_amd64', 44 | 'darwin-x64': 'macosx_10_9_x86_64', 45 | 'darwin-arm64': 'macosx_11_0_arm64', 46 | 'linux-x64': 'manylinux_2_12_x86_64.manylinux2010_x86_64', 47 | 'linux-armv7l': 'manylinux_2_17_armv7l.manylinux2014_armv7l', 48 | 'linux-arm64': 'manylinux_2_17_aarch64.manylinux2014_aarch64', 49 | 'linux-x64-musl': 'musllinux_1_1_x86_64' 50 | } 51 | 52 | # https://github.com/nodejs/unofficial-builds/ 53 | # Versions added here should match the keys above 54 | UNOFFICIAL_NODEJS_BUILDS = {'linux-x64-musl'} 55 | 56 | _mismatched_versions = UNOFFICIAL_NODEJS_BUILDS - set(PLATFORMS.keys()) 57 | if _mismatched_versions: 58 | raise Exception(f"A version mismatch occurred. Check the usage of {_mismatched_versions}") 59 | 60 | 61 | class ReproducibleWheelFile(WheelFile): 62 | def writestr(self, zinfo, *args, **kwargs): 63 | if not isinstance(zinfo, ZipInfo): 64 | raise ValueError("ZipInfo required") 65 | zinfo.date_time = (1980, 1, 1, 0, 0, 0) 66 | zinfo.create_system = 3 67 | super().writestr(zinfo, *args, **kwargs) 68 | 69 | 70 | def make_message(headers, payload=None): 71 | msg = EmailMessage() 72 | for name, value in headers.items(): 73 | if isinstance(value, list): 74 | for value_part in value: 75 | msg[name] = value_part 76 | else: 77 | msg[name] = value 78 | if payload: 79 | msg.set_payload(payload) 80 | return msg 81 | 82 | 83 | def write_wheel_file(filename, contents): 84 | with ReproducibleWheelFile(filename, 'w') as wheel: 85 | for member_info, member_source in contents.items(): 86 | if not isinstance(member_info, ZipInfo): 87 | member_info = ZipInfo(member_info) 88 | member_info.external_attr = 0o644 << 16 89 | member_info.file_size = len(member_source) 90 | member_info.compress_type = ZIP_DEFLATED 91 | wheel.writestr(member_info, bytes(member_source)) 92 | return filename 93 | 94 | 95 | def write_wheel(out_dir, *, name, version, tag, metadata, description, contents, entry_points): 96 | name_snake = name.replace('-', '_') 97 | wheel_name = f'{name_snake}-{version}-{tag}.whl' 98 | dist_info = f'{name_snake}-{version}.dist-info' 99 | if entry_points: 100 | contents[f'{dist_info}/entry_points.txt'] = (cleandoc(""" 101 | [console_scripts] 102 | {entry_points} 103 | """).format(entry_points='\n'.join([f'{k} = {v}' for k, v in entry_points.items()] if entry_points else []))).encode('ascii'), 104 | return write_wheel_file(os.path.join(out_dir, wheel_name), { 105 | **contents, 106 | f'{dist_info}/METADATA': make_message({ 107 | 'Metadata-Version': '2.1', 108 | 'Name': name, 109 | 'Version': version, 110 | **metadata, 111 | }, description), 112 | f'{dist_info}/WHEEL': make_message({ 113 | 'Wheel-Version': '1.0', 114 | 'Generator': 'nodejs-pypi make_wheels.py', 115 | 'Root-Is-Purelib': 'false', 116 | 'Tag': tag, 117 | }), 118 | }) 119 | 120 | 121 | def write_nodejs_wheel(out_dir, *, node_version, version, platform, archive): 122 | contents = {} 123 | entry_points = {} 124 | init_imports = [] 125 | 126 | # Create the output directory if it does not exist 127 | out_dir_path = pathlib.Path(out_dir) 128 | if not out_dir_path.exists(): 129 | out_dir_path.mkdir(parents=True) 130 | 131 | with libarchive.memory_reader(archive) as archive: 132 | for entry in archive: 133 | entry_name = '/'.join(entry.name.split('/')[1:]) 134 | if entry.isdir or not entry_name: 135 | continue 136 | 137 | zip_info = ZipInfo(f'nodejs/{entry_name}') 138 | zip_info.external_attr = (entry.mode & 0xFFFF) << 16 139 | contents[zip_info] = b''.join(entry.get_blocks()) 140 | 141 | if entry_name in NODE_BINS: 142 | # entry_points['node'] = 'nodejs.node:main' 143 | init_imports.append('from . import node as node') 144 | contents['nodejs/node.py'] = cleandoc(f""" 145 | import os, sys, subprocess 146 | from typing import TYPE_CHECKING 147 | 148 | path = os.path.join(os.path.dirname(__file__), "{entry_name}") 149 | 150 | if TYPE_CHECKING: 151 | call = subprocess.call 152 | run = subprocess.run 153 | Popen = subprocess.Popen 154 | 155 | else: 156 | def call(args, **kwargs): 157 | return subprocess.call([ 158 | path, 159 | *args 160 | ], **kwargs) 161 | 162 | def run(args, **kwargs): 163 | return subprocess.run([ 164 | path, 165 | *args 166 | ], **kwargs) 167 | 168 | def Popen(args, **kwargs): 169 | return subprocess.Popen([ 170 | path, 171 | *args 172 | ], **kwargs) 173 | 174 | def main() -> None: 175 | sys.exit(call(sys.argv[1:])) 176 | 177 | if __name__ == '__main__': 178 | main() 179 | """).encode('ascii') 180 | contents['nodejs/__main__.py'] = cleandoc(f""" 181 | from .node import main 182 | 183 | if __name__ == '__main__': 184 | main() 185 | """).encode('ascii') 186 | elif entry_name in NODE_OTHER_BINS and NODE_OTHER_BINS[entry_name][1]: 187 | other_bin = NODE_OTHER_BINS[entry_name][0] 188 | init_imports.append(f'from . import {other_bin} as {other_bin}') 189 | script_name = '/'.join(os.path.normpath(os.path.join(os.path.dirname(entry.name), entry.linkpath)).split('/')[1:]) 190 | contents[f'nodejs/{NODE_OTHER_BINS[entry_name][0]}.py'] = cleandoc(f""" 191 | import os, sys 192 | from typing import TYPE_CHECKING 193 | from . import node 194 | 195 | if TYPE_CHECKING: 196 | call = subprocess.call 197 | run = subprocess.run 198 | Popen = subprocess.Popen 199 | 200 | else: 201 | def call(args, **kwargs): 202 | return node.call([ 203 | os.path.join(os.path.dirname(__file__), "{script_name}"), 204 | *args 205 | ], **kwargs) 206 | 207 | def run(args, **kwargs): 208 | return node.run([ 209 | os.path.join(os.path.dirname(__file__), "{script_name}"), 210 | *args 211 | ], **kwargs) 212 | 213 | def Popen(args, **kwargs): 214 | return node.Popen([ 215 | os.path.join(os.path.dirname(__file__), "{script_name}"), 216 | *args 217 | ], **kwargs) 218 | 219 | def main() -> None: 220 | sys.exit(call(sys.argv[1:])) 221 | 222 | if __name__ == '__main__': 223 | main() 224 | """).encode('ascii') 225 | elif entry_name in NODE_OTHER_BINS: 226 | other_bin = NODE_OTHER_BINS[entry_name][0] 227 | init_imports.append(f'from . import {other_bin} as {other_bin}') 228 | contents[f'nodejs/{NODE_OTHER_BINS[entry_name][0]}.py'] = cleandoc(f""" 229 | import os, sys, subprocess 230 | from typing import TYPE_CHECKING 231 | 232 | if TYPE_CHECKING: 233 | call = subprocess.call 234 | run = subprocess.run 235 | Popen = subprocess.Popen 236 | 237 | else: 238 | def call(args, **kwargs): 239 | return subprocess.call([ 240 | os.path.join(os.path.dirname(__file__), "{entry_name}"), 241 | *args 242 | ], **kwargs) 243 | 244 | def run(args, **kwargs): 245 | return subprocess.run([ 246 | os.path.join(os.path.dirname(__file__), "{entry_name}"), 247 | *args 248 | ], **kwargs) 249 | 250 | def Popen(args, **kwargs): 251 | return subprocess.Popen([ 252 | os.path.join(os.path.dirname(__file__), "{entry_name}"), 253 | *args 254 | ], **kwargs) 255 | 256 | def main() -> None: 257 | sys.exit(call(sys.argv[1:])) 258 | 259 | if __name__ == '__main__': 260 | main() 261 | """).encode('ascii') 262 | 263 | contents['nodejs/__init__.py'] = (cleandoc(""" 264 | import sys 265 | from .node import path as path, main as main, call as call, run as run, Popen as Popen 266 | if not '-m' in sys.argv: 267 | {init_imports} 268 | 269 | __version__ = "{version}" 270 | node_version = "{node_version}" 271 | """)).format( 272 | # Note: two space indentation above and below is necessary to align 273 | init_imports='\n '.join(init_imports), 274 | version=version, 275 | node_version=node_version, 276 | ).encode('ascii') 277 | contents['nodejs/py.typed'] = b'' 278 | 279 | with open('README.md') as f: 280 | description = f.read() 281 | 282 | return write_wheel(out_dir, 283 | name='nodejs-bin', 284 | version=version, 285 | tag=f'py3-none-{platform}', 286 | metadata={ 287 | 'Summary': 'Node.js is an open-source, cross-platform, back-end JavaScript runtime environment that runs on the V8 engine and executes JavaScript code outside a web browser.', 288 | 'Description-Content-Type': 'text/markdown', 289 | 'License': 'MIT', 290 | 'Classifier': [ 291 | 'License :: OSI Approved :: MIT License', 292 | ], 293 | 'Project-URL': [ 294 | 'Project Homepage, https://github.com/samwillis/nodejs-pypi', 295 | 'Node.js Homepage, https://nodejs.org', 296 | ], 297 | 'Requires-Python': '~=3.5', 298 | 'Provides-Extra': 'cmd', 299 | 'Requires-Dist': "nodejs-cmd; extra == 'cmd'", 300 | }, 301 | description=description, 302 | contents=contents, 303 | entry_points=entry_points, 304 | ) 305 | 306 | 307 | def make_nodejs_version(node_version, suffix=''): 308 | wheel_version = f'{node_version}{suffix}' 309 | print('--') 310 | print('Making Node.js Wheels for version', node_version) 311 | if suffix: 312 | print('Suffix:', suffix) 313 | 314 | for node_platform, python_platform in PLATFORMS.items(): 315 | filetype = 'zip' if node_platform.startswith('win-') else 'tar.xz' 316 | if node_platform in UNOFFICIAL_NODEJS_BUILDS: 317 | node_url = f'https://unofficial-builds.nodejs.org/download/release/v{node_version}/node-v{node_version}-{node_platform}.{filetype}' 318 | else: 319 | node_url = f'https://nodejs.org/dist/v{node_version}/node-v{node_version}-{node_platform}.{filetype}' 320 | 321 | print(f'- Making Wheel for {node_platform} from {node_url}') 322 | try: 323 | with urllib.request.urlopen(node_url) as request: 324 | node_archive = request.read() 325 | print(f' {node_url}') 326 | print(f' {hashlib.sha256(node_archive).hexdigest()}') 327 | except urllib.error.HTTPError as e: 328 | print(f' {e.code} {e.reason}') 329 | print(f' Skipping {node_platform}') 330 | continue 331 | 332 | wheel_path = write_nodejs_wheel('dist/', 333 | node_version=node_version, 334 | version=wheel_version, 335 | platform=python_platform, 336 | archive=node_archive) 337 | with open(wheel_path, 'rb') as wheel: 338 | print(f' {wheel_path}') 339 | print(f' {hashlib.sha256(wheel.read()).hexdigest()}') 340 | 341 | def main(): 342 | for node_version in BUILD_VERSIONS: 343 | make_nodejs_version(node_version, suffix=BUILD_SUFFIX) 344 | 345 | if __name__ == '__main__': 346 | main() -------------------------------------------------------------------------------- /nodejs-cmd/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Sam Willis 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /nodejs-cmd/README.md: -------------------------------------------------------------------------------- 1 | # Additional Standard Command Line Commands For "nodejs-bin" 2 | 3 | This package provides the `node`, `npm` and `npx` commands for the (nodejs-bin project)[https://pypi.org/project/nodejs-bin/]. 4 | 5 | It should not be installed directly, but rather as an option when installing the `nodejs-bin` package: 6 | 7 | ```shell 8 | pip install 'nodejs-bin[cmd]' 9 | ``` 10 | -------------------------------------------------------------------------------- /nodejs-cmd/nodejs_cmd.py: -------------------------------------------------------------------------------- 1 | from nodejs import node, npm, npx 2 | 3 | def node_main(): 4 | node.main() 5 | 6 | def npm_main(): 7 | npm.main() 8 | 9 | def npx_main(): 10 | npx.main() 11 | -------------------------------------------------------------------------------- /nodejs-cmd/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setup( 7 | name='nodejs-cmd', 8 | version='0.0.1a', 9 | author="Sam Willis", 10 | description="Additional Standard Command Line Commands For nodejs-bin", 11 | long_description=long_description, 12 | long_description_content_type="text/markdown", 13 | url="https://github.com/samwillis/nodejs-pypi", 14 | py_modules=['nodejs_cmd'], 15 | classifiers=[ 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | ], 19 | python_requires='~=3.5', 20 | entry_points={ 21 | 'console_scripts': [ 22 | 'node = nodejs_cmd:node_main', 23 | 'npm = nodejs_cmd:npm_main', 24 | 'npx = nodejs_cmd:npx_main', 25 | ], 26 | }, 27 | ) 28 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | wheel 2 | twine 3 | libarchive-c 4 | pytest -------------------------------------------------------------------------------- /tests/test_comand_line.py: -------------------------------------------------------------------------------- 1 | "Test nodejs command line" 2 | 3 | import os, sys, subprocess 4 | 5 | 6 | THIS_DIR = os.path.dirname(os.path.abspath(__file__)) 7 | 8 | 9 | def test_runs(): 10 | assert subprocess.call([sys.executable, "-m", "nodejs", "--version"]) == 0 11 | 12 | 13 | def test_version(capfd): 14 | subprocess.call([sys.executable, "-m", "nodejs", "--version"]) 15 | out, err = capfd.readouterr() 16 | assert out.startswith('v') 17 | 18 | 19 | def test_eval(capfd): 20 | subprocess.call([sys.executable, "-m", "nodejs", "--eval", "console.log('hello')"]) 21 | out, err = capfd.readouterr() 22 | assert out.strip() == 'hello' 23 | 24 | 25 | def test_eval_error(capfd): 26 | subprocess.call([sys.executable, "-m", "nodejs", "--eval", "console.error('error')"]) 27 | out, err = capfd.readouterr() 28 | assert err.strip() == 'error' 29 | 30 | 31 | def test_eval_error_exit(): 32 | ret = subprocess.call([sys.executable, "-m", "nodejs", "--eval", "process.exit(1)"]) 33 | assert ret == 1 34 | 35 | 36 | def test_script(capfd): 37 | subprocess.call([sys.executable, "-m", "nodejs", os.path.join(THIS_DIR, "test_node", "test_script.js")]) 38 | out, err = capfd.readouterr() 39 | assert out.strip() == 'hello' 40 | 41 | 42 | def test_args(capfd): 43 | subprocess.call([sys.executable, "-m", "nodejs", os.path.join(THIS_DIR, "test_node", "test_args.js"), "hello"]) 44 | out, err = capfd.readouterr() 45 | assert out.strip() == 'hello' 46 | 47 | 48 | def test_npm_runs(): 49 | assert subprocess.call([sys.executable, "-m", "nodejs.npm", "--version"]) == 0 50 | 51 | 52 | def test_npm_version(capfd): 53 | subprocess.call([sys.executable, "-m", "nodejs.npm", "--version"]) 54 | out, err = capfd.readouterr() 55 | assert isinstance(out, str) 56 | 57 | 58 | def test_install_package(tmp_path, capfd): 59 | os.chdir(tmp_path) 60 | subprocess.call([sys.executable, "-m", "nodejs.npm", "init", "-y"]) 61 | assert (tmp_path / 'package.json').exists() 62 | subprocess.call([sys.executable, "-m", "nodejs.npm", "install", "is-even"]) 63 | assert (tmp_path / 'node_modules' / 'is-even').exists() 64 | out, err = capfd.readouterr() 65 | subprocess.call([sys.executable, "-m", "nodejs", "--eval", 'console.log(require("is-even")(42))']) 66 | out, err = capfd.readouterr() 67 | assert out.strip() == 'true' 68 | subprocess.call([sys.executable, "-m", "nodejs", "--eval", 'console.log(require("is-even")(43))']) 69 | out, err = capfd.readouterr() 70 | assert out.strip() == 'false' 71 | -------------------------------------------------------------------------------- /tests/test_node.py: -------------------------------------------------------------------------------- 1 | "Test nodejs.node" 2 | 3 | import os 4 | 5 | 6 | THIS_DIR = os.path.dirname(os.path.abspath(__file__)) 7 | 8 | 9 | def test_package_installed(): 10 | import nodejs 11 | assert nodejs.__version__ is not None 12 | 13 | 14 | def test_runs(): 15 | from nodejs import node 16 | assert node.call(['--version']) is 0 17 | 18 | 19 | def test_version(capfd): 20 | from nodejs import node, node_version 21 | node.call(['--version']) 22 | out, err = capfd.readouterr() 23 | assert out.startswith('v') 24 | assert out.strip() == f'v{node_version}' 25 | 26 | 27 | def test_eval(capfd): 28 | from nodejs import node 29 | node.call(['--eval', 'console.log("hello")']) 30 | out, err = capfd.readouterr() 31 | assert out.strip() == 'hello' 32 | 33 | 34 | def test_eval_error(capfd): 35 | from nodejs import node 36 | node.call(['--eval', 'console.error("error")']) 37 | out, err = capfd.readouterr() 38 | assert err.strip() == 'error' 39 | 40 | 41 | def test_eval_error_exit(): 42 | from nodejs import node 43 | ret = node.call(['--eval', 'process.exit(1)']) 44 | assert ret == 1 45 | 46 | 47 | def test_script(capfd): 48 | from nodejs import node 49 | node.call([os.path.join(THIS_DIR, 'test_node', 'test_script.js')]) 50 | out, err = capfd.readouterr() 51 | assert out.strip() == 'hello' 52 | 53 | 54 | def test_args(capfd): 55 | from nodejs import node 56 | node.call([os.path.join(THIS_DIR, 'test_node', 'test_args.js'), 'hello']) 57 | out, err = capfd.readouterr() 58 | assert out.strip() == 'hello' 59 | 60 | -------------------------------------------------------------------------------- /tests/test_node/test_args.js: -------------------------------------------------------------------------------- 1 | console.log(process.argv[2]); -------------------------------------------------------------------------------- /tests/test_node/test_script.js: -------------------------------------------------------------------------------- 1 | console.log('hello'); -------------------------------------------------------------------------------- /tests/test_npm.py: -------------------------------------------------------------------------------- 1 | "Test nodejs.npm" 2 | 3 | import os 4 | 5 | 6 | def test_runs(): 7 | from nodejs import npm 8 | assert npm.call(['--version']) is 0 9 | 10 | 11 | def test_version(capfd): 12 | from nodejs import npm 13 | npm.call(['--version']) 14 | out, err = capfd.readouterr() 15 | assert isinstance(out, str) 16 | 17 | 18 | def test_install_package(tmp_path, capfd): 19 | from nodejs import npm, node 20 | import json 21 | os.chdir(tmp_path) 22 | npm.call(['init', '-y']) 23 | assert (tmp_path / 'package.json').exists() 24 | npm.call(['install', 'is-even']) 25 | assert (tmp_path / 'node_modules' / 'is-even').exists() 26 | out, err = capfd.readouterr() 27 | node.call(['--eval', 'console.log(require("is-even")(42))']) 28 | out, err = capfd.readouterr() 29 | assert out.strip() == 'true' 30 | node.call(['--eval', 'console.log(require("is-even")(43))']) 31 | out, err = capfd.readouterr() 32 | assert out.strip() == 'false' 33 | --------------------------------------------------------------------------------