Skip to content

Commit 0fd523f

Browse files
committed
node: Refactor rc file handling
- Use more "proper" package-manager-specific parsing, including what should be a fully-compliant (albiet over-engineered) parser for npm's ini format used in .npmrc. - Moves all config loading into its own set of classes. (As it turns out, this *does* walk recursively, contrary to what I said before, oops!) - Terminology change: rename rcfile -> config, just to match more with how npm describes itself. Signed-off-by: Ryan Gonzalez <ryan.gonzalez@collabora.com>
1 parent 33e4e62 commit 0fd523f

File tree

7 files changed

+364
-109
lines changed

7 files changed

+364
-109
lines changed

node/flatpak_node_generator/main.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from pathlib import Path
2-
from typing import Iterator, List, Set
2+
from typing import Dict, Iterator, List, Set
33

44
import argparse
55
import asyncio
@@ -12,7 +12,7 @@
1212
from .node_headers import NodeHeaders
1313
from .package import Package
1414
from .progress import GeneratorProgress
15-
from .providers import ProviderFactory
15+
from .providers import Config, ProviderFactory
1616
from .providers.npm import NpmLockfileProvider, NpmModuleProvider, NpmProviderFactory
1717
from .providers.special import SpecialSourceProvider
1818
from .providers.yarn import YarnProviderFactory
@@ -189,20 +189,20 @@ async def _async_main() -> None:
189189

190190
print('Reading packages from lockfiles...')
191191
packages: Set[Package] = set()
192-
rcfile_node_headers: Set[NodeHeaders] = set()
192+
config_node_headers: Set[NodeHeaders] = set()
193193

194-
for lockfile in lockfiles:
195-
lockfile_provider = provider_factory.create_lockfile_provider()
196-
rcfile_providers = provider_factory.create_rcfile_providers()
194+
lockfile_provider = provider_factory.create_lockfile_provider()
195+
config_provider = provider_factory.create_config_provider()
196+
197+
lockfile_configs: Dict[Path, Config] = {}
197198

199+
for lockfile in lockfiles:
198200
packages.update(lockfile_provider.process_lockfile(lockfile))
201+
lockfile_configs[lockfile] = config = config_provider.load_config(lockfile)
199202

200-
for rcfile_provider in rcfile_providers:
201-
rcfile = lockfile.parent / rcfile_provider.RCFILE_NAME
202-
if rcfile.is_file():
203-
nh = rcfile_provider.get_node_headers(rcfile)
204-
if nh is not None:
205-
rcfile_node_headers.add(nh)
203+
nh = config.get_node_headers()
204+
if nh is not None:
205+
config_node_headers.add(nh)
206206

207207
print(f'{len(packages)} packages read.')
208208

@@ -220,14 +220,18 @@ async def _async_main() -> None:
220220
)
221221
special = SpecialSourceProvider(gen, options)
222222

223-
with provider_factory.create_module_provider(gen, special) as module_provider:
223+
with provider_factory.create_module_provider(
224+
gen,
225+
special,
226+
lockfile_configs,
227+
) as module_provider:
224228
with GeneratorProgress(
225229
packages,
226230
module_provider,
227231
args.max_parallel,
228232
) as progress:
229233
await progress.run()
230-
for headers in rcfile_node_headers:
234+
for headers in config_node_headers:
231235
print(f'Generating headers {headers.runtime} @ {headers.target}')
232236
await special.generate_node_headers(headers)
233237

node/flatpak_node_generator/providers/__init__.py

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
from dataclasses import dataclass
12
from pathlib import Path
2-
from typing import ContextManager, Dict, Iterator, List, Optional
3+
from typing import Any, ContextManager, Dict, Iterator, List, Optional, Tuple
34

4-
import re
5+
import dataclasses
56
import urllib.parse
67

78
from ..manifest import ManifestGenerator
@@ -45,32 +46,48 @@ def process_lockfile(self, lockfile: Path) -> Iterator[Package]:
4546
raise NotImplementedError()
4647

4748

48-
class RCFileProvider:
49-
RCFILE_NAME: str
49+
@dataclass
50+
class Config:
51+
data: Dict[str, Any] = dataclasses.field(default_factory=lambda: {})
5052

51-
def parse_rcfile(self, rcfile: Path) -> Dict[str, str]:
52-
with open(rcfile, 'r') as r:
53-
rcfile_text = r.read()
54-
parser_re = re.compile(
55-
r'^(?!#|;)(\S+)(?:\s+|\s*=\s*)(?:"(.+)"|(\S+))$', re.MULTILINE
56-
)
57-
result: Dict[str, str] = {}
58-
for key, quoted_val, val in parser_re.findall(rcfile_text):
59-
result[key] = quoted_val or val
60-
return result
61-
62-
def get_node_headers(self, rcfile: Path) -> Optional[NodeHeaders]:
63-
rc_data = self.parse_rcfile(rcfile)
64-
if 'target' not in rc_data:
53+
def merge_new_keys_only(self, other: Dict[str, Any]) -> None:
54+
for key, value in other.items():
55+
if key not in self.data:
56+
self.data[key] = value
57+
58+
def get_node_headers(self) -> Optional[NodeHeaders]:
59+
if 'target' not in self.data:
6560
return None
66-
target = rc_data['target']
67-
runtime = rc_data.get('runtime')
68-
disturl = rc_data.get('disturl')
61+
target = self.data['target']
62+
runtime = self.data.get('runtime')
63+
disturl = self.data.get('disturl')
6964

7065
assert isinstance(runtime, str) and isinstance(disturl, str)
7166

7267
return NodeHeaders.with_defaults(target, runtime, disturl)
7368

69+
def get_registry_for_scope(self, scope: str) -> Optional[str]:
70+
return self.data.get(f'{scope}:registry')
71+
72+
73+
class ConfigProvider:
74+
@property
75+
def _filename(self) -> str:
76+
raise NotImplementedError()
77+
78+
def parse_config(self, path: Path) -> Dict[str, Any]:
79+
raise NotImplementedError()
80+
81+
def load_config(self, lockfile: Path) -> Config:
82+
config = Config()
83+
84+
for parent in lockfile.parents:
85+
path = parent / self._filename
86+
if path.exists():
87+
config.merge_new_keys_only(self.parse_config(path))
88+
89+
return config
90+
7491

7592
class ModuleProvider(ContextManager['ModuleProvider']):
7693
async def generate_package(self, package: Package) -> None:
@@ -81,10 +98,13 @@ class ProviderFactory:
8198
def create_lockfile_provider(self) -> LockfileProvider:
8299
raise NotImplementedError()
83100

84-
def create_rcfile_providers(self) -> List[RCFileProvider]:
101+
def create_config_provider(self) -> ConfigProvider:
85102
raise NotImplementedError()
86103

87104
def create_module_provider(
88-
self, gen: ManifestGenerator, special: SpecialSourceProvider
105+
self,
106+
gen: ManifestGenerator,
107+
special: SpecialSourceProvider,
108+
lockfile_configs: Dict[Path, Config],
89109
) -> ModuleProvider:
90110
raise NotImplementedError()

node/flatpak_node_generator/providers/npm.py

Lines changed: 121 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
)
3535
from ..requests import Requests
3636
from ..url_metadata import RemoteUrlMetadata
37-
from . import LockfileProvider, ModuleProvider, ProviderFactory, RCFileProvider
37+
from . import Config, ConfigProvider, LockfileProvider, ModuleProvider, ProviderFactory
3838
from .special import SpecialSourceProvider
3939

4040
_NPM_CORGIDOC = (
@@ -103,8 +103,103 @@ def process_lockfile(self, lockfile: Path) -> Iterator[Package]:
103103
yield from self.process_dependencies(lockfile, data.get('dependencies', {}))
104104

105105

106-
class NpmRCFileProvider(RCFileProvider):
107-
RCFILE_NAME = '.npmrc'
106+
class NpmConfigProvider(ConfigProvider):
107+
_COMMENT = ('#', ';')
108+
109+
@property
110+
def _filename(self) -> str:
111+
return '.npmrc'
112+
113+
def _parse_value_as_json(self, string: str) -> Any:
114+
try:
115+
return json.loads(string)
116+
except json.JSONDecodeError:
117+
return string
118+
119+
def _parse_value_literal(self, string: str) -> str:
120+
result = ''
121+
escaped = False
122+
for c in string:
123+
if escaped:
124+
if c not in self._COMMENT and c != '\\':
125+
result += '\\'
126+
result += c
127+
escaped = False
128+
elif c == '\\':
129+
escaped = True
130+
elif c in self._COMMENT:
131+
break
132+
else:
133+
result += c
134+
135+
if escaped:
136+
result += '\\'
137+
return result.strip()
138+
139+
def _parse_value(self, string: str) -> Any:
140+
SINGLE_QUOTE = "'"
141+
DOUBLE_QUOTE = '"'
142+
143+
string = string.strip()
144+
145+
if string.startswith(SINGLE_QUOTE) and string.endswith(SINGLE_QUOTE):
146+
return self._parse_value_as_json(string[1:-1])
147+
elif string.startswith(DOUBLE_QUOTE) and string.endswith(DOUBLE_QUOTE):
148+
return self._parse_value_as_json(string)
149+
else:
150+
return self._parse_value_literal(string)
151+
152+
def _coalesce_to_string(self, value: Any) -> Any:
153+
if isinstance(value, list):
154+
return ','.join(map(self._coalesce_to_string, value))
155+
elif isinstance(value, dict):
156+
return '[object Object]'
157+
else:
158+
return str(value)
159+
160+
def parse_config(self, path: Path) -> Dict[str, Any]:
161+
LITERALS = {
162+
'true': True,
163+
'false': False,
164+
'null': None,
165+
}
166+
167+
result: Dict[str, Any] = {}
168+
169+
with path.open() as fp:
170+
for line in fp:
171+
line = line.strip()
172+
if not line or line.startswith(self._COMMENT):
173+
continue
174+
175+
try:
176+
key_s, value_s = line.split('=', 1)
177+
except ValueError:
178+
key_s = line
179+
value_s = 'true'
180+
181+
key = self._coalesce_to_string(self._parse_value(key_s))
182+
is_array = False
183+
if key.endswith('[]'):
184+
is_array = True
185+
key = key[:-2]
186+
187+
value = self._parse_value(value_s)
188+
if isinstance(value, str):
189+
value = LITERALS.get(value, value)
190+
191+
if is_array and key not in result:
192+
result[key] = []
193+
elif is_array and not isinstance(result[key], list):
194+
result[key] = [result[key]]
195+
196+
previous_value = result.get(key)
197+
if isinstance(previous_value, list):
198+
previous_value.append(value)
199+
else:
200+
result[key] = value
201+
202+
return result
108203

109204

110205
class NpmModuleProvider(ModuleProvider):
@@ -124,13 +219,15 @@ def __init__(
124219
special: SpecialSourceProvider,
125220
lockfile_root: Path,
126221
options: Options,
222+
lockfile_configs: Dict[Path, Config],
127223
) -> None:
128224
self.gen = gen
129225
self.special_source_provider = special
130226
self.lockfile_root = lockfile_root
131227
self.registry = options.registry
132228
self.no_autopatch = options.no_autopatch
133229
self.no_trim_index = options.no_trim_index
230+
self.lockfile_configs = lockfile_configs
134231
self.npm_cache_dir = self.gen.data_root / 'npm-cache'
135232
self.cacache_dir = self.npm_cache_dir / '_cacache'
136233
# Awaitable so multiple tasks can be waiting on the same package info.
@@ -143,8 +240,6 @@ def __init__(
143240
self.git_sources: DefaultDict[
144241
Path, Dict[Path, GitSource]
145242
] = collections.defaultdict(lambda: {})
146-
# FIXME better pass the same provider object we created in main
147-
self.rcfile_provider = NpmRCFileProvider()
148243

149244
def __exit__(
150245
self,
@@ -324,21 +419,16 @@ async def generate_package(self, package: Package) -> None:
324419
def relative_lockfile_dir(self, lockfile: Path) -> Path:
325420
return lockfile.parent.relative_to(self.lockfile_root)
326421

327-
@functools.lru_cache(typed=True)
328-
def get_lockfile_rc(self, lockfile: Path) -> Dict[str, str]:
329-
rc = {}
330-
rcfile_path = lockfile.parent / self.rcfile_provider.RCFILE_NAME
331-
if rcfile_path.is_file():
332-
rc.update(self.rcfile_provider.parse_rcfile(rcfile_path))
333-
return rc
334-
335422
def get_package_registry(self, package: Package) -> str:
336423
assert isinstance(package.source, RegistrySource)
337-
rc = self.get_lockfile_rc(package.lockfile)
338-
if rc and '/' in package.name:
339-
scope, _ = package.name.split('/', maxsplit=1)
340-
if f'{scope}:registry' in rc:
341-
return rc[f'{scope}:registry']
424+
if '/' in package.name:
425+
config = self.lockfile_configs.get(package.lockfile)
426+
if config is not None:
427+
scope, _ = package.name.split('/', maxsplit=1)
428+
registry = config.get_registry_for_scope(scope)
429+
if registry is not None:
430+
return registry
431+
342432
return self.registry
343433

344434
def _finalize(self) -> None:
@@ -468,10 +558,19 @@ def __init__(self, lockfile_root: Path, options: Options) -> None:
468558
def create_lockfile_provider(self) -> NpmLockfileProvider:
469559
return NpmLockfileProvider(self.options.lockfile)
470560

471-
def create_rcfile_providers(self) -> List[RCFileProvider]:
472-
return [NpmRCFileProvider()]
561+
def create_config_provider(self) -> NpmConfigProvider:
562+
return NpmConfigProvider()
473563

474564
def create_module_provider(
475-
self, gen: ManifestGenerator, special: SpecialSourceProvider
565+
self,
566+
gen: ManifestGenerator,
567+
special: SpecialSourceProvider,
568+
lockfile_configs: Dict[Path, Config],
476569
) -> NpmModuleProvider:
477-
return NpmModuleProvider(gen, special, self.lockfile_root, self.options.module)
570+
return NpmModuleProvider(
571+
gen,
572+
special,
573+
self.lockfile_root,
574+
self.options.module,
575+
lockfile_configs,
576+
)

0 commit comments

Comments
 (0)