Skip to content

Commit 5063b12

Browse files
committed
Add support for pub lockfiles #2110
Signed-off-by: Philippe Ombredanne <[email protected]>
1 parent 8732def commit 5063b12

12 files changed

+2956
-32
lines changed

src/packagedcode/pubspec.py

Lines changed: 159 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import logging
1111
import sys
12+
import warnings
1213

1314
import attr
1415
import saneyaml
@@ -58,8 +59,8 @@ def logger_debug(*args):
5859

5960
@attr.s()
6061
class PubspecPackage(models.Package):
61-
metafiles = ('pubspec.yaml',)
62-
extensions = ('.yaml',)
62+
metafiles = ('pubspec.yaml', 'pubspec.lock',)
63+
extensions = ('.yaml', '.lock',)
6364
default_type = 'pubspec'
6465
default_primary_language = 'dart'
6566
default_web_baseurl = 'https://pub.dev/packages'
@@ -68,7 +69,10 @@ class PubspecPackage(models.Package):
6869

6970
@classmethod
7071
def recognize(cls, location):
71-
yield parse_pub(location)
72+
if is_pubspec_yaml(location):
73+
yield parse_pub(location)
74+
elif is_pubspec_lock(location):
75+
yield parse_lock(location)
7276

7377
def repository_homepage_url(self, baseurl=default_web_baseurl):
7478
return f'{baseurl}/{self.name}/versions/{self.version}'
@@ -116,7 +120,7 @@ def parse_pub(location, compute_normalized_license=False):
116120
Return a PubspecPackage constructed from the pubspec.yaml file at ``location``
117121
or None.
118122
"""
119-
if not is_pubspec(location):
123+
if not is_pubspec_yaml(location):
120124
return
121125
with open(location) as inp:
122126
package_data = saneyaml.load(inp.read())
@@ -127,44 +131,172 @@ def parse_pub(location, compute_normalized_license=False):
127131
return package
128132

129133

130-
def is_pubspec(location):
134+
def file_endswith(location, endswith):
131135
"""
132-
Check if the file is a yaml file or not
136+
Check if the file at ``location`` ends with ``endswith`` string or tuple.
133137
"""
134-
return filetype.is_file(location) and location.endswith('pubspec.yaml')
138+
return filetype.is_file(location) and location.endswith(endswith)
135139

136140

137-
def collect_deps(data, dependency_field_name, is_runtime=True, is_optional=False):
141+
def is_pubspec_yaml(location):
142+
return file_endswith(location, 'pubspec.yaml')
143+
144+
145+
def is_pubspec_lock(location):
146+
return file_endswith(location, 'pubspec.lock')
147+
148+
149+
def parse_lock(location):
138150
"""
139-
Yield DependentPackage found in the ``dependency_field_name`` of ``data``
151+
Yield PubspecPackages dependencies constructed from the pubspec.lock file at
152+
``location``.
153+
"""
154+
if not is_pubspec_lock(location):
155+
return
156+
157+
with open(location) as inp:
158+
locks_data = saneyaml.load(inp.read())
159+
160+
return PubspecPackage(dependencies=list(collect_locks(locks_data)))
161+
162+
163+
def collect_locks(locks_data):
164+
"""
165+
Yield DependentPackage from locks data
166+
167+
The general form is
168+
packages:
169+
_fe_analyzer_shared:
170+
dependency: transitive
171+
description:
172+
name: _fe_analyzer_shared
173+
url: "https://pub.dartlang.org"
174+
source: hosted
175+
version: "22.0.0"
176+
sdks:
177+
dart: ">=2.12.0 <3.0.0"
178+
"""
179+
# FIXME: we treat all as nno optioanl for now
180+
sdks = locks_data.get('sdks') or {}
181+
for name, version in sdks.items():
182+
dep = build_dep(
183+
name,
184+
version,
185+
scope='sdk',
186+
is_runtime=True,
187+
is_optional=False,
188+
)
189+
yield dep
190+
191+
packages = locks_data.get('packages') or {}
192+
for name, details in packages.items():
193+
version = details.get('version')
194+
195+
# FIXME: see https://github.com/dart-lang/pub/blob/2a08832e0b997ff92de65571b6d79a9b9099faa0/lib/src/lock_file.dart#L344
196+
# transitive, direct main, direct dev, direct overridden.
197+
# they do not map exactly to the pubspec scopes since transitive can be
198+
# either main or dev
199+
scope = details.get('dependency')
200+
if scope == 'direct dev':
201+
is_runtime = False
202+
else:
203+
is_runtime = True
204+
205+
desc = details.get('description') or {}
206+
known_desc = isinstance(desc, dict)
207+
208+
# issue a warning for unknown data structure
209+
warn = False
210+
if not known_desc:
211+
if not (isinstance(desc, str) and desc == 'flutter'):
212+
warn = True
213+
else:
214+
dname = desc.get('name')
215+
durl = desc.get('url')
216+
dsource = details.get('source')
217+
218+
if (
219+
(dname and dname != name)
220+
or (durl and durl != 'https://pub.dartlang.org')
221+
or (dsource and dsource not in ['hosted', 'sdk', ])
222+
):
223+
warn = True
224+
225+
if warn:
226+
warnings.warn(
227+
f'Dart pubspec.locks with unsupported external repo '
228+
f'description or source: {details}',
229+
stacklevel=1,
230+
)
231+
232+
dep = build_dep(
233+
name,
234+
version,
235+
scope=scope,
236+
is_runtime=is_runtime,
237+
is_optional=False,
238+
)
239+
yield dep
240+
241+
242+
def collect_deps(data, dependency_field_name, is_runtime=True, is_optional=False):
140243
"""
244+
Yield DependentPackage found in the ``dependency_field_name`` of ``data``.
245+
Use is_runtime and is_optional in created DependentPackage.
141246
247+
The shape of the data is:
248+
dependencies:
249+
path: 1.7.0
250+
meta: ^1.2.4
251+
yaml: ^3.1.0
252+
253+
environment:
254+
sdk: '>=2.12.0 <3.0.0'
255+
"""
142256
# TODO: these can be more complex for SDKs
143257
# https://dart.dev/tools/pub/dependencies#dependency-sources
144-
145258
dependencies = data.get(dependency_field_name) or {}
146259
for name, version in dependencies.items():
147-
if isinstance(version, dict) and 'sdk' in version:
148-
# {'sdk': 'flutter'} type of deps....
149-
# which is a wart that we keep as a requiremnet
150-
version = ', '.join(': '.join([k, str(v)]) for k, v in version.items())
151-
152-
if version.replace('.', '').isdigit():
153-
# version is pinned exactly if it is only made of dots and digits
154-
purl = PackageURL(type='pubspec', name=name, version=version)
155-
is_resolved = True
156-
else:
157-
purl = PackageURL(type='pubspec', name=name)
158-
is_resolved = False
159-
160-
yield models.DependentPackage(
161-
purl=purl.to_string(),
162-
requirement=version,
260+
dep = build_dep(
261+
name,
262+
version,
163263
scope=dependency_field_name,
164264
is_runtime=is_runtime,
165265
is_optional=is_optional,
166-
is_resolved=is_resolved,
167266
)
267+
yield dep
268+
269+
270+
def build_dep(name, version, scope, is_runtime=True, is_optional=False):
271+
"""
272+
Return DependentPackage from the provided data.
273+
"""
274+
275+
# TODO: these can be more complex for SDKs
276+
# https://dart.dev/tools/pub/dependencies#dependency-sources
277+
278+
if isinstance(version, dict) and 'sdk' in version:
279+
# {'sdk': 'flutter'} type of deps....
280+
# which is a wart that we keep as a requiremnet
281+
version = ', '.join(': '.join([k, str(v)]) for k, v in version.items())
282+
283+
if version.replace('.', '').isdigit():
284+
# version is pinned exactly if it is only made of dots and digits
285+
purl = PackageURL(type='pubspec', name=name, version=version)
286+
is_resolved = True
287+
else:
288+
purl = PackageURL(type='pubspec', name=name)
289+
is_resolved = False
290+
291+
dep = models.DependentPackage(
292+
purl=purl.to_string(),
293+
requirement=version,
294+
scope=scope,
295+
is_runtime=is_runtime,
296+
is_optional=is_optional,
297+
is_resolved=is_resolved,
298+
)
299+
return dep
168300

169301

170302
def build_package(pubspec_data):

0 commit comments

Comments
 (0)