Skip to content

Commit 69dabf0

Browse files
committed
Add pnpm support
1 parent 5de4612 commit 69dabf0

7 files changed

Lines changed: 782 additions & 21 deletions

File tree

node/README.md

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# flatpak-node-generator
22

3-
A more modern successor for flatpak-npm-generator and flatpak-yarn-generator, for Node 10+ only.
3+
A more modern successor for flatpak-npm-generator and flatpak-yarn-generator, for Node 10+ only. Supports npm, yarn, and pnpm.
44
(For Node 8, use flatpak-npm-generator and flatpak-yarn-generator.)
55

66
**NOTE:** `--xdg-layout` was recently changed to be the default. In the stark
@@ -50,26 +50,34 @@ get npm with electron-builder.
5050
## Usage
5151

5252
```
53-
usage: flatpak-node-generator [-h] [-o OUTPUT] [-r] [-R RECURSIVE_PATTERN] [--registry REGISTRY] [--no-trim-index] [--no-devel] [--no-requests-cache] [--max-parallel MAX_PARALLEL] [--retries RETRIES] [-P]
54-
[-s] [-S SPLIT_SIZE] [--node-chromedriver-from-electron NODE_CHROMEDRIVER_FROM_ELECTRON] [--electron-ffmpeg {archive,lib}] [--electron-node-headers]
55-
[--nwjs-version NWJS_VERSION] [--nwjs-node-headers] [--nwjs-ffmpeg] [--no-xdg-layout] [--node-sdk-extension NODE_SDK_EXTENSION]
56-
{npm,yarn} lockfile
53+
usage: flatpak-node-generator [-h] [-o OUTPUT] [-r] [-R RECURSIVE_PATTERN] [--registry REGISTRY] [--no-trim-index]
54+
[--no-devel] [--no-requests-cache]
55+
[--max-parallel MAX_PARALLEL]
56+
[--retries RETRIES] [-P] [-s] [-S SPLIT_SIZE]
57+
[--node-chromedriver-from-electron NODE_CHROMEDRIVER_FROM_ELECTRON]
58+
[--electron-ffmpeg {archive,lib}]
59+
[--electron-node-headers]
60+
[--nwjs-version NWJS_VERSION]
61+
[--nwjs-node-headers] [--nwjs-ffmpeg]
62+
[--no-xdg-layout]
63+
[--node-sdk-extension NODE_SDK_EXTENSION]
64+
{npm,yarn,pnpm} lockfile
5765
5866
Flatpak Node generator
5967
6068
positional arguments:
61-
{npm,yarn}
62-
lockfile The lockfile path (package-lock.json or yarn.lock)
69+
{npm,yarn,pnpm}
70+
lockfile The lockfile path (package-lock.json, yarn.lock, or pnpm-lock.yaml)
6371
6472
options:
6573
-h, --help show this help message and exit
6674
-o, --output OUTPUT The output sources file
6775
-r, --recursive Recursively process all files under the lockfile directory with the lockfile basename
6876
-R, --recursive-pattern RECURSIVE_PATTERN
6977
Given -r, restrict files to those matching the given pattern.
70-
--registry REGISTRY The registry to use (npm only)
78+
--registry REGISTRY The registry to use (npm/pnpm)
7179
--no-trim-index Don't trim npm package metadata (npm only)
72-
--no-devel Don't include devel dependencies (npm only)
80+
--no-devel Don't include devel dependencies (npm/pnpm)
7381
--no-requests-cache Disable the requests cache
7482
--max-parallel MAX_PARALLEL
7583
Maximium number of packages to process in parallel
@@ -93,12 +101,12 @@ options:
93101
Flatpak node SDK extension (e.g. org.freedesktop.Sdk.Extension.node24//25.08)
94102
```
95103

96-
flatpak-node-generator.py takes the package manager (npm or yarn), and a path to a lockfile for
97-
that package manager. It will then write an output sources file (default is generated-sources.json)
98-
containing all the sources set up like needed for the given package manager.
104+
flatpak-node-generator takes a package manager (npm, yarn, or pnpm), and a path to a lockfile for
105+
that package manager. It writes an output sources file (default is generated-sources.json)
106+
containing all the sources needed for the given package manager.
99107

100-
If you're on npm and you don't want to include devel dependencies, pass `--no-devel`, and pass
101-
`--production` to `npm install` itself when you call.
108+
If you're on npm or pnpm and you don't want to include devel dependencies, pass `--no-devel`.
109+
For npm, also pass `--production` to `npm install` itself.
102110

103111
If you're using npm, you must run this script when the `node_modules` directory is **NOT** present.
104112
If you generate the `generated-sources.json` in CI, you can do this by passing `--package-lock-only`

node/flatpak_node_generator/main.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
from .progress import GeneratorProgress
1414
from .providers import ProviderFactory
1515
from .providers.npm import NpmLockfileProvider, NpmModuleProvider, NpmProviderFactory
16+
from .providers.pnpm import (
17+
PnpmLockfileProvider,
18+
PnpmProviderFactory,
19+
)
1620
from .providers.special import SpecialSourceProvider
1721
from .providers.yarn import YarnProviderFactory
1822
from .requests import Requests, StubRequests
@@ -28,9 +32,10 @@ def _scan_for_lockfiles(base: Path, patterns: list[str]) -> Iterator[Path]:
2832

2933
async def _async_main() -> None:
3034
parser = argparse.ArgumentParser(description='Flatpak Node generator')
31-
parser.add_argument('type', choices=['npm', 'yarn'])
35+
parser.add_argument('type', choices=['npm', 'yarn', 'pnpm'])
3236
parser.add_argument(
33-
'lockfile', help='The lockfile path (package-lock.json or yarn.lock)'
37+
'lockfile',
38+
help='The lockfile path (package-lock.json, yarn.lock, or pnpm-lock.yaml)',
3439
)
3540
parser.add_argument(
3641
'-o',
@@ -53,7 +58,7 @@ async def _async_main() -> None:
5358
)
5459
parser.add_argument(
5560
'--registry',
56-
help='The registry to use (npm only)',
61+
help='The registry to use (npm/pnpm)',
5762
default='https://registry.npmjs.org',
5863
)
5964
parser.add_argument(
@@ -64,7 +69,7 @@ async def _async_main() -> None:
6469
parser.add_argument(
6570
'--no-devel',
6671
action='store_true',
67-
help="Don't include devel dependencies (npm only)",
72+
help="Don't include devel dependencies (npm/pnpm)",
6873
)
6974
parser.add_argument(
7075
'--no-requests-cache',
@@ -157,6 +162,9 @@ async def _async_main() -> None:
157162
if args.type == 'yarn' and (args.no_devel or args.no_autopatch):
158163
sys.exit('--no-devel and --no-autopatch do not apply to Yarn.')
159164

165+
if args.type == 'pnpm' and args.no_autopatch:
166+
sys.exit('--no-autopatch does not apply to pnpm.')
167+
160168
if args.electron_chromedriver:
161169
print('WARNING: --electron-chromedriver is deprecated', file=sys.stderr)
162170
print(
@@ -196,6 +204,14 @@ async def _async_main() -> None:
196204
provider_factory = NpmProviderFactory(lockfile_root, npm_options)
197205
elif args.type == 'yarn':
198206
provider_factory = YarnProviderFactory()
207+
elif args.type == 'pnpm':
208+
pnpm_options = PnpmProviderFactory.Options(
209+
PnpmLockfileProvider.Options(
210+
no_devel=args.no_devel,
211+
registry=args.registry,
212+
),
213+
)
214+
provider_factory = PnpmProviderFactory(lockfile_root, pnpm_options)
199215
else:
200216
assert False, args.type
201217

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
from __future__ import annotations
2+
3+
import base64
4+
import hashlib
5+
import json
6+
import os
7+
import re
8+
import sys
9+
import tarfile
10+
import time
11+
12+
_SANITIZE_RE = re.compile(r'[\\/:*?"<>|]')
13+
14+
15+
def populate_store(manifest_path: str, tarball_dir: str, store_dir: str) -> None:
16+
with open(manifest_path, encoding='utf-8') as f:
17+
manifest = json.load(f)
18+
19+
store_version = manifest['store_version']
20+
packages = manifest['packages']
21+
22+
store = os.path.join(store_dir, store_version)
23+
os.makedirs(os.path.join(store, 'files'), exist_ok=True)
24+
os.makedirs(os.path.join(store, 'index'), exist_ok=True)
25+
26+
now = int(time.time() * 1000)
27+
28+
for tarball_name, info in packages.items():
29+
tarball_path = os.path.join(tarball_dir, tarball_name)
30+
if not os.path.isfile(tarball_path):
31+
raise FileNotFoundError(tarball_path)
32+
33+
_process_tarball(
34+
tarball_path=tarball_path,
35+
pkg_name=info['name'],
36+
pkg_version=info['version'],
37+
integrity_hex=info['integrity_hex'],
38+
store=store,
39+
now=now,
40+
)
41+
42+
43+
def _process_tarball(
44+
*,
45+
tarball_path: str,
46+
pkg_name: str,
47+
pkg_version: str,
48+
integrity_hex: str,
49+
store: str,
50+
now: int,
51+
) -> None:
52+
index_files: dict[str, dict[str, object]] = {}
53+
54+
with tarfile.open(tarball_path, 'r:gz') as tf:
55+
for member in tf.getmembers():
56+
if not member.isfile():
57+
continue
58+
fobj = tf.extractfile(member)
59+
if fobj is None:
60+
continue
61+
data = fobj.read()
62+
63+
digest = hashlib.sha512(data).digest()
64+
file_hex = digest.hex()
65+
is_exec = bool(member.mode & 0o111)
66+
67+
cas_dir = os.path.join(store, 'files', file_hex[:2])
68+
cas_name = file_hex[2:] + ('-exec' if is_exec else '')
69+
cas_path = os.path.join(cas_dir, cas_name)
70+
if not os.path.exists(cas_path):
71+
os.makedirs(cas_dir, exist_ok=True)
72+
with open(cas_path, 'wb') as out:
73+
out.write(data)
74+
if is_exec:
75+
os.chmod(cas_path, 0o755)
76+
77+
rel_name = member.name
78+
if '/' in rel_name:
79+
rel_name = rel_name.split('/', 1)[1]
80+
81+
b64 = base64.b64encode(digest).decode()
82+
index_files[rel_name] = {
83+
'checkedAt': now,
84+
'integrity': f'sha512-{b64}',
85+
'mode': member.mode,
86+
'size': len(data),
87+
}
88+
89+
idx_prefix = integrity_hex[:2]
90+
idx_rest = integrity_hex[2:64]
91+
pkg_id = _SANITIZE_RE.sub('+', f'{pkg_name}@{pkg_version}')
92+
idx_dir = os.path.join(store, 'index', idx_prefix)
93+
os.makedirs(idx_dir, exist_ok=True)
94+
idx_path = os.path.join(idx_dir, f'{idx_rest}-{pkg_id}.json')
95+
index_data = {
96+
'name': pkg_name,
97+
'version': pkg_version,
98+
'files': index_files,
99+
}
100+
with open(idx_path, 'w', encoding='utf-8') as out:
101+
json.dump(index_data, out)
102+
103+
104+
if __name__ == '__main__':
105+
if len(sys.argv) != 4:
106+
print(
107+
f'Usage: {sys.argv[0]} <manifest.json> <tarball-dir> <store-dir>',
108+
file=sys.stderr,
109+
)
110+
sys.exit(1)
111+
populate_store(sys.argv[1], sys.argv[2], sys.argv[3])

0 commit comments

Comments
 (0)