Skip to content

Commit 5714a15

Browse files
committed
Initial commit
0 parents  commit 5714a15

File tree

6 files changed

+321
-0
lines changed

6 files changed

+321
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/dist
2+
.DS_Store
3+
env*/

LICENSE

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
The MIT License (Expat)
2+
3+
Copyright (c) 2021, whitequark <whitequark@whitequark.org>
4+
Copyright (c) 2022, Sam Willis <sam.willis@gmail.com>
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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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/node-js/
13+
14+
This tool is based on the work of the creators of the [Zig language][ziglang], see [the original][basedon]. Thank you to them!
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 wheel twine libarchive-c
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 [Zig 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).

README.pypi.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
Node.js PyPI distribution
2+
=====================
3+
4+
[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.
5+
6+
The [node-js][pypi] Python package redistributes Node.js so that it can be used as a dependency of Python projects.
7+
8+
[zig]: https://nodejs.org/
9+
[pypi]: https://pypi.org/project/node-js/
10+
11+
Usage
12+
-----
13+
14+
To run the Zig toolchain from the command line, use:
15+
16+
```shell
17+
python -m nodejs
18+
```
19+
20+
To run Node.js from a Python program, use `sys.executable` to locate the Python binary to invoke. For example:
21+
22+
```python
23+
import sys, subprocess
24+
25+
subprocess.call([sys.executable, "-m", "nodejs"])
26+
```
27+
28+
License
29+
-------
30+
31+
The [Node.js license](https://raw.githubusercontent.com/nodejs/node/master/LICENSE).

make_wheels.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import os
2+
import hashlib
3+
import urllib.request
4+
from email.message import EmailMessage
5+
from wheel.wheelfile import WheelFile, get_zipinfo_datetime
6+
from zipfile import ZipInfo, ZIP_DEFLATED
7+
import libarchive # from libarchive-c
8+
9+
10+
class ReproducibleWheelFile(WheelFile):
11+
def writestr(self, zinfo, *args, **kwargs):
12+
if not isinstance(zinfo, ZipInfo):
13+
raise ValueError("ZipInfo required")
14+
zinfo.date_time = (1980,1,1,0,0,0)
15+
zinfo.create_system = 3
16+
super().writestr(zinfo, *args, **kwargs)
17+
18+
19+
def make_message(headers, payload=None):
20+
msg = EmailMessage()
21+
for name, value in headers.items():
22+
if isinstance(value, list):
23+
for value_part in value:
24+
msg[name] = value_part
25+
else:
26+
msg[name] = value
27+
if payload:
28+
msg.set_payload(payload)
29+
return msg
30+
31+
32+
def write_wheel_file(filename, contents):
33+
with ReproducibleWheelFile(filename, 'w') as wheel:
34+
for member_info, member_source in contents.items():
35+
if not isinstance(member_info, ZipInfo):
36+
member_info = ZipInfo(member_info)
37+
member_info.external_attr = 0o644 << 16
38+
member_info.file_size = len(member_source)
39+
member_info.compress_type = ZIP_DEFLATED
40+
wheel.writestr(member_info, bytes(member_source))
41+
return filename
42+
43+
44+
def write_wheel(out_dir, *, name, version, tag, metadata, description, contents, entry_points):
45+
wheel_name = f'{name}-{version}-{tag}.whl'
46+
dist_info = f'{name}-{version}.dist-info'
47+
return write_wheel_file(os.path.join(out_dir, wheel_name), {
48+
**contents,
49+
f'{dist_info}/entry_points.txt': ("""\
50+
[console_scripts]
51+
{entry_points}
52+
""".format(entry_points='\n'.join([f'{k} = {v}' for k, v in entry_points.items()] if entry_points else []))).encode('ascii'),
53+
f'{dist_info}/METADATA': make_message({
54+
'Metadata-Version': '2.1',
55+
'Name': name,
56+
'Version': version,
57+
**metadata,
58+
}, description),
59+
f'{dist_info}/WHEEL': make_message({
60+
'Wheel-Version': '1.0',
61+
'Generator': 'nodejs-pypi make_wheels.py',
62+
'Root-Is-Purelib': 'false',
63+
'Tag': tag,
64+
}),
65+
})
66+
67+
68+
NODE_BINS = ('bin/node', 'node.exe')
69+
NODE_OTHER_BINS = {
70+
'bin/npm': ('npm', True),
71+
'npm.cmd': ('npm', False),
72+
'bin/npx': ('npx', True),
73+
'npx.cmd': ('npx', False),
74+
'bin/corepack': ('corepack', True),
75+
'corepack.cmd': ('corepack', False),
76+
}
77+
78+
79+
def write_nodejs_wheel(out_dir, *, version, platform, archive):
80+
contents = {}
81+
entry_points = {}
82+
contents['nodejs/__init__.py'] = f'__version__ = "{version}"\n'.encode('ascii')
83+
84+
with libarchive.memory_reader(archive) as archive:
85+
for entry in archive:
86+
entry_name = '/'.join(entry.name.split('/')[1:])
87+
if entry.isdir or not entry_name:
88+
continue
89+
90+
zip_info = ZipInfo(f'nodejs/{entry_name}')
91+
zip_info.external_attr = (entry.mode & 0xFFFF) << 16
92+
contents[zip_info] = b''.join(entry.get_blocks())
93+
94+
if entry_name in NODE_BINS:
95+
entry_points['node'] = 'nodejs.node:main'
96+
contents['nodejs/node.py'] = f'''\
97+
import os, sys, subprocess
98+
def run(args):
99+
return subprocess.call([
100+
os.path.join(os.path.dirname(__file__), "{entry_name}"),
101+
*args
102+
])
103+
def main():
104+
sys.exit(run(sys.argv[1:]))
105+
if __name__ == '__main__':
106+
main()
107+
'''.encode('ascii')
108+
contents['nodejs/__main__.py'] = f'''\
109+
from .node import main
110+
if __name__ == '__main__':
111+
main()
112+
'''.encode('ascii')
113+
elif entry_name in NODE_OTHER_BINS and NODE_OTHER_BINS[entry_name][1]:
114+
entry_points[NODE_OTHER_BINS[entry_name][0]] = f'nodejs.{NODE_OTHER_BINS[entry_name][0]}:main'
115+
script_name = '/'.join(os.path.normpath(os.path.join(os.path.dirname(entry.name), entry.linkpath)).split('/')[1:])
116+
contents[f'nodejs/{NODE_OTHER_BINS[entry_name][0]}.py'] = f'''\
117+
import os, sys
118+
from .node import run as run_node
119+
def run(args):
120+
return run_node([
121+
os.path.join(os.path.dirname(__file__), "{script_name}"),
122+
*args
123+
])
124+
def main():
125+
sys.exit(run(sys.argv[1:]))
126+
if __name__ == '__main__':
127+
main()
128+
'''.encode('ascii')
129+
elif entry_name in NODE_OTHER_BINS:
130+
entry_points[NODE_OTHER_BINS[entry_name][0]] = f'nodejs.{NODE_OTHER_BINS[entry_name][0]}:main'
131+
contents[f'nodejs/{NODE_OTHER_BINS[entry_name][0]}.py'] = f'''\
132+
import os, sys, subprocess
133+
def run(args):
134+
return subprocess.call([
135+
os.path.join(os.path.dirname(__file__), "{entry_name}"),
136+
*args
137+
])
138+
def main():
139+
sys.exit(run(sys.argv[1:]))
140+
if __name__ == '__main__':
141+
main()
142+
'''.encode('ascii')
143+
144+
with open('README.pypi.md') as f:
145+
description = f.read()
146+
147+
return write_wheel(out_dir,
148+
name='nodejs',
149+
version=version,
150+
tag=f'py3-none-{platform}',
151+
metadata={
152+
'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.',
153+
'Description-Content-Type': 'text/markdown',
154+
'License': 'MIT',
155+
'Classifier': [
156+
'License :: OSI Approved :: MIT License',
157+
],
158+
'Project-URL': [
159+
'Homepage, https://nodejs.org',
160+
],
161+
'Requires-Python': '~=3.5',
162+
},
163+
description=description,
164+
contents=contents,
165+
entry_points=entry_points,
166+
)
167+
168+
169+
def main():
170+
print('Making Node.js Wheels')
171+
node_version = '16.15.1'
172+
out_version = f'{node_version}a1'
173+
174+
for node_platform, python_platform in {
175+
'win-x86': 'win32',
176+
'win-x64': 'win_amd64',
177+
'darwin-x64': 'macosx_10_9_x86_64',
178+
'darwin-arm64': 'macosx_11_0_arm64',
179+
'linux-x64': 'manylinux_2_12_x86_64.manylinux2010_x86_64',
180+
'linux-armv7l': 'manylinux_2_17_armv7l.manylinux2014_armv7l',
181+
'linux-arm64': 'manylinux_2_17_aarch64.manylinux2014_aarch64',
182+
}.items():
183+
print(f'- Making Wheel for {node_platform}')
184+
node_url = f'https://nodejs.org/dist/v{node_version}/node-v{node_version}-{node_platform}.' + \
185+
('zip' if node_platform.startswith('win-') else 'tar.xz')
186+
with urllib.request.urlopen(node_url) as request:
187+
node_archive = request.read()
188+
print(f' {hashlib.sha256(node_archive).hexdigest()} {node_url}')
189+
190+
wheel_path = write_nodejs_wheel('dist/',
191+
version=out_version,
192+
platform=python_platform,
193+
archive=node_archive)
194+
with open(wheel_path, 'rb') as wheel:
195+
print(f' {hashlib.sha256(wheel.read()).hexdigest()} {wheel_path}')
196+
197+
198+
if __name__ == '__main__':
199+
main()

requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
wheel
2+
twine
3+
libarchive-c

0 commit comments

Comments
 (0)