Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 22 additions & 14 deletions node/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# flatpak-node-generator

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

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

```
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]
[-s] [-S SPLIT_SIZE] [--node-chromedriver-from-electron NODE_CHROMEDRIVER_FROM_ELECTRON] [--electron-ffmpeg {archive,lib}] [--electron-node-headers]
[--nwjs-version NWJS_VERSION] [--nwjs-node-headers] [--nwjs-ffmpeg] [--no-xdg-layout] [--node-sdk-extension NODE_SDK_EXTENSION]
{npm,yarn} lockfile
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] [-s] [-S SPLIT_SIZE]
[--node-chromedriver-from-electron NODE_CHROMEDRIVER_FROM_ELECTRON]
[--electron-ffmpeg {archive,lib}]
[--electron-node-headers]
[--nwjs-version NWJS_VERSION]
[--nwjs-node-headers] [--nwjs-ffmpeg]
[--no-xdg-layout]
[--node-sdk-extension NODE_SDK_EXTENSION]
{npm,yarn,pnpm} lockfile
Flatpak Node generator
positional arguments:
{npm,yarn}
lockfile The lockfile path (package-lock.json or yarn.lock)
{npm,yarn,pnpm}
lockfile The lockfile path (package-lock.json, yarn.lock, or pnpm-lock.yaml)
options:
-h, --help show this help message and exit
-o, --output OUTPUT The output sources file
-r, --recursive Recursively process all files under the lockfile directory with the lockfile basename
-R, --recursive-pattern RECURSIVE_PATTERN
Given -r, restrict files to those matching the given pattern.
--registry REGISTRY The registry to use (npm only)
--registry REGISTRY The registry to use (npm/pnpm)
--no-trim-index Don't trim npm package metadata (npm only)
--no-devel Don't include devel dependencies (npm only)
--no-devel Don't include devel dependencies (npm/pnpm)
--no-requests-cache Disable the requests cache
--max-parallel MAX_PARALLEL
Maximium number of packages to process in parallel
Expand All @@ -93,12 +101,12 @@ options:
Flatpak node SDK extension (e.g. org.freedesktop.Sdk.Extension.node24//25.08)
```

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

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

If you're using npm, you must run this script when the `node_modules` directory is **NOT** present.
If you generate the `generated-sources.json` in CI, you can do this by passing `--package-lock-only`
Expand Down
24 changes: 20 additions & 4 deletions node/flatpak_node_generator/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
from .progress import GeneratorProgress
from .providers import ProviderFactory
from .providers.npm import NpmLockfileProvider, NpmModuleProvider, NpmProviderFactory
from .providers.pnpm import (
PnpmLockfileProvider,
PnpmProviderFactory,
)
from .providers.special import SpecialSourceProvider
from .providers.yarn import YarnProviderFactory
from .requests import Requests, StubRequests
Expand All @@ -28,9 +32,10 @@ def _scan_for_lockfiles(base: Path, patterns: list[str]) -> Iterator[Path]:

async def _async_main() -> None:
parser = argparse.ArgumentParser(description='Flatpak Node generator')
parser.add_argument('type', choices=['npm', 'yarn'])
parser.add_argument('type', choices=['npm', 'yarn', 'pnpm'])
parser.add_argument(
'lockfile', help='The lockfile path (package-lock.json or yarn.lock)'
'lockfile',
help='The lockfile path (package-lock.json, yarn.lock, or pnpm-lock.yaml)',
)
parser.add_argument(
'-o',
Expand All @@ -53,7 +58,7 @@ async def _async_main() -> None:
)
parser.add_argument(
'--registry',
help='The registry to use (npm only)',
help='The registry to use (npm/pnpm)',
default='https://registry.npmjs.org',
)
parser.add_argument(
Expand All @@ -64,7 +69,7 @@ async def _async_main() -> None:
parser.add_argument(
'--no-devel',
action='store_true',
help="Don't include devel dependencies (npm only)",
help="Don't include devel dependencies (npm/pnpm)",
)
parser.add_argument(
'--no-requests-cache',
Expand Down Expand Up @@ -157,6 +162,9 @@ async def _async_main() -> None:
if args.type == 'yarn' and (args.no_devel or args.no_autopatch):
sys.exit('--no-devel and --no-autopatch do not apply to Yarn.')

if args.type == 'pnpm' and args.no_autopatch:
sys.exit('--no-autopatch does not apply to pnpm.')

if args.electron_chromedriver:
print('WARNING: --electron-chromedriver is deprecated', file=sys.stderr)
print(
Expand Down Expand Up @@ -196,6 +204,14 @@ async def _async_main() -> None:
provider_factory = NpmProviderFactory(lockfile_root, npm_options)
elif args.type == 'yarn':
provider_factory = YarnProviderFactory()
elif args.type == 'pnpm':
pnpm_options = PnpmProviderFactory.Options(
PnpmLockfileProvider.Options(
no_devel=args.no_devel,
registry=args.registry,
),
)
provider_factory = PnpmProviderFactory(lockfile_root, pnpm_options)
else:
assert False, args.type

Expand Down
111 changes: 111 additions & 0 deletions node/flatpak_node_generator/populate_pnpm_store.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from __future__ import annotations

import base64
import hashlib
import json
import os
import re
import sys
import tarfile
import time

_SANITIZE_RE = re.compile(r'[\\/:*?"<>|]')


def populate_store(manifest_path: str, tarball_dir: str, store_dir: str) -> None:
with open(manifest_path, encoding='utf-8') as f:
manifest = json.load(f)

store_version = manifest['store_version']
packages = manifest['packages']

store = os.path.join(store_dir, store_version)
os.makedirs(os.path.join(store, 'files'), exist_ok=True)
os.makedirs(os.path.join(store, 'index'), exist_ok=True)

now = int(time.time() * 1000)

for tarball_name, info in packages.items():
tarball_path = os.path.join(tarball_dir, tarball_name)
if not os.path.isfile(tarball_path):
raise FileNotFoundError(tarball_path)

_process_tarball(
tarball_path=tarball_path,
pkg_name=info['name'],
pkg_version=info['version'],
integrity_hex=info['integrity_hex'],
store=store,
now=now,
)


def _process_tarball(
*,
tarball_path: str,
pkg_name: str,
pkg_version: str,
integrity_hex: str,
store: str,
now: int,
) -> None:
index_files: dict[str, dict[str, object]] = {}

with tarfile.open(tarball_path, 'r:gz') as tf:
for member in tf.getmembers():
if not member.isfile():
continue
fobj = tf.extractfile(member)
if fobj is None:
continue
data = fobj.read()

digest = hashlib.sha512(data).digest()
file_hex = digest.hex()
is_exec = bool(member.mode & 0o111)

cas_dir = os.path.join(store, 'files', file_hex[:2])
cas_name = file_hex[2:] + ('-exec' if is_exec else '')
cas_path = os.path.join(cas_dir, cas_name)
if not os.path.exists(cas_path):
os.makedirs(cas_dir, exist_ok=True)
with open(cas_path, 'wb') as out:
out.write(data)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs os.chmod(cas_path, member.mode) or something to preserve executable files.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did this:

if is_exec:
            os.chmod(cas_path, 0o755)

if is_exec:
os.chmod(cas_path, 0o755)

rel_name = member.name
if '/' in rel_name:
rel_name = rel_name.split('/', 1)[1]

b64 = base64.b64encode(digest).decode()
index_files[rel_name] = {
'checkedAt': now,
'integrity': f'sha512-{b64}',
'mode': member.mode,
'size': len(data),
}

idx_prefix = integrity_hex[:2]
idx_rest = integrity_hex[2:64]
pkg_id = _SANITIZE_RE.sub('+', f'{pkg_name}@{pkg_version}')
idx_dir = os.path.join(store, 'index', idx_prefix)
os.makedirs(idx_dir, exist_ok=True)
idx_path = os.path.join(idx_dir, f'{idx_rest}-{pkg_id}.json')
index_data = {
'name': pkg_name,
'version': pkg_version,
'files': index_files,
}
with open(idx_path, 'w', encoding='utf-8') as out:
json.dump(index_data, out)


if __name__ == '__main__':
if len(sys.argv) != 4:
print(
f'Usage: {sys.argv[0]} <manifest.json> <tarball-dir> <store-dir>',
file=sys.stderr,
)
sys.exit(1)
populate_store(sys.argv[1], sys.argv[2], sys.argv[3])
Loading
Loading